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