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 android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.ObjectAnimator;
22 import android.content.Context;
23 import android.content.res.Resources;
24 import android.content.res.TypedArray;
25 import android.graphics.Rect;
26 import android.util.AttributeSet;
27 import android.util.DisplayMetrics;
28 import android.view.Gravity;
29 import android.view.View;
30 import android.view.ViewGroup;
31 import android.widget.LinearLayout;
32 
33 public class MultiPaneChallengeLayout extends ViewGroup implements ChallengeLayout {
34     private static final String TAG = "MultiPaneChallengeLayout";
35 
36     final int mOrientation;
37     private boolean mIsBouncing;
38 
39     public static final int HORIZONTAL = LinearLayout.HORIZONTAL;
40     public static final int VERTICAL = LinearLayout.VERTICAL;
41     public static final int ANIMATE_BOUNCE_DURATION = 350;
42 
43     private KeyguardSecurityContainer mChallengeView;
44     private View mUserSwitcherView;
45     private View mScrimView;
46     private OnBouncerStateChangedListener mBouncerListener;
47 
48     private final Rect mTempRect = new Rect();
49     private final Rect mZeroPadding = new Rect();
50     private final Rect mInsets = new Rect();
51 
52     private final DisplayMetrics mDisplayMetrics;
53 
54     private final OnClickListener mScrimClickListener = new OnClickListener() {
55         @Override
56         public void onClick(View v) {
57             hideBouncer();
58         }
59     };
60 
MultiPaneChallengeLayout(Context context)61     public MultiPaneChallengeLayout(Context context) {
62         this(context, null);
63     }
64 
MultiPaneChallengeLayout(Context context, AttributeSet attrs)65     public MultiPaneChallengeLayout(Context context, AttributeSet attrs) {
66         this(context, attrs, 0);
67     }
68 
MultiPaneChallengeLayout(Context context, AttributeSet attrs, int defStyleAttr)69     public MultiPaneChallengeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
70         super(context, attrs, defStyleAttr);
71 
72         final TypedArray a = context.obtainStyledAttributes(attrs,
73                 R.styleable.MultiPaneChallengeLayout, defStyleAttr, 0);
74         mOrientation = a.getInt(R.styleable.MultiPaneChallengeLayout_android_orientation,
75                 HORIZONTAL);
76         a.recycle();
77 
78         final Resources res = getResources();
79         mDisplayMetrics = res.getDisplayMetrics();
80 
81         setSystemUiVisibility(SYSTEM_UI_FLAG_LAYOUT_STABLE | SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
82     }
83 
setInsets(Rect insets)84     public void setInsets(Rect insets) {
85         mInsets.set(insets);
86     }
87 
88     @Override
isChallengeShowing()89     public boolean isChallengeShowing() {
90         return true;
91     }
92 
93     @Override
isChallengeOverlapping()94     public boolean isChallengeOverlapping() {
95         return false;
96     }
97 
98     @Override
showChallenge(boolean b)99     public void showChallenge(boolean b) {
100     }
101 
102     @Override
getBouncerAnimationDuration()103     public int getBouncerAnimationDuration() {
104         return ANIMATE_BOUNCE_DURATION;
105     }
106 
107     @Override
showBouncer()108     public void showBouncer() {
109         if (mIsBouncing) return;
110         mIsBouncing = true;
111         if (mScrimView != null) {
112             if (mChallengeView != null) {
113                 mChallengeView.showBouncer(ANIMATE_BOUNCE_DURATION);
114             }
115 
116             Animator anim = ObjectAnimator.ofFloat(mScrimView, "alpha", 1f);
117             anim.setDuration(ANIMATE_BOUNCE_DURATION);
118             anim.addListener(new AnimatorListenerAdapter() {
119                 @Override
120                 public void onAnimationStart(Animator animation) {
121                     mScrimView.setVisibility(VISIBLE);
122                 }
123             });
124             anim.start();
125         }
126         if (mBouncerListener != null) {
127             mBouncerListener.onBouncerStateChanged(true);
128         }
129     }
130 
131     @Override
hideBouncer()132     public void hideBouncer() {
133         if (!mIsBouncing) return;
134         mIsBouncing = false;
135         if (mScrimView != null) {
136             if (mChallengeView != null) {
137                 mChallengeView.hideBouncer(ANIMATE_BOUNCE_DURATION);
138             }
139 
140             Animator anim = ObjectAnimator.ofFloat(mScrimView, "alpha", 0f);
141             anim.setDuration(ANIMATE_BOUNCE_DURATION);
142             anim.addListener(new AnimatorListenerAdapter() {
143                 @Override
144                 public void onAnimationEnd(Animator animation) {
145                     mScrimView.setVisibility(INVISIBLE);
146                 }
147             });
148             anim.start();
149         }
150         if (mBouncerListener != null) {
151             mBouncerListener.onBouncerStateChanged(false);
152         }
153     }
154 
155     @Override
isBouncing()156     public boolean isBouncing() {
157         return mIsBouncing;
158     }
159 
160     @Override
setOnBouncerStateChangedListener(OnBouncerStateChangedListener listener)161     public void setOnBouncerStateChangedListener(OnBouncerStateChangedListener listener) {
162         mBouncerListener = listener;
163     }
164 
165     @Override
requestChildFocus(View child, View focused)166     public void requestChildFocus(View child, View focused) {
167         if (mIsBouncing && child != mChallengeView) {
168             // Clear out of the bouncer if the user tries to move focus outside of
169             // the security challenge view.
170             hideBouncer();
171         }
172         super.requestChildFocus(child, focused);
173     }
174 
setScrimView(View scrim)175     void setScrimView(View scrim) {
176         if (mScrimView != null) {
177             mScrimView.setOnClickListener(null);
178         }
179         mScrimView = scrim;
180         if (mScrimView != null) {
181             mScrimView.setAlpha(mIsBouncing ? 1.0f : 0.0f);
182             mScrimView.setVisibility(mIsBouncing ? VISIBLE : INVISIBLE);
183             mScrimView.setFocusable(true);
184             mScrimView.setOnClickListener(mScrimClickListener);
185         }
186     }
187 
getVirtualHeight(LayoutParams lp, int height, int heightUsed)188     private int getVirtualHeight(LayoutParams lp, int height, int heightUsed) {
189         int virtualHeight = height;
190         final View root = getRootView();
191         if (root != null) {
192             // This calculation is super dodgy and relies on several assumptions.
193             // Specifically that the root of the window will be padded in for insets
194             // and that the window is LAYOUT_IN_SCREEN.
195             virtualHeight = mDisplayMetrics.heightPixels - root.getPaddingTop() - mInsets.top;
196         }
197         if (lp.childType == LayoutParams.CHILD_TYPE_USER_SWITCHER) {
198             // Always measure the user switcher as if there were no IME insets
199             // on the window.
200             return virtualHeight - heightUsed;
201         } else if (lp.childType == LayoutParams.CHILD_TYPE_PAGE_DELETE_DROP_TARGET) {
202             return height;
203         }
204         return Math.min(virtualHeight - heightUsed, height);
205     }
206 
207     @Override
onMeasure(final int widthSpec, final int heightSpec)208     protected void onMeasure(final int widthSpec, final int heightSpec) {
209         if (MeasureSpec.getMode(widthSpec) != MeasureSpec.EXACTLY ||
210                 MeasureSpec.getMode(heightSpec) != MeasureSpec.EXACTLY) {
211             throw new IllegalArgumentException(
212                     "MultiPaneChallengeLayout must be measured with an exact size");
213         }
214 
215         final int width = MeasureSpec.getSize(widthSpec);
216         final int height = MeasureSpec.getSize(heightSpec);
217         setMeasuredDimension(width, height);
218 
219         final int insetHeight = height - mInsets.top - mInsets.bottom;
220         final int insetHeightSpec = MeasureSpec.makeMeasureSpec(insetHeight, MeasureSpec.EXACTLY);
221 
222         int widthUsed = 0;
223         int heightUsed = 0;
224 
225         // First pass. Find the challenge view and measure the user switcher,
226         // which consumes space in the layout.
227         mChallengeView = null;
228         mUserSwitcherView = null;
229         final int count = getChildCount();
230         for (int i = 0; i < count; i++) {
231             final View child = getChildAt(i);
232             final LayoutParams lp = (LayoutParams) child.getLayoutParams();
233 
234             if (lp.childType == LayoutParams.CHILD_TYPE_CHALLENGE) {
235                 if (mChallengeView != null) {
236                     throw new IllegalStateException(
237                             "There may only be one child of type challenge");
238                 }
239                 if (!(child instanceof KeyguardSecurityContainer)) {
240                     throw new IllegalArgumentException(
241                             "Challenge must be a KeyguardSecurityContainer");
242                 }
243                 mChallengeView = (KeyguardSecurityContainer) child;
244             } else if (lp.childType == LayoutParams.CHILD_TYPE_USER_SWITCHER) {
245                 if (mUserSwitcherView != null) {
246                     throw new IllegalStateException(
247                             "There may only be one child of type userSwitcher");
248                 }
249                 mUserSwitcherView = child;
250 
251                 if (child.getVisibility() == GONE) continue;
252 
253                 int adjustedWidthSpec = widthSpec;
254                 int adjustedHeightSpec = insetHeightSpec;
255                 if (lp.maxWidth >= 0) {
256                     adjustedWidthSpec = MeasureSpec.makeMeasureSpec(
257                             Math.min(lp.maxWidth, width), MeasureSpec.EXACTLY);
258                 }
259                 if (lp.maxHeight >= 0) {
260                     adjustedHeightSpec = MeasureSpec.makeMeasureSpec(
261                             Math.min(lp.maxHeight, insetHeight), MeasureSpec.EXACTLY);
262                 }
263                 // measureChildWithMargins will resolve layout direction for the LayoutParams
264                 measureChildWithMargins(child, adjustedWidthSpec, 0, adjustedHeightSpec, 0);
265 
266                 // Only subtract out space from one dimension. Favor vertical.
267                 // Offset by 1.5x to add some balance along the other edge.
268                 if (Gravity.isVertical(lp.gravity)) {
269                     heightUsed += child.getMeasuredHeight() * 1.5f;
270                 } else if (Gravity.isHorizontal(lp.gravity)) {
271                     widthUsed += child.getMeasuredWidth() * 1.5f;
272                 }
273             } else if (lp.childType == LayoutParams.CHILD_TYPE_SCRIM) {
274                 setScrimView(child);
275                 child.measure(widthSpec, heightSpec);
276             }
277         }
278 
279         // Second pass. Measure everything that's left.
280         for (int i = 0; i < count; i++) {
281             final View child = getChildAt(i);
282             final LayoutParams lp = (LayoutParams) child.getLayoutParams();
283 
284             if (lp.childType == LayoutParams.CHILD_TYPE_USER_SWITCHER ||
285                     lp.childType == LayoutParams.CHILD_TYPE_SCRIM ||
286                     child.getVisibility() == GONE) {
287                 // Don't need to measure GONE children, and the user switcher was already measured.
288                 continue;
289             }
290 
291             final int virtualHeight = getVirtualHeight(lp, insetHeight, heightUsed);
292 
293             int adjustedWidthSpec;
294             int adjustedHeightSpec;
295             if (lp.centerWithinArea > 0) {
296                 if (mOrientation == HORIZONTAL) {
297                     adjustedWidthSpec = MeasureSpec.makeMeasureSpec(
298                             (int) ((width - widthUsed) * lp.centerWithinArea + 0.5f),
299                             MeasureSpec.EXACTLY);
300                     adjustedHeightSpec = MeasureSpec.makeMeasureSpec(
301                             virtualHeight, MeasureSpec.EXACTLY);
302                 } else {
303                     adjustedWidthSpec = MeasureSpec.makeMeasureSpec(
304                             width - widthUsed, MeasureSpec.EXACTLY);
305                     adjustedHeightSpec = MeasureSpec.makeMeasureSpec(
306                             (int) (virtualHeight * lp.centerWithinArea + 0.5f),
307                             MeasureSpec.EXACTLY);
308                 }
309             } else {
310                 adjustedWidthSpec = MeasureSpec.makeMeasureSpec(
311                         width - widthUsed, MeasureSpec.EXACTLY);
312                 adjustedHeightSpec = MeasureSpec.makeMeasureSpec(
313                         virtualHeight, MeasureSpec.EXACTLY);
314             }
315             if (lp.maxWidth >= 0) {
316                 adjustedWidthSpec = MeasureSpec.makeMeasureSpec(
317                         Math.min(lp.maxWidth, MeasureSpec.getSize(adjustedWidthSpec)),
318                         MeasureSpec.EXACTLY);
319             }
320             if (lp.maxHeight >= 0) {
321                 adjustedHeightSpec = MeasureSpec.makeMeasureSpec(
322                         Math.min(lp.maxHeight, MeasureSpec.getSize(adjustedHeightSpec)),
323                         MeasureSpec.EXACTLY);
324             }
325 
326             measureChildWithMargins(child, adjustedWidthSpec, 0, adjustedHeightSpec, 0);
327         }
328     }
329 
330     @Override
onLayout(boolean changed, int l, int t, int r, int b)331     protected void onLayout(boolean changed, int l, int t, int r, int b) {
332         final Rect padding = mTempRect;
333         padding.left = getPaddingLeft();
334         padding.top = getPaddingTop();
335         padding.right = getPaddingRight();
336         padding.bottom = getPaddingBottom();
337         final int width = r - l;
338         final int height = b - t;
339         final int insetHeight = height - mInsets.top - mInsets.bottom;
340 
341         // Reserve extra space in layout for the user switcher by modifying
342         // local padding during this layout pass
343         if (mUserSwitcherView != null && mUserSwitcherView.getVisibility() != GONE) {
344             layoutWithGravity(width, insetHeight, mUserSwitcherView, padding, true);
345         }
346 
347         final int count = getChildCount();
348         for (int i = 0; i < count; i++) {
349             final View child = getChildAt(i);
350             LayoutParams lp = (LayoutParams) child.getLayoutParams();
351 
352             // We did the user switcher above if we have one.
353             if (child == mUserSwitcherView || child.getVisibility() == GONE) continue;
354 
355             if (child == mScrimView) {
356                 child.layout(0, 0, width, height);
357                 continue;
358             } else if (lp.childType == LayoutParams.CHILD_TYPE_PAGE_DELETE_DROP_TARGET) {
359                 layoutWithGravity(width, insetHeight, child, mZeroPadding, false);
360                 continue;
361             }
362 
363             layoutWithGravity(width, insetHeight, child, padding, false);
364         }
365     }
366 
layoutWithGravity(int width, int height, View child, Rect padding, boolean adjustPadding)367     private void layoutWithGravity(int width, int height, View child, Rect padding,
368             boolean adjustPadding) {
369         final LayoutParams lp = (LayoutParams) child.getLayoutParams();
370 
371         final int heightUsed = padding.top + padding.bottom - getPaddingTop() - getPaddingBottom();
372         height = getVirtualHeight(lp, height, heightUsed);
373 
374         final int gravity = Gravity.getAbsoluteGravity(lp.gravity, getLayoutDirection());
375 
376         final boolean fixedLayoutSize = lp.centerWithinArea > 0;
377         final boolean fixedLayoutHorizontal = fixedLayoutSize && mOrientation == HORIZONTAL;
378         final boolean fixedLayoutVertical = fixedLayoutSize && mOrientation == VERTICAL;
379 
380         final int adjustedWidth;
381         final int adjustedHeight;
382         if (fixedLayoutHorizontal) {
383             final int paddedWidth = width - padding.left - padding.right;
384             adjustedWidth = (int) (paddedWidth * lp.centerWithinArea + 0.5f);
385             adjustedHeight = height;
386         } else if (fixedLayoutVertical) {
387             final int paddedHeight = height - getPaddingTop() - getPaddingBottom();
388             adjustedWidth = width;
389             adjustedHeight = (int) (paddedHeight * lp.centerWithinArea + 0.5f);
390         } else {
391             adjustedWidth = width;
392             adjustedHeight = height;
393         }
394 
395         final boolean isVertical = Gravity.isVertical(gravity);
396         final boolean isHorizontal = Gravity.isHorizontal(gravity);
397         final int childWidth = child.getMeasuredWidth();
398         final int childHeight = child.getMeasuredHeight();
399 
400         int left = padding.left;
401         int top = padding.top;
402         int right = left + childWidth;
403         int bottom = top + childHeight;
404         switch (gravity & Gravity.VERTICAL_GRAVITY_MASK) {
405             case Gravity.TOP:
406                 top = fixedLayoutVertical ?
407                         padding.top + (adjustedHeight - childHeight) / 2 : padding.top;
408                 bottom = top + childHeight;
409                 if (adjustPadding && isVertical) {
410                     padding.top = bottom;
411                     padding.bottom += childHeight / 2;
412                 }
413                 break;
414             case Gravity.BOTTOM:
415                 bottom = fixedLayoutVertical
416                         ? padding.top + height - (adjustedHeight - childHeight) / 2
417                         : padding.top + height;
418                 top = bottom - childHeight;
419                 if (adjustPadding && isVertical) {
420                     padding.bottom = height - top;
421                     padding.top += childHeight / 2;
422                 }
423                 break;
424             case Gravity.CENTER_VERTICAL:
425                 top = padding.top + (height - childHeight) / 2;
426                 bottom = top + childHeight;
427                 break;
428         }
429         switch (gravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
430             case Gravity.LEFT:
431                 left = fixedLayoutHorizontal ?
432                         padding.left + (adjustedWidth - childWidth) / 2 : padding.left;
433                 right = left + childWidth;
434                 if (adjustPadding && isHorizontal && !isVertical) {
435                     padding.left = right;
436                     padding.right += childWidth / 2;
437                 }
438                 break;
439             case Gravity.RIGHT:
440                 right = fixedLayoutHorizontal
441                         ? width - padding.right - (adjustedWidth - childWidth) / 2
442                         : width - padding.right;
443                 left = right - childWidth;
444                 if (adjustPadding && isHorizontal && !isVertical) {
445                     padding.right = width - left;
446                     padding.left += childWidth / 2;
447                 }
448                 break;
449             case Gravity.CENTER_HORIZONTAL:
450                 final int paddedWidth = width - padding.left - padding.right;
451                 left = (paddedWidth - childWidth) / 2;
452                 right = left + childWidth;
453                 break;
454         }
455         top += mInsets.top;
456         bottom += mInsets.top;
457         child.layout(left, top, right, bottom);
458     }
459 
460     @Override
generateLayoutParams(AttributeSet attrs)461     public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
462         return new LayoutParams(getContext(), attrs, this);
463     }
464 
465     @Override
generateLayoutParams(ViewGroup.LayoutParams p)466     protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
467         return p instanceof LayoutParams ? new LayoutParams((LayoutParams) p) :
468                 p instanceof MarginLayoutParams ? new LayoutParams((MarginLayoutParams) p) :
469                 new LayoutParams(p);
470     }
471 
472     @Override
generateDefaultLayoutParams()473     protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
474         return new LayoutParams();
475     }
476 
477     @Override
checkLayoutParams(ViewGroup.LayoutParams p)478     protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
479         return p instanceof LayoutParams;
480     }
481 
482     public static class LayoutParams extends MarginLayoutParams {
483 
484         public float centerWithinArea = 0;
485 
486         public int childType = 0;
487 
488         public static final int CHILD_TYPE_NONE = 0;
489         public static final int CHILD_TYPE_WIDGET = 1;
490         public static final int CHILD_TYPE_CHALLENGE = 2;
491         public static final int CHILD_TYPE_USER_SWITCHER = 3;
492         public static final int CHILD_TYPE_SCRIM = 4;
493         public static final int CHILD_TYPE_PAGE_DELETE_DROP_TARGET = 7;
494 
495         public int gravity = Gravity.NO_GRAVITY;
496 
497         public int maxWidth = -1;
498         public int maxHeight = -1;
499 
LayoutParams()500         public LayoutParams() {
501             this(WRAP_CONTENT, WRAP_CONTENT);
502         }
503 
LayoutParams(Context c, AttributeSet attrs, MultiPaneChallengeLayout parent)504         LayoutParams(Context c, AttributeSet attrs, MultiPaneChallengeLayout parent) {
505             super(c, attrs);
506 
507             final TypedArray a = c.obtainStyledAttributes(attrs,
508                     R.styleable.MultiPaneChallengeLayout_Layout);
509 
510             centerWithinArea = a.getFloat(
511                     R.styleable.MultiPaneChallengeLayout_Layout_layout_centerWithinArea, 0);
512             childType = a.getInt(R.styleable.MultiPaneChallengeLayout_Layout_layout_childType,
513                     CHILD_TYPE_NONE);
514             gravity = a.getInt(R.styleable.MultiPaneChallengeLayout_Layout_layout_gravity,
515                     Gravity.NO_GRAVITY);
516             maxWidth = a.getDimensionPixelSize(
517                     R.styleable.MultiPaneChallengeLayout_Layout_layout_maxWidth, -1);
518             maxHeight = a.getDimensionPixelSize(
519                     R.styleable.MultiPaneChallengeLayout_Layout_layout_maxHeight, -1);
520 
521             // Default gravity settings based on type and parent orientation
522             if (gravity == Gravity.NO_GRAVITY) {
523                 if (parent.mOrientation == HORIZONTAL) {
524                     switch (childType) {
525                         case CHILD_TYPE_WIDGET:
526                             gravity = Gravity.LEFT | Gravity.CENTER_VERTICAL;
527                             break;
528                         case CHILD_TYPE_CHALLENGE:
529                             gravity = Gravity.RIGHT | Gravity.CENTER_VERTICAL;
530                             break;
531                         case CHILD_TYPE_USER_SWITCHER:
532                             gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
533                             break;
534                     }
535                 } else {
536                     switch (childType) {
537                         case CHILD_TYPE_WIDGET:
538                             gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL;
539                             break;
540                         case CHILD_TYPE_CHALLENGE:
541                             gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
542                             break;
543                         case CHILD_TYPE_USER_SWITCHER:
544                             gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
545                             break;
546                     }
547                 }
548             }
549 
550             a.recycle();
551         }
552 
LayoutParams(int width, int height)553         public LayoutParams(int width, int height) {
554             super(width, height);
555         }
556 
LayoutParams(ViewGroup.LayoutParams source)557         public LayoutParams(ViewGroup.LayoutParams source) {
558             super(source);
559         }
560 
LayoutParams(MarginLayoutParams source)561         public LayoutParams(MarginLayoutParams source) {
562             super(source);
563         }
564 
LayoutParams(LayoutParams source)565         public LayoutParams(LayoutParams source) {
566             this((MarginLayoutParams) source);
567 
568             centerWithinArea = source.centerWithinArea;
569             childType = source.childType;
570             gravity = source.gravity;
571             maxWidth = source.maxWidth;
572             maxHeight = source.maxHeight;
573         }
574     }
575 }
576