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
17package com.android.keyguard;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ObjectAnimator;
22import android.content.Context;
23import android.content.res.Resources;
24import android.content.res.TypedArray;
25import android.graphics.Rect;
26import android.util.AttributeSet;
27import android.util.DisplayMetrics;
28import android.view.Gravity;
29import android.view.View;
30import android.view.ViewGroup;
31import android.widget.LinearLayout;
32
33public 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
61    public MultiPaneChallengeLayout(Context context) {
62        this(context, null);
63    }
64
65    public MultiPaneChallengeLayout(Context context, AttributeSet attrs) {
66        this(context, attrs, 0);
67    }
68
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
84    public void setInsets(Rect insets) {
85        mInsets.set(insets);
86    }
87
88    @Override
89    public boolean isChallengeShowing() {
90        return true;
91    }
92
93    @Override
94    public boolean isChallengeOverlapping() {
95        return false;
96    }
97
98    @Override
99    public void showChallenge(boolean b) {
100    }
101
102    @Override
103    public int getBouncerAnimationDuration() {
104        return ANIMATE_BOUNCE_DURATION;
105    }
106
107    @Override
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
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
156    public boolean isBouncing() {
157        return mIsBouncing;
158    }
159
160    @Override
161    public void setOnBouncerStateChangedListener(OnBouncerStateChangedListener listener) {
162        mBouncerListener = listener;
163    }
164
165    @Override
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
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
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
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
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
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
461    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
462        return new LayoutParams(getContext(), attrs, this);
463    }
464
465    @Override
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
473    protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
474        return new LayoutParams();
475    }
476
477    @Override
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
500        public LayoutParams() {
501            this(WRAP_CONTENT, WRAP_CONTENT);
502        }
503
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
553        public LayoutParams(int width, int height) {
554            super(width, height);
555        }
556
557        public LayoutParams(ViewGroup.LayoutParams source) {
558            super(source);
559        }
560
561        public LayoutParams(MarginLayoutParams source) {
562            super(source);
563        }
564
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