diff options
15 files changed, 1650 insertions, 40 deletions
diff --git a/packages/SystemUI/res/layout/keyguard_bottom_area.xml b/packages/SystemUI/res/layout/keyguard_bottom_area.xml index 12dfa1042dd7..8f8993f3c8d9 100644 --- a/packages/SystemUI/res/layout/keyguard_bottom_area.xml +++ b/packages/SystemUI/res/layout/keyguard_bottom_area.xml @@ -59,6 +59,26 @@ </LinearLayout> + <com.android.systemui.statusbar.KeyguardAffordanceView + android:id="@+id/camera_button" + android:layout_height="@dimen/keyguard_affordance_height" + android:layout_width="@dimen/keyguard_affordance_width" + android:layout_gravity="bottom|end" + android:src="@drawable/ic_camera_alt_24dp" + android:scaleType="center" + android:contentDescription="@string/accessibility_camera_button" + android:tint="?attr/wallpaperTextColor" /> + + <com.android.systemui.statusbar.KeyguardAffordanceView + android:id="@+id/left_button" + android:layout_height="@dimen/keyguard_affordance_height" + android:layout_width="@dimen/keyguard_affordance_width" + android:layout_gravity="bottom|start" + android:src="@*android:drawable/ic_phone" + android:scaleType="center" + android:contentDescription="@string/accessibility_phone_button" + android:tint="?attr/wallpaperTextColor" /> + <ImageView android:id="@+id/wallet_button" android:layout_height="@dimen/keyguard_affordance_fixed_height" diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 771973c36053..eff4e00fd07b 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -37,6 +37,12 @@ <item>400</item> </integer-array> + <!-- Show mic or phone affordance on Keyguard --> + <bool name="config_keyguardShowLeftAffordance">false</bool> + + <!-- Show camera affordance on Keyguard --> + <bool name="config_keyguardShowCameraAffordance">false</bool> + <!-- decay duration (from size_max -> size), in ms --> <integer name="navigation_bar_deadzone_hold">333</integer> <integer name="navigation_bar_deadzone_decay">333</integer> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 7f7b8716d27c..4d365412828e 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -676,6 +676,9 @@ <!-- The minimum background radius when swiping to a side for the camera / phone affordances. --> <dimen name="keyguard_affordance_min_background_radius">30dp</dimen> + <!-- The size of the touch targets on the keyguard for the affordances. --> + <dimen name="keyguard_affordance_touch_target_size">120dp</dimen> + <!-- The grow amount for the camera and phone circles when hinting --> <dimen name="hint_grow_amount_sideways">60dp</dimen> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index ef672f3a6213..343ec4f67964 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -313,6 +313,13 @@ <string name="accessibility_scanning_face">Scanning face</string> <!-- Click action label for accessibility for the smart reply buttons (not shown on-screen).". [CHAR LIMIT=NONE] --> <string name="accessibility_send_smart_reply">Send</string> + <!-- Content description of the manage notification button for accessibility (not shown on the screen). [CHAR LIMIT=NONE] --> + <!-- Click action label for accessibility for the phone button. [CHAR LIMIT=NONE] --> + <string name="phone_label">open phone</string> + <!-- Click action label for accessibility for the voice assist button. This is not shown on-screen and is an accessibility label for the icon which launches the voice assist from the lock screen.[CHAR LIMIT=NONE] --> + <string name="voice_assist_label">open voice assist</string> + <!-- Click action label for accessibility for the phone button. [CHAR LIMIT=NONE] --> + <string name="camera_label">open camera</string> <!-- Button name for "Cancel". [CHAR LIMIT=NONE] --> <string name="cancel">Cancel</string> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java index d25bbbd2a591..2c22bc68c198 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java @@ -427,6 +427,12 @@ public interface CentralSurfaces extends Dumpable, ActivityStarter, LifecycleOwn void onHintFinished(); + void onCameraHintStarted(); + + void onVoiceAssistHintStarted(); + + void onPhoneHintStarted(); + void onTrackingStopped(boolean expand); // TODO: Figure out way to remove these. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java index 38c37f03f643..9060d5f67913 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java @@ -388,7 +388,8 @@ public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callba if (mStatusBarKeyguardViewManager.isBouncerShowing()) { mStatusBarKeyguardViewManager.reset(true /* hide */); } - mNotificationPanelViewController.launchCamera(source); + mNotificationPanelViewController.launchCamera( + mCentralSurfaces.isDeviceInteractive() /* animate */, source); mCentralSurfaces.updateScrimController(); } else { // We need to defer the camera launch until the screen comes on, since otherwise diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index 4f99deaaf799..5181af773d9a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -1285,6 +1285,8 @@ public class CentralSurfacesImpl extends CoreStartable implements backdrop.setScaleY(scale); }); + mNotificationPanelViewController.setUserSetupComplete(mUserSetup); + // Set up the quick settings tile panel final View container = mNotificationShadeWindowView.findViewById(R.id.qs_frame); if (container != null) { @@ -3022,7 +3024,8 @@ public class CentralSurfacesImpl extends CoreStartable implements @Override public boolean isInLaunchTransition() { - return mNotificationPanelViewController.isLaunchTransitionFinished(); + return mNotificationPanelViewController.isLaunchTransitionRunning() + || mNotificationPanelViewController.isLaunchTransitionFinished(); } /** @@ -3054,7 +3057,11 @@ public class CentralSurfacesImpl extends CoreStartable implements mCommandQueue.appTransitionStarting(mDisplayId, SystemClock.uptimeMillis(), LightBarTransitionsController.DEFAULT_TINT_ANIMATION_DURATION, true); }; - hideRunnable.run(); + if (mNotificationPanelViewController.isLaunchTransitionRunning()) { + mNotificationPanelViewController.setLaunchTransitionEndRunnable(hideRunnable); + } else { + hideRunnable.run(); + } } private void cancelAfterLaunchTransitionRunnables() { @@ -3063,6 +3070,7 @@ public class CentralSurfacesImpl extends CoreStartable implements } mLaunchTransitionEndRunnable = null; mLaunchTransitionCancelRunnable = null; + mNotificationPanelViewController.setLaunchTransitionEndRunnable(null); } /** @@ -3491,6 +3499,24 @@ public class CentralSurfacesImpl extends CoreStartable implements } @Override + public void onCameraHintStarted() { + mFalsingCollector.onCameraHintStarted(); + mKeyguardIndicationController.showTransientIndication(R.string.camera_hint); + } + + @Override + public void onVoiceAssistHintStarted() { + mFalsingCollector.onLeftAffordanceHintStarted(); + mKeyguardIndicationController.showTransientIndication(R.string.voice_hint); + } + + @Override + public void onPhoneHintStarted() { + mFalsingCollector.onLeftAffordanceHintStarted(); + mKeyguardIndicationController.showTransientIndication(R.string.phone_hint); + } + + @Override public void onTrackingStopped(boolean expand) { } @@ -3674,7 +3700,8 @@ public class CentralSurfacesImpl extends CoreStartable implements mWakeUpCoordinator.setFullyAwake(true); mWakeUpCoordinator.setWakingUp(false); if (mLaunchCameraWhenFinishedWaking) { - mNotificationPanelViewController.launchCamera(mLastCameraLaunchSource); + mNotificationPanelViewController.launchCamera( + false /* animate */, mLastCameraLaunchSource); mLaunchCameraWhenFinishedWaking = false; } if (mLaunchEmergencyActionWhenFinishedWaking) { @@ -4316,6 +4343,9 @@ public class CentralSurfacesImpl extends CoreStartable implements if (!mUserSetup) { animateCollapseQuickSettings(); } + if (mNotificationPanelViewController != null) { + mNotificationPanelViewController.setUserSetupComplete(mUserSetup); + } updateQsExpansionEnabled(); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardAffordanceHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardAffordanceHelper.java new file mode 100644 index 000000000000..2922b4c98d34 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardAffordanceHelper.java @@ -0,0 +1,587 @@ +/* + * Copyright (C) 2014 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; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.content.Context; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; + +import com.android.systemui.R; +import com.android.systemui.animation.Interpolators; +import com.android.systemui.classifier.Classifier; +import com.android.systemui.plugins.FalsingManager; +import com.android.systemui.statusbar.KeyguardAffordanceView; +import com.android.wm.shell.animation.FlingAnimationUtils; + +/** + * A touch handler of the keyguard which is responsible for launching phone and camera affordances. + */ +public class KeyguardAffordanceHelper { + + public static final long HINT_PHASE1_DURATION = 200; + private static final long HINT_PHASE2_DURATION = 350; + private static final float BACKGROUND_RADIUS_SCALE_FACTOR = 0.25f; + private static final int HINT_CIRCLE_OPEN_DURATION = 500; + + private final Context mContext; + private final Callback mCallback; + + private FlingAnimationUtils mFlingAnimationUtils; + private VelocityTracker mVelocityTracker; + private boolean mSwipingInProgress; + private float mInitialTouchX; + private float mInitialTouchY; + private float mTranslation; + private float mTranslationOnDown; + private int mTouchSlop; + private int mMinTranslationAmount; + private int mMinFlingVelocity; + private int mHintGrowAmount; + private KeyguardAffordanceView mLeftIcon; + private KeyguardAffordanceView mRightIcon; + private Animator mSwipeAnimator; + private final FalsingManager mFalsingManager; + private int mMinBackgroundRadius; + private boolean mMotionCancelled; + private int mTouchTargetSize; + private View mTargetedView; + private boolean mTouchSlopExeeded; + private AnimatorListenerAdapter mFlingEndListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mSwipeAnimator = null; + mSwipingInProgress = false; + mTargetedView = null; + } + }; + private Runnable mAnimationEndRunnable = new Runnable() { + @Override + public void run() { + mCallback.onAnimationToSideEnded(); + } + }; + + KeyguardAffordanceHelper(Callback callback, Context context, FalsingManager falsingManager) { + mContext = context; + mCallback = callback; + initIcons(); + updateIcon(mLeftIcon, 0.0f, mLeftIcon.getRestingAlpha(), false, false, true, false); + updateIcon(mRightIcon, 0.0f, mRightIcon.getRestingAlpha(), false, false, true, false); + mFalsingManager = falsingManager; + initDimens(); + } + + private void initDimens() { + final ViewConfiguration configuration = ViewConfiguration.get(mContext); + mTouchSlop = configuration.getScaledPagingTouchSlop(); + mMinFlingVelocity = configuration.getScaledMinimumFlingVelocity(); + mMinTranslationAmount = mContext.getResources().getDimensionPixelSize( + R.dimen.keyguard_min_swipe_amount); + mMinBackgroundRadius = mContext.getResources().getDimensionPixelSize( + R.dimen.keyguard_affordance_min_background_radius); + mTouchTargetSize = mContext.getResources().getDimensionPixelSize( + R.dimen.keyguard_affordance_touch_target_size); + mHintGrowAmount = + mContext.getResources().getDimensionPixelSize(R.dimen.hint_grow_amount_sideways); + mFlingAnimationUtils = new FlingAnimationUtils(mContext.getResources().getDisplayMetrics(), + 0.4f); + } + + private void initIcons() { + mLeftIcon = mCallback.getLeftIcon(); + mRightIcon = mCallback.getRightIcon(); + updatePreviews(); + } + + public void updatePreviews() { + mLeftIcon.setPreviewView(mCallback.getLeftPreview()); + mRightIcon.setPreviewView(mCallback.getRightPreview()); + } + + public boolean onTouchEvent(MotionEvent event) { + int action = event.getActionMasked(); + if (mMotionCancelled && action != MotionEvent.ACTION_DOWN) { + return false; + } + final float y = event.getY(); + final float x = event.getX(); + + boolean isUp = false; + switch (action) { + case MotionEvent.ACTION_DOWN: + View targetView = getIconAtPosition(x, y); + if (targetView == null || (mTargetedView != null && mTargetedView != targetView)) { + mMotionCancelled = true; + return false; + } + if (mTargetedView != null) { + cancelAnimation(); + } else { + mTouchSlopExeeded = false; + } + startSwiping(targetView); + mInitialTouchX = x; + mInitialTouchY = y; + mTranslationOnDown = mTranslation; + initVelocityTracker(); + trackMovement(event); + mMotionCancelled = false; + break; + case MotionEvent.ACTION_POINTER_DOWN: + mMotionCancelled = true; + endMotion(true /* forceSnapBack */, x, y); + break; + case MotionEvent.ACTION_MOVE: + trackMovement(event); + float xDist = x - mInitialTouchX; + float yDist = y - mInitialTouchY; + float distance = (float) Math.hypot(xDist, yDist); + if (!mTouchSlopExeeded && distance > mTouchSlop) { + mTouchSlopExeeded = true; + } + if (mSwipingInProgress) { + if (mTargetedView == mRightIcon) { + distance = mTranslationOnDown - distance; + distance = Math.min(0, distance); + } else { + distance = mTranslationOnDown + distance; + distance = Math.max(0, distance); + } + setTranslation(distance, false /* isReset */, false /* animateReset */); + } + break; + + case MotionEvent.ACTION_UP: + isUp = true; + case MotionEvent.ACTION_CANCEL: + boolean hintOnTheRight = mTargetedView == mRightIcon; + trackMovement(event); + endMotion(!isUp, x, y); + if (!mTouchSlopExeeded && isUp) { + mCallback.onIconClicked(hintOnTheRight); + } + break; + } + return true; + } + + private void startSwiping(View targetView) { + mCallback.onSwipingStarted(targetView == mRightIcon); + mSwipingInProgress = true; + mTargetedView = targetView; + } + + private View getIconAtPosition(float x, float y) { + if (leftSwipePossible() && isOnIcon(mLeftIcon, x, y)) { + return mLeftIcon; + } + if (rightSwipePossible() && isOnIcon(mRightIcon, x, y)) { + return mRightIcon; + } + return null; + } + + public boolean isOnAffordanceIcon(float x, float y) { + return isOnIcon(mLeftIcon, x, y) || isOnIcon(mRightIcon, x, y); + } + + private boolean isOnIcon(View icon, float x, float y) { + float iconX = icon.getX() + icon.getWidth() / 2.0f; + float iconY = icon.getY() + icon.getHeight() / 2.0f; + double distance = Math.hypot(x - iconX, y - iconY); + return distance <= mTouchTargetSize / 2; + } + + private void endMotion(boolean forceSnapBack, float lastX, float lastY) { + if (mSwipingInProgress) { + flingWithCurrentVelocity(forceSnapBack, lastX, lastY); + } else { + mTargetedView = null; + } + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + private boolean rightSwipePossible() { + return mRightIcon.getVisibility() == View.VISIBLE; + } + + private boolean leftSwipePossible() { + return mLeftIcon.getVisibility() == View.VISIBLE; + } + + public boolean onInterceptTouchEvent(MotionEvent ev) { + return false; + } + + public void startHintAnimation(boolean right, + Runnable onFinishedListener) { + cancelAnimation(); + startHintAnimationPhase1(right, onFinishedListener); + } + + private void startHintAnimationPhase1(final boolean right, final Runnable onFinishedListener) { + final KeyguardAffordanceView targetView = right ? mRightIcon : mLeftIcon; + ValueAnimator animator = getAnimatorToRadius(right, mHintGrowAmount); + animator.addListener(new AnimatorListenerAdapter() { + private boolean mCancelled; + + @Override + public void onAnimationCancel(Animator animation) { + mCancelled = true; + } + + @Override + public void onAnimationEnd(Animator animation) { + if (mCancelled) { + mSwipeAnimator = null; + mTargetedView = null; + onFinishedListener.run(); + } else { + startUnlockHintAnimationPhase2(right, onFinishedListener); + } + } + }); + animator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN); + animator.setDuration(HINT_PHASE1_DURATION); + animator.start(); + mSwipeAnimator = animator; + mTargetedView = targetView; + } + + /** + * Phase 2: Move back. + */ + private void startUnlockHintAnimationPhase2(boolean right, final Runnable onFinishedListener) { + ValueAnimator animator = getAnimatorToRadius(right, 0); + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mSwipeAnimator = null; + mTargetedView = null; + onFinishedListener.run(); + } + }); + animator.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN); + animator.setDuration(HINT_PHASE2_DURATION); + animator.setStartDelay(HINT_CIRCLE_OPEN_DURATION); + animator.start(); + mSwipeAnimator = animator; + } + + private ValueAnimator getAnimatorToRadius(final boolean right, int radius) { + final KeyguardAffordanceView targetView = right ? mRightIcon : mLeftIcon; + ValueAnimator animator = ValueAnimator.ofFloat(targetView.getCircleRadius(), radius); + animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + float newRadius = (float) animation.getAnimatedValue(); + targetView.setCircleRadiusWithoutAnimation(newRadius); + float translation = getTranslationFromRadius(newRadius); + mTranslation = right ? -translation : translation; + updateIconsFromTranslation(targetView); + } + }); + return animator; + } + + private void cancelAnimation() { + if (mSwipeAnimator != null) { + mSwipeAnimator.cancel(); + } + } + + private void flingWithCurrentVelocity(boolean forceSnapBack, float lastX, float lastY) { + float vel = getCurrentVelocity(lastX, lastY); + + // We snap back if the current translation is not far enough + boolean snapBack = false; + if (mCallback.needsAntiFalsing()) { + snapBack = snapBack || mFalsingManager.isFalseTouch( + mTargetedView == mRightIcon + ? Classifier.RIGHT_AFFORDANCE : Classifier.LEFT_AFFORDANCE); + } + snapBack = snapBack || isBelowFalsingThreshold(); + + // or if the velocity is in the opposite direction. + boolean velIsInWrongDirection = vel * mTranslation < 0; + snapBack |= Math.abs(vel) > mMinFlingVelocity && velIsInWrongDirection; + vel = snapBack ^ velIsInWrongDirection ? 0 : vel; + fling(vel, snapBack || forceSnapBack, mTranslation < 0); + } + + private boolean isBelowFalsingThreshold() { + return Math.abs(mTranslation) < Math.abs(mTranslationOnDown) + getMinTranslationAmount(); + } + + private int getMinTranslationAmount() { + float factor = mCallback.getAffordanceFalsingFactor(); + return (int) (mMinTranslationAmount * factor); + } + + private void fling(float vel, final boolean snapBack, boolean right) { + float target = right ? -mCallback.getMaxTranslationDistance() + : mCallback.getMaxTranslationDistance(); + target = snapBack ? 0 : target; + + ValueAnimator animator = ValueAnimator.ofFloat(mTranslation, target); + mFlingAnimationUtils.apply(animator, mTranslation, target, vel); + animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + mTranslation = (float) animation.getAnimatedValue(); + } + }); + animator.addListener(mFlingEndListener); + if (!snapBack) { + startFinishingCircleAnimation(vel * 0.375f, mAnimationEndRunnable, right); + mCallback.onAnimationToSideStarted(right, mTranslation, vel); + } else { + reset(true); + } + animator.start(); + mSwipeAnimator = animator; + if (snapBack) { + mCallback.onSwipingAborted(); + } + } + + private void startFinishingCircleAnimation(float velocity, Runnable animationEndRunnable, + boolean right) { + KeyguardAffordanceView targetView = right ? mRightIcon : mLeftIcon; + targetView.finishAnimation(velocity, animationEndRunnable); + } + + private void setTranslation(float translation, boolean isReset, boolean animateReset) { + translation = rightSwipePossible() ? translation : Math.max(0, translation); + translation = leftSwipePossible() ? translation : Math.min(0, translation); + float absTranslation = Math.abs(translation); + if (translation != mTranslation || isReset) { + KeyguardAffordanceView targetView = translation > 0 ? mLeftIcon : mRightIcon; + KeyguardAffordanceView otherView = translation > 0 ? mRightIcon : mLeftIcon; + float alpha = absTranslation / getMinTranslationAmount(); + + // We interpolate the alpha of the other icons to 0 + float fadeOutAlpha = 1.0f - alpha; + fadeOutAlpha = Math.max(fadeOutAlpha, 0.0f); + + boolean animateIcons = isReset && animateReset; + boolean forceNoCircleAnimation = isReset && !animateReset; + float radius = getRadiusFromTranslation(absTranslation); + boolean slowAnimation = isReset && isBelowFalsingThreshold(); + if (!isReset) { + updateIcon(targetView, radius, alpha + fadeOutAlpha * targetView.getRestingAlpha(), + false, false, false, false); + } else { + updateIcon(targetView, 0.0f, fadeOutAlpha * targetView.getRestingAlpha(), + animateIcons, slowAnimation, true /* isReset */, forceNoCircleAnimation); + } + updateIcon(otherView, 0.0f, fadeOutAlpha * otherView.getRestingAlpha(), + animateIcons, slowAnimation, isReset, forceNoCircleAnimation); + + mTranslation = translation; + } + } + + private void updateIconsFromTranslation(KeyguardAffordanceView targetView) { + float absTranslation = Math.abs(mTranslation); + float alpha = absTranslation / getMinTranslationAmount(); + + // We interpolate the alpha of the other icons to 0 + float fadeOutAlpha = 1.0f - alpha; + fadeOutAlpha = Math.max(0.0f, fadeOutAlpha); + + // We interpolate the alpha of the targetView to 1 + KeyguardAffordanceView otherView = targetView == mRightIcon ? mLeftIcon : mRightIcon; + updateIconAlpha(targetView, alpha + fadeOutAlpha * targetView.getRestingAlpha(), false); + updateIconAlpha(otherView, fadeOutAlpha * otherView.getRestingAlpha(), false); + } + + private float getTranslationFromRadius(float circleSize) { + float translation = (circleSize - mMinBackgroundRadius) + / BACKGROUND_RADIUS_SCALE_FACTOR; + return translation > 0.0f ? translation + mTouchSlop : 0.0f; + } + + private float getRadiusFromTranslation(float translation) { + if (translation <= mTouchSlop) { + return 0.0f; + } + return (translation - mTouchSlop) * BACKGROUND_RADIUS_SCALE_FACTOR + mMinBackgroundRadius; + } + + public void animateHideLeftRightIcon() { + cancelAnimation(); + updateIcon(mRightIcon, 0f, 0f, true, false, false, false); + updateIcon(mLeftIcon, 0f, 0f, true, false, false, false); + } + + private void updateIcon(KeyguardAffordanceView view, float circleRadius, float alpha, + boolean animate, boolean slowRadiusAnimation, boolean force, + boolean forceNoCircleAnimation) { + if (view.getVisibility() != View.VISIBLE && !force) { + return; + } + if (forceNoCircleAnimation) { + view.setCircleRadiusWithoutAnimation(circleRadius); + } else { + view.setCircleRadius(circleRadius, slowRadiusAnimation); + } + updateIconAlpha(view, alpha, animate); + } + + private void updateIconAlpha(KeyguardAffordanceView view, float alpha, boolean animate) { + float scale = getScale(alpha, view); + alpha = Math.min(1.0f, alpha); + view.setImageAlpha(alpha, animate); + view.setImageScale(scale, animate); + } + + private float getScale(float alpha, KeyguardAffordanceView icon) { + float scale = alpha / icon.getRestingAlpha() * 0.2f + + KeyguardAffordanceView.MIN_ICON_SCALE_AMOUNT; + return Math.min(scale, KeyguardAffordanceView.MAX_ICON_SCALE_AMOUNT); + } + + private void trackMovement(MotionEvent event) { + if (mVelocityTracker != null) { + mVelocityTracker.addMovement(event); + } + } + + private void initVelocityTracker() { + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + } + mVelocityTracker = VelocityTracker.obtain(); + } + + private float getCurrentVelocity(float lastX, float lastY) { + if (mVelocityTracker == null) { + return 0; + } + mVelocityTracker.computeCurrentVelocity(1000); + float aX = mVelocityTracker.getXVelocity(); + float aY = mVelocityTracker.getYVelocity(); + float bX = lastX - mInitialTouchX; + float bY = lastY - mInitialTouchY; + float bLen = (float) Math.hypot(bX, bY); + // Project the velocity onto the distance vector: a * b / |b| + float projectedVelocity = (aX * bX + aY * bY) / bLen; + if (mTargetedView == mRightIcon) { + projectedVelocity = -projectedVelocity; + } + return projectedVelocity; + } + + public void onConfigurationChanged() { + initDimens(); + initIcons(); + } + + public void onRtlPropertiesChanged() { + initIcons(); + } + + public void reset(boolean animate) { + cancelAnimation(); + setTranslation(0.0f, true /* isReset */, animate); + mMotionCancelled = true; + if (mSwipingInProgress) { + mCallback.onSwipingAborted(); + mSwipingInProgress = false; + } + } + + public boolean isSwipingInProgress() { + return mSwipingInProgress; + } + + public void launchAffordance(boolean animate, boolean left) { + if (mSwipingInProgress) { + // We don't want to mess with the state if the user is actually swiping already. + return; + } + KeyguardAffordanceView targetView = left ? mLeftIcon : mRightIcon; + KeyguardAffordanceView otherView = left ? mRightIcon : mLeftIcon; + startSwiping(targetView); + + // Do not animate the circle expanding if the affordance isn't visible, + // otherwise the circle will be meaningless. + if (targetView.getVisibility() != View.VISIBLE) { + animate = false; + } + + if (animate) { + fling(0, false, !left); + updateIcon(otherView, 0.0f, 0, true, false, true, false); + } else { + mCallback.onAnimationToSideStarted(!left, mTranslation, 0); + mTranslation = left ? mCallback.getMaxTranslationDistance() + : mCallback.getMaxTranslationDistance(); + updateIcon(otherView, 0.0f, 0.0f, false, false, true, false); + targetView.instantFinishAnimation(); + mFlingEndListener.onAnimationEnd(null); + mAnimationEndRunnable.run(); + } + } + + public interface Callback { + + /** + * Notifies the callback when an animation to a side page was started. + * + * @param rightPage Is the page animated to the right page? + */ + void onAnimationToSideStarted(boolean rightPage, float translation, float vel); + + /** + * Notifies the callback the animation to a side page has ended. + */ + void onAnimationToSideEnded(); + + float getMaxTranslationDistance(); + + void onSwipingStarted(boolean rightIcon); + + void onSwipingAborted(); + + void onIconClicked(boolean rightIcon); + + KeyguardAffordanceView getLeftIcon(); + + KeyguardAffordanceView getRightIcon(); + + View getLeftPreview(); + + View getRightPreview(); + + /** + * @return The factor the minimum swipe amount should be multiplied with. + */ + float getAffordanceFalsingFactor(); + + boolean needsAntiFalsing(); + } +} 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 52c04b9de29a..b43e9df79241 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java @@ -16,30 +16,47 @@ package com.android.systemui.statusbar.phone; +import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK; +import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; + import static com.android.systemui.controls.dagger.ControlsComponent.Visibility.AVAILABLE; import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset; +import static com.android.systemui.tuner.LockscreenFragment.LOCKSCREEN_LEFT_BUTTON; +import static com.android.systemui.tuner.LockscreenFragment.LOCKSCREEN_LEFT_UNLOCK; +import static com.android.systemui.tuner.LockscreenFragment.LOCKSCREEN_RIGHT_BUTTON; +import static com.android.systemui.tuner.LockscreenFragment.LOCKSCREEN_RIGHT_UNLOCK; import static com.android.systemui.wallet.controller.QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE; import static com.android.systemui.wallet.controller.QuickAccessWalletController.WalletChangeEvent.WALLET_PREFERENCE_CHANGE; +import android.app.ActivityOptions; import android.app.ActivityTaskManager; import android.app.admin.DevicePolicyManager; +import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; import android.content.res.ColorStateList; import android.content.res.Configuration; import android.graphics.drawable.Drawable; +import android.os.AsyncTask; +import android.os.Bundle; import android.os.RemoteException; import android.os.UserHandle; import android.service.quickaccesswallet.GetWalletCardsError; import android.service.quickaccesswallet.GetWalletCardsResponse; import android.service.quickaccesswallet.QuickAccessWalletClient; +import android.telecom.TelecomManager; +import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; import android.util.TypedValue; import android.view.View; import android.view.ViewGroup; import android.view.WindowInsets; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; @@ -47,35 +64,81 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.widget.LockPatternUtils; +import com.android.keyguard.KeyguardUpdateMonitor; +import com.android.keyguard.KeyguardUpdateMonitorCallback; import com.android.settingslib.Utils; +import com.android.systemui.ActivityIntentHelper; import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.animation.ActivityLaunchAnimator; import com.android.systemui.animation.Interpolators; +import com.android.systemui.assist.AssistManager; +import com.android.systemui.camera.CameraIntents; +import com.android.systemui.controls.ControlsServiceInfo; import com.android.systemui.controls.dagger.ControlsComponent; import com.android.systemui.controls.management.ControlsListingController; import com.android.systemui.controls.ui.ControlsActivity; import com.android.systemui.controls.ui.ControlsUiController; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.FalsingManager; +import com.android.systemui.plugins.IntentButtonProvider; +import com.android.systemui.plugins.IntentButtonProvider.IntentButton; +import com.android.systemui.plugins.IntentButtonProvider.IntentButton.IconState; import com.android.systemui.qrcodescanner.controller.QRCodeScannerController; +import com.android.systemui.statusbar.KeyguardAffordanceView; +import com.android.systemui.statusbar.policy.AccessibilityController; +import com.android.systemui.statusbar.policy.ExtensionController; +import com.android.systemui.statusbar.policy.ExtensionController.Extension; +import com.android.systemui.statusbar.policy.FlashlightController; import com.android.systemui.statusbar.policy.KeyguardStateController; +import com.android.systemui.statusbar.policy.PreviewInflater; +import com.android.systemui.tuner.LockscreenFragment.LockButtonFactory; +import com.android.systemui.tuner.TunerService; import com.android.systemui.wallet.controller.QuickAccessWalletController; +import java.util.List; + /** * Implementation for the bottom area of the Keyguard, including camera/phone affordance and status * text. */ -public class KeyguardBottomAreaView extends FrameLayout { +public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickListener, + KeyguardStateController.Callback, + AccessibilityController.AccessibilityStateChangedCallback { + + final static String TAG = "CentralSurfaces/KeyguardBottomAreaView"; + + public static final String CAMERA_LAUNCH_SOURCE_AFFORDANCE = "lockscreen_affordance"; + public static final String CAMERA_LAUNCH_SOURCE_WIGGLE = "wiggle_gesture"; + public static final String CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP = "power_double_tap"; + public static final String CAMERA_LAUNCH_SOURCE_LIFT_TRIGGER = "lift_to_launch_ml"; + + public static final String EXTRA_CAMERA_LAUNCH_SOURCE + = "com.android.systemui.camera_launch_source"; + + private static final String LEFT_BUTTON_PLUGIN + = "com.android.systemui.action.PLUGIN_LOCKSCREEN_LEFT_BUTTON"; + private static final String RIGHT_BUTTON_PLUGIN + = "com.android.systemui.action.PLUGIN_LOCKSCREEN_RIGHT_BUTTON"; - private static final String TAG = "CentralSurfaces/KeyguardBottomAreaView"; + 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; + // TODO(b/179494051): May no longer be needed + private final boolean mShowLeftAffordance; + private final boolean mShowCameraAffordance; + + private KeyguardAffordanceView mRightAffordanceView; + private KeyguardAffordanceView mLeftAffordanceView; + private ImageView mWalletButton; private ImageView mQRCodeScannerButton; private ImageView mControlsButton; private boolean mHasCard = false; - private final WalletCardRetriever mCardRetriever = new WalletCardRetriever(); + private WalletCardRetriever mCardRetriever = new WalletCardRetriever(); private QuickAccessWalletController mQuickAccessWalletController; private QRCodeScannerController mQRCodeScannerController; private ControlsComponent mControlsComponent; @@ -85,42 +148,54 @@ public class KeyguardBottomAreaView extends FrameLayout { private ViewGroup mIndicationArea; private TextView mIndicationText; private TextView mIndicationTextBottom; + private ViewGroup mPreviewContainer; private ViewGroup mOverlayContainer; + private View mLeftPreview; + private View mCameraPreview; + private ActivityStarter mActivityStarter; private KeyguardStateController mKeyguardStateController; + private FlashlightController mFlashlightController; + private PreviewInflater mPreviewInflater; + private AccessibilityController mAccessibilityController; private CentralSurfaces mCentralSurfaces; + private KeyguardAffordanceHelper mAffordanceHelper; private FalsingManager mFalsingManager; + private boolean mUserSetupComplete; + private boolean mLeftIsVoiceAssist; + private Drawable mLeftAssistIcon; + + private IntentButton mRightButton = new DefaultRightButton(); + private Extension<IntentButton> mRightExtension; + private String mRightButtonStr; + private IntentButton mLeftButton = new DefaultLeftButton(); + private Extension<IntentButton> mLeftExtension; + private String mLeftButtonStr; private boolean mDozing; private int mIndicationBottomMargin; private int mIndicationPadding; private float mDarkAmount; private int mBurnInXOffset; private int mBurnInYOffset; - - private final ControlsListingController.ControlsListingCallback mListingCallback = - serviceInfos -> post(() -> { - boolean available = !serviceInfos.isEmpty(); - - if (available != mControlServicesAvailable) { - mControlServicesAvailable = available; - updateControlsVisibility(); - updateAffordanceColors(); + private ActivityIntentHelper mActivityIntentHelper; + private KeyguardUpdateMonitor mKeyguardUpdateMonitor; + + private ControlsListingController.ControlsListingCallback mListingCallback = + new ControlsListingController.ControlsListingCallback() { + public void onServicesUpdated(List<ControlsServiceInfo> serviceInfos) { + post(() -> { + boolean available = !serviceInfos.isEmpty(); + + if (available != mControlServicesAvailable) { + mControlServicesAvailable = available; + updateControlsVisibility(); + updateAffordanceColors(); + } + }); } - }); - - private final KeyguardStateController.Callback mKeyguardStateCallback = - new KeyguardStateController.Callback() { - @Override - public void onKeyguardShowingChanged() { - if (mKeyguardStateController.isShowing()) { - if (mQuickAccessWalletController != null) { - mQuickAccessWalletController.queryWalletCards(mCardRetriever); - } - } - } - }; + }; public KeyguardBottomAreaView(Context context) { this(context, null); @@ -137,8 +212,43 @@ public class KeyguardBottomAreaView extends FrameLayout { public KeyguardBottomAreaView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); + mShowLeftAffordance = getResources().getBoolean(R.bool.config_keyguardShowLeftAffordance); + mShowCameraAffordance = getResources() + .getBoolean(R.bool.config_keyguardShowCameraAffordance); } + private AccessibilityDelegate mAccessibilityDelegate = new AccessibilityDelegate() { + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + String label = null; + if (host == mRightAffordanceView) { + label = getResources().getString(R.string.camera_label); + } else if (host == mLeftAffordanceView) { + if (mLeftIsVoiceAssist) { + label = getResources().getString(R.string.voice_assist_label); + } else { + label = getResources().getString(R.string.phone_label); + } + } + info.addAction(new AccessibilityAction(ACTION_CLICK, label)); + } + + @Override + public boolean performAccessibilityAction(View host, int action, Bundle args) { + if (action == ACTION_CLICK) { + if (host == mRightAffordanceView) { + launchCamera(CAMERA_LAUNCH_SOURCE_AFFORDANCE); + return true; + } else if (host == mLeftAffordanceView) { + launchLeftAffordance(); + return true; + } + } + return super.performAccessibilityAction(host, action, args); + } + }; + public void initFrom(KeyguardBottomAreaView oldBottomArea) { setCentralSurfaces(oldBottomArea.mCentralSurfaces); @@ -169,7 +279,11 @@ public class KeyguardBottomAreaView extends FrameLayout { @Override protected void onFinishInflate() { super.onFinishInflate(); + mPreviewInflater = new PreviewInflater(mContext, new LockPatternUtils(mContext), + new ActivityIntentHelper(mContext)); mOverlayContainer = findViewById(R.id.overlay_container); + mRightAffordanceView = findViewById(R.id.camera_button); + mLeftAffordanceView = findViewById(R.id.left_button); mWalletButton = findViewById(R.id.wallet_button); mQRCodeScannerButton = findViewById(R.id.qr_code_scanner_button); mControlsButton = findViewById(R.id.controls_button); @@ -181,11 +295,18 @@ public class KeyguardBottomAreaView extends FrameLayout { R.dimen.keyguard_indication_margin_bottom); mBurnInYOffset = getResources().getDimensionPixelSize( R.dimen.default_burn_in_prevention_offset); + updateCameraVisibility(); mKeyguardStateController = Dependency.get(KeyguardStateController.class); - mKeyguardStateController.addCallback(mKeyguardStateCallback); + mKeyguardStateController.addCallback(this); setClipChildren(false); setClipToPadding(false); + mRightAffordanceView.setOnClickListener(this); + mLeftAffordanceView.setOnClickListener(this); + initAccessibility(); mActivityStarter = Dependency.get(ActivityStarter.class); + mFlashlightController = Dependency.get(FlashlightController.class); + mAccessibilityController = Dependency.get(AccessibilityController.class); + mActivityIntentHelper = new ActivityIntentHelper(getContext()); mIndicationPadding = getResources().getDimensionPixelSize( R.dimen.keyguard_indication_area_padding); @@ -194,18 +315,51 @@ public class KeyguardBottomAreaView extends FrameLayout { updateControlsVisibility(); } + /** + * Set the container where the previews are rendered. + */ + public void setPreviewContainer(ViewGroup previewContainer) { + mPreviewContainer = previewContainer; + inflateCameraPreview(); + updateLeftAffordance(); + } + @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); + mAccessibilityController.addStateChangedCallback(this); + mRightExtension = Dependency.get(ExtensionController.class).newExtension(IntentButton.class) + .withPlugin(IntentButtonProvider.class, RIGHT_BUTTON_PLUGIN, + p -> p.getIntentButton()) + .withTunerFactory(new LockButtonFactory(mContext, LOCKSCREEN_RIGHT_BUTTON)) + .withDefault(() -> new DefaultRightButton()) + .withCallback(button -> setRightButton(button)) + .build(); + mLeftExtension = Dependency.get(ExtensionController.class).newExtension(IntentButton.class) + .withPlugin(IntentButtonProvider.class, LEFT_BUTTON_PLUGIN, + p -> p.getIntentButton()) + .withTunerFactory(new LockButtonFactory(mContext, LOCKSCREEN_LEFT_BUTTON)) + .withDefault(() -> new DefaultLeftButton()) + .withCallback(button -> setLeftButton(button)) + .build(); final IntentFilter filter = new IntentFilter(); filter.addAction(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED); - mKeyguardStateController.addCallback(mKeyguardStateCallback); + getContext().registerReceiverAsUser(mDevicePolicyReceiver, + UserHandle.ALL, filter, null, null); + mKeyguardUpdateMonitor = Dependency.get(KeyguardUpdateMonitor.class); + mKeyguardUpdateMonitor.registerCallback(mUpdateMonitorCallback); + mKeyguardStateController.addCallback(this); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); - mKeyguardStateController.removeCallback(mKeyguardStateCallback); + mKeyguardStateController.removeCallback(this); + mAccessibilityController.removeStateChangedCallback(this); + mRightExtension.destroy(); + mLeftExtension.destroy(); + getContext().unregisterReceiver(mDevicePolicyReceiver); + mKeyguardUpdateMonitor.removeCallback(mUpdateMonitorCallback); if (mQuickAccessWalletController != null) { mQuickAccessWalletController.unregisterWalletChangeObservers( @@ -224,6 +378,11 @@ public class KeyguardBottomAreaView extends FrameLayout { } } + private void initAccessibility() { + mLeftAffordanceView.setAccessibilityDelegate(mAccessibilityDelegate); + mRightAffordanceView.setAccessibilityDelegate(mAccessibilityDelegate); + } + @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); @@ -245,7 +404,19 @@ public class KeyguardBottomAreaView extends FrameLayout { getResources().getDimensionPixelSize( com.android.internal.R.dimen.text_size_small_material)); - ViewGroup.LayoutParams lp = mWalletButton.getLayoutParams(); + ViewGroup.LayoutParams lp = mRightAffordanceView.getLayoutParams(); + lp.width = getResources().getDimensionPixelSize(R.dimen.keyguard_affordance_width); + lp.height = getResources().getDimensionPixelSize(R.dimen.keyguard_affordance_height); + mRightAffordanceView.setLayoutParams(lp); + updateRightAffordanceIcon(); + + lp = mLeftAffordanceView.getLayoutParams(); + lp.width = getResources().getDimensionPixelSize(R.dimen.keyguard_affordance_width); + lp.height = getResources().getDimensionPixelSize(R.dimen.keyguard_affordance_height); + mLeftAffordanceView.setLayoutParams(lp); + updateLeftAffordanceIcon(); + + lp = mWalletButton.getLayoutParams(); lp.width = getResources().getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_width); lp.height = getResources().getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_height); mWalletButton.setLayoutParams(lp); @@ -268,8 +439,74 @@ public class KeyguardBottomAreaView extends FrameLayout { updateAffordanceColors(); } + private void updateRightAffordanceIcon() { + IconState state = mRightButton.getIcon(); + mRightAffordanceView.setVisibility(!mDozing && state.isVisible ? View.VISIBLE : View.GONE); + if (state.drawable != mRightAffordanceView.getDrawable() + || state.tint != mRightAffordanceView.shouldTint()) { + mRightAffordanceView.setImageDrawable(state.drawable, state.tint); + } + mRightAffordanceView.setContentDescription(state.contentDescription); + } + public void setCentralSurfaces(CentralSurfaces centralSurfaces) { mCentralSurfaces = centralSurfaces; + updateCameraVisibility(); // in case onFinishInflate() was called too early + } + + public void setAffordanceHelper(KeyguardAffordanceHelper affordanceHelper) { + mAffordanceHelper = affordanceHelper; + } + + public void setUserSetupComplete(boolean userSetupComplete) { + mUserSetupComplete = userSetupComplete; + updateCameraVisibility(); + updateLeftAffordanceIcon(); + } + + private Intent getCameraIntent() { + return mRightButton.getIntent(); + } + + /** + * Resolves the intent to launch the camera application. + */ + public ResolveInfo resolveCameraIntent() { + return mContext.getPackageManager().resolveActivityAsUser(getCameraIntent(), + PackageManager.MATCH_DEFAULT_ONLY, + KeyguardUpdateMonitor.getCurrentUser()); + } + + private void updateCameraVisibility() { + if (mRightAffordanceView == null) { + // Things are not set up yet; reply hazy, ask again later + return; + } + mRightAffordanceView.setVisibility(!mDozing && mShowCameraAffordance + && mRightButton.getIcon().isVisible ? View.VISIBLE : View.GONE); + } + + /** + * Set an alternate icon for the left assist affordance (replace the mic icon) + */ + public void setLeftAssistIcon(Drawable drawable) { + mLeftAssistIcon = drawable; + updateLeftAffordanceIcon(); + } + + private void updateLeftAffordanceIcon() { + if (!mShowLeftAffordance || mDozing) { + mLeftAffordanceView.setVisibility(GONE); + return; + } + + IconState state = mLeftButton.getIcon(); + mLeftAffordanceView.setVisibility(state.isVisible ? View.VISIBLE : View.GONE); + if (state.drawable != mLeftAffordanceView.getDrawable() + || state.tint != mLeftAffordanceView.shouldTint()) { + mLeftAffordanceView.setImageDrawable(state.drawable, state.tint); + } + mLeftAffordanceView.setContentDescription(state.contentDescription); } private void updateWalletVisibility() { @@ -315,6 +552,73 @@ public class KeyguardBottomAreaView extends FrameLayout { } } + public boolean isLeftVoiceAssist() { + return mLeftIsVoiceAssist; + } + + private boolean isPhoneVisible() { + PackageManager pm = mContext.getPackageManager(); + return pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY) + && pm.resolveActivity(PHONE_INTENT, 0) != null; + } + + @Override + public void onStateChanged(boolean accessibilityEnabled, boolean touchExplorationEnabled) { + mRightAffordanceView.setClickable(touchExplorationEnabled); + mLeftAffordanceView.setClickable(touchExplorationEnabled); + mRightAffordanceView.setFocusable(accessibilityEnabled); + mLeftAffordanceView.setFocusable(accessibilityEnabled); + } + + @Override + public void onClick(View v) { + if (v == mRightAffordanceView) { + launchCamera(CAMERA_LAUNCH_SOURCE_AFFORDANCE); + } else if (v == mLeftAffordanceView) { + launchLeftAffordance(); + } + } + + public void launchCamera(String source) { + final Intent intent = getCameraIntent(); + intent.putExtra(EXTRA_CAMERA_LAUNCH_SOURCE, source); + boolean wouldLaunchResolverActivity = mActivityIntentHelper.wouldLaunchResolverActivity( + intent, KeyguardUpdateMonitor.getCurrentUser()); + if (CameraIntents.isSecureCameraIntent(intent) && !wouldLaunchResolverActivity) { + AsyncTask.execute(new Runnable() { + @Override + public void run() { + // 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 + // the default animation is used. This doesn't look good for + // the camera (as it rotates the camera contents out of sync + // with physical reality). So, we ask the WindowManager to + // force the crossfade animation if an orientation change + // happens to occur during the launch. + ActivityOptions o = ActivityOptions.makeBasic(); + o.setDisallowEnterPictureInPictureWhileLaunching(true); + o.setRotationAnimationHint( + WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS); + try { + ActivityTaskManager.getService().startActivityAsUser( + null, getContext().getBasePackageName(), + getContext().getAttributionTag(), intent, + intent.resolveTypeIfNeeded(getContext().getContentResolver()), + null, null, 0, Intent.FLAG_ACTIVITY_NEW_TASK, null, o.toBundle(), + UserHandle.CURRENT.getIdentifier()); + } catch (RemoteException e) { + Log.w(TAG, "Unable to start camera activity", e); + } + } + }); + } else { + // We need to delay starting the activity because ResolverActivity finishes itself if + // launched behind lockscreen. + mActivityStarter.startActivity(intent, false /* dismissShade */); + } + } + public void setDarkAmount(float darkAmount) { if (darkAmount == mDarkAmount) { return; @@ -323,6 +627,77 @@ public class KeyguardBottomAreaView extends FrameLayout { dozeTimeTick(); } + public void launchLeftAffordance() { + if (mLeftIsVoiceAssist) { + launchVoiceAssist(); + } else { + launchPhone(); + } + } + + @VisibleForTesting + void launchVoiceAssist() { + Runnable runnable = new Runnable() { + @Override + public void run() { + Dependency.get(AssistManager.class).launchVoiceAssistFromKeyguard(); + } + }; + if (!mKeyguardStateController.canDismissLockScreen()) { + Dependency.get(Dependency.BACKGROUND_EXECUTOR).execute(runnable); + } else { + boolean dismissShade = !TextUtils.isEmpty(mRightButtonStr) + && Dependency.get(TunerService.class).getValue(LOCKSCREEN_RIGHT_UNLOCK, 1) != 0; + mCentralSurfaces.executeRunnableDismissingKeyguard(runnable, null /* cancelAction */, + dismissShade, false /* afterKeyguardGone */, true /* deferred */); + } + } + + private boolean canLaunchVoiceAssist() { + return Dependency.get(AssistManager.class).canVoiceAssistBeLaunchedFromKeyguard(); + } + + private void launchPhone() { + final TelecomManager tm = TelecomManager.from(mContext); + if (tm.isInCall()) { + AsyncTask.execute(new Runnable() { + @Override + public void run() { + tm.showInCallScreen(false /* showDialpad */); + } + }); + } else { + boolean dismissShade = !TextUtils.isEmpty(mLeftButtonStr) + && Dependency.get(TunerService.class).getValue(LOCKSCREEN_LEFT_UNLOCK, 1) != 0; + mActivityStarter.startActivity(mLeftButton.getIntent(), dismissShade); + } + } + + + @Override + protected void onVisibilityChanged(View changedView, int visibility) { + super.onVisibilityChanged(changedView, visibility); + if (changedView == this && visibility == VISIBLE) { + updateCameraVisibility(); + } + } + + public KeyguardAffordanceView getLeftView() { + return mLeftAffordanceView; + } + + public KeyguardAffordanceView getRightView() { + return mRightAffordanceView; + } + + public View getLeftPreview() { + return mLeftPreview; + } + + public View getRightPreview() { + return mCameraPreview; + } + public View getIndicationArea() { return mIndicationArea; } @@ -332,6 +707,66 @@ public class KeyguardBottomAreaView extends FrameLayout { return false; } + @Override + public void onUnlockedChanged() { + updateCameraVisibility(); + } + + @Override + public void onKeyguardShowingChanged() { + if (mKeyguardStateController.isShowing()) { + if (mQuickAccessWalletController != null) { + mQuickAccessWalletController.queryWalletCards(mCardRetriever); + } + } + } + + private void inflateCameraPreview() { + if (mPreviewContainer == null) { + return; + } + View previewBefore = mCameraPreview; + boolean visibleBefore = false; + if (previewBefore != null) { + mPreviewContainer.removeView(previewBefore); + visibleBefore = previewBefore.getVisibility() == View.VISIBLE; + } + mCameraPreview = mPreviewInflater.inflatePreview(getCameraIntent()); + if (mCameraPreview != null) { + mPreviewContainer.addView(mCameraPreview); + mCameraPreview.setVisibility(visibleBefore ? View.VISIBLE : View.INVISIBLE); + } + if (mAffordanceHelper != null) { + mAffordanceHelper.updatePreviews(); + } + } + + private void updateLeftPreview() { + if (mPreviewContainer == null) { + return; + } + View previewBefore = mLeftPreview; + if (previewBefore != null) { + mPreviewContainer.removeView(previewBefore); + } + + if (mLeftIsVoiceAssist) { + if (Dependency.get(AssistManager.class).getVoiceInteractorComponentName() != null) { + mLeftPreview = mPreviewInflater.inflatePreviewFromService( + Dependency.get(AssistManager.class).getVoiceInteractorComponentName()); + } + } else { + mLeftPreview = mPreviewInflater.inflatePreview(mLeftButton.getIntent()); + } + if (mLeftPreview != null) { + mPreviewContainer.addView(mLeftPreview); + mLeftPreview.setVisibility(View.INVISIBLE); + } + if (mAffordanceHelper != null) { + mAffordanceHelper.updatePreviews(); + } + } + public void startFinishDozeAnimation() { long delay = 0; if (mWalletButton.getVisibility() == View.VISIBLE) { @@ -343,6 +778,13 @@ public class KeyguardBottomAreaView extends FrameLayout { if (mControlsButton.getVisibility() == View.VISIBLE) { startFinishDozeAnimationElement(mControlsButton, delay); } + if (mLeftAffordanceView.getVisibility() == View.VISIBLE) { + startFinishDozeAnimationElement(mLeftAffordanceView, delay); + delay += DOZE_ANIMATION_STAGGER_DELAY; + } + if (mRightAffordanceView.getVisibility() == View.VISIBLE) { + startFinishDozeAnimationElement(mRightAffordanceView, delay); + } } private void startFinishDozeAnimationElement(View element, long delay) { @@ -356,9 +798,58 @@ public class KeyguardBottomAreaView extends FrameLayout { .setDuration(DOZE_ANIMATION_ELEMENT_DURATION); } + private final BroadcastReceiver mDevicePolicyReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + post(new Runnable() { + @Override + public void run() { + updateCameraVisibility(); + } + }); + } + }; + + private final KeyguardUpdateMonitorCallback mUpdateMonitorCallback = + new KeyguardUpdateMonitorCallback() { + @Override + public void onUserSwitchComplete(int userId) { + updateCameraVisibility(); + } + + @Override + public void onUserUnlocked() { + inflateCameraPreview(); + updateCameraVisibility(); + updateLeftAffordance(); + } + }; + + public void updateLeftAffordance() { + updateLeftAffordanceIcon(); + updateLeftPreview(); + } + + private void setRightButton(IntentButton button) { + mRightButton = button; + updateRightAffordanceIcon(); + updateCameraVisibility(); + inflateCameraPreview(); + } + + private void setLeftButton(IntentButton button) { + mLeftButton = button; + if (!(mLeftButton instanceof DefaultLeftButton)) { + mLeftIsVoiceAssist = false; + } + updateLeftAffordance(); + } + public void setDozing(boolean dozing, boolean animate) { mDozing = dozing; + updateCameraVisibility(); + updateLeftAffordanceIcon(); updateWalletVisibility(); updateControlsVisibility(); updateQRCodeButtonVisibility(); @@ -397,12 +888,77 @@ public class KeyguardBottomAreaView extends FrameLayout { * Sets the alpha of the indication areas and affordances, excluding the lock icon. */ public void setAffordanceAlpha(float alpha) { + mLeftAffordanceView.setAlpha(alpha); + mRightAffordanceView.setAlpha(alpha); mIndicationArea.setAlpha(alpha); mWalletButton.setAlpha(alpha); mQRCodeScannerButton.setAlpha(alpha); mControlsButton.setAlpha(alpha); } + private class DefaultLeftButton implements IntentButton { + + private IconState mIconState = new IconState(); + + @Override + public IconState getIcon() { + mLeftIsVoiceAssist = canLaunchVoiceAssist(); + if (mLeftIsVoiceAssist) { + mIconState.isVisible = mUserSetupComplete && mShowLeftAffordance; + if (mLeftAssistIcon == null) { + mIconState.drawable = mContext.getDrawable(R.drawable.ic_mic_26dp); + } else { + mIconState.drawable = mLeftAssistIcon; + } + mIconState.contentDescription = mContext.getString( + R.string.accessibility_voice_assist_button); + } else { + mIconState.isVisible = mUserSetupComplete && mShowLeftAffordance + && isPhoneVisible(); + mIconState.drawable = mContext.getDrawable( + com.android.internal.R.drawable.ic_phone); + mIconState.contentDescription = mContext.getString( + R.string.accessibility_phone_button); + } + return mIconState; + } + + @Override + public Intent getIntent() { + return PHONE_INTENT; + } + } + + private class DefaultRightButton implements IntentButton { + + private IconState mIconState = new IconState(); + + @Override + public IconState getIcon() { + boolean isCameraDisabled = (mCentralSurfaces != null) + && !mCentralSurfaces.isCameraAllowedByAdmin(); + mIconState.isVisible = !isCameraDisabled + && mShowCameraAffordance + && mUserSetupComplete + && resolveCameraIntent() != null; + mIconState.drawable = mContext.getDrawable(R.drawable.ic_camera_alt_24dp); + mIconState.contentDescription = + mContext.getString(R.string.accessibility_camera_button); + return mIconState; + } + + @Override + public Intent getIntent() { + boolean canDismissLs = mKeyguardStateController.canDismissLockScreen(); + boolean secure = mKeyguardStateController.isMethodSecure(); + if (secure && !canDismissLs) { + return CameraIntents.getSecureCameraIntent(getContext()); + } else { + return CameraIntents.getInsecureCameraIntent(getContext()); + } + } + } + @Override public WindowInsets onApplyWindowInsets(WindowInsets insets) { int bottom = insets.getDisplayCutout() != null 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 c757cba31967..fbbb587872b4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java @@ -50,6 +50,8 @@ import android.app.ActivityManager; import android.app.Fragment; import android.app.StatusBarManager; import android.content.ContentResolver; +import android.content.pm.ResolveInfo; +import android.content.res.Configuration; import android.content.res.Resources; import android.database.ContentObserver; import android.graphics.Canvas; @@ -143,6 +145,7 @@ import com.android.systemui.screenrecord.RecordingController; import com.android.systemui.shared.system.QuickStepContract; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.GestureRecorder; +import com.android.systemui.statusbar.KeyguardAffordanceView; import com.android.systemui.statusbar.KeyguardIndicationController; import com.android.systemui.statusbar.LockscreenShadeTransitionController; import com.android.systemui.statusbar.NotificationRemoteInputManager; @@ -249,6 +252,9 @@ public class NotificationPanelViewController extends PanelViewController { private final OnOverscrollTopChangedListener mOnOverscrollTopChangedListener = new OnOverscrollTopChangedListener(); + private final KeyguardAffordanceHelperCallback + mKeyguardAffordanceHelperCallback = + new KeyguardAffordanceHelperCallback(); private final OnEmptySpaceClickListener mOnEmptySpaceClickListener = new OnEmptySpaceClickListener(); @@ -332,6 +338,8 @@ public class NotificationPanelViewController extends PanelViewController { // Current max allowed keyguard notifications determined by measuring the panel private int mMaxAllowedKeyguardNotifications; + private ViewGroup mPreviewContainer; + private KeyguardAffordanceHelper mAffordanceHelper; private KeyguardQsUserSwitchController mKeyguardQsUserSwitchController; private KeyguardUserSwitcherController mKeyguardUserSwitcherController; private KeyguardStatusBarView mKeyguardStatusBar; @@ -429,6 +437,8 @@ public class NotificationPanelViewController extends PanelViewController { */ private boolean mQsAnimatorExpand; private boolean mIsLaunchTransitionFinished; + private boolean mIsLaunchTransitionRunning; + private Runnable mLaunchAnimationEndRunnable; private boolean mOnlyAffordanceInThisMotion; private ValueAnimator mQsSizeChangeAnimator; @@ -445,8 +455,10 @@ public class NotificationPanelViewController extends PanelViewController { private boolean mClosingWithAlphaFadeOut; private boolean mHeadsUpAnimatingAway; private boolean mLaunchingAffordance; + private boolean mAffordanceHasPreview; private final FalsingManager mFalsingManager; private final FalsingCollector mFalsingCollector; + private String mLastCameraLaunchSource = KeyguardBottomAreaView.CAMERA_LAUNCH_SOURCE_AFFORDANCE; private Runnable mHeadsUpExistenceChangedRunnable = () -> { setHeadsUpAnimatingAway(false); @@ -479,6 +491,7 @@ public class NotificationPanelViewController extends PanelViewController { private float mLinearDarkAmount; private boolean mPulsing; + private boolean mUserSetupComplete; private boolean mHideIconsDuringLaunchAnimation = true; private int mStackScrollerMeasuringPass; /** @@ -995,6 +1008,8 @@ public class NotificationPanelViewController extends PanelViewController { mOnEmptySpaceClickListener); addTrackingHeadsUpListener(mNotificationStackScrollLayoutController::setTrackingHeadsUp); mKeyguardBottomArea = mView.findViewById(R.id.keyguard_bottom_area); + mPreviewContainer = mView.findViewById(R.id.preview_container); + mKeyguardBottomArea.setPreviewContainer(mPreviewContainer); initBottomArea(); @@ -1018,6 +1033,7 @@ public class NotificationPanelViewController extends PanelViewController { mView.setRtlChangeListener(layoutDirection -> { if (layoutDirection != mOldLayoutDirection) { + mAffordanceHelper.onRtlPropertiesChanged(); mOldLayoutDirection = layoutDirection; } }); @@ -1251,6 +1267,7 @@ public class NotificationPanelViewController extends PanelViewController { mKeyguardBottomArea = (KeyguardBottomAreaView) mLayoutInflater.inflate( R.layout.keyguard_bottom_area, mView, false); mKeyguardBottomArea.initFrom(oldBottomArea); + mKeyguardBottomArea.setPreviewContainer(mPreviewContainer); mView.addView(mKeyguardBottomArea, index); initBottomArea(); mKeyguardIndicationController.setIndicationArea(mKeyguardBottomArea); @@ -1287,7 +1304,11 @@ public class NotificationPanelViewController extends PanelViewController { } private void initBottomArea() { + mAffordanceHelper = new KeyguardAffordanceHelper( + mKeyguardAffordanceHelperCallback, mView.getContext(), mFalsingManager); + mKeyguardBottomArea.setAffordanceHelper(mAffordanceHelper); mKeyguardBottomArea.setCentralSurfaces(mCentralSurfaces); + mKeyguardBottomArea.setUserSetupComplete(mUserSetupComplete); mKeyguardBottomArea.setFalsingManager(mFalsingManager); mKeyguardBottomArea.initWallet(mQuickAccessWalletController); mKeyguardBottomArea.initControls(mControlsComponent); @@ -1652,6 +1673,10 @@ public class NotificationPanelViewController extends PanelViewController { public void resetViews(boolean animate) { mIsLaunchTransitionFinished = false; mBlockTouches = false; + if (!mLaunchingAffordance) { + mAffordanceHelper.reset(false); + mLastCameraLaunchSource = KeyguardBottomAreaView.CAMERA_LAUNCH_SOURCE_AFFORDANCE; + } mCentralSurfaces.getGutsManager().closeAndSaveGuts(true /* leavebehind */, true /* force */, true /* controls */, -1 /* x */, -1 /* y */, true /* resetMenu */); if (animate && !isFullyCollapsed()) { @@ -2194,6 +2219,11 @@ public class NotificationPanelViewController extends PanelViewController { return isFullyCollapsed() || mBarState != StatusBarState.SHADE; } + @Override + protected boolean shouldGestureIgnoreXTouchSlop(float x, float y) { + return !mAffordanceHelper.isOnAffordanceIcon(x, y); + } + private void onQsTouch(MotionEvent event) { int pointerIndex = event.findPointerIndex(mTrackingPointer); if (pointerIndex < 0) { @@ -3358,6 +3388,9 @@ public class NotificationPanelViewController extends PanelViewController { mQsExpandImmediate = true; setShowShelfOnly(true); } + if (mBarState == KEYGUARD || mBarState == StatusBarState.SHADE_LOCKED) { + mAffordanceHelper.animateHideLeftRightIcon(); + } mNotificationStackScrollLayoutController.onPanelTrackingStarted(); cancelPendingPanelCollapse(); } @@ -3371,6 +3404,12 @@ public class NotificationPanelViewController extends PanelViewController { true /* animate */); } mNotificationStackScrollLayoutController.onPanelTrackingStopped(); + if (expand && (mBarState == KEYGUARD + || mBarState == StatusBarState.SHADE_LOCKED)) { + if (!mHintAnimationRunning) { + mAffordanceHelper.reset(true); + } + } // If we unlocked from a swipe, the user's finger might still be down after the // unlock animation ends. We need to wait until ACTION_UP to enable blurs again. @@ -3443,6 +3482,10 @@ public class NotificationPanelViewController extends PanelViewController { return mIsLaunchTransitionFinished; } + public boolean isLaunchTransitionRunning() { + return mIsLaunchTransitionRunning; + } + @Override public void setIsLaunchAnimationRunning(boolean running) { boolean wasRunning = mIsLaunchAnimationRunning; @@ -3461,6 +3504,10 @@ public class NotificationPanelViewController extends PanelViewController { } } + public void setLaunchTransitionEndRunnable(Runnable r) { + mLaunchAnimationEndRunnable = r; + } + private void updateDozingVisibilities(boolean animate) { mKeyguardBottomArea.setDozing(mDozing, animate); if (!mDozing && animate) { @@ -3639,8 +3686,30 @@ public class NotificationPanelViewController extends PanelViewController { && mBarState == StatusBarState.SHADE; } - /** Launches the camera. */ - public void launchCamera(int source) { + public void launchCamera(boolean animate, int source) { + if (source == StatusBarManager.CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP) { + mLastCameraLaunchSource = KeyguardBottomAreaView.CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP; + } else if (source == StatusBarManager.CAMERA_LAUNCH_SOURCE_WIGGLE) { + mLastCameraLaunchSource = KeyguardBottomAreaView.CAMERA_LAUNCH_SOURCE_WIGGLE; + } else if (source == StatusBarManager.CAMERA_LAUNCH_SOURCE_LIFT_TRIGGER) { + mLastCameraLaunchSource = KeyguardBottomAreaView.CAMERA_LAUNCH_SOURCE_LIFT_TRIGGER; + } else { + + // Default. + mLastCameraLaunchSource = KeyguardBottomAreaView.CAMERA_LAUNCH_SOURCE_AFFORDANCE; + } + + // If we are launching it when we are occluded already we don't want it to animate, + // nor setting these flags, since the occluded state doesn't change anymore, hence it's + // never reset. + if (!isFullyCollapsed()) { + setLaunchingAffordance(true); + } else { + animate = false; + } + mAffordanceHasPreview = mKeyguardBottomArea.getRightPreview() != null; + mAffordanceHelper.launchAffordance( + animate, mView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL); } public void onAffordanceLaunchEnded() { @@ -3653,6 +3722,9 @@ public class NotificationPanelViewController extends PanelViewController { */ private void setLaunchingAffordance(boolean launchingAffordance) { mLaunchingAffordance = launchingAffordance; + mKeyguardAffordanceHelperCallback.getLeftIcon().setLaunchingAffordance(launchingAffordance); + mKeyguardAffordanceHelperCallback.getRightIcon().setLaunchingAffordance( + launchingAffordance); mKeyguardBypassController.setLaunchingAffordance(launchingAffordance); } @@ -3660,14 +3732,24 @@ public class NotificationPanelViewController extends PanelViewController { * Return true when a bottom affordance is launching an occluded activity with a splash screen. */ public boolean isLaunchingAffordanceWithPreview() { - return mLaunchingAffordance; + return mLaunchingAffordance && mAffordanceHasPreview; } /** * Whether the camera application can be launched for the camera launch gesture. */ public boolean canCameraGestureBeLaunched() { - return false; + if (!mCentralSurfaces.isCameraAllowedByAdmin()) { + return false; + } + + ResolveInfo resolveInfo = mKeyguardBottomArea.resolveCameraIntent(); + String + packageToLaunch = + (resolveInfo == null || resolveInfo.activityInfo == null) ? null + : resolveInfo.activityInfo.packageName; + return packageToLaunch != null && (mBarState != StatusBarState.SHADE || !isForegroundApp( + packageToLaunch)) && !mAffordanceHelper.isSwipingInProgress(); } /** @@ -3756,6 +3838,9 @@ public class NotificationPanelViewController extends PanelViewController { @Override public void setTouchAndAnimationDisabled(boolean disabled) { super.setTouchAndAnimationDisabled(disabled); + if (disabled && mAffordanceHelper.isSwipingInProgress() && !mIsLaunchTransitionRunning) { + mAffordanceHelper.reset(false /* animate */); + } mNotificationStackScrollLayoutController.setAnimationsEnabled(!disabled); } @@ -3838,6 +3923,11 @@ public class NotificationPanelViewController extends PanelViewController { return mKeyguardBottomArea; } + public void setUserSetupComplete(boolean userSetupComplete) { + mUserSetupComplete = userSetupComplete; + mKeyguardBottomArea.setUserSetupComplete(userSetupComplete); + } + public void applyLaunchAnimationProgress(float linearProgress) { boolean hideIcons = LaunchAnimator.getProgress(ActivityLaunchAnimator.TIMINGS, linearProgress, ANIMATION_DELAY_ICON_FADE_IN, 100) == 0.0f; @@ -4191,6 +4281,10 @@ public class NotificationPanelViewController extends PanelViewController { mMetricsLogger.count(COUNTER_PANEL_OPEN_PEEK, 1); } boolean handled = false; + if ((!mIsExpanding || mHintAnimationRunning) && !mQsExpanded + && mBarState != StatusBarState.SHADE && !mDozing) { + handled |= mAffordanceHelper.onTouchEvent(event); + } if (mOnlyAffordanceInThisMotion) { return true; } @@ -4433,6 +4527,139 @@ public class NotificationPanelViewController extends PanelViewController { } } + private class KeyguardAffordanceHelperCallback implements KeyguardAffordanceHelper.Callback { + @Override + public void onAnimationToSideStarted(boolean rightPage, float translation, float vel) { + boolean + start = + mView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL ? rightPage + : !rightPage; + mIsLaunchTransitionRunning = true; + mLaunchAnimationEndRunnable = null; + float displayDensity = mCentralSurfaces.getDisplayDensity(); + int lengthDp = Math.abs((int) (translation / displayDensity)); + int velocityDp = Math.abs((int) (vel / displayDensity)); + if (start) { + mLockscreenGestureLogger.write(MetricsEvent.ACTION_LS_DIALER, lengthDp, velocityDp); + mLockscreenGestureLogger.log(LockscreenUiEvent.LOCKSCREEN_DIALER); + mFalsingCollector.onLeftAffordanceOn(); + if (mFalsingCollector.shouldEnforceBouncer()) { + mCentralSurfaces.executeRunnableDismissingKeyguard( + () -> mKeyguardBottomArea.launchLeftAffordance(), null, + true /* dismissShade */, false /* afterKeyguardGone */, + true /* deferred */); + } else { + mKeyguardBottomArea.launchLeftAffordance(); + } + } else { + if (KeyguardBottomAreaView.CAMERA_LAUNCH_SOURCE_AFFORDANCE.equals( + mLastCameraLaunchSource)) { + mLockscreenGestureLogger.write( + MetricsEvent.ACTION_LS_CAMERA, lengthDp, velocityDp); + mLockscreenGestureLogger.log(LockscreenUiEvent.LOCKSCREEN_CAMERA); + } + mFalsingCollector.onCameraOn(); + if (mFalsingCollector.shouldEnforceBouncer()) { + mCentralSurfaces.executeRunnableDismissingKeyguard( + () -> mKeyguardBottomArea.launchCamera(mLastCameraLaunchSource), null, + true /* dismissShade */, false /* afterKeyguardGone */, + true /* deferred */); + } else { + mKeyguardBottomArea.launchCamera(mLastCameraLaunchSource); + } + } + mCentralSurfaces.startLaunchTransitionTimeout(); + mBlockTouches = true; + } + + @Override + public void onAnimationToSideEnded() { + mIsLaunchTransitionRunning = false; + mIsLaunchTransitionFinished = true; + if (mLaunchAnimationEndRunnable != null) { + mLaunchAnimationEndRunnable.run(); + mLaunchAnimationEndRunnable = null; + } + mCentralSurfaces.readyForKeyguardDone(); + } + + @Override + public float getMaxTranslationDistance() { + return (float) Math.hypot(mView.getWidth(), getHeight()); + } + + @Override + public void onSwipingStarted(boolean rightIcon) { + mFalsingCollector.onAffordanceSwipingStarted(rightIcon); + mView.requestDisallowInterceptTouchEvent(true); + mOnlyAffordanceInThisMotion = true; + mQsTracking = false; + } + + @Override + public void onSwipingAborted() { + mFalsingCollector.onAffordanceSwipingAborted(); + } + + @Override + public void onIconClicked(boolean rightIcon) { + if (mHintAnimationRunning) { + return; + } + mHintAnimationRunning = true; + mAffordanceHelper.startHintAnimation(rightIcon, () -> { + mHintAnimationRunning = false; + mCentralSurfaces.onHintFinished(); + }); + rightIcon = + mView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL ? !rightIcon + : rightIcon; + if (rightIcon) { + mCentralSurfaces.onCameraHintStarted(); + } else { + if (mKeyguardBottomArea.isLeftVoiceAssist()) { + mCentralSurfaces.onVoiceAssistHintStarted(); + } else { + mCentralSurfaces.onPhoneHintStarted(); + } + } + } + + @Override + public KeyguardAffordanceView getLeftIcon() { + return mView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL + ? mKeyguardBottomArea.getRightView() : mKeyguardBottomArea.getLeftView(); + } + + @Override + public KeyguardAffordanceView getRightIcon() { + return mView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL + ? mKeyguardBottomArea.getLeftView() : mKeyguardBottomArea.getRightView(); + } + + @Override + public View getLeftPreview() { + return mView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL + ? mKeyguardBottomArea.getRightPreview() : mKeyguardBottomArea.getLeftPreview(); + } + + @Override + public View getRightPreview() { + return mView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL + ? mKeyguardBottomArea.getLeftPreview() : mKeyguardBottomArea.getRightPreview(); + } + + @Override + public float getAffordanceFalsingFactor() { + return mCentralSurfaces.isWakeUpComingFromTouch() ? 1.5f : 1.0f; + } + + @Override + public boolean needsAntiFalsing() { + return mBarState == KEYGUARD; + } + } + private class OnEmptySpaceClickListener implements NotificationStackScrollLayout.OnEmptySpaceClickListener { @Override @@ -4891,6 +5118,15 @@ public class NotificationPanelViewController extends PanelViewController { } } + private class OnConfigurationChangedListener extends + PanelViewController.OnConfigurationChangedListener { + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + mAffordanceHelper.onConfigurationChanged(); + } + } + private class OnApplyWindowInsetsListener implements View.OnApplyWindowInsetsListener { public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) { // the same types of insets that are handled in NotificationShadeWindowView diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java index d2fc1af010b9..ed12b00cc644 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java @@ -484,6 +484,8 @@ public abstract class PanelViewController { protected abstract boolean shouldGestureWaitForTouchSlop(); + protected abstract boolean shouldGestureIgnoreXTouchSlop(float x, float y); + protected void onTrackingStopped(boolean expand) { mTracking = false; mCentralSurfaces.onTrackingStopped(expand); @@ -1331,7 +1333,7 @@ public abstract class PanelViewController { if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { mGestureWaitForTouchSlop = shouldGestureWaitForTouchSlop(); - mIgnoreXTouchSlop = true; + mIgnoreXTouchSlop = isFullyCollapsed() || shouldGestureIgnoreXTouchSlop(x, y); } switch (event.getActionMasked()) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PreviewInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PreviewInflater.java new file mode 100644 index 000000000000..3d317143eb51 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PreviewInflater.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2014 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.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; + +import com.android.internal.widget.LockPatternUtils; +import com.android.keyguard.KeyguardUpdateMonitor; +import com.android.systemui.ActivityIntentHelper; +import com.android.systemui.statusbar.phone.KeyguardPreviewContainer; + +import java.util.List; + +/** + * Utility class to inflate previews for phone and camera affordance. + */ +public class PreviewInflater { + + private static final String TAG = "PreviewInflater"; + + private static final String META_DATA_KEYGUARD_LAYOUT = "com.android.keyguard.layout"; + private final ActivityIntentHelper mActivityIntentHelper; + + private Context mContext; + private LockPatternUtils mLockPatternUtils; + + public PreviewInflater(Context context, LockPatternUtils lockPatternUtils, + ActivityIntentHelper activityIntentHelper) { + mContext = context; + mLockPatternUtils = lockPatternUtils; + mActivityIntentHelper = activityIntentHelper; + } + + public View inflatePreview(Intent intent) { + WidgetInfo info = getWidgetInfo(intent); + return inflatePreview(info); + } + + public View inflatePreviewFromService(ComponentName componentName) { + WidgetInfo info = getWidgetInfoFromService(componentName); + return inflatePreview(info); + } + + private KeyguardPreviewContainer inflatePreview(WidgetInfo info) { + if (info == null) { + return null; + } + View v = inflateWidgetView(info); + if (v == null) { + return null; + } + KeyguardPreviewContainer container = new KeyguardPreviewContainer(mContext, null); + container.addView(v); + return container; + } + + private View inflateWidgetView(WidgetInfo widgetInfo) { + View widgetView = null; + try { + Context appContext = mContext.createPackageContext( + widgetInfo.contextPackage, Context.CONTEXT_RESTRICTED); + LayoutInflater appInflater = (LayoutInflater) + appContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + appInflater = appInflater.cloneInContext(appContext); + widgetView = appInflater.inflate(widgetInfo.layoutId, null, false); + } catch (PackageManager.NameNotFoundException|RuntimeException e) { + Log.w(TAG, "Error creating widget view", e); + } + return widgetView; + } + + private WidgetInfo getWidgetInfoFromService(ComponentName componentName) { + PackageManager packageManager = mContext.getPackageManager(); + // Look for the preview specified in the service meta-data + try { + Bundle metaData = packageManager.getServiceInfo( + componentName, PackageManager.GET_META_DATA).metaData; + return getWidgetInfoFromMetaData(componentName.getPackageName(), metaData); + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, "Failed to load preview; " + componentName.flattenToShortString() + + " not found", e); + } + return null; + } + + private WidgetInfo getWidgetInfoFromMetaData(String contextPackage, + Bundle metaData) { + if (metaData == null) { + return null; + } + int layoutId = metaData.getInt(META_DATA_KEYGUARD_LAYOUT); + if (layoutId == 0) { + return null; + } + WidgetInfo info = new WidgetInfo(); + info.contextPackage = contextPackage; + info.layoutId = layoutId; + return info; + } + + private WidgetInfo getWidgetInfo(Intent intent) { + PackageManager packageManager = mContext.getPackageManager(); + int flags = PackageManager.MATCH_DEFAULT_ONLY + | PackageManager.MATCH_DIRECT_BOOT_AWARE + | PackageManager.MATCH_DIRECT_BOOT_UNAWARE; + final List<ResolveInfo> appList = packageManager.queryIntentActivitiesAsUser( + intent, flags, KeyguardUpdateMonitor.getCurrentUser()); + if (appList.size() == 0) { + return null; + } + ResolveInfo resolved = packageManager.resolveActivityAsUser(intent, + flags | PackageManager.GET_META_DATA, + KeyguardUpdateMonitor.getCurrentUser()); + if (mActivityIntentHelper.wouldLaunchResolverActivity(resolved, appList)) { + return null; + } + if (resolved == null || resolved.activityInfo == null) { + return null; + } + return getWidgetInfoFromMetaData(resolved.activityInfo.packageName, + resolved.activityInfo.metaData); + } + + private static class WidgetInfo { + String contextPackage; + int layoutId; + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java index 266f0e95f2ce..2faff0ced70a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java @@ -848,6 +848,7 @@ public class CentralSurfacesImplTest extends SysuiTestCase { mCentralSurfaces.showKeyguardImpl(); // Starting a pulse should change the scrim controller to the pulsing state + when(mNotificationPanelViewController.isLaunchTransitionRunning()).thenReturn(true); when(mNotificationPanelViewController.isLaunchingAffordanceWithPreview()).thenReturn(true); mCentralSurfaces.updateScrimController(); verify(mScrimController).transitionTo(eq(ScrimState.UNLOCKED), any()); @@ -884,6 +885,7 @@ public class CentralSurfacesImplTest extends SysuiTestCase { mCentralSurfaces.showKeyguardImpl(); // Starting a pulse should change the scrim controller to the pulsing state + when(mNotificationPanelViewController.isLaunchTransitionRunning()).thenReturn(true); when(mNotificationPanelViewController.isLaunchingAffordanceWithPreview()).thenReturn(false); mCentralSurfaces.updateScrimController(); verify(mScrimController).transitionTo(eq(ScrimState.KEYGUARD)); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaTest.kt index 4b557dc423ff..31465f45af42 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaTest.kt @@ -12,12 +12,12 @@ import com.android.systemui.statusbar.policy.AccessibilityController import com.android.systemui.statusbar.policy.FlashlightController import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.tuner.TunerService -import java.util.concurrent.Executor import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.MockitoAnnotations +import java.util.concurrent.Executor @SmallTest @RunWith(AndroidTestingRunner::class) @@ -51,5 +51,6 @@ class KeyguardBottomAreaTest : SysuiTestCase() { null, false) as KeyguardBottomAreaView other.initFrom(mKeyguardBottomArea) + other.launchVoiceAssist() } -} +}
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewControllerTest.java index 046af95c8c1d..79c7e55d4d09 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewControllerTest.java @@ -106,6 +106,7 @@ import com.android.systemui.plugins.qs.QS; import com.android.systemui.qrcodescanner.controller.QRCodeScannerController; import com.android.systemui.screenrecord.RecordingController; import com.android.systemui.statusbar.CommandQueue; +import com.android.systemui.statusbar.KeyguardAffordanceView; import com.android.systemui.statusbar.KeyguardIndicationController; import com.android.systemui.statusbar.LockscreenShadeTransitionController; import com.android.systemui.statusbar.NotificationRemoteInputManager; @@ -400,6 +401,8 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { when(mNotificationStackScrollLayoutController.getHeadsUpCallback()) .thenReturn(mHeadsUpCallback); when(mView.findViewById(R.id.keyguard_bottom_area)).thenReturn(mKeyguardBottomArea); + when(mKeyguardBottomArea.getLeftView()).thenReturn(mock(KeyguardAffordanceView.class)); + when(mKeyguardBottomArea.getRightView()).thenReturn(mock(KeyguardAffordanceView.class)); when(mKeyguardBottomArea.animate()).thenReturn(mock(ViewPropertyAnimator.class)); when(mView.findViewById(R.id.qs_frame)).thenReturn(mQsFrame); when(mView.findViewById(R.id.keyguard_status_view)) |