diff options
Diffstat (limited to 'packages/SystemUI/src')
152 files changed, 6910 insertions, 5344 deletions
diff --git a/packages/SystemUI/src/com/android/keyguard/DisabledUdfpsController.java b/packages/SystemUI/src/com/android/keyguard/DisabledUdfpsController.java index 118f98da11e2..15312ad9dfd1 100644 --- a/packages/SystemUI/src/com/android/keyguard/DisabledUdfpsController.java +++ b/packages/SystemUI/src/com/android/keyguard/DisabledUdfpsController.java @@ -18,27 +18,32 @@ package com.android.keyguard; import static android.hardware.biometrics.BiometricSourceType.FINGERPRINT; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.InsetDrawable; import android.hardware.biometrics.BiometricSourceType; +import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; +import com.android.settingslib.Utils; import com.android.systemui.Dumpable; +import com.android.systemui.R; import com.android.systemui.biometrics.AuthController; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.statusbar.StatusBarState; +import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.util.ViewController; import java.io.FileDescriptor; import java.io.PrintWriter; /** - * Controls when to show the DisabledUdfpsView to unlock the device on the lockscreen. - * If the device is not authenticated, the bouncer will show. + * Controls when to show the DisabledUdfpsView affordance (unlock icon or circle) on lock screen. * - * This tap target will only show when: + * This view only exists when: * - User has UDFPS enrolled * - UDFPS is currently unavailable see {@link KeyguardUpdateMonitor#shouldListenForUdfps} */ @@ -47,48 +52,66 @@ public class DisabledUdfpsController extends ViewController<DisabledUdfpsView> i @NonNull private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; @NonNull private final KeyguardViewController mKeyguardViewController; @NonNull private final StatusBarStateController mStatusBarStateController; + @NonNull private final KeyguardStateController mKeyguardStateController; + @NonNull private final Drawable mButton; + @NonNull private final Drawable mUnlockIcon; private boolean mIsDozing; private boolean mIsBouncerShowing; private boolean mIsKeyguardShowing; private boolean mRunningFPS; - private boolean mAuthenticated; + private boolean mCanDismissLockScreen; private boolean mShowButton; + private boolean mShowUnlockIcon; public DisabledUdfpsController( @NonNull DisabledUdfpsView view, @NonNull StatusBarStateController statusBarStateController, @NonNull KeyguardUpdateMonitor keyguardUpdateMonitor, @NonNull AuthController authController, - @NonNull KeyguardViewController keyguardViewController + @NonNull KeyguardViewController keyguardViewController, + @NonNull KeyguardStateController keyguardStateController ) { super(view); - mView.setOnClickListener(mOnClickListener); + mView.setOnTouchListener(mOnTouchListener); mView.setSensorProperties(authController.getUdfpsProps().get(0)); mStatusBarStateController = statusBarStateController; mKeyguardUpdateMonitor = keyguardUpdateMonitor; mKeyguardViewController = keyguardViewController; + mKeyguardStateController = keyguardStateController; + + 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( + com.android.systemui.R.dimen.udfps_unlock_icon_inset)); } @Override protected void onViewAttached() { mIsBouncerShowing = mKeyguardViewController.isBouncerShowing(); - mIsKeyguardShowing = mStatusBarStateController.getState() == StatusBarState.KEYGUARD; + mIsKeyguardShowing = mKeyguardStateController.isShowing(); mIsDozing = mStatusBarStateController.isDozing(); mRunningFPS = mKeyguardUpdateMonitor.isFingerprintDetectionRunning(); - mAuthenticated = false; - updateButtonVisibility(); + mCanDismissLockScreen = mKeyguardStateController.canDismissLockScreen(); + mUnlockIcon.setTint(Utils.getColorAttrDefaultColor(mView.getContext(), + R.attr.wallpaperTextColorAccent)); + updateVisibility(); mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback); mStatusBarStateController.addCallback(mStatusBarStateListener); + mKeyguardStateController.addCallback(mKeyguardStateCallback); } @Override protected void onViewDetached() { mKeyguardUpdateMonitor.removeCallback(mKeyguardUpdateMonitorCallback); mStatusBarStateController.removeCallback(mStatusBarStateListener); + mKeyguardStateController.removeCallback(mKeyguardStateCallback); } /** @@ -100,46 +123,51 @@ public class DisabledUdfpsController extends ViewController<DisabledUdfpsView> i } } - private void updateButtonVisibility() { - mShowButton = !mAuthenticated && !mIsDozing && mIsKeyguardShowing - && !mIsBouncerShowing && !mRunningFPS; + private void updateVisibility() { + mShowButton = !mCanDismissLockScreen && !mRunningFPS && isLockScreen(); + mShowUnlockIcon = mCanDismissLockScreen && isLockScreen(); + if (mShowButton) { + mView.setImageDrawable(mButton); + mView.setVisibility(View.VISIBLE); + } else if (mShowUnlockIcon) { + mView.setImageDrawable(mUnlockIcon); mView.setVisibility(View.VISIBLE); } else { mView.setVisibility(View.INVISIBLE); } } + private boolean isLockScreen() { + return mIsKeyguardShowing && !mIsDozing && !mIsBouncerShowing; + } + @Override public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) { pw.println("DisabledUdfpsController state:"); pw.println(" mShowBouncerButton: " + mShowButton); + pw.println(" mShowUnlockIcon: " + mShowUnlockIcon); pw.println(" mIsDozing: " + mIsDozing); pw.println(" mIsKeyguardShowing: " + mIsKeyguardShowing); pw.println(" mIsBouncerShowing: " + mIsBouncerShowing); pw.println(" mRunningFPS: " + mRunningFPS); - pw.println(" mAuthenticated: " + mAuthenticated); + pw.println(" mCanDismissLockScreen: " + mCanDismissLockScreen); } - private final View.OnClickListener mOnClickListener = new View.OnClickListener() { + private final View.OnTouchListener mOnTouchListener = new View.OnTouchListener() { @Override - public void onClick(View v) { + public boolean onTouch(View v, MotionEvent event) { mKeyguardViewController.showBouncer(/* scrim */ true); + return true; } }; private StatusBarStateController.StateListener mStatusBarStateListener = new StatusBarStateController.StateListener() { @Override - public void onStateChanged(int newState) { - mIsKeyguardShowing = newState == StatusBarState.KEYGUARD; - updateButtonVisibility(); - } - - @Override public void onDozingChanged(boolean isDozing) { mIsDozing = isDozing; - updateButtonVisibility(); + updateVisibility(); } }; @@ -148,7 +176,7 @@ public class DisabledUdfpsController extends ViewController<DisabledUdfpsView> i @Override public void onKeyguardBouncerChanged(boolean bouncer) { mIsBouncerShowing = bouncer; - updateButtonVisibility(); + updateVisibility(); } @Override @@ -157,21 +185,29 @@ public class DisabledUdfpsController extends ViewController<DisabledUdfpsView> i if (biometricSourceType == FINGERPRINT) { mRunningFPS = running; } - mAuthenticated &= !mRunningFPS; - updateButtonVisibility(); - } - @Override - public void onBiometricAuthenticated(int userId, - BiometricSourceType biometricSourceType, boolean isStrongBiometric) { - mAuthenticated = true; - updateButtonVisibility(); - } - - @Override - public void onUserUnlocked() { - mAuthenticated = true; - updateButtonVisibility(); + updateVisibility(); } }; + + private final KeyguardStateController.Callback mKeyguardStateCallback = + new KeyguardStateController.Callback() { + @Override + public void onKeyguardShowingChanged() { + updateIsKeyguardShowing(); + updateVisibility(); + } + + @Override + public void onUnlockedChanged() { + updateIsKeyguardShowing(); + mCanDismissLockScreen = mKeyguardStateController.canDismissLockScreen(); + updateVisibility(); + } + + private void updateIsKeyguardShowing() { + mIsKeyguardShowing = mKeyguardStateController.isShowing() + && !mKeyguardStateController.isKeyguardGoingAway(); + } + }; } diff --git a/packages/SystemUI/src/com/android/keyguard/DisabledUdfpsView.java b/packages/SystemUI/src/com/android/keyguard/DisabledUdfpsView.java index d8ab780eb6bd..8ae753e7f3f7 100644 --- a/packages/SystemUI/src/com/android/keyguard/DisabledUdfpsView.java +++ b/packages/SystemUI/src/com/android/keyguard/DisabledUdfpsView.java @@ -22,14 +22,13 @@ import android.graphics.RectF; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.util.AttributeSet; import android.view.Surface; -import android.widget.Button; import android.widget.FrameLayout; +import android.widget.ImageView; /** - * A full screen view with an oval target where the UDFPS sensor is. - * Controlled by {@link DisabledUdfpsController}. + * A view positioned in the area of the UDPFS sensor. */ -public class DisabledUdfpsView extends Button { +public class DisabledUdfpsView extends ImageView { @NonNull private final RectF mSensorRect; @NonNull private final Context mContext; diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java index 02a8958ef657..31f1332b265c 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java @@ -492,4 +492,11 @@ public class KeyguardHostViewController extends ViewController<KeyguardHostView> mKeyguardSecurityContainerController.updateResources(); } } + + /** Update keyguard position based on a tapped X coordinate. */ + public void updateKeyguardPosition(float x) { + if (mKeyguardSecurityContainerController != null) { + mKeyguardSecurityContainerController.updateKeyguardPosition(x); + } + } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java index 708b2d55b75a..7ed63375a334 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java @@ -267,6 +267,13 @@ public class KeyguardSecurityContainer extends FrameLayout { updateSecurityViewLocation(false); } + /** Update keyguard position based on a tapped X coordinate. */ + public void updateKeyguardPosition(float x) { + if (mOneHandedMode) { + moveBouncerForXCoordinate(x, /* animate= */false); + } + } + /** Return whether the one-handed keyguard should be enabled. */ private boolean canUseOneHandedBouncer() { // Is it enabled? @@ -488,9 +495,13 @@ public class KeyguardSecurityContainer extends FrameLayout { return; } + moveBouncerForXCoordinate(event.getX(), /* animate= */true); + } + + private void moveBouncerForXCoordinate(float x, boolean animate) { // Did the tap hit the "other" side of the bouncer? - if ((mIsSecurityViewLeftAligned && (event.getX() > getWidth() / 2f)) - || (!mIsSecurityViewLeftAligned && (event.getX() < getWidth() / 2f))) { + if ((mIsSecurityViewLeftAligned && (x > getWidth() / 2f)) + || (!mIsSecurityViewLeftAligned && (x < getWidth() / 2f))) { mIsSecurityViewLeftAligned = !mIsSecurityViewLeftAligned; Settings.Global.putInt( @@ -499,7 +510,7 @@ public class KeyguardSecurityContainer extends FrameLayout { mIsSecurityViewLeftAligned ? Settings.Global.ONE_HANDED_KEYGUARD_SIDE_LEFT : Settings.Global.ONE_HANDED_KEYGUARD_SIDE_RIGHT); - updateSecurityViewLocation(true); + updateSecurityViewLocation(animate); } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java index 760eaecae247..4827cab3b5c0 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java @@ -515,6 +515,11 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard } } + /** Update keyguard position based on a tapped X coordinate. */ + public void updateKeyguardPosition(float x) { + mView.updateKeyguardPosition(x); + } + static class Factory { private final KeyguardSecurityContainer mView; diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index 825affef9dbc..36109d691808 100755 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -428,6 +428,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab public void onTrustChanged(boolean enabled, int userId, int flags) { Assert.isMainThread(); mUserHasTrust.put(userId, enabled); + updateBiometricListeningState(); for (int i = 0; i < mCallbacks.size(); i++) { KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); if (cb != null) { @@ -628,6 +629,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } // Don't send cancel if authentication succeeds mFingerprintCancelSignal = null; + updateBiometricListeningState(); for (int i = 0; i < mCallbacks.size(); i++) { KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); if (cb != null) { @@ -811,6 +813,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } // Don't send cancel if authentication succeeds mFaceCancelSignal = null; + updateBiometricListeningState(); for (int i = 0; i < mCallbacks.size(); i++) { KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); if (cb != null) { @@ -1775,6 +1778,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab break; case MSG_TIME_FORMAT_UPDATE: handleTimeFormatUpdate((String) msg.obj); + break; case MSG_REQUIRE_NFC_UNLOCK: handleRequireUnlockForNfc(); break; @@ -2110,6 +2114,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab boolean shouldListenForUdfps() { return shouldListenForFingerprint() && !mBouncer + && !getUserCanSkipBouncer(getCurrentUser()) + && !isEncryptedOrLockdown(getCurrentUser()) && mStrongAuthTracker.hasUserAuthenticatedSinceBoot(); } diff --git a/packages/SystemUI/src/com/android/systemui/CornerHandleView.java b/packages/SystemUI/src/com/android/systemui/CornerHandleView.java deleted file mode 100644 index e52245b83e19..000000000000 --- a/packages/SystemUI/src/com/android/systemui/CornerHandleView.java +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Copyright (C) 2019 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; - -import android.animation.ArgbEvaluator; -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Path; -import android.graphics.RectF; -import android.util.AttributeSet; -import android.util.DisplayMetrics; -import android.view.ContextThemeWrapper; -import android.view.View; -import android.os.SystemProperties; - -import com.android.settingslib.Utils; - -/** - * CornerHandleView draws an inset arc intended to be displayed within the screen decoration - * corners. - */ -public class CornerHandleView extends View { - private static final float STROKE_DP_LARGE = 2f; - private static final float STROKE_DP_SMALL = 1.95f; - // Radius to use if none is available. - private static final int FALLBACK_RADIUS_DP = 15; - private static final float MARGIN_DP = 8; - private static final int MAX_ARC_DEGREES = 90; - // Arc length along the phone's perimeter used to measure the desired angle. - private static final float ARC_LENGTH_DP = 31f; - private static int mDisableRoundedCorner = - SystemProperties.getInt("vendor.display.disable_rounded_corner", 0); - - private Paint mPaint; - private int mLightColor; - private int mDarkColor; - private Path mPath; - private boolean mRequiresInvalidate; - - public CornerHandleView(Context context, AttributeSet attrs) { - super(context, attrs); - - mPaint = new Paint(); - mPaint.setAntiAlias(true); - mPaint.setStyle(Paint.Style.STROKE); - mPaint.setStrokeCap(Paint.Cap.ROUND); - mPaint.setStrokeWidth(getStrokePx()); - - final int dualToneDarkTheme = Utils.getThemeAttr(mContext, R.attr.darkIconTheme); - final int dualToneLightTheme = Utils.getThemeAttr(mContext, R.attr.lightIconTheme); - Context lightContext = new ContextThemeWrapper(mContext, dualToneLightTheme); - Context darkContext = new ContextThemeWrapper(mContext, dualToneDarkTheme); - mLightColor = Utils.getColorAttrDefaultColor(lightContext, R.attr.singleToneColor); - mDarkColor = Utils.getColorAttrDefaultColor(darkContext, R.attr.singleToneColor); - - updatePath(); - } - - @Override - public void setAlpha(float alpha) { - super.setAlpha(alpha); - if (alpha > 0f && mRequiresInvalidate) { - mRequiresInvalidate = false; - invalidate(); - } - } - - private void updatePath() { - mPath = new Path(); - - float marginPx = getMarginPx(); - float radiusPx = getInnerRadiusPx(); - float halfStrokePx = getStrokePx() / 2f; - float angle = getAngle(); - float startAngle = 180 + ((90 - angle) / 2); - RectF circle = new RectF(marginPx + halfStrokePx, - marginPx + halfStrokePx, - marginPx + 2 * radiusPx - halfStrokePx, - marginPx + 2 * radiusPx - halfStrokePx); - - if (angle >= 90f) { - float innerCircumferenceDp = convertPixelToDp(radiusPx * 2 * (float) Math.PI, - mContext); - float arcDp = innerCircumferenceDp * getAngle() / 360f; - // Add additional "arms" to the two ends of the arc. The length computation is - // hand-tuned. - float lineLengthPx = convertDpToPixel((ARC_LENGTH_DP - arcDp - MARGIN_DP) / 2, - mContext); - - mPath.moveTo(marginPx + halfStrokePx, marginPx + radiusPx + lineLengthPx); - mPath.lineTo(marginPx + halfStrokePx, marginPx + radiusPx); - mPath.arcTo(circle, startAngle, angle); - mPath.moveTo(marginPx + radiusPx, marginPx + halfStrokePx); - mPath.lineTo(marginPx + radiusPx + lineLengthPx, marginPx + halfStrokePx); - } else { - mPath.arcTo(circle, startAngle, angle); - } - } - - /** - * Receives an intensity from 0 (lightest) to 1 (darkest) and sets the handle color - * appropriately. Intention is to match the home handle color. - */ - public void updateDarkness(float darkIntensity) { - // Handle color is same as home handle color. - int color = (int) ArgbEvaluator.getInstance().evaluate(darkIntensity, - mLightColor, mDarkColor); - if (mPaint.getColor() != color) { - mPaint.setColor(color); - if (getVisibility() == VISIBLE && getAlpha() > 0) { - invalidate(); - } else { - // If we are currently invisible, then invalidate when we are next made visible - mRequiresInvalidate = true; - } - } - } - - @Override - public void onDraw(Canvas canvas) { - super.onDraw(canvas); - canvas.drawPath(mPath, mPaint); - } - - private static float convertDpToPixel(float dp, Context context) { - return dp * ((float) context.getResources().getDisplayMetrics().densityDpi - / DisplayMetrics.DENSITY_DEFAULT); - } - - private static float convertPixelToDp(float px, Context context) { - return px * DisplayMetrics.DENSITY_DEFAULT - / ((float) context.getResources().getDisplayMetrics().densityDpi); - } - - private float getAngle() { - // Measure a length of ARC_LENGTH_DP along the *screen's* perimeter, get the angle and cap - // it at 90. - float circumferenceDp = convertPixelToDp(( - getOuterRadiusPx()) * 2 * (float) Math.PI, mContext); - float angleDeg = (ARC_LENGTH_DP / circumferenceDp) * 360; - if (angleDeg > MAX_ARC_DEGREES) { - angleDeg = MAX_ARC_DEGREES; - } - return angleDeg; - } - - private float getMarginPx() { - return convertDpToPixel(MARGIN_DP, mContext); - } - - private float getInnerRadiusPx() { - return getOuterRadiusPx() - getMarginPx(); - } - - private float getOuterRadiusPx() { - // Attempt to get the bottom corner radius, otherwise fall back on the generic or top - // values. If none are available, use the FALLBACK_RADIUS_DP. - int radius = getResources().getDimensionPixelSize( - com.android.systemui.R.dimen.config_rounded_mask_size_bottom); - if (radius == 0 && mDisableRoundedCorner == 0) { - radius = getResources().getDimensionPixelSize( - com.android.systemui.R.dimen.config_rounded_mask_size); - } - if (radius == 0) { - radius = getResources().getDimensionPixelSize( - com.android.systemui.R.dimen.config_rounded_mask_size_top); - } - if (radius == 0) { - radius = (int) convertDpToPixel(FALLBACK_RADIUS_DP, mContext); - } - return radius; - } - - private float getStrokePx() { - // Use a slightly smaller stroke if we need to cover the full corner angle. - return convertDpToPixel((getAngle() < 90) ? STROKE_DP_LARGE : STROKE_DP_SMALL, - getContext()); - } -} 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 ab05c2a273ad..57be4e8477b2 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java @@ -31,6 +31,7 @@ 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; @@ -39,6 +40,8 @@ import android.view.MotionEvent; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.WindowManager; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.view.animation.OvershootInterpolator; import android.widget.FrameLayout; @@ -284,6 +287,44 @@ 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) { + if (super.performAccessibilityAction(action, arguments)) { + return true; + } + + fadeIn(); + + final Rect bounds = getAvailableBounds(); + if (action == R.id.action_move_top_left) { + snapToLocation(bounds.left, bounds.top); + return true; + } + + if (action == R.id.action_move_top_right) { + snapToLocation(bounds.right, bounds.top); + return true; + } + + if (action == R.id.action_move_bottom_left) { + snapToLocation(bounds.left, bounds.bottom); + return true; + } + + if (action == R.id.action_move_bottom_right) { + snapToLocation(bounds.right, bounds.bottom); + return true; + } + + return false; + } + void show() { if (isShowing()) { return; @@ -380,6 +421,33 @@ 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); + } + private boolean onTouched(MotionEvent event) { final int action = event.getAction(); final int currentX = (int) event.getX(); @@ -524,7 +592,8 @@ public class AccessibilityFloatingMenuView extends FrameLayout updateLocationWith(mAlignment, mPercentageY); } - private void snapToLocation(int endX, int endY) { + @VisibleForTesting + void snapToLocation(int endX, int endY) { mDragAnimator.cancel(); mDragAnimator.removeAllUpdateListeners(); mDragAnimator.addUpdateListener(anim -> onDragAnimationUpdate(anim, endX, endY)); @@ -662,6 +731,11 @@ public class AccessibilityFloatingMenuView extends FrameLayout : R.dimen.accessibility_floating_menu_large_single_radius; } + @VisibleForTesting + Rect getAvailableBounds() { + return new Rect(0, 0, mScreenWidth - getWindowWidth(), mScreenHeight - getWindowHeight()); + } + private int getLayoutWidth() { return mPadding * 2 + mIconWidth; } diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistHandleBehaviorController.java b/packages/SystemUI/src/com/android/systemui/assist/AssistHandleBehaviorController.java deleted file mode 100644 index 4390d513a9fb..000000000000 --- a/packages/SystemUI/src/com/android/systemui/assist/AssistHandleBehaviorController.java +++ /dev/null @@ -1,336 +0,0 @@ -/* - * Copyright (C) 2019 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.assist; - -import static com.android.systemui.assist.AssistModule.ASSIST_HANDLE_THREAD_NAME; - -import android.content.ComponentName; -import android.content.Context; -import android.os.Handler; -import android.os.SystemClock; -import android.provider.Settings; -import android.util.Log; -import android.view.accessibility.AccessibilityManager; - -import androidx.annotation.Nullable; - -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.app.AssistUtils; -import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; -import com.android.keyguard.KeyguardUpdateMonitor; -import com.android.systemui.Dumpable; -import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.dump.DumpManager; -import com.android.systemui.navigationbar.NavigationModeController; -import com.android.systemui.shared.system.QuickStepContract; - -import java.io.FileDescriptor; -import java.io.PrintWriter; -import java.util.Map; -import java.util.concurrent.TimeUnit; - -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Provider; - -import dagger.Lazy; - -/** - * A class for managing Assistant handle logic. - * - * Controls when visual handles for Assistant gesture affordance should be shown or hidden using an - * {@link AssistHandleBehavior}. - */ -@SysUISingleton -public final class AssistHandleBehaviorController implements AssistHandleCallbacks, Dumpable { - - private static final String TAG = "AssistHandleBehavior"; - - private static final long DEFAULT_SHOWN_FREQUENCY_THRESHOLD_MS = 0; - private static final long DEFAULT_SHOW_AND_GO_DURATION_MS = TimeUnit.SECONDS.toMillis(3); - private static final String SETTINGS_SECURE_USER_SETUP_COMPLETE = "user_setup_complete"; - - /** - * This is the default behavior that will be used once the system is up. It will be set once the - * behavior dependencies are available. This ensures proper behavior lifecycle. - */ - private static final AssistHandleBehavior DEFAULT_BEHAVIOR = AssistHandleBehavior.REMINDER_EXP; - - private final Context mContext; - private final AssistUtils mAssistUtils; - private final Handler mHandler; - private final Runnable mHideHandles = this::hideHandles; - private final Runnable mShowAndGo = this::showAndGoInternal; - private final Provider<AssistHandleViewController> mAssistHandleViewController; - private final DeviceConfigHelper mDeviceConfigHelper; - private final Map<AssistHandleBehavior, BehaviorController> mBehaviorMap; - private final Lazy<AccessibilityManager> mA11yManager; - - private boolean mHandlesShowing = false; - private long mHandlesLastHiddenAt; - private long mShowAndGoEndsAt; - /** - * This should always be initialized as {@link AssistHandleBehavior#OFF} to ensure proper - * behavior lifecycle. - */ - private AssistHandleBehavior mCurrentBehavior = AssistHandleBehavior.OFF; - private boolean mInGesturalMode; - - @Inject - AssistHandleBehaviorController( - Context context, - AssistUtils assistUtils, - @Named(ASSIST_HANDLE_THREAD_NAME) Handler handler, - Provider<AssistHandleViewController> assistHandleViewController, - DeviceConfigHelper deviceConfigHelper, - Map<AssistHandleBehavior, BehaviorController> behaviorMap, - NavigationModeController navigationModeController, - Lazy<AccessibilityManager> a11yManager, - DumpManager dumpManager) { - mContext = context; - mAssistUtils = assistUtils; - mHandler = handler; - mAssistHandleViewController = assistHandleViewController; - mDeviceConfigHelper = deviceConfigHelper; - mBehaviorMap = behaviorMap; - mA11yManager = a11yManager; - - mInGesturalMode = QuickStepContract.isGesturalMode( - navigationModeController.addListener(this::handleNavigationModeChange)); - - setBehavior(getBehaviorMode()); - mDeviceConfigHelper.addOnPropertiesChangedListener( - mHandler::post, - (properties) -> { - if (properties.getKeyset().contains( - SystemUiDeviceConfigFlags.ASSIST_HANDLES_BEHAVIOR_MODE)) { - setBehavior(properties.getString( - SystemUiDeviceConfigFlags.ASSIST_HANDLES_BEHAVIOR_MODE, null)); - } - }); - - dumpManager.registerDumpable(TAG, this); - } - - @Override // AssistHandleCallbacks - public void hide() { - clearPendingCommands(); - mHandler.post(mHideHandles); - } - - @Override // AssistHandleCallbacks - public void showAndGo() { - clearPendingCommands(); - mHandler.post(mShowAndGo); - } - - private void showAndGoInternal() { - maybeShowHandles(/* ignoreThreshold = */ false); - long showAndGoDuration = getShowAndGoDuration(); - mShowAndGoEndsAt = SystemClock.elapsedRealtime() + showAndGoDuration; - mHandler.postDelayed(mHideHandles, showAndGoDuration); - } - - @Override // AssistHandleCallbacks - public void showAndGoDelayed(long delayMs, boolean hideIfShowing) { - clearPendingCommands(); - if (hideIfShowing) { - mHandler.post(mHideHandles); - } - mHandler.postDelayed(mShowAndGo, delayMs); - } - - @Override // AssistHandleCallbacks - public void showAndStay() { - clearPendingCommands(); - mHandler.post(() -> maybeShowHandles(/* ignoreThreshold = */ true)); - } - - public long getShowAndGoRemainingTimeMs() { - return Long.max(mShowAndGoEndsAt - SystemClock.elapsedRealtime(), 0); - } - - public boolean areHandlesShowing() { - return mHandlesShowing; - } - - void onAssistantGesturePerformed() { - mBehaviorMap.get(mCurrentBehavior).onAssistantGesturePerformed(); - } - - void onAssistHandlesRequested() { - if (mInGesturalMode) { - mBehaviorMap.get(mCurrentBehavior).onAssistHandlesRequested(); - } - } - - void setBehavior(AssistHandleBehavior behavior) { - if (mCurrentBehavior == behavior) { - return; - } - - if (!mBehaviorMap.containsKey(behavior)) { - Log.e(TAG, "Unsupported behavior requested: " + behavior.toString()); - return; - } - - if (mInGesturalMode) { - mBehaviorMap.get(mCurrentBehavior).onModeDeactivated(); - mBehaviorMap.get(behavior).onModeActivated(mContext, /* callbacks = */ this); - } - - mCurrentBehavior = behavior; - } - - private void setBehavior(@Nullable String behavior) { - try { - setBehavior(AssistHandleBehavior.valueOf(behavior)); - } catch (IllegalArgumentException | NullPointerException e) { - Log.e(TAG, "Invalid behavior: " + behavior); - } - } - - private boolean handlesUnblocked(boolean ignoreThreshold) { - if (!isUserSetupComplete()) { - return false; - } - - long timeSinceHidden = SystemClock.elapsedRealtime() - mHandlesLastHiddenAt; - boolean notThrottled = ignoreThreshold || timeSinceHidden >= getShownFrequencyThreshold(); - ComponentName assistantComponent = - mAssistUtils.getAssistComponentForUser(KeyguardUpdateMonitor.getCurrentUser()); - return notThrottled && assistantComponent != null; - } - - private long getShownFrequencyThreshold() { - return mDeviceConfigHelper.getLong( - SystemUiDeviceConfigFlags.ASSIST_HANDLES_SHOWN_FREQUENCY_THRESHOLD_MS, - DEFAULT_SHOWN_FREQUENCY_THRESHOLD_MS); - } - - private long getShowAndGoDuration() { - long configuredTime = mDeviceConfigHelper.getLong( - SystemUiDeviceConfigFlags.ASSIST_HANDLES_SHOW_AND_GO_DURATION_MS, - DEFAULT_SHOW_AND_GO_DURATION_MS); - return mA11yManager.get().getRecommendedTimeoutMillis( - (int) configuredTime, AccessibilityManager.FLAG_CONTENT_ICONS); - } - - private String getBehaviorMode() { - return mDeviceConfigHelper.getString( - SystemUiDeviceConfigFlags.ASSIST_HANDLES_BEHAVIOR_MODE, - DEFAULT_BEHAVIOR.toString()); - } - - private void maybeShowHandles(boolean ignoreThreshold) { - if (mHandlesShowing) { - return; - } - - if (handlesUnblocked(ignoreThreshold)) { - mHandlesShowing = true; - AssistHandleViewController assistHandleViewController = - mAssistHandleViewController.get(); - if (assistHandleViewController == null) { - Log.w(TAG, "Couldn't show handles, AssistHandleViewController unavailable"); - } else { - assistHandleViewController.setAssistHintVisible(true); - } - } - } - - private void hideHandles() { - if (!mHandlesShowing) { - return; - } - - mHandlesShowing = false; - mHandlesLastHiddenAt = SystemClock.elapsedRealtime(); - AssistHandleViewController assistHandleViewController = - mAssistHandleViewController.get(); - if (assistHandleViewController == null) { - Log.w(TAG, "Couldn't show handles, AssistHandleViewController unavailable"); - } else { - assistHandleViewController.setAssistHintVisible(false); - } - } - - private void handleNavigationModeChange(int navigationMode) { - boolean inGesturalMode = QuickStepContract.isGesturalMode(navigationMode); - if (mInGesturalMode == inGesturalMode) { - return; - } - - mInGesturalMode = inGesturalMode; - if (mInGesturalMode) { - mBehaviorMap.get(mCurrentBehavior).onModeActivated(mContext, /* callbacks = */ this); - } else { - mBehaviorMap.get(mCurrentBehavior).onModeDeactivated(); - hide(); - } - } - - private void clearPendingCommands() { - mHandler.removeCallbacks(mHideHandles); - mHandler.removeCallbacks(mShowAndGo); - mShowAndGoEndsAt = 0; - } - - private boolean isUserSetupComplete() { - return Settings.Secure.getInt( - mContext.getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1; - } - - @VisibleForTesting - void setInGesturalModeForTest(boolean inGesturalMode) { - mInGesturalMode = inGesturalMode; - } - - @Override // Dumpable - public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { - pw.println("Current AssistHandleBehaviorController State:"); - - pw.println(" mHandlesShowing=" + mHandlesShowing); - pw.println(" mHandlesLastHiddenAt=" + mHandlesLastHiddenAt); - pw.println(" mInGesturalMode=" + mInGesturalMode); - - pw.println(" Phenotype Flags:"); - pw.println(" " - + SystemUiDeviceConfigFlags.ASSIST_HANDLES_SHOW_AND_GO_DURATION_MS + "(a11y modded)" - + "=" - + getShowAndGoDuration()); - pw.println(" " - + SystemUiDeviceConfigFlags.ASSIST_HANDLES_SHOWN_FREQUENCY_THRESHOLD_MS - + "=" - + getShownFrequencyThreshold()); - pw.println(" " - + SystemUiDeviceConfigFlags.ASSIST_HANDLES_BEHAVIOR_MODE - + "=" - + getBehaviorMode()); - - pw.println(" mCurrentBehavior=" + mCurrentBehavior.toString()); - mBehaviorMap.get(mCurrentBehavior).dump(pw, " "); - } - - interface BehaviorController { - void onModeActivated(Context context, AssistHandleCallbacks callbacks); - default void onModeDeactivated() {} - default void onAssistantGesturePerformed() {} - default void onAssistHandlesRequested() {} - default void dump(PrintWriter pw, String prefix) {} - } -} diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistHandleCallbacks.java b/packages/SystemUI/src/com/android/systemui/assist/AssistHandleCallbacks.java deleted file mode 100644 index 3db861d57268..000000000000 --- a/packages/SystemUI/src/com/android/systemui/assist/AssistHandleCallbacks.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (C) 2019 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.assist; - -/** Callback for controlling Assistant handle behavior. */ -public interface AssistHandleCallbacks { - - /** Hide the Assistant handles. */ - void hide(); - - /** - * Show the Assistant handles for the configured duration and then hide them. - * - * Won't show if the handles have been shown within the configured timeout. - */ - void showAndGo(); - - /** - * Same as show and go, but will not do anything until a delay has elapsed. - * - * Will be cancelled if another command is given during the delay. - */ - void showAndGoDelayed(long delayMs, boolean hideIfShowing); - - /** Show the Assistant handles. */ - void showAndStay(); -} diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistHandleLikeHomeBehavior.java b/packages/SystemUI/src/com/android/systemui/assist/AssistHandleLikeHomeBehavior.java deleted file mode 100644 index 5d8ec4bb330b..000000000000 --- a/packages/SystemUI/src/com/android/systemui/assist/AssistHandleLikeHomeBehavior.java +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Copyright (C) 2019 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.assist; - -import android.content.Context; - -import androidx.annotation.Nullable; - -import com.android.systemui.assist.AssistHandleBehaviorController.BehaviorController; -import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.keyguard.WakefulnessLifecycle; -import com.android.systemui.model.SysUiState; -import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.shared.system.QuickStepContract; - -import java.io.PrintWriter; - -import javax.inject.Inject; - -import dagger.Lazy; - -/** - * Assistant Handle behavior that makes Assistant handles show/hide when the home handle is - * shown/hidden, respectively. - */ -@SysUISingleton -final class AssistHandleLikeHomeBehavior implements BehaviorController { - - private final StatusBarStateController.StateListener mStatusBarStateListener = - new StatusBarStateController.StateListener() { - @Override - public void onDozingChanged(boolean isDozing) { - handleDozingChanged(isDozing); - } - }; - private final WakefulnessLifecycle.Observer mWakefulnessLifecycleObserver = - new WakefulnessLifecycle.Observer() { - @Override - public void onStartedWakingUp() { - handleWakefullnessChanged(/* isAwake = */ false); - } - - @Override - public void onFinishedWakingUp() { - handleWakefullnessChanged(/* isAwake = */ true); - } - - @Override - public void onStartedGoingToSleep() { - handleWakefullnessChanged(/* isAwake = */ false); - } - - @Override - public void onFinishedGoingToSleep() { - handleWakefullnessChanged(/* isAwake = */ false); - } - }; - - private final SysUiState.SysUiStateCallback mSysUiStateCallback = - this::handleSystemUiStateChange; - - private final Lazy<StatusBarStateController> mStatusBarStateController; - private final Lazy<WakefulnessLifecycle> mWakefulnessLifecycle; - private final Lazy<SysUiState> mSysUiFlagContainer; - - private boolean mIsDozing; - private boolean mIsAwake; - private boolean mIsHomeHandleHiding; - - @Nullable private AssistHandleCallbacks mAssistHandleCallbacks; - - @Inject - AssistHandleLikeHomeBehavior( - Lazy<StatusBarStateController> statusBarStateController, - Lazy<WakefulnessLifecycle> wakefulnessLifecycle, - Lazy<SysUiState> sysUiFlagContainer) { - mStatusBarStateController = statusBarStateController; - mWakefulnessLifecycle = wakefulnessLifecycle; - mSysUiFlagContainer = sysUiFlagContainer; - } - - @Override - public void onModeActivated(Context context, AssistHandleCallbacks callbacks) { - mAssistHandleCallbacks = callbacks; - mIsDozing = mStatusBarStateController.get().isDozing(); - mStatusBarStateController.get().addCallback(mStatusBarStateListener); - mIsAwake = mWakefulnessLifecycle.get().getWakefulness() - == WakefulnessLifecycle.WAKEFULNESS_AWAKE; - mWakefulnessLifecycle.get().addObserver(mWakefulnessLifecycleObserver); - mSysUiFlagContainer.get().addCallback(mSysUiStateCallback); - callbackForCurrentState(); - } - - @Override - public void onModeDeactivated() { - mAssistHandleCallbacks = null; - mStatusBarStateController.get().removeCallback(mStatusBarStateListener); - mWakefulnessLifecycle.get().removeObserver(mWakefulnessLifecycleObserver); - mSysUiFlagContainer.get().removeCallback(mSysUiStateCallback); - } - - private static boolean isHomeHandleHiding(int sysuiStateFlags) { - return (sysuiStateFlags & QuickStepContract.SYSUI_STATE_NAV_BAR_HIDDEN) != 0; - } - - private void handleDozingChanged(boolean isDozing) { - if (mIsDozing == isDozing) { - return; - } - - mIsDozing = isDozing; - callbackForCurrentState(); - } - - private void handleWakefullnessChanged(boolean isAwake) { - if (mIsAwake == isAwake) { - return; - } - - mIsAwake = isAwake; - callbackForCurrentState(); - } - - private void handleSystemUiStateChange(int sysuiStateFlags) { - boolean isHomeHandleHiding = isHomeHandleHiding(sysuiStateFlags); - if (mIsHomeHandleHiding == isHomeHandleHiding) { - return; - } - - mIsHomeHandleHiding = isHomeHandleHiding; - callbackForCurrentState(); - } - - private void callbackForCurrentState() { - if (mAssistHandleCallbacks == null) { - return; - } - - if (mIsHomeHandleHiding || !isFullyAwake()) { - mAssistHandleCallbacks.hide(); - } else { - mAssistHandleCallbacks.showAndStay(); - } - } - - private boolean isFullyAwake() { - return mIsAwake && !mIsDozing; - } - - @Override - public void dump(PrintWriter pw, String prefix) { - pw.println(prefix + "Current AssistHandleLikeHomeBehavior State:"); - - pw.println(prefix + " mIsDozing=" + mIsDozing); - pw.println(prefix + " mIsAwake=" + mIsAwake); - pw.println(prefix + " mIsHomeHandleHiding=" + mIsHomeHandleHiding); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistHandleOffBehavior.java b/packages/SystemUI/src/com/android/systemui/assist/AssistHandleOffBehavior.java deleted file mode 100644 index 86d3254dc7f9..000000000000 --- a/packages/SystemUI/src/com/android/systemui/assist/AssistHandleOffBehavior.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (C) 2019 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.assist; - -import android.content.Context; - -import com.android.systemui.assist.AssistHandleBehaviorController.BehaviorController; -import com.android.systemui.dagger.SysUISingleton; - -import javax.inject.Inject; - -/** Assistant handle behavior that hides the Assistant handles. */ -@SysUISingleton -final class AssistHandleOffBehavior implements BehaviorController { - - @Inject - AssistHandleOffBehavior() { - } - - @Override - public void onModeActivated(Context context, AssistHandleCallbacks callbacks) { - callbacks.hide(); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistHandleReminderExpBehavior.java b/packages/SystemUI/src/com/android/systemui/assist/AssistHandleReminderExpBehavior.java deleted file mode 100644 index c1c2de166627..000000000000 --- a/packages/SystemUI/src/com/android/systemui/assist/AssistHandleReminderExpBehavior.java +++ /dev/null @@ -1,662 +0,0 @@ -/* - * Copyright (C) 2019 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.assist; - -import static com.android.systemui.assist.AssistModule.ASSIST_HANDLE_THREAD_NAME; -import static com.android.systemui.assist.AssistModule.UPTIME_NAME; - -import android.app.ActivityManager; -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.ResolveInfo; -import android.database.ContentObserver; -import android.net.Uri; -import android.os.Handler; -import android.provider.Settings; - -import androidx.annotation.Nullable; -import androidx.slice.Clock; - -import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; -import com.android.systemui.BootCompleteCache; -import com.android.systemui.assist.AssistHandleBehaviorController.BehaviorController; -import com.android.systemui.broadcast.BroadcastDispatcher; -import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.keyguard.WakefulnessLifecycle; -import com.android.systemui.model.SysUiState; -import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.recents.OverviewProxyService; -import com.android.systemui.shared.system.ActivityManagerWrapper; -import com.android.systemui.shared.system.PackageManagerWrapper; -import com.android.systemui.shared.system.QuickStepContract; -import com.android.systemui.shared.system.TaskStackChangeListener; -import com.android.systemui.shared.system.TaskStackChangeListeners; -import com.android.systemui.statusbar.StatusBarState; - -import java.io.PrintWriter; -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import javax.inject.Inject; -import javax.inject.Named; - -import dagger.Lazy; - -/** - * Assistant handle behavior that hides the handles when the phone is dozing or in immersive mode, - * shows the handles when on lockscreen, and shows the handles temporarily when changing tasks or - * entering overview. - */ -@SysUISingleton -final class AssistHandleReminderExpBehavior implements BehaviorController { - - private static final Uri LEARNING_TIME_ELAPSED_URI = - Settings.Secure.getUriFor(Settings.Secure.ASSIST_HANDLES_LEARNING_TIME_ELAPSED_MILLIS); - private static final Uri LEARNING_EVENT_COUNT_URI = - Settings.Secure.getUriFor(Settings.Secure.ASSIST_HANDLES_LEARNING_EVENT_COUNT); - private static final String LEARNED_HINT_LAST_SHOWN_KEY = - "reminder_exp_learned_hint_last_shown"; - private static final long DEFAULT_LEARNING_TIME_MS = TimeUnit.DAYS.toMillis(10); - private static final int DEFAULT_LEARNING_COUNT = 10; - private static final long DEFAULT_SHOW_AND_GO_DELAYED_SHORT_DELAY_MS = 150; - private static final long DEFAULT_SHOW_AND_GO_DELAYED_LONG_DELAY_MS = - TimeUnit.SECONDS.toMillis(1); - private static final long DEFAULT_SHOW_AND_GO_DELAY_RESET_TIMEOUT_MS = - TimeUnit.SECONDS.toMillis(3); - private static final boolean DEFAULT_SUPPRESS_ON_LOCKSCREEN = false; - private static final boolean DEFAULT_SUPPRESS_ON_LAUNCHER = false; - private static final boolean DEFAULT_SUPPRESS_ON_APPS = true; - private static final boolean DEFAULT_SHOW_WHEN_TAUGHT = false; - - private static final String[] DEFAULT_HOME_CHANGE_ACTIONS = new String[] { - PackageManagerWrapper.ACTION_PREFERRED_ACTIVITY_CHANGED, - Intent.ACTION_PACKAGE_ADDED, - Intent.ACTION_PACKAGE_CHANGED, - Intent.ACTION_PACKAGE_REMOVED - }; - - private final StatusBarStateController.StateListener mStatusBarStateListener = - new StatusBarStateController.StateListener() { - @Override - public void onStateChanged(int newState) { - handleStatusBarStateChanged(newState); - } - - @Override - public void onDozingChanged(boolean isDozing) { - handleDozingChanged(isDozing); - } - }; - private final TaskStackChangeListener mTaskStackChangeListener = - new TaskStackChangeListener() { - @Override - public void onTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) { - handleTaskStackTopChanged(taskInfo.taskId, taskInfo.topActivity); - } - - @Override - public void onTaskCreated(int taskId, ComponentName componentName) { - handleTaskStackTopChanged(taskId, componentName); - } - }; - private final OverviewProxyService.OverviewProxyListener mOverviewProxyListener = - new OverviewProxyService.OverviewProxyListener() { - @Override - public void onOverviewShown(boolean fromHome) { - handleOverviewShown(); - } - }; - private final SysUiState.SysUiStateCallback mSysUiStateCallback = - this::handleSystemUiStateChanged; - private final WakefulnessLifecycle.Observer mWakefulnessLifecycleObserver = - new WakefulnessLifecycle.Observer() { - @Override - public void onStartedWakingUp() { - handleWakefullnessChanged(/* isAwake = */ false); - } - - @Override - public void onFinishedWakingUp() { - handleWakefullnessChanged(/* isAwake = */ true); - } - - @Override - public void onStartedGoingToSleep() { - handleWakefullnessChanged(/* isAwake = */ false); - } - - @Override - public void onFinishedGoingToSleep() { - handleWakefullnessChanged(/* isAwake = */ false); - } - }; - private final BroadcastReceiver mDefaultHomeBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - mDefaultHome = getCurrentDefaultHome(); - } - }; - - private final BootCompleteCache.BootCompleteListener mBootCompleteListener = - new BootCompleteCache.BootCompleteListener() { - @Override - public void onBootComplete() { - mDefaultHome = getCurrentDefaultHome(); - } - }; - - private final IntentFilter mDefaultHomeIntentFilter; - private final Runnable mResetConsecutiveTaskSwitches = this::resetConsecutiveTaskSwitches; - - private final Clock mClock; - private final Handler mHandler; - private final DeviceConfigHelper mDeviceConfigHelper; - private final Lazy<StatusBarStateController> mStatusBarStateController; - private final Lazy<ActivityManagerWrapper> mActivityManagerWrapper; - private final Lazy<TaskStackChangeListeners> mTaskStackChangeListeners; - private final Lazy<OverviewProxyService> mOverviewProxyService; - private final Lazy<SysUiState> mSysUiFlagContainer; - private final Lazy<WakefulnessLifecycle> mWakefulnessLifecycle; - private final Lazy<PackageManagerWrapper> mPackageManagerWrapper; - private final Lazy<BroadcastDispatcher> mBroadcastDispatcher; - private final Lazy<BootCompleteCache> mBootCompleteCache; - - private boolean mOnLockscreen; - private boolean mIsDozing; - private boolean mIsAwake; - private int mRunningTaskId; - private boolean mIsNavBarHidden; - private boolean mIsLauncherShowing; - private int mConsecutiveTaskSwitches; - @Nullable private ContentObserver mSettingObserver; - - /** Whether user has learned the gesture. */ - private boolean mIsLearned; - private long mLastLearningTimestamp; - /** Uptime while in this behavior. */ - private long mLearningTimeElapsed; - /** Number of successful Assistant invocations while in this behavior. */ - private int mLearningCount; - private long mLearnedHintLastShownEpochDay; - - @Nullable private Context mContext; - @Nullable private AssistHandleCallbacks mAssistHandleCallbacks; - @Nullable private ComponentName mDefaultHome; - - @Inject - AssistHandleReminderExpBehavior( - @Named(UPTIME_NAME) Clock clock, - @Named(ASSIST_HANDLE_THREAD_NAME) Handler handler, - DeviceConfigHelper deviceConfigHelper, - Lazy<StatusBarStateController> statusBarStateController, - Lazy<ActivityManagerWrapper> activityManagerWrapper, - Lazy<TaskStackChangeListeners> taskStackChangeListeners, - Lazy<OverviewProxyService> overviewProxyService, - Lazy<SysUiState> sysUiFlagContainer, - Lazy<WakefulnessLifecycle> wakefulnessLifecycle, - Lazy<PackageManagerWrapper> packageManagerWrapper, - Lazy<BroadcastDispatcher> broadcastDispatcher, - Lazy<BootCompleteCache> bootCompleteCache) { - mClock = clock; - mHandler = handler; - mDeviceConfigHelper = deviceConfigHelper; - mStatusBarStateController = statusBarStateController; - mActivityManagerWrapper = activityManagerWrapper; - mTaskStackChangeListeners = taskStackChangeListeners; - mOverviewProxyService = overviewProxyService; - mSysUiFlagContainer = sysUiFlagContainer; - mWakefulnessLifecycle = wakefulnessLifecycle; - mPackageManagerWrapper = packageManagerWrapper; - mDefaultHomeIntentFilter = new IntentFilter(); - for (String action : DEFAULT_HOME_CHANGE_ACTIONS) { - mDefaultHomeIntentFilter.addAction(action); - } - mBroadcastDispatcher = broadcastDispatcher; - mBootCompleteCache = bootCompleteCache; - } - - @Override - public void onModeActivated(Context context, AssistHandleCallbacks callbacks) { - mContext = context; - mAssistHandleCallbacks = callbacks; - mConsecutiveTaskSwitches = 0; - mBootCompleteCache.get().addListener(mBootCompleteListener); - mDefaultHome = getCurrentDefaultHome(); - mBroadcastDispatcher.get() - .registerReceiver(mDefaultHomeBroadcastReceiver, mDefaultHomeIntentFilter); - mOnLockscreen = onLockscreen(mStatusBarStateController.get().getState()); - mIsDozing = mStatusBarStateController.get().isDozing(); - mStatusBarStateController.get().addCallback(mStatusBarStateListener); - ActivityManager.RunningTaskInfo runningTaskInfo = - mActivityManagerWrapper.get().getRunningTask(); - mRunningTaskId = runningTaskInfo == null ? 0 : runningTaskInfo.taskId; - mTaskStackChangeListeners.get().registerTaskStackListener(mTaskStackChangeListener); - mOverviewProxyService.get().addCallback(mOverviewProxyListener); - mSysUiFlagContainer.get().addCallback(mSysUiStateCallback); - mIsAwake = mWakefulnessLifecycle.get().getWakefulness() - == WakefulnessLifecycle.WAKEFULNESS_AWAKE; - mWakefulnessLifecycle.get().addObserver(mWakefulnessLifecycleObserver); - - mLearningTimeElapsed = Settings.Secure.getLong( - context.getContentResolver(), - Settings.Secure.ASSIST_HANDLES_LEARNING_TIME_ELAPSED_MILLIS, - /* default = */ 0); - mLearningCount = Settings.Secure.getInt( - context.getContentResolver(), - Settings.Secure.ASSIST_HANDLES_LEARNING_EVENT_COUNT, - /* default = */ 0); - mSettingObserver = new SettingsObserver(context, mHandler); - context.getContentResolver().registerContentObserver( - LEARNING_TIME_ELAPSED_URI, - /* notifyForDescendants = */ true, - mSettingObserver); - context.getContentResolver().registerContentObserver( - LEARNING_EVENT_COUNT_URI, - /* notifyForDescendants = */ true, - mSettingObserver); - mLearnedHintLastShownEpochDay = Settings.Secure.getLong( - context.getContentResolver(), LEARNED_HINT_LAST_SHOWN_KEY, /* default = */ 0); - mLastLearningTimestamp = mClock.currentTimeMillis(); - - callbackForCurrentState(/* justUnlocked = */ false); - } - - @Override - public void onModeDeactivated() { - mAssistHandleCallbacks = null; - if (mContext != null) { - mBroadcastDispatcher.get().unregisterReceiver(mDefaultHomeBroadcastReceiver); - mBootCompleteCache.get().removeListener(mBootCompleteListener); - mContext.getContentResolver().unregisterContentObserver(mSettingObserver); - mSettingObserver = null; - // putString in order to use overrideableByRestore - Settings.Secure.putString( - mContext.getContentResolver(), - Settings.Secure.ASSIST_HANDLES_LEARNING_TIME_ELAPSED_MILLIS, - Long.toString(0L), - /* overrideableByRestore = */ true); - // putString in order to use overrideableByRestore - Settings.Secure.putString( - mContext.getContentResolver(), - Settings.Secure.ASSIST_HANDLES_LEARNING_EVENT_COUNT, - Integer.toString(0), - /* overrideableByRestore = */ true); - Settings.Secure.putLong(mContext.getContentResolver(), LEARNED_HINT_LAST_SHOWN_KEY, 0); - mContext = null; - } - mStatusBarStateController.get().removeCallback(mStatusBarStateListener); - mTaskStackChangeListeners.get().unregisterTaskStackListener(mTaskStackChangeListener); - mOverviewProxyService.get().removeCallback(mOverviewProxyListener); - mSysUiFlagContainer.get().removeCallback(mSysUiStateCallback); - mWakefulnessLifecycle.get().removeObserver(mWakefulnessLifecycleObserver); - } - - @Override - public void onAssistantGesturePerformed() { - if (mContext == null) { - return; - } - - // putString in order to use overrideableByRestore - Settings.Secure.putString( - mContext.getContentResolver(), - Settings.Secure.ASSIST_HANDLES_LEARNING_EVENT_COUNT, - Integer.toString(++mLearningCount), - /* overrideableByRestore = */ true); - } - - @Override - public void onAssistHandlesRequested() { - if (mAssistHandleCallbacks != null - && isFullyAwake() - && !mIsNavBarHidden - && !mOnLockscreen) { - mAssistHandleCallbacks.showAndGo(); - } - } - - @Nullable - private ComponentName getCurrentDefaultHome() { - List<ResolveInfo> homeActivities = new ArrayList<>(); - ComponentName defaultHome = mPackageManagerWrapper.get().getHomeActivities(homeActivities); - if (defaultHome != null) { - return defaultHome; - } - - int topPriority = Integer.MIN_VALUE; - ComponentName topComponent = null; - for (ResolveInfo resolveInfo : homeActivities) { - if (resolveInfo.priority > topPriority) { - topComponent = resolveInfo.activityInfo.getComponentName(); - topPriority = resolveInfo.priority; - } else if (resolveInfo.priority == topPriority) { - topComponent = null; - } - } - return topComponent; - } - - private void handleStatusBarStateChanged(int newState) { - boolean onLockscreen = onLockscreen(newState); - if (mOnLockscreen == onLockscreen) { - return; - } - - resetConsecutiveTaskSwitches(); - mOnLockscreen = onLockscreen; - callbackForCurrentState(!onLockscreen); - } - - private void handleDozingChanged(boolean isDozing) { - if (mIsDozing == isDozing) { - return; - } - - resetConsecutiveTaskSwitches(); - mIsDozing = isDozing; - callbackForCurrentState(/* justUnlocked = */ false); - } - - private void handleWakefullnessChanged(boolean isAwake) { - if (mIsAwake == isAwake) { - return; - } - - resetConsecutiveTaskSwitches(); - mIsAwake = isAwake; - callbackForCurrentState(/* justUnlocked = */ false); - } - - private void handleTaskStackTopChanged(int taskId, @Nullable ComponentName taskComponentName) { - if (mRunningTaskId == taskId || taskComponentName == null) { - return; - } - - mRunningTaskId = taskId; - mIsLauncherShowing = taskComponentName.equals(mDefaultHome); - if (mIsLauncherShowing) { - resetConsecutiveTaskSwitches(); - } else { - rescheduleConsecutiveTaskSwitchesReset(); - mConsecutiveTaskSwitches++; - } - callbackForCurrentState(/* justUnlocked = */ false); - } - - private void handleSystemUiStateChanged(int sysuiStateFlags) { - boolean isNavBarHidden = - (sysuiStateFlags & QuickStepContract.SYSUI_STATE_NAV_BAR_HIDDEN) != 0; - if (mIsNavBarHidden == isNavBarHidden) { - return; - } - - resetConsecutiveTaskSwitches(); - mIsNavBarHidden = isNavBarHidden; - callbackForCurrentState(/* justUnlocked = */ false); - } - - private void handleOverviewShown() { - resetConsecutiveTaskSwitches(); - callbackForCurrentState(/* justUnlocked = */ false); - } - - private boolean onLockscreen(int statusBarState) { - return statusBarState == StatusBarState.KEYGUARD - || statusBarState == StatusBarState.SHADE_LOCKED; - } - - private void callbackForCurrentState(boolean justUnlocked) { - updateLearningStatus(); - - if (mIsLearned) { - callbackForLearnedState(justUnlocked); - } else { - callbackForUnlearnedState(); - } - } - - private void callbackForLearnedState(boolean justUnlocked) { - if (mAssistHandleCallbacks == null) { - return; - } - - if (!isFullyAwake() || mIsNavBarHidden || mOnLockscreen || !getShowWhenTaught()) { - mAssistHandleCallbacks.hide(); - } else if (justUnlocked) { - long currentEpochDay = LocalDate.now().toEpochDay(); - if (mLearnedHintLastShownEpochDay < currentEpochDay) { - if (mContext != null) { - Settings.Secure.putLong( - mContext.getContentResolver(), - LEARNED_HINT_LAST_SHOWN_KEY, - currentEpochDay); - } - mLearnedHintLastShownEpochDay = currentEpochDay; - mAssistHandleCallbacks.showAndGo(); - } - } - } - - private void callbackForUnlearnedState() { - if (mAssistHandleCallbacks == null) { - return; - } - - if (!isFullyAwake() || mIsNavBarHidden || isSuppressed()) { - mAssistHandleCallbacks.hide(); - } else if (mOnLockscreen) { - mAssistHandleCallbacks.showAndStay(); - } else if (mIsLauncherShowing) { - mAssistHandleCallbacks.showAndGo(); - } else if (mConsecutiveTaskSwitches == 1) { - mAssistHandleCallbacks.showAndGoDelayed( - getShowAndGoDelayedShortDelayMs(), /* hideIfShowing = */ false); - } else { - mAssistHandleCallbacks.showAndGoDelayed( - getShowAndGoDelayedLongDelayMs(), /* hideIfShowing = */ true); - } - } - - private boolean isSuppressed() { - if (mOnLockscreen) { - return getSuppressOnLockscreen(); - } else if (mIsLauncherShowing) { - return getSuppressOnLauncher(); - } else { - return getSuppressOnApps(); - } - } - - private void updateLearningStatus() { - if (mContext == null) { - return; - } - - long currentTimestamp = mClock.currentTimeMillis(); - mLearningTimeElapsed += currentTimestamp - mLastLearningTimestamp; - mLastLearningTimestamp = currentTimestamp; - - mIsLearned = - mLearningCount >= getLearningCount() || mLearningTimeElapsed >= getLearningTimeMs(); - - // putString in order to use overrideableByRestore - mHandler.post(() -> Settings.Secure.putString( - mContext.getContentResolver(), - Settings.Secure.ASSIST_HANDLES_LEARNING_TIME_ELAPSED_MILLIS, - Long.toString(mLearningTimeElapsed), - /* overrideableByRestore = */ true)); - } - - private void resetConsecutiveTaskSwitches() { - mHandler.removeCallbacks(mResetConsecutiveTaskSwitches); - mConsecutiveTaskSwitches = 0; - } - - private void rescheduleConsecutiveTaskSwitchesReset() { - mHandler.removeCallbacks(mResetConsecutiveTaskSwitches); - mHandler.postDelayed(mResetConsecutiveTaskSwitches, getShowAndGoDelayResetTimeoutMs()); - } - - private boolean isFullyAwake() { - return mIsAwake && !mIsDozing; - } - - private long getLearningTimeMs() { - return mDeviceConfigHelper.getLong( - SystemUiDeviceConfigFlags.ASSIST_HANDLES_LEARN_TIME_MS, - DEFAULT_LEARNING_TIME_MS); - } - - private int getLearningCount() { - return mDeviceConfigHelper.getInt( - SystemUiDeviceConfigFlags.ASSIST_HANDLES_LEARN_COUNT, - DEFAULT_LEARNING_COUNT); - } - - private long getShowAndGoDelayedShortDelayMs() { - return mDeviceConfigHelper.getLong( - SystemUiDeviceConfigFlags.ASSIST_HANDLES_SHOW_AND_GO_DELAYED_SHORT_DELAY_MS, - DEFAULT_SHOW_AND_GO_DELAYED_SHORT_DELAY_MS); - } - - private long getShowAndGoDelayedLongDelayMs() { - return mDeviceConfigHelper.getLong( - SystemUiDeviceConfigFlags.ASSIST_HANDLES_SHOW_AND_GO_DELAYED_LONG_DELAY_MS, - DEFAULT_SHOW_AND_GO_DELAYED_LONG_DELAY_MS); - } - - private long getShowAndGoDelayResetTimeoutMs() { - return mDeviceConfigHelper.getLong( - SystemUiDeviceConfigFlags.ASSIST_HANDLES_SHOW_AND_GO_DELAY_RESET_TIMEOUT_MS, - DEFAULT_SHOW_AND_GO_DELAY_RESET_TIMEOUT_MS); - } - - private boolean getSuppressOnLockscreen() { - return mDeviceConfigHelper.getBoolean( - SystemUiDeviceConfigFlags.ASSIST_HANDLES_SUPPRESS_ON_LOCKSCREEN, - DEFAULT_SUPPRESS_ON_LOCKSCREEN); - } - - private boolean getSuppressOnLauncher() { - return mDeviceConfigHelper.getBoolean( - SystemUiDeviceConfigFlags.ASSIST_HANDLES_SUPPRESS_ON_LAUNCHER, - DEFAULT_SUPPRESS_ON_LAUNCHER); - } - - private boolean getSuppressOnApps() { - return mDeviceConfigHelper.getBoolean( - SystemUiDeviceConfigFlags.ASSIST_HANDLES_SUPPRESS_ON_APPS, - DEFAULT_SUPPRESS_ON_APPS); - } - - private boolean getShowWhenTaught() { - return mDeviceConfigHelper.getBoolean( - SystemUiDeviceConfigFlags.ASSIST_HANDLES_SHOW_WHEN_TAUGHT, - DEFAULT_SHOW_WHEN_TAUGHT); - } - - @Override - public void dump(PrintWriter pw, String prefix) { - pw.println(prefix + "Current AssistHandleReminderExpBehavior State:"); - pw.println(prefix + " mOnLockscreen=" + mOnLockscreen); - pw.println(prefix + " mIsDozing=" + mIsDozing); - pw.println(prefix + " mIsAwake=" + mIsAwake); - pw.println(prefix + " mRunningTaskId=" + mRunningTaskId); - pw.println(prefix + " mDefaultHome=" + mDefaultHome); - pw.println(prefix + " mIsNavBarHidden=" + mIsNavBarHidden); - pw.println(prefix + " mIsLauncherShowing=" + mIsLauncherShowing); - pw.println(prefix + " mConsecutiveTaskSwitches=" + mConsecutiveTaskSwitches); - pw.println(prefix + " mIsLearned=" + mIsLearned); - pw.println(prefix + " mLastLearningTimestamp=" + mLastLearningTimestamp); - pw.println(prefix + " mLearningTimeElapsed=" + mLearningTimeElapsed); - pw.println(prefix + " mLearningCount=" + mLearningCount); - pw.println(prefix + " mLearnedHintLastShownEpochDay=" + mLearnedHintLastShownEpochDay); - pw.println( - prefix + " mAssistHandleCallbacks present: " + (mAssistHandleCallbacks != null)); - - pw.println(prefix + " Phenotype Flags:"); - pw.println(prefix + " " - + SystemUiDeviceConfigFlags.ASSIST_HANDLES_LEARN_TIME_MS - + "=" - + getLearningTimeMs()); - pw.println(prefix + " " - + SystemUiDeviceConfigFlags.ASSIST_HANDLES_LEARN_COUNT - + "=" - + getLearningCount()); - pw.println(prefix + " " - + SystemUiDeviceConfigFlags.ASSIST_HANDLES_SHOW_AND_GO_DELAYED_SHORT_DELAY_MS - + "=" - + getShowAndGoDelayedShortDelayMs()); - pw.println(prefix + " " - + SystemUiDeviceConfigFlags.ASSIST_HANDLES_SHOW_AND_GO_DELAYED_LONG_DELAY_MS - + "=" - + getShowAndGoDelayedLongDelayMs()); - pw.println(prefix + " " - + SystemUiDeviceConfigFlags.ASSIST_HANDLES_SHOW_AND_GO_DELAY_RESET_TIMEOUT_MS - + "=" - + getShowAndGoDelayResetTimeoutMs()); - pw.println(prefix + " " - + SystemUiDeviceConfigFlags.ASSIST_HANDLES_SUPPRESS_ON_LOCKSCREEN - + "=" - + getSuppressOnLockscreen()); - pw.println(prefix + " " - + SystemUiDeviceConfigFlags.ASSIST_HANDLES_SUPPRESS_ON_LAUNCHER - + "=" - + getSuppressOnLauncher()); - pw.println(prefix + " " - + SystemUiDeviceConfigFlags.ASSIST_HANDLES_SUPPRESS_ON_APPS - + "=" - + getSuppressOnApps()); - pw.println(prefix + " " - + SystemUiDeviceConfigFlags.ASSIST_HANDLES_SHOW_WHEN_TAUGHT - + "=" - + getShowWhenTaught()); - } - - private final class SettingsObserver extends ContentObserver { - - private final Context mContext; - - SettingsObserver(Context context, Handler handler) { - super(handler); - mContext = context; - } - - @Override - public void onChange(boolean selfChange, @Nullable Uri uri) { - if (LEARNING_TIME_ELAPSED_URI.equals(uri)) { - mLastLearningTimestamp = mClock.currentTimeMillis(); - mLearningTimeElapsed = Settings.Secure.getLong( - mContext.getContentResolver(), - Settings.Secure.ASSIST_HANDLES_LEARNING_TIME_ELAPSED_MILLIS, - /* default = */ 0); - } else if (LEARNING_EVENT_COUNT_URI.equals(uri)) { - mLearningCount = Settings.Secure.getInt( - mContext.getContentResolver(), - Settings.Secure.ASSIST_HANDLES_LEARNING_EVENT_COUNT, - /* default = */ 0); - } - - super.onChange(selfChange, uri); - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistHandleViewController.java b/packages/SystemUI/src/com/android/systemui/assist/AssistHandleViewController.java deleted file mode 100644 index f19e53f03849..000000000000 --- a/packages/SystemUI/src/com/android/systemui/assist/AssistHandleViewController.java +++ /dev/null @@ -1,191 +0,0 @@ -/* - * Copyright (C) 2019 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.assist; - -import android.animation.Animator; -import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; -import android.os.Handler; -import android.util.Log; -import android.util.MathUtils; -import android.view.View; -import android.view.animation.AccelerateInterpolator; -import android.view.animation.Interpolator; -import android.view.animation.PathInterpolator; - -import com.android.internal.annotations.VisibleForTesting; -import com.android.systemui.CornerHandleView; -import com.android.systemui.R; -import com.android.systemui.navigationbar.NavigationBarTransitions; - -/** - * A class for managing Assistant handle show, hide and animation. - */ -public class AssistHandleViewController implements NavigationBarTransitions.DarkIntensityListener { - - private static final boolean DEBUG = false; - private static final String TAG = "AssistHandleViewController"; - - private Handler mHandler; - private CornerHandleView mAssistHintLeft; - private CornerHandleView mAssistHintRight; - private int mBottomOffset; - - @VisibleForTesting - boolean mAssistHintVisible; - @VisibleForTesting - boolean mAssistHintBlocked = false; - - public AssistHandleViewController(Handler handler, View navBar) { - mHandler = handler; - mAssistHintLeft = navBar.findViewById(R.id.assist_hint_left); - mAssistHintRight = navBar.findViewById(R.id.assist_hint_right); - } - - @Override - public void onDarkIntensity(float darkIntensity) { - mAssistHintLeft.updateDarkness(darkIntensity); - mAssistHintRight.updateDarkness(darkIntensity); - } - - /** - * Set the bottom offset. - * - * @param bottomOffset the bottom offset to translate. - */ - public void setBottomOffset(int bottomOffset) { - if (mBottomOffset != bottomOffset) { - mBottomOffset = bottomOffset; - if (mAssistHintVisible) { - // If assist handles are visible, hide them without animation and then make them - // show once again (with corrected bottom offset). - hideAssistHandles(); - setAssistHintVisible(true); - } - } - } - - /** - * Controls the visibility of the assist gesture handles. - * - * @param visible whether the handles should be shown - */ - public void setAssistHintVisible(boolean visible) { - if (!mHandler.getLooper().isCurrentThread()) { - mHandler.post(() -> setAssistHintVisible(visible)); - return; - } - - if (mAssistHintBlocked && visible) { - if (DEBUG) { - Log.v(TAG, "Assist hint blocked, cannot make it visible"); - } - return; - } - - if (mAssistHintVisible != visible) { - mAssistHintVisible = visible; - fade(mAssistHintLeft, mAssistHintVisible, /* isLeft = */ true); - fade(mAssistHintRight, mAssistHintVisible, /* isLeft = */ false); - } - } - - /** - * Prevents the assist hint from becoming visible even if `mAssistHintVisible` is true. - */ - public void setAssistHintBlocked(boolean blocked) { - if (!mHandler.getLooper().isCurrentThread()) { - mHandler.post(() -> setAssistHintBlocked(blocked)); - return; - } - - mAssistHintBlocked = blocked; - if (mAssistHintVisible && mAssistHintBlocked) { - hideAssistHandles(); - } - } - - private void hideAssistHandles() { - mAssistHintLeft.setVisibility(View.GONE); - mAssistHintRight.setVisibility(View.GONE); - mAssistHintVisible = false; - } - - /** - * Returns an animator that animates the given view from start to end over durationMs. Start and - * end represent total animation progress: 0 is the start, 1 is the end, 1.1 would be an - * overshoot. - */ - Animator getHandleAnimator(View view, float start, float end, boolean isLeft, long durationMs, - Interpolator interpolator) { - // Note that lerp does allow overshoot, in cases where start and end are outside of [0,1]. - float scaleStart = MathUtils.lerp(2f, 1f, start); - float scaleEnd = MathUtils.lerp(2f, 1f, end); - Animator scaleX = ObjectAnimator.ofFloat(view, View.SCALE_X, scaleStart, scaleEnd); - Animator scaleY = ObjectAnimator.ofFloat(view, View.SCALE_Y, scaleStart, scaleEnd); - float translationStart = MathUtils.lerp(0.2f, 0f, start); - float translationEnd = MathUtils.lerp(0.2f, 0f, end); - int xDirection = isLeft ? -1 : 1; - Animator translateX = ObjectAnimator.ofFloat(view, View.TRANSLATION_X, - xDirection * translationStart * view.getWidth(), - xDirection * translationEnd * view.getWidth()); - Animator translateY = ObjectAnimator.ofFloat(view, View.TRANSLATION_Y, - translationStart * view.getHeight() + mBottomOffset, - translationEnd * view.getHeight() + mBottomOffset); - - AnimatorSet set = new AnimatorSet(); - set.play(scaleX).with(scaleY); - set.play(scaleX).with(translateX); - set.play(scaleX).with(translateY); - set.setDuration(durationMs); - set.setInterpolator(interpolator); - return set; - } - - private void fade(View view, boolean fadeIn, boolean isLeft) { - if (fadeIn) { - view.animate().cancel(); - view.setAlpha(1f); - view.setVisibility(View.VISIBLE); - - // A piecewise spring-like interpolation. - // End value in one animator call must match the start value in the next, otherwise - // there will be a discontinuity. - AnimatorSet anim = new AnimatorSet(); - Animator first = getHandleAnimator(view, 0, 1.1f, isLeft, 750, - new PathInterpolator(0, 0.45f, .67f, 1f)); - Interpolator secondInterpolator = new PathInterpolator(0.33f, 0, 0.67f, 1f); - Animator second = getHandleAnimator(view, 1.1f, 0.97f, isLeft, 400, - secondInterpolator); - Animator third = getHandleAnimator(view, 0.97f, 1.02f, isLeft, 400, - secondInterpolator); - Animator fourth = getHandleAnimator(view, 1.02f, 1f, isLeft, 400, - secondInterpolator); - anim.play(first).before(second); - anim.play(second).before(third); - anim.play(third).before(fourth); - anim.start(); - } else { - view.animate().cancel(); - view.animate() - .setInterpolator(new AccelerateInterpolator(1.5f)) - .setDuration(250) - .alpha(0f); - } - - } -} diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistLogger.kt b/packages/SystemUI/src/com/android/systemui/assist/AssistLogger.kt index 4d0fd4313209..e30f2e440d7a 100644 --- a/packages/SystemUI/src/com/android/systemui/assist/AssistLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/assist/AssistLogger.kt @@ -37,8 +37,7 @@ open class AssistLogger @Inject constructor( protected val context: Context, protected val uiEventLogger: UiEventLogger, private val assistUtils: AssistUtils, - private val phoneStateMonitor: PhoneStateMonitor, - private val assistHandleBehaviorController: AssistHandleBehaviorController + private val phoneStateMonitor: PhoneStateMonitor ) { private val instanceIdSequence = InstanceIdSequence(INSTANCE_ID_MAX) @@ -82,7 +81,7 @@ open class AssistLogger @Inject constructor( assistComponentFinal.flattenToString(), getOrCreateInstanceId().id, deviceStateFinal, - assistHandleBehaviorController.areHandlesShowing()) + false) reportAssistantInvocationExtraData() } diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java index b2c620d88de9..b1197e6aa6f5 100644 --- a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java +++ b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java @@ -87,7 +87,6 @@ public class AssistManager { private static final String INVOCATION_TIME_MS_KEY = "invocation_time_ms"; private static final String INVOCATION_PHONE_STATE_KEY = "invocation_phone_state"; protected static final String ACTION_KEY = "action"; - protected static final String SHOW_ASSIST_HANDLES_ACTION = "show_assist_handles"; protected static final String SET_ASSIST_GESTURE_CONSTRAINED_ACTION = "set_assist_gesture_constrained"; protected static final String CONSTRAINED_KEY = "should_constrain"; @@ -119,7 +118,6 @@ public class AssistManager { protected final Context mContext; private final AssistDisclosure mAssistDisclosure; private final PhoneStateMonitor mPhoneStateMonitor; - private final AssistHandleBehaviorController mHandleController; private final UiController mUiController; protected final Lazy<SysUiState> mSysUiState; protected final AssistLogger mAssistLogger; @@ -148,7 +146,6 @@ public class AssistManager { DeviceProvisionedController controller, Context context, AssistUtils assistUtils, - AssistHandleBehaviorController handleController, CommandQueue commandQueue, PhoneStateMonitor phoneStateMonitor, OverviewProxyService overviewProxyService, @@ -162,7 +159,6 @@ public class AssistManager { mAssistUtils = assistUtils; mAssistDisclosure = new AssistDisclosure(context, new Handler()); mPhoneStateMonitor = phoneStateMonitor; - mHandleController = handleController; mAssistLogger = assistLogger; mOrbController = new AssistOrbController(configurationController, context); @@ -216,9 +212,7 @@ public class AssistManager { } String action = hints.getString(ACTION_KEY); - if (SHOW_ASSIST_HANDLES_ACTION.equals(action)) { - requestAssistHandles(); - } else if (SET_ASSIST_GESTURE_CONSTRAINED_ACTION.equals(action)) { + if (SET_ASSIST_GESTURE_CONSTRAINED_ACTION.equals(action)) { mSysUiState.get() .setFlag( SYSUI_STATE_ASSIST_GESTURE_CONSTRAINED, @@ -249,9 +243,6 @@ public class AssistManager { args = new Bundle(); } int legacyInvocationType = args.getInt(INVOCATION_TYPE_KEY, 0); - if (legacyInvocationType == INVOCATION_TYPE_GESTURE) { - mHandleController.onAssistantGesturePerformed(); - } int legacyDeviceState = mPhoneStateMonitor.getPhoneState(); args.putInt(INVOCATION_PHONE_STATE_KEY, legacyDeviceState); args.putLong(INVOCATION_TIME_MS_KEY, SystemClock.elapsedRealtime()); @@ -278,10 +269,6 @@ public class AssistManager { mUiController.onGestureCompletion(velocity); } - protected void requestAssistHandles() { - mHandleController.onAssistHandlesRequested(); - } - public void hideAssist() { mAssistUtils.hideCurrentSession(); } @@ -362,10 +349,6 @@ public class AssistManager { return mAssistUtils.isSessionRunning(); } - protected AssistHandleBehaviorController getHandleBehaviorController() { - return mHandleController; - } - @Nullable public ComponentName getAssistInfoForUser(int userId) { return mAssistUtils.getAssistComponentForUser(userId); @@ -389,10 +372,6 @@ public class AssistManager { }); } - public long getAssistHandleShowAndGoRemainingDurationMs() { - return mHandleController.getShowAndGoRemainingTimeMs(); - } - /** Returns the logging flags for the given Assistant invocation type. */ public int toLoggingSubType(int invocationType) { return toLoggingSubType(invocationType, mPhoneStateMonitor.getPhoneState()); @@ -409,7 +388,7 @@ public class AssistManager { // Note that this logic will break if the number of Assistant invocation types exceeds 7. // There are currently 5 invocation types, but we will be migrating to the new logging // framework in the next update. - int subType = mHandleController.areHandlesShowing() ? 0 : 1; + int subType = 0; subType |= invocationType << 1; subType |= phoneState << 4; return subType; diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistModule.java b/packages/SystemUI/src/com/android/systemui/assist/AssistModule.java index ef43f87d20e8..f9138b6eac54 100644 --- a/packages/SystemUI/src/com/android/systemui/assist/AssistModule.java +++ b/packages/SystemUI/src/com/android/systemui/assist/AssistModule.java @@ -21,15 +21,10 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.SystemClock; -import androidx.annotation.Nullable; import androidx.slice.Clock; import com.android.internal.app.AssistUtils; import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.navigationbar.NavigationBarController; - -import java.util.EnumMap; -import java.util.Map; import javax.inject.Named; @@ -55,28 +50,6 @@ public abstract class AssistModule { @Provides @SysUISingleton - static Map<AssistHandleBehavior, AssistHandleBehaviorController.BehaviorController> - provideAssistHandleBehaviorControllerMap( - AssistHandleOffBehavior offBehavior, - AssistHandleLikeHomeBehavior likeHomeBehavior, - AssistHandleReminderExpBehavior reminderExpBehavior) { - Map<AssistHandleBehavior, AssistHandleBehaviorController.BehaviorController> map = - new EnumMap<>(AssistHandleBehavior.class); - map.put(AssistHandleBehavior.OFF, offBehavior); - map.put(AssistHandleBehavior.LIKE_HOME, likeHomeBehavior); - map.put(AssistHandleBehavior.REMINDER_EXP, reminderExpBehavior); - return map; - } - - @Provides - @Nullable - static AssistHandleViewController provideAssistHandleViewController( - NavigationBarController navigationBarController) { - return navigationBarController.getAssistHandlerViewController(); - } - - @Provides - @SysUISingleton static AssistUtils provideAssistUtils(Context context) { return new AssistUtils(context); } diff --git a/packages/SystemUI/src/com/android/systemui/assist/ui/DefaultUiController.java b/packages/SystemUI/src/com/android/systemui/assist/ui/DefaultUiController.java index 1d9009668b47..57321454ce96 100644 --- a/packages/SystemUI/src/com/android/systemui/assist/ui/DefaultUiController.java +++ b/packages/SystemUI/src/com/android/systemui/assist/ui/DefaultUiController.java @@ -37,12 +37,10 @@ import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.systemui.Dependency; import com.android.systemui.R; -import com.android.systemui.assist.AssistHandleViewController; import com.android.systemui.assist.AssistLogger; import com.android.systemui.assist.AssistManager; import com.android.systemui.assist.AssistantSessionEvent; import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.navigationbar.NavigationBarController; import java.util.Locale; @@ -113,7 +111,6 @@ public class DefaultUiController implements AssistManager.UiController { if (!mInvocationInProgress) { attach(); mInvocationInProgress = true; - updateAssistHandleVisibility(); } setProgressInternal(type, progress); } @@ -136,7 +133,6 @@ public class DefaultUiController implements AssistManager.UiController { } mInvocationLightsView.hide(); mInvocationInProgress = false; - updateAssistHandleVisibility(); } protected void logInvocationProgressMetrics( @@ -174,17 +170,6 @@ public class DefaultUiController implements AssistManager.UiController { } } - private void updateAssistHandleVisibility() { - NavigationBarController navigationBarController = - Dependency.get(NavigationBarController.class); - AssistHandleViewController controller = - navigationBarController == null - ? null : navigationBarController.getAssistHandlerViewController(); - if (controller != null) { - controller.setAssistHintBlocked(mInvocationInProgress); - } - } - private void attach() { if (!mAttached) { mWindowManager.addView(mRoot, mLayoutParams); diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceToUdfpsView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceToUdfpsView.java new file mode 100644 index 000000000000..197f35ba3e86 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceToUdfpsView.java @@ -0,0 +1,147 @@ +/* + * 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 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; +import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; +import android.util.AttributeSet; +import android.util.Log; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.systemui.R; + +/** + * Manages the layout of an auth dialog for devices with a face sensor and an under-display + * fingerprint sensor (UDFPS). Face authentication is attempted first, followed by fingerprint if + * the initial attempt is unsuccessful. + */ +public class AuthBiometricFaceToUdfpsView extends AuthBiometricFaceView { + private static final String TAG = "BiometricPrompt/AuthBiometricFaceToUdfpsView"; + + protected static class UdfpsIconController extends IconController { + protected UdfpsIconController( + @NonNull Context context, @NonNull ImageView iconView, @NonNull TextView textView) { + super(context, iconView, textView); + } + + @Override + protected void updateState(int lastState, int newState) { + final boolean lastStateIsErrorIcon = + lastState == STATE_ERROR || lastState == STATE_HELP; + + switch (newState) { + case STATE_IDLE: + case STATE_AUTHENTICATING_ANIMATING_IN: + case STATE_AUTHENTICATING: + case STATE_PENDING_CONFIRMATION: + case STATE_AUTHENTICATED: + if (lastStateIsErrorIcon) { + animateOnce(R.drawable.fingerprint_dialog_error_to_fp); + } else { + showStaticDrawable(R.drawable.fingerprint_dialog_fp_to_error); + } + mIconView.setContentDescription(mContext.getString( + R.string.accessibility_fingerprint_dialog_fingerprint_icon)); + break; + + case STATE_ERROR: + case STATE_HELP: + if (!lastStateIsErrorIcon) { + animateOnce(R.drawable.fingerprint_dialog_fp_to_error); + } else { + showStaticDrawable(R.drawable.fingerprint_dialog_error_to_fp); + } + mIconView.setContentDescription(mContext.getString( + R.string.biometric_dialog_try_again)); + break; + + default: + Log.e(TAG, "Unknown biometric dialog state: " + newState); + break; + } + + mState = newState; + } + } + + @BiometricAuthenticator.Modality private int mActiveSensorType = TYPE_FACE; + + @Nullable UdfpsDialogMeasureAdapter mMeasureAdapter; + @Nullable private UdfpsIconController mUdfpsIconController; + + public AuthBiometricFaceToUdfpsView(Context context) { + super(context); + } + + public AuthBiometricFaceToUdfpsView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + void setFingerprintSensorProps(@NonNull FingerprintSensorPropertiesInternal sensorProps) { + if (mMeasureAdapter == null || mMeasureAdapter.getSensorProps() != sensorProps) { + mMeasureAdapter = new UdfpsDialogMeasureAdapter(this, sensorProps); + } + } + + @Override + protected int getDelayAfterAuthenticatedDurationMs() { + return mActiveSensorType == TYPE_FINGERPRINT ? 0 + : super.getDelayAfterAuthenticatedDurationMs(); + } + + @Override + protected boolean supportsManualRetry() { + return false; + } + + @Override + @NonNull + protected IconController getIconController() { + if (mActiveSensorType == TYPE_FINGERPRINT) { + if (!(mIconController instanceof UdfpsIconController)) { + mIconController = new UdfpsIconController(getContext(), mIconView, mIndicatorView); + } + return mIconController; + } + return super.getIconController(); + } + + @Override + public void updateState(int newState) { + if (mState == STATE_HELP || mState == STATE_ERROR) { + mActiveSensorType = TYPE_FINGERPRINT; + setRequireConfirmation(false); + } + super.updateState(newState); + } + + @Override + @NonNull + AuthDialog.LayoutParams onMeasureInternal(int width, int height) { + final AuthDialog.LayoutParams layoutParams = super.onMeasureInternal(width, height); + return mMeasureAdapter != null + ? mMeasureAdapter.onMeasureInternal(width, height, layoutParams) + : layoutParams; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceView.java index 9b09cfd0dba6..4c5ca691939c 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceView.java @@ -16,6 +16,7 @@ package com.android.systemui.biometrics; +import android.annotation.NonNull; import android.content.Context; import android.graphics.drawable.Animatable2; import android.graphics.drawable.AnimatedVectorDrawable; @@ -28,7 +29,6 @@ import android.view.View; import android.widget.ImageView; import android.widget.TextView; -import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.R; public class AuthBiometricFaceView extends AuthBiometricView { @@ -38,15 +38,15 @@ public class AuthBiometricFaceView extends AuthBiometricView { // Delay before dismissing after being authenticated/confirmed. private static final int HIDE_DELAY_MS = 500; - public static class IconController extends Animatable2.AnimationCallback { - Context mContext; - ImageView mIconView; - TextView mTextView; - Handler mHandler; - boolean mLastPulseLightToDark; // false = dark to light, true = light to dark - @BiometricState int mState; + protected static class IconController extends Animatable2.AnimationCallback { + protected Context mContext; + protected ImageView mIconView; + protected TextView mTextView; + protected Handler mHandler; + protected boolean mLastPulseLightToDark; // false = dark to light, true = light to dark + protected @BiometricState int mState; - IconController(Context context, ImageView iconView, TextView textView) { + protected IconController(Context context, ImageView iconView, TextView textView) { mContext = context; mIconView = iconView; mTextView = textView; @@ -54,15 +54,15 @@ public class AuthBiometricFaceView extends AuthBiometricView { showStaticDrawable(R.drawable.face_dialog_pulse_dark_to_light); } - void animateOnce(int iconRes) { + protected void animateOnce(int iconRes) { animateIcon(iconRes, false); } - public void showStaticDrawable(int iconRes) { + protected void showStaticDrawable(int iconRes) { mIconView.setImageDrawable(mContext.getDrawable(iconRes)); } - void animateIcon(int iconRes, boolean repeat) { + protected void animateIcon(int iconRes, boolean repeat) { final AnimatedVectorDrawable icon = (AnimatedVectorDrawable) mContext.getDrawable(iconRes); mIconView.setImageDrawable(icon); @@ -73,12 +73,12 @@ public class AuthBiometricFaceView extends AuthBiometricView { icon.start(); } - void startPulsing() { + protected void startPulsing() { mLastPulseLightToDark = false; animateIcon(R.drawable.face_dialog_pulse_dark_to_light, true); } - void pulseInNextDirection() { + protected void pulseInNextDirection() { int iconRes = mLastPulseLightToDark ? R.drawable.face_dialog_pulse_dark_to_light : R.drawable.face_dialog_pulse_light_to_dark; animateIcon(iconRes, true /* repeat */); @@ -93,7 +93,7 @@ public class AuthBiometricFaceView extends AuthBiometricView { } } - public void updateState(int lastState, int newState) { + protected void updateState(int lastState, int newState) { final boolean lastStateIsErrorIcon = lastState == STATE_ERROR || lastState == STATE_HELP; @@ -138,7 +138,7 @@ public class AuthBiometricFaceView extends AuthBiometricView { } } - @VisibleForTesting IconController mIconController; + protected IconController mIconController; public AuthBiometricFaceView(Context context) { this(context, null); @@ -174,14 +174,21 @@ public class AuthBiometricFaceView extends AuthBiometricView { } @Override - protected void onFinishInflate() { - super.onFinishInflate(); - mIconController = new IconController(mContext, mIconView, mIndicatorView); + protected boolean supportsManualRetry() { + return true; + } + + @NonNull + protected IconController getIconController() { + if (mIconController == null) { + mIconController = new IconController(mContext, mIconView, mIndicatorView); + } + return mIconController; } @Override public void updateState(@BiometricState int newState) { - mIconController.updateState(mState, newState); + getIconController().updateState(mState, newState); if (newState == STATE_AUTHENTICATING_ANIMATING_IN || (newState == STATE_AUTHENTICATING && getSize() == AuthDialog.SIZE_MEDIUM)) { @@ -195,11 +202,13 @@ public class AuthBiometricFaceView extends AuthBiometricView { @Override public void onAuthenticationFailed(String failureReason) { if (getSize() == AuthDialog.SIZE_MEDIUM) { - mTryAgainButton.setVisibility(View.VISIBLE); - mConfirmButton.setVisibility(View.GONE); + if (supportsManualRetry()) { + mTryAgainButton.setVisibility(View.VISIBLE); + mConfirmButton.setVisibility(View.GONE); + } } - // Do this last since wa want to know if the button is being animated (in the case of + // Do this last since we want to know if the button is being animated (in the case of // small -> medium dialog) super.onAuthenticationFailed(failureReason); } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricUdfpsView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricUdfpsView.java index 007080bc8603..376368fbf9d4 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricUdfpsView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricUdfpsView.java @@ -16,33 +16,18 @@ package com.android.systemui.biometrics; -import android.annotation.IdRes; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; -import android.graphics.Insets; -import android.graphics.Rect; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.util.AttributeSet; -import android.util.Log; -import android.view.Surface; -import android.view.View; -import android.view.WindowInsets; -import android.view.WindowManager; -import android.widget.FrameLayout; - -import com.android.internal.annotations.VisibleForTesting; -import com.android.systemui.R; /** * Manages the layout for under-display fingerprint sensors (UDFPS). Ensures that UI elements * do not overlap with */ public class AuthBiometricUdfpsView extends AuthBiometricFingerprintView { - - private static final String TAG = "AuthBiometricUdfpsView"; - - @Nullable private FingerprintSensorPropertiesInternal mSensorProps; + @Nullable private UdfpsDialogMeasureAdapter mMeasureAdapter; public AuthBiometricUdfpsView(Context context) { this(context, null /* attrs */); @@ -52,269 +37,18 @@ public class AuthBiometricUdfpsView extends AuthBiometricFingerprintView { super(context, attrs); } - void setSensorProps(@NonNull FingerprintSensorPropertiesInternal prop) { - mSensorProps = prop; + void setSensorProps(@NonNull FingerprintSensorPropertiesInternal sensorProps) { + if (mMeasureAdapter == null || mMeasureAdapter.getSensorProps() != sensorProps) { + mMeasureAdapter = new UdfpsDialogMeasureAdapter(this, sensorProps); + } } @Override @NonNull AuthDialog.LayoutParams onMeasureInternal(int width, int height) { - final int displayRotation = getDisplay().getRotation(); - switch (displayRotation) { - case Surface.ROTATION_0: - return onMeasureInternalPortrait(width, height); - case Surface.ROTATION_90: - case Surface.ROTATION_270: - return onMeasureInternalLandscape(width, height); - default: - Log.e(TAG, "Unsupported display rotation: " + displayRotation); - return super.onMeasureInternal(width, height); - } - } - - @NonNull - private AuthDialog.LayoutParams onMeasureInternalPortrait(int width, int height) { - // Get the height of the everything below the icon. Currently, that's the indicator and - // button bar. - final int textIndicatorHeight = getViewHeightPx(R.id.indicator); - final int buttonBarHeight = getViewHeightPx(R.id.button_bar); - - // Figure out where the bottom of the sensor anim should be. - // Navbar + dialogMargin + buttonBar + textIndicator + spacerHeight = sensorDistFromBottom - final int dialogMargin = getDialogMarginPx(); - final WindowManager windowManager = getContext().getSystemService(WindowManager.class); - final int displayHeight = getWindowBounds(windowManager).height(); - final Insets navbarInsets = getNavbarInsets(windowManager); - final int bottomSpacerHeight = calculateBottomSpacerHeightForPortrait( - mSensorProps, displayHeight, textIndicatorHeight, buttonBarHeight, - dialogMargin, navbarInsets.bottom); - - // Go through each of the children and do the custom measurement. - int totalHeight = 0; - final int numChildren = getChildCount(); - final int sensorDiameter = mSensorProps.sensorRadius * 2; - for (int i = 0; i < numChildren; i++) { - final View child = getChildAt(i); - if (child.getId() == R.id.biometric_icon_frame) { - final FrameLayout iconFrame = (FrameLayout) child; - final View icon = iconFrame.getChildAt(0); - - // Ensure that the icon is never larger than the sensor. - icon.measure( - MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST), - MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST)); - - // Create a frame that's exactly the height of the sensor circle. - iconFrame.measure( - MeasureSpec.makeMeasureSpec( - child.getLayoutParams().width, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.EXACTLY)); - } else if (child.getId() == R.id.space_above_icon) { - child.measure( - MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec( - child.getLayoutParams().height, MeasureSpec.EXACTLY)); - } else if (child.getId() == R.id.button_bar) { - child.measure( - MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(child.getLayoutParams().height, - MeasureSpec.EXACTLY)); - } else if (child.getId() == R.id.space_below_icon) { - // Set the spacer height so the fingerprint icon is on the physical sensor area - child.measure( - MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(bottomSpacerHeight, MeasureSpec.EXACTLY)); - } else { - child.measure( - MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST)); - } - - if (child.getVisibility() != View.GONE) { - totalHeight += child.getMeasuredHeight(); - } - } - - return new AuthDialog.LayoutParams(width, totalHeight); - } - - @NonNull - private AuthDialog.LayoutParams onMeasureInternalLandscape(int width, int height) { - // Find the spacer height needed to vertically align the icon with the sensor. - final int titleHeight = getViewHeightPx(R.id.title); - final int subtitleHeight = getViewHeightPx(R.id.subtitle); - final int descriptionHeight = getViewHeightPx(R.id.description); - final int topSpacerHeight = getViewHeightPx(R.id.space_above_icon); - final int textIndicatorHeight = getViewHeightPx(R.id.indicator); - final int buttonBarHeight = getViewHeightPx(R.id.button_bar); - final WindowManager windowManager = getContext().getSystemService(WindowManager.class); - final Insets navbarInsets = getNavbarInsets(windowManager); - final int bottomSpacerHeight = calculateBottomSpacerHeightForLandscape(titleHeight, - subtitleHeight, descriptionHeight, topSpacerHeight, textIndicatorHeight, - buttonBarHeight, navbarInsets.bottom); - - // Find the spacer width needed to horizontally align the icon with the sensor. - final int displayWidth = getWindowBounds(windowManager).width(); - final int dialogMargin = getDialogMarginPx(); - final int horizontalInset = navbarInsets.left + navbarInsets.right; - final int horizontalSpacerWidth = calculateHorizontalSpacerWidthForLandscape( - mSensorProps, displayWidth, dialogMargin, horizontalInset); - - final int sensorDiameter = mSensorProps.sensorRadius * 2; - final int remeasuredWidth = sensorDiameter + 2 * horizontalSpacerWidth; - - int remeasuredHeight = 0; - final int numChildren = getChildCount(); - for (int i = 0; i < numChildren; i++) { - final View child = getChildAt(i); - if (child.getId() == R.id.biometric_icon_frame) { - final FrameLayout iconFrame = (FrameLayout) child; - final View icon = iconFrame.getChildAt(0); - - // Ensure that the icon is never larger than the sensor. - icon.measure( - MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST), - MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST)); - - // Create a frame that's exactly the height of the sensor circle. - 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. - 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. - child.measure( - MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(bottomSpacerHeight, MeasureSpec.EXACTLY)); - } else { - // Use the remeasured width for all other child views. - child.measure( - MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST)); - } - - if (child.getVisibility() != View.GONE) { - remeasuredHeight += child.getMeasuredHeight(); - } - } - - return new AuthDialog.LayoutParams(remeasuredWidth, remeasuredHeight); - } - - private int getViewHeightPx(@IdRes int viewId) { - final View view = findViewById(viewId); - return view != null ? view.getMeasuredHeight() : 0; - } - - private int getDialogMarginPx() { - return getResources().getDimensionPixelSize(R.dimen.biometric_dialog_border_padding); - } - - @NonNull - private static Insets getNavbarInsets(@Nullable WindowManager windowManager) { - return windowManager != null && windowManager.getCurrentWindowMetrics() != null - ? windowManager.getCurrentWindowMetrics().getWindowInsets() - .getInsets(WindowInsets.Type.navigationBars()) - : Insets.NONE; - } - - @NonNull - private static Rect getWindowBounds(@Nullable WindowManager windowManager) { - return windowManager != null && windowManager.getCurrentWindowMetrics() != null - ? windowManager.getCurrentWindowMetrics().getBounds() - : new Rect(); - } - - /** - * For devices in portrait orientation where the sensor is too high up, calculates the amount of - * padding necessary to center the biometric icon within the sensor's physical location. - */ - @VisibleForTesting - static int calculateBottomSpacerHeightForPortrait( - @NonNull FingerprintSensorPropertiesInternal sensorProperties, int displayHeightPx, - int textIndicatorHeightPx, int buttonBarHeightPx, int dialogMarginPx, - int navbarBottomInsetPx) { - - final int sensorDistanceFromBottom = displayHeightPx - - sensorProperties.sensorLocationY - - sensorProperties.sensorRadius; - - final int spacerHeight = sensorDistanceFromBottom - - textIndicatorHeightPx - - buttonBarHeightPx - - dialogMarginPx - - navbarBottomInsetPx; - - Log.d(TAG, "Display height: " + displayHeightPx - + ", Distance from bottom: " + sensorDistanceFromBottom - + ", Bottom margin: " + dialogMarginPx - + ", Navbar bottom inset: " + navbarBottomInsetPx - + ", Bottom spacer height (portrait): " + spacerHeight); - - return spacerHeight; - } - - /** - * For devices in landscape orientation where the sensor is too high up, calculates the amount - * of padding necessary to center the biometric icon within the sensor's physical location. - */ - @VisibleForTesting - static int calculateBottomSpacerHeightForLandscape(int titleHeightPx, int subtitleHeightPx, - int descriptionHeightPx, int topSpacerHeightPx, int textIndicatorHeightPx, - int buttonBarHeightPx, int navbarBottomInsetPx) { - - final int dialogHeightAboveIcon = titleHeightPx - + subtitleHeightPx - + descriptionHeightPx - + topSpacerHeightPx; - - final int dialogHeightBelowIcon = textIndicatorHeightPx + buttonBarHeightPx; - - final int bottomSpacerHeight = dialogHeightAboveIcon - - dialogHeightBelowIcon - - navbarBottomInsetPx; - - Log.d(TAG, "Title height: " + titleHeightPx - + ", Subtitle height: " + subtitleHeightPx - + ", Description height: " + descriptionHeightPx - + ", Top spacer height: " + topSpacerHeightPx - + ", Text indicator height: " + textIndicatorHeightPx - + ", Button bar height: " + buttonBarHeightPx - + ", Navbar bottom inset: " + navbarBottomInsetPx - + ", Bottom spacer height (landscape): " + bottomSpacerHeight); - - return bottomSpacerHeight; - } - - /** - * For devices in landscape orientation where the sensor is too left/right, calculates the - * amount of padding necessary to center the biometric icon within the sensor's physical - * location. - */ - @VisibleForTesting - static int calculateHorizontalSpacerWidthForLandscape( - @NonNull FingerprintSensorPropertiesInternal sensorProperties, int displayWidthPx, - int dialogMarginPx, int navbarHorizontalInsetPx) { - - final int sensorDistanceFromEdge = displayWidthPx - - sensorProperties.sensorLocationY - - sensorProperties.sensorRadius; - - final int horizontalPadding = sensorDistanceFromEdge - - dialogMarginPx - - navbarHorizontalInsetPx; - - Log.d(TAG, "Display width: " + displayWidthPx - + ", Distance from edge: " + sensorDistanceFromEdge - + ", Dialog margin: " + dialogMarginPx - + ", Navbar horizontal inset: " + navbarHorizontalInsetPx - + ", Horizontal spacer width (landscape): " + horizontalPadding); - - return horizontalPadding; + final AuthDialog.LayoutParams layoutParams = super.onMeasureInternal(width, height); + return mMeasureAdapter != null + ? mMeasureAdapter.onMeasureInternal(width, height, layoutParams) + : layoutParams; } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java index d59a865e2add..a40af70adb8f 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java @@ -379,7 +379,9 @@ public abstract class AuthBiometricView extends LinearLayout { } else { mNegativeButton.setVisibility(View.VISIBLE); } - mTryAgainButton.setVisibility(View.VISIBLE); + if (supportsManualRetry()) { + mTryAgainButton.setVisibility(View.VISIBLE); + } if (!TextUtils.isEmpty(mSubtitleView.getText())) { mSubtitleView.setVisibility(View.VISIBLE); @@ -462,6 +464,10 @@ public abstract class AuthBiometricView extends LinearLayout { Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this); } + protected boolean supportsManualRetry() { + return false; + } + public void updateState(@BiometricState int newState) { Log.v(TAG, "newState: " + newState); @@ -749,7 +755,9 @@ public abstract class AuthBiometricView extends LinearLayout { for (int i = 0; i < numChildren; i++) { final View child = getChildAt(i); - if (child.getId() == R.id.space_above_icon) { + if (child.getId() == R.id.space_above_icon + || child.getId() == R.id.space_below_icon + || child.getId() == R.id.button_bar) { child.measure( MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(child.getLayoutParams().height, @@ -765,11 +773,6 @@ public abstract class AuthBiometricView extends LinearLayout { child.measure( MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST)); - } else if (child.getId() == R.id.button_bar) { - child.measure( - MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(child.getLayoutParams().height, - MeasureSpec.EXACTLY)); } else { child.measure( MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java index d05e9278762d..4e93f58dd51f 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java @@ -282,8 +282,9 @@ public class AuthContainerView extends LinearLayout mPanelController = mInjector.getPanelController(mContext, mPanelView); // Inflate biometric view only if necessary. + final int sensorCount = config.mSensorIds.length; if (Utils.isBiometricAllowed(mConfig.mPromptInfo)) { - if (config.mSensorIds.length == 1 || config.mSensorIds.length == 2) { + if (sensorCount == 1) { final int singleSensorAuthId = config.mSensorIds[0]; if (Utils.containsSensorId(mFpProps, singleSensorAuthId)) { FingerprintSensorPropertiesInternal sensorProps = null; @@ -314,8 +315,54 @@ public class AuthContainerView extends LinearLayout mBiometricScrollView = null; return; } + } else if (sensorCount == 2) { + int fingerprintSensorId = -1; + int faceSensorId = -1; + for (final int sensorId : config.mSensorIds) { + if (Utils.containsSensorId(mFpProps, sensorId)) { + fingerprintSensorId = sensorId; + continue; + } else if (Utils.containsSensorId(mFaceProps, sensorId)) { + faceSensorId = sensorId; + continue; + } + + if (fingerprintSensorId != -1 && faceSensorId != -1) { + break; + } + } + + if (fingerprintSensorId == -1 || faceSensorId == -1) { + Log.e(TAG, "Missing fingerprint or face for dual-sensor config"); + mBiometricView = null; + mBackgroundView = null; + mBiometricScrollView = null; + return; + } + + FingerprintSensorPropertiesInternal fingerprintSensorProps = null; + for (FingerprintSensorPropertiesInternal prop : mFpProps) { + if (prop.sensorId == fingerprintSensorId) { + fingerprintSensorProps = prop; + break; + } + } + + if (fingerprintSensorProps != null && fingerprintSensorProps.isAnyUdfpsType()) { + final AuthBiometricFaceToUdfpsView faceToUdfpsView = + (AuthBiometricFaceToUdfpsView) factory.inflate( + R.layout.auth_biometric_face_to_udfps_view, null, false); + faceToUdfpsView.setFingerprintSensorProps(fingerprintSensorProps); + mBiometricView = faceToUdfpsView; + } else { + Log.e(TAG, "Fingerprint must be UDFPS for dual-sensor config"); + mBiometricView = null; + mBackgroundView = null; + mBiometricScrollView = null; + return; + } } else { - Log.e(TAG, "Unsupported sensor array, length: " + config.mSensorIds.length); + Log.e(TAG, "Unsupported sensor array, length: " + sensorCount); mBiometricView = null; mBackgroundView = null; mBiometricScrollView = null; @@ -442,14 +489,18 @@ public class AuthContainerView extends LinearLayout 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); diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt index 2e321338999c..1270677ccbc3 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt @@ -60,9 +60,8 @@ class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, at animator.duration = RIPPLE_ANIMATION_DURATION animator.addUpdateListener { animator -> val now = animator.currentPlayTime - val phase = now / 30000f rippleShader.progress = animator.animatedValue as Float - rippleShader.noisePhase = phase + rippleShader.time = now.toFloat() invalidate() } animator.addListener(object : AnimatorListenerAdapter() { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationView.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationView.java index 60fdbab8482c..5647e4366075 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationView.java @@ -32,9 +32,10 @@ import android.widget.FrameLayout; * - optionally can override dozeTimeTick to adjust views for burn-in mitigation */ abstract class UdfpsAnimationView extends FrameLayout { - + // mAlpha takes into consideration the status bar expansion amount to fade out icon when + // the status bar is expanded private int mAlpha; - private boolean mPauseAuth; + boolean mPauseAuth; public UdfpsAnimationView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); @@ -75,7 +76,7 @@ abstract class UdfpsAnimationView extends FrameLayout { getDrawable().setAlpha(calculateAlpha()); } - protected final int calculateAlpha() { + int calculateAlpha() { return mPauseAuth ? mAlpha : 255; } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.java index 2f025f63034e..f4993f46bf1d 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.java @@ -16,10 +16,6 @@ package com.android.systemui.biometrics; -import static com.android.systemui.statusbar.StatusBarState.FULLSCREEN_USER_SWITCHER; -import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; -import static com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED; - import android.annotation.NonNull; import android.graphics.PointF; import android.graphics.RectF; @@ -68,18 +64,13 @@ abstract class UdfpsAnimationViewController<T extends UdfpsAnimationView> @Override protected void onViewAttached() { - mStatusBarStateController.addCallback(mStateListener); - mStateListener.onStateChanged(mStatusBarStateController.getState()); mStatusBar.addExpansionChangedListener(mStatusBarExpansionChangedListener); - mDumpManger.registerDumpable(getDumpTag(), this); } @Override protected void onViewDetached() { - mStatusBarStateController.removeCallback(mStateListener); mStatusBar.removeExpansionChangedListener(mStatusBarExpansionChangedListener); - mDumpManger.unregisterDumpable(getDumpTag()); } @@ -106,9 +97,7 @@ abstract class UdfpsAnimationViewController<T extends UdfpsAnimationView> * authentication. */ boolean shouldPauseAuth() { - return (mNotificationShadeExpanded && mStatusBarState != KEYGUARD) - || mStatusBarState == SHADE_LOCKED - || mStatusBarState == FULLSCREEN_USER_SWITCHER; + return mNotificationShadeExpanded; } /** @@ -188,13 +177,4 @@ abstract class UdfpsAnimationViewController<T extends UdfpsAnimationView> updatePauseAuth(); } }; - - private final StatusBarStateController.StateListener mStateListener = - new StatusBarStateController.StateListener() { - @Override - public void onStateChanged(int newState) { - mStatusBarState = newState; - updatePauseAuth(); - } - }; } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index 078ec9fdfd1c..2bdbf518e203 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -45,12 +45,14 @@ import android.view.VelocityTracker; import android.view.WindowManager; import com.android.internal.annotations.VisibleForTesting; +import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.R; import com.android.systemui.biometrics.HbmTypes.HbmType; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.doze.DozeReceiver; import com.android.systemui.dump.DumpManager; +import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.phone.StatusBar; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; @@ -88,6 +90,8 @@ public class UdfpsController implements DozeReceiver, HbmCallback { @NonNull private final StatusBarKeyguardViewManager mKeyguardViewManager; @NonNull private final DumpManager mDumpManager; @NonNull private final AuthRippleController mAuthRippleController; + @NonNull private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; + @NonNull private final KeyguardViewMediator mKeyguardViewMediator; // Currently the UdfpsController supports a single UDFPS sensor. If devices have multiple // sensors, this, in addition to a lot of the code here, will be updated. @VisibleForTesting final FingerprintSensorPropertiesInternal mSensorProps; @@ -307,7 +311,9 @@ public class UdfpsController implements DozeReceiver, HbmCallback { @NonNull StatusBar statusBar, @NonNull StatusBarKeyguardViewManager statusBarKeyguardViewManager, @NonNull DumpManager dumpManager, - @NonNull AuthRippleController authRippleController) { + @NonNull AuthRippleController authRippleController, + @NonNull KeyguardUpdateMonitor keyguardUpdateMonitor, + @NonNull KeyguardViewMediator keyguardViewMediator) { mContext = context; mInflater = inflater; // The fingerprint manager is queried for UDFPS before this class is constructed, so the @@ -320,6 +326,8 @@ public class UdfpsController implements DozeReceiver, HbmCallback { mKeyguardViewManager = statusBarKeyguardViewManager; mDumpManager = dumpManager; mAuthRippleController = authRippleController; + mKeyguardUpdateMonitor = keyguardUpdateMonitor; + mKeyguardViewMediator = keyguardViewMediator; mSensorProps = findFirstUdfps(); // At least one UDFPS sensor exists @@ -327,8 +335,7 @@ public class UdfpsController implements DozeReceiver, HbmCallback { mStatusBar.setSensorRect(getSensorLocation()); mCoreLayoutParams = new WindowManager.LayoutParams( - // TODO(b/152419866): Use the UDFPS window type when it becomes available. - WindowManager.LayoutParams.TYPE_BOOT_PROGRESS, + WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG, WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE @@ -486,7 +493,10 @@ public class UdfpsController implements DozeReceiver, HbmCallback { mStatusBarStateController, mStatusBar, mKeyguardViewManager, - mDumpManager + mKeyguardUpdateMonitor, + mFgExecutor, + mDumpManager, + mKeyguardViewMediator ); case IUdfpsOverlayController.REASON_AUTH_BP: // note: empty controller, currently shows no visual affordance diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java new file mode 100644 index 000000000000..1ad2b9ca856c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java @@ -0,0 +1,330 @@ +/* + * 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.annotation.IdRes; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.graphics.Insets; +import android.graphics.Rect; +import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; +import android.util.Log; +import android.view.Surface; +import android.view.View; +import android.view.View.MeasureSpec; +import android.view.ViewGroup; +import android.view.WindowInsets; +import android.view.WindowManager; +import android.widget.FrameLayout; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.systemui.R; + +/** + * Adapter that remeasures an auth dialog view to ensure that it matches the location of a physical + * under-display fingerprint sensor (UDFPS). + */ +public class UdfpsDialogMeasureAdapter { + private static final String TAG = "UdfpsDialogMeasurementAdapter"; + + @NonNull private final ViewGroup mView; + @NonNull private final FingerprintSensorPropertiesInternal mSensorProps; + + @Nullable private WindowManager mWindowManager; + + public UdfpsDialogMeasureAdapter( + @NonNull ViewGroup view, @NonNull FingerprintSensorPropertiesInternal sensorProps) { + mView = view; + mSensorProps = sensorProps; + } + + @NonNull + FingerprintSensorPropertiesInternal getSensorProps() { + return mSensorProps; + } + + @NonNull + AuthDialog.LayoutParams onMeasureInternal( + int width, int height, @NonNull AuthDialog.LayoutParams layoutParams) { + + final int displayRotation = mView.getDisplay().getRotation(); + switch (displayRotation) { + case Surface.ROTATION_0: + return onMeasureInternalPortrait(width, height); + case Surface.ROTATION_90: + case Surface.ROTATION_270: + return onMeasureInternalLandscape(width, height); + default: + Log.e(TAG, "Unsupported display rotation: " + displayRotation); + return layoutParams; + } + } + + @NonNull + private AuthDialog.LayoutParams onMeasureInternalPortrait(int width, int height) { + // Get the height of the everything below the icon. Currently, that's the indicator and + // button bar. + final int textIndicatorHeight = getViewHeightPx(R.id.indicator); + final int buttonBarHeight = getViewHeightPx(R.id.button_bar); + + // Figure out where the bottom of the sensor anim should be. + // Navbar + dialogMargin + buttonBar + textIndicator + spacerHeight = sensorDistFromBottom + final int dialogMargin = getDialogMarginPx(); + final int displayHeight = getWindowBounds().height(); + final Insets navbarInsets = getNavbarInsets(); + final int bottomSpacerHeight = calculateBottomSpacerHeightForPortrait( + mSensorProps, displayHeight, textIndicatorHeight, buttonBarHeight, + dialogMargin, navbarInsets.bottom); + + // Go through each of the children and do the custom measurement. + int totalHeight = 0; + final int numChildren = mView.getChildCount(); + final int sensorDiameter = mSensorProps.sensorRadius * 2; + for (int i = 0; i < numChildren; i++) { + final View child = mView.getChildAt(i); + if (child.getId() == R.id.biometric_icon_frame) { + final FrameLayout iconFrame = (FrameLayout) child; + final View icon = iconFrame.getChildAt(0); + + // Ensure that the icon is never larger than the sensor. + icon.measure( + MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST), + MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST)); + + // Create a frame that's exactly the height of the sensor circle. + iconFrame.measure( + MeasureSpec.makeMeasureSpec( + child.getLayoutParams().width, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.EXACTLY)); + } else if (child.getId() == R.id.space_above_icon) { + child.measure( + MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec( + child.getLayoutParams().height, MeasureSpec.EXACTLY)); + } else if (child.getId() == R.id.button_bar) { + child.measure( + MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(child.getLayoutParams().height, + MeasureSpec.EXACTLY)); + } else if (child.getId() == R.id.space_below_icon) { + // Set the spacer height so the fingerprint icon is on the physical sensor area + child.measure( + MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(bottomSpacerHeight, MeasureSpec.EXACTLY)); + } else { + child.measure( + MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST)); + } + + if (child.getVisibility() != View.GONE) { + totalHeight += child.getMeasuredHeight(); + } + } + + return new AuthDialog.LayoutParams(width, totalHeight); + } + + @NonNull + private AuthDialog.LayoutParams onMeasureInternalLandscape(int width, int height) { + // Find the spacer height needed to vertically align the icon with the sensor. + final int titleHeight = getViewHeightPx(R.id.title); + final int subtitleHeight = getViewHeightPx(R.id.subtitle); + final int descriptionHeight = getViewHeightPx(R.id.description); + final int topSpacerHeight = getViewHeightPx(R.id.space_above_icon); + final int textIndicatorHeight = getViewHeightPx(R.id.indicator); + final int buttonBarHeight = getViewHeightPx(R.id.button_bar); + final Insets navbarInsets = getNavbarInsets(); + final int bottomSpacerHeight = calculateBottomSpacerHeightForLandscape(titleHeight, + subtitleHeight, descriptionHeight, topSpacerHeight, textIndicatorHeight, + buttonBarHeight, navbarInsets.bottom); + + // Find the spacer width needed to horizontally align the icon with the sensor. + final int displayWidth = getWindowBounds().width(); + final int dialogMargin = getDialogMarginPx(); + final int horizontalInset = navbarInsets.left + navbarInsets.right; + final int horizontalSpacerWidth = calculateHorizontalSpacerWidthForLandscape( + mSensorProps, displayWidth, dialogMargin, horizontalInset); + + final int sensorDiameter = mSensorProps.sensorRadius * 2; + final int remeasuredWidth = sensorDiameter + 2 * horizontalSpacerWidth; + + int remeasuredHeight = 0; + final int numChildren = mView.getChildCount(); + for (int i = 0; i < numChildren; i++) { + final View child = mView.getChildAt(i); + if (child.getId() == R.id.biometric_icon_frame) { + final FrameLayout iconFrame = (FrameLayout) child; + final View icon = iconFrame.getChildAt(0); + + // Ensure that the icon is never larger than the sensor. + icon.measure( + MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST), + MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST)); + + // Create a frame that's exactly the height of the sensor circle. + 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. + 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. + child.measure( + MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(bottomSpacerHeight, MeasureSpec.EXACTLY)); + } else { + // Use the remeasured width for all other child views. + child.measure( + MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST)); + } + + if (child.getVisibility() != View.GONE) { + remeasuredHeight += child.getMeasuredHeight(); + } + } + + return new AuthDialog.LayoutParams(remeasuredWidth, remeasuredHeight); + } + + private int getViewHeightPx(@IdRes int viewId) { + final View view = mView.findViewById(viewId); + return view != null ? view.getMeasuredHeight() : 0; + } + + private int getDialogMarginPx() { + return mView.getResources().getDimensionPixelSize(R.dimen.biometric_dialog_border_padding); + } + + @NonNull + private Insets getNavbarInsets() { + final WindowManager windowManager = getWindowManager(); + return windowManager != null && windowManager.getCurrentWindowMetrics() != null + ? windowManager.getCurrentWindowMetrics().getWindowInsets() + .getInsets(WindowInsets.Type.navigationBars()) + : Insets.NONE; + } + + @NonNull + private Rect getWindowBounds() { + final WindowManager windowManager = getWindowManager(); + return windowManager != null && windowManager.getCurrentWindowMetrics() != null + ? windowManager.getCurrentWindowMetrics().getBounds() + : new Rect(); + } + + @Nullable + private WindowManager getWindowManager() { + if (mWindowManager == null) { + mWindowManager = mView.getContext().getSystemService(WindowManager.class); + } + return mWindowManager; + } + + /** + * For devices in portrait orientation where the sensor is too high up, calculates the amount of + * padding necessary to center the biometric icon within the sensor's physical location. + */ + @VisibleForTesting + static int calculateBottomSpacerHeightForPortrait( + @NonNull FingerprintSensorPropertiesInternal sensorProperties, int displayHeightPx, + int textIndicatorHeightPx, int buttonBarHeightPx, int dialogMarginPx, + int navbarBottomInsetPx) { + + final int sensorDistanceFromBottom = displayHeightPx + - sensorProperties.sensorLocationY + - sensorProperties.sensorRadius; + + final int spacerHeight = sensorDistanceFromBottom + - textIndicatorHeightPx + - buttonBarHeightPx + - dialogMarginPx + - navbarBottomInsetPx; + + Log.d(TAG, "Display height: " + displayHeightPx + + ", Distance from bottom: " + sensorDistanceFromBottom + + ", Bottom margin: " + dialogMarginPx + + ", Navbar bottom inset: " + navbarBottomInsetPx + + ", Bottom spacer height (portrait): " + spacerHeight); + + return spacerHeight; + } + + /** + * For devices in landscape orientation where the sensor is too high up, calculates the amount + * of padding necessary to center the biometric icon within the sensor's physical location. + */ + @VisibleForTesting + static int calculateBottomSpacerHeightForLandscape(int titleHeightPx, int subtitleHeightPx, + int descriptionHeightPx, int topSpacerHeightPx, int textIndicatorHeightPx, + int buttonBarHeightPx, int navbarBottomInsetPx) { + + final int dialogHeightAboveIcon = titleHeightPx + + subtitleHeightPx + + descriptionHeightPx + + topSpacerHeightPx; + + final int dialogHeightBelowIcon = textIndicatorHeightPx + buttonBarHeightPx; + + final int bottomSpacerHeight = dialogHeightAboveIcon + - dialogHeightBelowIcon + - navbarBottomInsetPx; + + Log.d(TAG, "Title height: " + titleHeightPx + + ", Subtitle height: " + subtitleHeightPx + + ", Description height: " + descriptionHeightPx + + ", Top spacer height: " + topSpacerHeightPx + + ", Text indicator height: " + textIndicatorHeightPx + + ", Button bar height: " + buttonBarHeightPx + + ", Navbar bottom inset: " + navbarBottomInsetPx + + ", Bottom spacer height (landscape): " + bottomSpacerHeight); + + return bottomSpacerHeight; + } + + /** + * For devices in landscape orientation where the sensor is too left/right, calculates the + * amount of padding necessary to center the biometric icon within the sensor's physical + * location. + */ + @VisibleForTesting + static int calculateHorizontalSpacerWidthForLandscape( + @NonNull FingerprintSensorPropertiesInternal sensorProperties, int displayWidthPx, + int dialogMarginPx, int navbarHorizontalInsetPx) { + + final int sensorDistanceFromEdge = displayWidthPx + - sensorProperties.sensorLocationY + - sensorProperties.sensorRadius; + + final int horizontalPadding = sensorDistanceFromEdge + - dialogMarginPx + - navbarHorizontalInsetPx; + + Log.d(TAG, "Display width: " + displayWidthPx + + ", Distance from edge: " + sensorDistanceFromEdge + + ", Dialog margin: " + dialogMarginPx + + ", Navbar horizontal inset: " + navbarHorizontalInsetPx + + ", Horizontal spacer width (landscape): " + horizontalPadding); + + return horizontalPadding; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDrawable.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDrawable.java index 18f54166ad28..55ed5aaff958 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDrawable.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDrawable.java @@ -18,9 +18,13 @@ package com.android.systemui.biometrics; import android.content.Context; import android.graphics.ColorFilter; +import android.graphics.Paint; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.Drawable; +import android.graphics.drawable.ShapeDrawable; +import android.graphics.drawable.shapes.PathShape; +import android.util.PathParser; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -32,15 +36,30 @@ import com.android.systemui.R; * sensor area. */ public abstract class UdfpsDrawable extends Drawable { - @NonNull protected final Context mContext; - @NonNull protected final Drawable mFingerprintDrawable; + static final float DEFAULT_STROKE_WIDTH = 3f; + + @NonNull final Context mContext; + @NonNull final ShapeDrawable mFingerprintDrawable; + private final Paint mPaint; private boolean mIlluminationShowing; int mAlpha = 255; // 0 - 255 public UdfpsDrawable(@NonNull Context context) { mContext = context; - mFingerprintDrawable = context.getResources().getDrawable(R.drawable.ic_fingerprint, null); + final String fpPath = context.getResources().getString(R.string.config_udfpsIcon); + mFingerprintDrawable = new ShapeDrawable( + new PathShape(PathParser.createPathFromPathData(fpPath), 72, 72)); mFingerprintDrawable.mutate(); + + mPaint = mFingerprintDrawable.getPaint(); + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setStrokeCap(Paint.Cap.ROUND); + setStrokeWidth(DEFAULT_STROKE_WIDTH); + } + + void setStrokeWidth(float strokeWidth) { + mPaint.setStrokeWidth(strokeWidth); + invalidateSelf(); } /** diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardDrawable.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardDrawable.java index 12c15a0882f9..71ed3f8c9b0a 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardDrawable.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardDrawable.java @@ -18,6 +18,7 @@ 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; @@ -27,6 +28,7 @@ import androidx.annotation.NonNull; import com.android.internal.graphics.ColorUtils; import com.android.settingslib.Utils; +import com.android.systemui.Interpolators; import com.android.systemui.R; import com.android.systemui.doze.DozeReceiver; @@ -37,6 +39,7 @@ 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; @@ -48,22 +51,31 @@ public class UdfpsKeyguardDrawable extends UdfpsDrawable implements DozeReceiver 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; - // TODO: move burn-in to view 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.wallpaperTextColor); mAmbientDisplayColor = Color.WHITE; - updateAodPositionAndColor(); + + updateIcon(); } - private void updateAodPositionAndColor() { + private void updateIcon() { mBurnInOffsetX = MathUtils.lerp(0f, getBurnInOffset(mMaxBurnInOffsetX * 2, true /* xAxis */) - mMaxBurnInOffsetX, @@ -75,12 +87,14 @@ public class UdfpsKeyguardDrawable extends UdfpsDrawable implements DozeReceiver mFingerprintDrawable.setTint(ColorUtils.blendARGB(mLockScreenColor, mAmbientDisplayColor, mInterpolatedDarkAmount)); + setStrokeWidth(MathUtils.lerp(DEFAULT_STROKE_WIDTH, DEFAULT_AOD_STROKE_WIDTH, + mInterpolatedDarkAmount)); invalidateSelf(); } @Override public void dozeTimeTick() { - updateAodPositionAndColor(); + updateIcon(); } @Override @@ -88,17 +102,25 @@ public class UdfpsKeyguardDrawable extends UdfpsDrawable implements DozeReceiver 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; - updateAodPositionAndColor(); + updateIcon(); } void setLockScreenColor(int color) { if (mLockScreenColor == color) return; mLockScreenColor = color; - updateAodPositionAndColor(); + updateIcon(); } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardView.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardView.java index e2748437c0c6..fc906f2137f2 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardView.java @@ -85,7 +85,6 @@ public class UdfpsKeyguardView extends UdfpsAnimationView { @Override public boolean dozeTimeTick() { - // TODO: burnin mFingerprintDrawable.dozeTimeTick(); return true; } @@ -98,15 +97,23 @@ public class UdfpsKeyguardView extends UdfpsAnimationView { } } + @Override + int calculateAlpha() { + return mPauseAuth ? 0 : 255; + } + void onDozeAmountChanged(float linear, float eased) { mFingerprintDrawable.onDozeAmountChanged(linear, eased); - invalidate(); + } + + void animateHint() { + mFingerprintDrawable.animateHint(); } /** * Animates in the bg protection circle behind the fp icon to highlight the icon. */ - void animateHighlightFp() { + void animateUdfpsBouncer() { if (mBgProtection.getVisibility() == View.VISIBLE && mBgProtection.getAlpha() == 1f) { // already fully highlighted, don't re-animate return; @@ -151,10 +158,14 @@ public class UdfpsKeyguardView extends UdfpsAnimationView { return mStatusBarState == StatusBarState.SHADE_LOCKED; } + boolean isHome() { + return mStatusBarState == StatusBarState.SHADE; + } + /** * Animates out the bg protection circle behind the fp icon to unhighlight the icon. */ - void animateUnhighlightFp(@Nullable Runnable onEndAnimation) { + void animateAwayUdfpsBouncer(@Nullable Runnable onEndAnimation) { if (mBgProtection.getVisibility() == View.GONE) { // already hidden return; diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java index 1f652dbfdaf2..dc0c685bf01e 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java @@ -16,33 +16,63 @@ package com.android.systemui.biometrics; +import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; + import android.annotation.NonNull; +import android.hardware.biometrics.BiometricSourceType; + +import androidx.annotation.Nullable; +import com.android.keyguard.KeyguardUpdateMonitor; +import com.android.keyguard.KeyguardUpdateMonitorCallback; import com.android.systemui.dump.DumpManager; +import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.phone.StatusBar; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; +import com.android.systemui.util.concurrency.DelayableExecutor; import java.io.FileDescriptor; import java.io.PrintWriter; /** * Class that coordinates non-HBM animations during keyguard authentication. + * + * Highlights the udfps icon when: + * - Face authentication has failed + * - Face authentication has been run for > 2 seconds */ public class UdfpsKeyguardViewController extends UdfpsAnimationViewController<UdfpsKeyguardView> { + private static final long AFTER_FACE_AUTH_HINT_DELAY = 2000; + @NonNull private final StatusBarKeyguardViewManager mKeyguardViewManager; + @NonNull private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; + @NonNull private final DelayableExecutor mExecutor; + @NonNull private final KeyguardViewMediator mKeyguardViewMediator; - private boolean mForceShow; + @Nullable private Runnable mCancelRunnable; + private boolean mShowBouncer; private boolean mQsExpanded; + private boolean mFaceDetectRunning; + private boolean mHintShown; + private boolean mTransitioningFromHome; + private int mStatusBarState; protected UdfpsKeyguardViewController( @NonNull UdfpsKeyguardView view, @NonNull StatusBarStateController statusBarStateController, @NonNull StatusBar statusBar, @NonNull StatusBarKeyguardViewManager statusBarKeyguardViewManager, - @NonNull DumpManager dumpManager) { + @NonNull KeyguardUpdateMonitor keyguardUpdateMonitor, + @NonNull DelayableExecutor mainDelayableExecutor, + @NonNull DumpManager dumpManager, + @NonNull KeyguardViewMediator keyguardViewMediator) { super(view, statusBarStateController, statusBar, dumpManager); mKeyguardViewManager = statusBarKeyguardViewManager; + mKeyguardUpdateMonitor = keyguardUpdateMonitor; + mExecutor = mainDelayableExecutor; + mKeyguardViewMediator = keyguardViewMediator; } @Override @@ -53,6 +83,9 @@ public class UdfpsKeyguardViewController extends UdfpsAnimationViewController<Ud @Override protected void onViewAttached() { super.onViewAttached(); + mHintShown = false; + mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback); + updateFaceDetectRunning(mKeyguardUpdateMonitor.isFaceDetectionRunning()); final float dozeAmount = mStatusBarStateController.getDozeAmount(); mStatusBarStateController.addCallback(mStateListener); @@ -64,90 +97,177 @@ public class UdfpsKeyguardViewController extends UdfpsAnimationViewController<Ud @Override protected void onViewDetached() { super.onViewDetached(); + mKeyguardUpdateMonitor.removeCallback(mKeyguardUpdateMonitorCallback); + mFaceDetectRunning = false; + mStatusBarStateController.removeCallback(mStateListener); - mAlternateAuthInterceptor.resetForceShow(); + mAlternateAuthInterceptor.hideAlternateAuthBouncer(); mKeyguardViewManager.setAlternateAuthInterceptor(null); + mTransitioningFromHome = false; + + if (mCancelRunnable != null) { + mCancelRunnable.run(); + mCancelRunnable = null; + } } @Override public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { super.dump(fd, pw, args); - pw.println("mForceShow=" + mForceShow); + pw.println("mShowBouncer=" + mShowBouncer); + pw.println("mFaceDetectRunning=" + mFaceDetectRunning); + pw.println("mTransitioningFromHomeToKeyguard=" + mTransitioningFromHome); } /** - * Overrides non-force show logic in shouldPauseAuth to still auth. + * Overrides non-bouncer show logic in shouldPauseAuth to still auth. */ - private void forceShow(boolean forceShow) { - if (mForceShow == forceShow) { + private void showBouncer(boolean show) { + if (mShowBouncer == show) { return; } - mForceShow = forceShow; + mShowBouncer = show; updatePauseAuth(); - if (mForceShow) { - mView.animateHighlightFp(); + if (mShowBouncer) { + mView.animateUdfpsBouncer(); } else { - mView.animateUnhighlightFp(() -> mKeyguardViewManager.cancelPostAuthActions()); + mView.animateAwayUdfpsBouncer(() -> mKeyguardViewManager.cancelPostAuthActions()); } } /** * Returns true if the fingerprint manager is running but we want to temporarily pause * authentication. On the keyguard, we may want to show udfps when the shade - * is expanded, so this can be overridden with the forceShow method. + * is expanded, so this can be overridden with the showBouncer method. */ public boolean shouldPauseAuth() { - if (mForceShow) { + if (mShowBouncer) { return false; } + if (mStatusBarState != KEYGUARD) { + return true; + } + + if (mTransitioningFromHome && mKeyguardViewMediator.isAnimatingScreenOff()) { + return true; + } + if (mQsExpanded) { return true; } - return super.shouldPauseAuth(); + return false; + } + + private void cancelDelayedHint() { + if (mCancelRunnable != null) { + mCancelRunnable.run(); + mCancelRunnable = null; + } + } + + private void updateFaceDetectRunning(boolean running) { + if (mFaceDetectRunning == running) { + return; + } + + // show udfps hint a few seconds after face auth started running + if (!mFaceDetectRunning && running && !mHintShown && mCancelRunnable == null) { + // Face detect started running, show udfps hint after a delay + mCancelRunnable = mExecutor.executeDelayed(() -> showHint(false), + AFTER_FACE_AUTH_HINT_DELAY); + } + + mFaceDetectRunning = running; + } + + private void showHint(boolean forceShow) { + cancelDelayedHint(); + if (!mHintShown || forceShow) { + mHintShown = true; + mView.animateHint(); + } } private final StatusBarStateController.StateListener mStateListener = new StatusBarStateController.StateListener() { @Override public void onDozeAmountChanged(float linear, float eased) { + if (linear != 0) showBouncer(false); mView.onDozeAmountChanged(linear, eased); - if (linear != 0) forceShow(false); + if (linear == 1f) { + // transition has finished + mTransitioningFromHome = false; + } + updatePauseAuth(); + } + + @Override + public void onStatePreChange(int oldState, int newState) { + mTransitioningFromHome = oldState == StatusBarState.SHADE + && newState == StatusBarState.KEYGUARD; + updatePauseAuth(); } @Override public void onStateChanged(int statusBarState) { + mStatusBarState = statusBarState; mView.setStatusBarState(statusBarState); } }; + private final KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback = + new KeyguardUpdateMonitorCallback() { + public void onBiometricRunningStateChanged(boolean running, + BiometricSourceType biometricSourceType) { + if (biometricSourceType == BiometricSourceType.FACE) { + updateFaceDetectRunning(running); + } + } + + public void onBiometricAuthFailed(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) { + // cancel delayed hint if face auth succeeded + cancelDelayedHint(); + } + } + }; + private final StatusBarKeyguardViewManager.AlternateAuthInterceptor mAlternateAuthInterceptor = new StatusBarKeyguardViewManager.AlternateAuthInterceptor() { @Override - public boolean showAlternativeAuthMethod() { - if (mForceShow) { + public boolean showAlternateAuthBouncer() { + if (mShowBouncer) { return false; } - forceShow(true); + showBouncer(true); return true; } @Override - public boolean resetForceShow() { - if (!mForceShow) { + public boolean hideAlternateAuthBouncer() { + if (!mShowBouncer) { return false; } - forceShow(false); + showBouncer(false); return true; } @Override - public boolean isShowingAlternateAuth() { - return mForceShow; + public boolean isShowingAlternateAuthBouncer() { + return mShowBouncer; } @Override diff --git a/packages/SystemUI/src/com/android/systemui/camera/CameraIntents.kt b/packages/SystemUI/src/com/android/systemui/camera/CameraIntents.kt new file mode 100644 index 000000000000..f8a20023e47a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/camera/CameraIntents.kt @@ -0,0 +1,71 @@ +/* + * 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.camera + +import android.content.Context +import android.content.Intent +import android.provider.MediaStore +import android.text.TextUtils + +import com.android.systemui.R + +class CameraIntents { + companion object { + val DEFAULT_SECURE_CAMERA_INTENT_ACTION = + MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE + val DEFAULT_INSECURE_CAMERA_INTENT_ACTION = + MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA + + @JvmStatic + fun getOverrideCameraPackage(context: Context): String? { + context.resources.getString(R.string.config_cameraGesturePackage)?.let { + if (!TextUtils.isEmpty(it)) { + return it + } + } + return null + } + + @JvmStatic + fun getInsecureCameraIntent(context: Context): Intent { + val intent = Intent(DEFAULT_INSECURE_CAMERA_INTENT_ACTION) + getOverrideCameraPackage(context)?.let { + intent.setPackage(it) + } + return intent + } + + @JvmStatic + fun getSecureCameraIntent(context: Context): Intent { + val intent = Intent(DEFAULT_SECURE_CAMERA_INTENT_ACTION) + getOverrideCameraPackage(context)?.let { + intent.setPackage(it) + } + return intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) + } + + @JvmStatic + fun isSecureCameraIntent(intent: Intent?): Boolean { + return intent?.getAction()?.equals(DEFAULT_SECURE_CAMERA_INTENT_ACTION) ?: false + } + + @JvmStatic + fun isInsecureCameraIntent(intent: Intent?): Boolean { + return intent?.getAction()?.equals(DEFAULT_INSECURE_CAMERA_INTENT_ACTION) ?: false + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingLayout.java b/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingLayout.java index ce0b51489490..5cc8299acb94 100644 --- a/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingLayout.java +++ b/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingLayout.java @@ -42,7 +42,7 @@ import java.text.NumberFormat; */ public class WirelessChargingLayout extends FrameLayout { public static final int UNKNOWN_BATTERY_LEVEL = -1; - private static final long RIPPLE_ANIMATION_DURATION = 2000; + private static final long RIPPLE_ANIMATION_DURATION = 1133; private ChargingRippleView mRippleView; public WirelessChargingLayout(Context context) { diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollector.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollector.java index bb037202d985..3871248eccd5 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollector.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollector.java @@ -112,9 +112,21 @@ public interface FalsingCollector { /** */ void onBouncerHidden(); - /** */ + /** + * Call this to record a MotionEvent in the {@link com.android.systemui.plugins.FalsingManager}. + * + * Be sure to call {@link #onMotionEventComplete()} after the rest of SystemUI is done with the + * MotionEvent. + */ void onTouchEvent(MotionEvent ev); + /** + * Call this once SystemUI has completed all processing of a given MotionEvent. + * + * See {@link #onTouchEvent(MotionEvent)}. + */ + void onMotionEventComplete(); + /** */ void avoidGesture(); diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorFake.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorFake.java index 939b45a2b4a5..28aac051c66d 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorFake.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorFake.java @@ -147,6 +147,10 @@ public class FalsingCollectorFake implements FalsingCollector { } @Override + public void onMotionEventComplete() { + } + + @Override public void avoidGesture() { } diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java index cf6169703dca..aaea9ce98359 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java @@ -275,6 +275,11 @@ class FalsingCollectorImpl implements FalsingCollector { } @Override + public void onMotionEventComplete() { + mFalsingDataProvider.onMotionEventComplete(); + } + + @Override public void avoidGesture() { mAvoidGesture = true; if (mPendingDownEvent != null) { diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java index 1aaa139eb7ce..2e60a65c3bd1 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java @@ -81,8 +81,8 @@ public class FalsingDataProvider { } if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) { + // Ensure prior gesture was completed. May be a no-op. completePriorGesture(); - mRecentMotionEvents = new TimeLimitedMotionEventBuffer(MOTION_EVENT_AGE_MS); } mRecentMotionEvents.addAll(motionEvents); @@ -100,12 +100,23 @@ public class FalsingDataProvider { mDirty = true; } + void onMotionEventComplete() { + if (mRecentMotionEvents.isEmpty()) { + return; + } + int action = mRecentMotionEvents.get(mRecentMotionEvents.size() - 1).getActionMasked(); + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { + completePriorGesture(); + } + } + private void completePriorGesture() { if (!mRecentMotionEvents.isEmpty()) { mGestureFinalizedListeners.forEach(listener -> listener.onGestureFinalized( mRecentMotionEvents.get(mRecentMotionEvents.size() - 1).getEventTime())); mPriorMotionEvents = mRecentMotionEvents; + mRecentMotionEvents = new TimeLimitedMotionEventBuffer(MOTION_EVENT_AGE_MS); } } diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt index 96f207215f71..8c3ef68f0bde 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt @@ -44,8 +44,9 @@ import com.android.systemui.controls.ui.ControlsUiController import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dump.DumpManager -import com.android.systemui.globalactions.GlobalActionsDialog import com.android.systemui.settings.UserTracker +import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl.Companion.PREFS_CONTROLS_FILE +import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl.Companion.PREFS_CONTROLS_SEEDING_COMPLETED import com.android.systemui.util.concurrency.DelayableExecutor import java.io.FileDescriptor import java.io.PrintWriter @@ -198,11 +199,11 @@ class ControlsControllerImpl @Inject constructor ( // When a component is uninstalled, allow seeding to happen again if the user // reinstalls the app val prefs = userStructure.userContext.getSharedPreferences( - GlobalActionsDialog.PREFS_CONTROLS_FILE, Context.MODE_PRIVATE) + PREFS_CONTROLS_FILE, Context.MODE_PRIVATE) val completedSeedingPackageSet = prefs.getStringSet( - GlobalActionsDialog.PREFS_CONTROLS_SEEDING_COMPLETED, mutableSetOf<String>()) + PREFS_CONTROLS_SEEDING_COMPLETED, mutableSetOf<String>()) val servicePackageSet = serviceInfoSet.map { it.packageName } - prefs.edit().putStringSet(GlobalActionsDialog.PREFS_CONTROLS_SEEDING_COMPLETED, + prefs.edit().putStringSet(PREFS_CONTROLS_SEEDING_COMPLETED, completedSeedingPackageSet.intersect(servicePackageSet)).apply() var changed = false diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt index 7dd1d28170b2..6f94943472b1 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt @@ -16,6 +16,7 @@ package com.android.systemui.controls.management +import android.app.ActivityOptions import android.content.ComponentName import android.content.Intent import android.os.Bundle @@ -34,7 +35,6 @@ import com.android.systemui.controls.controller.ControlsControllerImpl import com.android.systemui.controls.controller.StructureInfo import com.android.systemui.controls.ui.ControlsActivity import com.android.systemui.controls.ui.ControlsUiController -import com.android.systemui.globalactions.GlobalActionsComponent import com.android.systemui.settings.CurrentUserTracker import com.android.systemui.util.LifecycleActivity import javax.inject.Inject @@ -45,7 +45,6 @@ import javax.inject.Inject class ControlsEditingActivity @Inject constructor( private val controller: ControlsControllerImpl, private val broadcastDispatcher: BroadcastDispatcher, - private val globalActionsComponent: GlobalActionsComponent, private val customIconCache: CustomIconCache, private val uiController: ControlsUiController ) : LifecycleActivity() { @@ -62,7 +61,6 @@ class ControlsEditingActivity @Inject constructor( private lateinit var model: FavoritesModel private lateinit var subtitle: TextView private lateinit var saveButton: View - private var backToGlobalActions = true private val currentUserTracker = object : CurrentUserTracker(broadcastDispatcher) { private val startingUser = controller.currentUserId @@ -86,11 +84,6 @@ class ControlsEditingActivity @Inject constructor( structure = it } ?: run(this::finish) - backToGlobalActions = intent.getBooleanExtra( - ControlsUiController.BACK_TO_GLOBAL_ACTIONS, - true - ) - bindViews() bindButtons() @@ -109,15 +102,6 @@ class ControlsEditingActivity @Inject constructor( } override fun onBackPressed() { - if (backToGlobalActions) { - globalActionsComponent.handleShowGlobalActionsMenu() - } else { - val i = Intent().apply { - component = ComponentName(applicationContext, ControlsActivity::class.java) - addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK) - } - startActivity(i) - } animateExitAndFinish() } @@ -161,8 +145,12 @@ class ControlsEditingActivity @Inject constructor( setText(R.string.save) setOnClickListener { saveFavorites() + startActivity( + Intent(applicationContext, ControlsActivity::class.java), + ActivityOptions + .makeSceneTransitionAnimation(this@ControlsEditingActivity).toBundle() + ) animateExitAndFinish() - globalActionsComponent.handleShowGlobalActionsMenu() } } } diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt index 309901443393..dca52a9678b9 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt @@ -43,7 +43,6 @@ import com.android.systemui.controls.controller.StructureInfo import com.android.systemui.controls.ui.ControlsActivity import com.android.systemui.controls.ui.ControlsUiController import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.globalactions.GlobalActionsComponent import com.android.systemui.settings.CurrentUserTracker import com.android.systemui.util.LifecycleActivity import java.text.Collator @@ -56,7 +55,6 @@ class ControlsFavoritingActivity @Inject constructor( private val controller: ControlsControllerImpl, private val listingController: ControlsListingController, private val broadcastDispatcher: BroadcastDispatcher, - private val globalActionsComponent: GlobalActionsComponent, private val uiController: ControlsUiController ) : LifecycleActivity() { @@ -92,7 +90,6 @@ class ControlsFavoritingActivity @Inject constructor( private lateinit var comparator: Comparator<StructureContainer> private var cancelLoadRunnable: Runnable? = null private var isPagerLoaded = false - private var backToGlobalActions = true private val currentUserTracker = object : CurrentUserTracker(broadcastDispatcher) { private val startingUser = controller.currentUserId @@ -133,11 +130,6 @@ class ControlsFavoritingActivity @Inject constructor( component = intent.getParcelableExtra<ComponentName>(Intent.EXTRA_COMPONENT_NAME) fromProviderSelector = intent.getBooleanExtra(EXTRA_FROM_PROVIDER_SELECTOR, false) - backToGlobalActions = intent.getBooleanExtra( - ControlsUiController.BACK_TO_GLOBAL_ACTIONS, - true - ) - bindViews() } @@ -311,13 +303,6 @@ class ControlsFavoritingActivity @Inject constructor( private fun bindButtons() { otherAppsButton = requireViewById<Button>(R.id.other_apps).apply { setOnClickListener { - val i = Intent().apply { - component = ComponentName(context, ControlsProviderSelectorActivity::class.java) - putExtra( - ControlsUiController.BACK_TO_GLOBAL_ACTIONS, - backToGlobalActions - ) - } if (doneButton.isEnabled) { // The user has made changes Toast.makeText( @@ -326,8 +311,11 @@ class ControlsFavoritingActivity @Inject constructor( Toast.LENGTH_SHORT ).show() } - startActivity(i, ActivityOptions - .makeSceneTransitionAnimation(this@ControlsFavoritingActivity).toBundle()) + startActivity( + Intent(context, ControlsProviderSelectorActivity::class.java), + ActivityOptions + .makeSceneTransitionAnimation(this@ControlsFavoritingActivity).toBundle() + ) animateExitAndFinish() } } @@ -349,15 +337,10 @@ class ControlsFavoritingActivity @Inject constructor( } private fun openControlsOrigin() { - if (backToGlobalActions) { - globalActionsComponent.handleShowGlobalActionsMenu() - } else { - val i = Intent().apply { - component = ComponentName(applicationContext, ControlsActivity::class.java) - addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK) - } - startActivity(i) - } + startActivity( + Intent(applicationContext, ControlsActivity::class.java), + ActivityOptions.makeSceneTransitionAnimation(this).toBundle() + ) } override fun onPause() { diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt index fa1c41f01e6c..cba3dabc4ff4 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt @@ -36,7 +36,6 @@ import com.android.systemui.controls.ui.ControlsActivity import com.android.systemui.controls.ui.ControlsUiController import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.globalactions.GlobalActionsComponent import com.android.systemui.settings.CurrentUserTracker import com.android.systemui.util.LifecycleActivity import java.util.concurrent.Executor @@ -50,7 +49,6 @@ class ControlsProviderSelectorActivity @Inject constructor( @Background private val backExecutor: Executor, private val listingController: ControlsListingController, private val controlsController: ControlsController, - private val globalActionsComponent: GlobalActionsComponent, private val broadcastDispatcher: BroadcastDispatcher, private val uiController: ControlsUiController ) : LifecycleActivity() { @@ -59,7 +57,6 @@ class ControlsProviderSelectorActivity @Inject constructor( private const val TAG = "ControlsProviderSelectorActivity" } - private var backToGlobalActions = true private lateinit var recyclerView: RecyclerView private val currentUserTracker = object : CurrentUserTracker(broadcastDispatcher) { private val startingUser = listingController.currentUserId @@ -105,23 +102,13 @@ class ControlsProviderSelectorActivity @Inject constructor( } } requireViewById<View>(R.id.done).visibility = View.GONE - - backToGlobalActions = intent.getBooleanExtra( - ControlsUiController.BACK_TO_GLOBAL_ACTIONS, - true - ) } override fun onBackPressed() { - if (backToGlobalActions) { - globalActionsComponent.handleShowGlobalActionsMenu() - } else { - val i = Intent().apply { - component = ComponentName(applicationContext, ControlsActivity::class.java) - addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK) - } - startActivity(i) + val i = Intent().apply { + component = ComponentName(applicationContext, ControlsActivity::class.java) } + startActivity(i, ActivityOptions.makeSceneTransitionAnimation(this).toBundle()) animateExitAndFinish() } @@ -169,10 +156,6 @@ class ControlsProviderSelectorActivity @Inject constructor( listingController.getAppLabel(it)) putExtra(Intent.EXTRA_COMPONENT_NAME, it) putExtra(ControlsFavoritingActivity.EXTRA_FROM_PROVIDER_SELECTOR, true) - putExtra( - ControlsUiController.BACK_TO_GLOBAL_ACTIONS, - backToGlobalActions - ) } startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(this).toBundle()) animateExitAndFinish() diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinator.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinator.kt index 0db15e83fc93..8029ba844850 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinator.kt @@ -26,7 +26,7 @@ import android.service.controls.Control interface ControlActionCoordinator { // If launched from an Activity, continue within this stack - var activityContext: Context? + var activityContext: Context /** * Close any dialogs which may have been open diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt index 477c22068851..d3d6e03c9bc7 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt @@ -63,7 +63,7 @@ class ControlActionCoordinatorImpl @Inject constructor( private var actionsInProgress = mutableSetOf<String>() private val isLocked: Boolean get() = !keyguardStateController.isUnlocked() - override var activityContext: Context? = null + override lateinit var activityContext: Context companion object { private const val RESPONSE_TIMEOUT_IN_MILLIS = 3000L @@ -153,14 +153,9 @@ class ControlActionCoordinatorImpl @Inject constructor( // pending actions will only run after the control state has been refreshed pendingAction = action } - val wasLocked = isLocked activityStarter.dismissKeyguardThenExecute({ Log.d(ControlsUiController.TAG, "Device unlocked, invoking controls action") - if (wasLocked && activityContext == null) { - globalActionsComponent.handleShowGlobalActionsMenu() - } else { - action.invoke() - } + action.invoke() true }, { pendingAction = null }, true /* afterKeyguardGone */) } else { diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt index a35b792ac7f1..c241c083a0a3 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt @@ -41,6 +41,14 @@ class ControlsActivity @Inject constructor( setContentView(R.layout.controls_fullscreen) + getLifecycle().addObserver( + ControlsAnimations.observerForAnimations( + requireViewById<ViewGroup>(R.id.control_detail_root), + window, + intent + ) + ) + requireViewById<ViewGroup>(R.id.control_detail_root).apply { setOnApplyWindowInsetsListener { v: View, insets: WindowInsets -> @@ -61,7 +69,7 @@ class ControlsActivity @Inject constructor( parent = requireViewById<ViewGroup>(R.id.global_actions_controls) parent.alpha = 0f - uiController.show(parent, { animateExitAndFinish() }, this) + uiController.show(parent, { finish() }, this) } override fun onResume() { @@ -71,7 +79,7 @@ class ControlsActivity @Inject constructor( } override fun onBackPressed() { - animateExitAndFinish() + finish() } override fun onStop() { @@ -79,8 +87,4 @@ class ControlsActivity @Inject constructor( uiController.hide() } - - private fun animateExitAndFinish() { - ControlsAnimations.exitAnimation(parent, { finish() }).start() - } } diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt index f86948ee8957..ac13aadeeb40 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt @@ -28,10 +28,9 @@ interface ControlsUiController { companion object { public const val TAG = "ControlsUiController" public const val EXTRA_ANIMATE = "extra_animate" - public const val BACK_TO_GLOBAL_ACTIONS = "back_to_global_actions" } - fun show(parent: ViewGroup, onDismiss: Runnable, activityContext: Context?) + fun show(parent: ViewGroup, onDismiss: Runnable, activityContext: Context) fun hide() /** diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt index d08882b1dbd2..954bfb3ff891 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt @@ -19,6 +19,8 @@ package com.android.systemui.controls.ui import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.ObjectAnimator +import android.app.Activity +import android.app.ActivityOptions import android.content.ComponentName import android.content.Context import android.content.Intent @@ -129,6 +131,7 @@ class ControlsUiControllerImpl @Inject constructor ( override val available: Boolean get() = controlsController.get().available + private lateinit var activityContext: Context private lateinit var listingCallback: ControlsListingController.ControlsListingCallback private fun createCallback( @@ -153,11 +156,12 @@ class ControlsUiControllerImpl @Inject constructor ( override fun show( parent: ViewGroup, onDismiss: Runnable, - activityContext: Context? + activityContext: Context ) { Log.d(ControlsUiController.TAG, "show()") this.parent = parent this.onDismiss = onDismiss + this.activityContext = activityContext hidden = false retainCache = false @@ -198,7 +202,7 @@ class ControlsUiControllerImpl @Inject constructor ( controlViewsById.clear() controlsById.clear() - show(parent, onDismiss, controlActionCoordinator.activityContext) + show(parent, onDismiss, activityContext) val showAnim = ObjectAnimator.ofFloat(parent, "alpha", 0.0f, 1.0f) showAnim.setInterpolator(DecelerateInterpolator(1.0f)) showAnim.setDuration(FADE_IN_MILLIS) @@ -220,7 +224,7 @@ class ControlsUiControllerImpl @Inject constructor ( inflater.inflate(R.layout.controls_no_favorites, parent, true) val viewGroup = parent.requireViewById(R.id.controls_no_favorites_group) as ViewGroup - viewGroup.setOnClickListener { v: View -> startProviderSelectorActivity(v.context) } + viewGroup.setOnClickListener { _: View -> startProviderSelectorActivity() } val subtitle = parent.requireViewById<TextView>(R.id.controls_subtitle) subtitle.setText(context.resources.getString(R.string.quick_controls_subtitle)) @@ -234,20 +238,18 @@ class ControlsUiControllerImpl @Inject constructor ( } } - private fun startFavoritingActivity(context: Context, si: StructureInfo) { - startTargetedActivity(context, si, ControlsFavoritingActivity::class.java) + private fun startFavoritingActivity(si: StructureInfo) { + startTargetedActivity(si, ControlsFavoritingActivity::class.java) } - private fun startEditingActivity(context: Context, si: StructureInfo) { - startTargetedActivity(context, si, ControlsEditingActivity::class.java) + private fun startEditingActivity(si: StructureInfo) { + startTargetedActivity(si, ControlsEditingActivity::class.java) } - private fun startTargetedActivity(context: Context, si: StructureInfo, klazz: Class<*>) { - val i = Intent(context, klazz).apply { - addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK) - } + private fun startTargetedActivity(si: StructureInfo, klazz: Class<*>) { + val i = Intent(activityContext, klazz) putIntentExtras(i, si) - startActivity(context, i) + startActivity(i) retainCache = true } @@ -261,27 +263,22 @@ class ControlsUiControllerImpl @Inject constructor ( } } - private fun startProviderSelectorActivity(context: Context) { - val i = Intent(context, ControlsProviderSelectorActivity::class.java).apply { - addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK) - } - startActivity(context, i) + private fun startProviderSelectorActivity() { + startActivity(Intent(activityContext, ControlsProviderSelectorActivity::class.java)) } - private fun startActivity(context: Context, intent: Intent) { + private fun startActivity(intent: Intent) { // Force animations when transitioning from a dialog to an activity intent.putExtra(ControlsUiController.EXTRA_ANIMATE, true) - intent.putExtra( - ControlsUiController.BACK_TO_GLOBAL_ACTIONS, - controlActionCoordinator.activityContext == null - ) - onDismiss.run() - activityStarter.dismissKeyguardThenExecute({ - shadeController.collapsePanel(false) - context.startActivity(intent) - true - }, null, true) + if (keyguardStateController.isUnlocked()) { + activityContext.startActivity( + intent, + ActivityOptions.makeSceneTransitionAnimation(activityContext as Activity).toBundle() + ) + } else { + activityStarter.postStartActivityDismissingKeyguard(intent, 0 /* delay */) + } } private fun showControlsView(items: List<SelectionItem>) { @@ -328,9 +325,9 @@ class ControlsUiControllerImpl @Inject constructor ( ) { when (pos) { // 0: Add Control - 0 -> startFavoritingActivity(view.context, selectedStructure) + 0 -> startFavoritingActivity(selectedStructure) // 1: Edit controls - 1 -> startEditingActivity(view.context, selectedStructure) + 1 -> startEditingActivity(selectedStructure) } dismiss() } @@ -399,15 +396,9 @@ class ControlsUiControllerImpl @Inject constructor ( val inflater = LayoutInflater.from(context) inflater.inflate(R.layout.controls_with_favorites, parent, true) - if (controlActionCoordinator.activityContext == null) { - parent.requireViewById<View>(R.id.controls_spacer).apply { - visibility = View.VISIBLE - } - } else { - parent.requireViewById<ImageView>(R.id.controls_close).apply { - setOnClickListener { _: View -> onDismiss.run() } - visibility = View.VISIBLE - } + parent.requireViewById<ImageView>(R.id.controls_close).apply { + setOnClickListener { _: View -> onDismiss.run() } + visibility = View.VISIBLE } val maxColumns = findMaxColumns() @@ -522,8 +513,6 @@ class ControlsUiControllerImpl @Inject constructor ( override fun hide() { hidden = true - controlActionCoordinator.activityContext = null - closeDialogs(true) controlsController.get().unsubscribe() diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DependencyProvider.java b/packages/SystemUI/src/com/android/systemui/dagger/DependencyProvider.java index 1a729295165c..187caf9c2b2e 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/DependencyProvider.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/DependencyProvider.java @@ -31,7 +31,6 @@ import android.os.HandlerThread; import android.os.Looper; import android.os.ServiceManager; import android.os.UserHandle; -import android.service.quickaccesswallet.QuickAccessWalletClient; import android.view.Choreographer; import android.view.IWindowManager; import android.view.LayoutInflater; @@ -376,11 +375,4 @@ public class DependencyProvider { public ModeSwitchesController providesModeSwitchesController(Context context) { return new ModeSwitchesController(context); } - - /** */ - @Provides - @SysUISingleton - public QuickAccessWalletClient provideQuickAccessWalletClient(Context context) { - return QuickAccessWalletClient.create(context); - } } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java index 1c5715c0296d..fd80d50c2894 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java @@ -59,6 +59,7 @@ import android.permission.PermissionManager; import android.service.dreams.DreamService; import android.service.dreams.IDreamManager; import android.telecom.TelecomManager; +import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; import android.view.IWindowManager; import android.view.ViewConfiguration; @@ -328,6 +329,12 @@ public class FrameworkServicesModule { @Provides @Singleton + static SubscriptionManager provideSubcriptionManager(Context context) { + return context.getSystemService(SubscriptionManager.class); + } + + @Provides + @Singleton @Nullable static TelecomManager provideTelecomManager(Context context) { return context.getSystemService(TelecomManager.class); diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index b67db03a743c..365a102b61dd 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -73,6 +73,7 @@ import com.android.systemui.util.settings.SettingsUtilModule; import com.android.systemui.util.time.SystemClock; import com.android.systemui.util.time.SystemClockImpl; import com.android.systemui.volume.dagger.VolumeModule; +import com.android.systemui.wallet.dagger.WalletModule; import com.android.systemui.wmshell.BubblesManager; import com.android.wm.shell.bubbles.Bubbles; @@ -109,7 +110,8 @@ import dagger.Provides; TunerModule.class, UserModule.class, UtilModule.class, - VolumeModule.class + VolumeModule.class, + WalletModule.class }, subcomponents = { StatusBarComponent.class, diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java index b7f6e2bb965a..5cea31b5a905 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java @@ -81,6 +81,10 @@ public class DozeSensors { private boolean mSettingRegistered; private boolean mListening; private boolean mListeningTouchScreenSensors; + private boolean mListeningProxSensors; + + // whether to only register sensors that use prox when the display state is dozing or off + private boolean mSelectivelyRegisterProxSensors; @VisibleForTesting public enum DozeSensorsUiEvent implements UiEventLogger.UiEventEnum { @@ -112,6 +116,8 @@ public class DozeSensors { mSecureSettings = secureSettings; mCallback = callback; mProximitySensor = proximitySensor; + mSelectivelyRegisterProxSensors = dozeParameters.getSelectivelyRegisterSensorsUsingProx(); + mListeningProxSensors = !mSelectivelyRegisterProxSensors; boolean udfpsEnrolled = authController.isUdfpsEnrolled(KeyguardUpdateMonitor.getCurrentUser()); @@ -131,6 +137,7 @@ public class DozeSensors { DozeLog.REASON_SENSOR_PICKUP, false /* touchCoords */, false /* touchscreen */, false /* ignoresSetting */, + false /* requires prox */, dozeLog), new TriggerSensor( findSensorWithType(config.doubleTapSensorType()), @@ -143,10 +150,13 @@ public class DozeSensors { new TriggerSensor( findSensorWithType(config.tapSensorType()), Settings.Secure.DOZE_TAP_SCREEN_GESTURE, + true /* settingDef */, true /* configured */, DozeLog.REASON_SENSOR_TAP, false /* reports touch coordinates */, true /* touchscreen */, + false /* ignoresSetting */, + dozeParameters.singleTapUsesProx() /* requiresProx */, dozeLog), new TriggerSensor( findSensorWithType(config.longPressSensorType()), @@ -156,6 +166,8 @@ public class DozeSensors { DozeLog.PULSE_REASON_SENSOR_LONG_PRESS, true /* reports touch coordinates */, true /* touchscreen */, + false /* ignoresSetting */, + dozeParameters.longPressUsesProx() /* requiresProx */, dozeLog), new TriggerSensor( findSensorWithType(config.udfpsLongPressSensorType()), @@ -165,6 +177,8 @@ public class DozeSensors { DozeLog.REASON_SENSOR_UDFPS_LONG_PRESS, true /* reports touch coordinates */, true /* touchscreen */, + false /* ignoresSetting */, + dozeParameters.longPressUsesProx(), dozeLog), new PluginSensor( new SensorManagerPlugin.Sensor(TYPE_WAKE_DISPLAY), @@ -255,13 +269,31 @@ public class DozeSensors { } /** + * If sensors should be registered and sending signals. + */ + public void setListening(boolean listen, boolean includeTouchScreenSensors, + boolean lowPowerStateOrOff) { + final boolean shouldRegisterProxSensors = + !mSelectivelyRegisterProxSensors || lowPowerStateOrOff; + if (mListening == listen && mListeningTouchScreenSensors == includeTouchScreenSensors + && mListeningProxSensors == shouldRegisterProxSensors) { + return; + } + mListening = listen; + mListeningTouchScreenSensors = includeTouchScreenSensors; + mListeningProxSensors = shouldRegisterProxSensors; + updateListening(); + } + + /** * Registers/unregisters sensors based on internal state. */ private void updateListening() { boolean anyListening = false; for (TriggerSensor s : mSensors) { boolean listen = mListening - && (!s.mRequiresTouchscreen || mListeningTouchScreenSensors); + && (!s.mRequiresTouchscreen || mListeningTouchScreenSensors) + && (!s.mRequiresProx || mListeningProxSensors); s.setListening(listen); if (listen) { anyListening = true; @@ -337,6 +369,8 @@ public class DozeSensors { public void dump(PrintWriter pw) { pw.println("mListening=" + mListening); pw.println("mListeningTouchScreenSensors=" + mListeningTouchScreenSensors); + pw.println("mSelectivelyRegisterProxSensors=" + mSelectivelyRegisterProxSensors); + pw.println("mListeningProxSensors=" + mListeningProxSensors); IndentingPrintWriter idpw = new IndentingPrintWriter(pw); idpw.increaseIndent(); for (TriggerSensor s : mSensors) { @@ -361,6 +395,7 @@ public class DozeSensors { private final boolean mReportsTouchCoordinates; private final boolean mSettingDefault; private final boolean mRequiresTouchscreen; + private final boolean mRequiresProx; protected boolean mRequested; protected boolean mRegistered; @@ -378,12 +413,14 @@ public class DozeSensors { boolean configured, int pulseReason, boolean reportsTouchCoordinates, boolean requiresTouchscreen, DozeLog dozeLog) { this(sensor, setting, settingDef, configured, pulseReason, reportsTouchCoordinates, - requiresTouchscreen, false /* ignoresSetting */, dozeLog); + requiresTouchscreen, false /* ignoresSetting */, + false /* requiresProx */, dozeLog); } private TriggerSensor(Sensor sensor, String setting, boolean settingDef, boolean configured, int pulseReason, boolean reportsTouchCoordinates, - boolean requiresTouchscreen, boolean ignoresSetting, DozeLog dozeLog) { + boolean requiresTouchscreen, boolean ignoresSetting, boolean requiresProx, + DozeLog dozeLog) { mSensor = sensor; mSetting = setting; mSettingDefault = settingDef; @@ -392,6 +429,7 @@ public class DozeSensors { mReportsTouchCoordinates = reportsTouchCoordinates; mRequiresTouchscreen = requiresTouchscreen; mIgnoresSetting = ignoresSetting; + mRequiresProx = requiresProx; mDozeLog = dozeLog; } diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java index aa66b75325c2..ee559659801b 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java @@ -17,7 +17,6 @@ package com.android.systemui.doze; import android.annotation.Nullable; -import android.app.AlarmManager; import android.app.UiModeManager; import android.content.BroadcastReceiver; import android.content.Context; @@ -42,7 +41,6 @@ import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.systemui.Dependency; import com.android.systemui.biometrics.AuthController; import com.android.systemui.broadcast.BroadcastDispatcher; -import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dock.DockManager; import com.android.systemui.doze.dagger.DozeScope; @@ -101,15 +99,16 @@ public class DozeTriggers implements DozeMachine.Part { private final BroadcastDispatcher mBroadcastDispatcher; private final AuthController mAuthController; private final DelayableExecutor mMainExecutor; - private final DelayableExecutor mBgExecutor; private long mNotificationPulseTime; private boolean mPulsePending; private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class); - private boolean mWantProx; - private boolean mWantSensors; + + /** see {@link #onProximityFar} prox for callback */ + private boolean mWantProxSensor; private boolean mWantTouchScreenSensors; + private boolean mWantSensors; @VisibleForTesting public enum DozingUpdateUiEvent implements UiEventLogger.UiEventEnum { @@ -177,13 +176,13 @@ public class DozeTriggers implements DozeMachine.Part { @Inject public DozeTriggers(Context context, DozeHost dozeHost, - AlarmManager alarmManager, AmbientDisplayConfiguration config, + AmbientDisplayConfiguration config, DozeParameters dozeParameters, AsyncSensorManager sensorManager, WakeLock wakeLock, DockManager dockManager, ProximitySensor proximitySensor, ProximitySensor.ProximityCheck proxCheck, DozeLog dozeLog, BroadcastDispatcher broadcastDispatcher, SecureSettings secureSettings, AuthController authController, - @Main DelayableExecutor mainExecutor, @Background DelayableExecutor bgExecutor) { + @Main DelayableExecutor mainExecutor) { mContext = context; mDozeHost = dozeHost; mConfig = config; @@ -201,7 +200,6 @@ public class DozeTriggers implements DozeMachine.Part { mBroadcastDispatcher = broadcastDispatcher; mAuthController = authController; mMainExecutor = mainExecutor; - mBgExecutor = bgExecutor; } @Override @@ -459,7 +457,7 @@ public class DozeTriggers implements DozeMachine.Part { break; case DOZE: case DOZE_AOD: - mWantProx = newState != DozeMachine.State.DOZE; + mWantProxSensor = newState != DozeMachine.State.DOZE; mWantSensors = true; mWantTouchScreenSensors = true; if (newState == DozeMachine.State.DOZE_AOD && !sWakeDisplaySensorState) { @@ -468,15 +466,15 @@ public class DozeTriggers implements DozeMachine.Part { break; case DOZE_AOD_PAUSED: case DOZE_AOD_PAUSING: - mWantProx = true; + mWantProxSensor = true; break; case DOZE_PULSING: case DOZE_PULSING_BRIGHT: - mWantProx = true; + mWantProxSensor = true; mWantTouchScreenSensors = false; break; case DOZE_AOD_DOCKED: - mWantProx = false; + mWantProxSensor = false; mWantTouchScreenSensors = false; break; case DOZE_PULSE_DONE: @@ -490,7 +488,7 @@ public class DozeTriggers implements DozeMachine.Part { mDozeSensors.setListening(false, false); mDozeSensors.setProxListening(false); mWantSensors = false; - mWantProx = false; + mWantProxSensor = false; mWantTouchScreenSensors = false; break; default: @@ -501,10 +499,10 @@ public class DozeTriggers implements DozeMachine.Part { @Override public void onScreenState(int state) { mDozeSensors.onScreenState(state); - mDozeSensors.setProxListening(mWantProx && (state == Display.STATE_DOZE - || state == Display.STATE_DOZE_SUSPEND - || state == Display.STATE_OFF)); - mDozeSensors.setListening(mWantSensors, mWantTouchScreenSensors); + final boolean lowPowerStateOrOff = state == Display.STATE_DOZE + || state == Display.STATE_DOZE_SUSPEND || state == Display.STATE_OFF; + mDozeSensors.setProxListening(mWantProxSensor && lowPowerStateOrOff); + mDozeSensors.setListening(mWantSensors, mWantTouchScreenSensors, lowPowerStateOrOff); } /** diff --git a/packages/SystemUI/src/com/android/systemui/fragments/FragmentService.java b/packages/SystemUI/src/com/android/systemui/fragments/FragmentService.java index 0673879758ad..37b8a2c953fe 100644 --- a/packages/SystemUI/src/com/android/systemui/fragments/FragmentService.java +++ b/packages/SystemUI/src/com/android/systemui/fragments/FragmentService.java @@ -23,6 +23,7 @@ import android.view.View; import com.android.systemui.Dumpable; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.qs.QSFragment; +import com.android.systemui.statusbar.phone.CollapsedStatusBarFragment; import com.android.systemui.statusbar.policy.ConfigurationController; import java.io.FileDescriptor; @@ -128,6 +129,9 @@ public class FragmentService implements Dumpable { * Inject a QSFragment. */ QSFragment createQSFragment(); + + /** Inject a CollapsedStatusBarFragment. */ + CollapsedStatusBarFragment createCollapsedStatusBarFragment(); } private class FragmentHostState { diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java index 18627189f188..ab1de9528586 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java @@ -15,18 +15,8 @@ package com.android.systemui.globalactions; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; -import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; -import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; -import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_GLOBAL_ACTIONS; -import static android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN; -import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_2BUTTON; -import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST; -import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT; -import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN; -import static com.android.systemui.controls.dagger.ControlsComponent.Visibility.AVAILABLE; -import static com.android.systemui.controls.dagger.ControlsComponent.Visibility.AVAILABLE_AFTER_UNLOCK; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_GLOBAL_ACTIONS_SHOWING; import android.animation.Animator; @@ -34,121 +24,62 @@ import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.annotation.Nullable; -import android.app.ActivityManager; -import android.app.Dialog; import android.app.IActivityManager; import android.app.PendingIntent; -import android.app.StatusBarManager; -import android.app.WallpaperManager; import android.app.admin.DevicePolicyManager; import android.app.trust.TrustManager; -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.content.pm.UserInfo; -import android.content.res.ColorStateList; -import android.content.res.Configuration; import android.content.res.Resources; import android.database.ContentObserver; -import android.graphics.Color; import android.graphics.drawable.Drawable; import android.media.AudioManager; -import android.net.ConnectivityManager; -import android.os.Binder; import android.os.Handler; -import android.os.IBinder; -import android.os.Message; -import android.os.RemoteException; -import android.os.SystemProperties; -import android.os.UserHandle; import android.os.UserManager; import android.os.Vibrator; import android.provider.Settings; import android.service.dreams.IDreamManager; -import android.sysprop.TelephonyProperties; import android.telecom.TelecomManager; -import android.telephony.ServiceState; -import android.telephony.TelephonyCallback; -import android.telephony.TelephonyManager; import android.transition.AutoTransition; import android.transition.TransitionManager; import android.transition.TransitionSet; -import android.util.ArraySet; -import android.util.Log; -import android.view.ContextThemeWrapper; import android.view.IWindowManager; -import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.view.WindowInsets; import android.view.WindowManager; -import android.view.accessibility.AccessibilityEvent; -import android.widget.BaseAdapter; import android.widget.FrameLayout; -import android.widget.ImageView; -import android.widget.ImageView.ScaleType; -import android.widget.LinearLayout; -import android.widget.ListPopupWindow; import android.widget.TextView; -import androidx.annotation.NonNull; -import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleOwner; -import androidx.lifecycle.LifecycleRegistry; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.colorextraction.ColorExtractor; -import com.android.internal.colorextraction.ColorExtractor.GradientColors; -import com.android.internal.colorextraction.drawable.ScrimDrawable; 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.MetricsEvent; import com.android.internal.statusbar.IStatusBarService; -import com.android.internal.util.EmergencyAffordanceManager; -import com.android.internal.util.ScreenshotHelper; import com.android.internal.view.RotationPolicy; import com.android.internal.widget.LockPatternUtils; import com.android.systemui.Interpolators; -import com.android.systemui.MultiListLayout; -import com.android.systemui.MultiListLayout.MultiListAdapter; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.colorextraction.SysuiColorExtractor; -import com.android.systemui.controls.ControlsServiceInfo; -import com.android.systemui.controls.controller.ControlsController; -import com.android.systemui.controls.dagger.ControlsComponent; -import com.android.systemui.controls.management.ControlsAnimations; -import com.android.systemui.controls.ui.ControlsUiController; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; 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.settings.UserContextProvider; import com.android.systemui.statusbar.NotificationShadeDepthController; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.telephony.TelephonyListenerManager; -import com.android.systemui.util.EmergencyDialerConstants; import com.android.systemui.util.RingerModeTracker; import com.android.systemui.util.leak.RotationUtils; +import com.android.systemui.util.settings.GlobalSettings; +import com.android.systemui.util.settings.SecureSettings; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; import java.util.concurrent.Executor; import javax.inject.Inject; @@ -157,144 +88,29 @@ import javax.inject.Provider; /** * Helper to show the global actions dialog. Each item is an {@link Action} that may show depending * on whether the keyguard is showing, and whether the device is provisioned. + * This version includes wallet. */ -public class GlobalActionsDialog implements DialogInterface.OnDismissListener, +public class GlobalActionsDialog extends GlobalActionsDialogLite + implements DialogInterface.OnDismissListener, DialogInterface.OnShowListener, ConfigurationController.ConfigurationListener, GlobalActionsPanelPlugin.Callbacks, LifecycleOwner { - public static final String SYSTEM_DIALOG_REASON_KEY = "reason"; - public static final String SYSTEM_DIALOG_REASON_GLOBAL_ACTIONS = "globalactions"; - public static final String SYSTEM_DIALOG_REASON_DREAM = "dream"; - private static final String TAG = "GlobalActionsDialog"; - private static final boolean SHOW_SILENT_TOGGLE = true; - - /* Valid settings for global actions keys. - * see config.xml config_globalActionList */ - @VisibleForTesting - static final String GLOBAL_ACTION_KEY_POWER = "power"; - private static final String GLOBAL_ACTION_KEY_AIRPLANE = "airplane"; - static final String GLOBAL_ACTION_KEY_BUGREPORT = "bugreport"; - private static final String GLOBAL_ACTION_KEY_SILENT = "silent"; - private static final String GLOBAL_ACTION_KEY_USERS = "users"; - private static final String GLOBAL_ACTION_KEY_SETTINGS = "settings"; - static final String GLOBAL_ACTION_KEY_LOCKDOWN = "lockdown"; - private static final String GLOBAL_ACTION_KEY_VOICEASSIST = "voiceassist"; - private static final String GLOBAL_ACTION_KEY_ASSIST = "assist"; - static final String GLOBAL_ACTION_KEY_RESTART = "restart"; - private static final String GLOBAL_ACTION_KEY_LOGOUT = "logout"; - static final String GLOBAL_ACTION_KEY_EMERGENCY = "emergency"; - static final String GLOBAL_ACTION_KEY_SCREENSHOT = "screenshot"; - - public static final String PREFS_CONTROLS_SEEDING_COMPLETED = "SeedingCompleted"; - public static final String PREFS_CONTROLS_FILE = "controls_prefs"; - private static final int SEEDING_MAX = 2; - - private final Context mContext; - private final GlobalActionsManager mWindowManagerFuncs; - private final AudioManager mAudioManager; - private final IDreamManager mDreamManager; - private final DevicePolicyManager mDevicePolicyManager; private final LockPatternUtils mLockPatternUtils; private final KeyguardStateController mKeyguardStateController; - private final BroadcastDispatcher mBroadcastDispatcher; - private final ContentResolver mContentResolver; - private final Resources mResources; - private final ConfigurationController mConfigurationController; - private final UserManager mUserManager; - private final TrustManager mTrustManager; - private final IActivityManager mIActivityManager; - private final TelecomManager mTelecomManager; - private final MetricsLogger mMetricsLogger; - private final UiEventLogger mUiEventLogger; private final NotificationShadeDepthController mDepthController; private final SysUiState mSysUiState; - - // Used for RingerModeTracker - private final LifecycleRegistry mLifecycle = new LifecycleRegistry(this); - - @VisibleForTesting - protected final ArrayList<Action> mItems = new ArrayList<>(); - @VisibleForTesting - protected final ArrayList<Action> mOverflowItems = new ArrayList<>(); - @VisibleForTesting - protected final ArrayList<Action> mPowerItems = new ArrayList<>(); - - @VisibleForTesting - protected ActionsDialog mDialog; - - private Action mSilentModeAction; - private ToggleAction mAirplaneModeOn; - - private MyAdapter mAdapter; - private MyOverflowAdapter mOverflowAdapter; - private MyPowerOptionsAdapter mPowerAdapter; - - private boolean mKeyguardShowing = false; - private boolean mDeviceProvisioned = false; - private ToggleState mAirplaneState = ToggleState.Off; - private boolean mIsWaitingForEcmExit = false; - private boolean mHasTelephony; - private boolean mHasVibrator; - private final boolean mShowSilentToggle; - private final EmergencyAffordanceManager mEmergencyAffordanceManager; - private final ScreenshotHelper mScreenshotHelper; private final ActivityStarter mActivityStarter; private final SysuiColorExtractor mSysuiColorExtractor; private final IStatusBarService mStatusBarService; private final NotificationShadeWindowController mNotificationShadeWindowController; private GlobalActionsPanelPlugin mWalletPlugin; - private Optional<ControlsUiController> mControlsUiControllerOptional; - private final IWindowManager mIWindowManager; - private final Executor mBackgroundExecutor; - private List<ControlsServiceInfo> mControlsServiceInfos = new ArrayList<>(); - private ControlsComponent mControlsComponent; - private Optional<ControlsController> mControlsControllerOptional; - private final RingerModeTracker mRingerModeTracker; - private int mDialogPressDelay = DIALOG_PRESS_DELAY; // ms - private Handler mMainHandler; - private UserContextProvider mUserContextProvider; - @VisibleForTesting - boolean mShowLockScreenCardsAndControls = false; - private int mSmallestScreenWidthDp; @VisibleForTesting - public enum GlobalActionsEvent implements UiEventLogger.UiEventEnum { - @UiEvent(doc = "The global actions / power menu surface became visible on the screen.") - GA_POWER_MENU_OPEN(337), - - @UiEvent(doc = "The global actions / power menu surface was dismissed.") - GA_POWER_MENU_CLOSE(471), - - @UiEvent(doc = "The global actions bugreport button was pressed.") - GA_BUGREPORT_PRESS(344), - - @UiEvent(doc = "The global actions bugreport button was long pressed.") - GA_BUGREPORT_LONG_PRESS(345), - - @UiEvent(doc = "The global actions emergency button was pressed.") - GA_EMERGENCY_DIALER_PRESS(346), - - @UiEvent(doc = "The global actions screenshot button was pressed.") - GA_SCREENSHOT_PRESS(347), - - @UiEvent(doc = "The global actions screenshot button was long pressed.") - GA_SCREENSHOT_LONG_PRESS(348); - - private final int mId; - - GlobalActionsEvent(int id) { - mId = id; - } - - @Override - public int getId() { - return mId; - } - } + boolean mShowLockScreenCards = false; /** * @param context everything needs a context :( @@ -304,9 +120,9 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, AudioManager audioManager, IDreamManager iDreamManager, DevicePolicyManager devicePolicyManager, LockPatternUtils lockPatternUtils, BroadcastDispatcher broadcastDispatcher, - ConnectivityManager connectivityManager, TelephonyListenerManager telephonyListenerManager, - ContentResolver contentResolver, @Nullable Vibrator vibrator, @Main Resources resources, + GlobalSettings globalSettings, SecureSettings secureSettings, + @Nullable Vibrator vibrator, @Main Resources resources, ConfigurationController configurationController, ActivityStarter activityStarter, KeyguardStateController keyguardStateController, UserManager userManager, TrustManager trustManager, IActivityManager iActivityManager, @@ -317,114 +133,57 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, IWindowManager iWindowManager, @Background Executor backgroundExecutor, UiEventLogger uiEventLogger, - RingerModeTracker ringerModeTracker, SysUiState sysUiState, @Main Handler handler, - ControlsComponent controlsComponent, - UserContextProvider userContextProvider) { - mContext = context; - mWindowManagerFuncs = windowManagerFuncs; - mAudioManager = audioManager; - mDreamManager = iDreamManager; - mDevicePolicyManager = devicePolicyManager; + RingerModeTracker ringerModeTracker, SysUiState sysUiState, @Main Handler handler) { + + super(context, windowManagerFuncs, + audioManager, iDreamManager, + devicePolicyManager, lockPatternUtils, + broadcastDispatcher, telephonyListenerManager, + globalSettings, secureSettings, + vibrator, resources, + configurationController, + keyguardStateController, userManager, + trustManager, iActivityManager, + telecomManager, metricsLogger, + depthController, colorExtractor, + statusBarService, + notificationShadeWindowController, + iWindowManager, + backgroundExecutor, + uiEventLogger, + ringerModeTracker, sysUiState, handler); + mLockPatternUtils = lockPatternUtils; mKeyguardStateController = keyguardStateController; - mBroadcastDispatcher = broadcastDispatcher; - mContentResolver = contentResolver; - mResources = resources; - mConfigurationController = configurationController; - mUserManager = userManager; - mTrustManager = trustManager; - mIActivityManager = iActivityManager; - mTelecomManager = telecomManager; - mMetricsLogger = metricsLogger; - mUiEventLogger = uiEventLogger; mDepthController = depthController; mSysuiColorExtractor = colorExtractor; mStatusBarService = statusBarService; mNotificationShadeWindowController = notificationShadeWindowController; - mControlsComponent = controlsComponent; - mControlsUiControllerOptional = controlsComponent.getControlsUiController(); - mIWindowManager = iWindowManager; - mBackgroundExecutor = backgroundExecutor; - mRingerModeTracker = ringerModeTracker; - mControlsControllerOptional = controlsComponent.getControlsController(); mSysUiState = sysUiState; - mMainHandler = handler; - mUserContextProvider = userContextProvider; - mSmallestScreenWidthDp = mContext.getResources().getConfiguration().smallestScreenWidthDp; - - // receive broadcasts - IntentFilter filter = new IntentFilter(); - filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); - filter.addAction(Intent.ACTION_SCREEN_OFF); - filter.addAction(TelephonyManager.ACTION_EMERGENCY_CALLBACK_MODE_CHANGED); - mBroadcastDispatcher.registerReceiver(mBroadcastReceiver, filter); - - mHasTelephony = - context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY); - - // get notified of phone state changes - telephonyListenerManager.addServiceStateListener(mPhoneStateListener); - contentResolver.registerContentObserver( - Settings.Global.getUriFor(Settings.Global.AIRPLANE_MODE_ON), true, - mAirplaneModeObserver); - mHasVibrator = vibrator != null && vibrator.hasVibrator(); - - mShowSilentToggle = SHOW_SILENT_TOGGLE && !resources.getBoolean( - R.bool.config_useFixedVolume); - if (mShowSilentToggle) { - mRingerModeTracker.getRingerMode().observe(this, ringer -> - mHandler.sendEmptyMessage(MESSAGE_REFRESH) - ); - } - - mEmergencyAffordanceManager = new EmergencyAffordanceManager(context); - mScreenshotHelper = new ScreenshotHelper(context); - - mConfigurationController.addCallback(this); - mActivityStarter = activityStarter; keyguardStateController.addCallback(new KeyguardStateController.Callback() { @Override public void onUnlockedChanged() { if (mDialog != null) { + ActionsDialog dialog = (ActionsDialog) mDialog; boolean unlocked = mKeyguardStateController.isUnlocked(); - if (mDialog.mWalletViewController != null) { - mDialog.mWalletViewController.onDeviceLockStateChanged(!unlocked); - } - if (!mDialog.isShowingControls() - && mControlsComponent.getVisibility() == AVAILABLE) { - mDialog.showControls(mControlsUiControllerOptional.get()); + if (dialog.mWalletViewController != null) { + dialog.mWalletViewController.onDeviceLockStateChanged(!unlocked); } + if (unlocked) { - mDialog.hideLockMessage(); + dialog.hideLockMessage(); } } } }); - if (mControlsComponent.getControlsListingController().isPresent()) { - mControlsComponent.getControlsListingController().get() - .addCallback(list -> { - mControlsServiceInfos = list; - // This callback may occur after the dialog has been shown. If so, add - // controls into the already visible space or show the lock msg if needed. - if (mDialog != null) { - if (!mDialog.isShowingControls() - && mControlsComponent.getVisibility() == AVAILABLE) { - mDialog.showControls(mControlsUiControllerOptional.get()); - } else if (shouldShowLockMessage(mDialog)) { - mDialog.showLockMessage(); - } - } - }); - } - - // Listen for changes to show controls on the power menu while locked + // Listen for changes to show pay on the power menu while locked onPowerMenuLockScreenSettingsChanged(); - mContext.getContentResolver().registerContentObserver( + mGlobalSettings.registerContentObserver( Settings.Secure.getUriFor(Settings.Secure.POWER_MENU_LOCKED_SHOW_CONTENT), false /* notifyForDescendants */, - new ContentObserver(mMainHandler) { + new ContentObserver(handler) { @Override public void onChange(boolean selfChange) { onPowerMenuLockScreenSettingsChanged(); @@ -433,132 +192,14 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, } /** - * See if any available control service providers match one of the preferred components. If - * they do, and there are no current favorites for that component, query the preferred - * component for a limited number of suggested controls. - */ - private void seedFavorites() { - if (!mControlsControllerOptional.isPresent() - || mControlsServiceInfos.isEmpty()) { - return; - } - - String[] preferredControlsPackages = mContext.getResources() - .getStringArray(com.android.systemui.R.array.config_controlsPreferredPackages); - - SharedPreferences prefs = mUserContextProvider.getUserContext() - .getSharedPreferences(PREFS_CONTROLS_FILE, Context.MODE_PRIVATE); - Set<String> seededPackages = prefs.getStringSet(PREFS_CONTROLS_SEEDING_COMPLETED, - Collections.emptySet()); - - List<ComponentName> componentsToSeed = new ArrayList<>(); - for (int i = 0; i < Math.min(SEEDING_MAX, preferredControlsPackages.length); i++) { - String pkg = preferredControlsPackages[i]; - for (ControlsServiceInfo info : mControlsServiceInfos) { - if (!pkg.equals(info.componentName.getPackageName())) continue; - if (seededPackages.contains(pkg)) { - break; - } else if (mControlsControllerOptional.get() - .countFavoritesForComponent(info.componentName) > 0) { - // When there are existing controls but no saved preference, assume it - // is out of sync, perhaps through a device restore, and update the - // preference - addPackageToSeededSet(prefs, pkg); - break; - } - componentsToSeed.add(info.componentName); - break; - } - } - - if (componentsToSeed.isEmpty()) return; - - mControlsControllerOptional.get().seedFavoritesForComponents( - componentsToSeed, - (response) -> { - Log.d(TAG, "Controls seeded: " + response); - if (response.getAccepted()) { - addPackageToSeededSet(prefs, response.getPackageName()); - } - }); - } - - private void addPackageToSeededSet(SharedPreferences prefs, String pkg) { - Set<String> seededPackages = prefs.getStringSet(PREFS_CONTROLS_SEEDING_COMPLETED, - Collections.emptySet()); - Set<String> updatedPkgs = new HashSet<>(seededPackages); - updatedPkgs.add(pkg); - prefs.edit().putStringSet(PREFS_CONTROLS_SEEDING_COMPLETED, updatedPkgs).apply(); - } - - /** * Show the global actions dialog (creating if necessary) * * @param keyguardShowing True if keyguard is showing */ public void showOrHideDialog(boolean keyguardShowing, boolean isDeviceProvisioned, GlobalActionsPanelPlugin walletPlugin) { - mKeyguardShowing = keyguardShowing; - mDeviceProvisioned = isDeviceProvisioned; mWalletPlugin = walletPlugin; - if (mDialog != null && mDialog.isShowing()) { - // In order to force global actions to hide on the same affordance press, we must - // register a call to onGlobalActionsShown() first to prevent the default actions - // menu from showing. This will be followed by a subsequent call to - // onGlobalActionsHidden() on dismiss() - mWindowManagerFuncs.onGlobalActionsShown(); - mDialog.dismiss(); - mDialog = null; - } else { - handleShow(); - } - } - - /** - * Dismiss the global actions dialog, if it's currently shown - */ - public void dismissDialog() { - mHandler.removeMessages(MESSAGE_DISMISS); - mHandler.sendEmptyMessage(MESSAGE_DISMISS); - } - - private void awakenIfNecessary() { - if (mDreamManager != null) { - try { - if (mDreamManager.isDreaming()) { - mDreamManager.awaken(); - } - } catch (RemoteException e) { - // we tried - } - } - } - - private void handleShow() { - awakenIfNecessary(); - mDialog = createDialog(); - prepareDialog(); - seedFavorites(); - - WindowManager.LayoutParams attrs = mDialog.getWindow().getAttributes(); - attrs.setTitle("ActionsDialog"); - attrs.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; - mDialog.getWindow().setAttributes(attrs); - // Don't acquire soft keyboard focus, to avoid destroying state when capturing bugreports - mDialog.getWindow().setFlags(FLAG_ALT_FOCUSABLE_IM, FLAG_ALT_FOCUSABLE_IM); - mDialog.show(); - mWindowManagerFuncs.onGlobalActionsShown(); - } - - @VisibleForTesting - protected boolean shouldShowAction(Action action) { - if (mKeyguardShowing && !action.showDuringKeyguard()) { - return false; - } - if (!mDeviceProvisioned && !action.showBeforeProvisioning()) { - return false; - } - return action.shouldShow(); + super.showOrHideDialog(keyguardShowing, isDeviceProvisioned); } /** @@ -566,134 +207,10 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, * layout is being used. */ @VisibleForTesting + @Override protected int getMaxShownPowerItems() { - return mResources.getInteger(com.android.systemui.R.integer.power_menu_max_columns); - } - - /** - * Add a power menu action item for to either the main or overflow items lists, depending on - * whether controls are enabled and whether the max number of shown items has been reached. - */ - private void addActionItem(Action action) { - if (mItems.size() < getMaxShownPowerItems()) { - mItems.add(action); - } else { - mOverflowItems.add(action); - } - } - - @VisibleForTesting - protected String[] getDefaultActions() { - return mResources.getStringArray(R.array.config_globalActionsList); - } - - private void addIfShouldShowAction(List<Action> actions, Action action) { - if (shouldShowAction(action)) { - actions.add(action); - } - } - - @VisibleForTesting - protected void createActionItems() { - // Simple toggle style if there's no vibrator, otherwise use a tri-state - if (!mHasVibrator) { - mSilentModeAction = new SilentModeToggleAction(); - } else { - mSilentModeAction = new SilentModeTriStateAction(mAudioManager, mHandler); - } - mAirplaneModeOn = new AirplaneModeAction(); - onAirplaneModeChanged(); - - mItems.clear(); - mOverflowItems.clear(); - mPowerItems.clear(); - String[] defaultActions = getDefaultActions(); - - ShutDownAction shutdownAction = new ShutDownAction(); - RestartAction restartAction = new RestartAction(); - ArraySet<String> addedKeys = new ArraySet<String>(); - List<Action> tempActions = new ArrayList<>(); - CurrentUserProvider currentUser = new CurrentUserProvider(); - - // make sure emergency affordance action is first, if needed - if (mEmergencyAffordanceManager.needsEmergencyAffordance()) { - addIfShouldShowAction(tempActions, new EmergencyAffordanceAction()); - addedKeys.add(GLOBAL_ACTION_KEY_EMERGENCY); - } - - for (int i = 0; i < defaultActions.length; i++) { - String actionKey = defaultActions[i]; - if (addedKeys.contains(actionKey)) { - // If we already have added this, don't add it again. - continue; - } - if (GLOBAL_ACTION_KEY_POWER.equals(actionKey)) { - addIfShouldShowAction(tempActions, shutdownAction); - } else if (GLOBAL_ACTION_KEY_AIRPLANE.equals(actionKey)) { - addIfShouldShowAction(tempActions, mAirplaneModeOn); - } else if (GLOBAL_ACTION_KEY_BUGREPORT.equals(actionKey)) { - if (shouldDisplayBugReport(currentUser.get())) { - addIfShouldShowAction(tempActions, new BugReportAction()); - } - } else if (GLOBAL_ACTION_KEY_SILENT.equals(actionKey)) { - if (mShowSilentToggle) { - addIfShouldShowAction(tempActions, mSilentModeAction); - } - } else if (GLOBAL_ACTION_KEY_USERS.equals(actionKey)) { - if (SystemProperties.getBoolean("fw.power_user_switcher", false)) { - addUserActions(tempActions, currentUser.get()); - } - } else if (GLOBAL_ACTION_KEY_SETTINGS.equals(actionKey)) { - addIfShouldShowAction(tempActions, getSettingsAction()); - } else if (GLOBAL_ACTION_KEY_LOCKDOWN.equals(actionKey)) { - if (shouldDisplayLockdown(currentUser.get())) { - addIfShouldShowAction(tempActions, new LockDownAction()); - } - } else if (GLOBAL_ACTION_KEY_VOICEASSIST.equals(actionKey)) { - addIfShouldShowAction(tempActions, getVoiceAssistAction()); - } else if (GLOBAL_ACTION_KEY_ASSIST.equals(actionKey)) { - addIfShouldShowAction(tempActions, getAssistAction()); - } else if (GLOBAL_ACTION_KEY_RESTART.equals(actionKey)) { - addIfShouldShowAction(tempActions, restartAction); - } else if (GLOBAL_ACTION_KEY_SCREENSHOT.equals(actionKey)) { - addIfShouldShowAction(tempActions, new ScreenshotAction()); - } else if (GLOBAL_ACTION_KEY_LOGOUT.equals(actionKey)) { - if (mDevicePolicyManager.isLogoutEnabled() - && currentUser.get() != null - && currentUser.get().id != UserHandle.USER_SYSTEM) { - addIfShouldShowAction(tempActions, new LogoutAction()); - } - } else if (GLOBAL_ACTION_KEY_EMERGENCY.equals(actionKey)) { - addIfShouldShowAction(tempActions, new EmergencyDialerAction()); - } else { - Log.e(TAG, "Invalid global action key " + actionKey); - } - // Add here so we don't add more than one. - addedKeys.add(actionKey); - } - - // replace power and restart with a single power options action, if needed - if (tempActions.contains(shutdownAction) && tempActions.contains(restartAction) - && tempActions.size() > getMaxShownPowerItems()) { - // transfer shutdown and restart to their own list of power actions - int powerOptionsIndex = Math.min(tempActions.indexOf(restartAction), - tempActions.indexOf(shutdownAction)); - tempActions.remove(shutdownAction); - tempActions.remove(restartAction); - mPowerItems.add(shutdownAction); - mPowerItems.add(restartAction); - - // add the PowerOptionsAction after Emergency, if present - tempActions.add(powerOptionsIndex, new PowerOptionsAction()); - } - for (Action action : tempActions) { - addActionItem(action); - } - } - - private void onRotate() { - // re-allocate actions between main and overflow lists - this.createActionItems(); + return getContext().getResources().getInteger( + com.android.systemui.R.integer.power_menu_max_columns); } /** @@ -701,23 +218,15 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, * * @return A new dialog. */ - private ActionsDialog createDialog() { - createActionItems(); - - mAdapter = new MyAdapter(); - mOverflowAdapter = new MyOverflowAdapter(); - mPowerAdapter = new MyPowerOptionsAdapter(); + @Override + protected ActionsDialogLite createDialog() { + initDialogItems(); mDepthController.setShowingHomeControls(true); - ControlsUiController uiController = null; - if (mControlsComponent.getVisibility() == AVAILABLE) { - uiController = mControlsUiControllerOptional.get(); - } - ActionsDialog dialog = new ActionsDialog(mContext, mAdapter, mOverflowAdapter, + ActionsDialog dialog = new ActionsDialog(getContext(), mAdapter, mOverflowAdapter, this::getWalletViewController, mDepthController, mSysuiColorExtractor, mStatusBarService, mNotificationShadeWindowController, - controlsAvailable(), uiController, - mSysUiState, this::onRotate, mKeyguardShowing, mPowerAdapter); + mSysUiState, this::onRotate, isKeyguardShowing(), mPowerAdapter); if (shouldShowLockMessage(dialog)) { dialog.showLockMessage(); @@ -729,59 +238,6 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, return dialog; } - @VisibleForTesting - boolean shouldDisplayLockdown(UserInfo user) { - if (user == null) { - return false; - } - - int userId = user.id; - - // No lockdown option if it's not turned on in Settings - if (Settings.Secure.getIntForUser(mContentResolver, - Settings.Secure.LOCKDOWN_IN_POWER_MENU, 0, userId) == 0) { - return false; - } - - // Lockdown is meaningless without a place to go. - if (!mKeyguardStateController.isMethodSecure()) { - return false; - } - - // Only show the lockdown button if the device isn't locked down (for whatever reason). - int state = mLockPatternUtils.getStrongAuthForUser(userId); - return (state == STRONG_AUTH_NOT_REQUIRED - || state == SOME_AUTH_REQUIRED_AFTER_USER_REQUEST); - } - - @VisibleForTesting - boolean shouldDisplayBugReport(UserInfo currentUser) { - return Settings.Global.getInt( - mContentResolver, Settings.Global.BUGREPORT_IN_POWER_MENU, 0) != 0 - && (currentUser == null || currentUser.isPrimary()); - } - - @Override - public void onUiModeChanged() { - mContext.getTheme().applyStyle(mContext.getThemeResId(), true); - if (mDialog != null && mDialog.isShowing()) { - mDialog.refreshDialog(); - } - } - - @Override - public void onConfigChanged(Configuration newConfig) { - if (mDialog != null && mDialog.isShowing() - && (newConfig.smallestScreenWidthDp != mSmallestScreenWidthDp)) { - mSmallestScreenWidthDp = newConfig.smallestScreenWidthDp; - mDialog.refreshDialog(); - } - } - - public void destroy() { - mConfigurationController.removeCallback(this); - } - @Nullable private GlobalActionsPanelPlugin.PanelViewController getWalletViewController() { if (mWalletPlugin == null) { @@ -792,15 +248,6 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, /** * Implements {@link GlobalActionsPanelPlugin.Callbacks#dismissGlobalActionsMenu()}, which is - * called when the quick access wallet requests dismissal. - */ - @Override - public void dismissGlobalActionsMenu() { - dismissDialog(); - } - - /** - * Implements {@link GlobalActionsPanelPlugin.Callbacks#dismissGlobalActionsMenu()}, which is * called when the quick access wallet requests that an intent be started (with lock screen * shown first if needed). */ @@ -809,1364 +256,35 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, mActivityStarter.startPendingIntentDismissingKeyguard(pendingIntent); } - @VisibleForTesting - protected final class PowerOptionsAction extends SinglePressAction { - private PowerOptionsAction() { - super(com.android.systemui.R.drawable.ic_settings_power, - R.string.global_action_power_options); - } - - @Override - public boolean showDuringKeyguard() { - return true; - } - - @Override - public boolean showBeforeProvisioning() { - return true; - } - - @Override - public void onPress() { - if (mDialog != null) { - mDialog.showPowerOptionsMenu(); - } - } - } - - @VisibleForTesting - final class ShutDownAction extends SinglePressAction implements LongPressAction { - private ShutDownAction() { - super(R.drawable.ic_lock_power_off, - R.string.global_action_power_off); - } - - @Override - public boolean onLongPress() { - if (!mUserManager.hasUserRestriction(UserManager.DISALLOW_SAFE_BOOT)) { - mWindowManagerFuncs.reboot(true); - return true; - } - return false; - } - - @Override - public boolean showDuringKeyguard() { - return true; - } - - @Override - public boolean showBeforeProvisioning() { - return true; - } - - @Override - public void onPress() { - // shutdown by making sure radio and power are handled accordingly. - mWindowManagerFuncs.shutdown(); - } - } - - @VisibleForTesting - protected abstract class EmergencyAction extends SinglePressAction { - EmergencyAction(int iconResId, int messageResId) { - super(iconResId, messageResId); - } - - @Override - public boolean shouldBeSeparated() { - return false; - } - - @Override - public View create( - Context context, View convertView, ViewGroup parent, LayoutInflater inflater) { - View v = super.create(context, convertView, parent, inflater); - int textColor; - v.setBackgroundTintList(ColorStateList.valueOf(v.getResources().getColor( - com.android.systemui.R.color.global_actions_emergency_background))); - textColor = v.getResources().getColor( - com.android.systemui.R.color.global_actions_emergency_text); - TextView messageView = v.findViewById(R.id.message); - messageView.setTextColor(textColor); - messageView.setSelected(true); // necessary for marquee to work - ImageView icon = v.findViewById(R.id.icon); - icon.getDrawable().setTint(textColor); - return v; - } - - @Override - public boolean showDuringKeyguard() { - return true; - } - - @Override - public boolean showBeforeProvisioning() { - return true; - } - } - - private class EmergencyAffordanceAction extends EmergencyAction { - EmergencyAffordanceAction() { - super(R.drawable.emergency_icon, - R.string.global_action_emergency); - } - - @Override - public void onPress() { - mEmergencyAffordanceManager.performEmergencyCall(); - } - } - - @VisibleForTesting - class EmergencyDialerAction extends EmergencyAction { - private EmergencyDialerAction() { - super(com.android.systemui.R.drawable.ic_emergency_star, - R.string.global_action_emergency); - } - - @Override - public void onPress() { - mMetricsLogger.action(MetricsEvent.ACTION_EMERGENCY_DIALER_FROM_POWER_MENU); - mUiEventLogger.log(GlobalActionsEvent.GA_EMERGENCY_DIALER_PRESS); - if (mTelecomManager != null) { - Intent intent = mTelecomManager.createLaunchEmergencyDialerIntent( - null /* number */); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK - | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS - | Intent.FLAG_ACTIVITY_CLEAR_TOP); - intent.putExtra(EmergencyDialerConstants.EXTRA_ENTRY_TYPE, - EmergencyDialerConstants.ENTRY_TYPE_POWER_MENU); - mContext.startActivityAsUser(intent, UserHandle.CURRENT); - } - } - } - - @VisibleForTesting - EmergencyDialerAction makeEmergencyDialerActionForTesting() { - return new EmergencyDialerAction(); - } - - @VisibleForTesting - final class RestartAction extends SinglePressAction implements LongPressAction { - private RestartAction() { - super(R.drawable.ic_restart, R.string.global_action_restart); - } - - @Override - public boolean onLongPress() { - if (!mUserManager.hasUserRestriction(UserManager.DISALLOW_SAFE_BOOT)) { - mWindowManagerFuncs.reboot(true); - return true; - } - return false; - } - - @Override - public boolean showDuringKeyguard() { - return true; - } - - @Override - public boolean showBeforeProvisioning() { - return true; - } - - @Override - public void onPress() { - mWindowManagerFuncs.reboot(false); - } - } - - @VisibleForTesting - class ScreenshotAction extends SinglePressAction { - final String KEY_SYSTEM_NAV_2BUTTONS = "system_nav_2buttons"; - - public ScreenshotAction() { - super(R.drawable.ic_screenshot, R.string.global_action_screenshot); - } - - @Override - public void onPress() { - // Add a little delay before executing, to give the - // dialog a chance to go away before it takes a - // screenshot. - // TODO: instead, omit global action dialog layer - mHandler.postDelayed(new Runnable() { - @Override - public void run() { - mScreenshotHelper.takeScreenshot(TAKE_SCREENSHOT_FULLSCREEN, true, true, - SCREENSHOT_GLOBAL_ACTIONS, mHandler, null); - mMetricsLogger.action(MetricsEvent.ACTION_SCREENSHOT_POWER_MENU); - mUiEventLogger.log(GlobalActionsEvent.GA_SCREENSHOT_PRESS); - } - }, mDialogPressDelay); - } - - @Override - public boolean showDuringKeyguard() { - return true; - } - - @Override - public boolean showBeforeProvisioning() { - return false; - } - - @Override - public boolean shouldShow() { - // Include screenshot in power menu for legacy nav because it is not accessible - // through Recents in that mode - return is2ButtonNavigationEnabled(); - } - - boolean is2ButtonNavigationEnabled() { - return NAV_BAR_MODE_2BUTTON == mContext.getResources().getInteger( - com.android.internal.R.integer.config_navBarInteractionMode); - } - } - - @VisibleForTesting - ScreenshotAction makeScreenshotActionForTesting() { - return new ScreenshotAction(); - } - - @VisibleForTesting - class BugReportAction extends SinglePressAction implements LongPressAction { - - public BugReportAction() { - super(R.drawable.ic_lock_bugreport, R.string.bugreport_title); - } - - @Override - public void onPress() { - // don't actually trigger the bugreport if we are running stability - // tests via monkey - if (ActivityManager.isUserAMonkey()) { - return; - } - // Add a little delay before executing, to give the - // dialog a chance to go away before it takes a - // screenshot. - mHandler.postDelayed(new Runnable() { - @Override - public void run() { - try { - // Take an "interactive" bugreport. - mMetricsLogger.action( - MetricsEvent.ACTION_BUGREPORT_FROM_POWER_MENU_INTERACTIVE); - mUiEventLogger.log(GlobalActionsEvent.GA_BUGREPORT_PRESS); - if (!mIActivityManager.launchBugReportHandlerApp()) { - Log.w(TAG, "Bugreport handler could not be launched"); - mIActivityManager.requestInteractiveBugReport(); - } - } catch (RemoteException e) { - } - } - }, mDialogPressDelay); - } - - @Override - public boolean onLongPress() { - // don't actually trigger the bugreport if we are running stability - // tests via monkey - if (ActivityManager.isUserAMonkey()) { - return false; - } - try { - // Take a "full" bugreport. - mMetricsLogger.action(MetricsEvent.ACTION_BUGREPORT_FROM_POWER_MENU_FULL); - mUiEventLogger.log(GlobalActionsEvent.GA_BUGREPORT_LONG_PRESS); - mIActivityManager.requestFullBugReport(); - } catch (RemoteException e) { - } - return false; - } - - public boolean showDuringKeyguard() { - return true; - } - - @Override - public boolean showBeforeProvisioning() { - return false; - } - } - - @VisibleForTesting - BugReportAction makeBugReportActionForTesting() { - return new BugReportAction(); - } - - private final class LogoutAction extends SinglePressAction { - private LogoutAction() { - super(R.drawable.ic_logout, R.string.global_action_logout); - } - - @Override - public boolean showDuringKeyguard() { - return true; - } - - @Override - public boolean showBeforeProvisioning() { - return false; - } - - @Override - public void onPress() { - // Add a little delay before executing, to give the dialog a chance to go away before - // switching user - mHandler.postDelayed(() -> { - try { - int currentUserId = getCurrentUser().id; - mIActivityManager.switchUser(UserHandle.USER_SYSTEM); - mIActivityManager.stopUser(currentUserId, true /*force*/, null); - } catch (RemoteException re) { - Log.e(TAG, "Couldn't logout user " + re); - } - }, mDialogPressDelay); - } - } - - private Action getSettingsAction() { - return new SinglePressAction(R.drawable.ic_settings, - R.string.global_action_settings) { - - @Override - public void onPress() { - Intent intent = new Intent(Settings.ACTION_SETTINGS); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); - mContext.startActivity(intent); - } - - @Override - public boolean showDuringKeyguard() { - return true; - } - - @Override - public boolean showBeforeProvisioning() { - return true; - } - }; - } - - private Action getAssistAction() { - return new SinglePressAction(R.drawable.ic_action_assist_focused, - R.string.global_action_assist) { - @Override - public void onPress() { - Intent intent = new Intent(Intent.ACTION_ASSIST); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); - mContext.startActivity(intent); - } - - @Override - public boolean showDuringKeyguard() { - return true; - } - - @Override - public boolean showBeforeProvisioning() { - return true; - } - }; - } - - private Action getVoiceAssistAction() { - return new SinglePressAction(R.drawable.ic_voice_search, - R.string.global_action_voice_assist) { - @Override - public void onPress() { - Intent intent = new Intent(Intent.ACTION_VOICE_ASSIST); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); - mContext.startActivity(intent); - } - - @Override - public boolean showDuringKeyguard() { - return true; - } - - @Override - public boolean showBeforeProvisioning() { - return true; - } - }; - } - - @VisibleForTesting - class LockDownAction extends SinglePressAction { - LockDownAction() { - super(R.drawable.ic_lock_lockdown, R.string.global_action_lockdown); - } - - @Override - public void onPress() { - mLockPatternUtils.requireStrongAuth(STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN, - UserHandle.USER_ALL); - try { - mIWindowManager.lockNow(null); - // Lock profiles (if any) on the background thread. - mBackgroundExecutor.execute(() -> lockProfiles()); - } catch (RemoteException e) { - Log.e(TAG, "Error while trying to lock device.", e); - } - } - - @Override - public boolean showDuringKeyguard() { - return true; - } - - @Override - public boolean showBeforeProvisioning() { - return false; - } - } - - private void lockProfiles() { - final int currentUserId = getCurrentUser().id; - final int[] profileIds = mUserManager.getEnabledProfileIds(currentUserId); - for (final int id : profileIds) { - if (id != currentUserId) { - mTrustManager.setDeviceLockedForUser(id, true); - } - } - } - - private UserInfo getCurrentUser() { - try { - return mIActivityManager.getCurrentUser(); - } catch (RemoteException re) { - return null; - } - } - - /** - * Non-thread-safe current user provider that caches the result - helpful when a method needs - * to fetch it an indeterminate number of times. - */ - private class CurrentUserProvider { - private UserInfo mUserInfo = null; - private boolean mFetched = false; - - @Nullable - UserInfo get() { - if (!mFetched) { - mFetched = true; - mUserInfo = getCurrentUser(); - } - return mUserInfo; - } - } - - private void addUserActions(List<Action> actions, UserInfo currentUser) { - if (mUserManager.isUserSwitcherEnabled()) { - List<UserInfo> users = mUserManager.getUsers(); - for (final UserInfo user : users) { - if (user.supportsSwitchToByUser()) { - boolean isCurrentUser = currentUser == null - ? user.id == 0 : (currentUser.id == user.id); - Drawable icon = user.iconPath != null ? Drawable.createFromPath(user.iconPath) - : null; - SinglePressAction switchToUser = new SinglePressAction( - R.drawable.ic_menu_cc, icon, - (user.name != null ? user.name : "Primary") - + (isCurrentUser ? " \u2714" : "")) { - public void onPress() { - try { - mIActivityManager.switchUser(user.id); - } catch (RemoteException re) { - Log.e(TAG, "Couldn't switch user " + re); - } - } - - public boolean showDuringKeyguard() { - return true; - } - - public boolean showBeforeProvisioning() { - return false; - } - }; - addIfShouldShowAction(actions, switchToUser); - } - } - } - } - - private void prepareDialog() { - refreshSilentMode(); - mAirplaneModeOn.updateState(mAirplaneState); - mAdapter.notifyDataSetChanged(); - mLifecycle.setCurrentState(Lifecycle.State.RESUMED); - } - - private void refreshSilentMode() { - if (!mHasVibrator) { - Integer value = mRingerModeTracker.getRingerMode().getValue(); - final boolean silentModeOn = value != null && value != AudioManager.RINGER_MODE_NORMAL; - ((ToggleAction) mSilentModeAction).updateState( - silentModeOn ? ToggleState.On : ToggleState.Off); - } - } - - /** - * {@inheritDoc} - */ - public void onDismiss(DialogInterface dialog) { - if (mDialog == dialog) { - mDialog = null; - } - mUiEventLogger.log(GlobalActionsEvent.GA_POWER_MENU_CLOSE); - mWindowManagerFuncs.onGlobalActionsHidden(); - mLifecycle.setCurrentState(Lifecycle.State.CREATED); - } - - /** - * {@inheritDoc} - */ - public void onShow(DialogInterface dialog) { - mMetricsLogger.visible(MetricsEvent.POWER_MENU); - mUiEventLogger.log(GlobalActionsEvent.GA_POWER_MENU_OPEN); - } - - /** - * The adapter used for power menu items shown in the global actions dialog. - */ - public class MyAdapter extends MultiListAdapter { - private int countItems(boolean separated) { - int count = 0; - for (int i = 0; i < mItems.size(); i++) { - final Action action = mItems.get(i); - - if (action.shouldBeSeparated() == separated) { - count++; - } - } - return count; - } - - @Override - public int countSeparatedItems() { - return countItems(true); - } - - @Override - public int countListItems() { - return countItems(false); - } - - @Override - public int getCount() { - return countSeparatedItems() + countListItems(); - } - - @Override - public boolean isEnabled(int position) { - return getItem(position).isEnabled(); - } - - @Override - public boolean areAllItemsEnabled() { - return false; - } - - @Override - public Action getItem(int position) { - int filteredPos = 0; - for (int i = 0; i < mItems.size(); i++) { - final Action action = mItems.get(i); - if (!shouldShowAction(action)) { - continue; - } - if (filteredPos == position) { - return action; - } - filteredPos++; - } - - throw new IllegalArgumentException("position " + position - + " out of range of showable actions" - + ", filtered count=" + getCount() - + ", keyguardshowing=" + mKeyguardShowing - + ", provisioned=" + mDeviceProvisioned); - } - - - public long getItemId(int position) { - return position; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - Action action = getItem(position); - View view = action.create(mContext, convertView, parent, LayoutInflater.from(mContext)); - view.setOnClickListener(v -> onClickItem(position)); - if (action instanceof LongPressAction) { - view.setOnLongClickListener(v -> onLongClickItem(position)); - } - return view; - } - - @Override - public boolean onLongClickItem(int position) { - final Action action = mAdapter.getItem(position); - if (action instanceof LongPressAction) { - if (mDialog != null) { - mDialog.dismiss(); - } else { - Log.w(TAG, "Action long-clicked while mDialog is null."); - } - return ((LongPressAction) action).onLongPress(); - } - return false; - } - - @Override - public void onClickItem(int position) { - Action item = mAdapter.getItem(position); - if (!(item instanceof SilentModeTriStateAction)) { - if (mDialog != null) { - // don't dismiss the dialog if we're opening the power options menu - if (!(item instanceof PowerOptionsAction)) { - mDialog.dismiss(); - } - } else { - Log.w(TAG, "Action clicked while mDialog is null."); - } - item.onPress(); - } - } - - @Override - public boolean shouldBeSeparated(int position) { - return getItem(position).shouldBeSeparated(); - } - } - - /** - * The adapter used for items in the overflow menu. - */ - public class MyPowerOptionsAdapter extends BaseAdapter { - @Override - public int getCount() { - return mPowerItems.size(); - } - - @Override - public Action getItem(int position) { - return mPowerItems.get(position); - } - - @Override - public long getItemId(int position) { - return position; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - Action action = getItem(position); - if (action == null) { - Log.w(TAG, "No power options action found at position: " + position); - return null; - } - int viewLayoutResource = com.android.systemui.R.layout.global_actions_power_item; - View view = convertView != null ? convertView - : LayoutInflater.from(mContext).inflate(viewLayoutResource, parent, false); - view.setOnClickListener(v -> onClickItem(position)); - if (action instanceof LongPressAction) { - view.setOnLongClickListener(v -> onLongClickItem(position)); - } - ImageView icon = view.findViewById(R.id.icon); - TextView messageView = view.findViewById(R.id.message); - messageView.setSelected(true); // necessary for marquee to work - - icon.setImageDrawable(action.getIcon(mContext)); - icon.setScaleType(ScaleType.CENTER_CROP); - - if (action.getMessage() != null) { - messageView.setText(action.getMessage()); - } else { - messageView.setText(action.getMessageResId()); - } - return view; - } - - private boolean onLongClickItem(int position) { - final Action action = getItem(position); - if (action instanceof LongPressAction) { - if (mDialog != null) { - mDialog.dismiss(); - } else { - Log.w(TAG, "Action long-clicked while mDialog is null."); - } - return ((LongPressAction) action).onLongPress(); - } - return false; - } - - private void onClickItem(int position) { - Action item = getItem(position); - if (!(item instanceof SilentModeTriStateAction)) { - if (mDialog != null) { - mDialog.dismiss(); - } else { - Log.w(TAG, "Action clicked while mDialog is null."); - } - item.onPress(); - } - } - } - - /** - * The adapter used for items in the power options menu, triggered by the PowerOptionsAction. - */ - public class MyOverflowAdapter extends BaseAdapter { - @Override - public int getCount() { - return mOverflowItems.size(); - } - - @Override - public Action getItem(int position) { - return mOverflowItems.get(position); - } - - @Override - public long getItemId(int position) { - return position; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - Action action = getItem(position); - if (action == null) { - Log.w(TAG, "No overflow action found at position: " + position); - return null; - } - int viewLayoutResource = com.android.systemui.R.layout.controls_more_item; - View view = convertView != null ? convertView - : LayoutInflater.from(mContext).inflate(viewLayoutResource, parent, false); - TextView textView = (TextView) view; - if (action.getMessageResId() != 0) { - textView.setText(action.getMessageResId()); - } else { - textView.setText(action.getMessage()); - } - return textView; - } - - private boolean onLongClickItem(int position) { - final Action action = getItem(position); - if (action instanceof LongPressAction) { - if (mDialog != null) { - mDialog.dismiss(); - } else { - Log.w(TAG, "Action long-clicked while mDialog is null."); - } - return ((LongPressAction) action).onLongPress(); - } - return false; - } - - private void onClickItem(int position) { - Action item = getItem(position); - if (!(item instanceof SilentModeTriStateAction)) { - if (mDialog != null) { - mDialog.dismiss(); - } else { - Log.w(TAG, "Action clicked while mDialog is null."); - } - item.onPress(); - } - } - } - - // note: the scheme below made more sense when we were planning on having - // 8 different things in the global actions dialog. seems overkill with - // only 3 items now, but may as well keep this flexible approach so it will - // be easy should someone decide at the last minute to include something - // else, such as 'enable wifi', or 'enable bluetooth' - - /** - * What each item in the global actions dialog must be able to support. - */ - public interface Action { - /** - * @return Text that will be announced when dialog is created. null for none. - */ - CharSequence getLabelForAccessibility(Context context); - - View create(Context context, View convertView, ViewGroup parent, LayoutInflater inflater); - - void onPress(); - - /** - * @return whether this action should appear in the dialog when the keygaurd is showing. - */ - boolean showDuringKeyguard(); - - /** - * @return whether this action should appear in the dialog before the - * device is provisioned.f - */ - boolean showBeforeProvisioning(); - - boolean isEnabled(); - - default boolean shouldBeSeparated() { - return false; - } - - /** - * Return the id of the message associated with this action, or 0 if it doesn't have one. - * @return - */ - int getMessageResId(); - - /** - * Return the icon drawable for this action. - */ - Drawable getIcon(Context context); - - /** - * Return the message associated with this action, or null if it doesn't have one. - * @return - */ - CharSequence getMessage(); - - default boolean shouldShow() { - return true; - } - } - - /** - * An action that also supports long press. - */ - private interface LongPressAction extends Action { - boolean onLongPress(); - } - - /** - * A single press action maintains no state, just responds to a press and takes an action. - */ - - private abstract class SinglePressAction implements Action { - private final int mIconResId; - private final Drawable mIcon; - private final int mMessageResId; - private final CharSequence mMessage; - - protected SinglePressAction(int iconResId, int messageResId) { - mIconResId = iconResId; - mMessageResId = messageResId; - mMessage = null; - mIcon = null; - } - - protected SinglePressAction(int iconResId, Drawable icon, CharSequence message) { - mIconResId = iconResId; - mMessageResId = 0; - mMessage = message; - mIcon = icon; - } - - public boolean isEnabled() { - return true; - } - - public String getStatus() { - return null; - } - - abstract public void onPress(); - - public CharSequence getLabelForAccessibility(Context context) { - if (mMessage != null) { - return mMessage; - } else { - return context.getString(mMessageResId); - } - } - - public int getMessageResId() { - return mMessageResId; - } - - public CharSequence getMessage() { - return mMessage; - } - - @Override - public Drawable getIcon(Context context) { - if (mIcon != null) { - return mIcon; - } else { - return context.getDrawable(mIconResId); - } - } - - public View create( - Context context, View convertView, ViewGroup parent, LayoutInflater inflater) { - View v = inflater.inflate(com.android.systemui.R.layout.global_actions_grid_item_v2, - parent, false /* attach */); - - ImageView icon = v.findViewById(R.id.icon); - TextView messageView = v.findViewById(R.id.message); - messageView.setSelected(true); // necessary for marquee to work - - icon.setImageDrawable(getIcon(context)); - icon.setScaleType(ScaleType.CENTER_CROP); - - if (mMessage != null) { - messageView.setText(mMessage); - } else { - messageView.setText(mMessageResId); - } - - return v; - } - } - - private enum ToggleState { - Off(false), - TurningOn(true), - TurningOff(true), - On(false); - - private final boolean mInTransition; - - ToggleState(boolean intermediate) { - mInTransition = intermediate; - } - - public boolean inTransition() { - return mInTransition; - } - } - - /** - * A toggle action knows whether it is on or off, and displays an icon and status message - * accordingly. - */ - private abstract class ToggleAction implements Action { - - protected ToggleState mState = ToggleState.Off; - - // prefs - protected int mEnabledIconResId; - protected int mDisabledIconResid; - protected int mMessageResId; - protected int mEnabledStatusMessageResId; - protected int mDisabledStatusMessageResId; - - /** - * @param enabledIconResId The icon for when this action is on. - * @param disabledIconResid The icon for when this action is off. - * @param message The general information message, e.g 'Silent Mode' - * @param enabledStatusMessageResId The on status message, e.g 'sound disabled' - * @param disabledStatusMessageResId The off status message, e.g. 'sound enabled' - */ - public ToggleAction(int enabledIconResId, - int disabledIconResid, - int message, - int enabledStatusMessageResId, - int disabledStatusMessageResId) { - mEnabledIconResId = enabledIconResId; - mDisabledIconResid = disabledIconResid; - mMessageResId = message; - mEnabledStatusMessageResId = enabledStatusMessageResId; - mDisabledStatusMessageResId = disabledStatusMessageResId; - } - - /** - * Override to make changes to resource IDs just before creating the View. - */ - void willCreate() { - - } - - @Override - public CharSequence getLabelForAccessibility(Context context) { - return context.getString(mMessageResId); - } - - private boolean isOn() { - return mState == ToggleState.On || mState == ToggleState.TurningOn; - } - - @Override - public CharSequence getMessage() { - return null; - } - @Override - public int getMessageResId() { - return isOn() ? mEnabledStatusMessageResId : mDisabledStatusMessageResId; - } - - private int getIconResId() { - return isOn() ? mEnabledIconResId : mDisabledIconResid; - } - - @Override - public Drawable getIcon(Context context) { - return context.getDrawable(getIconResId()); - } - - public View create(Context context, View convertView, ViewGroup parent, - LayoutInflater inflater) { - willCreate(); - - View v = inflater.inflate(com.android.systemui.R.layout.global_actions_grid_item_v2, - parent, false /* attach */); - - ImageView icon = (ImageView) v.findViewById(R.id.icon); - TextView messageView = (TextView) v.findViewById(R.id.message); - final boolean enabled = isEnabled(); - - if (messageView != null) { - messageView.setText(getMessageResId()); - messageView.setEnabled(enabled); - messageView.setSelected(true); // necessary for marquee to work - } - - if (icon != null) { - icon.setImageDrawable(context.getDrawable(getIconResId())); - icon.setEnabled(enabled); - } - - v.setEnabled(enabled); - - return v; - } - - public final void onPress() { - if (mState.inTransition()) { - Log.w(TAG, "shouldn't be able to toggle when in transition"); - return; - } - - final boolean nowOn = !(mState == ToggleState.On); - onToggle(nowOn); - changeStateFromPress(nowOn); - } - - public boolean isEnabled() { - return !mState.inTransition(); - } - - /** - * Implementations may override this if their state can be in on of the intermediate states - * until some notification is received (e.g airplane mode is 'turning off' until we know the - * wireless connections are back online - * - * @param buttonOn Whether the button was turned on or off - */ - protected void changeStateFromPress(boolean buttonOn) { - mState = buttonOn ? ToggleState.On : ToggleState.Off; - } - - abstract void onToggle(boolean on); - - public void updateState(ToggleState state) { - mState = state; - } - } - - private class AirplaneModeAction extends ToggleAction { - AirplaneModeAction() { - super( - R.drawable.ic_lock_airplane_mode, - R.drawable.ic_lock_airplane_mode_off, - R.string.global_actions_toggle_airplane_mode, - R.string.global_actions_airplane_mode_on_status, - R.string.global_actions_airplane_mode_off_status); - } - - void onToggle(boolean on) { - if (mHasTelephony && TelephonyProperties.in_ecm_mode().orElse(false)) { - mIsWaitingForEcmExit = true; - // Launch ECM exit dialog - Intent ecmDialogIntent = - new Intent(TelephonyManager.ACTION_SHOW_NOTICE_ECM_BLOCK_OTHERS, null); - ecmDialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - mContext.startActivity(ecmDialogIntent); - } else { - changeAirplaneModeSystemSetting(on); - } - } - - @Override - protected void changeStateFromPress(boolean buttonOn) { - if (!mHasTelephony) return; - - // In ECM mode airplane state cannot be changed - if (!TelephonyProperties.in_ecm_mode().orElse(false)) { - mState = buttonOn ? ToggleState.TurningOn : ToggleState.TurningOff; - mAirplaneState = mState; - } - } - - public boolean showDuringKeyguard() { - return true; - } - - public boolean showBeforeProvisioning() { - return false; - } - } - - private class SilentModeToggleAction extends ToggleAction { - public SilentModeToggleAction() { - super(R.drawable.ic_audio_vol_mute, - R.drawable.ic_audio_vol, - R.string.global_action_toggle_silent_mode, - R.string.global_action_silent_mode_on_status, - R.string.global_action_silent_mode_off_status); - } - - void onToggle(boolean on) { - if (on) { - mAudioManager.setRingerMode(AudioManager.RINGER_MODE_SILENT); - } else { - mAudioManager.setRingerMode(AudioManager.RINGER_MODE_NORMAL); - } - } - - public boolean showDuringKeyguard() { - return true; - } - - public boolean showBeforeProvisioning() { - return false; - } - } - - private static class SilentModeTriStateAction implements Action, View.OnClickListener { - - private final int[] ITEM_IDS = {R.id.option1, R.id.option2, R.id.option3}; - - private final AudioManager mAudioManager; - private final Handler mHandler; - - SilentModeTriStateAction(AudioManager audioManager, Handler handler) { - mAudioManager = audioManager; - mHandler = handler; - } - - private int ringerModeToIndex(int ringerMode) { - // They just happen to coincide - return ringerMode; - } - - private int indexToRingerMode(int index) { - // They just happen to coincide - return index; - } - - @Override - public CharSequence getLabelForAccessibility(Context context) { - return null; - } - - @Override - public int getMessageResId() { - return 0; - } - - @Override - public CharSequence getMessage() { - return null; - } - - @Override - public Drawable getIcon(Context context) { - return null; - } - - - public View create(Context context, View convertView, ViewGroup parent, - LayoutInflater inflater) { - View v = inflater.inflate(R.layout.global_actions_silent_mode, parent, false); - - int selectedIndex = ringerModeToIndex(mAudioManager.getRingerMode()); - for (int i = 0; i < 3; i++) { - View itemView = v.findViewById(ITEM_IDS[i]); - itemView.setSelected(selectedIndex == i); - // Set up click handler - itemView.setTag(i); - itemView.setOnClickListener(this); - } - return v; - } - - public void onPress() { - } - - public boolean showDuringKeyguard() { - return true; - } - - public boolean showBeforeProvisioning() { - return false; - } - - public boolean isEnabled() { - return true; - } - - void willCreate() { - } - - public void onClick(View v) { - if (!(v.getTag() instanceof Integer)) return; - - int index = (Integer) v.getTag(); - mAudioManager.setRingerMode(indexToRingerMode(index)); - mHandler.sendEmptyMessageDelayed(MESSAGE_DISMISS, DIALOG_DISMISS_DELAY); - } - } - - private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(action) - || Intent.ACTION_SCREEN_OFF.equals(action)) { - String reason = intent.getStringExtra(SYSTEM_DIALOG_REASON_KEY); - if (!SYSTEM_DIALOG_REASON_GLOBAL_ACTIONS.equals(reason)) { - mHandler.sendMessage(mHandler.obtainMessage(MESSAGE_DISMISS, reason)); - } - } else if (TelephonyManager.ACTION_EMERGENCY_CALLBACK_MODE_CHANGED.equals(action)) { - // Airplane mode can be changed after ECM exits if airplane toggle button - // is pressed during ECM mode - if (!(intent.getBooleanExtra(TelephonyManager.EXTRA_PHONE_IN_ECM_STATE, false)) - && mIsWaitingForEcmExit) { - mIsWaitingForEcmExit = false; - changeAirplaneModeSystemSetting(true); - } - } - } - }; - - private final TelephonyCallback.ServiceStateListener mPhoneStateListener = - new TelephonyCallback.ServiceStateListener() { - @Override - public void onServiceStateChanged(ServiceState serviceState) { - if (!mHasTelephony) return; - final boolean inAirplaneMode = serviceState.getState() == ServiceState.STATE_POWER_OFF; - mAirplaneState = inAirplaneMode ? ToggleState.On : ToggleState.Off; - mAirplaneModeOn.updateState(mAirplaneState); - mAdapter.notifyDataSetChanged(); - mOverflowAdapter.notifyDataSetChanged(); - mPowerAdapter.notifyDataSetChanged(); - } - }; - - private ContentObserver mAirplaneModeObserver = new ContentObserver(mMainHandler) { - @Override - public void onChange(boolean selfChange) { - onAirplaneModeChanged(); - } - }; - - private static final int MESSAGE_DISMISS = 0; - private static final int MESSAGE_REFRESH = 1; - private static final int DIALOG_DISMISS_DELAY = 300; // ms - private static final int DIALOG_PRESS_DELAY = 850; // ms - - @VisibleForTesting void setZeroDialogPressDelayForTesting() { - mDialogPressDelay = 0; // ms + @Override + protected int getEmergencyTextColor(Context context) { + return context.getResources().getColor( + com.android.systemui.R.color.global_actions_emergency_text); } - private Handler mHandler = new Handler() { - public void handleMessage(Message msg) { - switch (msg.what) { - case MESSAGE_DISMISS: - if (mDialog != null) { - if (SYSTEM_DIALOG_REASON_DREAM.equals(msg.obj)) { - mDialog.completeDismiss(); - } else { - mDialog.dismiss(); - } - mDialog = null; - } - break; - case MESSAGE_REFRESH: - refreshSilentMode(); - mAdapter.notifyDataSetChanged(); - break; - } - } - }; - - private void onAirplaneModeChanged() { - // Let the service state callbacks handle the state. - if (mHasTelephony) return; - - boolean airplaneModeOn = Settings.Global.getInt( - mContentResolver, - Settings.Global.AIRPLANE_MODE_ON, - 0) == 1; - mAirplaneState = airplaneModeOn ? ToggleState.On : ToggleState.Off; - mAirplaneModeOn.updateState(mAirplaneState); + @Override + protected int getEmergencyIconColor(Context context) { + return getContext().getResources().getColor( + com.android.systemui.R.color.global_actions_emergency_text); } - /** - * Change the airplane mode system setting - */ - private void changeAirplaneModeSystemSetting(boolean on) { - Settings.Global.putInt( - mContentResolver, - Settings.Global.AIRPLANE_MODE_ON, - on ? 1 : 0); - Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED); - intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING); - intent.putExtra("state", on); - mContext.sendBroadcastAsUser(intent, UserHandle.ALL); - if (!mHasTelephony) { - mAirplaneState = on ? ToggleState.On : ToggleState.Off; - } + @Override + protected int getEmergencyBackgroundColor(Context context) { + return getContext().getResources().getColor( + com.android.systemui.R.color.global_actions_emergency_background); } - @NonNull @Override - public Lifecycle getLifecycle() { - return mLifecycle; + protected int getGridItemLayoutResource() { + return com.android.systemui.R.layout.global_actions_grid_item_v2; } @VisibleForTesting - static final class ActionsDialog extends Dialog implements DialogInterface, - ColorExtractor.OnColorsChangedListener { + static class ActionsDialog extends ActionsDialogLite { - private final Context mContext; - private final MyAdapter mAdapter; - private final MyOverflowAdapter mOverflowAdapter; - private final MyPowerOptionsAdapter mPowerOptionsAdapter; - private final IStatusBarService mStatusBarService; - private final IBinder mToken = new Binder(); - private MultiListLayout mGlobalActionsLayout; - private Drawable mBackgroundDrawable; - private final SysuiColorExtractor mColorExtractor; private final Provider<GlobalActionsPanelPlugin.PanelViewController> mWalletFactory; @Nullable private GlobalActionsPanelPlugin.PanelViewController mWalletViewController; - private boolean mKeyguardShowing; - private boolean mShowing; - private float mScrimAlpha; private ResetOrientationData mResetOrientationData; - private final NotificationShadeWindowController mNotificationShadeWindowController; - private final NotificationShadeDepthController mDepthController; - private final SysUiState mSysUiState; - private ListPopupWindow mOverflowPopup; - private Dialog mPowerOptionsDialog; - private final Runnable mOnRotateCallback; - private final boolean mControlsAvailable; - - private ControlsUiController mControlsUiController; - private ViewGroup mControlsView; - private ViewGroup mContainer; @VisibleForTesting ViewGroup mLockMessageContainer; private TextView mLockMessage; @@ -2175,30 +293,16 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, NotificationShadeDepthController depthController, SysuiColorExtractor sysuiColorExtractor, IStatusBarService statusBarService, NotificationShadeWindowController notificationShadeWindowController, - boolean controlsAvailable, @Nullable ControlsUiController controlsUiController, SysUiState sysuiState, Runnable onRotateCallback, boolean keyguardShowing, MyPowerOptionsAdapter powerAdapter) { - super(context, com.android.systemui.R.style.Theme_SystemUI_Dialog_GlobalActions); - mContext = context; - mAdapter = adapter; - mOverflowAdapter = overflowAdapter; - mPowerOptionsAdapter = powerAdapter; - mDepthController = depthController; - mColorExtractor = sysuiColorExtractor; - mStatusBarService = statusBarService; - mNotificationShadeWindowController = notificationShadeWindowController; - mControlsAvailable = controlsAvailable; - mControlsUiController = controlsUiController; - mSysUiState = sysuiState; - mOnRotateCallback = onRotateCallback; - mKeyguardShowing = keyguardShowing; + super(context, com.android.systemui.R.style.Theme_SystemUI_Dialog_GlobalActions, + adapter, overflowAdapter, depthController, sysuiColorExtractor, + statusBarService, notificationShadeWindowController, sysuiState, + onRotateCallback, keyguardShowing, powerAdapter); mWalletFactory = walletFactory; - // Window initialization + // Update window attributes Window window = getWindow(); - window.requestFeature(Window.FEATURE_NO_TITLE); - // Inflate the decor view, so the attributes below are not overwritten by the theme. - window.getDecorView(); window.getAttributes().systemUiVisibility |= View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; @@ -2211,28 +315,18 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED); - window.setType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY); - window.getAttributes().setFitInsetsTypes(0 /* types */); setTitle(R.string.global_actions); - initializeLayout(); } - private boolean isShowingControls() { - return mControlsUiController != null; - } - - private void showControls(ControlsUiController controller) { - mControlsUiController = controller; - mControlsUiController.show(mControlsView, this::dismissForControlsActivity, - null /* activityContext */); - } - private boolean isWalletViewAvailable() { return mWalletViewController != null && mWalletViewController.getPanelContent() != null; } private void initializeWalletView() { + if (mWalletFactory == null) { + return; + } mWalletViewController = mWalletFactory.get(); if (!isWalletViewAvailable()) { return; @@ -2291,10 +385,8 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, new FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT); - if (!mControlsAvailable) { - panelParams.topMargin = mContext.getResources().getDimensionPixelSize( - com.android.systemui.R.dimen.global_actions_wallet_top_margin); - } + panelParams.topMargin = mContext.getResources().getDimensionPixelSize( + com.android.systemui.R.dimen.global_actions_wallet_top_margin); View walletView = mWalletViewController.getPanelContent(); panelContainer.addView(walletView, panelParams); // Smooth transitions when wallet is resized, which can happen when a card is added @@ -2313,132 +405,23 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, } } - private ListPopupWindow createPowerOverflowPopup() { - GlobalActionsPopupMenu popup = new GlobalActionsPopupMenu( - new ContextThemeWrapper( - mContext, - com.android.systemui.R.style.Control_ListPopupWindow - ), false /* isDropDownMode */); - popup.setOnItemClickListener( - (parent, view, position, id) -> mOverflowAdapter.onClickItem(position)); - popup.setOnItemLongClickListener( - (parent, view, position, id) -> mOverflowAdapter.onLongClickItem(position)); - View overflowButton = - findViewById(com.android.systemui.R.id.global_actions_overflow_button); - popup.setAnchorView(overflowButton); - popup.setAdapter(mOverflowAdapter); - return popup; - } - - public void showPowerOptionsMenu() { - mPowerOptionsDialog = GlobalActionsPowerDialog.create(mContext, mPowerOptionsAdapter); - mPowerOptionsDialog.show(); - } - - private void showPowerOverflowMenu() { - mOverflowPopup = createPowerOverflowPopup(); - mOverflowPopup.show(); + @Override + protected int getLayoutResource() { + return com.android.systemui.R.layout.global_actions_grid_v2; } - private void initializeLayout() { - setContentView(com.android.systemui.R.layout.global_actions_grid_v2); - fixNavBarClipping(); - mControlsView = findViewById(com.android.systemui.R.id.global_actions_controls); - mGlobalActionsLayout = findViewById(com.android.systemui.R.id.global_actions_view); - mGlobalActionsLayout.setListViewAccessibilityDelegate(new View.AccessibilityDelegate() { - @Override - public boolean dispatchPopulateAccessibilityEvent( - View host, AccessibilityEvent event) { - // Populate the title here, just as Activity does - event.getText().add(mContext.getString(R.string.global_actions)); - return true; - } - }); - mGlobalActionsLayout.setRotationListener(this::onRotate); - mGlobalActionsLayout.setAdapter(mAdapter); - mContainer = findViewById(com.android.systemui.R.id.global_actions_container); + @Override + protected void initializeLayout() { + super.initializeLayout(); mLockMessageContainer = requireViewById( com.android.systemui.R.id.global_actions_lock_message_container); mLockMessage = requireViewById(com.android.systemui.R.id.global_actions_lock_message); - - View overflowButton = findViewById( - com.android.systemui.R.id.global_actions_overflow_button); - if (overflowButton != null) { - if (mOverflowAdapter.getCount() > 0) { - overflowButton.setOnClickListener((view) -> showPowerOverflowMenu()); - LinearLayout.LayoutParams params = - (LinearLayout.LayoutParams) mGlobalActionsLayout.getLayoutParams(); - params.setMarginEnd(0); - mGlobalActionsLayout.setLayoutParams(params); - } else { - overflowButton.setVisibility(View.GONE); - LinearLayout.LayoutParams params = - (LinearLayout.LayoutParams) mGlobalActionsLayout.getLayoutParams(); - params.setMarginEnd(mContext.getResources().getDimensionPixelSize( - com.android.systemui.R.dimen.global_actions_side_margin)); - mGlobalActionsLayout.setLayoutParams(params); - } - } - initializeWalletView(); - if (mBackgroundDrawable == null) { - mBackgroundDrawable = new ScrimDrawable(); - mScrimAlpha = 1.0f; - } getWindow().setBackgroundDrawable(mBackgroundDrawable); } - private void fixNavBarClipping() { - ViewGroup content = findViewById(android.R.id.content); - content.setClipChildren(false); - content.setClipToPadding(false); - ViewGroup contentParent = (ViewGroup) content.getParent(); - contentParent.setClipChildren(false); - contentParent.setClipToPadding(false); - } - - @Override - protected void onStart() { - super.setCanceledOnTouchOutside(true); - super.onStart(); - mGlobalActionsLayout.updateList(); - - if (mBackgroundDrawable instanceof ScrimDrawable) { - mColorExtractor.addOnColorsChangedListener(this); - GradientColors colors = mColorExtractor.getNeutralColors(); - updateColors(colors, false /* animate */); - } - } - - /** - * Updates background and system bars according to current GradientColors. - * - * @param colors Colors and hints to use. - * @param animate Interpolates gradient if true, just sets otherwise. - */ - private void updateColors(GradientColors colors, boolean animate) { - if (!(mBackgroundDrawable instanceof ScrimDrawable)) { - return; - } - ((ScrimDrawable) mBackgroundDrawable).setColor(Color.BLACK, animate); - View decorView = getWindow().getDecorView(); - if (colors.supportsDarkText()) { - decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR | - View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR); - } else { - decorView.setSystemUiVisibility(0); - } - } - - @Override - protected void onStop() { - super.onStop(); - mColorExtractor.removeOnColorsChangedListener(this); - } - @Override - public void show() { - super.show(); + protected void showDialog() { mShowing = true; mNotificationShadeWindowController.setRequestTopUi(true, TAG); mSysUiState.setFlag(SYSUI_STATE_GLOBAL_ACTIONS_SHOWING, true) @@ -2452,10 +435,6 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, windowInsets.getStableInsetBottom()); return WindowInsets.CONSUMED; }); - if (mControlsUiController != null) { - mControlsUiController.show(mControlsView, this::dismissForControlsActivity, - null /* activityContext */); - } mBackgroundDrawable.setAlpha(0); float xOffset = mGlobalActionsLayout.getAnimationOffsetX(); @@ -2482,71 +461,15 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, } @Override - public void dismiss() { - dismissWithAnimation(() -> { - mContainer.setTranslationX(0); - ObjectAnimator alphaAnimator = - ObjectAnimator.ofFloat(mContainer, "alpha", 1f, 0f); - alphaAnimator.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN); - alphaAnimator.setDuration(233); - alphaAnimator.addUpdateListener((animation) -> { - float animatedValue = 1f - animation.getAnimatedFraction(); - int alpha = (int) (animatedValue * mScrimAlpha * 255); - mBackgroundDrawable.setAlpha(alpha); - mDepthController.updateGlobalDialogVisibility(animatedValue, - mGlobalActionsLayout); - }); - - float xOffset = mGlobalActionsLayout.getAnimationOffsetX(); - ObjectAnimator xAnimator = - ObjectAnimator.ofFloat(mContainer, "translationX", 0f, xOffset); - xAnimator.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN); - xAnimator.setDuration(350); - - AnimatorSet animatorSet = new AnimatorSet(); - animatorSet.playTogether(alphaAnimator, xAnimator); - animatorSet.addListener(new AnimatorListenerAdapter() { - public void onAnimationEnd(Animator animation) { - completeDismiss(); - } - }); - - animatorSet.start(); - - // close first, as popup windows will not fade during the animation - dismissOverflow(false); - dismissPowerOptions(false); - if (mControlsUiController != null) mControlsUiController.closeDialogs(false); - }); - } - - private void dismissForControlsActivity() { - dismissWithAnimation(() -> { - ViewGroup root = (ViewGroup) mGlobalActionsLayout.getParent(); - ControlsAnimations.exitAnimation(root, this::completeDismiss).start(); - }); + protected void dismissInternal() { + super.dismissInternal(); } - void dismissWithAnimation(Runnable animation) { - if (!mShowing) { - return; - } - mShowing = false; - animation.run(); - } - - private void completeDismiss() { - mShowing = false; - resetOrientation(); + @Override + protected void completeDismiss() { dismissWallet(); - dismissOverflow(true); - dismissPowerOptions(true); - if (mControlsUiController != null) mControlsUiController.hide(); - mNotificationShadeWindowController.setRequestTopUi(false, TAG); - mDepthController.updateGlobalDialogVisibility(0, null /* view */); - mSysUiState.setFlag(SYSUI_STATE_GLOBAL_ACTIONS_SHOWING, false) - .commitUpdate(mContext.getDisplayId()); - super.dismiss(); + resetOrientation(); + super.completeDismiss(); } private void dismissWallet() { @@ -2557,38 +480,6 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, } } - private void dismissOverflow(boolean immediate) { - if (mOverflowPopup != null) { - if (immediate) { - mOverflowPopup.dismissImmediate(); - } else { - mOverflowPopup.dismiss(); - } - } - } - - private void dismissPowerOptions(boolean immediate) { - if (mPowerOptionsDialog != null) { - if (immediate) { - mPowerOptionsDialog.dismiss(); - } else { - mPowerOptionsDialog.dismiss(); - } - } - } - - private void setRotationSuggestionsEnabled(boolean enabled) { - try { - final int userId = Binder.getCallingUserHandle().getIdentifier(); - final int what = enabled - ? StatusBarManager.DISABLE2_NONE - : StatusBarManager.DISABLE2_ROTATE_SUGGESTIONS; - mStatusBarService.disable2ForUser(what, mToken, mContext.getPackageName(), userId); - } catch (RemoteException ex) { - throw ex.rethrowFromSystemServer(); - } - } - private void resetOrientation() { if (mResetOrientationData != null) { RotationPolicy.setRotationLockAtAngle(mContext, mResetOrientationData.locked, @@ -2598,47 +489,10 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, } @Override - public void onColorsChanged(ColorExtractor extractor, int which) { - if (mKeyguardShowing) { - if ((WallpaperManager.FLAG_LOCK & which) != 0) { - updateColors(extractor.getColors(WallpaperManager.FLAG_LOCK), - true /* animate */); - } - } else { - if ((WallpaperManager.FLAG_SYSTEM & which) != 0) { - updateColors(extractor.getColors(WallpaperManager.FLAG_SYSTEM), - true /* animate */); - } - } - } - - public void setKeyguardShowing(boolean keyguardShowing) { - mKeyguardShowing = keyguardShowing; - } - public void refreshDialog() { // ensure dropdown menus are dismissed before re-initializing the dialog dismissWallet(); - dismissOverflow(true); - dismissPowerOptions(true); - if (mControlsUiController != null) { - mControlsUiController.hide(); - } - - // re-create dialog - initializeLayout(); - mGlobalActionsLayout.updateList(); - if (mControlsUiController != null) { - mControlsUiController.show(mControlsView, this::dismissForControlsActivity, - null /* activityContext */); - } - } - - public void onRotate(int from, int to) { - if (mShowing) { - mOnRotateCallback.run(); - refreshDialog(); - } + super.refreshDialog(); } void hideLockMessage() { @@ -2682,15 +536,8 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, return isPanelDebugModeEnabled(context); } - private boolean controlsAvailable() { - return mDeviceProvisioned - && mControlsComponent.isEnabled() - && !mControlsServiceInfos.isEmpty(); - } - private boolean shouldShowLockMessage(ActionsDialog dialog) { - return mControlsComponent.getVisibility() == AVAILABLE_AFTER_UNLOCK - || isWalletAvailableAfterUnlock(dialog); + return isWalletAvailableAfterUnlock(dialog); } // Temporary while we move items out of the power menu @@ -2698,12 +545,12 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, boolean isLockedAfterBoot = mLockPatternUtils.getStrongAuthForUser(getCurrentUser().id) == STRONG_AUTH_REQUIRED_AFTER_BOOT; return !mKeyguardStateController.isUnlocked() - && (!mShowLockScreenCardsAndControls || isLockedAfterBoot) + && (!mShowLockScreenCards || isLockedAfterBoot) && dialog.isWalletViewAvailable(); } private void onPowerMenuLockScreenSettingsChanged() { - mShowLockScreenCardsAndControls = Settings.Secure.getInt(mContentResolver, + mShowLockScreenCards = mSecureSettings.getInt( Settings.Secure.POWER_MENU_LOCKED_SHOW_CONTENT, 0) != 0; } } diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java new file mode 100644 index 000000000000..47ae145590b5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java @@ -0,0 +1,2391 @@ +/* + * Copyright 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.globalactions; + +import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; +import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; +import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; +import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; +import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_GLOBAL_ACTIONS; +import static android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN; +import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_2BUTTON; + +import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST; +import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED; +import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_GLOBAL_ACTIONS_SHOWING; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.annotation.Nullable; +import android.app.ActivityManager; +import android.app.Dialog; +import android.app.IActivityManager; +import android.app.StatusBarManager; +import android.app.WallpaperManager; +import android.app.admin.DevicePolicyManager; +import android.app.trust.TrustManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.pm.UserInfo; +import android.content.res.ColorStateList; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.database.ContentObserver; +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.media.AudioManager; +import android.os.Binder; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.RemoteException; +import android.os.SystemProperties; +import android.os.UserHandle; +import android.os.UserManager; +import android.os.Vibrator; +import android.provider.Settings; +import android.service.dreams.IDreamManager; +import android.sysprop.TelephonyProperties; +import android.telecom.TelecomManager; +import android.telephony.ServiceState; +import android.telephony.TelephonyCallback; +import android.telephony.TelephonyManager; +import android.util.ArraySet; +import android.util.Log; +import android.view.ContextThemeWrapper; +import android.view.IWindowManager; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowInsets; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityEvent; +import android.widget.BaseAdapter; +import android.widget.ImageView; +import android.widget.ImageView.ScaleType; +import android.widget.LinearLayout; +import android.widget.ListPopupWindow; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LifecycleRegistry; + +import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.colorextraction.ColorExtractor; +import com.android.internal.colorextraction.ColorExtractor.GradientColors; +import com.android.internal.colorextraction.drawable.ScrimDrawable; +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.MetricsEvent; +import com.android.internal.statusbar.IStatusBarService; +import com.android.internal.util.EmergencyAffordanceManager; +import com.android.internal.util.ScreenshotHelper; +import com.android.internal.widget.LockPatternUtils; +import com.android.systemui.Interpolators; +import com.android.systemui.MultiListLayout; +import com.android.systemui.MultiListLayout.MultiListAdapter; +import com.android.systemui.broadcast.BroadcastDispatcher; +import com.android.systemui.colorextraction.SysuiColorExtractor; +import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.model.SysUiState; +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.policy.ConfigurationController; +import com.android.systemui.statusbar.policy.KeyguardStateController; +import com.android.systemui.telephony.TelephonyListenerManager; +import com.android.systemui.util.EmergencyDialerConstants; +import com.android.systemui.util.RingerModeTracker; +import com.android.systemui.util.settings.GlobalSettings; +import com.android.systemui.util.settings.SecureSettings; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; + +import javax.inject.Inject; + +/** + * Helper to show the global actions dialog. Each item is an {@link Action} that may show depending + * on whether the keyguard is showing, and whether the device is provisioned. + */ +public class GlobalActionsDialogLite implements DialogInterface.OnDismissListener, + DialogInterface.OnShowListener, + ConfigurationController.ConfigurationListener, + GlobalActionsPanelPlugin.Callbacks, + LifecycleOwner { + + public static final String SYSTEM_DIALOG_REASON_KEY = "reason"; + public static final String SYSTEM_DIALOG_REASON_GLOBAL_ACTIONS = "globalactions"; + public static final String SYSTEM_DIALOG_REASON_DREAM = "dream"; + + private static final String TAG = "GlobalActionsDialogLite"; + + private static final boolean SHOW_SILENT_TOGGLE = true; + + /* Valid settings for global actions keys. + * see config.xml config_globalActionList */ + @VisibleForTesting + static final String GLOBAL_ACTION_KEY_POWER = "power"; + private static final String GLOBAL_ACTION_KEY_AIRPLANE = "airplane"; + static final String GLOBAL_ACTION_KEY_BUGREPORT = "bugreport"; + private static final String GLOBAL_ACTION_KEY_SILENT = "silent"; + private static final String GLOBAL_ACTION_KEY_USERS = "users"; + private static final String GLOBAL_ACTION_KEY_SETTINGS = "settings"; + static final String GLOBAL_ACTION_KEY_LOCKDOWN = "lockdown"; + private static final String GLOBAL_ACTION_KEY_VOICEASSIST = "voiceassist"; + private static final String GLOBAL_ACTION_KEY_ASSIST = "assist"; + static final String GLOBAL_ACTION_KEY_RESTART = "restart"; + private static final String GLOBAL_ACTION_KEY_LOGOUT = "logout"; + static final String GLOBAL_ACTION_KEY_EMERGENCY = "emergency"; + static final String GLOBAL_ACTION_KEY_SCREENSHOT = "screenshot"; + + private final Context mContext; + private final GlobalActionsManager mWindowManagerFuncs; + private final AudioManager mAudioManager; + private final IDreamManager mDreamManager; + private final DevicePolicyManager mDevicePolicyManager; + private final LockPatternUtils mLockPatternUtils; + private final KeyguardStateController mKeyguardStateController; + private final BroadcastDispatcher mBroadcastDispatcher; + protected final GlobalSettings mGlobalSettings; + protected final SecureSettings mSecureSettings; + protected final Resources mResources; + private final ConfigurationController mConfigurationController; + private final UserManager mUserManager; + private final TrustManager mTrustManager; + private final IActivityManager mIActivityManager; + private final TelecomManager mTelecomManager; + private final MetricsLogger mMetricsLogger; + private final UiEventLogger mUiEventLogger; + private final NotificationShadeDepthController mDepthController; + private final SysUiState mSysUiState; + + // Used for RingerModeTracker + private final LifecycleRegistry mLifecycle = new LifecycleRegistry(this); + + @VisibleForTesting + protected final ArrayList<Action> mItems = new ArrayList<>(); + @VisibleForTesting + protected final ArrayList<Action> mOverflowItems = new ArrayList<>(); + @VisibleForTesting + protected final ArrayList<Action> mPowerItems = new ArrayList<>(); + + @VisibleForTesting + protected ActionsDialogLite mDialog; + + private Action mSilentModeAction; + private ToggleAction mAirplaneModeOn; + + protected MyAdapter mAdapter; + protected MyOverflowAdapter mOverflowAdapter; + protected MyPowerOptionsAdapter mPowerAdapter; + + private boolean mKeyguardShowing = false; + private boolean mDeviceProvisioned = false; + private ToggleState mAirplaneState = ToggleState.Off; + private boolean mIsWaitingForEcmExit = false; + private boolean mHasTelephony; + private boolean mHasVibrator; + private final boolean mShowSilentToggle; + private final EmergencyAffordanceManager mEmergencyAffordanceManager; + private final ScreenshotHelper mScreenshotHelper; + private final SysuiColorExtractor mSysuiColorExtractor; + private final IStatusBarService mStatusBarService; + protected final NotificationShadeWindowController mNotificationShadeWindowController; + private final IWindowManager mIWindowManager; + private final Executor mBackgroundExecutor; + private final RingerModeTracker mRingerModeTracker; + private int mDialogPressDelay = DIALOG_PRESS_DELAY; // ms + protected Handler mMainHandler; + private int mSmallestScreenWidthDp; + + @VisibleForTesting + public enum GlobalActionsEvent implements UiEventLogger.UiEventEnum { + @UiEvent(doc = "The global actions / power menu surface became visible on the screen.") + GA_POWER_MENU_OPEN(337), + + @UiEvent(doc = "The global actions / power menu surface was dismissed.") + GA_POWER_MENU_CLOSE(471), + + @UiEvent(doc = "The global actions bugreport button was pressed.") + GA_BUGREPORT_PRESS(344), + + @UiEvent(doc = "The global actions bugreport button was long pressed.") + GA_BUGREPORT_LONG_PRESS(345), + + @UiEvent(doc = "The global actions emergency button was pressed.") + GA_EMERGENCY_DIALER_PRESS(346), + + @UiEvent(doc = "The global actions screenshot button was pressed.") + GA_SCREENSHOT_PRESS(347), + + @UiEvent(doc = "The global actions screenshot button was long pressed.") + GA_SCREENSHOT_LONG_PRESS(348); + + private final int mId; + + GlobalActionsEvent(int id) { + mId = id; + } + + @Override + public int getId() { + return mId; + } + } + + /** + * @param context everything needs a context :( + */ + @Inject + public GlobalActionsDialogLite(Context context, GlobalActionsManager windowManagerFuncs, + AudioManager audioManager, IDreamManager iDreamManager, + DevicePolicyManager devicePolicyManager, LockPatternUtils lockPatternUtils, + BroadcastDispatcher broadcastDispatcher, + TelephonyListenerManager telephonyListenerManager, + GlobalSettings globalSettings, SecureSettings secureSettings, + @Nullable Vibrator vibrator, @Main Resources resources, + ConfigurationController configurationController, + KeyguardStateController keyguardStateController, UserManager userManager, + TrustManager trustManager, IActivityManager iActivityManager, + @Nullable TelecomManager telecomManager, MetricsLogger metricsLogger, + NotificationShadeDepthController depthController, SysuiColorExtractor colorExtractor, + IStatusBarService statusBarService, + NotificationShadeWindowController notificationShadeWindowController, + IWindowManager iWindowManager, + @Background Executor backgroundExecutor, + UiEventLogger uiEventLogger, + RingerModeTracker ringerModeTracker, SysUiState sysUiState, @Main Handler handler) { + mContext = context; + mWindowManagerFuncs = windowManagerFuncs; + mAudioManager = audioManager; + mDreamManager = iDreamManager; + mDevicePolicyManager = devicePolicyManager; + mLockPatternUtils = lockPatternUtils; + mKeyguardStateController = keyguardStateController; + mBroadcastDispatcher = broadcastDispatcher; + mGlobalSettings = globalSettings; + mSecureSettings = secureSettings; + mResources = resources; + mConfigurationController = configurationController; + mUserManager = userManager; + mTrustManager = trustManager; + mIActivityManager = iActivityManager; + mTelecomManager = telecomManager; + mMetricsLogger = metricsLogger; + mUiEventLogger = uiEventLogger; + mDepthController = depthController; + mSysuiColorExtractor = colorExtractor; + mStatusBarService = statusBarService; + mNotificationShadeWindowController = notificationShadeWindowController; + mIWindowManager = iWindowManager; + mBackgroundExecutor = backgroundExecutor; + mRingerModeTracker = ringerModeTracker; + mSysUiState = sysUiState; + mMainHandler = handler; + mSmallestScreenWidthDp = mContext.getResources().getConfiguration().smallestScreenWidthDp; + + // receive broadcasts + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); + filter.addAction(Intent.ACTION_SCREEN_OFF); + filter.addAction(TelephonyManager.ACTION_EMERGENCY_CALLBACK_MODE_CHANGED); + mBroadcastDispatcher.registerReceiver(mBroadcastReceiver, filter); + + mHasTelephony = + context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY); + + // get notified of phone state changes + telephonyListenerManager.addServiceStateListener(mPhoneStateListener); + mGlobalSettings.registerContentObserver( + Settings.Global.getUriFor(Settings.Global.AIRPLANE_MODE_ON), true, + mAirplaneModeObserver); + mHasVibrator = vibrator != null && vibrator.hasVibrator(); + + mShowSilentToggle = SHOW_SILENT_TOGGLE && !resources.getBoolean( + R.bool.config_useFixedVolume); + if (mShowSilentToggle) { + mRingerModeTracker.getRingerMode().observe(this, ringer -> + mHandler.sendEmptyMessage(MESSAGE_REFRESH) + ); + } + + mEmergencyAffordanceManager = new EmergencyAffordanceManager(context); + mScreenshotHelper = new ScreenshotHelper(context); + + mConfigurationController.addCallback(this); + } + + protected Context getContext() { + return mContext; + } + + /** + * Show the global actions dialog (creating if necessary) + * + * @param keyguardShowing True if keyguard is showing + */ + public void showOrHideDialog(boolean keyguardShowing, boolean isDeviceProvisioned) { + mKeyguardShowing = keyguardShowing; + mDeviceProvisioned = isDeviceProvisioned; + if (mDialog != null && mDialog.isShowing()) { + // In order to force global actions to hide on the same affordance press, we must + // register a call to onGlobalActionsShown() first to prevent the default actions + // menu from showing. This will be followed by a subsequent call to + // onGlobalActionsHidden() on dismiss() + mWindowManagerFuncs.onGlobalActionsShown(); + mDialog.dismiss(); + mDialog = null; + } else { + handleShow(); + } + } + + protected boolean isKeyguardShowing() { + return mKeyguardShowing; + } + + protected boolean isDeviceProvisioned() { + return mDeviceProvisioned; + } + + /** + * Dismiss the global actions dialog, if it's currently shown + */ + public void dismissDialog() { + mHandler.removeMessages(MESSAGE_DISMISS); + mHandler.sendEmptyMessage(MESSAGE_DISMISS); + } + + protected void awakenIfNecessary() { + if (mDreamManager != null) { + try { + if (mDreamManager.isDreaming()) { + mDreamManager.awaken(); + } + } catch (RemoteException e) { + // we tried + } + } + } + + protected void handleShow() { + awakenIfNecessary(); + mDialog = createDialog(); + prepareDialog(); + + WindowManager.LayoutParams attrs = mDialog.getWindow().getAttributes(); + attrs.setTitle("ActionsDialog"); + attrs.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; + mDialog.getWindow().setAttributes(attrs); + // Don't acquire soft keyboard focus, to avoid destroying state when capturing bugreports + mDialog.getWindow().setFlags(FLAG_ALT_FOCUSABLE_IM, FLAG_ALT_FOCUSABLE_IM); + mDialog.show(); + mWindowManagerFuncs.onGlobalActionsShown(); + } + + @VisibleForTesting + protected boolean shouldShowAction(Action action) { + if (mKeyguardShowing && !action.showDuringKeyguard()) { + return false; + } + if (!mDeviceProvisioned && !action.showBeforeProvisioning()) { + return false; + } + return action.shouldShow(); + } + + /** + * Returns the maximum number of power menu items to show based on which GlobalActions + * layout is being used. + */ + @VisibleForTesting + protected int getMaxShownPowerItems() { + return mResources.getInteger(com.android.systemui.R.integer.power_menu_lite_max_columns) + * mResources.getInteger(com.android.systemui.R.integer.power_menu_lite_max_rows); + } + + /** + * Add a power menu action item for to either the main or overflow items lists, depending on + * whether controls are enabled and whether the max number of shown items has been reached. + */ + private void addActionItem(Action action) { + if (mItems.size() < getMaxShownPowerItems()) { + mItems.add(action); + } else { + mOverflowItems.add(action); + } + } + + @VisibleForTesting + protected String[] getDefaultActions() { + return mResources.getStringArray(R.array.config_globalActionsList); + } + + private void addIfShouldShowAction(List<Action> actions, Action action) { + if (shouldShowAction(action)) { + actions.add(action); + } + } + + @VisibleForTesting + protected void createActionItems() { + // Simple toggle style if there's no vibrator, otherwise use a tri-state + if (!mHasVibrator) { + mSilentModeAction = new SilentModeToggleAction(); + } else { + mSilentModeAction = new SilentModeTriStateAction(mAudioManager, mHandler); + } + mAirplaneModeOn = new AirplaneModeAction(); + onAirplaneModeChanged(); + + mItems.clear(); + mOverflowItems.clear(); + mPowerItems.clear(); + String[] defaultActions = getDefaultActions(); + + ShutDownAction shutdownAction = new ShutDownAction(); + RestartAction restartAction = new RestartAction(); + ArraySet<String> addedKeys = new ArraySet<>(); + List<Action> tempActions = new ArrayList<>(); + CurrentUserProvider currentUser = new CurrentUserProvider(); + + // make sure emergency affordance action is first, if needed + if (mEmergencyAffordanceManager.needsEmergencyAffordance()) { + addIfShouldShowAction(tempActions, new EmergencyAffordanceAction()); + addedKeys.add(GLOBAL_ACTION_KEY_EMERGENCY); + } + + for (int i = 0; i < defaultActions.length; i++) { + String actionKey = defaultActions[i]; + if (addedKeys.contains(actionKey)) { + // If we already have added this, don't add it again. + continue; + } + if (GLOBAL_ACTION_KEY_POWER.equals(actionKey)) { + addIfShouldShowAction(tempActions, shutdownAction); + } else if (GLOBAL_ACTION_KEY_AIRPLANE.equals(actionKey)) { + addIfShouldShowAction(tempActions, mAirplaneModeOn); + } else if (GLOBAL_ACTION_KEY_BUGREPORT.equals(actionKey)) { + if (shouldDisplayBugReport(currentUser.get())) { + addIfShouldShowAction(tempActions, new BugReportAction()); + } + } else if (GLOBAL_ACTION_KEY_SILENT.equals(actionKey)) { + if (mShowSilentToggle) { + addIfShouldShowAction(tempActions, mSilentModeAction); + } + } else if (GLOBAL_ACTION_KEY_USERS.equals(actionKey)) { + if (SystemProperties.getBoolean("fw.power_user_switcher", false)) { + addUserActions(tempActions, currentUser.get()); + } + } else if (GLOBAL_ACTION_KEY_SETTINGS.equals(actionKey)) { + addIfShouldShowAction(tempActions, getSettingsAction()); + } else if (GLOBAL_ACTION_KEY_LOCKDOWN.equals(actionKey)) { + if (shouldDisplayLockdown(currentUser.get())) { + addIfShouldShowAction(tempActions, new LockDownAction()); + } + } else if (GLOBAL_ACTION_KEY_VOICEASSIST.equals(actionKey)) { + addIfShouldShowAction(tempActions, getVoiceAssistAction()); + } else if (GLOBAL_ACTION_KEY_ASSIST.equals(actionKey)) { + addIfShouldShowAction(tempActions, getAssistAction()); + } else if (GLOBAL_ACTION_KEY_RESTART.equals(actionKey)) { + addIfShouldShowAction(tempActions, restartAction); + } else if (GLOBAL_ACTION_KEY_SCREENSHOT.equals(actionKey)) { + addIfShouldShowAction(tempActions, new ScreenshotAction()); + } else if (GLOBAL_ACTION_KEY_LOGOUT.equals(actionKey)) { + if (mDevicePolicyManager.isLogoutEnabled() + && currentUser.get() != null + && currentUser.get().id != UserHandle.USER_SYSTEM) { + addIfShouldShowAction(tempActions, new LogoutAction()); + } + } else if (GLOBAL_ACTION_KEY_EMERGENCY.equals(actionKey)) { + addIfShouldShowAction(tempActions, new EmergencyDialerAction()); + } else { + Log.e(TAG, "Invalid global action key " + actionKey); + } + // Add here so we don't add more than one. + addedKeys.add(actionKey); + } + + // replace power and restart with a single power options action, if needed + if (tempActions.contains(shutdownAction) && tempActions.contains(restartAction) + && tempActions.size() > getMaxShownPowerItems()) { + // transfer shutdown and restart to their own list of power actions + int powerOptionsIndex = Math.min(tempActions.indexOf(restartAction), + tempActions.indexOf(shutdownAction)); + tempActions.remove(shutdownAction); + tempActions.remove(restartAction); + mPowerItems.add(shutdownAction); + mPowerItems.add(restartAction); + + // add the PowerOptionsAction after Emergency, if present + tempActions.add(powerOptionsIndex, new PowerOptionsAction()); + } + for (Action action : tempActions) { + addActionItem(action); + } + } + + protected void onRotate() { + // re-allocate actions between main and overflow lists + this.createActionItems(); + } + + protected void initDialogItems() { + createActionItems(); + mAdapter = new MyAdapter(); + mOverflowAdapter = new MyOverflowAdapter(); + mPowerAdapter = new MyPowerOptionsAdapter(); + } + + /** + * Create the global actions dialog. + * + * @return A new dialog. + */ + 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, + mStatusBarService, mNotificationShadeWindowController, + mSysUiState, this::onRotate, mKeyguardShowing, mPowerAdapter); + + dialog.setCanceledOnTouchOutside(true); + dialog.setOnDismissListener(this); + dialog.setOnShowListener(this); + + return dialog; + } + + @VisibleForTesting + boolean shouldDisplayLockdown(UserInfo user) { + if (user == null) { + return false; + } + + int userId = user.id; + + // No lockdown option if it's not turned on in Settings + if (mSecureSettings.getIntForUser(Settings.Secure.LOCKDOWN_IN_POWER_MENU, 0, userId) == 0) { + return false; + } + + // Lockdown is meaningless without a place to go. + if (!mKeyguardStateController.isMethodSecure()) { + return false; + } + + // Only show the lockdown button if the device isn't locked down (for whatever reason). + int state = mLockPatternUtils.getStrongAuthForUser(userId); + return (state == STRONG_AUTH_NOT_REQUIRED + || state == SOME_AUTH_REQUIRED_AFTER_USER_REQUEST); + } + + @VisibleForTesting + boolean shouldDisplayBugReport(UserInfo currentUser) { + return mGlobalSettings.getInt(Settings.Global.BUGREPORT_IN_POWER_MENU, 0) != 0 + && (currentUser == null || currentUser.isPrimary()); + } + + @Override + public void onUiModeChanged() { + mContext.getTheme().applyStyle(mContext.getThemeResId(), true); + if (mDialog != null && mDialog.isShowing()) { + mDialog.refreshDialog(); + } + } + + @Override + public void onConfigChanged(Configuration newConfig) { + if (mDialog != null && mDialog.isShowing() + && (newConfig.smallestScreenWidthDp != mSmallestScreenWidthDp)) { + mSmallestScreenWidthDp = newConfig.smallestScreenWidthDp; + mDialog.refreshDialog(); + } + } + + /** + * Clean up callbacks + */ + public void destroy() { + mConfigurationController.removeCallback(this); + } + + /** + * Implements {@link GlobalActionsPanelPlugin.Callbacks#dismissGlobalActionsMenu()}, which is + * called when the quick access wallet requests dismissal. + */ + @Override + public void dismissGlobalActionsMenu() { + dismissDialog(); + } + + @VisibleForTesting + protected final class PowerOptionsAction extends SinglePressAction { + private PowerOptionsAction() { + super(com.android.systemui.R.drawable.ic_settings_power, + R.string.global_action_power_options); + } + + @Override + public boolean showDuringKeyguard() { + return true; + } + + @Override + public boolean showBeforeProvisioning() { + return true; + } + + @Override + public void onPress() { + if (mDialog != null) { + mDialog.showPowerOptionsMenu(); + } + } + } + + @VisibleForTesting + final class ShutDownAction extends SinglePressAction implements LongPressAction { + private ShutDownAction() { + super(R.drawable.ic_lock_power_off, + R.string.global_action_power_off); + } + + @Override + public boolean onLongPress() { + if (!mUserManager.hasUserRestriction(UserManager.DISALLOW_SAFE_BOOT)) { + mWindowManagerFuncs.reboot(true); + return true; + } + return false; + } + + @Override + public boolean showDuringKeyguard() { + return true; + } + + @Override + public boolean showBeforeProvisioning() { + return true; + } + + @Override + public void onPress() { + // shutdown by making sure radio and power are handled accordingly. + mWindowManagerFuncs.shutdown(); + } + } + + @VisibleForTesting + protected abstract class EmergencyAction extends SinglePressAction { + EmergencyAction(int iconResId, int messageResId) { + super(iconResId, messageResId); + } + + @Override + public boolean shouldBeSeparated() { + return false; + } + + @Override + public View create( + Context context, View convertView, ViewGroup parent, LayoutInflater inflater) { + View v = super.create(context, convertView, parent, inflater); + int textColor = getEmergencyTextColor(context); + int iconColor = getEmergencyIconColor(context); + int backgroundColor = getEmergencyBackgroundColor(context); + TextView messageView = v.findViewById(R.id.message); + messageView.setTextColor(textColor); + messageView.setSelected(true); // necessary for marquee to work + ImageView icon = v.findViewById(R.id.icon); + icon.getDrawable().setTint(iconColor); + icon.setBackgroundTintList(ColorStateList.valueOf(backgroundColor)); + v.setBackgroundTintList(ColorStateList.valueOf(backgroundColor)); + return v; + } + + @Override + public boolean showDuringKeyguard() { + return true; + } + + @Override + public boolean showBeforeProvisioning() { + return true; + } + } + + protected int getEmergencyTextColor(Context context) { + return context.getResources().getColor( + com.android.systemui.R.color.global_actions_lite_text); + } + + protected int getEmergencyIconColor(Context context) { + return context.getResources().getColor( + com.android.systemui.R.color.global_actions_lite_emergency_icon); + } + + protected int getEmergencyBackgroundColor(Context context) { + return context.getResources().getColor( + com.android.systemui.R.color.global_actions_lite_emergency_background); + } + + private class EmergencyAffordanceAction extends EmergencyAction { + EmergencyAffordanceAction() { + super(R.drawable.emergency_icon, + R.string.global_action_emergency); + } + + @Override + public void onPress() { + mEmergencyAffordanceManager.performEmergencyCall(); + } + } + + @VisibleForTesting + class EmergencyDialerAction extends EmergencyAction { + private EmergencyDialerAction() { + super(com.android.systemui.R.drawable.ic_emergency_star, + R.string.global_action_emergency); + } + + @Override + public void onPress() { + mMetricsLogger.action(MetricsEvent.ACTION_EMERGENCY_DIALER_FROM_POWER_MENU); + mUiEventLogger.log(GlobalActionsEvent.GA_EMERGENCY_DIALER_PRESS); + if (mTelecomManager != null) { + Intent intent = mTelecomManager.createLaunchEmergencyDialerIntent( + null /* number */); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS + | Intent.FLAG_ACTIVITY_CLEAR_TOP); + intent.putExtra(EmergencyDialerConstants.EXTRA_ENTRY_TYPE, + EmergencyDialerConstants.ENTRY_TYPE_POWER_MENU); + mContext.startActivityAsUser(intent, UserHandle.CURRENT); + } + } + } + + @VisibleForTesting + EmergencyDialerAction makeEmergencyDialerActionForTesting() { + return new EmergencyDialerAction(); + } + + @VisibleForTesting + final class RestartAction extends SinglePressAction implements LongPressAction { + private RestartAction() { + super(R.drawable.ic_restart, R.string.global_action_restart); + } + + @Override + public boolean onLongPress() { + if (!mUserManager.hasUserRestriction(UserManager.DISALLOW_SAFE_BOOT)) { + mWindowManagerFuncs.reboot(true); + return true; + } + return false; + } + + @Override + public boolean showDuringKeyguard() { + return true; + } + + @Override + public boolean showBeforeProvisioning() { + return true; + } + + @Override + public void onPress() { + mWindowManagerFuncs.reboot(false); + } + } + + @VisibleForTesting + class ScreenshotAction extends SinglePressAction { + ScreenshotAction() { + super(R.drawable.ic_screenshot, R.string.global_action_screenshot); + } + + @Override + public void onPress() { + // Add a little delay before executing, to give the + // dialog a chance to go away before it takes a + // screenshot. + // TODO: instead, omit global action dialog layer + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + mScreenshotHelper.takeScreenshot(TAKE_SCREENSHOT_FULLSCREEN, true, true, + SCREENSHOT_GLOBAL_ACTIONS, mHandler, null); + mMetricsLogger.action(MetricsEvent.ACTION_SCREENSHOT_POWER_MENU); + mUiEventLogger.log(GlobalActionsEvent.GA_SCREENSHOT_PRESS); + } + }, mDialogPressDelay); + } + + @Override + public boolean showDuringKeyguard() { + return true; + } + + @Override + public boolean showBeforeProvisioning() { + return false; + } + + @Override + public boolean shouldShow() { + // Include screenshot in power menu for legacy nav because it is not accessible + // through Recents in that mode + return is2ButtonNavigationEnabled(); + } + + boolean is2ButtonNavigationEnabled() { + return NAV_BAR_MODE_2BUTTON == mContext.getResources().getInteger( + com.android.internal.R.integer.config_navBarInteractionMode); + } + } + + @VisibleForTesting + ScreenshotAction makeScreenshotActionForTesting() { + return new ScreenshotAction(); + } + + @VisibleForTesting + class BugReportAction extends SinglePressAction implements LongPressAction { + + BugReportAction() { + super(R.drawable.ic_lock_bugreport, R.string.bugreport_title); + } + + @Override + public void onPress() { + // don't actually trigger the bugreport if we are running stability + // tests via monkey + if (ActivityManager.isUserAMonkey()) { + return; + } + // Add a little delay before executing, to give the + // dialog a chance to go away before it takes a + // screenshot. + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + try { + // Take an "interactive" bugreport. + mMetricsLogger.action( + MetricsEvent.ACTION_BUGREPORT_FROM_POWER_MENU_INTERACTIVE); + mUiEventLogger.log(GlobalActionsEvent.GA_BUGREPORT_PRESS); + if (!mIActivityManager.launchBugReportHandlerApp()) { + Log.w(TAG, "Bugreport handler could not be launched"); + mIActivityManager.requestInteractiveBugReport(); + } + } catch (RemoteException e) { + } + } + }, mDialogPressDelay); + } + + @Override + public boolean onLongPress() { + // don't actually trigger the bugreport if we are running stability + // tests via monkey + if (ActivityManager.isUserAMonkey()) { + return false; + } + try { + // Take a "full" bugreport. + mMetricsLogger.action(MetricsEvent.ACTION_BUGREPORT_FROM_POWER_MENU_FULL); + mUiEventLogger.log(GlobalActionsEvent.GA_BUGREPORT_LONG_PRESS); + mIActivityManager.requestFullBugReport(); + } catch (RemoteException e) { + } + return false; + } + + public boolean showDuringKeyguard() { + return true; + } + + @Override + public boolean showBeforeProvisioning() { + return false; + } + } + + @VisibleForTesting + BugReportAction makeBugReportActionForTesting() { + return new BugReportAction(); + } + + private final class LogoutAction extends SinglePressAction { + private LogoutAction() { + super(R.drawable.ic_logout, R.string.global_action_logout); + } + + @Override + public boolean showDuringKeyguard() { + return true; + } + + @Override + public boolean showBeforeProvisioning() { + return false; + } + + @Override + public void onPress() { + // Add a little delay before executing, to give the dialog a chance to go away before + // switching user + mHandler.postDelayed(() -> { + try { + int currentUserId = getCurrentUser().id; + mIActivityManager.switchUser(UserHandle.USER_SYSTEM); + mIActivityManager.stopUser(currentUserId, true /*force*/, null); + } catch (RemoteException re) { + Log.e(TAG, "Couldn't logout user " + re); + } + }, mDialogPressDelay); + } + } + + private Action getSettingsAction() { + return new SinglePressAction(R.drawable.ic_settings, + R.string.global_action_settings) { + + @Override + public void onPress() { + Intent intent = new Intent(Settings.ACTION_SETTINGS); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + mContext.startActivity(intent); + } + + @Override + public boolean showDuringKeyguard() { + return true; + } + + @Override + public boolean showBeforeProvisioning() { + return true; + } + }; + } + + private Action getAssistAction() { + return new SinglePressAction(R.drawable.ic_action_assist_focused, + R.string.global_action_assist) { + @Override + public void onPress() { + Intent intent = new Intent(Intent.ACTION_ASSIST); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + mContext.startActivity(intent); + } + + @Override + public boolean showDuringKeyguard() { + return true; + } + + @Override + public boolean showBeforeProvisioning() { + return true; + } + }; + } + + private Action getVoiceAssistAction() { + return new SinglePressAction(R.drawable.ic_voice_search, + R.string.global_action_voice_assist) { + @Override + public void onPress() { + Intent intent = new Intent(Intent.ACTION_VOICE_ASSIST); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + mContext.startActivity(intent); + } + + @Override + public boolean showDuringKeyguard() { + return true; + } + + @Override + public boolean showBeforeProvisioning() { + return true; + } + }; + } + + @VisibleForTesting + class LockDownAction extends SinglePressAction { + LockDownAction() { + super(R.drawable.ic_lock_lockdown, R.string.global_action_lockdown); + } + + @Override + public void onPress() { + mLockPatternUtils.requireStrongAuth(STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN, + UserHandle.USER_ALL); + try { + mIWindowManager.lockNow(null); + // Lock profiles (if any) on the background thread. + mBackgroundExecutor.execute(() -> lockProfiles()); + } catch (RemoteException e) { + Log.e(TAG, "Error while trying to lock device.", e); + } + } + + @Override + public boolean showDuringKeyguard() { + return true; + } + + @Override + public boolean showBeforeProvisioning() { + return false; + } + } + + private void lockProfiles() { + final int currentUserId = getCurrentUser().id; + final int[] profileIds = mUserManager.getEnabledProfileIds(currentUserId); + for (final int id : profileIds) { + if (id != currentUserId) { + mTrustManager.setDeviceLockedForUser(id, true); + } + } + } + + protected UserInfo getCurrentUser() { + try { + return mIActivityManager.getCurrentUser(); + } catch (RemoteException re) { + return null; + } + } + + /** + * Non-thread-safe current user provider that caches the result - helpful when a method needs + * to fetch it an indeterminate number of times. + */ + private class CurrentUserProvider { + private UserInfo mUserInfo = null; + private boolean mFetched = false; + + @Nullable + UserInfo get() { + if (!mFetched) { + mFetched = true; + mUserInfo = getCurrentUser(); + } + return mUserInfo; + } + } + + private void addUserActions(List<Action> actions, UserInfo currentUser) { + if (mUserManager.isUserSwitcherEnabled()) { + List<UserInfo> users = mUserManager.getUsers(); + for (final UserInfo user : users) { + if (user.supportsSwitchToByUser()) { + boolean isCurrentUser = currentUser == null + ? user.id == 0 : (currentUser.id == user.id); + Drawable icon = user.iconPath != null ? Drawable.createFromPath(user.iconPath) + : null; + SinglePressAction switchToUser = new SinglePressAction( + R.drawable.ic_menu_cc, icon, + (user.name != null ? user.name : "Primary") + + (isCurrentUser ? " \u2714" : "")) { + public void onPress() { + try { + mIActivityManager.switchUser(user.id); + } catch (RemoteException re) { + Log.e(TAG, "Couldn't switch user " + re); + } + } + + public boolean showDuringKeyguard() { + return true; + } + + public boolean showBeforeProvisioning() { + return false; + } + }; + addIfShouldShowAction(actions, switchToUser); + } + } + } + } + + protected void prepareDialog() { + refreshSilentMode(); + mAirplaneModeOn.updateState(mAirplaneState); + mAdapter.notifyDataSetChanged(); + mLifecycle.setCurrentState(Lifecycle.State.RESUMED); + } + + private void refreshSilentMode() { + if (!mHasVibrator) { + Integer value = mRingerModeTracker.getRingerMode().getValue(); + final boolean silentModeOn = value != null && value != AudioManager.RINGER_MODE_NORMAL; + ((ToggleAction) mSilentModeAction).updateState( + silentModeOn ? ToggleState.On : ToggleState.Off); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void onDismiss(DialogInterface dialog) { + if (mDialog == dialog) { + mDialog = null; + } + mUiEventLogger.log(GlobalActionsEvent.GA_POWER_MENU_CLOSE); + mWindowManagerFuncs.onGlobalActionsHidden(); + mLifecycle.setCurrentState(Lifecycle.State.CREATED); + } + + /** + * {@inheritDoc} + */ + @Override + public void onShow(DialogInterface dialog) { + mMetricsLogger.visible(MetricsEvent.POWER_MENU); + mUiEventLogger.log(GlobalActionsEvent.GA_POWER_MENU_OPEN); + } + + /** + * The adapter used for power menu items shown in the global actions dialog. + */ + public class MyAdapter extends MultiListAdapter { + private int countItems(boolean separated) { + int count = 0; + for (int i = 0; i < mItems.size(); i++) { + final Action action = mItems.get(i); + + if (action.shouldBeSeparated() == separated) { + count++; + } + } + return count; + } + + @Override + public int countSeparatedItems() { + return countItems(true); + } + + @Override + public int countListItems() { + return countItems(false); + } + + @Override + public int getCount() { + return countSeparatedItems() + countListItems(); + } + + @Override + public boolean isEnabled(int position) { + return getItem(position).isEnabled(); + } + + @Override + public boolean areAllItemsEnabled() { + return false; + } + + @Override + public Action getItem(int position) { + int filteredPos = 0; + for (int i = 0; i < mItems.size(); i++) { + final Action action = mItems.get(i); + if (!shouldShowAction(action)) { + continue; + } + if (filteredPos == position) { + return action; + } + filteredPos++; + } + + throw new IllegalArgumentException("position " + position + + " out of range of showable actions" + + ", filtered count=" + getCount() + + ", keyguardshowing=" + mKeyguardShowing + + ", provisioned=" + mDeviceProvisioned); + } + + /** + * Get the row ID for an item + * @param position The position of the item within the adapter's data set + * @return + */ + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + Action action = getItem(position); + View view = action.create(mContext, convertView, parent, LayoutInflater.from(mContext)); + view.setOnClickListener(v -> onClickItem(position)); + if (action instanceof LongPressAction) { + view.setOnLongClickListener(v -> onLongClickItem(position)); + } + return view; + } + + @Override + public boolean onLongClickItem(int position) { + final Action action = mAdapter.getItem(position); + if (action instanceof LongPressAction) { + if (mDialog != null) { + mDialog.dismiss(); + } else { + Log.w(TAG, "Action long-clicked while mDialog is null."); + } + return ((LongPressAction) action).onLongPress(); + } + return false; + } + + @Override + public void onClickItem(int position) { + Action item = mAdapter.getItem(position); + if (!(item instanceof SilentModeTriStateAction)) { + if (mDialog != null) { + // don't dismiss the dialog if we're opening the power options menu + if (!(item instanceof PowerOptionsAction)) { + mDialog.dismiss(); + } + } else { + Log.w(TAG, "Action clicked while mDialog is null."); + } + item.onPress(); + } + } + + @Override + public boolean shouldBeSeparated(int position) { + return getItem(position).shouldBeSeparated(); + } + } + + /** + * The adapter used for items in the overflow menu. + */ + public class MyPowerOptionsAdapter extends BaseAdapter { + @Override + public int getCount() { + return mPowerItems.size(); + } + + @Override + public Action getItem(int position) { + return mPowerItems.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + Action action = getItem(position); + if (action == null) { + Log.w(TAG, "No power options action found at position: " + position); + return null; + } + int viewLayoutResource = com.android.systemui.R.layout.global_actions_power_item; + View view = convertView != null ? convertView + : LayoutInflater.from(mContext).inflate(viewLayoutResource, parent, false); + view.setOnClickListener(v -> onClickItem(position)); + if (action instanceof LongPressAction) { + view.setOnLongClickListener(v -> onLongClickItem(position)); + } + ImageView icon = view.findViewById(R.id.icon); + TextView messageView = view.findViewById(R.id.message); + messageView.setSelected(true); // necessary for marquee to work + + icon.setImageDrawable(action.getIcon(mContext)); + icon.setScaleType(ScaleType.CENTER_CROP); + + if (action.getMessage() != null) { + messageView.setText(action.getMessage()); + } else { + messageView.setText(action.getMessageResId()); + } + return view; + } + + private boolean onLongClickItem(int position) { + final Action action = getItem(position); + if (action instanceof LongPressAction) { + if (mDialog != null) { + mDialog.dismiss(); + } else { + Log.w(TAG, "Action long-clicked while mDialog is null."); + } + return ((LongPressAction) action).onLongPress(); + } + return false; + } + + private void onClickItem(int position) { + Action item = getItem(position); + if (!(item instanceof SilentModeTriStateAction)) { + if (mDialog != null) { + mDialog.dismiss(); + } else { + Log.w(TAG, "Action clicked while mDialog is null."); + } + item.onPress(); + } + } + } + + /** + * The adapter used for items in the power options menu, triggered by the PowerOptionsAction. + */ + public class MyOverflowAdapter extends BaseAdapter { + @Override + public int getCount() { + return mOverflowItems.size(); + } + + @Override + public Action getItem(int position) { + return mOverflowItems.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + Action action = getItem(position); + if (action == null) { + Log.w(TAG, "No overflow action found at position: " + position); + return null; + } + int viewLayoutResource = com.android.systemui.R.layout.controls_more_item; + View view = convertView != null ? convertView + : LayoutInflater.from(mContext).inflate(viewLayoutResource, parent, false); + TextView textView = (TextView) view; + if (action.getMessageResId() != 0) { + textView.setText(action.getMessageResId()); + } else { + textView.setText(action.getMessage()); + } + return textView; + } + + protected boolean onLongClickItem(int position) { + final Action action = getItem(position); + if (action instanceof LongPressAction) { + if (mDialog != null) { + mDialog.dismiss(); + } else { + Log.w(TAG, "Action long-clicked while mDialog is null."); + } + return ((LongPressAction) action).onLongPress(); + } + return false; + } + + protected void onClickItem(int position) { + Action item = getItem(position); + if (!(item instanceof SilentModeTriStateAction)) { + if (mDialog != null) { + mDialog.dismiss(); + } else { + Log.w(TAG, "Action clicked while mDialog is null."); + } + item.onPress(); + } + } + } + + // note: the scheme below made more sense when we were planning on having + // 8 different things in the global actions dialog. seems overkill with + // only 3 items now, but may as well keep this flexible approach so it will + // be easy should someone decide at the last minute to include something + // else, such as 'enable wifi', or 'enable bluetooth' + + /** + * What each item in the global actions dialog must be able to support. + */ + public interface Action { + /** + * @return Text that will be announced when dialog is created. null for none. + */ + CharSequence getLabelForAccessibility(Context context); + + /** + * Create the item's view + * @param context + * @param convertView + * @param parent + * @param inflater + * @return + */ + View create(Context context, View convertView, ViewGroup parent, LayoutInflater inflater); + + /** + * Handle a regular press + */ + void onPress(); + + /** + * @return whether this action should appear in the dialog when the keygaurd is showing. + */ + boolean showDuringKeyguard(); + + /** + * @return whether this action should appear in the dialog before the + * device is provisioned.f + */ + boolean showBeforeProvisioning(); + + /** + * @return whether this action is enabled + */ + boolean isEnabled(); + + /** + * @return whether this action should be in a separate section + */ + default boolean shouldBeSeparated() { + return false; + } + + /** + * Return the id of the message associated with this action, or 0 if it doesn't have one. + * @return + */ + int getMessageResId(); + + /** + * Return the icon drawable for this action. + */ + Drawable getIcon(Context context); + + /** + * Return the message associated with this action, or null if it doesn't have one. + * @return + */ + CharSequence getMessage(); + + /** + * @return whether the action should be visible + */ + default boolean shouldShow() { + return true; + } + } + + /** + * An action that also supports long press. + */ + private interface LongPressAction extends Action { + boolean onLongPress(); + } + + /** + * A single press action maintains no state, just responds to a press and takes an action. + */ + + private abstract class SinglePressAction implements Action { + private final int mIconResId; + private final Drawable mIcon; + private final int mMessageResId; + private final CharSequence mMessage; + + protected SinglePressAction(int iconResId, int messageResId) { + mIconResId = iconResId; + mMessageResId = messageResId; + mMessage = null; + mIcon = null; + } + + protected SinglePressAction(int iconResId, Drawable icon, CharSequence message) { + mIconResId = iconResId; + mMessageResId = 0; + mMessage = message; + mIcon = icon; + } + + public boolean isEnabled() { + return true; + } + + public String getStatus() { + return null; + } + + public abstract void onPress(); + + public CharSequence getLabelForAccessibility(Context context) { + if (mMessage != null) { + return mMessage; + } else { + return context.getString(mMessageResId); + } + } + + public int getMessageResId() { + return mMessageResId; + } + + public CharSequence getMessage() { + return mMessage; + } + + @Override + public Drawable getIcon(Context context) { + if (mIcon != null) { + return mIcon; + } else { + return context.getDrawable(mIconResId); + } + } + + public View create( + Context context, View convertView, ViewGroup parent, LayoutInflater inflater) { + View v = inflater.inflate(getGridItemLayoutResource(), parent, false /* attach */); + // ConstraintLayout flow needs an ID to reference + v.setId(View.generateViewId()); + + ImageView icon = v.findViewById(R.id.icon); + TextView messageView = v.findViewById(R.id.message); + messageView.setSelected(true); // necessary for marquee to work + + icon.setImageDrawable(getIcon(context)); + icon.setScaleType(ScaleType.CENTER_CROP); + + if (mMessage != null) { + messageView.setText(mMessage); + } else { + messageView.setText(mMessageResId); + } + + return v; + } + } + + protected int getGridItemLayoutResource() { + return com.android.systemui.R.layout.global_actions_grid_item_lite; + } + + private enum ToggleState { + Off(false), + TurningOn(true), + TurningOff(true), + On(false); + + private final boolean mInTransition; + + ToggleState(boolean intermediate) { + mInTransition = intermediate; + } + + public boolean inTransition() { + return mInTransition; + } + } + + /** + * A toggle action knows whether it is on or off, and displays an icon and status message + * accordingly. + */ + private abstract class ToggleAction implements Action { + + protected ToggleState mState = ToggleState.Off; + + // prefs + protected int mEnabledIconResId; + protected int mDisabledIconResid; + protected int mMessageResId; + protected int mEnabledStatusMessageResId; + protected int mDisabledStatusMessageResId; + + /** + * @param enabledIconResId The icon for when this action is on. + * @param disabledIconResid The icon for when this action is off. + * @param message The general information message, e.g 'Silent Mode' + * @param enabledStatusMessageResId The on status message, e.g 'sound disabled' + * @param disabledStatusMessageResId The off status message, e.g. 'sound enabled' + */ + ToggleAction(int enabledIconResId, + int disabledIconResid, + int message, + int enabledStatusMessageResId, + int disabledStatusMessageResId) { + mEnabledIconResId = enabledIconResId; + mDisabledIconResid = disabledIconResid; + mMessageResId = message; + mEnabledStatusMessageResId = enabledStatusMessageResId; + mDisabledStatusMessageResId = disabledStatusMessageResId; + } + + /** + * Override to make changes to resource IDs just before creating the View. + */ + void willCreate() { + + } + + @Override + public CharSequence getLabelForAccessibility(Context context) { + return context.getString(mMessageResId); + } + + private boolean isOn() { + return mState == ToggleState.On || mState == ToggleState.TurningOn; + } + + @Override + public CharSequence getMessage() { + return null; + } + @Override + public int getMessageResId() { + return isOn() ? mEnabledStatusMessageResId : mDisabledStatusMessageResId; + } + + private int getIconResId() { + return isOn() ? mEnabledIconResId : mDisabledIconResid; + } + + @Override + public Drawable getIcon(Context context) { + return context.getDrawable(getIconResId()); + } + + public View create(Context context, View convertView, ViewGroup parent, + LayoutInflater inflater) { + willCreate(); + + View v = inflater.inflate(com.android.systemui.R.layout.global_actions_grid_item_v2, + parent, false /* attach */); + ViewGroup.LayoutParams p = v.getLayoutParams(); + p.width = WRAP_CONTENT; + v.setLayoutParams(p); + + ImageView icon = (ImageView) v.findViewById(R.id.icon); + TextView messageView = (TextView) v.findViewById(R.id.message); + final boolean enabled = isEnabled(); + + if (messageView != null) { + messageView.setText(getMessageResId()); + messageView.setEnabled(enabled); + messageView.setSelected(true); // necessary for marquee to work + } + + if (icon != null) { + icon.setImageDrawable(context.getDrawable(getIconResId())); + icon.setEnabled(enabled); + } + + v.setEnabled(enabled); + + return v; + } + + public final void onPress() { + if (mState.inTransition()) { + Log.w(TAG, "shouldn't be able to toggle when in transition"); + return; + } + + final boolean nowOn = !(mState == ToggleState.On); + onToggle(nowOn); + changeStateFromPress(nowOn); + } + + public boolean isEnabled() { + return !mState.inTransition(); + } + + /** + * Implementations may override this if their state can be in on of the intermediate states + * until some notification is received (e.g airplane mode is 'turning off' until we know the + * wireless connections are back online + * + * @param buttonOn Whether the button was turned on or off + */ + protected void changeStateFromPress(boolean buttonOn) { + mState = buttonOn ? ToggleState.On : ToggleState.Off; + } + + abstract void onToggle(boolean on); + + public void updateState(ToggleState state) { + mState = state; + } + } + + private class AirplaneModeAction extends ToggleAction { + AirplaneModeAction() { + super( + R.drawable.ic_lock_airplane_mode, + R.drawable.ic_lock_airplane_mode_off, + R.string.global_actions_toggle_airplane_mode, + R.string.global_actions_airplane_mode_on_status, + R.string.global_actions_airplane_mode_off_status); + } + + void onToggle(boolean on) { + if (mHasTelephony && TelephonyProperties.in_ecm_mode().orElse(false)) { + mIsWaitingForEcmExit = true; + // Launch ECM exit dialog + Intent ecmDialogIntent = + new Intent(TelephonyManager.ACTION_SHOW_NOTICE_ECM_BLOCK_OTHERS, null); + ecmDialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mContext.startActivity(ecmDialogIntent); + } else { + changeAirplaneModeSystemSetting(on); + } + } + + @Override + protected void changeStateFromPress(boolean buttonOn) { + if (!mHasTelephony) return; + + // In ECM mode airplane state cannot be changed + if (!TelephonyProperties.in_ecm_mode().orElse(false)) { + mState = buttonOn ? ToggleState.TurningOn : ToggleState.TurningOff; + mAirplaneState = mState; + } + } + + public boolean showDuringKeyguard() { + return true; + } + + public boolean showBeforeProvisioning() { + return false; + } + } + + private class SilentModeToggleAction extends ToggleAction { + SilentModeToggleAction() { + super(R.drawable.ic_audio_vol_mute, + R.drawable.ic_audio_vol, + R.string.global_action_toggle_silent_mode, + R.string.global_action_silent_mode_on_status, + R.string.global_action_silent_mode_off_status); + } + + void onToggle(boolean on) { + if (on) { + mAudioManager.setRingerMode(AudioManager.RINGER_MODE_SILENT); + } else { + mAudioManager.setRingerMode(AudioManager.RINGER_MODE_NORMAL); + } + } + + public boolean showDuringKeyguard() { + return true; + } + + public boolean showBeforeProvisioning() { + return false; + } + } + + private static class SilentModeTriStateAction implements Action, View.OnClickListener { + + private static final int[] ITEM_IDS = {R.id.option1, R.id.option2, R.id.option3}; + + private final AudioManager mAudioManager; + private final Handler mHandler; + + SilentModeTriStateAction(AudioManager audioManager, Handler handler) { + mAudioManager = audioManager; + mHandler = handler; + } + + private int ringerModeToIndex(int ringerMode) { + // They just happen to coincide + return ringerMode; + } + + private int indexToRingerMode(int index) { + // They just happen to coincide + return index; + } + + @Override + public CharSequence getLabelForAccessibility(Context context) { + return null; + } + + @Override + public int getMessageResId() { + return 0; + } + + @Override + public CharSequence getMessage() { + return null; + } + + @Override + public Drawable getIcon(Context context) { + return null; + } + + + public View create(Context context, View convertView, ViewGroup parent, + LayoutInflater inflater) { + View v = inflater.inflate(R.layout.global_actions_silent_mode, parent, false); + + int selectedIndex = ringerModeToIndex(mAudioManager.getRingerMode()); + for (int i = 0; i < 3; i++) { + View itemView = v.findViewById(ITEM_IDS[i]); + itemView.setSelected(selectedIndex == i); + // Set up click handler + itemView.setTag(i); + itemView.setOnClickListener(this); + } + return v; + } + + public void onPress() { + } + + public boolean showDuringKeyguard() { + return true; + } + + public boolean showBeforeProvisioning() { + return false; + } + + public boolean isEnabled() { + return true; + } + + void willCreate() { + } + + public void onClick(View v) { + if (!(v.getTag() instanceof Integer)) return; + + int index = (Integer) v.getTag(); + mAudioManager.setRingerMode(indexToRingerMode(index)); + mHandler.sendEmptyMessageDelayed(MESSAGE_DISMISS, DIALOG_DISMISS_DELAY); + } + } + + private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(action) + || Intent.ACTION_SCREEN_OFF.equals(action)) { + String reason = intent.getStringExtra(SYSTEM_DIALOG_REASON_KEY); + if (!SYSTEM_DIALOG_REASON_GLOBAL_ACTIONS.equals(reason)) { + mHandler.sendMessage(mHandler.obtainMessage(MESSAGE_DISMISS, reason)); + } + } else if (TelephonyManager.ACTION_EMERGENCY_CALLBACK_MODE_CHANGED.equals(action)) { + // Airplane mode can be changed after ECM exits if airplane toggle button + // is pressed during ECM mode + if (!(intent.getBooleanExtra(TelephonyManager.EXTRA_PHONE_IN_ECM_STATE, false)) + && mIsWaitingForEcmExit) { + mIsWaitingForEcmExit = false; + changeAirplaneModeSystemSetting(true); + } + } + } + }; + + private final TelephonyCallback.ServiceStateListener mPhoneStateListener = + new TelephonyCallback.ServiceStateListener() { + @Override + public void onServiceStateChanged(ServiceState serviceState) { + if (!mHasTelephony) return; + if (mAirplaneModeOn == null) { + Log.d(TAG, "Service changed before actions created"); + return; + } + final boolean inAirplaneMode = serviceState.getState() == ServiceState.STATE_POWER_OFF; + mAirplaneState = inAirplaneMode ? ToggleState.On : ToggleState.Off; + mAirplaneModeOn.updateState(mAirplaneState); + mAdapter.notifyDataSetChanged(); + mOverflowAdapter.notifyDataSetChanged(); + mPowerAdapter.notifyDataSetChanged(); + } + }; + + private ContentObserver mAirplaneModeObserver = new ContentObserver(mMainHandler) { + @Override + public void onChange(boolean selfChange) { + onAirplaneModeChanged(); + } + }; + + private static final int MESSAGE_DISMISS = 0; + private static final int MESSAGE_REFRESH = 1; + private static final int DIALOG_DISMISS_DELAY = 300; // ms + private static final int DIALOG_PRESS_DELAY = 850; // ms + + @VisibleForTesting void setZeroDialogPressDelayForTesting() { + mDialogPressDelay = 0; // ms + } + + private Handler mHandler = new Handler() { + public void handleMessage(Message msg) { + switch (msg.what) { + case MESSAGE_DISMISS: + if (mDialog != null) { + if (SYSTEM_DIALOG_REASON_DREAM.equals(msg.obj)) { + mDialog.completeDismiss(); + } else { + mDialog.dismiss(); + } + mDialog = null; + } + break; + case MESSAGE_REFRESH: + refreshSilentMode(); + mAdapter.notifyDataSetChanged(); + break; + } + } + }; + + private void onAirplaneModeChanged() { + // Let the service state callbacks handle the state. + if (mHasTelephony || mAirplaneModeOn == null) return; + + boolean airplaneModeOn = mGlobalSettings.getInt( + Settings.Global.AIRPLANE_MODE_ON, + 0) == 1; + mAirplaneState = airplaneModeOn ? ToggleState.On : ToggleState.Off; + mAirplaneModeOn.updateState(mAirplaneState); + } + + /** + * Change the airplane mode system setting + */ + private void changeAirplaneModeSystemSetting(boolean on) { + mGlobalSettings.putInt(Settings.Global.AIRPLANE_MODE_ON, on ? 1 : 0); + Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED); + intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING); + intent.putExtra("state", on); + mContext.sendBroadcastAsUser(intent, UserHandle.ALL); + if (!mHasTelephony) { + mAirplaneState = on ? ToggleState.On : ToggleState.Off; + } + } + + @NonNull + @Override + public Lifecycle getLifecycle() { + return mLifecycle; + } + + @VisibleForTesting + static class ActionsDialogLite extends Dialog implements DialogInterface, + ColorExtractor.OnColorsChangedListener { + + protected final Context mContext; + protected MultiListLayout mGlobalActionsLayout; + protected final MyAdapter mAdapter; + protected final MyOverflowAdapter mOverflowAdapter; + protected final MyPowerOptionsAdapter mPowerOptionsAdapter; + protected final IStatusBarService mStatusBarService; + protected final IBinder mToken = new Binder(); + protected Drawable mBackgroundDrawable; + protected final SysuiColorExtractor mColorExtractor; + private boolean mKeyguardShowing; + protected boolean mShowing; + protected float mScrimAlpha; + protected final NotificationShadeWindowController mNotificationShadeWindowController; + protected final NotificationShadeDepthController mDepthController; + protected final SysUiState mSysUiState; + private ListPopupWindow mOverflowPopup; + private Dialog mPowerOptionsDialog; + protected final Runnable mOnRotateCallback; + + protected ViewGroup mContainer; + + ActionsDialogLite(Context context, int themeRes, MyAdapter adapter, + MyOverflowAdapter overflowAdapter, + NotificationShadeDepthController depthController, + SysuiColorExtractor sysuiColorExtractor, IStatusBarService statusBarService, + NotificationShadeWindowController notificationShadeWindowController, + SysUiState sysuiState, Runnable onRotateCallback, boolean keyguardShowing, + MyPowerOptionsAdapter powerAdapter) { + super(context, themeRes); + mContext = context; + mAdapter = adapter; + mOverflowAdapter = overflowAdapter; + mPowerOptionsAdapter = powerAdapter; + mDepthController = depthController; + mColorExtractor = sysuiColorExtractor; + mStatusBarService = statusBarService; + mNotificationShadeWindowController = notificationShadeWindowController; + mSysUiState = sysuiState; + mOnRotateCallback = onRotateCallback; + mKeyguardShowing = keyguardShowing; + + // Window initialization + Window window = getWindow(); + window.requestFeature(Window.FEATURE_NO_TITLE); + // Inflate the decor view, so the attributes below are not overwritten by the theme. + window.getDecorView(); + window.getAttributes().systemUiVisibility |= View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; + window.setLayout(MATCH_PARENT, WRAP_CONTENT); + window.addFlags( + WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN + | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR + | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED + | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED); + window.setType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY); + window.getAttributes().setFitInsetsTypes(0 /* types */); + setTitle(R.string.global_actions); + + initializeLayout(); + } + + private ListPopupWindow createPowerOverflowPopup() { + GlobalActionsPopupMenu popup = new GlobalActionsPopupMenu( + new ContextThemeWrapper( + mContext, + com.android.systemui.R.style.Control_ListPopupWindow + ), false /* isDropDownMode */); + popup.setOnItemClickListener( + (parent, view, position, id) -> mOverflowAdapter.onClickItem(position)); + popup.setOnItemLongClickListener( + (parent, view, position, id) -> mOverflowAdapter.onLongClickItem(position)); + View overflowButton = + findViewById(com.android.systemui.R.id.global_actions_overflow_button); + popup.setAnchorView(overflowButton); + popup.setAdapter(mOverflowAdapter); + return popup; + } + + public void showPowerOptionsMenu() { + mPowerOptionsDialog = GlobalActionsPowerDialog.create(mContext, mPowerOptionsAdapter); + mPowerOptionsDialog.show(); + } + + protected void showPowerOverflowMenu() { + mOverflowPopup = createPowerOverflowPopup(); + mOverflowPopup.show(); + } + + protected int getLayoutResource() { + return com.android.systemui.R.layout.global_actions_grid_lite; + } + + protected void initializeLayout() { + setContentView(getLayoutResource()); + fixNavBarClipping(); + + mGlobalActionsLayout = findViewById(com.android.systemui.R.id.global_actions_view); + mGlobalActionsLayout.setListViewAccessibilityDelegate(new View.AccessibilityDelegate() { + @Override + public boolean dispatchPopulateAccessibilityEvent( + View host, AccessibilityEvent event) { + // Populate the title here, just as Activity does + event.getText().add(mContext.getString(R.string.global_actions)); + return true; + } + }); + mGlobalActionsLayout.setRotationListener(this::onRotate); + mGlobalActionsLayout.setAdapter(mAdapter); + mContainer = findViewById(com.android.systemui.R.id.global_actions_container); + + View overflowButton = findViewById( + com.android.systemui.R.id.global_actions_overflow_button); + if (overflowButton != null) { + if (mOverflowAdapter.getCount() > 0) { + overflowButton.setOnClickListener((view) -> showPowerOverflowMenu()); + LinearLayout.LayoutParams params = + (LinearLayout.LayoutParams) mGlobalActionsLayout.getLayoutParams(); + params.setMarginEnd(0); + mGlobalActionsLayout.setLayoutParams(params); + } else { + overflowButton.setVisibility(View.GONE); + LinearLayout.LayoutParams params = + (LinearLayout.LayoutParams) mGlobalActionsLayout.getLayoutParams(); + params.setMarginEnd(mContext.getResources().getDimensionPixelSize( + com.android.systemui.R.dimen.global_actions_side_margin)); + mGlobalActionsLayout.setLayoutParams(params); + } + } + + if (mBackgroundDrawable == null) { + mBackgroundDrawable = new ScrimDrawable(); + mScrimAlpha = 1.0f; + } + } + + protected void fixNavBarClipping() { + ViewGroup content = findViewById(android.R.id.content); + content.setClipChildren(false); + content.setClipToPadding(false); + ViewGroup contentParent = (ViewGroup) content.getParent(); + contentParent.setClipChildren(false); + contentParent.setClipToPadding(false); + } + + @Override + protected void onStart() { + super.setCanceledOnTouchOutside(true); + super.onStart(); + mGlobalActionsLayout.updateList(); + + if (mBackgroundDrawable instanceof ScrimDrawable) { + mColorExtractor.addOnColorsChangedListener(this); + GradientColors colors = mColorExtractor.getNeutralColors(); + updateColors(colors, false /* animate */); + } + } + + /** + * Updates background and system bars according to current GradientColors. + * + * @param colors Colors and hints to use. + * @param animate Interpolates gradient if true, just sets otherwise. + */ + private void updateColors(GradientColors colors, boolean animate) { + if (!(mBackgroundDrawable instanceof ScrimDrawable)) { + return; + } + ((ScrimDrawable) mBackgroundDrawable).setColor(Color.BLACK, animate); + View decorView = getWindow().getDecorView(); + if (colors.supportsDarkText()) { + decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR + | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR); + } else { + decorView.setSystemUiVisibility(0); + } + } + + @Override + protected void onStop() { + super.onStop(); + mColorExtractor.removeOnColorsChangedListener(this); + } + + @Override + public void show() { + super.show(); + // split this up so we can override but still call Dialog.show + showDialog(); + } + + protected void showDialog() { + mShowing = true; + mNotificationShadeWindowController.setRequestTopUi(true, TAG); + mSysUiState.setFlag(SYSUI_STATE_GLOBAL_ACTIONS_SHOWING, true) + .commitUpdate(mContext.getDisplayId()); + + ViewGroup root = (ViewGroup) mGlobalActionsLayout.getRootView(); + root.setOnApplyWindowInsetsListener((v, windowInsets) -> { + root.setPadding(windowInsets.getStableInsetLeft(), + windowInsets.getStableInsetTop(), + windowInsets.getStableInsetRight(), + windowInsets.getStableInsetBottom()); + return WindowInsets.CONSUMED; + }); + + mBackgroundDrawable.setAlpha(0); + float xOffset = mGlobalActionsLayout.getAnimationOffsetX(); + ObjectAnimator alphaAnimator = + ObjectAnimator.ofFloat(mContainer, "alpha", 0f, 1f); + alphaAnimator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN); + alphaAnimator.setDuration(183); + alphaAnimator.addUpdateListener((animation) -> { + float animatedValue = animation.getAnimatedFraction(); + int alpha = (int) (animatedValue * mScrimAlpha * 255); + mBackgroundDrawable.setAlpha(alpha); + mDepthController.updateGlobalDialogVisibility(animatedValue, mGlobalActionsLayout); + }); + + ObjectAnimator xAnimator = + ObjectAnimator.ofFloat(mContainer, "translationX", xOffset, 0f); + xAnimator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN); + xAnimator.setDuration(350); + + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playTogether(alphaAnimator, xAnimator); + animatorSet.start(); + } + + @Override + public void dismiss() { + dismissWithAnimation(() -> { + dismissInternal(); + }); + } + + protected void dismissInternal() { + mContainer.setTranslationX(0); + ObjectAnimator alphaAnimator = + ObjectAnimator.ofFloat(mContainer, "alpha", 1f, 0f); + alphaAnimator.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN); + alphaAnimator.setDuration(233); + alphaAnimator.addUpdateListener((animation) -> { + float animatedValue = 1f - animation.getAnimatedFraction(); + int alpha = (int) (animatedValue * mScrimAlpha * 255); + mBackgroundDrawable.setAlpha(alpha); + mDepthController.updateGlobalDialogVisibility(animatedValue, mGlobalActionsLayout); + }); + + float xOffset = mGlobalActionsLayout.getAnimationOffsetX(); + ObjectAnimator xAnimator = + ObjectAnimator.ofFloat(mContainer, "translationX", 0f, xOffset); + xAnimator.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN); + xAnimator.setDuration(350); + + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playTogether(alphaAnimator, xAnimator); + animatorSet.addListener(new AnimatorListenerAdapter() { + public void onAnimationEnd(Animator animation) { + completeDismiss(); + } + }); + + animatorSet.start(); + + // close first, as popup windows will not fade during the animation + dismissOverflow(false); + dismissPowerOptions(false); + } + + void dismissWithAnimation(Runnable animation) { + if (!mShowing) { + return; + } + mShowing = false; + animation.run(); + } + + protected void completeDismiss() { + mShowing = false; + 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(); + } + + protected final void dismissOverflow(boolean immediate) { + if (mOverflowPopup != null) { + if (immediate) { + mOverflowPopup.dismissImmediate(); + } else { + mOverflowPopup.dismiss(); + } + } + } + + protected final void dismissPowerOptions(boolean immediate) { + if (mPowerOptionsDialog != null) { + if (immediate) { + mPowerOptionsDialog.dismiss(); + } else { + mPowerOptionsDialog.dismiss(); + } + } + } + + protected final void setRotationSuggestionsEnabled(boolean enabled) { + try { + final int userId = Binder.getCallingUserHandle().getIdentifier(); + final int what = enabled + ? StatusBarManager.DISABLE2_NONE + : StatusBarManager.DISABLE2_ROTATE_SUGGESTIONS; + mStatusBarService.disable2ForUser(what, mToken, mContext.getPackageName(), userId); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); + } + } + + @Override + public void onColorsChanged(ColorExtractor extractor, int which) { + if (mKeyguardShowing) { + if ((WallpaperManager.FLAG_LOCK & which) != 0) { + updateColors(extractor.getColors(WallpaperManager.FLAG_LOCK), + true /* animate */); + } + } else { + if ((WallpaperManager.FLAG_SYSTEM & which) != 0) { + updateColors(extractor.getColors(WallpaperManager.FLAG_SYSTEM), + true /* animate */); + } + } + } + + public void setKeyguardShowing(boolean keyguardShowing) { + mKeyguardShowing = keyguardShowing; + } + + public void refreshDialog() { + // ensure dropdown menus are dismissed before re-initializing the dialog + dismissOverflow(true); + dismissPowerOptions(true); + + // re-create dialog + initializeLayout(); + mGlobalActionsLayout.updateList(); + } + + public void onRotate(int from, int to) { + if (mShowing) { + mOnRotateCallback.run(); + refreshDialog(); + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsLayoutLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsLayoutLite.java new file mode 100644 index 000000000000..eb4cd6b449a2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsLayoutLite.java @@ -0,0 +1,133 @@ +/* + * 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.globalactions; + +import static com.android.systemui.util.leak.RotationUtils.ROTATION_NONE; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; + +import androidx.constraintlayout.helper.widget.Flow; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.systemui.HardwareBgDrawable; +import com.android.systemui.R; + +/** + * ConstraintLayout implementation of the button layout created by the global actions dialog. + */ +public class GlobalActionsLayoutLite extends GlobalActionsLayout { + private final int mMaxColumns; + private final int mMaxRows; + + public GlobalActionsLayoutLite(Context context, AttributeSet attrs) { + super(context, attrs); + mMaxColumns = getResources().getInteger( + com.android.systemui.R.integer.power_menu_lite_max_columns); + mMaxRows = getResources().getInteger( + com.android.systemui.R.integer.power_menu_lite_max_rows); + } + + @VisibleForTesting + @Override + protected boolean shouldReverseListItems() { + // Handled in XML + return false; + } + + @Override + protected HardwareBgDrawable getBackgroundDrawable(int backgroundColor) { + return null; + } + + @Override + public void onUpdateList() { + super.onUpdateList(); + int nElementsWrap = (getCurrentRotation() == ROTATION_NONE) ? mMaxColumns : mMaxRows; + int nChildren = getListView().getChildCount() - 1; // don't count flow element + if (getCurrentRotation() != ROTATION_NONE && nChildren > mMaxRows) { + // up to 4 elements can fit in a row in landscape, otherwise limit for balance + nElementsWrap -= 1; + } + Flow flow = findViewById(R.id.list_flow); + flow.setMaxElementsWrap(nElementsWrap); + } + + @Override + protected void addToListView(View v, boolean reverse) { + super.addToListView(v, reverse); + Flow flow = findViewById(R.id.list_flow); + flow.addView(v); + } + + @Override + protected void removeAllListViews() { + View flow = findViewById(R.id.list_flow); + super.removeAllListViews(); + + // Add flow element back after clearing the list view + super.addToListView(flow, false); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + boolean anyTruncated = false; + ViewGroup listView = getListView(); + + // Check to see if any of the GlobalActionsItems have had their messages truncated + for (int i = 0; i < listView.getChildCount(); i++) { + View child = listView.getChildAt(i); + if (child instanceof GlobalActionsItem) { + GlobalActionsItem item = (GlobalActionsItem) child; + anyTruncated = anyTruncated || item.isTruncated(); + } + } + // If any of the items have been truncated, set the all to single-line marquee + if (anyTruncated) { + for (int i = 0; i < listView.getChildCount(); i++) { + View child = listView.getChildAt(i); + if (child instanceof GlobalActionsItem) { + GlobalActionsItem item = (GlobalActionsItem) child; + item.setMarquee(true); + } + } + } + } + + @VisibleForTesting + protected float getGridItemSize() { + return getContext().getResources().getDimension(R.dimen.global_actions_grid_item_height); + } + + @VisibleForTesting + protected float getAnimationDistance() { + return getGridItemSize() / 2; + } + + @Override + public float getAnimationOffsetX() { + return getAnimationDistance(); + } + + @Override + public float getAnimationOffsetY() { + return 0f; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java index 743ac86cbdcb..d3ae9320d91d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java @@ -16,6 +16,7 @@ package com.android.systemui.media; +import static android.app.Notification.safeCharSequence; import static android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS; import android.app.PendingIntent; @@ -266,7 +267,7 @@ public class MediaControlPanel { // Song name TextView titleText = mViewHolder.getTitleText(); - titleText.setText(data.getSong()); + titleText.setText(safeCharSequence(data.getSong())); // App title TextView appName = mViewHolder.getAppName(); @@ -277,7 +278,7 @@ public class MediaControlPanel { // Artist name TextView artistText = mViewHolder.getArtistText(); - artistText.setText(data.getArtist()); + artistText.setText(safeCharSequence(data.getArtist())); // Transfer chip mViewHolder.getSeamless().setVisibility(View.VISIBLE); diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java index 594c0af4cb44..be9d6bd6edd2 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java @@ -116,7 +116,6 @@ import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.accessibility.AccessibilityButtonModeObserver; import com.android.systemui.accessibility.SystemActions; -import com.android.systemui.assist.AssistHandleViewController; import com.android.systemui.assist.AssistManager; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.qualifiers.Main; @@ -276,11 +275,6 @@ public class NavigationBar implements View.OnAttachStateChangeListener, } } - /** Only for default display */ - @Nullable - AssistHandleViewController mAssistHandlerViewController; - - private final AutoHideUiElement mAutoHideUiElement = new AutoHideUiElement() { @Override public void synchronizeState() { @@ -630,11 +624,6 @@ public class NavigationBar implements View.OnAttachStateChangeListener, mDisabledFlags2 |= StatusBarManager.DISABLE2_ROTATE_SUGGESTIONS; } setDisabled2Flags(mDisabledFlags2); - if (mIsOnDefaultDisplay) { - mAssistHandlerViewController = - new AssistHandleViewController(mHandler, mNavigationBarView); - getBarTransitions().addDarkIntensityListener(mAssistHandlerViewController); - } initSecondaryHomeHandleForRotation(); @@ -664,11 +653,6 @@ public class NavigationBar implements View.OnAttachStateChangeListener, @Override public void onViewDetachedFromWindow(View v) { if (mNavigationBarView != null) { - if (mIsOnDefaultDisplay) { - mNavigationBarView.getBarTransitions() - .removeDarkIntensityListener(mAssistHandlerViewController); - mAssistHandlerViewController = null; - } mNavigationBarView.getBarTransitions().destroy(); mNavigationBarView.getLightTransitionsController().destroy(mContext); } @@ -1596,11 +1580,6 @@ public class NavigationBar implements View.OnAttachStateChangeListener, delay + StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE); } - @Nullable - public AssistHandleViewController getAssistHandlerViewController() { - return mAssistHandlerViewController; - } - /** * Performs transitions on navigation bar. * diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java index 50efa8def5bf..6d1109ee5f51 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java @@ -45,7 +45,6 @@ import com.android.settingslib.applications.InterestingConfigChanges; import com.android.systemui.Dumpable; import com.android.systemui.accessibility.AccessibilityButtonModeObserver; import com.android.systemui.accessibility.SystemActions; -import com.android.systemui.assist.AssistHandleViewController; import com.android.systemui.assist.AssistManager; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; @@ -399,13 +398,6 @@ public class NavigationBarController implements Callbacks, return mNavigationBars.get(DEFAULT_DISPLAY); } - /** @return {@link AssistHandleViewController} (only on the default display). */ - @Nullable - public AssistHandleViewController getAssistHandlerViewController() { - NavigationBar navBar = getDefaultNavigationBar(); - return navBar == null ? null : navBar.getAssistHandlerViewController(); - } - @Override public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) { for (int i = 0; i < mNavigationBars.size(); i++) { diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java index 091b17dbb0c0..339331bdd827 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java @@ -69,7 +69,6 @@ import com.android.settingslib.Utils; import com.android.systemui.Dependency; import com.android.systemui.Interpolators; import com.android.systemui.R; -import com.android.systemui.assist.AssistHandleViewController; import com.android.systemui.model.SysUiState; import com.android.systemui.navigationbar.buttons.ButtonDispatcher; import com.android.systemui.navigationbar.buttons.ContextualButton; @@ -1357,14 +1356,6 @@ public class NavigationBarView extends FrameLayout implements setClipChildren(shouldClip); setClipToPadding(shouldClip); - NavigationBarController navigationBarController = - Dependency.get(NavigationBarController.class); - AssistHandleViewController controller = - navigationBarController == null - ? null : navigationBarController.getAssistHandlerViewController(); - if (controller != null) { - controller.setBottomOffset(insets.getSystemWindowInsetBottom()); - } return super.onApplyWindowInsets(insets); } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/ButtonDispatcher.java b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/ButtonDispatcher.java index ade2923acdf6..a4f5548a659c 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/ButtonDispatcher.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/ButtonDispatcher.java @@ -196,8 +196,6 @@ public class ButtonDispatcher { if (animate) { setVisibility(View.VISIBLE); mFadeAnimator = ValueAnimator.ofFloat(getAlpha(), alpha); - mFadeAnimator.setStartDelay( - mAssistManager.getAssistHandleShowAndGoRemainingDurationMs()); mFadeAnimator.setDuration(duration); mFadeAnimator.setInterpolator(LINEAR); mFadeAnimator.addListener(mFadeListener); diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java index f0e4cce299ee..9ea938325659 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java @@ -127,8 +127,10 @@ public class KeyButtonView extends ImageView implements ButtonInterface { performLongClick(); mLongClicked = true; } else { - sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.FLAG_LONG_PRESS); - sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED); + if (mCode != KEYCODE_UNKNOWN) { + sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.FLAG_LONG_PRESS); + sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED); + } mLongClicked = true; } } @@ -434,6 +436,9 @@ public class KeyButtonView extends ImageView implements ButtonInterface { @Override public void abortCurrentGesture() { Log.d("b/63783866", "KeyButtonView.abortCurrentGesture"); + if (mCode != KeyEvent.KEYCODE_UNKNOWN) { + sendEvent(KeyEvent.ACTION_UP, KeyEvent.FLAG_CANCELED); + } setPressed(false); mRipple.abortDelayedRipple(); mGestureAborted = true; diff --git a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java index 54d6a2eda82a..02c12f6d51e5 100644 --- a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java +++ b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java @@ -19,6 +19,9 @@ package com.android.systemui.people; import static android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID; import static android.appwidget.AppWidgetManager.INVALID_APPWIDGET_ID; +import static com.android.systemui.people.PeopleTileViewHelper.getPersonIconBitmap; +import static com.android.systemui.people.PeopleTileViewHelper.getSizeInDp; + import android.app.Activity; import android.app.INotificationManager; import android.app.people.IPeopleManager; @@ -27,28 +30,33 @@ import android.content.Context; import android.content.Intent; import android.content.pm.LauncherApps; import android.content.pm.PackageManager; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.Outline; +import android.graphics.drawable.GradientDrawable; import android.os.Bundle; import android.os.ServiceManager; import android.util.Log; +import android.view.View; import android.view.ViewGroup; +import android.view.ViewOutlineProvider; +import android.widget.LinearLayout; import com.android.systemui.R; import com.android.systemui.people.widget.PeopleSpaceWidgetManager; import com.android.systemui.statusbar.notification.NotificationEntryManager; +import java.util.ArrayList; import java.util.List; import javax.inject.Inject; -/** - * Shows the user their tiles for their priority People (go/live-status). - */ +/** People Tile Widget configuration activity that shows the user their conversation tiles. */ public class PeopleSpaceActivity extends Activity { private static final String TAG = "PeopleSpaceActivity"; private static final boolean DEBUG = PeopleSpaceUtils.DEBUG; - private ViewGroup mPeopleSpaceLayout; private IPeopleManager mPeopleManager; private PeopleSpaceWidgetManager mPeopleSpaceWidgetManager; private INotificationManager mNotificationManager; @@ -70,8 +78,6 @@ public class PeopleSpaceActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.people_space_activity); - mPeopleSpaceLayout = findViewById(R.id.people_space_layout); mContext = getApplicationContext(); mNotificationManager = INotificationManager.Stub.asInterface( ServiceManager.getService(Context.NOTIFICATION_SERVICE)); @@ -79,42 +85,78 @@ public class PeopleSpaceActivity extends Activity { mPeopleManager = IPeopleManager.Stub.asInterface( ServiceManager.getService(Context.PEOPLE_SERVICE)); mLauncherApps = mContext.getSystemService(LauncherApps.class); - setTileViewsWithPriorityConversations(); mAppWidgetId = getIntent().getIntExtra(EXTRA_APPWIDGET_ID, INVALID_APPWIDGET_ID); setResult(RESULT_CANCELED); } - /** - * Retrieves all priority conversations and sets a {@link PeopleSpaceTileView}s for each - * priority conversation. - */ - private void setTileViewsWithPriorityConversations() { + /** Builds the conversation selection activity. */ + private void buildActivity() { + List<PeopleSpaceTile> priorityTiles = new ArrayList<>(); + List<PeopleSpaceTile> recentTiles = new ArrayList<>(); try { - List<PeopleSpaceTile> tiles = PeopleSpaceUtils.getTiles(mContext, mNotificationManager, + priorityTiles = PeopleSpaceUtils.getPriorityTiles(mContext, mNotificationManager, + mPeopleManager, mLauncherApps, mNotificationEntryManager); + recentTiles = PeopleSpaceUtils.getRecentTiles(mContext, mNotificationManager, mPeopleManager, mLauncherApps, mNotificationEntryManager); - for (PeopleSpaceTile tile : tiles) { - PeopleSpaceTileView tileView = new PeopleSpaceTileView(mContext, mPeopleSpaceLayout, - tile.getId()); - setTileView(tileView, tile); - } } catch (Exception e) { Log.e(TAG, "Couldn't retrieve conversations", e); } + + // If no conversations, render activity without conversations + if (recentTiles.isEmpty() && priorityTiles.isEmpty()) { + setContentView(R.layout.people_space_activity_no_conversations); + + // 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); + GradientDrawable shape = (GradientDrawable) item.getBackground(); + final TypedArray ta = mContext.obtainStyledAttributes( + new int[] {android.R.attr.colorBackgroundFloating}); + shape.setColor(ta.getColor(0, Color.WHITE)); + return; + } + + setContentView(R.layout.people_space_activity); + setTileViews(R.id.priority, R.id.priority_tiles, priorityTiles); + setTileViews(R.id.recent, R.id.recent_tiles, recentTiles); + } + + private ViewOutlineProvider mViewOutlineProvider = new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), + mContext.getResources().getDimension(R.dimen.people_space_widget_radius)); + } + }; + + /** Sets a {@link PeopleSpaceTileView}s for each conversation. */ + private void setTileViews(int viewId, int tilesId, List<PeopleSpaceTile> tiles) { + if (tiles.isEmpty()) { + LinearLayout view = findViewById(viewId); + view.setVisibility(View.GONE); + return; + } + + ViewGroup layout = findViewById(tilesId); + layout.setClipToOutline(true); + layout.setOutlineProvider(mViewOutlineProvider); + for (int i = 0; i < tiles.size(); ++i) { + PeopleSpaceTile tile = tiles.get(i); + PeopleSpaceTileView tileView = new PeopleSpaceTileView(mContext, + layout, tile.getId(), i == (tiles.size() - 1)); + setTileView(tileView, tile); + } } /** Sets {@code tileView} with the data in {@code conversation}. */ private void setTileView(PeopleSpaceTileView tileView, PeopleSpaceTile tile) { try { - String pkg = tile.getPackageName(); - String status = - PeopleSpaceUtils.getLastInteractionString(mContext, - tile.getLastInteractionTimestamp()); - tileView.setStatus(status); - tileView.setName(tile.getUserName().toString()); - tileView.setPackageIcon(mPackageManager.getApplicationIcon(pkg)); - tileView.setPersonIcon(tile.getUserIcon()); + tileView.setPersonIcon(getPersonIconBitmap(mContext, tile, + getSizeInDp(mContext, R.dimen.avatar_size_for_medium, + mContext.getResources().getDisplayMetrics().density))); + tileView.setOnClickListener(v -> storeWidgetConfiguration(tile)); } catch (Exception e) { Log.e(TAG, "Couldn't retrieve shortcut information", e); @@ -141,6 +183,12 @@ public class PeopleSpaceActivity extends Activity { finish(); } + /** Finish activity without choosing a widget. */ + public void dismissActivity(View v) { + if (DEBUG) Log.d(TAG, "Activity dismissed with no widgets added!"); + finish(); + } + private void setActivityResult(int result) { Intent resultValue = new Intent(); resultValue.putExtra(EXTRA_APPWIDGET_ID, mAppWidgetId); @@ -151,6 +199,12 @@ public class PeopleSpaceActivity extends Activity { protected void onResume() { super.onResume(); // Refresh tile views to sync new conversations. - setTileViewsWithPriorityConversations(); + buildActivity(); + } + + @Override + protected void onPause() { + super.onPause(); + finish(); } } diff --git a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceTileView.java b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceTileView.java index 6f89332e66a9..36b435b04fd6 100644 --- a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceTileView.java +++ b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceTileView.java @@ -19,8 +19,7 @@ package com.android.systemui.people; import android.app.people.PeopleSpaceTile; import android.content.Context; import android.content.pm.LauncherApps; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.Icon; +import android.graphics.Bitmap; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -37,11 +36,9 @@ public class PeopleSpaceTileView extends LinearLayout { private View mTileView; private TextView mNameView; - private TextView mStatusView; - private ImageView mPackageIconView; private ImageView mPersonIconView; - public PeopleSpaceTileView(Context context, ViewGroup view, String shortcutId) { + public PeopleSpaceTileView(Context context, ViewGroup view, String shortcutId, boolean isLast) { super(context); mTileView = view.findViewWithTag(shortcutId); if (mTileView == null) { @@ -50,10 +47,13 @@ public class PeopleSpaceTileView extends LinearLayout { view.addView(mTileView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); mTileView.setTag(shortcutId); + + // If it's not the last conversation in this section, add a divider. + if (!isLast) { + inflater.inflate(R.layout.people_space_activity_list_divider, view, true); + } } mNameView = mTileView.findViewById(R.id.tile_view_name); - mStatusView = mTileView.findViewById(R.id.tile_view_status); - mPackageIconView = mTileView.findViewById(R.id.tile_view_package_icon); mPersonIconView = mTileView.findViewById(R.id.tile_view_person_icon); } @@ -62,19 +62,9 @@ public class PeopleSpaceTileView extends LinearLayout { mNameView.setText(name); } - /** Sets the status text on the tile. */ - public void setStatus(String status) { - mStatusView.setText(status); - } - - /** Sets the package drawable on the tile. */ - public void setPackageIcon(Drawable drawable) { - mPackageIconView.setImageDrawable(drawable); - } - - /** Sets the person bitmap on the tile. */ - public void setPersonIcon(Icon icon) { - mPersonIconView.setImageIcon(icon); + /** Sets the person and package drawable on the tile. */ + public void setPersonIcon(Bitmap bitmap) { + mPersonIconView.setImageBitmap(bitmap); } /** Sets the click listener of the tile. */ diff --git a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceUtils.java b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceUtils.java index 440c5efab008..5f6d95f4824d 100644 --- a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceUtils.java +++ b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceUtils.java @@ -19,6 +19,7 @@ package com.android.systemui.people; import static android.app.Notification.CATEGORY_MISSED_CALL; import static android.app.Notification.EXTRA_MESSAGES; +import android.annotation.NonNull; import android.app.INotificationManager; import android.app.Notification; import android.app.people.ConversationChannel; @@ -126,8 +127,9 @@ public class PeopleSpaceUtils { } } - /** Returns a list of map entries corresponding to user's conversations. */ - public static List<PeopleSpaceTile> getTiles( + /** Returns a list of map entries corresponding to user's priority conversations. */ + @NonNull + public static List<PeopleSpaceTile> getPriorityTiles( Context context, INotificationManager notificationManager, IPeopleManager peopleManager, LauncherApps launcherApps, NotificationEntryManager notificationEntryManager) throws Exception { @@ -139,11 +141,23 @@ public class PeopleSpaceUtils { .filter(c -> c.getNotificationChannel() != null && c.getNotificationChannel().isImportantConversation()) .map(c -> c.getShortcutInfo()); - List<PeopleSpaceTile> tiles = getSortedTiles(peopleManager, launcherApps, + List<PeopleSpaceTile> priorityTiles = getSortedTiles(peopleManager, launcherApps, priorityConversations); + priorityTiles = augmentTilesFromVisibleNotifications( + context, priorityTiles, notificationEntryManager); + return priorityTiles; + } - // Sort and then add recent and non priority conversations to tiles list. + /** Returns a list of map entries corresponding to user's recent conversations. */ + @NonNull + public static List<PeopleSpaceTile> getRecentTiles( + Context context, INotificationManager notificationManager, IPeopleManager peopleManager, + LauncherApps launcherApps, NotificationEntryManager notificationEntryManager) + throws Exception { if (DEBUG) Log.d(TAG, "Add recent conversations"); + List<ConversationChannelWrapper> conversations = + notificationManager.getConversations( + false).getList(); Stream<ShortcutInfo> nonPriorityConversations = conversations.stream() .filter(c -> c.getNotificationChannel() == null || !c.getNotificationChannel().isImportantConversation()) @@ -159,10 +173,10 @@ public class PeopleSpaceUtils { recentConversations); List<PeopleSpaceTile> recentTiles = getSortedTiles(peopleManager, launcherApps, mergedStream); - tiles.addAll(recentTiles); - tiles = augmentTilesFromVisibleNotifications(context, tiles, notificationEntryManager); - return tiles; + recentTiles = augmentTilesFromVisibleNotifications( + context, recentTiles, notificationEntryManager); + return recentTiles; } /** Returns stored widgets for the conversation specified. */ diff --git a/packages/SystemUI/src/com/android/systemui/people/PeopleTileViewHelper.java b/packages/SystemUI/src/com/android/systemui/people/PeopleTileViewHelper.java index 8d1b712e0807..96fbe6987562 100644 --- a/packages/SystemUI/src/com/android/systemui/people/PeopleTileViewHelper.java +++ b/packages/SystemUI/src/com/android/systemui/people/PeopleTileViewHelper.java @@ -192,7 +192,11 @@ class PeopleTileViewHelper { } private int getSizeInDp(int dimenResourceId) { - return (int) (mContext.getResources().getDimension(dimenResourceId) / mDensity); + return getSizeInDp(mContext, dimenResourceId, mDensity); + } + + public static int getSizeInDp(Context context, int dimenResourceId, float density) { + return (int) (context.getResources().getDimension(dimenResourceId) / density); } private int getContentHeightForLayout(int lineHeight) { @@ -278,24 +282,11 @@ class PeopleTileViewHelper { } else { views.setViewVisibility(R.id.availability, View.GONE); } - boolean hasNewStory = - mTile.getStatuses() != null && mTile.getStatuses().stream().anyMatch( - c -> c.getActivity() == ACTIVITY_NEW_STORY); + views.setTextViewText(R.id.name, mTile.getUserName().toString()); views.setBoolean(R.id.image, "setClipToOutline", true); - - Icon icon = mTile.getUserIcon(); - PeopleStoryIconFactory storyIcon = new PeopleStoryIconFactory(mContext, - mContext.getPackageManager(), - IconDrawableFactory.newInstance(mContext, false), - maxAvatarSize); - Drawable drawable = icon.loadDrawable(mContext); - Drawable personDrawable = storyIcon.getPeopleTileDrawable(drawable, - mTile.getPackageName(), getUserId(mTile), mTile.isImportantConversation(), - hasNewStory); - Bitmap bitmap = convertDrawableToBitmap(personDrawable); - views.setImageViewBitmap(R.id.person_icon, bitmap); - + views.setImageViewBitmap(R.id.person_icon, + getPersonIconBitmap(mContext, mTile, maxAvatarSize)); return views; } catch (Exception e) { Log.e(TAG, "Failed to set common fields: " + e); @@ -583,4 +574,23 @@ class PeopleTileViewHelper { return R.layout.people_tile_small; } } + + /** 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); + + Icon icon = tile.getUserIcon(); + PeopleStoryIconFactory storyIcon = new PeopleStoryIconFactory(context, + context.getPackageManager(), + IconDrawableFactory.newInstance(context, false), + maxAvatarSize); + Drawable drawable = icon.loadDrawable(context); + Drawable personDrawable = storyIcon.getPeopleTileDrawable(drawable, + tile.getPackageName(), getUserId(tile), tile.isImportantConversation(), + hasNewStory); + return convertDrawableToBitmap(personDrawable); + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java index a089a05f2776..ea471b957d68 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java @@ -31,6 +31,8 @@ import com.android.systemui.qs.QSPanel.QSTileLayout; import com.android.systemui.qs.TouchAnimator.Builder; import com.android.systemui.qs.TouchAnimator.Listener; import com.android.systemui.qs.dagger.QSScope; +import com.android.systemui.qs.tileimpl.HeightOverrideable; +import com.android.systemui.statusbar.CrossFadeHelper; import com.android.systemui.statusbar.FeatureFlags; import com.android.systemui.tuner.TunerService; import com.android.systemui.tuner.TunerService.Tunable; @@ -53,6 +55,9 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha private static final String MOVE_FULL_ROWS = "sysui_qs_move_whole_rows"; public static final float EXPANDED_TILE_DELAY = .86f; + private static final long QQS_FADE_IN_DURATION = 200L; + // Fade out faster than fade in to finish before QQS hides. + private static final long QQS_FADE_OUT_DURATION = 50L; private final ArrayList<View> mAllViews = new ArrayList<>(); @@ -86,7 +91,7 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha private HeightExpansionAnimator mOtherTilesExpandAnimator; private boolean mNeedsAnimatorUpdate = false; - + private boolean mToShowing; private boolean mOnKeyguard; private boolean mAllowFancy; @@ -149,6 +154,18 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha } } + void startAlphaAnimation(boolean show) { + if (show == mToShowing) { + return; + } + mToShowing = show; + if (show) { + CrossFadeHelper.fadeIn(mQs.getView(), QQS_FADE_IN_DURATION, 0 /* delay */); + } else { + CrossFadeHelper.fadeOut(mQs.getView(), QQS_FADE_OUT_DURATION, 0 /* delay */, + null /* endRunnable */); + } + } /** * Sets whether or not the keyguard is currently being shown with a collapsed header. @@ -590,6 +607,15 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { float t = valueAnimator.getAnimatedFraction(); + final int viewCount = mViews.size(); + int height = (Integer) valueAnimator.getAnimatedValue(); + for (int i = 0; i < viewCount; i++) { + View v = mViews.get(i); + v.setBottom(v.getTop() + height); + if (v instanceof HeightOverrideable) { + ((HeightOverrideable) v).setHeightOverride(height); + } + } if (t == 0f) { mListener.onAnimationAtStart(); } else if (t == 1f) { @@ -598,12 +624,6 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha mListener.onAnimationStarted(); } mLastT = t; - final int viewCount = mViews.size(); - int height = (Integer) valueAnimator.getAnimatedValue(); - for (int i = 0; i < viewCount; i++) { - View v = mViews.get(i); - v.setBottom(v.getTop() + height); - } } }; @@ -632,6 +652,9 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha for (int i = 0; i < viewsCount; i++) { View v = mViews.get(i); v.setBottom(v.getTop() + v.getMeasuredHeight()); + if (v instanceof HeightOverrideable) { + ((HeightOverrideable) v).resetOverride(); + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java index e8976d379fef..5256bc45b7b5 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java @@ -92,6 +92,11 @@ public class QSContainerImpl extends FrameLayout { setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); } + @Override + public boolean hasOverlappingRendering() { + return false; + } + void onMediaVisibilityChanged(boolean qsVisible) { mAnimateBottomOnNextLayout = qsVisible; } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSDetail.java b/packages/SystemUI/src/com/android/systemui/qs/QSDetail.java index d54d3f21f0b6..9a889e0ff088 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSDetail.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSDetail.java @@ -210,6 +210,7 @@ public class QSDetail extends LinearLayout { mDetailAdapter = adapter; listener = mHideGridContentWhenDone; setVisibility(View.VISIBLE); + updateDetailText(); } else { if (wasShowingDetail) { Dependency.get(MetricsLogger.class).hidden(mDetailAdapter.getMetricsCategory()); @@ -223,7 +224,6 @@ public class QSDetail extends LinearLayout { mQsPanelController.setGridContentVisibility(true); mQsPanelCallback.onScanStateChanged(false); } - updateDetailText(); sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); if (mShouldAnimate) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFooterView.java b/packages/SystemUI/src/com/android/systemui/qs/QSFooterView.java index 3d8784b29e4c..eb7b115700a7 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFooterView.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFooterView.java @@ -75,6 +75,8 @@ public class QSFooterView extends FrameLayout { private TouchAnimator mSettingsCogAnimator; private View mActionsContainer; + private View mTunerIcon; + private int mTunerIconTranslation; private OnClickListener mExpandClickListener; @@ -107,6 +109,7 @@ public class QSFooterView extends FrameLayout { mActionsContainer = requireViewById(R.id.qs_footer_actions_container); mEditContainer = findViewById(R.id.qs_footer_actions_edit_container); mBuildText = findViewById(R.id.build); + mTunerIcon = requireViewById(R.id.tuner_icon); // RenderThread is doing more harm than good when touching the header (to expand quick // settings), so disable it for this view @@ -166,6 +169,9 @@ public class QSFooterView extends FrameLayout { private void updateResources() { updateFooterAnimator(); + mTunerIconTranslation = mContext.getResources() + .getDimensionPixelOffset(R.dimen.qs_footer_tuner_icon_translation); + mTunerIcon.setTranslationX(isLayoutRtl() ? -mTunerIconTranslation : mTunerIconTranslation); } private void updateFooterAnimator() { @@ -274,8 +280,7 @@ public class QSFooterView extends FrameLayout { private void updateVisibilities(boolean isTunerEnabled) { mSettingsContainer.setVisibility(mQsDisabled ? View.GONE : View.VISIBLE); - mSettingsContainer.findViewById(R.id.tuner_icon).setVisibility( - isTunerEnabled ? View.VISIBLE : View.INVISIBLE); + mTunerIcon.setVisibility(isTunerEnabled ? View.VISIBLE : View.INVISIBLE); final boolean isDemo = UserManager.isDeviceInDemoMode(mContext); mMultiUserSwitch.setVisibility(showUserSwitcher() ? View.VISIBLE : View.GONE); if (mEditContainer != null) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFooterViewController.java b/packages/SystemUI/src/com/android/systemui/qs/QSFooterViewController.java index f1f4e16206a1..52e05a4fd6c5 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFooterViewController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFooterViewController.java @@ -32,6 +32,7 @@ import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.R; +import com.android.systemui.globalactions.GlobalActionsDialogLite; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.qs.dagger.QSScope; import com.android.systemui.settings.UserTracker; @@ -68,6 +69,7 @@ public class QSFooterViewController extends ViewController<QSFooterView> impleme private final PageIndicator mPageIndicator; private final View mPowerMenuLite; private final boolean mShowPMLiteButton; + private GlobalActionsDialogLite mGlobalActionsDialog; private final UserInfoController.OnUserInfoChangedListener mOnUserInfoChangedListener = new UserInfoController.OnUserInfoChangedListener() { @@ -115,6 +117,8 @@ public class QSFooterViewController extends ViewController<QSFooterView> impleme } else { startSettingsActivity(); } + } else if (v == mPowerMenuLite) { + mGlobalActionsDialog.showOrHideDialog(false, true); } } }; @@ -129,7 +133,8 @@ public class QSFooterViewController extends ViewController<QSFooterView> impleme QSPanelController qsPanelController, QSDetailDisplayer qsDetailDisplayer, QuickQSPanelController quickQSPanelController, TunerService tunerService, MetricsLogger metricsLogger, - @Named(PM_LITE_ENABLED) boolean showPMLiteButton) { + @Named(PM_LITE_ENABLED) boolean showPMLiteButton, + GlobalActionsDialogLite globalActionsDialog) { super(view); mUserManager = userManager; mUserInfoController = userInfoController; @@ -149,11 +154,15 @@ public class QSFooterViewController extends ViewController<QSFooterView> impleme mPageIndicator = mView.findViewById(R.id.footer_page_indicator); mPowerMenuLite = mView.findViewById(R.id.pm_lite); mShowPMLiteButton = showPMLiteButton; + mGlobalActionsDialog = globalActionsDialog; } @Override protected void onViewAttached() { - if (!mShowPMLiteButton) { + if (mShowPMLiteButton) { + mPowerMenuLite.setVisibility(View.VISIBLE); + mPowerMenuLite.setOnClickListener(mSettingsOnClickListener); + } else { mPowerMenuLite.setVisibility(View.GONE); } mView.addOnLayoutChangeListener( diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java index 7c9f0b082d8f..cb58eff650c1 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java @@ -132,7 +132,8 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { inflater = mInjectionInflater.injectable( - inflater.cloneInContext(new ContextThemeWrapper(getContext(), R.style.qs_theme))); + inflater.cloneInContext(new ContextThemeWrapper(getContext(), + R.style.Theme_SystemUI_QuickSettings))); return inflater.inflate(R.layout.qs_panel, container, false); } @@ -190,7 +191,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { boolean sizeChanged = (oldTop - oldBottom) != (top - bottom); if (sizeChanged) { - setQsExpansion(mLastQSExpansion, mLastQSExpansion); + setQsExpansion(mLastQSExpansion, mLastHeaderTranslation); } }); } @@ -395,6 +396,12 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca @Override public void setQsExpansion(float expansion, float headerTranslation) { if (DEBUG) Log.d(TAG, "setQSExpansion " + expansion + " " + headerTranslation); + + if (mQSAnimator != null) { + final boolean showQSOnLockscreen = expansion > 0; + final boolean showQSUnlocked = headerTranslation == 0; + mQSAnimator.startAlphaAnimation(showQSOnLockscreen || showQSUnlocked); + } mContainer.setExpansion(expansion); final float translationScaleY = expansion - 1; boolean onKeyguardAndExpanded = isKeyguardShowing() && !mShowCollapsedOnKeyguard; diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java index 86836f935aba..e41a0389e8c1 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java @@ -202,11 +202,10 @@ public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewContr private void addTile(final QSTile tile, boolean collapsedView) { final TileRecord r = new TileRecord(); r.tile = tile; - r.tileView = mHost.createTileView(tile, collapsedView); + r.tileView = mHost.createTileView(getContext(), tile, collapsedView); mView.addTile(r); mRecords.add(r); mCachedSpecs = getTilesSpecs(); - } /** */ diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java index 0a9c12fa4a13..525bad8a0e25 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java @@ -23,7 +23,6 @@ import android.os.Handler; import android.os.Looper; import android.os.UserHandle; import android.os.UserManager; -import android.provider.Settings; import android.provider.Settings.Secure; import android.service.quicksettings.Tile; import android.text.TextUtils; @@ -57,6 +56,7 @@ import com.android.systemui.statusbar.phone.StatusBarIconController; import com.android.systemui.tuner.TunerService; import com.android.systemui.tuner.TunerService.Tunable; import com.android.systemui.util.leak.GarbageMonitor; +import com.android.systemui.util.settings.SecureSettings; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -79,6 +79,7 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private static final int MAX_QS_INSTANCE_ID = 1 << 20; + public static final int POSITION_AT_END = -1; public static final String TILES_SETTING = Secure.QS_TILES; private final Context mContext; @@ -101,6 +102,7 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D private final Optional<StatusBar> mStatusBarOptional; private Context mUserContext; private UserTracker mUserTracker; + private SecureSettings mSecureSettings; @Inject public QSTileHost(Context context, @@ -116,7 +118,8 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D Optional<StatusBar> statusBarOptional, QSLogger qsLogger, UiEventLogger uiEventLogger, - UserTracker userTracker) { + UserTracker userTracker, + SecureSettings secureSettings) { mIconController = iconController; mContext = context; mUserContext = context; @@ -135,6 +138,7 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D pluginManager.addPluginListener(this, QSFactory.class, true); mDumpManager.registerDumpable(TAG, this); mUserTracker = userTracker; + mSecureSettings = secureSettings; mainHandler.post(() -> { // This is technically a hack to avoid circular dependency of @@ -343,19 +347,43 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D if (mAutoTiles != null) mAutoTiles.unmarkTileAsAutoAdded(spec); } + /** + * Add a tile to the end + * + * @param spec string matching a pre-defined tilespec + */ public void addTile(String spec) { - changeTileSpecs(tileSpecs-> !tileSpecs.contains(spec) && tileSpecs.add(spec)); + addTile(spec, POSITION_AT_END); + } + + /** + * Add a tile into the requested spot, or at the end if the position is greater than the number + * of tiles. + * @param spec string matching a pre-defined tilespec + * @param requestPosition -1 for end, 0 for beginning, or X for insertion at position X + */ + public void addTile(String spec, int requestPosition) { + changeTileSpecs(tileSpecs -> { + if (tileSpecs.contains(spec)) return false; + + int size = tileSpecs.size(); + if (requestPosition == POSITION_AT_END || requestPosition >= size) { + tileSpecs.add(spec); + } else { + tileSpecs.add(requestPosition, spec); + } + return true; + }); } - private void saveTilesToSettings(List<String> tileSpecs) { - Settings.Secure.putStringForUser(mContext.getContentResolver(), TILES_SETTING, - TextUtils.join(",", tileSpecs), null /* tag */, - false /* default */, mCurrentUser, true /* overrideable by restore */); + void saveTilesToSettings(List<String> tileSpecs) { + mSecureSettings.putStringForUser(TILES_SETTING, TextUtils.join(",", tileSpecs), + null /* tag */, false /* default */, mCurrentUser, + true /* overrideable by restore */); } private void changeTileSpecs(Predicate<List<String>> changeFunction) { - final String setting = Settings.Secure.getStringForUser(mContext.getContentResolver(), - TILES_SETTING, mCurrentUser); + final String setting = mSecureSettings.getStringForUser(TILES_SETTING, mCurrentUser); final List<String> tileSpecs = loadTileSpecs(mContext, setting); if (changeFunction.test(tileSpecs)) { saveTilesToSettings(tileSpecs); @@ -423,9 +451,15 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D return null; } - public QSTileView createTileView(QSTile tile, boolean collapsedView) { + /** + * Create a view for a tile, iterating over all possible {@link QSFactory}. + * + * @see QSFactory#createTileView + */ + public QSTileView createTileView(Context themedContext, QSTile tile, boolean collapsedView) { for (int i = 0; i < mQsFactories.size(); i++) { - QSTileView view = mQsFactories.get(i).createTileView(tile, collapsedView); + QSTileView view = mQsFactories.get(i) + .createTileView(themedContext, tile, collapsedView); if (view != null) { return view; } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java index 8aaf84b7d9f9..b661e2b6ae57 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java @@ -17,9 +17,6 @@ package com.android.systemui.qs; import static android.app.StatusBarManager.DISABLE2_QUICK_SETTINGS; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; -import static com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN; -import static com.android.systemui.statusbar.StatusBarIconView.STATE_ICON; - import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; @@ -41,7 +38,7 @@ import com.android.systemui.BatteryMeterView; import com.android.systemui.Interpolators; import com.android.systemui.R; import com.android.systemui.qs.QSDetail.Callback; -import com.android.systemui.statusbar.StatusBarMobileView; +import com.android.systemui.statusbar.StatusBarIconView; import com.android.systemui.statusbar.phone.StatusBarIconController.TintedIconManager; import com.android.systemui.statusbar.phone.StatusBarWindowView; import com.android.systemui.statusbar.phone.StatusIconContainer; @@ -91,7 +88,7 @@ public class QuickStatusBarHeader extends FrameLayout { public QuickStatusBarHeader(Context context, AttributeSet attrs) { super(context, attrs); - mMobileSlotName = context.getString(com.android.internal.R.string.status_bar_mobile); + mMobileSlotName = context.getString(com.android.internal.R.string.status_bar_no_calling); } /** @@ -221,8 +218,8 @@ public class QuickStatusBarHeader extends FrameLayout { } private void updateAlphaAnimator() { - StatusBarMobileView icon = - ((StatusBarMobileView) mIconContainer.getViewForSlot(mMobileSlotName)); + StatusBarIconView icon = + ((StatusBarIconView) mIconContainer.getViewForSlot(mMobileSlotName)); TouchAnimator.Builder builder = new TouchAnimator.Builder() .addFloat(mQSCarriers, "alpha", 0, 1) .addFloat(mDatePrivacyView, "alpha", 0, mDatePrivacyAlpha); @@ -231,14 +228,12 @@ public class QuickStatusBarHeader extends FrameLayout { builder.setListener(new TouchAnimator.ListenerAdapter() { @Override public void onAnimationAtEnd() { - icon.forceHidden(true); - icon.setVisibleState(STATE_HIDDEN); + mIconContainer.addIgnoredSlot(mMobileSlotName); } @Override public void onAnimationStarted() { - icon.forceHidden(false); - icon.setVisibleState(STATE_ICON); + mIconContainer.removeIgnoredSlot(mMobileSlotName); } }); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/SignalTileView.java b/packages/SystemUI/src/com/android/systemui/qs/SignalTileView.java index ce90fc107b82..b609df5e77fe 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/SignalTileView.java +++ b/packages/SystemUI/src/com/android/systemui/qs/SignalTileView.java @@ -134,8 +134,9 @@ public class SignalTileView extends QSIconViewImpl { mSignal.setPaddingRelative(0, 0, 0, 0); } final boolean shouldAnimate = allowAnimations && isShown(); - setVisibility(mIn, shouldAnimate, s.activityIn); - setVisibility(mOut, shouldAnimate, s.activityOut); + // Do not show activity indicators +// setVisibility(mIn, shouldAnimate, s.activityIn); +// setVisibility(mOut, shouldAnimate, s.activityOut); } private void setVisibility(View view, boolean shown, boolean visible) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java index dce081f21581..6f789d3c8541 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java +++ b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java @@ -63,7 +63,7 @@ public class QSCustomizer extends LinearLayout { private boolean mIsShowingNavBackdrop; public QSCustomizer(Context context, AttributeSet attrs) { - super(new ContextThemeWrapper(context, R.style.edit_theme), attrs); + super(new ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings_Edit), attrs); LayoutInflater.from(getContext()).inflate(R.layout.qs_customize_panel_content, this); mClipper = new QSDetailClipper(findViewById(R.id.customize_container)); 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 e9abef42a282..08aa599d34b7 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java @@ -728,7 +728,7 @@ public class TileAdapter extends RecyclerView.Adapter<Holder> implements TileSta GridLayoutManager lm = ((GridLayoutManager) parent.getLayoutManager()); SpanSizeLookup span = lm.getSpanSizeLookup(); ViewHolder holder = parent.getChildViewHolder(view); - int column = span.getSpanIndex(holder.getLayoutPosition(), lm.getSpanCount()); + int column = span.getSpanIndex(holder.getBindingAdapterPosition(), lm.getSpanCount()); if (view instanceof TextView) { super.getItemOffsets(outRect, view, parent, state); diff --git a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java index d41bd7ad4d3c..75a7e8e09afa 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java +++ b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java @@ -32,6 +32,7 @@ import com.android.systemui.statusbar.phone.AutoTileManager; import com.android.systemui.statusbar.phone.ManagedProfileController; import com.android.systemui.statusbar.policy.CastController; import com.android.systemui.statusbar.policy.DataSaverController; +import com.android.systemui.statusbar.policy.DeviceControlsController; import com.android.systemui.statusbar.policy.HotspotController; import com.android.systemui.util.settings.SecureSettings; @@ -61,6 +62,7 @@ public interface QSModule { NightDisplayListener nightDisplayListener, CastController castController, ReduceBrightColorsController reduceBrightColorsController, + DeviceControlsController deviceControlsController, @Named(RBC_AVAILABLE) boolean isReduceBrightColorsAvailable) { AutoTileManager manager = new AutoTileManager( context, @@ -74,6 +76,7 @@ public interface QSModule { nightDisplayListener, castController, reduceBrightColorsController, + deviceControlsController, isReduceBrightColorsAvailable ); manager.init(); diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistHandleBehavior.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/HeightOverrideable.kt index 2eda3d784382..866fa097d6fd 100644 --- a/packages/SystemUI/src/com/android/systemui/assist/AssistHandleBehavior.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/HeightOverrideable.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 The Android Open Source Project + * 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. @@ -14,12 +14,16 @@ * limitations under the License. */ -package com.android.systemui.assist; +package com.android.systemui.qs.tileimpl -public enum AssistHandleBehavior { +interface HeightOverrideable { + companion object { + const val NO_OVERRIDE = -1 + } - TEST, - OFF, - LIKE_HOME, - REMINDER_EXP; -} + var heightOverride: Int + + fun resetOverride() { + heightOverride = NO_OVERRIDE + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java index ba349c6273d9..9b0536c595ad 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java @@ -19,9 +19,7 @@ import static com.android.systemui.qs.dagger.QSFlagsModule.QS_LABELS_FLAG; import android.content.Context; import android.os.Build; import android.util.Log; -import android.view.ContextThemeWrapper; -import com.android.systemui.R; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.plugins.qs.QSFactory; import com.android.systemui.plugins.qs.QSIconView; @@ -251,8 +249,7 @@ public class QSFactoryImpl implements QSFactory { } @Override - public QSTileView createTileView(QSTile tile, boolean collapsedView) { - Context context = new ContextThemeWrapper(mQsHostLazy.get().getContext(), R.style.qs_theme); + public QSTileView createTileView(Context context, QSTile tile, boolean collapsedView) { QSIconView icon = tile.createTileView(context); if (mSideLabels) { return new QSTileViewHorizontal(context, icon, collapsedView); diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileView.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileView.java index c7ed89ba49b1..3d5a709a5fdd 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileView.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileView.java @@ -43,7 +43,7 @@ public class QSTileView extends QSTileBaseView { protected TextView mLabel; protected TextView mSecondLine; private ImageView mPadLock; - private int mState; + protected int mState; protected ViewGroup mLabelContainer; private View mExpandIndicator; private View mExpandSpace; @@ -133,14 +133,7 @@ public class QSTileView extends QSTileBaseView { protected void handleStateChanged(QSTile.State state) { super.handleStateChanged(state); if (!Objects.equals(mLabel.getText(), state.label) || mState != state.state) { - ColorStateList labelColor; - if (state.state == Tile.STATE_ACTIVE) { - labelColor = mColorLabelActive; - } else if (state.state == Tile.STATE_INACTIVE) { - labelColor = mColorLabelInactive; - } else { - labelColor = mColorLabelUnavailable; - } + ColorStateList labelColor = getLabelColor(state.state); changeLabelColor(labelColor); mState = state.state; mLabel.setText(state.label); @@ -163,6 +156,15 @@ public class QSTileView extends QSTileBaseView { mPadLock.setVisibility(state.disabledByPolicy ? View.VISIBLE : View.GONE); } + protected final ColorStateList getLabelColor(int state) { + if (state == Tile.STATE_ACTIVE) { + return mColorLabelActive; + } else if (state == Tile.STATE_INACTIVE) { + return mColorLabelInactive; + } + return mColorLabelUnavailable; + } + protected void changeLabelColor(ColorStateList color) { mLabel.setTextColor(color); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewHorizontal.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewHorizontal.kt index 32285cf797e4..7a8b2c696fa0 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewHorizontal.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewHorizontal.kt @@ -24,6 +24,8 @@ import android.graphics.drawable.Drawable import android.graphics.drawable.RippleDrawable import android.service.quicksettings.Tile.STATE_ACTIVE import android.view.Gravity +import android.view.View +import android.widget.ImageView import android.widget.LinearLayout import android.widget.RelativeLayout import com.android.systemui.R @@ -35,12 +37,14 @@ open class QSTileViewHorizontal( context: Context, icon: QSIconView, collapsed: Boolean -) : QSTileView(context, icon, collapsed) { +) : QSTileView(context, icon, collapsed), HeightOverrideable { protected var colorBackgroundDrawable: Drawable? = null private var paintColor = Color.WHITE private var paintAnimator: ValueAnimator? = null private var labelAnimator: ValueAnimator? = null + private var mSideView: ImageView = ImageView(mContext) + override var heightOverride: Int = HeightOverrideable.NO_OVERRIDE init { orientation = HORIZONTAL @@ -55,7 +59,23 @@ open class QSTileViewHorizontal( val iconSize = context.resources.getDimensionPixelSize(R.dimen.qs_icon_size) addView(mIcon, 0, LayoutParams(iconSize, iconSize)) + mSideView.visibility = View.GONE + addView( + mSideView, + -1, + LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply { + gravity = Gravity.CENTER_VERTICAL + }) + mColorLabelActive = ColorStateList.valueOf(getColorForState(getContext(), STATE_ACTIVE)) + changeLabelColor(getLabelColor(mState)) // Matches the default state of the tile + } + + override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { + super.onLayout(changed, l, t, r, b) + if (heightOverride != HeightOverrideable.NO_OVERRIDE) { + bottom = top + heightOverride + } } override fun createLabel() { @@ -113,14 +133,13 @@ open class QSTileViewHorizontal( if (allowAnimations) { animateBackground(newColor) } else { - if (newColor != paintColor) { - clearBackgroundAnimator() - colorBackgroundDrawable?.setTintList(ColorStateList.valueOf(newColor))?.also { - paintColor = newColor - } + clearBackgroundAnimator() + colorBackgroundDrawable?.setTintList(ColorStateList.valueOf(newColor))?.also { paintColor = newColor } + paintColor = newColor } + loadSideViewDrawableIfNecessary(state) } private fun animateBackground(newBackgroundColor: Int) { @@ -173,5 +192,21 @@ open class QSTileViewHorizontal( labelAnimator?.cancel()?.also { labelAnimator = null } } + private fun loadSideViewDrawableIfNecessary(state: QSTile.State) { + if (state.sideViewDrawable != null) { + (mSideView.layoutParams as MarginLayoutParams).apply { + marginStart = + context.resources.getDimensionPixelSize(R.dimen.qs_label_container_margin) + } + mSideView.setImageDrawable(state.sideViewDrawable) + mSideView.visibility = View.VISIBLE + mSideView.adjustViewBounds = true + mSideView.scaleType = ImageView.ScaleType.FIT_CENTER + } else { + mSideView.setImageDrawable(null) + mSideView.visibility = GONE + } + } + override fun handleExpand(dualTarget: Boolean) {} }
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CameraToggleTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CameraToggleTile.java index 0d73a5a97706..5b986b6fece9 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CameraToggleTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CameraToggleTile.java @@ -66,7 +66,7 @@ public class CameraToggleTile extends SensorPrivacyToggleTile { return getHost().getContext().getPackageManager().hasSystemFeature(FEATURE_CAMERA_TOGGLE) && whitelistIpcs(() -> DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PRIVACY, "camera_toggle_enabled", - false)); + true)); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DeviceControlsTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/DeviceControlsTile.kt index a74a50e7d6c2..a2c7633c4746 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DeviceControlsTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DeviceControlsTile.kt @@ -39,8 +39,6 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.qs.QSHost import com.android.systemui.qs.logging.QSLogger import com.android.systemui.qs.tileimpl.QSTileImpl -import com.android.systemui.statusbar.FeatureFlags -import com.android.systemui.util.settings.GlobalSettings import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject @@ -53,9 +51,7 @@ class DeviceControlsTile @Inject constructor( statusBarStateController: StatusBarStateController, activityStarter: ActivityStarter, qsLogger: QSLogger, - private val controlsComponent: ControlsComponent, - private val featureFlags: FeatureFlags, - globalSettings: GlobalSettings + private val controlsComponent: ControlsComponent ) : QSTileImpl<QSTile.State>( host, backgroundLooper, @@ -67,11 +63,6 @@ class DeviceControlsTile @Inject constructor( qsLogger ) { - companion object { - const val SETTINGS_FLAG = "controls_lockscreen" - } - - private val controlsLockscreen = globalSettings.getInt(SETTINGS_FLAG, 0) != 0 private var hasControlsApps = AtomicBoolean(false) private val intent = Intent(Settings.ACTION_DEVICE_CONTROLS_SETTINGS) @@ -92,9 +83,7 @@ class DeviceControlsTile @Inject constructor( } override fun isAvailable(): Boolean { - return featureFlags.isKeyguardLayoutEnabled && - controlsLockscreen && - controlsComponent.getControlsController().isPresent + return controlsComponent.getControlsController().isPresent } override fun newTileState(): QSTile.State { @@ -114,7 +103,7 @@ class DeviceControlsTile @Inject constructor( val i = Intent().apply { component = ComponentName(mContext, ControlsActivity::class.java) addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK) - putExtra(ControlsUiController.BACK_TO_GLOBAL_ACTIONS, false) + putExtra(ControlsUiController.EXTRA_ANIMATE, true) } mContext.startActivity(i) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/MicrophoneToggleTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/MicrophoneToggleTile.java index b8d879226f55..42bd77b354bb 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/MicrophoneToggleTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/MicrophoneToggleTile.java @@ -67,7 +67,7 @@ public class MicrophoneToggleTile extends SensorPrivacyToggleTile { .hasSystemFeature(FEATURE_MICROPHONE_TOGGLE) && whitelistIpcs(() -> DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PRIVACY, "mic_toggle_enabled", - false)); + true)); } @Override 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 60c5d1cafde9..bf9655837b25 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java @@ -20,11 +20,20 @@ import static android.provider.Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT; import android.content.Intent; import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; import android.os.Handler; import android.os.Looper; +import android.service.quickaccesswallet.GetWalletCardsError; +import android.service.quickaccesswallet.GetWalletCardsRequest; +import android.service.quickaccesswallet.GetWalletCardsResponse; import android.service.quickaccesswallet.QuickAccessWalletClient; +import android.service.quickaccesswallet.WalletCard; import android.service.quicksettings.Tile; +import android.util.Log; +import androidx.annotation.NonNull; + +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.MetricsLogger; import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.Background; @@ -40,20 +49,30 @@ import com.android.systemui.statusbar.FeatureFlags; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.util.settings.SecureSettings; +import java.util.List; +import java.util.concurrent.Executor; + import javax.inject.Inject; /** Quick settings tile: Quick access wallet **/ public class QuickAccessWalletTile extends QSTileImpl<QSTile.State> { + private static final String TAG = "QuickAccessWalletTile"; private static final String FEATURE_CHROME_OS = "org.chromium.arc"; + private final CharSequence mLabel = mContext.getString(R.string.wallet_title); + private final WalletCardRetriever mCardRetriever = new WalletCardRetriever(); // TODO(b/180959290): Re-create the QAW Client when the default NFC payment app changes. private final QuickAccessWalletClient mQuickAccessWalletClient; private final KeyguardStateController mKeyguardStateController; private final PackageManager mPackageManager; private final SecureSettings mSecureSettings; + private final Executor mExecutor; private final FeatureFlags mFeatureFlags; + @VisibleForTesting Drawable mCardViewDrawable; + private boolean mHasCard; + @Inject public QuickAccessWalletTile( QSHost host, @@ -68,6 +87,7 @@ public class QuickAccessWalletTile extends QSTileImpl<QSTile.State> { KeyguardStateController keyguardStateController, PackageManager packageManager, SecureSettings secureSettings, + @Background Executor executor, FeatureFlags featureFlags) { super(host, backgroundLooper, mainHandler, falsingManager, metricsLogger, statusBarStateController, activityStarter, qsLogger); @@ -75,6 +95,7 @@ public class QuickAccessWalletTile extends QSTileImpl<QSTile.State> { mKeyguardStateController = keyguardStateController; mPackageManager = packageManager; mSecureSettings = secureSettings; + mExecutor = executor; mFeatureFlags = featureFlags; } @@ -87,6 +108,14 @@ public class QuickAccessWalletTile extends QSTileImpl<QSTile.State> { } @Override + protected void handleSetListening(boolean listening) { + super.handleSetListening(listening); + if (listening) { + queryWalletCards(); + } + } + + @Override protected void handleClick() { mActivityStarter.postStartActivityDismissingKeyguard( mQuickAccessWalletClient.createWalletIntent(), /* delay= */ 0); @@ -100,14 +129,27 @@ public class QuickAccessWalletTile extends QSTileImpl<QSTile.State> { state.icon = ResourceIcon.get(R.drawable.ic_qs_wallet); boolean isDeviceLocked = !mKeyguardStateController.isUnlocked(); if (mQuickAccessWalletClient.isWalletFeatureAvailable()) { - state.state = isDeviceLocked ? Tile.STATE_INACTIVE : Tile.STATE_ACTIVE; - state.secondaryLabel = isDeviceLocked - ? null - : mContext.getString(R.string.wallet_secondary_label); + if (mHasCard) { + if (isDeviceLocked) { + state.state = Tile.STATE_INACTIVE; + state.secondaryLabel = + mContext.getString(R.string.wallet_secondary_label_device_locked); + } else { + state.state = Tile.STATE_ACTIVE; + state.secondaryLabel = + mContext.getString(R.string.wallet_secondary_label_active); + } + } else { + state.state = Tile.STATE_INACTIVE; + state.secondaryLabel = mContext.getString(R.string.wallet_secondary_label_no_card); + } state.stateDescription = state.secondaryLabel; } else { state.state = Tile.STATE_UNAVAILABLE; } + if (!isDeviceLocked) { + state.sideViewDrawable = mCardViewDrawable; + } } @Override @@ -133,4 +175,43 @@ public class QuickAccessWalletTile extends QSTileImpl<QSTile.State> { CharSequence qawLabel = mQuickAccessWalletClient.getServiceLabel(); return qawLabel == null ? mLabel : qawLabel; } + + private void queryWalletCards() { + int cardWidth = + mContext.getResources().getDimensionPixelSize(R.dimen.wallet_tile_card_view_width); + int cardHeight = + mContext.getResources().getDimensionPixelSize(R.dimen.wallet_tile_card_view_height); + int iconSizePx = mContext.getResources().getDimensionPixelSize(R.dimen.wallet_icon_size); + GetWalletCardsRequest request = + new GetWalletCardsRequest(cardWidth, cardHeight, iconSizePx, /* maxCards= */ 2); + mQuickAccessWalletClient.getWalletCards(mExecutor, request, mCardRetriever); + } + + private class WalletCardRetriever implements + QuickAccessWalletClient.OnWalletCardsRetrievedCallback { + + @Override + public void onWalletCardsRetrieved(@NonNull GetWalletCardsResponse response) { + Log.i(TAG, "Successfully retrieved wallet cards."); + List<WalletCard> cards = response.getWalletCards(); + if (cards.isEmpty()) { + Log.d(TAG, "No wallet cards exist."); + mCardViewDrawable = null; + mHasCard = false; + refreshState(); + return; + } + mCardViewDrawable = cards.get(0).getCardImage().loadDrawable(mContext); + mHasCard = true; + refreshState(); + } + + @Override + public void onWalletCardRetrievalError(@NonNull GetWalletCardsError error) { + Log.w(TAG, "Error retrieve wallet cards"); + mCardViewDrawable = null; + mHasCard = false; + refreshState(); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java index b0a3f437b5ec..afbb197c4a12 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java @@ -58,9 +58,12 @@ import android.os.IBinder; import android.os.Looper; import android.os.PatternMatcher; import android.os.RemoteException; +import android.os.SystemClock; import android.os.UserHandle; import android.util.Log; +import android.view.InputDevice; import android.view.InputMonitor; +import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.Surface; @@ -240,6 +243,36 @@ public class OverviewProxyService extends CurrentUserTracker implements } @Override + public void onBackPressed() throws RemoteException { + if (!verifyCaller("onBackPressed")) { + return; + } + final long token = Binder.clearCallingIdentity(); + try { + mHandler.post(() -> { + sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK); + sendEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK); + + notifyBackAction(true, -1, -1, true, false); + }); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + private boolean sendEvent(int action, int code) { + long when = SystemClock.uptimeMillis(); + final KeyEvent ev = new KeyEvent(when, when, action, code, 0 /* repeat */, + 0 /* metaState */, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /* scancode */, + KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY, + InputDevice.SOURCE_KEYBOARD); + + ev.setDisplayId(mContext.getDisplay().getDisplayId()); + return InputManager.getInstance() + .injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); + } + + @Override public void onOverviewShown(boolean fromHome) { if (!verifyCaller("onOverviewShown")) { return; diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java index 04d199645b24..9ce0eebfd441 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java @@ -411,19 +411,22 @@ public class LongScreenshotActivity extends Activity { float imageRatio = bounds.width() / (float) bounds.height(); int previewWidth = mPreview.getWidth() - mPreview.getPaddingLeft() - mPreview.getPaddingRight(); - float viewRatio = previewWidth / (float) mPreview.getHeight(); + int previewHeight = mPreview.getHeight() - mPreview.getPaddingTop() + - mPreview.getPaddingBottom(); + float viewRatio = previewWidth / (float) previewHeight; if (imageRatio > viewRatio) { // Image is full width and height is constrained, compute extra padding to inform // CropView - float imageHeight = mPreview.getHeight() * viewRatio / imageRatio; - int extraPadding = (int) (mPreview.getHeight() - imageHeight) / 2; - mCropView.setExtraPadding(extraPadding, extraPadding); + float imageHeight = previewHeight * viewRatio / imageRatio; + int extraPadding = (int) (previewHeight - imageHeight) / 2; + mCropView.setExtraPadding(extraPadding + mPreview.getPaddingTop(), + extraPadding + mPreview.getPaddingBottom()); mCropView.setImageWidth(previewWidth); } else { // Image is full height - mCropView.setExtraPadding(0, 0); - mCropView.setImageWidth((int) (mPreview.getHeight() * imageRatio)); + mCropView.setExtraPadding(mPreview.getPaddingTop(), mPreview.getPaddingBottom()); + mCropView.setImageWidth((int) (previewHeight * imageRatio)); } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureClient.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureClient.java index 926d5c4701aa..2863074bee0a 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureClient.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureClient.java @@ -83,6 +83,13 @@ public class ScrollCaptureClient { int getMaxTiles(); /** + * @return the maximum combined capture height for this session, in pixels. + */ + default int getMaxHeight() { + return getMaxTiles() * getTileHeight(); + } + + /** * @return the height of each image tile */ int getTileHeight(); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java index d3dd048a989e..bbcfdbd99bef 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java @@ -27,6 +27,7 @@ import android.view.ScrollCaptureResponse; import androidx.concurrent.futures.CallbackToFutureAdapter; import androidx.concurrent.futures.CallbackToFutureAdapter.Completer; +import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.screenshot.ScrollCaptureClient.CaptureResult; import com.android.systemui.screenshot.ScrollCaptureClient.Session; @@ -139,6 +140,11 @@ public class ScrollCaptureController { mImageTileSet = imageTileSet; } + @VisibleForTesting + float getTargetTopSizeRatio() { + return IDEAL_PORTION_ABOVE; + } + /** * Run scroll capture. Performs a batch capture, collecting image tiles. * @@ -197,7 +203,7 @@ public class ScrollCaptureController { && result.captured.height() < result.requested.height(); boolean finish = false; - if (partialResult || emptyResult) { + if (emptyResult) { // Potentially reached a vertical boundary. Extend in the other direction. if (mFinishOnBoundary) { Log.d(TAG, "Partial/empty: finished!"); @@ -211,12 +217,12 @@ public class ScrollCaptureController { Log.d(TAG, "Partial/empty: cleared, switch direction to finish"); } } else { - // Got the full requested result, but may have got enough bitmap data now + // Got a non-empty result, but may already have enough bitmap data now int expectedTiles = mImageTileSet.size() + 1; if (expectedTiles >= mSession.getMaxTiles()) { Log.d(TAG, "Hit max tiles: finished"); - // If we ever hit the max tiles, we've got enough bitmap data to finish (even if we - // weren't sure we'd finish on this pass). + // If we ever hit the max tiles, we've got enough bitmap data to finish + // (even if we weren't sure we'd finish on this pass). finish = true; } else { if (mScrollingUp && !mFinishOnBoundary) { @@ -253,10 +259,18 @@ public class ScrollCaptureController { return; } - // Partial or empty results caused the direction the flip, so we can reliably use the - // requested edges to determine the next top. - int nextTop = (mScrollingUp) ? result.requested.top - mSession.getTileHeight() - : result.requested.bottom; + int nextTop; + if (emptyResult) { + // An empty result caused the direction the flip, + // so use the requested edges to determine the next top. + nextTop = (mScrollingUp) + ? result.requested.top - mSession.getTileHeight() + : result.requested.bottom; + } else { + nextTop = (mScrollingUp) + ? result.captured.top - mSession.getTileHeight() + : result.captured.bottom; + } requestNextTile(nextTop); } diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java index bdb392608455..357256cba131 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java @@ -58,10 +58,9 @@ public class BrightnessController implements ToggleSlider.Listener { private static final int SLIDER_ANIMATION_DURATION = 3000; private static final int MSG_UPDATE_SLIDER = 1; - private static final int MSG_SET_CHECKED = 2; - private static final int MSG_ATTACH_LISTENER = 3; - private static final int MSG_DETACH_LISTENER = 4; - private static final int MSG_VR_MODE_CHANGED = 5; + private static final int MSG_ATTACH_LISTENER = 2; + private static final int MSG_DETACH_LISTENER = 3; + private static final int MSG_VR_MODE_CHANGED = 4; private static final Uri BRIGHTNESS_MODE_URI = Settings.System.getUriFor(Settings.System.SCREEN_BRIGHTNESS_MODE); @@ -80,7 +79,6 @@ public class BrightnessController implements ToggleSlider.Listener { private final int mDisplayId; private final Context mContext; private final ToggleSlider mControl; - private final boolean mAutomaticAvailable; private final DisplayManager mDisplayManager; private final CurrentUserTracker mUserTracker; private final IVrManager mVrManager; @@ -219,16 +217,12 @@ public class BrightnessController implements ToggleSlider.Listener { private final Runnable mUpdateModeRunnable = new Runnable() { @Override public void run() { - if (mAutomaticAvailable) { - int automatic; - automatic = Settings.System.getIntForUser(mContext.getContentResolver(), - Settings.System.SCREEN_BRIGHTNESS_MODE, - Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL, - UserHandle.USER_CURRENT); - mAutomatic = automatic != Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL; - } else { - mHandler.obtainMessage(MSG_SET_CHECKED, 0).sendToTarget(); - } + int automatic; + automatic = Settings.System.getIntForUser(mContext.getContentResolver(), + Settings.System.SCREEN_BRIGHTNESS_MODE, + Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL, + UserHandle.USER_CURRENT); + mAutomatic = automatic != Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL; } }; @@ -266,9 +260,6 @@ public class BrightnessController implements ToggleSlider.Listener { case MSG_UPDATE_SLIDER: updateSlider(Float.intBitsToFloat(msg.arg1), msg.arg2 != 0); break; - case MSG_SET_CHECKED: - mControl.setChecked(msg.arg1 != 0); - break; case MSG_ATTACH_LISTENER: mControl.setOnChangedListener(BrightnessController.this); break; @@ -312,9 +303,6 @@ public class BrightnessController implements ToggleSlider.Listener { mDefaultBacklightForVr = pm.getBrightnessConstraint( PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_DEFAULT_VR); - - mAutomaticAvailable = context.getResources().getBoolean( - com.android.internal.R.bool.config_automatic_brightness_available); mDisplayManager = context.getSystemService(DisplayManager.class); mVrManager = IVrManager.Stub.asInterface(ServiceManager.getService( Context.VR_SERVICE)); @@ -339,8 +327,7 @@ public class BrightnessController implements ToggleSlider.Listener { } @Override - public void onChanged(boolean tracking, boolean automatic, - int value, boolean stopTracking) { + public void onChanged(boolean tracking, int value, boolean stopTracking) { if (mExternalChange) return; if (mSliderAnimator != null) { @@ -398,12 +385,6 @@ public class BrightnessController implements ToggleSlider.Listener { }); } - private void setMode(int mode) { - Settings.System.putIntForUser(mContext.getContentResolver(), - Settings.System.SCREEN_BRIGHTNESS_MODE, mode, - mUserTracker.getCurrentUserId()); - } - private void setBrightness(float brightness) { mDisplayManager.setTemporaryBrightness(mDisplayId, brightness); } diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessControllerSettings.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessControllerSettings.java deleted file mode 100644 index 3c7d78c928fa..000000000000 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessControllerSettings.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2020 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.settings.brightness; - -import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.statusbar.FeatureFlags; - -import javax.inject.Inject; - -/** - * Settings for prototyping thick brightness slider - */ -@SysUISingleton -public class BrightnessControllerSettings { - - private static final String THICK_BRIGHTNESS_SLIDER = "sysui_thick_brightness"; - private final FeatureFlags mFeatureFlags; - - @Inject - public BrightnessControllerSettings(FeatureFlags featureFlags) { - mFeatureFlags = featureFlags; - } - - // Changing this setting between zero and non-zero may crash systemui down the line. Better to - // restart systemui after changing it. - /** */ - boolean useThickSlider() { - return mFeatureFlags.useNewBrightnessSlider(); - } - - /** */ - boolean useMirrorOnThickSlider() { - return !useThickSlider(); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSlider.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSlider.java index 5d964a47cf29..db8205783173 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSlider.java +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSlider.java @@ -21,7 +21,6 @@ import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; -import android.widget.CompoundButton; import android.widget.SeekBar; import androidx.annotation.Nullable; @@ -52,7 +51,6 @@ public class BrightnessSlider extends ViewController<View> implements ToggleSlid private final BrightnessSliderView mBrightnessSliderView; private BrightnessMirrorController mMirrorController; private boolean mTracking; - private final boolean mUseMirror; private final FalsingManager mFalsingManager; private final Gefingerpoken mOnInterceptListener = new Gefingerpoken() { @@ -75,11 +73,9 @@ public class BrightnessSlider extends ViewController<View> implements ToggleSlid BrightnessSlider( View rootView, BrightnessSliderView brightnessSliderView, - boolean useMirror, FalsingManager falsingManager) { super(rootView); mBrightnessSliderView = brightnessSliderView; - mUseMirror = useMirror; mFalsingManager = falsingManager; } @@ -97,14 +93,12 @@ public class BrightnessSlider extends ViewController<View> implements ToggleSlid @Override protected void onViewAttached() { mBrightnessSliderView.setOnSeekBarChangeListener(mSeekListener); - mBrightnessSliderView.setOnCheckedChangeListener(mCheckListener); mBrightnessSliderView.setOnInterceptListener(mOnInterceptListener); } @Override protected void onViewDetached() { mBrightnessSliderView.setOnSeekBarChangeListener(null); - mBrightnessSliderView.setOnCheckedChangeListener(null); mBrightnessSliderView.setOnDispatchTouchEventListener(null); mBrightnessSliderView.setOnInterceptListener(null); } @@ -134,7 +128,6 @@ public class BrightnessSlider extends ViewController<View> implements ToggleSlid private void setMirror(ToggleSlider toggleSlider) { mMirror = toggleSlider; if (mMirror != null) { - mMirror.setChecked(mBrightnessSliderView.isChecked()); mMirror.setMax(mBrightnessSliderView.getMax()); mMirror.setValue(mBrightnessSliderView.getValue()); mBrightnessSliderView.setOnDispatchTouchEventListener(this::mirrorTouchEvent); @@ -152,7 +145,6 @@ public class BrightnessSlider extends ViewController<View> implements ToggleSlid */ @Override public void setMirrorControllerAndMirror(BrightnessMirrorController c) { - if (!mUseMirror) return; mMirrorController = c; if (c != null) { setMirror(c.getToggleSlider()); @@ -169,16 +161,6 @@ public class BrightnessSlider extends ViewController<View> implements ToggleSlid } @Override - public void setChecked(boolean checked) { - mBrightnessSliderView.setChecked(checked); - } - - @Override - public boolean isChecked() { - return mBrightnessSliderView.isChecked(); - } - - @Override public void setMax(int max) { mBrightnessSliderView.setMax(max); if (mMirror != null) { @@ -209,7 +191,7 @@ public class BrightnessSlider extends ViewController<View> implements ToggleSlid @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { if (mListener != null) { - mListener.onChanged(mTracking, isChecked(), progress, false); + mListener.onChanged(mTracking, progress, false); } } @@ -218,12 +200,9 @@ public class BrightnessSlider extends ViewController<View> implements ToggleSlid mTracking = true; if (mListener != null) { - mListener.onChanged(mTracking, isChecked(), - getValue(), false); + mListener.onChanged(mTracking, getValue(), false); } - setChecked(false); - if (mMirrorController != null) { mMirrorController.showMirror(); mMirrorController.setLocation((View) mBrightnessSliderView.getParent()); @@ -235,8 +214,7 @@ public class BrightnessSlider extends ViewController<View> implements ToggleSlid mTracking = false; if (mListener != null) { - mListener.onChanged(mTracking, isChecked(), - getValue(), true); + mListener.onChanged(mTracking, getValue(), true); } if (mMirrorController != null) { @@ -245,35 +223,15 @@ public class BrightnessSlider extends ViewController<View> implements ToggleSlid } }; - private final CompoundButton.OnCheckedChangeListener mCheckListener = - new CompoundButton.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton toggle, boolean checked) { - enableSlider(!checked); - - if (mListener != null) { - mListener.onChanged(mTracking, checked, getValue(), false); - } - - if (mMirror != null) { - mMirror.setChecked(checked); - } - } - }; - /** * Creates a {@link BrightnessSlider} with its associated view. - * - * The views inflated are determined by {@link BrightnessControllerSettings#useThickSlider()}. */ public static class Factory { - BrightnessControllerSettings mSettings; private final FalsingManager mFalsingManager; @Inject - public Factory(BrightnessControllerSettings settings, FalsingManager falsingManager) { - mSettings = settings; + public Factory(FalsingManager falsingManager) { mFalsingManager = falsingManager; } @@ -288,20 +246,18 @@ public class BrightnessSlider extends ViewController<View> implements ToggleSlid int layout = getLayout(); ViewGroup root = (ViewGroup) LayoutInflater.from(context) .inflate(layout, viewRoot, false); - return fromTree(root, mSettings.useMirrorOnThickSlider()); + return fromTree(root); } - private BrightnessSlider fromTree(ViewGroup root, boolean useMirror) { + private BrightnessSlider fromTree(ViewGroup root) { BrightnessSliderView v = root.requireViewById(R.id.brightness_slider); - return new BrightnessSlider(root, v, useMirror, mFalsingManager); + return new BrightnessSlider(root, v, mFalsingManager); } /** Get the layout to inflate based on what slider to use */ private int getLayout() { - return mSettings.useThickSlider() - ? R.layout.quick_settings_brightness_dialog_thick - : R.layout.quick_settings_brightness_dialog; + return R.layout.quick_settings_brightness_dialog; } } } 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 5b71c626bb22..dbd6758b090d 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderView.java +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderView.java @@ -17,18 +17,13 @@ package com.android.systemui.settings.brightness; import android.content.Context; -import android.content.res.TypedArray; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; -import android.widget.CompoundButton; -import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.FrameLayout; import android.widget.SeekBar.OnSeekBarChangeListener; -import android.widget.TextView; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import com.android.settingslib.RestrictedLockUtils; import com.android.systemui.Gefingerpoken; @@ -37,23 +32,11 @@ import com.android.systemui.R; /** * {@code FrameLayout} used to show and manipulate a {@link ToggleSeekBar}. * - * It can additionally control a {@link CompoundButton} and display a label. For the class to work, - * add children before inflation with the following ids: - * <ul> - * <li>{@code @id/slider} of type {@link ToggleSeekBar}</li> - * <li>{@code @id/toggle} of type {@link CompoundButton} (optional)</li> - * <li>{@code @id/label} of type {@link TextView} (optional)</li> - * </ul> */ public class BrightnessSliderView extends FrameLayout { - @Nullable - private CompoundButton mToggle; @NonNull private ToggleSeekBar mSlider; - @Nullable - private TextView mLabel; - private final CharSequence mText; private DispatchTouchEventListener mListener; private Gefingerpoken mOnInterceptListener; @@ -62,31 +45,15 @@ public class BrightnessSliderView extends FrameLayout { } public BrightnessSliderView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public BrightnessSliderView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - final TypedArray a = context.obtainStyledAttributes( - attrs, R.styleable.ToggleSliderView, defStyle, 0); - mText = a.getString(R.styleable.ToggleSliderView_text); - - a.recycle(); + super(context, attrs); } // Inflated from quick_settings_brightness_dialog or quick_settings_brightness_dialog_thick @Override protected void onFinishInflate() { super.onFinishInflate(); - mToggle = findViewById(R.id.toggle); mSlider = requireViewById(R.id.slider); - - mLabel = findViewById(R.id.label); - if (mLabel != null) { - mLabel.setText(mText); - } mSlider.setAccessibilityLabel(getContentDescription().toString()); } @@ -125,25 +92,11 @@ public class BrightnessSliderView extends FrameLayout { } /** - * Attaches a listener to the {@link CompoundButton} in the view (if present) so changes to its - * state can be observed - * @param checkListener use {@code null} to remove listener - */ - public void setOnCheckedChangeListener(OnCheckedChangeListener checkListener) { - if (mToggle != null) { - mToggle.setOnCheckedChangeListener(checkListener); - } - } - - /** * Enforces admin rules for toggling auto-brightness and changing value of brightness * @param admin * @see ToggleSeekBar#setEnforcedAdmin */ public void setEnforcedAdmin(RestrictedLockUtils.EnforcedAdmin admin) { - if (mToggle != null) { - mToggle.setEnabled(admin == null); - } mSlider.setEnabled(admin == null); mSlider.setEnforcedAdmin(admin); } @@ -157,26 +110,6 @@ public class BrightnessSliderView extends FrameLayout { } /** - * Sets the state of the {@link CompoundButton} if present - * @param checked - */ - public void setChecked(boolean checked) { - if (mToggle != null) { - mToggle.setChecked(checked); - } - } - - /** - * @return the state of the {@link CompoundButton} if present, or {@code true} if not. - */ - public boolean isChecked() { - if (mToggle != null) { - return mToggle.isChecked(); - } - return true; - } - - /** * @return the maximum value of the {@link ToggleSeekBar}. */ public int getMax() { diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/ToggleSlider.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/ToggleSlider.java index 71e4818c605a..a988c7aeb436 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/ToggleSlider.java +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/ToggleSlider.java @@ -23,8 +23,7 @@ import com.android.systemui.statusbar.policy.BrightnessMirrorController; public interface ToggleSlider { interface Listener { - void onChanged(boolean tracking, boolean automatic, int value, - boolean stopTracking); + void onChanged(boolean tracking, int value, boolean stopTracking); } void setEnforcedAdmin(RestrictedLockUtils.EnforcedAdmin admin); @@ -32,8 +31,6 @@ public interface ToggleSlider { boolean mirrorTouchEvent(MotionEvent ev); void setOnChangedListener(Listener l); - default void setChecked(boolean checked) {} - default boolean isChecked() { return false; } void setMax(int max); int getMax(); void setValue(int value); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/FeatureFlags.java b/packages/SystemUI/src/com/android/systemui/statusbar/FeatureFlags.java index 90c3dfcee5bf..f51fbedebad2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/FeatureFlags.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/FeatureFlags.java @@ -62,11 +62,6 @@ public class FeatureFlags { return mFlagReader.isEnabled(R.bool.flag_keyguard_layout); } - /** b/178485354 */ - public boolean useNewBrightnessSlider() { - return mFlagReader.isEnabled(R.bool.flag_brightness_slider); - } - public boolean useNewLockscreenAnimations() { return mFlagReader.isEnabled(R.bool.flag_lockscreen_animations); } @@ -98,4 +93,8 @@ public class FeatureFlags { public boolean isChargingRippleEnabled() { return mFlagReader.isEnabled(R.bool.flag_charging_ripple); } + + public boolean isOngoingCallStatusBarChipEnabled() { + return mFlagReader.isEnabled(R.bool.flag_ongoing_call_status_bar_chip); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java index e090d0b13205..1c5df4119a24 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java @@ -161,22 +161,22 @@ public class NotificationGroupingUtil { private void sanitizeTopLineViews(ExpandableNotificationRow row) { if (row.isSummaryWithChildren()) { - sanitizeTopLine(row.getNotificationViewWrapper().getNotificationHeader()); + sanitizeTopLine(row.getNotificationViewWrapper().getNotificationHeader(), row); return; } final NotificationContentView layout = row.getPrivateLayout(); - sanitizeChild(layout.getContractedChild()); - sanitizeChild(layout.getHeadsUpChild()); - sanitizeChild(layout.getExpandedChild()); + sanitizeChild(layout.getContractedChild(), row); + sanitizeChild(layout.getHeadsUpChild(), row); + sanitizeChild(layout.getExpandedChild(), row); } - private void sanitizeChild(View child) { + private void sanitizeChild(View child, ExpandableNotificationRow row) { if (child != null) { - sanitizeTopLine(child.findViewById(R.id.notification_top_line)); + sanitizeTopLine(child.findViewById(R.id.notification_top_line), row); } } - private void sanitizeTopLine(ViewGroup rowHeader) { + private void sanitizeTopLine(ViewGroup rowHeader, ExpandableNotificationRow row) { if (rowHeader == null) { return; } @@ -195,7 +195,7 @@ public class NotificationGroupingUtil { } // in case no view is visible we make sure the time is visible int timeVisibility = !hasVisibleText - || mRow.getEntry().getSbn().getNotification().showsTime() + || row.getEntry().getSbn().getNotification().showsTime() ? View.VISIBLE : View.GONE; time.setVisibility(timeVisibility); View left = null; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java index 3daa2b3e1e64..f57fd21526b9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java @@ -173,7 +173,7 @@ public class NotificationShelf extends ActivatableNotificationView implements getFullyClosedTranslation()); viewState.zTranslation = ambientState.getBaseZHeight(); viewState.clipTopAmount = 0; - viewState.alpha = 1; + viewState.alpha = 1f - ambientState.getHideAmount(); viewState.belowSpeedBump = mHostLayoutController.getSpeedBumpIndex() == 0; viewState.hideSensitive = false; viewState.xTranslation = getTranslationX(); 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 05af08e0287a..77b418670ca6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/charging/ChargingRippleView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/charging/ChargingRippleView.kt @@ -20,13 +20,14 @@ import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.ValueAnimator import android.content.Context +import android.content.res.Configuration import android.graphics.Canvas import android.graphics.Paint import android.graphics.PointF import android.util.AttributeSet import android.view.View -private const val RIPPLE_SPARKLE_STRENGTH: Float = 0.4f +private const val RIPPLE_SPARKLE_STRENGTH: Float = 0.3f /** * Expanding ripple effect that shows when charging begins. @@ -41,7 +42,7 @@ class ChargingRippleView(context: Context?, attrs: AttributeSet?) : View(context set(value) { rippleShader.radius = value } var origin: PointF = PointF() set(value) { rippleShader.origin = value } - var duration: Long = 1500 + var duration: Long = 1750 init { rippleShader.color = defaultColor @@ -51,6 +52,16 @@ class ChargingRippleView(context: Context?, attrs: AttributeSet?) : View(context visibility = View.GONE } + override fun onConfigurationChanged(newConfig: Configuration?) { + rippleShader.pixelDensity = resources.displayMetrics.density + super.onConfigurationChanged(newConfig) + } + + override fun onAttachedToWindow() { + rippleShader.pixelDensity = resources.displayMetrics.density + super.onAttachedToWindow() + } + fun startRipple() { if (rippleInProgress) { return // Ignore if ripple effect is already playing @@ -59,9 +70,10 @@ class ChargingRippleView(context: Context?, attrs: AttributeSet?) : View(context animator.duration = duration animator.addUpdateListener { animator -> val now = animator.currentPlayTime - val phase = now / 30000f - rippleShader.progress = animator.animatedValue as Float - rippleShader.noisePhase = phase + val progress = animator.animatedValue as Float + rippleShader.progress = progress + rippleShader.distortionStrength = 1 - progress + rippleShader.time = now.toFloat() invalidate() } animator.addListener(object : AnimatorListenerAdapter() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/charging/RippleShader.kt b/packages/SystemUI/src/com/android/systemui/statusbar/charging/RippleShader.kt index d400205af50b..146046b33375 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/charging/RippleShader.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/charging/RippleShader.kt @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.charging import android.graphics.Color import android.graphics.PointF import android.graphics.RuntimeShader +import android.util.MathUtils /** * Shader class that renders an expanding charging ripple effect. A charging ripple contains @@ -33,7 +34,15 @@ class RippleShader internal constructor() : RuntimeShader(SHADER, false) { private const val SHADER_UNIFORMS = """uniform vec2 in_origin; uniform float in_progress; uniform float in_maxRadius; - uniform float in_noisePhase; + uniform float in_time; + uniform float in_distort_radial; + uniform float in_distort_xy; + uniform float in_radius; + uniform float in_fadeSparkle; + uniform float in_fadeCircle; + uniform float in_fadeRing; + uniform float in_blur; + uniform float in_pixelDensity; uniform vec4 in_color; uniform float in_sparkle_strength;""" private const val SHADER_LIB = """float triangleNoise(vec2 n) { @@ -52,12 +61,13 @@ class RippleShader internal constructor() : RuntimeShader(SHADER, false) { float n = triangleNoise(uv); float s = 0.0; for (float i = 0; i < 4; i += 1) { - float l = i * 0.25; - float h = l + 0.005; - float o = abs(sin(0.1 * PI * (t + i))); - s += threshold(n + o, l, h); + float l = i * 0.01; + float h = l + 0.1; + float o = smoothstep(n - l, h, n); + o *= abs(sin(PI * o * (t + 0.55 * i))); + s += o; } - return saturate(s); + return s; } float softCircle(vec2 uv, vec2 xy, float radius, float blur) { @@ -67,71 +77,98 @@ class RippleShader internal constructor() : RuntimeShader(SHADER, false) { } float softRing(vec2 uv, vec2 xy, float radius, float blur) { - float thickness = 0.4; - float circle_outer = softCircle(uv, xy, - radius + thickness * radius * 0.5, blur); - float circle_inner = softCircle(uv, xy, - radius - thickness * radius * 0.5, blur); + float thickness_half = radius * 0.25; + float circle_outer = softCircle(uv, xy, radius + thickness_half, blur); + float circle_inner = softCircle(uv, xy, radius - thickness_half, blur); return circle_outer - circle_inner; } - float subProgress(float start, float end, float progress) { - float sub = clamp(progress, start, end); - return (sub - start) / (end - start); - } - - float smoothstop2(float t) { - return 1 - (1 - t) * (1 - t); + vec2 distort(vec2 p, vec2 origin, float time, + float distort_amount_radial, float distort_amount_xy) { + float2 distance = origin - p; + float angle = atan(distance.y, distance.x); + return p + vec2(sin(angle * 8 + time * 0.003 + 1.641), + cos(angle * 5 + 2.14 + time * 0.00412)) * distort_amount_radial + + vec2(sin(p.x * 0.01 + time * 0.00215 + 0.8123), + cos(p.y * 0.01 + time * 0.005931)) * distort_amount_xy; }""" private const val SHADER_MAIN = """vec4 main(vec2 p) { - float fadeIn = subProgress(0., 0.1, in_progress); - float fadeOutNoise = subProgress(0.8, 1., in_progress); - float fadeOutRipple = subProgress(0.7, 1., in_progress); - float fadeCircle = subProgress(0., 0.5, in_progress); - float radius = smoothstop2(in_progress) * in_maxRadius; - float sparkleRing = softRing(p, in_origin, radius, 0.5); - float sparkleAlpha = min(fadeIn, 1. - fadeOutNoise); - float sparkle = sparkles(p, in_noisePhase) * sparkleRing * sparkleAlpha; - float circle = softCircle(p, in_origin, radius * 1.2, 0.5) - * (1 - fadeCircle); - float fadeRipple = min(fadeIn, 1.-fadeOutRipple); - float rippleAlpha = softRing(p, in_origin, radius, 0.5) - * fadeRipple * in_color.a; - vec4 ripple = in_color * max(circle, rippleAlpha) * 0.3; + vec2 p_distorted = distort(p, in_origin, in_time, in_distort_radial, + in_distort_xy); + + // Draw shapes + float sparkleRing = softRing(p_distorted, in_origin, in_radius, in_blur); + float sparkle = sparkles(p - mod(p, in_pixelDensity * 0.8), in_time * 0.00175) + * sparkleRing * in_fadeSparkle; + float circle = softCircle(p_distorted, in_origin, in_radius * 1.2, in_blur); + float rippleAlpha = max(circle * in_fadeCircle, + softRing(p_distorted, in_origin, in_radius, in_blur) * in_fadeRing) * 0.45; + vec4 ripple = in_color * rippleAlpha; return mix(ripple, vec4(sparkle), sparkle * in_sparkle_strength); }""" private const val SHADER = SHADER_UNIFORMS + SHADER_LIB + SHADER_MAIN + + private fun subProgress(start: Float, end: Float, progress: Float): Float { + val min = Math.min(start, end) + val max = Math.max(start, end) + val sub = Math.min(Math.max(progress, min), max) + return (sub - start) / (end - start) + } } /** * Maximum radius of the ripple. */ var radius: Float = 0.0f - set(value) { setUniform("in_maxRadius", value) } + set(value) { + field = value + setUniform("in_maxRadius", value) + } /** * Origin coordinate of the ripple. */ var origin: PointF = PointF() - set(value) { setUniform("in_origin", floatArrayOf(value.x, value.y)) } + set(value) { + field = value + setUniform("in_origin", floatArrayOf(value.x, value.y)) + } /** * Progress of the ripple. Float value between [0, 1]. */ var progress: Float = 0.0f - set(value) { setUniform("in_progress", value) } + set(value) { + field = value + setUniform("in_progress", value) + setUniform("in_radius", + (1 - (1 - value) * (1 - value) * (1 - value))* radius) + setUniform("in_blur", MathUtils.lerp(1.25f, 0.5f, value)) + + val fadeIn = subProgress(0f, 0.1f, value) + val fadeOutNoise = subProgress(0.4f, 1f, value) + val fadeOutRipple = subProgress(0.3f, 1f, value) + val fadeCircle = subProgress(0f, 0.2f, value) + setUniform("in_fadeSparkle", Math.min(fadeIn, 1 - fadeOutNoise)) + setUniform("in_fadeCircle", 1 - fadeCircle) + setUniform("in_fadeRing", Math.min(fadeIn, 1 - fadeOutRipple)) + } /** - * Continuous offset used as noise phase. + * Play time since the start of the effect. */ - var noisePhase: Float = 0.0f - set(value) { setUniform("in_noisePhase", value) } + var time: Float = 0.0f + set(value) { + field = value + setUniform("in_time", value) + } /** * A hex value representing the ripple color, in the format of ARGB */ var color: Int = 0xffffff.toInt() set(value) { + field = value val color = Color.valueOf(value) setUniform("in_color", floatArrayOf(color.red(), color.green(), color.blue(), color.alpha())) @@ -143,5 +180,24 @@ class RippleShader internal constructor() : RuntimeShader(SHADER, false) { * it's opaque white and looks the most grainy. */ var sparkleStrength: Float = 0.0f - set(value) { setUniform("in_sparkle_strength", value) } + set(value) { + field = value + setUniform("in_sparkle_strength", value) + } + + /** + * Distortion strength of the ripple. Expected value between[0, 1]. + */ + var distortionStrength: Float = 0.0f + set(value) { + field = value + setUniform("in_distort_radial", 75 * progress * value) + setUniform("in_distort_xy", 75 * value) + } + + var pixelDensity: Float = 1.0f + set(value) { + field = value + setUniform("in_pixelDensity", value) + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java index efd0519d6608..14c73b5cbb4c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java @@ -49,6 +49,7 @@ import com.android.systemui.statusbar.notification.collection.NotifPipeline; import com.android.systemui.statusbar.notification.collection.inflation.LowPriorityInflationHelper; import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy; import com.android.systemui.statusbar.notification.collection.legacy.VisualStabilityManager; +import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection; import com.android.systemui.statusbar.notification.stack.ForegroundServiceSectionController; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.ManagedProfileController; @@ -57,6 +58,7 @@ import com.android.systemui.statusbar.phone.StatusBar; import com.android.systemui.statusbar.phone.StatusBarIconController; import com.android.systemui.statusbar.phone.StatusBarIconControllerImpl; import com.android.systemui.statusbar.phone.StatusBarRemoteInputCallback; +import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController; import com.android.systemui.statusbar.policy.RemoteInputUriController; import com.android.systemui.tracing.ProtoTracer; import com.android.systemui.util.DeviceConfigProxy; @@ -226,4 +228,16 @@ public interface StatusBarDependenciesModule { @Binds StatusBarIconController provideStatusBarIconController( StatusBarIconControllerImpl controllerImpl); + + /** + */ + @Provides + @SysUISingleton + static OngoingCallController provideOngoingCallController( + CommonNotifCollection notifCollection, FeatureFlags featureFlags) { + OngoingCallController ongoingCallController = + new OngoingCallController(notifCollection, featureFlags); + ongoingCallController.init(); + return ongoingCallController; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionListener.java index 75d772e8a1c5..db0c1745f565 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionListener.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionListener.java @@ -57,7 +57,7 @@ public interface NotifCollectionListener { * Called whenever a notification with the same key as an existing notification is posted. By * the time this listener is called, the entry's SBN and Ranking will already have been updated. */ - default void onEntryUpdated(NotificationEntry entry) { + default void onEntryUpdated(@NonNull NotificationEntry entry) { } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java index c3ccba4c3771..a0b0b3dc57bd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java @@ -873,14 +873,19 @@ public class NotificationContentView extends FrameLayout { } public void setBackgroundTintColor(int color) { + boolean colorized = mNotificationEntry.getSbn().getNotification().isColorized(); if (mExpandedSmartReplyView != null) { - boolean colorized = mNotificationEntry.getSbn().getNotification().isColorized(); mExpandedSmartReplyView.setBackgroundTintColor(color, colorized); } if (mHeadsUpSmartReplyView != null) { - boolean colorized = mNotificationEntry.getSbn().getNotification().isColorized(); mHeadsUpSmartReplyView.setBackgroundTintColor(color, colorized); } + if (mExpandedRemoteInput != null) { + mExpandedRemoteInput.setBackgroundTintColor(color, colorized); + } + if (mHeadsUpRemoteInput != null) { + mHeadsUpRemoteInput.setBackgroundTintColor(color, colorized); + } } public int getVisibleType() { @@ -1243,8 +1248,7 @@ public class NotificationContentView extends FrameLayout { View actionContainerCandidate = view.findViewById( com.android.internal.R.id.actions_container); if (actionContainerCandidate instanceof FrameLayout) { - RemoteInputView existing = (RemoteInputView) - view.findViewWithTag(RemoteInputView.VIEW_TAG); + RemoteInputView existing = view.findViewWithTag(RemoteInputView.VIEW_TAG); if (existing != null) { existing.onNotificationUpdateOrReset(); @@ -1292,13 +1296,9 @@ public class NotificationContentView extends FrameLayout { } } if (existing != null) { - if (entry.getSbn().getNotification().isColorized()) { - existing.setBackgroundTintColor( - entry.getSbn().getNotification().color, true); - } else { - existing.setBackgroundTintColor( - entry.getRow().getCurrentBackgroundTint(), false); - } + int backgroundColor = entry.getRow().getCurrentBackgroundTint(); + boolean colorized = mNotificationEntry.getSbn().getNotification().isColorized(); + existing.setBackgroundTintColor(backgroundColor, colorized); } return existing; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java index b4ab8cf817dd..8ba036ce03c3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java @@ -346,9 +346,22 @@ public class NotificationConversationInfo extends LinearLayout implements } private void bindIcon(boolean important) { + Drawable person = mIconFactory.getBaseIconDrawable(mShortcutInfo); + if (person == null) { + person = mContext.getDrawable(R.drawable.ic_person).mutate(); + TypedArray ta = mContext.obtainStyledAttributes(new int[]{android.R.attr.colorAccent}); + int colorAccent = ta.getColor(0, 0); + ta.recycle(); + person.setTint(colorAccent); + } ImageView image = findViewById(R.id.conversation_icon); - image.setImageDrawable(mIconFactory.getConversationDrawable( - mShortcutInfo, mPackageName, mAppUid, important)); + image.setImageDrawable(person); + + ImageView app = findViewById(R.id.conversation_icon_badge_icon); + app.setImageDrawable(mIconFactory.getAppBadge( + mPackageName, UserHandle.getUserId(mSbn.getUid()))); + + findViewById(R.id.conversation_icon_badge_ring).setVisibility(important ? VISIBLE : GONE); } private void bindPackage() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java index 1d307364d661..5f3933b827c4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java @@ -105,7 +105,6 @@ public class NotificationChildrenContainer extends ViewGroup { private ViewGroup mCurrentHeader; private boolean mIsConversation; - private boolean mTintWithThemeAccent; private boolean mShowGroupCountInExpander; private boolean mShowDividersWhenExpanded; private boolean mHideDividersDuringExpand; @@ -149,8 +148,6 @@ public class NotificationChildrenContainer extends ViewGroup { com.android.internal.R.dimen.notification_content_margin); mEnableShadowOnChildNotifications = res.getBoolean(R.bool.config_enableShadowOnChildNotifications); - mTintWithThemeAccent = - res.getBoolean(com.android.internal.R.bool.config_tintNotificationsWithTheme); mShowGroupCountInExpander = res.getBoolean(R.bool.config_showNotificationGroupCountInExpander); mShowDividersWhenExpanded = @@ -1223,14 +1220,11 @@ public class NotificationChildrenContainer extends ViewGroup { return; } int color = mContainingNotification.getNotificationColor(); - if (mTintWithThemeAccent) { - // We're using the theme accent, color with the accent color instead of the notif color - Resources.Theme theme = new ContextThemeWrapper(mContext, - com.android.internal.R.style.Theme_DeviceDefault_DayNight).getTheme(); - TypedArray ta = theme.obtainStyledAttributes( - new int[]{com.android.internal.R.attr.colorAccent}); + Resources.Theme theme = new ContextThemeWrapper(mContext, + com.android.internal.R.style.Theme_DeviceDefault_DayNight).getTheme(); + try (TypedArray ta = theme.obtainStyledAttributes( + new int[]{com.android.internal.R.attr.colorAccent})) { color = ta.getColor(0, color); - ta.recycle(); } mHybridGroupManager.setOverflowNumberColor(mOverflowNumber, color); } 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 f6c1b1c50fee..40c0b895400e 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 @@ -1245,7 +1245,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } if (mAmbientState.isHiddenAtAll()) { - clipToOutline = true; + clipToOutline = false; invalidateOutline(); if (isFullyHidden()) { setClipBounds(null); 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 d7a98bdf2715..bbdbe809a8d8 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 @@ -320,6 +320,7 @@ public class StackScrollAlgorithm { } int childHeight = getMaxAllowedChildHeight(child); childViewState.yTranslation = currentYPosition; + childViewState.alpha = 1f - ambientState.getHideAmount(); boolean isFooterView = child instanceof FooterView; boolean isEmptyShadeView = child instanceof EmptyShadeView; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java index 204dd9f5e58c..88e5364cd55f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java @@ -36,6 +36,7 @@ import com.android.systemui.statusbar.policy.CastController; import com.android.systemui.statusbar.policy.CastController.CastDevice; import com.android.systemui.statusbar.policy.DataSaverController; import com.android.systemui.statusbar.policy.DataSaverController.Listener; +import com.android.systemui.statusbar.policy.DeviceControlsController; import com.android.systemui.statusbar.policy.HotspotController; import com.android.systemui.statusbar.policy.HotspotController.Callback; import com.android.systemui.util.UserAwareController; @@ -58,6 +59,7 @@ public class AutoTileManager implements UserAwareController { public static final String WORK = "work"; public static final String NIGHT = "night"; public static final String CAST = "cast"; + public static final String DEVICE_CONTROLS = "controls"; public static final String BRIGHTNESS = "reduce_brightness"; static final String SETTING_SEPARATOR = ":"; @@ -74,6 +76,7 @@ public class AutoTileManager implements UserAwareController { private final ManagedProfileController mManagedProfileController; private final NightDisplayListener mNightDisplayListener; private final CastController mCastController; + private final DeviceControlsController mDeviceControlsController; private final ReduceBrightColorsController mReduceBrightColorsController; private final boolean mIsReduceBrightColorsAvailable; private final ArrayList<AutoAddSetting> mAutoAddSettingList = new ArrayList<>(); @@ -88,6 +91,7 @@ public class AutoTileManager implements UserAwareController { NightDisplayListener nightDisplayListener, CastController castController, ReduceBrightColorsController reduceBrightColorsController, + DeviceControlsController deviceControlsController, @Named(RBC_AVAILABLE) boolean isReduceBrightColorsAvailable) { mContext = context; mHost = host; @@ -102,6 +106,7 @@ public class AutoTileManager implements UserAwareController { mCastController = castController; mReduceBrightColorsController = reduceBrightColorsController; mIsReduceBrightColorsAvailable = isReduceBrightColorsAvailable; + mDeviceControlsController = deviceControlsController; } /** @@ -138,6 +143,9 @@ public class AutoTileManager implements UserAwareController { if (!mAutoTracker.isAdded(BRIGHTNESS) && mIsReduceBrightColorsAvailable) { mReduceBrightColorsController.addCallback(mReduceBrightColorsCallback); } + if (!mAutoTracker.isAdded(DEVICE_CONTROLS)) { + mDeviceControlsController.setCallback(mDeviceControlsCallback); + } int settingsN = mAutoAddSettingList.size(); for (int i = 0; i < settingsN; i++) { @@ -158,6 +166,7 @@ public class AutoTileManager implements UserAwareController { mReduceBrightColorsController.removeCallback(mReduceBrightColorsCallback); } mCastController.removeCallback(mCastCallback); + mDeviceControlsController.removeCallback(); int settingsN = mAutoAddSettingList.size(); for (int i = 0; i < settingsN; i++) { mAutoAddSettingList.get(i).setListening(false); @@ -274,6 +283,17 @@ public class AutoTileManager implements UserAwareController { } }; + private final DeviceControlsController.Callback mDeviceControlsCallback = + new DeviceControlsController.Callback() { + @Override + public void onControlsAvailable(int position) { + if (mAutoTracker.isAdded(DEVICE_CONTROLS)) return; + mHost.addTile(DEVICE_CONTROLS, position); + mAutoTracker.setTileAdded(DEVICE_CONTROLS); + mHandler.post(() -> mDeviceControlsController.removeCallback()); + } + }; + @VisibleForTesting final NightDisplayListener.Callback mNightDisplayCallback = new NightDisplayListener.Callback() { 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 562d0ec06a63..f64a0e03698a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java @@ -37,6 +37,7 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.phone.StatusBarIconController.DarkIconManager; +import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController; import com.android.systemui.statusbar.policy.EncryptionHelper; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.NetworkController; @@ -45,6 +46,8 @@ import com.android.systemui.statusbar.policy.NetworkController.SignalCallback; import java.util.ArrayList; import java.util.List; +import javax.inject.Inject; + /** * Contains the collapsed status bar and handles hiding/showing based on disable flags * and keyguard state. Also manages lifecycle to make sure the views it contains are being @@ -72,6 +75,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue private DarkIconManager mDarkIconManager; private View mOperatorNameFrame; private CommandQueue mCommandQueue; + private OngoingCallController mOngoingCallController; private List<String> mBlockedIcons = new ArrayList<>(); @@ -82,6 +86,11 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue } }; + @Inject + public CollapsedStatusBarFragment(OngoingCallController ongoingCallController) { + mOngoingCallController = ongoingCallController; + } + @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -110,7 +119,6 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue 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)); - mBlockedIcons.add(getString(com.android.internal.R.string.status_bar_call_strength)); mDarkIconManager.setBlockList(mBlockedIcons); Dependency.get(StatusBarIconController.class).addIconGroup(mDarkIconManager); mSystemIconArea = mStatusBar.findViewById(R.id.system_icon_area); 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 c23f1ad6f9c9..86ef0a727831 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java @@ -24,15 +24,20 @@ import android.os.UserHandle; import android.provider.Settings; import android.util.MathUtils; +import androidx.annotation.NonNull; + +import com.android.systemui.Dumpable; import com.android.systemui.R; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.doze.AlwaysOnDisplayPolicy; import com.android.systemui.doze.DozeScreenState; +import com.android.systemui.dump.DumpManager; import com.android.systemui.statusbar.FeatureFlags; import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.tuner.TunerService; +import java.io.FileDescriptor; import java.io.PrintWriter; import javax.inject.Inject; @@ -42,7 +47,7 @@ import javax.inject.Inject; */ @SysUISingleton public class DozeParameters implements TunerService.Tunable, - com.android.systemui.plugins.statusbar.DozeParameters { + com.android.systemui.plugins.statusbar.DozeParameters, Dumpable { private static final int MAX_DURATION = 60 * 1000; public static final boolean FORCE_NO_BLANKING = SystemProperties.getBoolean("debug.force_no_blanking", false); @@ -68,11 +73,13 @@ public class DozeParameters implements TunerService.Tunable, PowerManager powerManager, BatteryController batteryController, TunerService tunerService, + DumpManager dumpManager, FeatureFlags featureFlags) { mResources = resources; mAmbientDisplayConfiguration = ambientDisplayConfiguration; mAlwaysOnPolicy = alwaysOnDisplayPolicy; mBatteryController = batteryController; + dumpManager.registerDumpable("DozeParameters", this); mControlScreenOffAnimation = !getDisplayNeedsBlanking(); mPowerManager = powerManager; @@ -85,20 +92,6 @@ public class DozeParameters implements TunerService.Tunable, Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED); } - public void dump(PrintWriter pw) { - pw.println(" DozeParameters:"); - pw.print(" getDisplayStateSupported(): "); pw.println(getDisplayStateSupported()); - pw.print(" getPulseDuration(): "); pw.println(getPulseDuration()); - pw.print(" getPulseInDuration(): "); pw.println(getPulseInDuration()); - pw.print(" getPulseInVisibleDuration(): "); pw.println(getPulseVisibleDuration()); - pw.print(" getPulseOutDuration(): "); pw.println(getPulseOutDuration()); - pw.print(" getPulseOnSigMotion(): "); pw.println(getPulseOnSigMotion()); - pw.print(" getVibrateOnSigMotion(): "); pw.println(getVibrateOnSigMotion()); - pw.print(" getVibrateOnPickup(): "); pw.println(getVibrateOnPickup()); - pw.print(" getProxCheckBeforePulse(): "); pw.println(getProxCheckBeforePulse()); - pw.print(" getPickupVibrationThreshold(): "); pw.println(getPickupVibrationThreshold()); - } - public boolean getDisplayStateSupported() { return getBoolean("doze.display.supported", R.bool.doze_display_state_supported); } @@ -144,6 +137,16 @@ public class DozeParameters implements TunerService.Tunable, return getBoolean("doze.pulse.proxcheck", R.bool.doze_proximity_check_before_pulse); } + /** + * @return true if we should only register for sensors that use the proximity sensor when the + * display state is {@link android.view.Display.STATE_OFF}, + * {@link android.view.Display.STATE_DOZE} or {@link android.view.Display.STATE_DOZE_SUSPEND} + */ + public boolean getSelectivelyRegisterSensorsUsingProx() { + return getBoolean("doze.prox.selectively_register", + R.bool.doze_selectively_register_prox); + } + public int getPickupVibrationThreshold() { return getInt("doze.pickup.vibration.threshold", R.integer.doze_pickup_vibration_threshold); } @@ -233,8 +236,38 @@ public class DozeParameters implements TunerService.Tunable, return mResources.getBoolean(R.bool.doze_double_tap_reports_touch_coordinates); } + /** + * Whether the single tap sensor uses the proximity sensor. + */ + public boolean singleTapUsesProx() { + return mResources.getBoolean(R.bool.doze_single_tap_uses_prox); + } + + /** + * Whether the long press sensor uses the proximity sensor. + */ + public boolean longPressUsesProx() { + return mResources.getBoolean(R.bool.doze_long_press_uses_prox); + } + @Override public void onTuningChanged(String key, String newValue) { mDozeAlwaysOn = mAmbientDisplayConfiguration.alwaysOnEnabled(UserHandle.USER_CURRENT); } + + @Override + public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) { + pw.print("getDisplayStateSupported(): "); pw.println(getDisplayStateSupported()); + pw.print("getPulseDuration(): "); pw.println(getPulseDuration()); + pw.print("getPulseInDuration(): "); pw.println(getPulseInDuration()); + pw.print("getPulseInVisibleDuration(): "); pw.println(getPulseVisibleDuration()); + pw.print("getPulseOutDuration(): "); pw.println(getPulseOutDuration()); + pw.print("getPulseOnSigMotion(): "); pw.println(getPulseOnSigMotion()); + pw.print("getVibrateOnSigMotion(): "); pw.println(getVibrateOnSigMotion()); + pw.print("getVibrateOnPickup(): "); pw.println(getVibrateOnPickup()); + pw.print("getProxCheckBeforePulse(): "); pw.println(getProxCheckBeforePulse()); + pw.print("getPickupVibrationThreshold(): "); pw.println(getPickupVibrationThreshold()); + pw.print("getSelectivelyRegisterSensorsUsingProx(): "); + pw.println(getSelectivelyRegisterSensorsUsingProx()); + } } 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 6b5a908fc09b..4b72b7f2f78a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java @@ -74,6 +74,7 @@ import com.android.systemui.Dependency; import com.android.systemui.Interpolators; import com.android.systemui.R; import com.android.systemui.assist.AssistManager; +import com.android.systemui.camera.CameraIntents; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.IntentButtonProvider; import com.android.systemui.plugins.IntentButtonProvider.IntentButton; @@ -111,11 +112,6 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL private static final String RIGHT_BUTTON_PLUGIN = "com.android.systemui.action.PLUGIN_LOCKSCREEN_RIGHT_BUTTON"; - private static final Intent SECURE_CAMERA_INTENT = - new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE) - .addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); - public static final Intent INSECURE_CAMERA_INTENT = - new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA); private static final Intent PHONE_INTENT = new Intent(Intent.ACTION_DIAL); private static final int DOZE_ANIMATION_STAGGER_DELAY = 48; private static final int DOZE_ANIMATION_ELEMENT_DURATION = 250; @@ -502,7 +498,7 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL intent.putExtra(EXTRA_CAMERA_LAUNCH_SOURCE, source); boolean wouldLaunchResolverActivity = mActivityIntentHelper.wouldLaunchResolverActivity( intent, KeyguardUpdateMonitor.getCurrentUser()); - if (intent == SECURE_CAMERA_INTENT && !wouldLaunchResolverActivity) { + if (CameraIntents.isSecureCameraIntent(intent) && !wouldLaunchResolverActivity) { AsyncTask.execute(new Runnable() { @Override public void run() { @@ -862,7 +858,11 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL public Intent getIntent() { boolean canDismissLs = mKeyguardStateController.canDismissLockScreen(); boolean secure = mKeyguardStateController.isMethodSecure(); - return (secure && !canDismissLs) ? SECURE_CAMERA_INTENT : INSECURE_CAMERA_INTENT; + if (secure && !canDismissLs) { + return CameraIntents.getSecureCameraIntent(getContext()); + } else { + return CameraIntents.getInsecureCameraIntent(getContext()); + } } } 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 6b69103f6030..5ff9b703924e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java @@ -555,6 +555,13 @@ public class KeyguardBouncer { pw.println(" mIsAnimatingAway: " + mIsAnimatingAway); } + /** Update keyguard position based on a tapped X coordinate. */ + public void updateKeyguardPosition(float x) { + if (mKeyguardViewController != null) { + mKeyguardViewController.updateKeyguardPosition(x); + } + } + public interface BouncerExpansionCallback { void onFullyShown(); void onStartingToHide(); 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 242bd0a29d2f..707135c3d95b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt @@ -124,7 +124,7 @@ open class KeyguardBypassController : Dumpable { biometricSourceType: BiometricSourceType, isStrongBiometric: Boolean ): Boolean { - if (bypassEnabled) { + if (biometricSourceType == BiometricSourceType.FACE && bypassEnabled) { val can = canBypass() if (!can && (isPulseExpanding || qSExpanded)) { pendingUnlock = PendingUnlock(biometricSourceType, isStrongBiometric) 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 2d760e6fc176..3f8e9d372d82 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java @@ -189,7 +189,6 @@ public class KeyguardStatusBarView extends RelativeLayout Resources r = getResources(); mBlockedIcons.add(r.getString(com.android.internal.R.string.status_bar_volume)); mBlockedIcons.add(r.getString(com.android.internal.R.string.status_bar_alarm_clock)); - mBlockedIcons.add(r.getString(com.android.internal.R.string.status_bar_call_strength)); } private void updateVisibilities() { 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 d3da0bce0a15..555df5c37ee3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java @@ -2625,8 +2625,7 @@ public class NotificationPanelViewController extends PanelViewController { return; } mSectionPadding = padding; - mQsFrame.setTranslationY(padding); - mNotificationStackScrollLayoutController.setSectionPadding(padding); + // TODO(b/172289889) update overscroll to spec } @Override @@ -3495,6 +3494,12 @@ public class NotificationPanelViewController extends PanelViewController { updateHorizontalPanelPosition(event.getX()); handled = true; } + + if (event.getActionMasked() == MotionEvent.ACTION_DOWN && isFullyExpanded() + && mStatusBarKeyguardViewManager.isShowing()) { + mStatusBarKeyguardViewManager.updateKeyguardPosition(event.getX()); + } + handled |= super.onTouch(v, event); return !mDozing || mPulsing || handled || showingOrAnimatingAltAuth; } @@ -3538,7 +3543,8 @@ public class NotificationPanelViewController extends PanelViewController { mStatusBarStateController, mUpdateMonitor, mAuthController, - mStatusBarKeyguardViewManager); + mStatusBarKeyguardViewManager, + mKeyguardStateController); mDisabledUdfpsController.init(); } else if (mDisabledUdfpsController != null && !udfpsEnrolled) { mDisabledUdfpsController.destroy(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowView.java index af595b60daa1..72f169564c10 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowView.java @@ -49,7 +49,6 @@ import android.view.WindowInsets; import android.view.WindowInsetsController; import android.widget.FrameLayout; -import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.view.FloatingActionMode; import com.android.internal.widget.FloatingToolbar; import com.android.systemui.R; @@ -174,7 +173,11 @@ public class NotificationShadeWindowView extends FrameLayout { public boolean dispatchTouchEvent(MotionEvent ev) { Boolean result = mInteractionEventHandler.handleDispatchTouchEvent(ev); - return result != null ? result : super.dispatchTouchEvent(ev); + result = result != null ? result : super.dispatchTouchEvent(ev); + + mInteractionEventHandler.dispatchTouchEventComplete(); + + return result; } @Override @@ -346,6 +349,12 @@ public class NotificationShadeWindowView extends FrameLayout { Boolean handleDispatchTouchEvent(MotionEvent ev); /** + * Called after all dispatching is done. + */ + + void dispatchTouchEventComplete(); + + /** * Returns if the view should intercept the touch event. * * The touch event may still be interecepted if 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 5595ae7ed820..2ff7c9933edf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java @@ -292,6 +292,11 @@ public class NotificationShadeWindowViewController { } @Override + public void dispatchTouchEventComplete() { + mFalsingCollector.onMotionEventComplete(); + } + + @Override public boolean shouldInterceptTouchEvent(MotionEvent ev) { if (mStatusBarStateController.isDozing() && !mService.isPulsing() && !mDockManager.isDocked()) { 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 4d42f4537b57..380000ee9b4e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java @@ -150,6 +150,7 @@ import com.android.systemui.SystemUI; import com.android.systemui.accessibility.floatingmenu.AccessibilityFloatingMenuController; import com.android.systemui.assist.AssistManager; import com.android.systemui.broadcast.BroadcastDispatcher; +import com.android.systemui.camera.CameraIntents; import com.android.systemui.charging.WirelessChargingAnimation; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.colorextraction.SysuiColorExtractor; @@ -224,6 +225,7 @@ import com.android.systemui.statusbar.notification.stack.NotificationStackScroll import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController; import com.android.systemui.statusbar.phone.dagger.StatusBarComponent; import com.android.systemui.statusbar.phone.dagger.StatusBarPhoneModule; +import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController; import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.BrightnessMirrorController; import com.android.systemui.statusbar.policy.ConfigurationController; @@ -237,6 +239,7 @@ import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener; import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler; import com.android.systemui.statusbar.policy.UserInfoControllerImpl; import com.android.systemui.statusbar.policy.UserSwitcherController; +import com.android.systemui.tuner.TunerService; import com.android.systemui.volume.VolumeComponent; import com.android.systemui.wmshell.BubblesManager; import com.android.wm.shell.bubbles.Bubbles; @@ -423,6 +426,7 @@ public class StatusBar extends SystemUI implements DemoMode, private final DismissCallbackRegistry mDismissCallbackRegistry; private final DemoModeController mDemoModeController; private NotificationsController mNotificationsController; + private final OngoingCallController mOngoingCallController; // expanded notifications // the sliding/resizing panel within the notification window @@ -787,6 +791,8 @@ public class StatusBar extends SystemUI implements DemoMode, NotificationIconAreaController notificationIconAreaController, BrightnessSlider.Factory brightnessSliderFactory, WiredChargingRippleController chargingRippleAnimationController, + OngoingCallController ongoingCallController, + TunerService tunerService, FeatureFlags featureFlags) { super(context); mNotificationsController = notificationsController; @@ -866,8 +872,18 @@ public class StatusBar extends SystemUI implements DemoMode, mNotificationIconAreaController = notificationIconAreaController; mBrightnessSliderFactory = brightnessSliderFactory; mChargingRippleAnimationController = chargingRippleAnimationController; + mOngoingCallController = ongoingCallController; mFeatureFlags = featureFlags; + tunerService.addTunable( + (key, newValue) -> { + if (key.equals(Settings.Secure.DOZE_ALWAYS_ON)) { + updateLightRevealScrimVisibility(); + } + }, + Settings.Secure.DOZE_ALWAYS_ON + ); + mExpansionChangedListeners = new ArrayList<>(); mBubbleExpandListener = @@ -1152,7 +1168,8 @@ public class StatusBar extends SystemUI implements DemoMode, checkBarModes(); }).getFragmentManager() .beginTransaction() - .replace(R.id.status_bar_container, new CollapsedStatusBarFragment(), + .replace(R.id.status_bar_container, + new CollapsedStatusBarFragment(mOngoingCallController), CollapsedStatusBarFragment.TAG) .commit(); @@ -1210,15 +1227,7 @@ public class StatusBar extends SystemUI implements DemoMode, mLightRevealScrim = mNotificationShadeWindowView.findViewById(R.id.light_reveal_scrim); mChargingRippleAnimationController.setViewHost(mNotificationShadeWindowView); - - - if (mFeatureFlags.useNewLockscreenAnimations() - && (mDozeParameters.getAlwaysOn() || mDozeParameters.isQuickPickupEnabled())) { - mLightRevealScrim.setVisibility(View.VISIBLE); - mLightRevealScrim.setRevealEffect(LiftReveal.INSTANCE); - } else { - mLightRevealScrim.setVisibility(View.GONE); - } + updateLightRevealScrimVisibility(); mNotificationPanelViewController.initDependencies( this, @@ -2607,7 +2616,8 @@ public class StatusBar extends SystemUI implements DemoMode, " (auto: " + UiModeManager.MODE_NIGHT_AUTO + ", yes: " + UiModeManager.MODE_NIGHT_YES + ", no: " + UiModeManager.MODE_NIGHT_NO + ")"); - final boolean lightWpTheme = mContext.getThemeResId() == R.style.Theme_SystemUI_Light; + final boolean lightWpTheme = mContext.getThemeResId() + == R.style.Theme_SystemUI_LightWallpaper; pw.println(" light wallpaper theme: " + lightWpTheme); if (mKeyguardIndicationController != null) { @@ -2662,6 +2672,12 @@ public class StatusBar extends SystemUI implements DemoMode, for (Map.Entry<String, ?> entry : Prefs.getAll(mContext).entrySet()) { pw.print(" "); pw.print(entry.getKey()); pw.print("="); pw.println(entry.getValue()); } + + pw.println("Camera gesture intents:"); + pw.println(" Insecure camera: " + CameraIntents.getInsecureCameraIntent(mContext)); + pw.println(" Secure camera: " + CameraIntents.getSecureCameraIntent(mContext)); + pw.println(" Override package: " + + String.valueOf(CameraIntents.getOverrideCameraPackage(mContext))); } public static void dumpBarTransitions(PrintWriter pw, String var, BarTransitions transitions) { @@ -2734,7 +2750,7 @@ public class StatusBar extends SystemUI implements DemoMode, null /* remoteAnimation */)); options.setDisallowEnterPictureInPictureWhileLaunching( disallowEnterPictureInPictureWhileLaunching); - if (intent == KeyguardBottomAreaView.INSECURE_CAMERA_INTENT) { + if (CameraIntents.isInsecureCameraIntent(intent)) { // Normally an activity will set it's requested rotation // animation on its window. However when launching an activity // causes the orientation to change this is too late. In these cases @@ -2964,7 +2980,7 @@ public class StatusBar extends SystemUI implements DemoMode, } mPowerButtonReveal = new PowerButtonReveal(mContext.getResources().getDimensionPixelSize( - R.dimen.global_actions_top_padding)); + com.android.systemui.R.dimen.physical_power_button_center_screen_location_y)); } // Visibility reporting @@ -3482,7 +3498,8 @@ public class StatusBar extends SystemUI implements DemoMode, // Lock wallpaper defines the color of the majority of the views, hence we'll use it // to set our default theme. final boolean lockDarkText = mColorExtractor.getNeutralColors().supportsDarkText(); - final int themeResId = lockDarkText ? R.style.Theme_SystemUI_Light : R.style.Theme_SystemUI; + final int themeResId = lockDarkText ? R.style.Theme_SystemUI_LightWallpaper + : R.style.Theme_SystemUI; if (mContext.getThemeResId() != themeResId) { mContext.setTheme(themeResId); mConfigurationController.notifyThemeChanged(); @@ -4024,7 +4041,8 @@ public class StatusBar extends SystemUI implements DemoMode, } if (!mStatusBarKeyguardViewManager.isShowing()) { - startActivityDismissingKeyguard(KeyguardBottomAreaView.INSECURE_CAMERA_INTENT, + final Intent cameraIntent = CameraIntents.getInsecureCameraIntent(mContext); + startActivityDismissingKeyguard(cameraIntent, false /* onlyProvisioned */, true /* dismissShade */, true /* disallowEnterPictureInPictureWhileLaunching */, null /* callback */, 0); } else { @@ -4640,4 +4658,19 @@ public class StatusBar extends SystemUI implements DemoMode, public void removeExpansionChangedListener(@NonNull ExpansionChangedListener listener) { mExpansionChangedListeners.remove(listener); } + + private void updateLightRevealScrimVisibility() { + if (mLightRevealScrim == null) { + // status bar may not be inflated yet + return; + } + + if (mFeatureFlags.useNewLockscreenAnimations() + && (mDozeParameters.getAlwaysOn() || mDozeParameters.isQuickPickupEnabled())) { + mLightRevealScrim.setVisibility(View.VISIBLE); + mLightRevealScrim.setRevealEffect(LiftReveal.INSTANCE); + } else { + mLightRevealScrim.setVisibility(View.GONE); + } + } } 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..89e701606c1a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java @@ -74,10 +74,6 @@ public interface StatusBarIconController { /** * Display the no calling & SMS icons. */ - void setCallStrengthIcons(String slot, List<CallIndicatorIconState> states); - /** - * Display the no calling & SMS icons. - */ void setNoCallingIcons(String slot, List<CallIndicatorIconState> states); public void setIconVisibility(String slot, boolean b); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java index 75900a2bffa1..068ded32bbdb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java @@ -224,33 +224,6 @@ public class StatusBarIconControllerImpl extends StatusBarIconList implements Tu } /** - * Accept a list of CallIndicatorIconStates, and show the call strength icons. - * @param slot StatusBar slot for the call strength icons - * @param states All of the no Calling & SMS icon states - */ - @Override - public void setCallStrengthIcons(String slot, List<CallIndicatorIconState> states) { - Slot callStrengthSlot = getSlot(slot); - int callStrengthSlotIndex = getSlotIndex(slot); - Collections.reverse(states); - for (CallIndicatorIconState state : states) { - if (!state.isNoCalling) { - StatusBarIconHolder holder = callStrengthSlot.getHolderForTag(state.subId); - if (holder == null) { - holder = StatusBarIconHolder.fromCallIndicatorState(mContext, state); - setIcon(callStrengthSlotIndex, holder); - } else { - holder.setIcon(new StatusBarIcon(UserHandle.SYSTEM, mContext.getPackageName(), - Icon.createWithResource(mContext, state.callStrengthResId), 0, 0, - state.callStrengthDescription)); - setIcon(callStrengthSlotIndex, holder); - } - } - setIconVisibility(slot, !state.isNoCalling, state.subId); - } - } - - /** * Accept a list of CallIndicatorIconStates, and show the no calling icons. * @param slot StatusBar slot for the no calling icons * @param states All of the no Calling & SMS icon states diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java index af342dd31a76..4ca71f081c0e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java @@ -86,11 +86,9 @@ public class StatusBarIconHolder { Context context, CallIndicatorIconState state) { StatusBarIconHolder holder = new StatusBarIconHolder(); - int resId = state.isNoCalling ? state.noCallingResId : state.callStrengthResId; - String contentDescription = state.isNoCalling - ? state.noCallingDescription : state.callStrengthDescription; holder.mIcon = new StatusBarIcon(UserHandle.SYSTEM, context.getPackageName(), - Icon.createWithResource(context, resId), 0, 0, contentDescription); + Icon.createWithResource(context, state.noCallingResId), + 0, 0, state.noCallingDescription); holder.mTag = state.subId; return holder; } 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 7ee7aa4100d4..ef2444eba814 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -479,7 +479,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb if (mAlternateAuthInterceptor != null) { mAfterKeyguardGoneAction = r; mKeyguardGoneCancelAction = cancelAction; - if (mAlternateAuthInterceptor.showAlternativeAuthMethod()) { + if (mAlternateAuthInterceptor.showAlternateAuthBouncer()) { mStatusBar.updateScrimController(); } return; @@ -529,7 +529,8 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb * Stop showing any alternate auth methods */ public void resetAlternateAuth() { - if (mAlternateAuthInterceptor != null && mAlternateAuthInterceptor.resetForceShow()) { + if (mAlternateAuthInterceptor != null + && mAlternateAuthInterceptor.hideAlternateAuthBouncer()) { mStatusBar.updateScrimController(); } } @@ -1141,15 +1142,22 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb public boolean isShowingAlternateAuth() { return mAlternateAuthInterceptor != null - && mAlternateAuthInterceptor.isShowingAlternateAuth(); + && mAlternateAuthInterceptor.isShowingAlternateAuthBouncer(); } public boolean isShowingAlternateAuthOrAnimating() { return mAlternateAuthInterceptor != null - && (mAlternateAuthInterceptor.isShowingAlternateAuth() + && (mAlternateAuthInterceptor.isShowingAlternateAuthBouncer() || mAlternateAuthInterceptor.isAnimating()); } + /** Update keyguard position based on a tapped X coordinate. */ + public void updateKeyguardPosition(float x) { + if (mBouncer != null) { + mBouncer.updateKeyguardPosition(x); + } + } + private static class DismissWithActionRequest { final OnDismissAction dismissAction; final Runnable cancelAction; @@ -1167,24 +1175,25 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb /** * Delegate used to send show/reset events to an alternate authentication method instead of the - * bouncer. + * regular pin/pattern/password bouncer. */ public interface AlternateAuthInterceptor { /** - * @return whether alternative auth method was newly shown + * Show alternate authentication bouncer. + * @return whether alternate auth method was newly shown */ - boolean showAlternativeAuthMethod(); + boolean showAlternateAuthBouncer(); /** - * reset the state to the default (only keyguard showing, no auth methods showing) - * @return whether alternative auth method was newly hidden + * Hide alternate authentication bouncer + * @return whether the alternate auth method was newly hidden */ - boolean resetForceShow(); + boolean hideAlternateAuthBouncer(); /** - * @return true if alternative auth method is showing + * @return true if the alternate auth bouncer is showing */ - boolean isShowingAlternateAuth(); + boolean isShowingAlternateAuthBouncer(); /** * print information for the alternate auth interceptor registered @@ -1192,12 +1201,12 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb void dump(PrintWriter pw); /** - * @return true if the new auth method is currently animating in or out. + * @return true if the new auth method bouncer is currently animating in or out. */ boolean isAnimating(); /** - * Set whether qs is currently expanded + * Set whether qs is currently expanded. */ void setQsExpanded(boolean expanded); } 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 52b32a2dee9b..6ec7c4a8f94b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarSignalPolicy.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarSignalPolicy.java @@ -22,7 +22,6 @@ import android.telephony.SubscriptionInfo; import android.util.ArraySet; import android.util.Log; -import com.android.settingslib.mobile.TelephonyIcons; import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.statusbar.policy.NetworkController; @@ -50,7 +49,6 @@ public class StatusBarSignalPolicy implements NetworkControllerImpl.SignalCallba private final String mSlotEthernet; private final String mSlotVpn; private final String mSlotNoCalling; - private final String mSlotCallStrength; private final Context mContext; private final StatusBarIconController mIconController; @@ -84,8 +82,6 @@ public class StatusBarSignalPolicy implements NetworkControllerImpl.SignalCallba mSlotEthernet = mContext.getString(com.android.internal.R.string.status_bar_ethernet); mSlotVpn = mContext.getString(com.android.internal.R.string.status_bar_vpn); mSlotNoCalling = mContext.getString(com.android.internal.R.string.status_bar_no_calling); - mSlotCallStrength = - mContext.getString(com.android.internal.R.string.status_bar_call_strength); mActivityEnabled = mContext.getResources().getBoolean(R.bool.config_showActivity); mIconController = iconController; @@ -211,14 +207,9 @@ public class StatusBarSignalPolicy implements NetworkControllerImpl.SignalCallba if (statusIcon.icon == R.drawable.ic_qs_no_calling_sms) { state.isNoCalling = statusIcon.visible; state.noCallingDescription = statusIcon.contentDescription; - } else { - state.callStrengthResId = statusIcon.icon; - state.callStrengthDescription = statusIcon.contentDescription; + mIconController.setNoCallingIcons(mSlotNoCalling, + CallIndicatorIconState.copyStates(mCallIndicatorStates)); } - mIconController.setCallStrengthIcons(mSlotCallStrength, - CallIndicatorIconState.copyStates(mCallIndicatorStates)); - mIconController.setNoCallingIcons(mSlotNoCalling, - CallIndicatorIconState.copyStates(mCallIndicatorStates)); } @Override @@ -306,7 +297,6 @@ public class StatusBarSignalPolicy implements NetworkControllerImpl.SignalCallba mIconController.removeAllIconsForSlot(mSlotMobile); mIconController.removeAllIconsForSlot(mSlotNoCalling); - mIconController.removeAllIconsForSlot(mSlotCallStrength); mMobileStates.clear(); List<CallIndicatorIconState> noCallingStates = new ArrayList<CallIndicatorIconState>(); noCallingStates.addAll(mCallIndicatorStates); @@ -421,15 +411,12 @@ public class StatusBarSignalPolicy implements NetworkControllerImpl.SignalCallba public static class CallIndicatorIconState { public boolean isNoCalling; public int noCallingResId; - public int callStrengthResId; public int subId; public String noCallingDescription; - public String callStrengthDescription; private CallIndicatorIconState(int subId) { this.subId = subId; this.noCallingResId = R.drawable.ic_qs_no_calling_sms; - this.callStrengthResId = TelephonyIcons.MOBILE_CALL_STRENGTH_ICONS[0]; } @Override @@ -441,26 +428,21 @@ public class StatusBarSignalPolicy implements NetworkControllerImpl.SignalCallba CallIndicatorIconState that = (CallIndicatorIconState) o; return isNoCalling == that.isNoCalling && noCallingResId == that.noCallingResId - && callStrengthResId == that.callStrengthResId && subId == that.subId - && noCallingDescription == that.noCallingDescription - && callStrengthDescription == that.callStrengthDescription; + && noCallingDescription == that.noCallingDescription; } @Override public int hashCode() { - return Objects.hash(isNoCalling, noCallingResId, - callStrengthResId, subId, noCallingDescription, callStrengthDescription); + return Objects.hash(isNoCalling, noCallingResId, subId, noCallingDescription); } private void copyTo(CallIndicatorIconState other) { other.isNoCalling = isNoCalling; other.noCallingResId = noCallingResId; - other.callStrengthResId = callStrengthResId; other.subId = subId; other.noCallingDescription = noCallingDescription; - other.callStrengthDescription = callStrengthDescription; } private static List<CallIndicatorIconState> copyStates( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java index 4f32712e81fa..2c2779e53e16 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java @@ -90,6 +90,7 @@ import com.android.systemui.statusbar.phone.StatusBarIconController; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.statusbar.phone.StatusBarNotificationActivityStarter; import com.android.systemui.statusbar.phone.StatusBarTouchableRegionManager; +import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController; import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.DeviceProvisionedController; @@ -99,6 +100,7 @@ import com.android.systemui.statusbar.policy.NetworkController; import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler; import com.android.systemui.statusbar.policy.UserInfoControllerImpl; import com.android.systemui.statusbar.policy.UserSwitcherController; +import com.android.systemui.tuner.TunerService; import com.android.systemui.volume.VolumeComponent; import com.android.systemui.wmshell.BubblesManager; import com.android.wm.shell.bubbles.Bubbles; @@ -206,6 +208,8 @@ public interface StatusBarPhoneModule { NotificationIconAreaController notificationIconAreaController, BrightnessSlider.Factory brightnessSliderFactory, WiredChargingRippleController chargingRippleAnimationController, + OngoingCallController ongoingCallController, + TunerService tunerService, FeatureFlags featureFlags) { return new StatusBar( context, @@ -288,6 +292,8 @@ public interface StatusBarPhoneModule { notificationIconAreaController, brightnessSliderFactory, chargingRippleAnimationController, + ongoingCallController, + tunerService, featureFlags); } } 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 new file mode 100644 index 000000000000..60d3ea3a7093 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt @@ -0,0 +1,67 @@ +/* + * 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.statusbar.phone.ongoingcall + +import android.app.Notification +import android.app.Notification.CallStyle.CALL_TYPE_ONGOING +import android.util.Log +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.FeatureFlags +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection +import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener +import javax.inject.Inject + +/** + * A controller to handle the ongoing call chip in the collapsed status bar. + */ +@SysUISingleton +class OngoingCallController @Inject constructor( + private val notifCollection: CommonNotifCollection, + private val featureFlags: FeatureFlags +) { + + private val notifListener = object : NotifCollectionListener { + override fun onEntryUpdated(entry: NotificationEntry) { + if (isOngoingCallNotification(entry) && DEBUG) { + Log.d(TAG, "Ongoing call notification updated") + } + } + + override fun onEntryRemoved(entry: NotificationEntry, reason: Int) { + if (isOngoingCallNotification(entry) && DEBUG) { + Log.d(TAG, "Ongoing call notification removed") + } + } + } + + fun init() { + if (featureFlags.isOngoingCallStatusBarChipEnabled) { + notifCollection.addCollectionListener(notifListener) + } + } +} + +private fun isOngoingCallNotification(entry: NotificationEntry): Boolean { + val extras = entry.sbn.notification.extras + val callStyleTemplateName = Notification.CallStyle::class.java.name + return extras.getString(Notification.EXTRA_TEMPLATE) == callStyleTemplateName && + extras.getInt(Notification.EXTRA_CALL_TYPE, -1) == CALL_TYPE_ONGOING +} + +private const val TAG = "OngoingCallController" +private val DEBUG = Log.isLoggable(TAG, Log.DEBUG) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CallbackHandler.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CallbackHandler.java index b96cb5e36c82..7ac6d63430d6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CallbackHandler.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CallbackHandler.java @@ -21,6 +21,7 @@ import android.os.Message; import android.telephony.SubscriptionInfo; import com.android.internal.annotations.VisibleForTesting; +import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.statusbar.policy.NetworkController.EmergencyListener; import com.android.systemui.statusbar.policy.NetworkController.IconState; import com.android.systemui.statusbar.policy.NetworkController.MobileDataIndicators; @@ -32,6 +33,8 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.List; +import javax.inject.Inject; + /** * Implements network listeners and forwards the calls along onto other listeners but on @@ -60,12 +63,9 @@ public class CallbackHandler extends Handler implements EmergencyListener, Signa private int mHistoryIndex; private String mLastCallback; - public CallbackHandler() { - super(Looper.getMainLooper()); - } - + @Inject @VisibleForTesting - CallbackHandler(Looper looper) { + CallbackHandler(@Main Looper looper) { super(looper); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java index 499d1e420272..97d344ad6b63 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java @@ -322,7 +322,7 @@ public class Clock extends TextView implements // Update text color based when shade scrim changes color. public void onColorsChanged(boolean lightTheme) { final Context context = new ContextThemeWrapper(mContext, - lightTheme ? R.style.Theme_SystemUI_Light : R.style.Theme_SystemUI); + lightTheme ? R.style.Theme_SystemUI_LightWallpaper : R.style.Theme_SystemUI); setTextColor(Utils.getColorAttrDefaultColor(context, R.attr.wallpaperTextColor)); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsController.kt new file mode 100644 index 000000000000..b21189802c19 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsController.kt @@ -0,0 +1,35 @@ +/* + * 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.statusbar.policy + +/** + * Supports adding a DeviceControls QS tile + */ +interface DeviceControlsController { + interface Callback { + /** + * If controls become available, initiate this callback with the desired position + */ + fun onControlsAvailable(position: Int) + } + + /** Add callback, supporting only a single callback at once */ + fun setCallback(callback: Callback) + + /** Remove any existing callback, if any */ + fun removeCallback() +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt new file mode 100644 index 000000000000..d3907ae9a150 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt @@ -0,0 +1,173 @@ +/* + * 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.statusbar.policy + +import android.content.ComponentName +import android.content.Context +import android.content.SharedPreferences +import android.util.Log + +import com.android.systemui.R +import com.android.systemui.controls.ControlsServiceInfo +import com.android.systemui.controls.dagger.ControlsComponent +import com.android.systemui.controls.management.ControlsListingController +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.qs.QSTileHost.POSITION_AT_END +import com.android.systemui.settings.UserContextProvider +import com.android.systemui.statusbar.policy.DeviceControlsController.Callback + +import javax.inject.Inject + +/** + * Watches for Device Controls QS Tile activation, which can happen in two ways: + * <ol> + * <li>Migration from Power Menu - For existing Android 11 users, create a tile in a high + * priority position. + * <li>Device controls service becomes available - For non-migrated users, create a tile and + * place at the end of active tiles, and initiate seeding where possible. + * </ol> + */ +@SysUISingleton +public class DeviceControlsControllerImpl @Inject constructor( + private val context: Context, + private val controlsComponent: ControlsComponent, + private val userContextProvider: UserContextProvider +) : DeviceControlsController { + + private var callback: Callback? = null + internal var position: Int? = null + + private val listingCallback = object : ControlsListingController.ControlsListingCallback { + override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) { + if (!serviceInfos.isEmpty()) { + seedFavorites(serviceInfos) + } + } + } + + companion object { + private const val TAG = "DeviceControlsControllerImpl" + internal const val QS_PRIORITY_POSITION = 3 + internal const val QS_DEFAULT_POSITION = POSITION_AT_END + + internal const val PREFS_CONTROLS_SEEDING_COMPLETED = "SeedingCompleted" + internal const val PREFS_CONTROLS_FILE = "controls_prefs" + private const val SEEDING_MAX = 2 + } + + private fun checkMigrationToQs() { + controlsComponent.getControlsController().ifPresent { + if (!it.getFavorites().isEmpty()) { + position = QS_PRIORITY_POSITION + } + } + } + + /** + * This migration logic assumes that something like [AutoTileManager] is tracking state + * externally, and won't call this method after receiving a response via + * [Callback#onControlsAvailable], once per user. Otherwise the calculated position may be + * incorrect. + */ + override fun setCallback(callback: Callback) { + // Treat any additional call as a reset before recalculating + removeCallback() + + checkMigrationToQs() + controlsComponent.getControlsListingController().ifPresent { + it.addCallback(listingCallback) + } + + this.callback = callback + fireControlsAvailable() + } + + override fun removeCallback() { + position = null + callback = null + controlsComponent.getControlsListingController().ifPresent { + it.removeCallback(listingCallback) + } + } + + private fun fireControlsAvailable() { + position?.let { + Log.i(TAG, "Setting DeviceControlsTile position: $it") + callback?.onControlsAvailable(it) + } + } + + /** + * See if any available control service providers match one of the preferred components. If + * they do, and there are no current favorites for that component, query the preferred + * component for a limited number of suggested controls. + */ + private fun seedFavorites(serviceInfos: List<ControlsServiceInfo>) { + val preferredControlsPackages = context.getResources().getStringArray( + R.array.config_controlsPreferredPackages) + + val prefs = userContextProvider.userContext.getSharedPreferences( + PREFS_CONTROLS_FILE, Context.MODE_PRIVATE) + val seededPackages = prefs.getStringSet(PREFS_CONTROLS_SEEDING_COMPLETED, emptySet()) + + val controlsController = controlsComponent.getControlsController().get() + val componentsToSeed = mutableListOf<ComponentName>() + var i = 0 + while (i < Math.min(SEEDING_MAX, preferredControlsPackages.size)) { + val pkg = preferredControlsPackages[i] + serviceInfos.forEach { + if (pkg.equals(it.componentName.packageName) && !seededPackages.contains(pkg)) { + if (controlsController.countFavoritesForComponent(it.componentName) > 0) { + // When there are existing controls but no saved preference, assume it + // is out of sync, perhaps through a device restore, and update the + // preference + addPackageToSeededSet(prefs, pkg) + } else { + componentsToSeed.add(it.componentName) + } + } + } + i++ + } + + if (componentsToSeed.isEmpty()) return + + controlsController.seedFavoritesForComponents( + componentsToSeed, + { response -> + Log.d(TAG, "Controls seeded: $response") + if (response.accepted) { + addPackageToSeededSet(prefs, response.packageName) + if (position == null) { + position = QS_DEFAULT_POSITION + } + fireControlsAvailable() + + controlsComponent.getControlsListingController().ifPresent { + it.removeCallback(listingCallback) + } + } + }) + } + + private fun addPackageToSeededSet(prefs: SharedPreferences, pkg: String) { + val seededPackages = prefs.getStringSet(PREFS_CONTROLS_SEEDING_COMPLETED, emptySet()) + val updatedPkgs = seededPackages.toMutableSet() + updatedPkgs.add(pkg) + prefs.edit().putStringSet(PREFS_CONTROLS_SEEDING_COMPLETED, updatedPkgs).apply() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java index fd37d89a24ec..d52ea890af7e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java @@ -224,35 +224,29 @@ public class KeyguardQsUserSwitchController extends ViewController<UserAvatarVie mView.setContentDescription(contentDescription); } - if (mCurrentUser.picture == null) { - mView.setDrawableWithBadge(getDrawable(mCurrentUser).mutate(), - mCurrentUser.resolveId()); - } else { - int avatarSize = - (int) mResources.getDimension(R.dimen.kg_framed_avatar_size); - Drawable drawable = new CircleFramedDrawable(mCurrentUser.picture, avatarSize); - drawable.setColorFilter( - mCurrentUser.isSwitchToEnabled ? null - : mAdapter.getDisabledUserAvatarColorFilter()); - mView.setDrawableWithBadge(drawable, mCurrentUser.info.id); - } + mView.setDrawableWithBadge(getCurrentUserIcon().mutate(), mCurrentUser.resolveId()); } - Drawable getDrawable(UserSwitcherController.UserRecord item) { + Drawable getCurrentUserIcon() { Drawable drawable; - if (item.isCurrent && item.isGuest) { - drawable = mContext.getDrawable(R.drawable.ic_avatar_guest_user); - } else { - drawable = mAdapter.getIconDrawable(mContext, item); - } - - int iconColorRes; - if (item.isSwitchToEnabled) { - iconColorRes = R.color.kg_user_switcher_avatar_icon_color; + if (mCurrentUser.picture == null) { + if (mCurrentUser.isCurrent && mCurrentUser.isGuest) { + drawable = mContext.getDrawable(R.drawable.ic_avatar_guest_user); + } else { + drawable = mAdapter.getIconDrawable(mContext, mCurrentUser); + } + int iconColorRes; + if (mCurrentUser.isSwitchToEnabled) { + iconColorRes = R.color.kg_user_switcher_avatar_icon_color; + } else { + iconColorRes = R.color.kg_user_switcher_restricted_avatar_icon_color; + } + drawable.setTint(mResources.getColor(iconColorRes, mContext.getTheme())); } else { - iconColorRes = R.color.kg_user_switcher_restricted_avatar_icon_color; + int avatarSize = + (int) mResources.getDimension(R.dimen.kg_framed_avatar_size); + drawable = new CircleFramedDrawable(mCurrentUser.picture, avatarSize); } - drawable.setTint(mResources.getColor(iconColorRes, mContext.getTheme())); Drawable bg = mContext.getDrawable(R.drawable.kg_bg_avatar); drawable = new LayerDrawable(new Drawable[]{bg, drawable}); 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 24a0a07f3300..09ea186fb638 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java @@ -87,6 +87,7 @@ import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Locale; +import java.util.concurrent.Executor; import javax.inject.Inject; @@ -173,6 +174,8 @@ public class NetworkControllerImpl extends BroadcastReceiver private NetworkCapabilities mLastDefaultNetworkCapabilities; // Handler that all broadcasts are received on. private final Handler mReceiverHandler; + private final Looper mBgLooper; + private final Executor mBgExecutor; // Handler that all callbacks are made on. private final CallbackHandler mCallbackHandler; @@ -203,6 +206,9 @@ public class NetworkControllerImpl extends BroadcastReceiver public NetworkControllerImpl( Context context, @Background Looper bgLooper, + @Background Executor bgExecutor, + SubscriptionManager subscriptionManager, + CallbackHandler callbackHandler, DeviceProvisionedController deviceProvisionedController, BroadcastDispatcher broadcastDispatcher, ConnectivityManager connectivityManager, @@ -217,8 +223,11 @@ public class NetworkControllerImpl extends BroadcastReceiver telephonyListenerManager, wifiManager, networkScoreManager, - SubscriptionManager.from(context), Config.readConfig(context), bgLooper, - new CallbackHandler(), + subscriptionManager, + Config.readConfig(context), + bgLooper, + bgExecutor, + callbackHandler, accessPointController, new DataUsageController(context), new SubscriptionDefaults(), @@ -235,6 +244,7 @@ public class NetworkControllerImpl extends BroadcastReceiver WifiManager wifiManager, NetworkScoreManager networkScoreManager, SubscriptionManager subManager, Config config, Looper bgLooper, + Executor bgExecutor, CallbackHandler callbackHandler, AccessPointControllerImpl accessPointController, DataUsageController dataUsageController, @@ -246,6 +256,8 @@ public class NetworkControllerImpl extends BroadcastReceiver mTelephonyListenerManager = telephonyListenerManager; mConfig = config; mReceiverHandler = new Handler(bgLooper); + mBgLooper = bgLooper; + mBgExecutor = bgExecutor; mCallbackHandler = callbackHandler; mDataSaverController = new DataSaverControllerImpl(context); mBroadcastDispatcher = broadcastDispatcher; @@ -384,21 +396,23 @@ public class NetworkControllerImpl extends BroadcastReceiver // TODO: Move off of the deprecated CONNECTIVITY_ACTION broadcast and rely on callbacks // exclusively for status bar icons. mConnectivityManager.registerDefaultNetworkCallback(callback, mReceiverHandler); - // Register the listener on our bg looper + // Run the listener on our bg looper mPhoneStateListener = subId -> { - // For data switching from A to B, we assume B is validated for up to 2 seconds iff: - // 1) A and B are in the same subscription group e.g. CBRS data switch. And - // 2) A was validated before the switch. - // This is to provide smooth transition for UI without showing cross during data - // switch. - if (keepCellularValidationBitInSwitch(mActiveMobileDataSubscription, subId)) { - if (DEBUG) Log.d(TAG, ": mForceCellularValidated to true."); - mForceCellularValidated = true; - mReceiverHandler.removeCallbacks(mClearForceValidated); - mReceiverHandler.postDelayed(mClearForceValidated, 2000); - } - mActiveMobileDataSubscription = subId; - doUpdateMobileControllers(); + mBgExecutor.execute(() -> { + // For data switching from A to B, we assume B is validated for up to 2 seconds if: + // 1) A and B are in the same subscription group e.g. CBRS data switch. And + // 2) A was validated before the switch. + // This is to provide smooth transition for UI without showing cross during data + // switch. + if (keepCellularValidationBitInSwitch(mActiveMobileDataSubscription, subId)) { + if (DEBUG) Log.d(TAG, ": mForceCellularValidated to true."); + mForceCellularValidated = true; + mReceiverHandler.removeCallbacks(mClearForceValidated); + mReceiverHandler.postDelayed(mClearForceValidated, 2000); + } + mActiveMobileDataSubscription = subId; + doUpdateMobileControllers(); + }); }; mDemoModeController.addCallback(this); @@ -436,7 +450,7 @@ public class NetworkControllerImpl extends BroadcastReceiver mobileSignalController.registerFiveGStateListener(mFiveGServiceClient); } if (mSubscriptionListener == null) { - mSubscriptionListener = new SubListener(); + mSubscriptionListener = new SubListener(mBgLooper); } mSubscriptionManager.addOnSubscriptionsChangedListener(mSubscriptionListener); mTelephonyListenerManager.addActiveDataSubscriptionIdListener(mPhoneStateListener); @@ -1348,6 +1362,10 @@ public class NetworkControllerImpl extends BroadcastReceiver } private class SubListener extends OnSubscriptionsChangedListener { + SubListener(Looper looper) { + super(looper); + } + @Override public void onSubscriptionsChanged() { updateMobileControllers(); @@ -1358,10 +1376,5 @@ public class NetworkControllerImpl extends BroadcastReceiver * Used to register listeners from the BG Looper, this way the PhoneStateListeners that * get created will also run on the BG Looper. */ - private final Runnable mRegisterListeners = new Runnable() { - @Override - public void run() { - registerListeners(); - } - }; + private final Runnable mRegisterListeners = () -> registerListeners(); } 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 5a78ea82ab04..6843eb509b86 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java @@ -34,6 +34,7 @@ import android.content.pm.ShortcutManager; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.Color; +import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; @@ -157,52 +158,48 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene /** * The remote view needs to adapt to colorized notifications when set * It overrides the background of itself as well as all of its childern - * @param color colorized notification color + * @param backgroundColor colorized notification color */ - public void setBackgroundTintColor(int color, boolean colorized) { - if (colorized == mColorized && color == mTint) return; + public void setBackgroundTintColor(final int backgroundColor, boolean colorized) { + if (colorized == mColorized && backgroundColor == mTint) return; mColorized = colorized; - mTint = color; - final int[][] states = new int[][]{ - new int[]{com.android.internal.R.attr.state_enabled}, - new int[]{}, - }; - final int[] colors; + mTint = backgroundColor; + final int editBgColor; + final int accentColor; + final int textColor; + final int hintTextColor; if (colorized) { - final boolean dark = !ContrastColorUtil.isColorLight(color); - final int finalColor = dark - ? Color.WHITE - : Color.BLACK; - colors = new int[]{ - finalColor, - finalColor & 0x4DFFFFFF // %30 opacity - }; - mEditText.setUniformBackgroundTintColor(color); - mEditText.setUniformForegroundColor(finalColor); - + final boolean dark = !ContrastColorUtil.isColorLight(backgroundColor); + final int foregroundColor = dark ? Color.WHITE : Color.BLACK; + editBgColor = backgroundColor; + accentColor = foregroundColor; + textColor = foregroundColor; + hintTextColor = ColorUtils.setAlphaComponent(foregroundColor, 0x99); } else { - mEditText.setTextColor(mContext.getColor(R.color.remote_input_text)); - mEditText.setHintTextColor(mContext.getColorStateList(R.color.remote_input_hint)); - TypedArray ta = getContext().getTheme().obtainStyledAttributes(new int[]{ + textColor = mContext.getColor(R.color.remote_input_text); + hintTextColor = mContext.getColor(R.color.remote_input_hint); + try (TypedArray ta = getContext().getTheme().obtainStyledAttributes(new int[]{ com.android.internal.R.attr.colorAccent, com.android.internal.R.attr.colorBackgroundFloating, - }); - int colorAccent = ta.getColor(0, 0); - int colorBackgroundFloating = ta.getColor(1, 0); - ta.recycle(); - mEditText.setTextBackgroundColors(colorAccent, colorBackgroundFloating); - colors = new int[]{ - colorAccent, - colorBackgroundFloating & 0x4DFFFFFF // %30 opacity - }; - } - mEditText.setBackgroundColor(color); - final ColorStateList tint = new ColorStateList(states, colors); - mSendButton.setImageTintList(tint); - mProgressBar.setProgressTintList(tint); - mProgressBar.setIndeterminateTintList(tint); - mProgressBar.setSecondaryProgressTintList(tint); - setBackgroundColor(color); + })) { + accentColor = ta.getColor(0, textColor); + editBgColor = ta.getColor(1, backgroundColor); + } + } + mEditText.setAllColors(backgroundColor, editBgColor, + accentColor, textColor, hintTextColor); + final ColorStateList accentTint = new ColorStateList(new int[][]{ + new int[]{com.android.internal.R.attr.state_enabled}, + new int[]{}, + }, new int[]{ + accentColor, + accentColor & 0x4DFFFFFF // %30 opacity + }); + mSendButton.setImageTintList(accentTint); + mProgressBar.setProgressTintList(accentTint); + mProgressBar.setIndeterminateTintList(accentTint); + mProgressBar.setSecondaryProgressTintList(accentTint); + setBackgroundColor(backgroundColor); } @Override @@ -796,20 +793,6 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene } } - protected void setUniformBackgroundTintColor(int color) { - mBackgroundColor.setColor(color); - mTextBackground.setColor(color); - } - - protected void setUniformForegroundColor(int color) { - int stroke = getContext().getResources() - .getDimensionPixelSize(R.dimen.remote_input_view_text_stroke); - mTextBackground.setStroke(stroke, color); - setTextColor(color); - setHintTextColor(ColorUtils.setAlphaComponent(color, 0x99)); - setTextCursorDrawable(null); - } - @Override public void getFocusedRect(Rect r) { super.getFocusedRect(r); @@ -938,11 +921,17 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene return remainingItems; } - protected void setTextBackgroundColors(int strokeColor, int textBackground) { - mTextBackground.setColor(textBackground); + protected void setAllColors(int backgroundColor, int editBackgroundColor, + int accentColor, int textColor, int hintTextColor) { + setBackgroundColor(backgroundColor); + mBackgroundColor.setColor(backgroundColor); + mTextBackground.setColor(editBackgroundColor); int stroke = getContext().getResources() .getDimensionPixelSize(R.dimen.remote_input_view_text_stroke); - mTextBackground.setStroke(stroke, strokeColor); + mTextBackground.setStroke(stroke, accentColor); + setTextColor(textColor); + setHintTextColor(hintTextColor); + getTextCursorDrawable().setColorFilter(accentColor, PorterDuff.Mode.SRC_IN); } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoControllerImpl.java index 9eaee22b54ec..f42e3885fe62 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoControllerImpl.java @@ -143,7 +143,7 @@ public class UserInfoControllerImpl implements UserInfoController { final int userId = userInfo.id; final boolean isGuest = userInfo.isGuest(); final String userName = userInfo.name; - final boolean lightIcon = mContext.getThemeResId() != R.style.Theme_SystemUI_Light; + final boolean lightIcon = mContext.getThemeResId() != R.style.Theme_SystemUI_LightWallpaper; final Resources res = mContext.getResources(); final int avatarSize = Math.max( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java index 7a4b912d4071..766602261099 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java @@ -26,6 +26,8 @@ import com.android.systemui.statusbar.policy.BluetoothController; import com.android.systemui.statusbar.policy.BluetoothControllerImpl; import com.android.systemui.statusbar.policy.CastController; import com.android.systemui.statusbar.policy.CastControllerImpl; +import com.android.systemui.statusbar.policy.DeviceControlsController; +import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl; import com.android.systemui.statusbar.policy.ExtensionController; import com.android.systemui.statusbar.policy.ExtensionControllerImpl; import com.android.systemui.statusbar.policy.FlashlightController; @@ -44,8 +46,6 @@ import com.android.systemui.statusbar.policy.RotationLockController; import com.android.systemui.statusbar.policy.RotationLockControllerImpl; import com.android.systemui.statusbar.policy.SecurityController; import com.android.systemui.statusbar.policy.SecurityControllerImpl; -import com.android.systemui.statusbar.policy.SensorPrivacyController; -import com.android.systemui.statusbar.policy.SensorPrivacyControllerImpl; import com.android.systemui.statusbar.policy.UserInfoController; import com.android.systemui.statusbar.policy.UserInfoControllerImpl; import com.android.systemui.statusbar.policy.ZenModeController; @@ -115,6 +115,11 @@ public interface StatusBarPolicyModule { /** */ @Binds + DeviceControlsController provideDeviceControlsController( + DeviceControlsControllerImpl controllerImpl); + + /** */ + @Binds NetworkController.AccessPointController provideAccessPointController( AccessPointControllerImpl accessPointControllerImpl); diff --git a/packages/SystemUI/src/com/android/systemui/telephony/TelephonyCallback.java b/packages/SystemUI/src/com/android/systemui/telephony/TelephonyCallback.java index 95216c559420..728907fd2d36 100644 --- a/packages/SystemUI/src/com/android/systemui/telephony/TelephonyCallback.java +++ b/packages/SystemUI/src/com/android/systemui/telephony/TelephonyCallback.java @@ -54,9 +54,11 @@ class TelephonyCallback extends android.telephony.TelephonyCallback @Override public void onActiveDataSubscriptionIdChanged(int subId) { - mActiveDataSubscriptionIdListeners.forEach(listener -> { - listener.onActiveDataSubscriptionIdChanged(subId); - }); + List<ActiveDataSubscriptionIdListener> listeners; + synchronized (mActiveDataSubscriptionIdListeners) { + listeners = new ArrayList<>(mActiveDataSubscriptionIdListeners); + } + listeners.forEach(listener -> listener.onActiveDataSubscriptionIdChanged(subId)); } void addActiveDataSubscriptionIdListener(ActiveDataSubscriptionIdListener listener) { @@ -69,9 +71,11 @@ class TelephonyCallback extends android.telephony.TelephonyCallback @Override public void onCallStateChanged(int state) { - mCallStateListeners.forEach(listener -> { - listener.onCallStateChanged(state); - }); + List<CallStateListener> listeners; + synchronized (mCallStateListeners) { + listeners = new ArrayList<>(mCallStateListeners); + } + listeners.forEach(listener -> listener.onCallStateChanged(state)); } void addCallStateListener(CallStateListener listener) { @@ -84,9 +88,11 @@ class TelephonyCallback extends android.telephony.TelephonyCallback @Override public void onServiceStateChanged(@NonNull ServiceState serviceState) { - mServiceStateListeners.forEach(listener -> { - listener.onServiceStateChanged(serviceState); - }); + List<ServiceStateListener> listeners; + synchronized (mServiceStateListeners) { + listeners = new ArrayList<>(mServiceStateListeners); + } + listeners.forEach(listener -> listener.onServiceStateChanged(serviceState)); } void addServiceStateListener(ServiceStateListener listener) { diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayApplier.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayApplier.java index 278663b270bc..3c1e12327d8f 100644 --- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayApplier.java +++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayApplier.java @@ -15,6 +15,7 @@ */ package com.android.systemui.theme; +import android.annotation.AnyThread; import android.content.om.FabricatedOverlay; import android.content.om.OverlayIdentifier; import android.content.om.OverlayInfo; @@ -159,44 +160,48 @@ public class ThemeOverlayApplier implements Dumpable { FabricatedOverlay[] pendingCreation, int currentUser, Set<UserHandle> managedProfiles) { - // Disable all overlays that have not been specified in the user setting. - final Set<String> overlayCategoriesToDisable = new HashSet<>(THEME_CATEGORIES); - final Set<String> targetPackagesToQuery = overlayCategoriesToDisable.stream() - .map(category -> mCategoryToTargetPackage.get(category)) - .collect(Collectors.toSet()); - final List<OverlayInfo> overlays = new ArrayList<>(); - targetPackagesToQuery.forEach(targetPackage -> overlays.addAll(mOverlayManager - .getOverlayInfosForTarget(targetPackage, UserHandle.SYSTEM))); - final List<Pair<String, String>> overlaysToDisable = overlays.stream() - .filter(o -> - mTargetPackageToCategories.get(o.targetPackageName).contains(o.category)) - .filter(o -> overlayCategoriesToDisable.contains(o.category)) - .filter(o -> !categoryToPackage.containsValue(new OverlayIdentifier(o.packageName))) - .filter(o -> o.isEnabled()) - .map(o -> new Pair<>(o.category, o.packageName)) - .collect(Collectors.toList()); - - OverlayManagerTransaction.Builder transaction = getTransactionBuilder(); - if (pendingCreation != null) { - for (FabricatedOverlay overlay : pendingCreation) { - transaction.registerFabricatedOverlay(overlay); + mExecutor.execute(() -> { + + // Disable all overlays that have not been specified in the user setting. + final Set<String> overlayCategoriesToDisable = new HashSet<>(THEME_CATEGORIES); + final Set<String> targetPackagesToQuery = overlayCategoriesToDisable.stream() + .map(category -> mCategoryToTargetPackage.get(category)) + .collect(Collectors.toSet()); + final List<OverlayInfo> overlays = new ArrayList<>(); + targetPackagesToQuery.forEach(targetPackage -> overlays.addAll(mOverlayManager + .getOverlayInfosForTarget(targetPackage, UserHandle.SYSTEM))); + final List<Pair<String, String>> overlaysToDisable = overlays.stream() + .filter(o -> + mTargetPackageToCategories.get(o.targetPackageName).contains( + o.category)) + .filter(o -> overlayCategoriesToDisable.contains(o.category)) + .filter(o -> !categoryToPackage.containsValue( + new OverlayIdentifier(o.packageName))) + .filter(o -> o.isEnabled()) + .map(o -> new Pair<>(o.category, o.packageName)) + .collect(Collectors.toList()); + + OverlayManagerTransaction.Builder transaction = getTransactionBuilder(); + if (pendingCreation != null) { + for (FabricatedOverlay overlay : pendingCreation) { + transaction.registerFabricatedOverlay(overlay); + } } - } - for (Pair<String, String> packageToDisable : overlaysToDisable) { - OverlayIdentifier overlayInfo = new OverlayIdentifier(packageToDisable.second); - setEnabled(transaction, overlayInfo, packageToDisable.first, currentUser, - managedProfiles, false); - } + for (Pair<String, String> packageToDisable : overlaysToDisable) { + OverlayIdentifier overlayInfo = new OverlayIdentifier(packageToDisable.second); + setEnabled(transaction, overlayInfo, packageToDisable.first, currentUser, + managedProfiles, false); + } - for (String category : THEME_CATEGORIES) { - if (categoryToPackage.containsKey(category)) { - OverlayIdentifier overlayInfo = categoryToPackage.get(category); - setEnabled(transaction, overlayInfo, category, currentUser, managedProfiles, true); + for (String category : THEME_CATEGORIES) { + if (categoryToPackage.containsKey(category)) { + OverlayIdentifier overlayInfo = categoryToPackage.get(category); + setEnabled(transaction, overlayInfo, category, currentUser, managedProfiles, + true); + } } - } - mExecutor.execute(() -> { try { mOverlayManager.commit(transaction.build()); } catch (SecurityException | IllegalStateException e) { @@ -210,6 +215,7 @@ public class ThemeOverlayApplier implements Dumpable { return new OverlayManagerTransaction.Builder(); } + @AnyThread private void setEnabled(OverlayManagerTransaction.Builder transaction, OverlayIdentifier identifier, String category, int currentUser, Set<UserHandle> managedProfiles, boolean enabled) { diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java index d317712ce87c..fc9a35d7d54c 100644 --- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java +++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java @@ -19,9 +19,9 @@ import static com.android.systemui.theme.ThemeOverlayApplier.OVERLAY_CATEGORY_AC import static com.android.systemui.theme.ThemeOverlayApplier.OVERLAY_CATEGORY_SYSTEM_PALETTE; import android.annotation.Nullable; -import android.app.ActivityManager; import android.app.WallpaperColors; import android.app.WallpaperManager; +import android.app.WallpaperManager.OnColorsChangedListener; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -49,8 +49,10 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; +import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.FeatureFlags; -import com.android.systemui.statusbar.policy.KeyguardStateController; +import com.android.systemui.statusbar.policy.DeviceProvisionedController; +import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener; import com.android.systemui.util.settings.SecureSettings; import org.json.JSONException; @@ -92,8 +94,9 @@ public class ThemeOverlayController extends SystemUI implements Dumpable { private final Executor mMainExecutor; private final Handler mBgHandler; private final WallpaperManager mWallpaperManager; - private final KeyguardStateController mKeyguardStateController; private final boolean mIsMonetEnabled; + private final UserTracker mUserTracker; + private DeviceProvisionedController mDeviceProvisionedController; private WallpaperColors mSystemColors; // If fabricated overlays were already created for the current theme. private boolean mNeedsOverlayCreation; @@ -107,17 +110,76 @@ public class ThemeOverlayController extends SystemUI implements Dumpable { private FabricatedOverlay mNeutralOverlay; // If wallpaper color event will be accepted and change the UI colors. private boolean mAcceptColorEvents = true; + // Defers changing themes until Setup Wizard is done. + private boolean mDeferredThemeEvaluation; + + private final DeviceProvisionedListener mDeviceProvisionedListener = + new DeviceProvisionedListener() { + @Override + public void onUserSetupChanged() { + if (!mDeviceProvisionedController.isCurrentUserSetup()) { + return; + } + if (!mDeferredThemeEvaluation) { + return; + } + Log.i(TAG, "Applying deferred theme"); + mDeferredThemeEvaluation = false; + reevaluateSystemTheme(true /* forceReload */); + } + }; + + private final OnColorsChangedListener mOnColorsChangedListener = (wallpaperColors, which) -> { + if (!mAcceptColorEvents) { + Log.i(TAG, "Wallpaper color event rejected: " + wallpaperColors); + return; + } + if (wallpaperColors != null) { + mAcceptColorEvents = false; + } + + if ((which & WallpaperManager.FLAG_SYSTEM) != 0) { + mSystemColors = wallpaperColors; + if (DEBUG) { + Log.d(TAG, "got new lock colors: " + wallpaperColors + " where: " + which); + } + } + + if (mDeviceProvisionedController != null + && !mDeviceProvisionedController.isCurrentUserSetup()) { + Log.i(TAG, "Wallpaper color event deferred until setup is finished: " + + wallpaperColors); + mDeferredThemeEvaluation = true; + return; + } + reevaluateSystemTheme(false /* forceReload */); + }; + + private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (Intent.ACTION_USER_SWITCHED.equals(intent.getAction()) + || Intent.ACTION_MANAGED_PROFILE_ADDED.equals(intent.getAction())) { + if (DEBUG) Log.d(TAG, "Updating overlays for user switch / profile added."); + reevaluateSystemTheme(true /* forceReload */); + } else if (Intent.ACTION_WALLPAPER_CHANGED.equals(intent.getAction())) { + mAcceptColorEvents = true; + Log.i(TAG, "Allowing color events again"); + } + } + }; @Inject public ThemeOverlayController(Context context, BroadcastDispatcher broadcastDispatcher, @Background Handler bgHandler, @Main Executor mainExecutor, @Background Executor bgExecutor, ThemeOverlayApplier themeOverlayApplier, SecureSettings secureSettings, WallpaperManager wallpaperManager, - UserManager userManager, KeyguardStateController keyguardStateController, - DumpManager dumpManager, FeatureFlags featureFlags) { + UserManager userManager, DeviceProvisionedController deviceProvisionedController, + UserTracker userTracker, DumpManager dumpManager, FeatureFlags featureFlags) { super(context); mIsMonetEnabled = featureFlags.isMonetEnabled(); + mDeviceProvisionedController = deviceProvisionedController; mBroadcastDispatcher = broadcastDispatcher; mUserManager = userManager; mBgExecutor = bgExecutor; @@ -126,7 +188,7 @@ public class ThemeOverlayController extends SystemUI implements Dumpable { mThemeManager = themeOverlayApplier; mSecureSettings = secureSettings; mWallpaperManager = wallpaperManager; - mKeyguardStateController = keyguardStateController; + mUserTracker = userTracker; dumpManager.registerDumpable(TAG, this); } @@ -137,19 +199,8 @@ public class ThemeOverlayController extends SystemUI implements Dumpable { filter.addAction(Intent.ACTION_USER_SWITCHED); filter.addAction(Intent.ACTION_MANAGED_PROFILE_ADDED); filter.addAction(Intent.ACTION_WALLPAPER_CHANGED); - mBroadcastDispatcher.registerReceiver(new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (Intent.ACTION_USER_SWITCHED.equals(intent.getAction()) - || Intent.ACTION_MANAGED_PROFILE_ADDED.equals(intent.getAction())) { - if (DEBUG) Log.d(TAG, "Updating overlays for user switch / profile added."); - reevaluateSystemTheme(true /* forceReload */); - } else if (Intent.ACTION_WALLPAPER_CHANGED.equals(intent.getAction())) { - mAcceptColorEvents = true; - Log.i(TAG, "Allowing color events again"); - } - } - }, filter, mMainExecutor, UserHandle.ALL); + mBroadcastDispatcher.registerReceiver(mBroadcastReceiver, filter, mMainExecutor, + UserHandle.ALL); mSecureSettings.registerContentObserverForUser( Settings.Secure.getUriFor(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), false, @@ -158,12 +209,19 @@ public class ThemeOverlayController extends SystemUI implements Dumpable { public void onChange(boolean selfChange, Collection<Uri> collection, int flags, int userId) { if (DEBUG) Log.d(TAG, "Overlay changed for user: " + userId); - if (ActivityManager.getCurrentUser() == userId) { - reevaluateSystemTheme(true /* forceReload */); + if (mUserTracker.getUserId() != userId) { + return; } + if (!mDeviceProvisionedController.isUserSetup(userId)) { + Log.i(TAG, "Theme application deferred when setting changed."); + mDeferredThemeEvaluation = true; + return; + } + reevaluateSystemTheme(true /* forceReload */); } }, UserHandle.USER_ALL); + mDeviceProvisionedController.addCallback(mDeviceProvisionedListener); // Upon boot, make sure we have the most up to date colors mBgExecutor.execute(() -> { @@ -174,23 +232,8 @@ public class ThemeOverlayController extends SystemUI implements Dumpable { reevaluateSystemTheme(false /* forceReload */); }); }); - mWallpaperManager.addOnColorsChangedListener((wallpaperColors, which) -> { - if (!mAcceptColorEvents) { - Log.i(TAG, "Wallpaper color event rejected: " + wallpaperColors); - return; - } - if (wallpaperColors != null && mAcceptColorEvents) { - mAcceptColorEvents = false; - } - - if ((which & WallpaperManager.FLAG_SYSTEM) != 0) { - mSystemColors = wallpaperColors; - if (DEBUG) { - Log.d(TAG, "got new lock colors: " + wallpaperColors + " where: " + which); - } - } - reevaluateSystemTheme(false /* forceReload */); - }, null, UserHandle.USER_ALL); + mWallpaperManager.addOnColorsChangedListener(mOnColorsChangedListener, null, + UserHandle.USER_ALL); } private void reevaluateSystemTheme(boolean forceReload) { @@ -252,7 +295,7 @@ public class ThemeOverlayController extends SystemUI implements Dumpable { } private void updateThemeOverlays() { - final int currentUser = ActivityManager.getCurrentUser(); + final int currentUser = mUserTracker.getUserId(); final String overlayPackageJson = mSecureSettings.getStringForUser( Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, currentUser); @@ -360,5 +403,6 @@ public class ThemeOverlayController extends SystemUI implements Dumpable { pw.println("mIsMonetEnabled=" + mIsMonetEnabled); pw.println("mNeedsOverlayCreation=" + mNeedsOverlayCreation); pw.println("mAcceptColorEvents=" + mAcceptColorEvents); + pw.println("mDeferredThemeEvaluation=" + mDeferredThemeEvaluation); } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index 044d8285b079..a8a3d79c67cf 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -76,6 +76,7 @@ import android.view.ContextThemeWrapper; import android.view.MotionEvent; import android.view.View; import android.view.View.AccessibilityDelegate; +import android.view.View.OnAttachStateChangeListener; import android.view.ViewGroup; import android.view.ViewPropertyAnimator; import android.view.ViewStub; @@ -95,6 +96,7 @@ import android.widget.SeekBar.OnSeekBarChangeListener; import android.widget.TextView; import android.widget.Toast; +import com.android.internal.graphics.drawable.BackgroundBlurDrawable; import com.android.settingslib.Utils; import com.android.systemui.Dependency; import com.android.systemui.Interpolators; @@ -115,6 +117,7 @@ import com.android.systemui.util.RoundedCornerProgressDrawable; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; /** * Visual presentation of the volume dialog. @@ -214,6 +217,10 @@ public class VolumeDialogImpl implements VolumeDialog, private ViewStub mODICaptionsTooltipViewStub; private View mODICaptionsTooltipView = null; + private final boolean mUseBackgroundBlur; + private Consumer<Boolean> mCrossWindowBlurEnabledListener; + private BackgroundBlurDrawable mDialogRowsViewBackground; + public VolumeDialogImpl(Context context) { mContext = new ContextThemeWrapper(context, R.style.volume_dialog_theme); @@ -233,6 +240,20 @@ public class VolumeDialogImpl implements VolumeDialog, mContext.getResources().getInteger(R.integer.config_dialogShowAnimationDurationMs); mDialogHideAnimationDurationMs = mContext.getResources().getInteger(R.integer.config_dialogHideAnimationDurationMs); + mUseBackgroundBlur = + mContext.getResources().getBoolean(R.bool.config_volumeDialogUseBackgroundBlur); + + if (mUseBackgroundBlur) { + final int dialogRowsViewColorAboveBlur = mContext.getColor( + R.color.volume_dialog_background_color_above_blur); + final int dialogRowsViewColorNoBlur = mContext.getColor( + R.color.volume_dialog_background_color); + mCrossWindowBlurEnabledListener = (enabled) -> { + mDialogRowsViewBackground.setColor( + enabled ? dialogRowsViewColorAboveBlur : dialogRowsViewColorNoBlur); + mDialogRowsView.invalidate(); + }; + } mRingerDrawerItemSize = mContext.getResources().getDimensionPixelSize( R.dimen.volume_ringer_drawer_item_size); mShowVibrate = mController.hasVibrator(); @@ -280,11 +301,13 @@ public class VolumeDialogImpl implements VolumeDialog, // the volume dialog container itself, so this is fine. for (int i = 0; i < mDialogView.getChildCount(); i++) { final View view = mDialogView.getChildAt(i); + final int[] locInWindow = new int[2]; + view.getLocationInWindow(locInWindow); mTouchableRegion.op( - view.getLeft(), - view.getTop(), - view.getRight(), - view.getBottom(), + locInWindow[0], + locInWindow[1], + locInWindow[0] + view.getWidth(), + locInWindow[1] + view.getHeight(), Region.Op.UNION); } @@ -356,6 +379,32 @@ public class VolumeDialogImpl implements VolumeDialog, }); mDialogRowsView = mDialog.findViewById(R.id.volume_dialog_rows); + if (mUseBackgroundBlur) { + mDialogView.addOnAttachStateChangeListener(new OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View v) { + mWindow.getWindowManager().addCrossWindowBlurEnabledListener( + mCrossWindowBlurEnabledListener); + + mDialogRowsViewBackground = v.getViewRootImpl().createBackgroundBlurDrawable(); + + final Resources resources = mContext.getResources(); + mDialogRowsViewBackground.setCornerRadius( + mContext.getResources().getDimensionPixelSize(Utils.getThemeAttr( + mContext, android.R.attr.dialogCornerRadius))); + mDialogRowsViewBackground.setBlurRadius(resources.getDimensionPixelSize( + R.dimen.volume_dialog_background_blur_radius)); + mDialogRowsView.setBackground(mDialogRowsViewBackground); + } + + @Override + public void onViewDetachedFromWindow(View v) { + mWindow.getWindowManager().removeCrossWindowBlurEnabledListener( + mCrossWindowBlurEnabledListener); + } + }); + } + mRinger = mDialog.findViewById(R.id.ringer); if (mRinger != null) { mRingerIcon = mRinger.findViewById(R.id.ringer_icon); diff --git a/packages/SystemUI/src/com/android/systemui/wallet/dagger/WalletModule.java b/packages/SystemUI/src/com/android/systemui/wallet/dagger/WalletModule.java new file mode 100644 index 000000000000..c1f5516f02bb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/wallet/dagger/WalletModule.java @@ -0,0 +1,51 @@ +/* + * 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.dagger; + +import android.app.Activity; +import android.content.Context; +import android.service.quickaccesswallet.QuickAccessWalletClient; + +import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.wallet.ui.WalletActivity; + +import dagger.Binds; +import dagger.Module; +import dagger.Provides; +import dagger.multibindings.ClassKey; +import dagger.multibindings.IntoMap; + + +/** + * Module for injecting classes in Wallet. + */ +@Module +public abstract class WalletModule { + + /** */ + @Binds + @IntoMap + @ClassKey(WalletActivity.class) + public abstract Activity provideWalletActivity(WalletActivity activity); + + /** */ + @SysUISingleton + @Provides + public static QuickAccessWalletClient provideQuickAccessWalletClient(Context context) { + return QuickAccessWalletClient.create(context); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/wallet/ui/DotIndicatorDecoration.java b/packages/SystemUI/src/com/android/systemui/wallet/ui/DotIndicatorDecoration.java new file mode 100644 index 000000000000..644addfef1d1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/wallet/ui/DotIndicatorDecoration.java @@ -0,0 +1,181 @@ +/* + * 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 android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.text.TextPaint; +import android.util.MathUtils; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.ColorInt; +import androidx.core.content.ContextCompat; +import androidx.core.graphics.ColorUtils; +import androidx.recyclerview.widget.RecyclerView; + +import com.android.systemui.R; + +final class DotIndicatorDecoration extends RecyclerView.ItemDecoration { + private final int mUnselectedRadius; + private final int mSelectedRadius; + private final int mDotMargin; + @ColorInt private final int mUnselectedColor; + @ColorInt private final int mSelectedColor; + private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private final TextPaint mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); + private WalletCardCarousel mCardCarousel; + + DotIndicatorDecoration(Context context) { + super(); + + mUnselectedRadius = + context.getResources().getDimensionPixelSize( + R.dimen.card_carousel_dot_unselected_radius); + mSelectedRadius = + context.getResources().getDimensionPixelSize( + R.dimen.card_carousel_dot_selected_radius); + mDotMargin = context.getResources().getDimensionPixelSize(R.dimen.card_carousel_dot_margin); + + TextView textView = new TextView(context); + mTextPaint.set(textView.getPaint()); + // Text color is not copied from text appearance. + mTextPaint.setColor(ContextCompat.getColor(context, R.color.GM2_blue_600)); + + mUnselectedColor = ContextCompat.getColor(context, R.color.GM2_grey_300); + mSelectedColor = ContextCompat.getColor(context, R.color.GM2_blue_600); + } + + @Override + public void getItemOffsets( + Rect rect, View view, RecyclerView recyclerView, RecyclerView.State state) { + super.getItemOffsets(rect, view, recyclerView, state); + if (recyclerView.getAdapter().getItemCount() > 1) { + rect.bottom = + view.getResources().getDimensionPixelSize(R.dimen.card_carousel_dot_offset); + } + } + + @Override + public void onDrawOver(Canvas canvas, RecyclerView recyclerView, RecyclerView.State state) { + super.onDrawOver(canvas, recyclerView, state); + + mCardCarousel = (WalletCardCarousel) recyclerView; + int itemCount = recyclerView.getAdapter().getItemCount(); + if (itemCount <= 1) { + // Only shown if there are at least 2 items, and it's not a shimmer loader + return; + } + canvas.save(); + + float animationStartOffset = recyclerView.getWidth() / 6f; + // 0 when a card is still very prominent, ie. edgeToCenterDistance is greater than + // animationStartOffset + // 1 when the two cards are equidistant from the center ie. edgeToCenterDistance == 0 + float interpolatedProgress = + 1 - Math.min(Math.abs(mCardCarousel.mEdgeToCenterDistance), animationStartOffset) + / animationStartOffset; + + float totalWidth = + mDotMargin * (itemCount - 1) + + 2 * mUnselectedRadius * (itemCount - 2) + + 2 * mSelectedRadius; + // Translate the canvas so the drawing can always start at (0, 0) coordinates. + canvas.translate( + (recyclerView.getWidth() - totalWidth) / 2f, + recyclerView.getHeight() - mDotMargin); + + int itemsDrawn = 0; + while (itemsDrawn < itemCount) { + // count up from 0 to itemCount - 1 if LTR; count down from itemCount - 1 to 0 if RTL. + int i = isLayoutLtr() ? itemsDrawn : itemCount - itemsDrawn - 1; + + if (isSelectedItem(i)) { + drawSelectedDot(canvas, interpolatedProgress, i); + } else if (isNextItemInScrollingDirection(i)) { + drawFadingUnselectedDot(canvas, interpolatedProgress, i); + } else { + drawUnselectedDot(canvas); + } + canvas.translate(mDotMargin, 0); + itemsDrawn++; + } + + canvas.restore(); + this.mCardCarousel = null; // No need to hold a reference. + } + + private void drawSelectedDot(Canvas canvas, float progress, int position) { + // Divide progress by 2 because the other half of the animation is done by + // drawFadingUnselectedDot. + mPaint.setColor( + getTransitionAdjustedColor( + ColorUtils.blendARGB(mSelectedColor, mUnselectedColor, progress / 2))); + float radius = MathUtils.lerp(mSelectedRadius, mUnselectedRadius, progress / 2); + canvas.drawCircle(radius, 0, radius, mPaint); + canvas.translate(radius * 2, 0); + } + + private void drawFadingUnselectedDot(Canvas canvas, float progress, int position) { + // Divide progress by 2 because the first half of the animation is done by drawSelectedDot. + int blendedColor = + ColorUtils.blendARGB( + mUnselectedColor, mSelectedColor, progress / 2); + mPaint.setColor(getTransitionAdjustedColor(blendedColor)); + float radius = MathUtils.lerp(mSelectedRadius, mUnselectedRadius, progress / 2); + canvas.drawCircle(radius, 0, radius, mPaint); + canvas.translate(radius * 2, 0); + } + + private void drawUnselectedDot(Canvas canvas) { + mPaint.setColor(mUnselectedColor); + canvas.drawCircle(mUnselectedRadius, 0, mUnselectedRadius, mPaint); + canvas.translate(mUnselectedRadius * 2, 0); + } + + private int getTransitionAdjustedColor(int color) { + int transitionAlphaOverride = 0xff; + return ColorUtils.setAlphaComponent(color, transitionAlphaOverride); + } + + private boolean isSelectedItem(int position) { + return mCardCarousel.mCenteredAdapterPosition == position; + } + + private boolean isNextItemInScrollingDirection(int position) { + if (isLayoutLtr()) { + return (mCardCarousel.mCenteredAdapterPosition + 1 == position + && mCardCarousel.mEdgeToCenterDistance >= 0f) + || (mCardCarousel.mCenteredAdapterPosition - 1 == position + && mCardCarousel.mEdgeToCenterDistance < 0f); + } + return (mCardCarousel.mCenteredAdapterPosition - 1 == position + && mCardCarousel.mEdgeToCenterDistance >= 0f) + || (mCardCarousel.mCenteredAdapterPosition + 1 == position + && mCardCarousel.mEdgeToCenterDistance < 0f); + } + + private boolean isLayoutLtr() { + if (mCardCarousel == null) { + // Shouldn't happen, but assume LTR for now. + return true; + } + return mCardCarousel.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletActivity.java b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletActivity.java new file mode 100644 index 000000000000..ba063a8ba303 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletActivity.java @@ -0,0 +1,122 @@ +/* + * 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 android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.os.Bundle; +import android.os.Handler; +import android.service.quickaccesswallet.QuickAccessWalletClient; +import android.view.MenuItem; +import android.view.Window; + +import androidx.annotation.NonNull; + +import com.android.systemui.R; +import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.settings.UserTracker; +import com.android.systemui.statusbar.policy.KeyguardStateController; +import com.android.systemui.util.LifecycleActivity; + +import java.util.concurrent.Executor; + +import javax.inject.Inject; + +/** + * Displays Wallet carousel screen inside an activity. + */ +public class WalletActivity extends LifecycleActivity { + + private final QuickAccessWalletClient mQuickAccessWalletClient; + private final KeyguardStateController mKeyguardStateController; + private final ActivityStarter mActivityStarter; + private final Executor mExecutor; + private final Handler mHandler; + private final UserTracker mUserTracker; + private WalletScreenController mWalletScreenController; + + @Inject + public WalletActivity( + QuickAccessWalletClient quickAccessWalletClient, + KeyguardStateController keyguardStateController, + ActivityStarter activityStarter, + @Background Executor executor, + @Background Handler handler, + UserTracker userTracker) { + mQuickAccessWalletClient = quickAccessWalletClient; + mKeyguardStateController = keyguardStateController; + mActivityStarter = activityStarter; + mExecutor = executor; + mHandler = handler; + mUserTracker = userTracker; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + getWindow().requestFeature(Window.FEATURE_ACTION_BAR_OVERLAY); + super.onCreate(savedInstanceState); + + setContentView(R.layout.quick_access_wallet); + + getWindow().getDecorView().setBackgroundColor(getColor(R.color.wallet_white)); + setTitle(""); + getActionBar().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); + getActionBar().setDisplayHomeAsUpEnabled(true); + getActionBar().setHomeAsUpIndicator(R.drawable.ic_close); + getActionBar().setHomeActionContentDescription(R.string.accessibility_desc_close); + WalletView walletView = requireViewById(R.id.wallet_view); + mWalletScreenController = new WalletScreenController( + this, + walletView, + mQuickAccessWalletClient, + mActivityStarter, + mExecutor, + mHandler, + mUserTracker, + !mKeyguardStateController.isUnlocked()); + walletView.getWalletButton().setOnClickListener( + v -> mActivityStarter.startActivity( + mQuickAccessWalletClient.createWalletIntent(), true)); + } + + @Override + protected void onStart() { + super.onStart(); + } + + @Override + protected void onResume() { + super.onResume(); + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + int itemId = item.getItemId(); + if (itemId == android.R.id.home) { + finish(); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + protected void onDestroy() { + mWalletScreenController.onDismissed(); + super.onDestroy(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletCardCarousel.java b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletCardCarousel.java new file mode 100644 index 000000000000..21e55496d3e6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletCardCarousel.java @@ -0,0 +1,473 @@ +/* + * 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 android.content.Context; +import android.content.res.Resources; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.view.HapticFeedbackConstants; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.widget.ImageView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.cardview.widget.CardView; +import androidx.core.view.ViewCompat; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.LinearSmoothScroller; +import androidx.recyclerview.widget.PagerSnapHelper; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate; + +import com.android.systemui.R; + +import java.util.Collections; +import java.util.List; + +/** + * Card Carousel for displaying Quick Access Wallet cards. + */ +public class WalletCardCarousel extends RecyclerView { + + // A negative card margin is required because card shrinkage pushes the cards too far apart + private static final float CARD_MARGIN_RATIO = -.03f; + // Size of the unselected card as a ratio to size of selected card. + private static final float UNSELECTED_CARD_SCALE = .83f; + private static final float CORNER_RADIUS_RATIO = 25f / 700f; + private static final float CARD_ASPECT_RATIO = 700f / 440f; + private static final float CARD_VIEW_WIDTH_RATIO = 0.69f; + + + static final int CARD_ANIM_ALPHA_DURATION = 100; + static final int CARD_ANIM_ALPHA_DELAY = 50; + + private final Rect mSystemGestureExclusionZone = new Rect(); + private WalletCardCarouselAdapter mWalletCardCarouselAdapter; + private int mExpectedViewWidth; + private int mCardMarginPx; + private int mCardWidthPx; + private int mCardHeightPx; + private float mCornerRadiusPx; + private int mTotalCardWidth; + private float mCardEdgeToCenterDistance; + + private OnSelectionListener mSelectionListener; + private OnCardScrollListener mCardScrollListener; + // Adapter position of the child that is closest to the center of the recycler view, will also + // be used in DotIndicatorDecoration. + int mCenteredAdapterPosition = RecyclerView.NO_POSITION; + // Pixel distance, along y-axis, from the center of the recycler view to the nearest child, will + // also be used in DotIndicatorDecoration. + float mEdgeToCenterDistance = Float.MAX_VALUE; + private float mCardCenterToScreenCenterDistancePx = Float.MAX_VALUE; + // When card data is loaded, this many cards should be animated as data is bound to them. + private int mNumCardsToAnimate; + // When card data is loaded, this is the position of the leftmost card to be animated. + private int mCardAnimationStartPosition; + // When card data is loaded, the animations may be delayed so that other animations can complete + private int mExtraAnimationDelay; + + interface OnSelectionListener { + /** + * The card was moved to the center, thus selecting it. + */ + void onCardSelected(@NonNull WalletCardViewInfo card); + + /** + * The card was clicked. + */ + void onCardClicked(@NonNull WalletCardViewInfo card); + + /** + * Cards should be re-queried due to a layout change + */ + void queryWalletCards(); + } + + interface OnCardScrollListener { + void onCardScroll(WalletCardViewInfo centerCard, WalletCardViewInfo nextCard, + float percentDistanceFromCenter); + } + + public WalletCardCarousel(Context context) { + this(context, null); + } + + public WalletCardCarousel(Context context, @Nullable AttributeSet attributeSet) { + super(context, attributeSet); + + setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)); + addOnScrollListener(new CardCarouselScrollListener()); + new CarouselSnapHelper().attachToRecyclerView(this); + mWalletCardCarouselAdapter = new WalletCardCarouselAdapter(); + mWalletCardCarouselAdapter.setHasStableIds(true); + setAdapter(mWalletCardCarouselAdapter); + ViewCompat.setAccessibilityDelegate(this, new CardCarouselAccessibilityDelegate(this)); + + addItemDecoration(new DotIndicatorDecoration(getContext())); + } + + /** + * We need to know the card width before we query cards. Card width depends on layout width. + * But the carousel isn't laid out until set to visible, which only happens after cards are + * returned. Setting the expected view width breaks the chicken-and-egg problem. + */ + void setExpectedViewWidth(int width) { + if (mExpectedViewWidth == width) { + return; + } + mExpectedViewWidth = width; + Resources res = getResources(); + DisplayMetrics metrics = res.getDisplayMetrics(); + int screenWidth = Math.min(metrics.widthPixels, metrics.heightPixels); + mCardWidthPx = Math.round(Math.min(width, screenWidth) * CARD_VIEW_WIDTH_RATIO); + mCardHeightPx = Math.round(mCardWidthPx / CARD_ASPECT_RATIO); + mCornerRadiusPx = mCardWidthPx * CORNER_RADIUS_RATIO; + mCardMarginPx = Math.round(mCardWidthPx * CARD_MARGIN_RATIO); + mTotalCardWidth = mCardWidthPx + res.getDimensionPixelSize(R.dimen.card_margin) * 2; + mCardEdgeToCenterDistance = mTotalCardWidth / 2f; + updatePadding(width); + if (mSelectionListener != null) { + mSelectionListener.queryWalletCards(); + } + } + + @Override + public void onViewAdded(View child) { + super.onViewAdded(child); + LayoutParams layoutParams = (LayoutParams) child.getLayoutParams(); + layoutParams.leftMargin = mCardMarginPx; + layoutParams.rightMargin = mCardMarginPx; + child.addOnLayoutChangeListener((v, l, t, r, b, ol, ot, or, ob) -> updateCardView(child)); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + int width = getWidth(); + if (mWalletCardCarouselAdapter.getItemCount() > 1 && width < mTotalCardWidth * 1.5) { + // When 2 or more cards are available but only one whole card can be shown on screen at + // a time, the entire carousel is opted out from system gesture to help users swipe + // between cards without accidentally performing the 'back' gesture. When there is only + // one card or when the carousel is large enough to accommodate several whole cards, + // there is no need to disable the back gesture since either the user can't swipe or has + // plenty of room with which to do so. + mSystemGestureExclusionZone.set(0, 0, width, getHeight()); + setSystemGestureExclusionRects(Collections.singletonList(mSystemGestureExclusionZone)); + } + if (width != mExpectedViewWidth) { + updatePadding(width); + } + } + + void setExtraAnimationDelay(int extraAnimationDelay) { + mExtraAnimationDelay = extraAnimationDelay; + } + + void setSelectionListener(OnSelectionListener selectionListener) { + mSelectionListener = selectionListener; + } + + void setCardScrollListener(OnCardScrollListener scrollListener) { + mCardScrollListener = scrollListener; + } + + int getCardWidthPx() { + return mCardWidthPx; + } + + int getCardHeightPx() { + return mCardHeightPx; + } + + /** + * Set card data. Returns true if carousel was empty, indicating that views will be animated + */ + boolean setData(List<WalletCardViewInfo> data, int selectedIndex) { + boolean wasEmpty = mWalletCardCarouselAdapter.getItemCount() == 0; + mWalletCardCarouselAdapter.setData(data); + if (wasEmpty) { + scrollToPosition(selectedIndex); + mNumCardsToAnimate = numCardsOnScreen(data.size(), selectedIndex); + mCardAnimationStartPosition = Math.max(selectedIndex - 1, 0); + } + WalletCardViewInfo selectedCard = data.get(selectedIndex); + mCardScrollListener.onCardScroll(selectedCard, selectedCard, 0); + return wasEmpty; + } + + @Override + public void scrollToPosition(int position) { + super.scrollToPosition(position); + mSelectionListener.onCardSelected(mWalletCardCarouselAdapter.mData.get(position)); + } + + /** + * The number of cards shown on screen when one of the cards is position in the center. This is + * also the num + */ + private static int numCardsOnScreen(int numCards, int selectedIndex) { + if (numCards <= 2) { + return numCards; + } + // When there are 3 or more cards, 3 cards will be shown unless the first or last card is + // centered on screen. + return selectedIndex > 0 && selectedIndex < (numCards - 1) ? 3 : 2; + } + + /** + * The padding pushes the first and last cards in the list to the center when they are + * selected. + */ + private void updatePadding(int viewWidth) { + int paddingHorizontal = (viewWidth - mTotalCardWidth) / 2 - mCardMarginPx; + paddingHorizontal = Math.max(0, paddingHorizontal); // just in case + setPadding(paddingHorizontal, getPaddingTop(), paddingHorizontal, getPaddingBottom()); + + // re-center selected card after changing padding (if card is selected) + if (mWalletCardCarouselAdapter != null + && mWalletCardCarouselAdapter.getItemCount() > 0 + && mCenteredAdapterPosition != NO_POSITION) { + ViewHolder viewHolder = findViewHolderForAdapterPosition(mCenteredAdapterPosition); + if (viewHolder != null) { + View cardView = viewHolder.itemView; + int cardCenter = (cardView.getLeft() + cardView.getRight()) / 2; + int viewCenter = (getLeft() + getRight()) / 2; + int scrollX = cardCenter - viewCenter; + scrollBy(scrollX, 0); + } + } + } + + private void updateCardView(View view) { + WalletCardViewHolder viewHolder = (WalletCardViewHolder) view.getTag(); + CardView cardView = viewHolder.mCardView; + float center = (float) getWidth() / 2f; + float viewCenter = (view.getRight() + view.getLeft()) / 2f; + float viewWidth = view.getWidth(); + float position = (viewCenter - center) / viewWidth; + float scaleFactor = Math.max(UNSELECTED_CARD_SCALE, 1f - Math.abs(position)); + + cardView.setScaleX(scaleFactor); + cardView.setScaleY(scaleFactor); + + // a card is the "centered card" until its edge has moved past the center of the recycler + // view. note that we also need to factor in the negative margin. + // Find the edge that is closer to the center. + int edgePosition = + viewCenter < center ? view.getRight() + mCardMarginPx + : view.getLeft() - mCardMarginPx; + + if (Math.abs(viewCenter - center) < mCardCenterToScreenCenterDistancePx) { + int childAdapterPosition = getChildAdapterPosition(view); + if (childAdapterPosition == RecyclerView.NO_POSITION) { + return; + } + mCenteredAdapterPosition = getChildAdapterPosition(view); + mEdgeToCenterDistance = edgePosition - center; + mCardCenterToScreenCenterDistancePx = Math.abs(viewCenter - center); + } + } + + private class CardCarouselScrollListener extends OnScrollListener { + + private int mOldState = -1; + + @Override + public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { + if (newState == RecyclerView.SCROLL_STATE_IDLE && newState != mOldState) { + performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); + } + mOldState = newState; + } + + /** + * Callback method to be invoked when the RecyclerView has been scrolled. This will be + * called after the scroll has completed. + * + * <p>This callback will also be called if visible item range changes after a layout + * calculation. In that case, dx and dy will be 0. + * + * @param recyclerView The RecyclerView which scrolled. + * @param dx The amount of horizontal scroll. + * @param dy The amount of vertical scroll. + */ + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + mCenteredAdapterPosition = RecyclerView.NO_POSITION; + mEdgeToCenterDistance = Float.MAX_VALUE; + mCardCenterToScreenCenterDistancePx = Float.MAX_VALUE; + for (int i = 0; i < getChildCount(); i++) { + updateCardView(getChildAt(i)); + } + if (mCenteredAdapterPosition == RecyclerView.NO_POSITION || dx == 0) { + return; + } + + int nextAdapterPosition = + mCenteredAdapterPosition + (mEdgeToCenterDistance > 0 ? 1 : -1); + if (nextAdapterPosition < 0 + || nextAdapterPosition >= mWalletCardCarouselAdapter.mData.size()) { + return; + } + + // Update the label text based on the currently selected card and the next one + WalletCardViewInfo centerCard = + mWalletCardCarouselAdapter.mData.get(mCenteredAdapterPosition); + WalletCardViewInfo nextCard = mWalletCardCarouselAdapter.mData.get(nextAdapterPosition); + float percentDistanceFromCenter = + Math.abs(mEdgeToCenterDistance) / mCardEdgeToCenterDistance; + mCardScrollListener.onCardScroll(centerCard, nextCard, percentDistanceFromCenter); + } + } + + private class CarouselSnapHelper extends PagerSnapHelper { + + private static final float MILLISECONDS_PER_INCH = 200.0F; + private static final int MAX_SCROLL_ON_FLING_DURATION = 80; // ms + + @Override + public View findSnapView(LayoutManager layoutManager) { + View view = super.findSnapView(layoutManager); + if (view == null) { + // implementation decides not to snap + return null; + } + WalletCardViewHolder viewHolder = (WalletCardViewHolder) view.getTag(); + WalletCardViewInfo card = viewHolder.mCardViewInfo; + mSelectionListener.onCardSelected(card); + mCardScrollListener.onCardScroll(card, card, 0); + return view; + } + + /** + * The default SnapScroller is a little sluggish + */ + @Override + protected LinearSmoothScroller createScroller(LayoutManager layoutManager) { + return new LinearSmoothScroller(getContext()) { + @Override + protected void onTargetFound(View targetView, State state, Action action) { + int[] snapDistances = calculateDistanceToFinalSnap(layoutManager, targetView); + final int dx = snapDistances[0]; + final int dy = snapDistances[1]; + final int time = calculateTimeForDeceleration( + Math.max(Math.abs(dx), Math.abs(dy))); + if (time > 0) { + action.update(dx, dy, time, mDecelerateInterpolator); + } + } + + @Override + protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) { + return MILLISECONDS_PER_INCH / displayMetrics.densityDpi; + } + + @Override + protected int calculateTimeForScrolling(int dx) { + return Math.min(MAX_SCROLL_ON_FLING_DURATION, + super.calculateTimeForScrolling(dx)); + } + }; + } + } + + private class WalletCardCarouselAdapter extends Adapter<WalletCardViewHolder> { + + private List<WalletCardViewInfo> mData = Collections.EMPTY_LIST; + + @NonNull + @Override + public WalletCardViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) { + LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext()); + View view = inflater.inflate(R.layout.wallet_card_view, viewGroup, false); + WalletCardViewHolder viewHolder = new WalletCardViewHolder(view); + CardView cardView = viewHolder.mCardView; + cardView.setRadius(mCornerRadiusPx); + ViewGroup.LayoutParams layoutParams = cardView.getLayoutParams(); + layoutParams.width = mCardWidthPx; + layoutParams.height = mCardHeightPx; + view.setTag(viewHolder); + return viewHolder; + } + + @Override + public void onBindViewHolder(@NonNull WalletCardViewHolder viewHolder, int position) { + WalletCardViewInfo cardViewInfo = mData.get(position); + viewHolder.mCardViewInfo = cardViewInfo; + if (cardViewInfo.getCardId().isEmpty()) { + viewHolder.mImageView.setScaleType(ImageView.ScaleType.CENTER); + } + viewHolder.mImageView.setImageDrawable(cardViewInfo.getCardDrawable()); + viewHolder.mCardView.setContentDescription(cardViewInfo.getContentDescription()); + viewHolder.mCardView.setOnClickListener( + v -> { + if (position != mCenteredAdapterPosition) { + smoothScrollToPosition(position); + } else { + mSelectionListener.onCardClicked(cardViewInfo); + } + }); + if (mNumCardsToAnimate > 0 && (position - mCardAnimationStartPosition < 2)) { + mNumCardsToAnimate--; + int startDelay = (position - mCardAnimationStartPosition) * CARD_ANIM_ALPHA_DELAY + + mExtraAnimationDelay; + viewHolder.itemView.setAlpha(0f); + viewHolder.itemView.animate().alpha(1f) + .setStartDelay(Math.max(0, startDelay)) + .setDuration(CARD_ANIM_ALPHA_DURATION).start(); + } + } + + @Override + public int getItemCount() { + return mData.size(); + } + + @Override + public long getItemId(int position) { + return mData.get(position).getCardId().hashCode(); + } + + void setData(List<WalletCardViewInfo> data) { + mData = data; + notifyDataSetChanged(); + } + } + + private class CardCarouselAccessibilityDelegate extends RecyclerViewAccessibilityDelegate { + + private CardCarouselAccessibilityDelegate(@NonNull RecyclerView recyclerView) { + super(recyclerView); + } + + @Override + public boolean onRequestSendAccessibilityEvent( + ViewGroup viewGroup, View view, AccessibilityEvent accessibilityEvent) { + int eventType = accessibilityEvent.getEventType(); + if (eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) { + scrollToPosition(getChildAdapterPosition(view)); + } + return super.onRequestSendAccessibilityEvent(viewGroup, view, accessibilityEvent); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletCardView.java b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletCardView.java new file mode 100644 index 000000000000..fc1adc37d09b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletCardView.java @@ -0,0 +1,54 @@ +/* + * 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 android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.util.AttributeSet; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.cardview.widget.CardView; + +import com.android.systemui.R; + +/** Customized card view of the wallet card carousel. */ +public class WalletCardView extends CardView { + private final Paint mBorderPaint; + + public WalletCardView(@NonNull Context context) { + this(context, null); + } + + public WalletCardView(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + mBorderPaint = new Paint(); + mBorderPaint.setColor(context.getColor(R.color.wallet_card_border)); + mBorderPaint.setStrokeWidth( + context.getResources().getDimension(R.dimen.wallet_card_border_width)); + mBorderPaint.setStyle(Paint.Style.STROKE); + mBorderPaint.setAntiAlias(true); + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + float radius = getRadius(); + canvas.drawRoundRect(0, 0, getWidth(), getHeight(), radius, radius, mBorderPaint); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletCardViewHolder.java b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletCardViewHolder.java new file mode 100644 index 000000000000..3197976456e3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletCardViewHolder.java @@ -0,0 +1,42 @@ +/* + * 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 android.view.View; +import android.widget.ImageView; + +import androidx.cardview.widget.CardView; +import androidx.recyclerview.widget.RecyclerView; + +import com.android.systemui.R; + +/** + * View holder for the quick access wallet card. + */ +class WalletCardViewHolder extends RecyclerView.ViewHolder { + + final CardView mCardView; + final ImageView mImageView; + WalletCardViewInfo mCardViewInfo; + + WalletCardViewHolder(View view) { + super(view); + mCardView = view.requireViewById(R.id.card); + mImageView = mCardView.requireViewById(R.id.card_image); + } + +} diff --git a/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletCardViewInfo.java b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletCardViewInfo.java new file mode 100644 index 000000000000..669d6664b305 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletCardViewInfo.java @@ -0,0 +1,50 @@ +/* + * 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 android.graphics.drawable.Drawable; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +interface WalletCardViewInfo { + String getCardId(); + + /** + * Image of the card. + */ + @NonNull + Drawable getCardDrawable(); + + /** + * Content description for the card. + */ + @Nullable + CharSequence getContentDescription(); + + /** + * Icon shown above the card. + */ + @Nullable + Drawable getIcon(); + + /** + * Text shown above the card. + */ + @NonNull + CharSequence getLabel(); +} diff --git a/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletScreenController.java b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletScreenController.java new file mode 100644 index 000000000000..a93f0f0ba165 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletScreenController.java @@ -0,0 +1,330 @@ +/* + * 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 android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; +import android.os.Handler; +import android.service.quickaccesswallet.GetWalletCardsError; +import android.service.quickaccesswallet.GetWalletCardsRequest; +import android.service.quickaccesswallet.GetWalletCardsResponse; +import android.service.quickaccesswallet.QuickAccessWalletClient; +import android.service.quickaccesswallet.SelectWalletCardRequest; +import android.service.quickaccesswallet.WalletCard; +import android.service.quickaccesswallet.WalletServiceEvent; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.systemui.R; +import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.settings.UserTracker; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; + +/** Controller for the wallet card carousel screen. */ +public class WalletScreenController implements + WalletCardCarousel.OnSelectionListener, + QuickAccessWalletClient.OnWalletCardsRetrievedCallback, + QuickAccessWalletClient.WalletServiceEventListener { + + private static final String TAG = "WalletScreenCtrl"; + private static final String PREFS_HAS_CARDS = "has_cards"; + private static final String PREFS_WALLET_VIEW_HEIGHT = "wallet_view_height"; + private static final int MAX_CARDS = 10; + private static final long SELECTION_DELAY_MILLIS = TimeUnit.SECONDS.toMillis(30); + + private Context mContext; + private final QuickAccessWalletClient mWalletClient; + private final ActivityStarter mActivityStarter; + private final Executor mExecutor; + private final Handler mHandler; + private final Runnable mSelectionRunnable = this::selectCard; + private final SharedPreferences mPrefs; + private final WalletView mWalletView; + private final WalletCardCarousel mCardCarousel; + + @VisibleForTesting String mSelectedCardId; + @VisibleForTesting boolean mIsDismissed; + private boolean mIsDeviceLocked; + private boolean mHasRegisteredListener; + + public WalletScreenController( + Context context, + WalletView walletView, + QuickAccessWalletClient walletClient, + ActivityStarter activityStarter, + Executor executor, + Handler handler, + UserTracker userTracker, + boolean isDeviceLocked) { + mContext = context; + mWalletClient = walletClient; + mActivityStarter = activityStarter; + mExecutor = executor; + mHandler = handler; + mPrefs = userTracker.getUserContext().getSharedPreferences(TAG, Context.MODE_PRIVATE); + mWalletView = walletView; + mWalletView.setMinimumHeight(getExpectedMinHeight()); + mWalletView.setLayoutParams( + new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + mCardCarousel = mWalletView.getCardCarousel(); + if (mCardCarousel != null) { + mCardCarousel.setSelectionListener(this); + } + + if (!mPrefs.getBoolean(PREFS_HAS_CARDS, false)) { + // The empty state view is shown preemptively when cards were not returned last time + // to decrease perceived latency. + showEmptyStateView(); + } + mIsDeviceLocked = isDeviceLocked; + } + + /** + * Implements {@link QuickAccessWalletClient.OnWalletCardsRetrievedCallback}. Called when cards + * are retrieved successfully from the service. This is called on {@link #mExecutor}. + */ + @Override + public void onWalletCardsRetrieved(@NonNull GetWalletCardsResponse response) { + if (mIsDismissed) { + return; + } + List<WalletCard> walletCards = response.getWalletCards(); + List<WalletCardViewInfo> data = new ArrayList<>(walletCards.size()); + for (WalletCard card : walletCards) { + data.add(new QAWalletCardViewInfo(mContext, card)); + } + mHandler.post(() -> { + if (data.isEmpty()) { + showEmptyStateView(); + } else { + mWalletView.showCardCarousel(data, response.getSelectedIndex(), mIsDeviceLocked); + } + // The empty state view will not be shown preemptively next time if cards were returned + mPrefs.edit().putBoolean(PREFS_HAS_CARDS, !data.isEmpty()).apply(); + removeMinHeightAndRecordHeightOnLayout(); + }); + } + + /** + * Implements {@link QuickAccessWalletClient.OnWalletCardsRetrievedCallback}. Called when there + * is an error during card retrieval. This will be run on the {@link #mExecutor}. + */ + @Override + public void onWalletCardRetrievalError(@NonNull GetWalletCardsError error) { + if (mIsDismissed) { + return; + } + mHandler.post(() -> { + mWalletView.showErrorMessage(error.getMessage()); + }); + } + + /** + * Implements {@link QuickAccessWalletClient.WalletServiceEventListener}. Called when the wallet + * application propagates an event, such as an NFC tap, to the quick access wallet view. + */ + @Override + public void onWalletServiceEvent(WalletServiceEvent event) { + if (mIsDismissed) { + return; + } + switch (event.getEventType()) { + case WalletServiceEvent.TYPE_NFC_PAYMENT_STARTED: + onDismissed(); + break; + case WalletServiceEvent.TYPE_WALLET_CARDS_UPDATED: + queryWalletCards(); + break; + default: + Log.w(TAG, "onWalletServiceEvent: Unknown event type"); + } + } + + @Override + public void onCardSelected(@NonNull WalletCardViewInfo card) { + if (mIsDismissed) { + return; + } + mSelectedCardId = card.getCardId(); + selectCard(); + } + + private void selectCard() { + mHandler.removeCallbacks(mSelectionRunnable); + String selectedCardId = mSelectedCardId; + if (mIsDismissed || selectedCardId == null) { + return; + } + mWalletClient.selectWalletCard(new SelectWalletCardRequest(selectedCardId)); + // Re-selecting the card keeps the connection bound so we continue to get service events + // even if the user keeps it open for a long time. + mHandler.postDelayed(mSelectionRunnable, SELECTION_DELAY_MILLIS); + } + + + + @Override + public void onCardClicked(@NonNull WalletCardViewInfo cardInfo) { + if (!(cardInfo instanceof QAWalletCardViewInfo) + || ((QAWalletCardViewInfo) cardInfo).mWalletCard == null + || ((QAWalletCardViewInfo) cardInfo).mWalletCard.getPendingIntent() == null) { + return; + } + mActivityStarter.startActivity( + ((QAWalletCardViewInfo) cardInfo).mWalletCard.getPendingIntent().getIntent(), + true); + } + + @Override + public void queryWalletCards() { + if (mIsDismissed) { + return; + } + if (!mHasRegisteredListener) { + // Listener is registered even when device is locked. Should only be registered once. + mWalletClient.addWalletServiceEventListener(this); + mHasRegisteredListener = true; + } + + mWalletView.show(); + mWalletView.hideErrorMessage(); + int iconSizePx = mContext.getResources().getDimensionPixelSize(R.dimen.wallet_icon_size); + int cardWidthPx = mCardCarousel.getCardWidthPx(); + int cardHeightPx = mCardCarousel.getCardHeightPx(); + GetWalletCardsRequest request = + new GetWalletCardsRequest(cardWidthPx, cardHeightPx, iconSizePx, MAX_CARDS); + mWalletClient.getWalletCards(mExecutor, request, this); + } + + void onDismissed() { + if (mIsDismissed) { + return; + } + mIsDismissed = true; + mSelectedCardId = null; + mHandler.removeCallbacks(mSelectionRunnable); + mWalletClient.notifyWalletDismissed(); + mWalletClient.removeWalletServiceEventListener(this); + mWalletView.animateDismissal(); + // clear refs to the Wallet Activity + mContext = null; + } + + private void showEmptyStateView() { + Drawable logo = mWalletClient.getLogo(); + CharSequence logoContentDesc = mWalletClient.getServiceLabel(); + CharSequence label = mWalletClient.getShortcutLongLabel(); + Intent intent = mWalletClient.createWalletIntent(); + if (logo == null + || TextUtils.isEmpty(logoContentDesc) + || TextUtils.isEmpty(label) + || intent == null) { + Log.w(TAG, "QuickAccessWalletService manifest entry mis-configured"); + // Issue is not likely to be resolved until manifest entries are enabled. + // Hide wallet feature until then. + mWalletView.hide(); + mPrefs.edit().putInt(PREFS_WALLET_VIEW_HEIGHT, 0).apply(); + } else { + logo.setTint(mContext.getColor(R.color.GM2_grey_900)); + mWalletView.showEmptyStateView( + logo, + logoContentDesc, + label, + v -> mActivityStarter.startActivity(intent, true)); + } + } + + private int getExpectedMinHeight() { + int expectedHeight = mPrefs.getInt(PREFS_WALLET_VIEW_HEIGHT, -1); + if (expectedHeight == -1) { + Resources res = mContext.getResources(); + expectedHeight = res.getDimensionPixelSize(R.dimen.min_wallet_empty_height); + } + return expectedHeight; + } + + private void removeMinHeightAndRecordHeightOnLayout() { + mWalletView.setMinimumHeight(0); + mWalletView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, + int oldLeft, int oldTop, int oldRight, int oldBottom) { + mWalletView.removeOnLayoutChangeListener(this); + mPrefs.edit().putInt(PREFS_WALLET_VIEW_HEIGHT, bottom - top).apply(); + } + }); + } + + @VisibleForTesting + static class QAWalletCardViewInfo implements WalletCardViewInfo { + + private final WalletCard mWalletCard; + private final Drawable mCardDrawable; + private final Drawable mIconDrawable; + + /** + * Constructor is called on background executor, so it is safe to load drawables + * synchronously. + */ + QAWalletCardViewInfo(Context context, WalletCard walletCard) { + mWalletCard = walletCard; + mCardDrawable = mWalletCard.getCardImage().loadDrawable(context); + Icon icon = mWalletCard.getCardIcon(); + mIconDrawable = icon == null ? null : icon.loadDrawable(context); + } + + @Override + public String getCardId() { + return mWalletCard.getCardId(); + } + + @Override + public Drawable getCardDrawable() { + return mCardDrawable; + } + + @Override + public CharSequence getContentDescription() { + return mWalletCard.getContentDescription(); + } + + @Override + public Drawable getIcon() { + return mIconDrawable; + } + + @Override + public CharSequence getLabel() { + return mWalletCard.getCardLabel(); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletView.java b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletView.java new file mode 100644 index 000000000000..d2f0720fa66b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletView.java @@ -0,0 +1,242 @@ +/* + * 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 static com.android.systemui.wallet.ui.WalletCardCarousel.CARD_ANIM_ALPHA_DELAY; +import static com.android.systemui.wallet.ui.WalletCardCarousel.CARD_ANIM_ALPHA_DURATION; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.annotation.Nullable; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.ViewGroup; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.systemui.R; + +import java.util.List; + +/** Layout for the wallet screen. */ +public class WalletView extends FrameLayout implements WalletCardCarousel.OnCardScrollListener { + + private static final int CAROUSEL_IN_ANIMATION_DURATION = 300; + private static final int CAROUSEL_OUT_ANIMATION_DURATION = 200; + private static final int CARD_LABEL_ANIM_DELAY = 133; + private static final int CONTACTLESS_ICON_SIZE = 90; + + private final WalletCardCarousel mCardCarousel; + private final ImageView mIcon; + private final TextView mCardLabel; + private final Button mWalletButton; + private final Interpolator mInInterpolator; + private final Interpolator mOutInterpolator; + private final float mAnimationTranslationX; + private final ViewGroup mCardCarouselContainer; + private final TextView mErrorView; + private final ViewGroup mEmptyStateView; + private CharSequence mCenterCardText; + + public WalletView(Context context) { + this(context, null); + } + + public WalletView(Context context, AttributeSet attrs) { + super(context, attrs); + inflate(context, R.layout.wallet_fullscreen, this); + mCardCarouselContainer = requireViewById(R.id.card_carousel_container); + mCardCarousel = requireViewById(R.id.card_carousel); + mCardCarousel.setCardScrollListener(this); + mIcon = requireViewById(R.id.icon); + mCardLabel = requireViewById(R.id.label); + mWalletButton = requireViewById(R.id.wallet_button); + mErrorView = requireViewById(R.id.error_view); + mEmptyStateView = requireViewById(R.id.wallet_empty_state); + mInInterpolator = + AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_slow_in); + mOutInterpolator = + AnimationUtils.loadInterpolator(context, android.R.interpolator.accelerate_cubic); + mAnimationTranslationX = mCardCarousel.getCardWidthPx() / 4f; + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + mCardCarousel.setExpectedViewWidth(getWidth()); + } + + @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); + } + + @Override + public void onCardScroll(WalletCardViewInfo centerCard, WalletCardViewInfo nextCard, + float percentDistanceFromCenter) { + CharSequence centerCardText = centerCard.getLabel(); + Drawable icon = centerCard.getIcon(); + if (icon != null) { + mIcon.setImageDrawable(resizeDrawable(getResources(), icon)); + mIcon.setVisibility(VISIBLE); + } else { + mIcon.setVisibility(INVISIBLE); + } + if (!TextUtils.equals(mCenterCardText, centerCardText)) { + mCenterCardText = centerCardText; + mCardLabel.setText(centerCardText); + } + if (TextUtils.equals(centerCardText, nextCard.getLabel())) { + mCardLabel.setAlpha(1f); + } else { + mCardLabel.setAlpha(percentDistanceFromCenter); + mIcon.setAlpha(percentDistanceFromCenter); + } + } + + void showCardCarousel( + List<WalletCardViewInfo> data, int selectedIndex, boolean isDeviceLocked) { + boolean shouldAnimate = mCardCarousel.setData(data, selectedIndex); + mCardCarouselContainer.setVisibility(VISIBLE); + mErrorView.setVisibility(GONE); + if (isDeviceLocked) { + // TODO(b/182964813): Add click action to prompt device unlock. + mWalletButton.setText(R.string.wallet_button_label_device_locked); + } else { + mWalletButton.setText(R.string.wallet_button_label_device_unlocked); + } + if (shouldAnimate) { + // If the empty state is visible, animate it away and delay the card carousel animation + int emptyStateAnimDelay = 0; + if (mEmptyStateView.getVisibility() == VISIBLE) { + emptyStateAnimDelay = CARD_ANIM_ALPHA_DURATION; + mEmptyStateView.animate() + .alpha(0) + .setDuration(emptyStateAnimDelay) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mEmptyStateView.setVisibility(GONE); + } + }) + .start(); + } + mCardLabel.setAlpha(0f); + mCardLabel.animate().alpha(1f) + .setStartDelay(CARD_LABEL_ANIM_DELAY + emptyStateAnimDelay) + .setDuration(CARD_ANIM_ALPHA_DURATION) + .start(); + mCardCarousel.setExtraAnimationDelay(emptyStateAnimDelay); + mCardCarousel.setTranslationX(mAnimationTranslationX); + mCardCarousel.animate().translationX(0) + .setInterpolator(mInInterpolator) + .setDuration(CAROUSEL_IN_ANIMATION_DURATION) + .setStartDelay(emptyStateAnimDelay) + .start(); + } + } + + void animateDismissal() { + if (mCardCarouselContainer.getVisibility() != VISIBLE) { + return; + } + mCardCarousel.animate().translationX(mAnimationTranslationX) + .setInterpolator(mOutInterpolator) + .setDuration(CAROUSEL_OUT_ANIMATION_DURATION) + .start(); + mCardCarouselContainer.animate() + .alpha(0f) + .setDuration(CARD_ANIM_ALPHA_DURATION) + .setStartDelay(CARD_ANIM_ALPHA_DELAY) + .start(); + } + + void showEmptyStateView(Drawable logo, CharSequence logoContentDescription, CharSequence label, + OnClickListener clickListener) { + mEmptyStateView.setVisibility(VISIBLE); + mErrorView.setVisibility(GONE); + mCardCarouselContainer.setVisibility(GONE); + ImageView logoView = mEmptyStateView.requireViewById(R.id.empty_state_icon); + logoView.setImageDrawable(logo); + logoView.setContentDescription(logoContentDescription); + mEmptyStateView.<TextView>requireViewById(R.id.empty_state_title).setText(label); + mEmptyStateView.setOnClickListener(clickListener); + } + + void showErrorMessage(@Nullable CharSequence message) { + if (TextUtils.isEmpty(message)) { + message = getResources().getText(R.string.wallet_error_generic); + } + mErrorView.setText(message); + mErrorView.setVisibility(VISIBLE); + mCardCarouselContainer.setVisibility(GONE); + mEmptyStateView.setVisibility(GONE); + } + + void hide() { + setVisibility(GONE); + } + + void show() { + setVisibility(VISIBLE); + } + + void hideErrorMessage() { + mErrorView.setVisibility(GONE); + } + + WalletCardCarousel getCardCarousel() { + return mCardCarousel; + } + + Button getWalletButton() { + return mWalletButton; + } + + @VisibleForTesting + TextView getErrorView() { + return mErrorView; + } + + @VisibleForTesting + ViewGroup getEmptyStateView() { + return mEmptyStateView; + } + + @VisibleForTesting + ViewGroup getCardCarouselContainer() { + return mCardCarouselContainer; + } + + private static Drawable resizeDrawable(Resources resources, Drawable drawable) { + Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap(); + return new BitmapDrawable(resources, Bitmap.createScaledBitmap( + bitmap, CONTACTLESS_ICON_SIZE, CONTACTLESS_ICON_SIZE, true)); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/TvPipModule.java b/packages/SystemUI/src/com/android/systemui/wmshell/TvPipModule.java index 82dad68f238c..2216a915b0d9 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/TvPipModule.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/TvPipModule.java @@ -34,6 +34,7 @@ import com.android.wm.shell.pip.PipAnimationController; import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; import com.android.wm.shell.pip.PipMediaController; +import com.android.wm.shell.pip.PipSnapAlgorithm; import com.android.wm.shell.pip.PipSurfaceTransactionHelper; import com.android.wm.shell.pip.PipTaskOrganizer; import com.android.wm.shell.pip.PipTransitionController; @@ -85,9 +86,15 @@ public abstract class TvPipModule { @WMSingleton @Provides - static PipBoundsAlgorithm providePipBoundsHandler(Context context, - PipBoundsState pipBoundsState) { - return new PipBoundsAlgorithm(context, pipBoundsState); + static PipSnapAlgorithm providePipSnapAlgorithm() { + return new PipSnapAlgorithm(); + } + + @WMSingleton + @Provides + static PipBoundsAlgorithm providePipBoundsAlgorithm(Context context, + PipBoundsState pipBoundsState, PipSnapAlgorithm pipSnapAlgorithm) { + return new PipBoundsAlgorithm(context, pipBoundsState, pipSnapAlgorithm); } @WMSingleton diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java index 4eb75ebe4553..74b79d57adcb 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java @@ -296,6 +296,11 @@ public final class WMShell extends SystemUI } oneHanded.stopOneHanded(); } + + @Override + public void onUserSwitchComplete(int userId) { + oneHanded.onUserSwitch(userId); + } }; mKeyguardUpdateMonitor.registerCallback(mOneHandedKeyguardCallback); diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java index e403cfaee34a..45b0b59cd1d9 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java @@ -416,6 +416,7 @@ public abstract class WMShellBaseModule { static ShellInitImpl provideShellInitImpl(DisplayImeController displayImeController, DragAndDropController dragAndDropController, ShellTaskOrganizer shellTaskOrganizer, + Optional<BubbleController> bubblesOptional, Optional<LegacySplitScreenController> legacySplitScreenOptional, Optional<SplitScreenController> splitScreenOptional, Optional<AppPairsController> appPairsOptional, @@ -427,6 +428,7 @@ public abstract class WMShellBaseModule { return new ShellInitImpl(displayImeController, dragAndDropController, shellTaskOrganizer, + bubblesOptional, legacySplitScreenOptional, splitScreenOptional, appPairsOptional, diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShellModule.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShellModule.java index d5183f85ad13..33b2d67e462e 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShellModule.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShellModule.java @@ -37,11 +37,13 @@ import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.common.annotations.ChoreographerSfVsync; import com.android.wm.shell.common.annotations.ShellMainThread; import com.android.wm.shell.legacysplitscreen.LegacySplitScreenController; +import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.pip.Pip; import com.android.wm.shell.pip.PipAnimationController; import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; import com.android.wm.shell.pip.PipMediaController; +import com.android.wm.shell.pip.PipSnapAlgorithm; import com.android.wm.shell.pip.PipSurfaceTransactionHelper; import com.android.wm.shell.pip.PipTaskOrganizer; import com.android.wm.shell.pip.PipTransition; @@ -50,6 +52,7 @@ import com.android.wm.shell.pip.PipUiEventLogger; import com.android.wm.shell.pip.phone.PhonePipMenuController; import com.android.wm.shell.pip.phone.PipAppOpsListener; import com.android.wm.shell.pip.phone.PipController; +import com.android.wm.shell.pip.phone.PipMotionHelper; import com.android.wm.shell.pip.phone.PipTouchHandler; import com.android.wm.shell.transition.Transitions; @@ -123,11 +126,12 @@ public class WMShellModule { PipTouchHandler pipTouchHandler, PipTransitionController pipTransitionController, WindowManagerShellWrapper windowManagerShellWrapper, TaskStackListenerImpl taskStackListener, + Optional<OneHandedController> oneHandedController, @ShellMainThread ShellExecutor mainExecutor) { return Optional.ofNullable(PipController.create(context, displayController, pipAppOpsListener, pipBoundsAlgorithm, pipBoundsState, pipMediaController, phonePipMenuController, pipTaskOrganizer, pipTouchHandler, pipTransitionController, - windowManagerShellWrapper, taskStackListener, mainExecutor)); + windowManagerShellWrapper, taskStackListener, oneHandedController, mainExecutor)); } @WMSingleton @@ -138,9 +142,15 @@ public class WMShellModule { @WMSingleton @Provides - static PipBoundsAlgorithm providesPipBoundsHandler(Context context, - PipBoundsState pipBoundsState) { - return new PipBoundsAlgorithm(context, pipBoundsState); + static PipSnapAlgorithm providePipSnapAlgorithm() { + return new PipSnapAlgorithm(); + } + + @WMSingleton + @Provides + static PipBoundsAlgorithm providesPipBoundsAlgorithm(Context context, + PipBoundsState pipBoundsState, PipSnapAlgorithm pipSnapAlgorithm) { + return new PipBoundsAlgorithm(context, pipBoundsState, pipSnapAlgorithm); } // Handler is used by Icon.loadDrawableAsync @@ -160,12 +170,12 @@ public class WMShellModule { PhonePipMenuController menuPhoneController, PipBoundsAlgorithm pipBoundsAlgorithm, PipBoundsState pipBoundsState, PipTaskOrganizer pipTaskOrganizer, - PipTransitionController pipTransitionController, + PipMotionHelper pipMotionHelper, FloatingContentCoordinator floatingContentCoordinator, PipUiEventLogger pipUiEventLogger, @ShellMainThread ShellExecutor mainExecutor) { return new PipTouchHandler(context, menuPhoneController, pipBoundsAlgorithm, - pipBoundsState, pipTaskOrganizer, pipTransitionController, + pipBoundsState, pipTaskOrganizer, pipMotionHelper, floatingContentCoordinator, pipUiEventLogger, mainExecutor); } @@ -206,4 +216,16 @@ public class WMShellModule { return new PipTransition(context, pipBoundsState, pipMenuController, pipBoundsAlgorithm, pipAnimationController, transitions, shellTaskOrganizer); } + + @WMSingleton + @Provides + static PipMotionHelper providePipMotionHelper(Context context, + PipBoundsState pipBoundsState, PipTaskOrganizer pipTaskOrganizer, + PhonePipMenuController menuController, PipSnapAlgorithm pipSnapAlgorithm, + PipTransitionController pipTransitionController, + FloatingContentCoordinator floatingContentCoordinator) { + return new PipMotionHelper(context, pipBoundsState, pipTaskOrganizer, + menuController, pipSnapAlgorithm, pipTransitionController, + floatingContentCoordinator); + } } |