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