1 /*
2  * Copyright (C) 2012 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.keyguard;
17 
18 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
19 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
20 
21 import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_CLOSED;
22 import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_HALF_OPENED;
23 import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_UNKNOWN;
24 
25 import android.annotation.Nullable;
26 import android.content.Context;
27 import android.content.res.Configuration;
28 import android.graphics.Rect;
29 import android.os.SystemClock;
30 import android.text.TextUtils;
31 import android.util.AttributeSet;
32 import android.view.MotionEvent;
33 import android.view.View;
34 import android.view.animation.AnimationUtils;
35 import android.view.animation.Interpolator;
36 
37 import androidx.constraintlayout.motion.widget.MotionLayout;
38 import androidx.constraintlayout.widget.ConstraintLayout;
39 import androidx.constraintlayout.widget.ConstraintSet;
40 
41 import com.android.internal.jank.InteractionJankMonitor;
42 import com.android.internal.widget.LockPatternView;
43 import com.android.settingslib.animation.AppearAnimationCreator;
44 import com.android.settingslib.animation.AppearAnimationUtils;
45 import com.android.settingslib.animation.DisappearAnimationUtils;
46 import com.android.systemui.res.R;
47 import com.android.systemui.statusbar.policy.DevicePostureController.DevicePostureInt;
48 
49 public class KeyguardPatternView extends KeyguardInputView
50         implements AppearAnimationCreator<LockPatternView.CellState> {
51 
52     private static final String TAG = "SecurityPatternView";
53     private static final boolean DEBUG = KeyguardConstants.DEBUG;
54 
55 
56     // how long we stay awake after each key beyond MIN_PATTERN_BEFORE_POKE_WAKELOCK
57     private static final int UNLOCK_PATTERN_WAKE_INTERVAL_MS = 7000;
58 
59     // How much we scale up the duration of the disappear animation when the current user is locked
60     public static final float DISAPPEAR_MULTIPLIER_LOCKED = 1.5f;
61 
62     // Extra padding, in pixels, that should eat touch events.
63     private static final int PATTERNS_TOUCH_AREA_EXTENSION = 40;
64 
65     private final AppearAnimationUtils mAppearAnimationUtils;
66     private final DisappearAnimationUtils mDisappearAnimationUtils;
67     private final DisappearAnimationUtils mDisappearAnimationUtilsLocked;
68     private final int[] mTmpPosition = new int[2];
69     private final Rect mTempRect = new Rect();
70     private final Rect mLockPatternScreenBounds = new Rect();
71 
72     private LockPatternView mLockPatternView;
73 
74     /**
75      * Keeps track of the last time we poked the wake lock during dispatching of the touch event.
76      * Initialized to something guaranteed to make us poke the wakelock when the user starts
77      * drawing the pattern.
78      * @see #dispatchTouchEvent(android.view.MotionEvent)
79      */
80     private long mLastPokeTime = -UNLOCK_PATTERN_WAKE_INTERVAL_MS;
81 
82     BouncerKeyguardMessageArea mSecurityMessageDisplay;
83     private View mEcaView;
84     @Nullable private MotionLayout mContainerMotionLayout;
85     // TODO (b/293252410) - usage of mContainerConstraintLayout should be removed
86     //  when the flag is enabled/removed
87     @Nullable private ConstraintLayout mContainerConstraintLayout;
88     private boolean mAlreadyUsingSplitBouncer = false;
89     private boolean mIsSmallLockScreenLandscapeEnabled = false;
90     @DevicePostureInt private int mLastDevicePosture = DEVICE_POSTURE_UNKNOWN;
91 
KeyguardPatternView(Context context)92     public KeyguardPatternView(Context context) {
93         this(context, null);
94     }
95 
KeyguardPatternView(Context context, AttributeSet attrs)96     public KeyguardPatternView(Context context, AttributeSet attrs) {
97         super(context, attrs);
98         mAppearAnimationUtils = new AppearAnimationUtils(context,
99                 AppearAnimationUtils.DEFAULT_APPEAR_DURATION, 1.5f /* translationScale */,
100                 2.0f /* delayScale */, AnimationUtils.loadInterpolator(
101                         mContext, android.R.interpolator.linear_out_slow_in));
102         mDisappearAnimationUtils = new DisappearAnimationUtils(context,
103                 125, 1.2f /* translationScale */,
104                 0.6f /* delayScale */, AnimationUtils.loadInterpolator(
105                         mContext, android.R.interpolator.fast_out_linear_in));
106         mDisappearAnimationUtilsLocked = new DisappearAnimationUtils(context,
107                 (long) (125 * DISAPPEAR_MULTIPLIER_LOCKED), 1.2f /* translationScale */,
108                 0.6f /* delayScale */, AnimationUtils.loadInterpolator(
109                 mContext, android.R.interpolator.fast_out_linear_in));
110     }
111 
112     /**
113      * Use motion layout (new bouncer implementation) if LOCKSCREEN_ENABLE_LANDSCAPE flag is
114      * enabled, instead of constraint layout (old bouncer implementation)
115      */
setIsLockScreenLandscapeEnabled(boolean isLockScreenLandscapeEnabled)116     public void setIsLockScreenLandscapeEnabled(boolean isLockScreenLandscapeEnabled) {
117         mIsSmallLockScreenLandscapeEnabled = isLockScreenLandscapeEnabled;
118         findContainerLayout();
119     }
120 
findContainerLayout()121     private void findContainerLayout() {
122         if (mIsSmallLockScreenLandscapeEnabled) {
123             mContainerMotionLayout = findViewById(R.id.pattern_container);
124         } else {
125             mContainerConstraintLayout = findViewById(R.id.pattern_container);
126         }
127     }
128 
129     @Override
onConfigurationChanged(Configuration newConfig)130     protected void onConfigurationChanged(Configuration newConfig) {
131         updateMargins();
132     }
133 
onDevicePostureChanged(@evicePostureInt int posture)134     void onDevicePostureChanged(@DevicePostureInt int posture) {
135         if (mLastDevicePosture == posture) return;
136         mLastDevicePosture = posture;
137 
138         if (mIsSmallLockScreenLandscapeEnabled) {
139             boolean useSplitBouncerAfterFold =
140                     mLastDevicePosture == DEVICE_POSTURE_CLOSED
141                     && getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE
142                     && getResources().getBoolean(R.bool.update_bouncer_constraints);
143 
144             if (mAlreadyUsingSplitBouncer != useSplitBouncerAfterFold) {
145                 updateConstraints(useSplitBouncerAfterFold);
146             }
147         }
148 
149         updateMargins();
150     }
151 
updateMargins()152     private void updateMargins() {
153         if (mIsSmallLockScreenLandscapeEnabled) {
154             updateHalfFoldedConstraints();
155         } else {
156             updateHalfFoldedGuideline();
157         }
158     }
159 
updateHalfFoldedConstraints()160     private void updateHalfFoldedConstraints() {
161         // Update the constraints based on the device posture...
162         if (mAlreadyUsingSplitBouncer) return;
163 
164         boolean shouldCollapsePattern =
165                 mLastDevicePosture == DEVICE_POSTURE_HALF_OPENED
166                         && mContext.getResources().getConfiguration().orientation
167                         == ORIENTATION_PORTRAIT;
168 
169         int expectedMotionLayoutState = shouldCollapsePattern
170                 ? R.id.half_folded_single_constraints
171                 : R.id.single_constraints;
172 
173         transitionToMotionLayoutState(expectedMotionLayoutState);
174     }
175 
176     // TODO (b/293252410) - this method can be removed when the flag is enabled/removed
updateHalfFoldedGuideline()177     private void updateHalfFoldedGuideline() {
178         // Update the guideline based on the device posture...
179         float halfOpenPercentage =
180                 mContext.getResources().getFloat(R.dimen.half_opened_bouncer_height_ratio);
181 
182         ConstraintSet cs = new ConstraintSet();
183         cs.clone(mContainerConstraintLayout);
184         cs.setGuidelinePercent(R.id.pattern_top_guideline,
185                 mLastDevicePosture == DEVICE_POSTURE_HALF_OPENED ? halfOpenPercentage : 0.0f);
186         cs.applyTo(mContainerConstraintLayout);
187     }
188 
transitionToMotionLayoutState(int state)189     private void transitionToMotionLayoutState(int state) {
190         if (mContainerMotionLayout.getCurrentState() != state) {
191             mContainerMotionLayout.transitionToState(state);
192         }
193     }
194 
195     /**
196      * Updates the keyguard view's constraints (single or split constraints).
197      * Split constraints are only used for small landscape screens.
198      * Only called when flag LANDSCAPE_ENABLE_LOCKSCREEN is enabled.
199      */
200     @Override
updateConstraints(boolean useSplitBouncer)201     protected void updateConstraints(boolean useSplitBouncer) {
202         if (!mIsSmallLockScreenLandscapeEnabled) return;
203 
204         mAlreadyUsingSplitBouncer = useSplitBouncer;
205 
206         if (useSplitBouncer) {
207             mContainerMotionLayout.jumpToState(R.id.split_constraints);
208             mContainerMotionLayout.setMaxWidth(Integer.MAX_VALUE);
209         } else {
210             boolean useHalfFoldedConstraints =
211                     mLastDevicePosture == DEVICE_POSTURE_HALF_OPENED
212                             && mContext.getResources().getConfiguration().orientation
213                             == ORIENTATION_PORTRAIT;
214 
215             if (useHalfFoldedConstraints) {
216                 mContainerMotionLayout.jumpToState(R.id.half_folded_single_constraints);
217             } else {
218                 mContainerMotionLayout.jumpToState(R.id.single_constraints);
219             }
220             mContainerMotionLayout.setMaxWidth(getResources()
221                     .getDimensionPixelSize(R.dimen.biometric_auth_pattern_view_max_size));
222         }
223     }
224 
225     @Override
onFinishInflate()226     protected void onFinishInflate() {
227         super.onFinishInflate();
228 
229         mLockPatternView = findViewById(R.id.lockPatternView);
230 
231         mEcaView = findViewById(R.id.keyguard_selector_fade_container);
232     }
233 
234     @Override
onAttachedToWindow()235     protected void onAttachedToWindow() {
236         super.onAttachedToWindow();
237         mSecurityMessageDisplay = findViewById(R.id.bouncer_message_area);
238     }
239 
240     @Override
onTouchEvent(MotionEvent ev)241     public boolean onTouchEvent(MotionEvent ev) {
242         boolean result = super.onTouchEvent(ev);
243         // as long as the user is entering a pattern (i.e sending a touch event that was handled
244         // by this screen), keep poking the wake lock so that the screen will stay on.
245         final long elapsed = SystemClock.elapsedRealtime() - mLastPokeTime;
246         if (result && (elapsed > (UNLOCK_PATTERN_WAKE_INTERVAL_MS - 100))) {
247             mLastPokeTime = SystemClock.elapsedRealtime();
248         }
249         mTempRect.set(0, 0, 0, 0);
250         offsetRectIntoDescendantCoords(mLockPatternView, mTempRect);
251         ev.offsetLocation(mTempRect.left, mTempRect.top);
252         result = mLockPatternView.dispatchTouchEvent(ev) || result;
253         ev.offsetLocation(-mTempRect.left, -mTempRect.top);
254         return result;
255     }
256 
257     @Override
onLayout(boolean changed, int l, int t, int r, int b)258     protected void onLayout(boolean changed, int l, int t, int r, int b) {
259         super.onLayout(changed, l, t, r, b);
260         mLockPatternView.getLocationOnScreen(mTmpPosition);
261         mLockPatternScreenBounds.set(mTmpPosition[0] - PATTERNS_TOUCH_AREA_EXTENSION,
262                 mTmpPosition[1] - PATTERNS_TOUCH_AREA_EXTENSION,
263                 mTmpPosition[0] + mLockPatternView.getWidth() + PATTERNS_TOUCH_AREA_EXTENSION,
264                 mTmpPosition[1] + mLockPatternView.getHeight() + PATTERNS_TOUCH_AREA_EXTENSION);
265     }
266 
267     @Override
disallowInterceptTouch(MotionEvent event)268     boolean disallowInterceptTouch(MotionEvent event) {
269         return !mLockPatternView.isEmpty()
270                 || mLockPatternScreenBounds.contains((int) event.getRawX(), (int) event.getRawY());
271     }
272 
startAppearAnimation()273     public void startAppearAnimation() {
274         enableClipping(false);
275         setAlpha(0f);
276         setTranslationY(mAppearAnimationUtils.getStartTranslation());
277         AppearAnimationUtils.startTranslationYAnimation(this, 0 /* delay */, 500 /* duration */,
278                 0, mAppearAnimationUtils.getInterpolator(),
279                 getAnimationListener(InteractionJankMonitor.CUJ_LOCKSCREEN_PATTERN_APPEAR));
280         mLockPatternView.post(() -> {
281             setAlpha(1f);
282             mAppearAnimationUtils.startAnimation2d(
283                     mLockPatternView.getCellStates(),
284                     () -> {
285                         enableClipping(true);
286                         mLockPatternView.invalidate();
287                     },
288                     KeyguardPatternView.this);
289         });
290         if (!TextUtils.isEmpty(mSecurityMessageDisplay.getText())) {
291             mAppearAnimationUtils.createAnimation(mSecurityMessageDisplay, 0,
292                     AppearAnimationUtils.DEFAULT_APPEAR_DURATION,
293                     mAppearAnimationUtils.getStartTranslation(),
294                     true /* appearing */,
295                     mAppearAnimationUtils.getInterpolator(),
296                     null /* finishRunnable */);
297         }
298     }
299 
startDisappearAnimation(boolean needsSlowUnlockTransition, final Runnable finishRunnable)300     public boolean startDisappearAnimation(boolean needsSlowUnlockTransition,
301             final Runnable finishRunnable) {
302         float durationMultiplier = needsSlowUnlockTransition ? DISAPPEAR_MULTIPLIER_LOCKED : 1f;
303         mLockPatternView.clearPattern();
304         enableClipping(false);
305         setTranslationY(0);
306         AppearAnimationUtils.startTranslationYAnimation(this, 0 /* delay */,
307                 (long) (300 * durationMultiplier),
308                 -mDisappearAnimationUtils.getStartTranslation(),
309                 mDisappearAnimationUtils.getInterpolator(),
310                 getAnimationListener(InteractionJankMonitor.CUJ_LOCKSCREEN_PATTERN_DISAPPEAR));
311 
312         DisappearAnimationUtils disappearAnimationUtils = needsSlowUnlockTransition
313                 ? mDisappearAnimationUtilsLocked : mDisappearAnimationUtils;
314         disappearAnimationUtils.startAnimation2d(mLockPatternView.getCellStates(),
315                 () -> {
316                     enableClipping(true);
317                     if (finishRunnable != null) {
318                         finishRunnable.run();
319                     }
320                 }, KeyguardPatternView.this);
321         if (!TextUtils.isEmpty(mSecurityMessageDisplay.getText())) {
322             mDisappearAnimationUtils.createAnimation(mSecurityMessageDisplay, 0,
323                     (long) (200 * durationMultiplier),
324                     -mDisappearAnimationUtils.getStartTranslation() * 3,
325                     false /* appearing */,
326                     mDisappearAnimationUtils.getInterpolator(),
327                     null /* finishRunnable */);
328         }
329         return true;
330     }
331 
enableClipping(boolean enable)332     private void enableClipping(boolean enable) {
333         if (mContainerConstraintLayout != null) {
334             setClipChildren(enable);
335             mContainerConstraintLayout.setClipToPadding(enable);
336             mContainerConstraintLayout.setClipChildren(enable);
337         }
338         if (mContainerMotionLayout != null) {
339             setClipChildren(enable);
340             mContainerMotionLayout.setClipToPadding(enable);
341             mContainerMotionLayout.setClipChildren(enable);
342         }
343     }
344 
345     @Override
createAnimation(final LockPatternView.CellState animatedCell, long delay, long duration, float translationY, final boolean appearing, Interpolator interpolator, final Runnable finishListener)346     public void createAnimation(final LockPatternView.CellState animatedCell, long delay,
347             long duration, float translationY, final boolean appearing,
348             Interpolator interpolator,
349             final Runnable finishListener) {
350         mLockPatternView.startCellStateAnimation(animatedCell,
351                 1f, appearing ? 1f : 0f, /* alpha */
352                 appearing ? translationY : 0f, appearing ? 0f : translationY, /* translation */
353                 appearing ? 0f : 1f, 1f /* scale */,
354                 delay, duration, interpolator, finishListener);
355         if (finishListener != null) {
356             // Also animate the Emergency call
357             mAppearAnimationUtils.createAnimation(mEcaView, delay, duration, translationY,
358                     appearing, interpolator, null);
359         }
360     }
361 
362     @Override
hasOverlappingRendering()363     public boolean hasOverlappingRendering() {
364         return false;
365     }
366 
367     @Override
getTitle()368     public CharSequence getTitle() {
369         return getResources().getString(
370                 com.android.internal.R.string.keyguard_accessibility_pattern_unlock);
371     }
372 }
373