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 
17 package com.android.keyguard;
18 
19 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
20 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
21 
22 import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_PIN_APPEAR;
23 import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_PIN_DISAPPEAR;
24 import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_CLOSED;
25 import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_HALF_OPENED;
26 import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_UNKNOWN;
27 
28 import android.animation.ValueAnimator;
29 import android.annotation.Nullable;
30 import android.content.Context;
31 import android.content.res.Configuration;
32 import android.util.AttributeSet;
33 import android.util.MathUtils;
34 import android.view.View;
35 import android.view.animation.AnimationUtils;
36 import android.view.animation.Interpolator;
37 
38 import androidx.constraintlayout.motion.widget.MotionLayout;
39 import androidx.constraintlayout.widget.ConstraintLayout;
40 import androidx.constraintlayout.widget.ConstraintSet;
41 
42 import com.android.app.animation.Interpolators;
43 import com.android.settingslib.animation.DisappearAnimationUtils;
44 import com.android.systemui.res.R;
45 import com.android.systemui.statusbar.policy.DevicePostureController.DevicePostureInt;
46 
47 /**
48  * Displays a PIN pad for unlocking.
49  */
50 public class KeyguardPINView extends KeyguardPinBasedInputView {
51 
52     ValueAnimator mAppearAnimator = ValueAnimator.ofFloat(0f, 1f);
53     private final DisappearAnimationUtils mDisappearAnimationUtils;
54     private final DisappearAnimationUtils mDisappearAnimationUtilsLocked;
55     @Nullable private MotionLayout mContainerMotionLayout;
56     // TODO (b/293252410) - usage of mContainerConstraintLayout should be removed
57     //  when the flag is enabled/removed
58     @Nullable private ConstraintLayout mContainerConstraintLayout;
59     private int mDisappearYTranslation;
60     private View[][] mViews;
61     private int mYTrans;
62     private int mYTransOffset;
63     private View mBouncerMessageArea;
64     private boolean mAlreadyUsingSplitBouncer = false;
65     private boolean mIsSmallLockScreenLandscapeEnabled = false;
66     @DevicePostureInt private int mLastDevicePosture = DEVICE_POSTURE_UNKNOWN;
67     public static final long ANIMATION_DURATION = 650;
68 
KeyguardPINView(Context context)69     public KeyguardPINView(Context context) {
70         this(context, null);
71     }
72 
KeyguardPINView(Context context, AttributeSet attrs)73     public KeyguardPINView(Context context, AttributeSet attrs) {
74         super(context, attrs);
75         mDisappearAnimationUtils = new DisappearAnimationUtils(context,
76                 125, 0.6f /* translationScale */,
77                 0.45f /* delayScale */, AnimationUtils.loadInterpolator(
78                         mContext, android.R.interpolator.fast_out_linear_in));
79         mDisappearAnimationUtilsLocked = new DisappearAnimationUtils(context,
80                 (long) (125 * KeyguardPatternView.DISAPPEAR_MULTIPLIER_LOCKED),
81                 0.6f /* translationScale */,
82                 0.45f /* delayScale */, AnimationUtils.loadInterpolator(
83                        mContext, android.R.interpolator.fast_out_linear_in));
84         mDisappearYTranslation = getResources().getDimensionPixelSize(
85                 R.dimen.disappear_y_translation);
86         mYTrans = getResources().getDimensionPixelSize(R.dimen.pin_view_trans_y_entry);
87         mYTransOffset = getResources().getDimensionPixelSize(R.dimen.pin_view_trans_y_entry_offset);
88     }
89 
90     /** Use motion layout (new bouncer implementation) if LOCKSCREEN_ENABLE_LANDSCAPE flag is
91      *  enabled, instead of constraint layout (old bouncer implementation) */
setIsLockScreenLandscapeEnabled(boolean isLockScreenLandscapeEnabled)92     public void setIsLockScreenLandscapeEnabled(boolean isLockScreenLandscapeEnabled) {
93         mIsSmallLockScreenLandscapeEnabled = isLockScreenLandscapeEnabled;
94         findContainerLayout();
95     }
96 
findContainerLayout()97     private void findContainerLayout() {
98         if (mIsSmallLockScreenLandscapeEnabled) {
99             mContainerMotionLayout = findViewById(R.id.pin_container);
100         } else {
101             mContainerConstraintLayout = findViewById(R.id.pin_container);
102         }
103     }
104 
105 
106     @Override
onConfigurationChanged(Configuration newConfig)107     protected void onConfigurationChanged(Configuration newConfig) {
108         updateMargins();
109     }
110 
onDevicePostureChanged(@evicePostureInt int posture)111     void onDevicePostureChanged(@DevicePostureInt int posture) {
112         if (mLastDevicePosture == posture) return;
113         mLastDevicePosture = posture;
114 
115         if (mIsSmallLockScreenLandscapeEnabled) {
116             boolean useSplitBouncerAfterFold =
117                     mLastDevicePosture == DEVICE_POSTURE_CLOSED
118                     && getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE
119                     && getResources().getBoolean(R.bool.update_bouncer_constraints);
120 
121             if (mAlreadyUsingSplitBouncer != useSplitBouncerAfterFold) {
122                 updateConstraints(useSplitBouncerAfterFold);
123             }
124         }
125 
126         updateMargins();
127     }
128 
129     @Override
resetState()130     protected void resetState() {
131     }
132 
133     @Override
getPasswordTextViewId()134     protected int getPasswordTextViewId() {
135         return R.id.pinEntry;
136     }
137 
updateMargins()138     private void updateMargins() {
139         // Re-apply everything to the keys...
140         int bottomMargin = mContext.getResources().getDimensionPixelSize(
141                 R.dimen.num_pad_entry_row_margin_bottom);
142         int rightMargin = mContext.getResources().getDimensionPixelSize(
143                 R.dimen.num_pad_key_margin_end);
144         String ratio = mContext.getResources().getString(R.string.num_pad_key_ratio);
145 
146         // mView contains all Views that make up the PIN pad; row0 = the entry test field, then
147         // rows 1-4 contain the buttons. Iterate over all views that make up the buttons in the pad,
148         // and re-set all the margins.
149         for (int row = 1; row < 5; row++) {
150             for (int column = 0; column < 3; column++) {
151                 View key = mViews[row][column];
152 
153                 ConstraintLayout.LayoutParams lp =
154                         (ConstraintLayout.LayoutParams) key.getLayoutParams();
155 
156                 lp.dimensionRatio = ratio;
157 
158                 // Don't set any margins on the last row of buttons.
159                 if (row != 4) {
160                     lp.bottomMargin = bottomMargin;
161                 }
162 
163                 // Don't set margins on the rightmost buttons.
164                 if (column != 2) {
165                     lp.rightMargin = rightMargin;
166                 }
167 
168                 key.setLayoutParams(lp);
169             }
170         }
171 
172         if (mIsSmallLockScreenLandscapeEnabled) {
173             updateHalfFoldedConstraints();
174         } else {
175             updateHalfFoldedGuideline();
176         }
177     }
178 
updateHalfFoldedConstraints()179     private void updateHalfFoldedConstraints() {
180         // Update the constraints based on the device posture...
181         if (mAlreadyUsingSplitBouncer) return;
182 
183         boolean shouldCollapsePin =
184                 mLastDevicePosture == DEVICE_POSTURE_HALF_OPENED
185                         && mContext.getResources().getConfiguration().orientation
186                         == ORIENTATION_PORTRAIT;
187 
188         int expectedMotionLayoutState = shouldCollapsePin
189                 ? R.id.half_folded_single_constraints
190                 : R.id.single_constraints;
191 
192         transitionToMotionLayoutState(expectedMotionLayoutState);
193     }
194 
195     // TODO (b/293252410) - this method can be removed when the flag is enabled/removed
updateHalfFoldedGuideline()196     private void updateHalfFoldedGuideline() {
197         // Update the guideline based on the device posture...
198         float halfOpenPercentage =
199                 mContext.getResources().getFloat(R.dimen.half_opened_bouncer_height_ratio);
200 
201         ConstraintSet cs = new ConstraintSet();
202         cs.clone(mContainerConstraintLayout);
203         cs.setGuidelinePercent(R.id.pin_pad_top_guideline,
204                 mLastDevicePosture == DEVICE_POSTURE_HALF_OPENED ? halfOpenPercentage : 0.0f);
205         cs.applyTo(mContainerConstraintLayout);
206     }
207 
transitionToMotionLayoutState(int state)208     private void transitionToMotionLayoutState(int state) {
209         if (mContainerMotionLayout.getCurrentState() != state) {
210             mContainerMotionLayout.transitionToState(state);
211         }
212     }
213 
214     /** Updates the keyguard view's constraints (single or split constraints).
215      *  Split constraints are only used for small landscape screens.
216      *  Only called when flag LANDSCAPE_ENABLE_LOCKSCREEN is enabled. */
217     @Override
updateConstraints(boolean useSplitBouncer)218     protected void updateConstraints(boolean useSplitBouncer) {
219         if (!mIsSmallLockScreenLandscapeEnabled) return;
220 
221         mAlreadyUsingSplitBouncer = useSplitBouncer;
222 
223         if (useSplitBouncer) {
224             mContainerMotionLayout.jumpToState(R.id.split_constraints);
225             mContainerMotionLayout.setMaxWidth(Integer.MAX_VALUE);
226         } else {
227             boolean useHalfFoldedConstraints =
228                     mLastDevicePosture == DEVICE_POSTURE_HALF_OPENED
229                             && mContext.getResources().getConfiguration().orientation
230                             == ORIENTATION_PORTRAIT;
231 
232             if (useHalfFoldedConstraints) {
233                 mContainerMotionLayout.jumpToState(R.id.half_folded_single_constraints);
234             } else {
235                 mContainerMotionLayout.jumpToState(R.id.single_constraints);
236             }
237             mContainerMotionLayout.setMaxWidth(getResources()
238                     .getDimensionPixelSize(R.dimen.keyguard_security_width));
239         }
240     }
241 
242     @Override
onFinishInflate()243     protected void onFinishInflate() {
244         super.onFinishInflate();
245 
246         mBouncerMessageArea = findViewById(R.id.bouncer_message_area);
247         mViews = new View[][]{
248                 new View[]{
249                         findViewById(R.id.row0), null, null
250                 },
251                 new View[]{
252                         findViewById(R.id.key1), findViewById(R.id.key2),
253                         findViewById(R.id.key3)
254                 },
255                 new View[]{
256                         findViewById(R.id.key4), findViewById(R.id.key5),
257                         findViewById(R.id.key6)
258                 },
259                 new View[]{
260                         findViewById(R.id.key7), findViewById(R.id.key8),
261                         findViewById(R.id.key9)
262                 },
263                 new View[]{
264                         findViewById(R.id.delete_button), findViewById(R.id.key0),
265                         findViewById(R.id.key_enter)
266                 },
267                 new View[]{
268                         null, mEcaView, null
269                 }};
270     }
271 
272     @Override
getWrongPasswordStringId()273     public int getWrongPasswordStringId() {
274         return R.string.kg_wrong_pin;
275     }
276 
277     @Override
startAppearAnimation()278     public void startAppearAnimation() {
279         setAlpha(1f);
280         setTranslationY(0);
281         if (mAppearAnimator.isRunning()) {
282             mAppearAnimator.cancel();
283         }
284         mAppearAnimator.setDuration(ANIMATION_DURATION);
285         mAppearAnimator.addUpdateListener(animation -> animate(animation.getAnimatedFraction()));
286         mAppearAnimator.addListener(getAnimationListener(CUJ_LOCKSCREEN_PIN_APPEAR));
287         mAppearAnimator.start();
288     }
289 
startDisappearAnimation(boolean needsSlowUnlockTransition, final Runnable finishRunnable)290     public boolean startDisappearAnimation(boolean needsSlowUnlockTransition,
291             final Runnable finishRunnable) {
292         if (mAppearAnimator.isRunning()) {
293             mAppearAnimator.cancel();
294         }
295 
296         setTranslationY(0);
297         DisappearAnimationUtils disappearAnimationUtils = needsSlowUnlockTransition
298                         ? mDisappearAnimationUtilsLocked
299                         : mDisappearAnimationUtils;
300         disappearAnimationUtils.createAnimation(
301                 this, 0, 200, mDisappearYTranslation, false,
302                 mDisappearAnimationUtils.getInterpolator(), () -> {
303                     if (finishRunnable != null) {
304                         finishRunnable.run();
305                     }
306                 },
307                 getAnimationListener(CUJ_LOCKSCREEN_PIN_DISAPPEAR));
308         return true;
309     }
310 
311     @Override
hasOverlappingRendering()312     public boolean hasOverlappingRendering() {
313         return false;
314     }
315 
316     /** Animate subviews according to expansion or time. */
animate(float progress)317     private void animate(float progress) {
318         Interpolator standardDecelerate = Interpolators.STANDARD_DECELERATE;
319         Interpolator legacyDecelerate = Interpolators.LEGACY_DECELERATE;
320         float standardProgress = standardDecelerate.getInterpolation(progress);
321 
322         mBouncerMessageArea.setTranslationY(
323                 mYTrans - mYTrans * standardProgress);
324         mBouncerMessageArea.setAlpha(standardProgress);
325 
326         for (int i = 0; i < mViews.length; i++) {
327             View[] row = mViews[i];
328             for (View view : row) {
329                 if (view == null) {
330                     continue;
331                 }
332 
333                 float scaledProgress = legacyDecelerate.getInterpolation(MathUtils.constrain(
334                         (progress - 0.075f * i) / (1f - 0.075f * mViews.length),
335                         0f,
336                         1f
337                 ));
338                 view.setAlpha(scaledProgress);
339                 int yDistance = mYTrans + mYTransOffset * i;
340                 view.setTranslationY(
341                         yDistance - (yDistance * standardProgress));
342                 if (view instanceof NumPadAnimationListener) {
343                     ((NumPadAnimationListener) view).setProgress(scaledProgress);
344                 }
345             }
346         }
347     }
348 }
349