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.Canvas;
28import android.graphics.Paint;
29import android.util.AttributeSet;
30import android.util.DisplayMetrics;
31import android.util.FloatProperty;
32import android.util.Log;
33import android.util.Property;
34import android.view.MotionEvent;
35import android.view.VelocityTracker;
36import android.view.View;
37import android.view.ViewConfiguration;
38import android.view.ViewGroup;
39import android.view.accessibility.AccessibilityManager;
40import android.view.animation.Interpolator;
41import android.widget.Scroller;
42
43/**
44 * This layout handles interaction with the sliding security challenge views
45 * that overlay/resize other keyguard contents.
46 */
47public class SlidingChallengeLayout extends ViewGroup implements ChallengeLayout {
48    private static final String TAG = "SlidingChallengeLayout";
49    private static final boolean DEBUG = false;
50
51    // The drag handle is measured in dp above & below the top edge of the
52    // challenge view; these parameters change based on whether the challenge
53    // is open or closed.
54    private static final int DRAG_HANDLE_CLOSED_ABOVE = 8; // dp
55    private static final int DRAG_HANDLE_CLOSED_BELOW = 0; // dp
56    private static final int DRAG_HANDLE_OPEN_ABOVE = 8; // dp
57    private static final int DRAG_HANDLE_OPEN_BELOW = 0; // dp
58
59    private static final int HANDLE_ANIMATE_DURATION = 250; // ms
60
61    // Drawn to show the drag handle in closed state; crossfades to the challenge view
62    // when challenge is fully visible
63    private boolean mEdgeCaptured;
64
65    private DisplayMetrics mDisplayMetrics;
66
67    // Initialized during measurement from child layoutparams
68    private View mExpandChallengeView;
69    private KeyguardSecurityContainer mChallengeView;
70    private View mScrimView;
71    private View mWidgetsView;
72
73    // Range: 0 (fully hidden) to 1 (fully visible)
74    private float mChallengeOffset = 1.f;
75    private boolean mChallengeShowing = true;
76    private boolean mChallengeShowingTargetState = true;
77    private boolean mWasChallengeShowing = true;
78    private boolean mIsBouncing = false;
79
80    private final Scroller mScroller;
81    private ObjectAnimator mFader;
82    private int mScrollState;
83    private OnChallengeScrolledListener mScrollListener;
84    private OnBouncerStateChangedListener mBouncerListener;
85
86    public static final int SCROLL_STATE_IDLE = 0;
87    public static final int SCROLL_STATE_DRAGGING = 1;
88    public static final int SCROLL_STATE_SETTLING = 2;
89    public static final int SCROLL_STATE_FADING = 3;
90
91    private static final int CHALLENGE_FADE_OUT_DURATION = 100;
92    private static final int CHALLENGE_FADE_IN_DURATION = 160;
93
94    private static final int MAX_SETTLE_DURATION = 600; // ms
95
96    // ID of the pointer in charge of a current drag
97    private int mActivePointerId = INVALID_POINTER;
98    private static final int INVALID_POINTER = -1;
99
100    // True if the user is currently dragging the slider
101    private boolean mDragging;
102    // True if the user may not drag until a new gesture begins
103    private boolean mBlockDrag;
104
105    private VelocityTracker mVelocityTracker;
106    private int mMinVelocity;
107    private int mMaxVelocity;
108    private float mGestureStartX, mGestureStartY; // where did you first touch the screen?
109    private int mGestureStartChallengeBottom; // where was the challenge at that time?
110
111    private int mDragHandleClosedBelow; // handle hitrect extension into the challenge view
112    private int mDragHandleClosedAbove; // extend the handle's hitrect this far above the line
113    private int mDragHandleOpenBelow; // handle hitrect extension into the challenge view
114    private int mDragHandleOpenAbove; // extend the handle's hitrect this far above the line
115
116    private int mDragHandleEdgeSlop;
117    private int mChallengeBottomBound; // Number of pixels from the top of the challenge view
118                                       // that should remain on-screen
119
120    private int mTouchSlop;
121    private int mTouchSlopSquare;
122
123    float mHandleAlpha;
124    float mFrameAlpha;
125    float mFrameAnimationTarget = Float.MIN_VALUE;
126    private ObjectAnimator mHandleAnimation;
127    private ObjectAnimator mFrameAnimation;
128
129    private boolean mHasGlowpad;
130
131    // We have an internal and external version, and we and them together.
132    private boolean mChallengeInteractiveExternal = true;
133    private boolean mChallengeInteractiveInternal = true;
134
135    static final Property<SlidingChallengeLayout, Float> HANDLE_ALPHA =
136            new FloatProperty<SlidingChallengeLayout>("handleAlpha") {
137        @Override
138        public void setValue(SlidingChallengeLayout view, float value) {
139            view.mHandleAlpha = value;
140            view.invalidate();
141        }
142
143        @Override
144        public Float get(SlidingChallengeLayout view) {
145            return view.mHandleAlpha;
146        }
147    };
148
149    // True if at least one layout pass has happened since the view was attached.
150    private boolean mHasLayout;
151
152    private static final Interpolator sMotionInterpolator = new Interpolator() {
153        public float getInterpolation(float t) {
154            t -= 1.0f;
155            return t * t * t * t * t + 1.0f;
156        }
157    };
158
159    private static final Interpolator sHandleFadeInterpolator = new Interpolator() {
160        public float getInterpolation(float t) {
161            return t * t;
162        }
163    };
164
165    private final Runnable mEndScrollRunnable = new Runnable () {
166        public void run() {
167            completeChallengeScroll();
168        }
169    };
170
171    private final OnClickListener mScrimClickListener = new OnClickListener() {
172        @Override
173        public void onClick(View v) {
174            hideBouncer();
175        }
176    };
177
178    private final OnClickListener mExpandChallengeClickListener = new OnClickListener() {
179        @Override
180        public void onClick(View v) {
181            if (!isChallengeShowing()) {
182                showChallenge(true);
183            }
184        }
185    };
186
187    /**
188     * Listener interface that reports changes in scroll state of the challenge area.
189     */
190    public interface OnChallengeScrolledListener {
191        /**
192         * The scroll state itself changed.
193         *
194         * <p>scrollState will be one of the following:</p>
195         *
196         * <ul>
197         * <li><code>SCROLL_STATE_IDLE</code> - The challenge area is stationary.</li>
198         * <li><code>SCROLL_STATE_DRAGGING</code> - The user is actively dragging
199         * the challenge area.</li>
200         * <li><code>SCROLL_STATE_SETTLING</code> - The challenge area is animating
201         * into place.</li>
202         * </ul>
203         *
204         * <p>Do not perform expensive operations (e.g. layout)
205         * while the scroll state is not <code>SCROLL_STATE_IDLE</code>.</p>
206         *
207         * @param scrollState The new scroll state of the challenge area.
208         */
209        public void onScrollStateChanged(int scrollState);
210
211        /**
212         * The precise position of the challenge area has changed.
213         *
214         * <p>NOTE: It is NOT safe to modify layout or call any View methods that may
215         * result in a requestLayout anywhere in your view hierarchy as a result of this call.
216         * It may be called during drawing.</p>
217         *
218         * @param scrollPosition New relative position of the challenge area.
219         *                       1.f = fully visible/ready to be interacted with.
220         *                       0.f = fully invisible/inaccessible to the user.
221         * @param challengeTop Position of the top edge of the challenge view in px in the
222         *                     SlidingChallengeLayout's coordinate system.
223         */
224        public void onScrollPositionChanged(float scrollPosition, int challengeTop);
225    }
226
227    public SlidingChallengeLayout(Context context) {
228        this(context, null);
229    }
230
231    public SlidingChallengeLayout(Context context, AttributeSet attrs) {
232        this(context, attrs, 0);
233    }
234
235    public SlidingChallengeLayout(Context context, AttributeSet attrs, int defStyle) {
236        super(context, attrs, defStyle);
237
238        mScroller = new Scroller(context, sMotionInterpolator);
239
240        final ViewConfiguration vc = ViewConfiguration.get(context);
241        mMinVelocity = vc.getScaledMinimumFlingVelocity();
242        mMaxVelocity = vc.getScaledMaximumFlingVelocity();
243
244        final Resources res = getResources();
245        mDragHandleEdgeSlop = res.getDimensionPixelSize(R.dimen.kg_edge_swipe_region_size);
246
247        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
248        mTouchSlopSquare = mTouchSlop * mTouchSlop;
249
250        mDisplayMetrics = res.getDisplayMetrics();
251        final float density = mDisplayMetrics.density;
252
253        // top half of the lock icon, plus another 25% to be sure
254        mDragHandleClosedAbove = (int) (DRAG_HANDLE_CLOSED_ABOVE * density + 0.5f);
255        mDragHandleClosedBelow = (int) (DRAG_HANDLE_CLOSED_BELOW * density + 0.5f);
256        mDragHandleOpenAbove = (int) (DRAG_HANDLE_OPEN_ABOVE * density + 0.5f);
257        mDragHandleOpenBelow = (int) (DRAG_HANDLE_OPEN_BELOW * density + 0.5f);
258
259        // how much space to account for in the handle when closed
260        mChallengeBottomBound = res.getDimensionPixelSize(R.dimen.kg_widget_pager_bottom_padding);
261
262        setWillNotDraw(false);
263        setSystemUiVisibility(SYSTEM_UI_FLAG_LAYOUT_STABLE);
264    }
265
266    public void setHandleAlpha(float alpha) {
267        if (mExpandChallengeView != null) {
268            mExpandChallengeView.setAlpha(alpha);
269        }
270    }
271
272    public void setChallengeInteractive(boolean interactive) {
273        mChallengeInteractiveExternal = interactive;
274        if (mExpandChallengeView != null) {
275            mExpandChallengeView.setEnabled(interactive);
276        }
277    }
278
279    void animateHandle(boolean visible) {
280        if (mHandleAnimation != null) {
281            mHandleAnimation.cancel();
282            mHandleAnimation = null;
283        }
284        final float targetAlpha = visible ? 1.f : 0.f;
285        if (targetAlpha == mHandleAlpha) {
286            return;
287        }
288        mHandleAnimation = ObjectAnimator.ofFloat(this, HANDLE_ALPHA, targetAlpha);
289        mHandleAnimation.setInterpolator(sHandleFadeInterpolator);
290        mHandleAnimation.setDuration(HANDLE_ANIMATE_DURATION);
291        mHandleAnimation.start();
292    }
293
294    private void sendInitialListenerUpdates() {
295        if (mScrollListener != null) {
296            int challengeTop = mChallengeView != null ? mChallengeView.getTop() : 0;
297            mScrollListener.onScrollPositionChanged(mChallengeOffset, challengeTop);
298            mScrollListener.onScrollStateChanged(mScrollState);
299        }
300    }
301
302    public void setOnChallengeScrolledListener(OnChallengeScrolledListener listener) {
303        mScrollListener = listener;
304        if (mHasLayout) {
305            sendInitialListenerUpdates();
306        }
307    }
308
309    public void setOnBouncerStateChangedListener(OnBouncerStateChangedListener listener) {
310        mBouncerListener = listener;
311    }
312
313    @Override
314    public void onAttachedToWindow() {
315        super.onAttachedToWindow();
316
317        mHasLayout = false;
318    }
319
320    @Override
321    public void onDetachedFromWindow() {
322        super.onDetachedFromWindow();
323
324        removeCallbacks(mEndScrollRunnable);
325        mHasLayout = false;
326    }
327
328    @Override
329    public void requestChildFocus(View child, View focused) {
330        if (mIsBouncing && child != mChallengeView) {
331            // Clear out of the bouncer if the user tries to move focus outside of
332            // the security challenge view.
333            hideBouncer();
334        }
335        super.requestChildFocus(child, focused);
336    }
337
338    // We want the duration of the page snap animation to be influenced by the distance that
339    // the screen has to travel, however, we don't want this duration to be effected in a
340    // purely linear fashion. Instead, we use this method to moderate the effect that the distance
341    // of travel has on the overall snap duration.
342    float distanceInfluenceForSnapDuration(float f) {
343        f -= 0.5f; // center the values about 0.
344        f *= 0.3f * Math.PI / 2.0f;
345        return (float) Math.sin(f);
346    }
347
348    void setScrollState(int state) {
349        if (mScrollState != state) {
350            mScrollState = state;
351
352            animateHandle(state == SCROLL_STATE_IDLE && !mChallengeShowing);
353            if (mScrollListener != null) {
354                mScrollListener.onScrollStateChanged(state);
355            }
356        }
357    }
358
359    void completeChallengeScroll() {
360        setChallengeShowing(mChallengeShowingTargetState);
361        mChallengeOffset = mChallengeShowing ? 1.f : 0.f;
362        setScrollState(SCROLL_STATE_IDLE);
363        mChallengeInteractiveInternal = true;
364        mChallengeView.setLayerType(LAYER_TYPE_NONE, null);
365    }
366
367    void setScrimView(View scrim) {
368        if (mScrimView != null) {
369            mScrimView.setOnClickListener(null);
370        }
371        mScrimView = scrim;
372        mScrimView.setVisibility(mIsBouncing ? VISIBLE : GONE);
373        mScrimView.setFocusable(true);
374        mScrimView.setOnClickListener(mScrimClickListener);
375    }
376
377    /**
378     * Animate the bottom edge of the challenge view to the given position.
379     *
380     * @param y desired final position for the bottom edge of the challenge view in px
381     * @param velocity velocity in
382     */
383    void animateChallengeTo(int y, int velocity) {
384        if (mChallengeView == null) {
385            // Nothing to do.
386            return;
387        }
388
389        cancelTransitionsInProgress();
390
391        mChallengeInteractiveInternal = false;
392        mChallengeView.setLayerType(LAYER_TYPE_HARDWARE, null);
393        final int sy = mChallengeView.getBottom();
394        final int dy = y - sy;
395        if (dy == 0) {
396            completeChallengeScroll();
397            return;
398        }
399
400        setScrollState(SCROLL_STATE_SETTLING);
401
402        final int childHeight = mChallengeView.getHeight();
403        final int halfHeight = childHeight / 2;
404        final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dy) / childHeight);
405        final float distance = halfHeight + halfHeight *
406                distanceInfluenceForSnapDuration(distanceRatio);
407
408        int duration = 0;
409        velocity = Math.abs(velocity);
410        if (velocity > 0) {
411            duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
412        } else {
413            final float childDelta = (float) Math.abs(dy) / childHeight;
414            duration = (int) ((childDelta + 1) * 100);
415        }
416        duration = Math.min(duration, MAX_SETTLE_DURATION);
417
418        mScroller.startScroll(0, sy, 0, dy, duration);
419        postInvalidateOnAnimation();
420    }
421
422    private void setChallengeShowing(boolean showChallenge) {
423        if (mChallengeShowing == showChallenge) {
424            return;
425        }
426        mChallengeShowing = showChallenge;
427
428        if (mExpandChallengeView == null || mChallengeView == null) {
429            // These might not be here yet if we haven't been through layout.
430            // If we haven't, the first layout pass will set everything up correctly
431            // based on mChallengeShowing as set above.
432            return;
433        }
434
435        if (mChallengeShowing) {
436            mExpandChallengeView.setVisibility(View.INVISIBLE);
437            mChallengeView.setVisibility(View.VISIBLE);
438            if (AccessibilityManager.getInstance(mContext).isEnabled()) {
439                mChallengeView.requestAccessibilityFocus();
440                mChallengeView.announceForAccessibility(mContext.getString(
441                        R.string.keyguard_accessibility_unlock_area_expanded));
442            }
443        } else {
444            mExpandChallengeView.setVisibility(View.VISIBLE);
445            mChallengeView.setVisibility(View.INVISIBLE);
446            if (AccessibilityManager.getInstance(mContext).isEnabled()) {
447                mExpandChallengeView.requestAccessibilityFocus();
448                mChallengeView.announceForAccessibility(mContext.getString(
449                        R.string.keyguard_accessibility_unlock_area_collapsed));
450            }
451        }
452    }
453
454    /**
455     * @return true if the challenge is at all visible.
456     */
457    public boolean isChallengeShowing() {
458        return mChallengeShowing;
459    }
460
461    @Override
462    public boolean isChallengeOverlapping() {
463        return mChallengeShowing;
464    }
465
466    @Override
467    public boolean isBouncing() {
468        return mIsBouncing;
469    }
470
471    @Override
472    public int getBouncerAnimationDuration() {
473        return HANDLE_ANIMATE_DURATION;
474    }
475
476    @Override
477    public void showBouncer() {
478        if (mIsBouncing) return;
479        mWasChallengeShowing = mChallengeShowing;
480        mIsBouncing = true;
481        showChallenge(true);
482        if (mScrimView != null) {
483            Animator anim = ObjectAnimator.ofFloat(mScrimView, "alpha", 1f);
484            anim.setDuration(HANDLE_ANIMATE_DURATION);
485            anim.addListener(new AnimatorListenerAdapter() {
486                @Override
487                public void onAnimationStart(Animator animation) {
488                    mScrimView.setVisibility(VISIBLE);
489                }
490            });
491            anim.start();
492        }
493        if (mChallengeView != null) {
494            mChallengeView.showBouncer(HANDLE_ANIMATE_DURATION);
495        }
496
497        if (mBouncerListener != null) {
498            mBouncerListener.onBouncerStateChanged(true);
499        }
500    }
501
502    @Override
503    public void hideBouncer() {
504        if (!mIsBouncing) return;
505        if (!mWasChallengeShowing) showChallenge(false);
506        mIsBouncing = false;
507
508        if (mScrimView != null) {
509            Animator anim = ObjectAnimator.ofFloat(mScrimView, "alpha", 0f);
510            anim.setDuration(HANDLE_ANIMATE_DURATION);
511            anim.addListener(new AnimatorListenerAdapter() {
512                @Override
513                public void onAnimationEnd(Animator animation) {
514                    mScrimView.setVisibility(GONE);
515                }
516            });
517            anim.start();
518        }
519        if (mChallengeView != null) {
520            mChallengeView.hideBouncer(HANDLE_ANIMATE_DURATION);
521        }
522        if (mBouncerListener != null) {
523            mBouncerListener.onBouncerStateChanged(false);
524        }
525    }
526
527    private int getChallengeMargin(boolean expanded) {
528        return expanded && mHasGlowpad ? 0 : mDragHandleEdgeSlop;
529    }
530
531    private float getChallengeAlpha() {
532        float x = mChallengeOffset - 1;
533        return x * x * x + 1.f;
534    }
535
536    @Override
537    public void requestDisallowInterceptTouchEvent(boolean allowIntercept) {
538        // We'll intercept whoever we feel like! ...as long as it isn't a challenge view.
539        // If there are one or more pointers in the challenge view before we take over
540        // touch events, onInterceptTouchEvent will set mBlockDrag.
541    }
542
543    @Override
544    public boolean onInterceptTouchEvent(MotionEvent ev) {
545        if (mVelocityTracker == null) {
546            mVelocityTracker = VelocityTracker.obtain();
547        }
548        mVelocityTracker.addMovement(ev);
549
550        final int action = ev.getActionMasked();
551        switch (action) {
552            case MotionEvent.ACTION_DOWN:
553                mGestureStartX = ev.getX();
554                mGestureStartY = ev.getY();
555                mBlockDrag = false;
556                break;
557
558            case MotionEvent.ACTION_CANCEL:
559            case MotionEvent.ACTION_UP:
560                resetTouch();
561                break;
562
563            case MotionEvent.ACTION_MOVE:
564                final int count = ev.getPointerCount();
565                for (int i = 0; i < count; i++) {
566                    final float x = ev.getX(i);
567                    final float y = ev.getY(i);
568                    if (!mIsBouncing && mActivePointerId == INVALID_POINTER
569                                && (crossedDragHandle(x, y, mGestureStartY)
570                                || (isInChallengeView(x, y) &&
571                                        mScrollState == SCROLL_STATE_SETTLING))) {
572                        mActivePointerId = ev.getPointerId(i);
573                        mGestureStartX = x;
574                        mGestureStartY = y;
575                        mGestureStartChallengeBottom = getChallengeBottom();
576                        mDragging = true;
577                        mChallengeView.setLayerType(LAYER_TYPE_HARDWARE, null);
578                    } else if (mChallengeShowing && isInChallengeView(x, y)) {
579                        mBlockDrag = true;
580                    }
581                }
582                break;
583        }
584
585        if (mBlockDrag || isChallengeInteractionBlocked()) {
586            mActivePointerId = INVALID_POINTER;
587            mDragging = false;
588        }
589
590        return mDragging;
591    }
592
593    private boolean isChallengeInteractionBlocked() {
594        return !mChallengeInteractiveExternal || !mChallengeInteractiveInternal;
595    }
596
597    private void resetTouch() {
598        mVelocityTracker.recycle();
599        mVelocityTracker = null;
600        mActivePointerId = INVALID_POINTER;
601        mDragging = mBlockDrag = false;
602    }
603
604    @Override
605    public boolean onTouchEvent(MotionEvent ev) {
606        if (mVelocityTracker == null) {
607            mVelocityTracker = VelocityTracker.obtain();
608        }
609        mVelocityTracker.addMovement(ev);
610
611        final int action = ev.getActionMasked();
612        switch (action) {
613            case MotionEvent.ACTION_DOWN:
614                mBlockDrag = false;
615                mGestureStartX = ev.getX();
616                mGestureStartY = ev.getY();
617                break;
618
619            case MotionEvent.ACTION_CANCEL:
620                if (mDragging && !isChallengeInteractionBlocked()) {
621                    showChallenge(0);
622                }
623                resetTouch();
624                break;
625
626            case MotionEvent.ACTION_POINTER_UP:
627                if (mActivePointerId != ev.getPointerId(ev.getActionIndex())) {
628                    break;
629                }
630            case MotionEvent.ACTION_UP:
631                if (mDragging && !isChallengeInteractionBlocked()) {
632                    mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
633                    showChallenge((int) mVelocityTracker.getYVelocity(mActivePointerId));
634                }
635                resetTouch();
636                break;
637
638            case MotionEvent.ACTION_MOVE:
639                if (!mDragging && !mBlockDrag && !mIsBouncing) {
640                    final int count = ev.getPointerCount();
641                    for (int i = 0; i < count; i++) {
642                        final float x = ev.getX(i);
643                        final float y = ev.getY(i);
644
645                        if ((isInDragHandle(x, y) || crossedDragHandle(x, y, mGestureStartY) ||
646                                (isInChallengeView(x, y) && mScrollState == SCROLL_STATE_SETTLING))
647                                && mActivePointerId == INVALID_POINTER
648                                && !isChallengeInteractionBlocked()) {
649                            mGestureStartX = x;
650                            mGestureStartY = y;
651                            mActivePointerId = ev.getPointerId(i);
652                            mGestureStartChallengeBottom = getChallengeBottom();
653                            mDragging = true;
654                            mChallengeView.setLayerType(LAYER_TYPE_HARDWARE, null);
655                            break;
656                        }
657                    }
658                }
659                // Not an else; this can be set above.
660                if (mDragging) {
661                    // No-op if already in this state, but set it here in case we arrived
662                    // at this point from either intercept or the above.
663                    setScrollState(SCROLL_STATE_DRAGGING);
664
665                    final int index = ev.findPointerIndex(mActivePointerId);
666                    if (index < 0) {
667                        // Oops, bogus state. We lost some touch events somewhere.
668                        // Just drop it with no velocity and let things settle.
669                        resetTouch();
670                        showChallenge(0);
671                        return true;
672                    }
673                    final float y = ev.getY(index);
674                    final float pos = Math.min(y - mGestureStartY,
675                            getLayoutBottom() - mChallengeBottomBound);
676
677                    moveChallengeTo(mGestureStartChallengeBottom + (int) pos);
678                }
679                break;
680        }
681        return true;
682    }
683
684    /**
685     * The lifecycle of touch events is subtle and it's very easy to do something
686     * that will cause bugs that will be nasty to track when overriding this method.
687     * Normally one should always override onInterceptTouchEvent instead.
688     *
689     * To put it another way, don't try this at home.
690     */
691    @Override
692    public boolean dispatchTouchEvent(MotionEvent ev) {
693        final int action = ev.getActionMasked();
694        boolean handled = false;
695        if (action == MotionEvent.ACTION_DOWN) {
696            // Defensive programming: if we didn't get the UP or CANCEL, reset anyway.
697            mEdgeCaptured = false;
698        }
699        if (mWidgetsView != null && !mIsBouncing && (mEdgeCaptured || isEdgeSwipeBeginEvent(ev))) {
700            // Normally we would need to do a lot of extra stuff here.
701            // We can only get away with this because we haven't padded in
702            // the widget pager or otherwise transformed it during layout.
703            // We also don't support things like splitting MotionEvents.
704
705            // We set handled to captured even if dispatch is returning false here so that
706            // we don't send a different view a busted or incomplete event stream.
707            handled = mEdgeCaptured |= mWidgetsView.dispatchTouchEvent(ev);
708        }
709
710        if (!handled && !mEdgeCaptured) {
711            handled = super.dispatchTouchEvent(ev);
712        }
713
714        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
715            mEdgeCaptured = false;
716        }
717
718        return handled;
719    }
720
721    private boolean isEdgeSwipeBeginEvent(MotionEvent ev) {
722        if (ev.getActionMasked() != MotionEvent.ACTION_DOWN) {
723            return false;
724        }
725
726        final float x = ev.getX();
727        return x < mDragHandleEdgeSlop || x >= getWidth() - mDragHandleEdgeSlop;
728    }
729
730    /**
731     * We only want to add additional vertical space to the drag handle when the panel is fully
732     * closed.
733     */
734    private int getDragHandleSizeAbove() {
735        return isChallengeShowing() ? mDragHandleOpenAbove : mDragHandleClosedAbove;
736    }
737    private int getDragHandleSizeBelow() {
738        return isChallengeShowing() ? mDragHandleOpenBelow : mDragHandleClosedBelow;
739    }
740
741    private boolean isInChallengeView(float x, float y) {
742        return isPointInView(x, y, mChallengeView);
743    }
744
745    private boolean isInDragHandle(float x, float y) {
746        return isPointInView(x, y, mExpandChallengeView);
747    }
748
749    private boolean isPointInView(float x, float y, View view) {
750        if (view == null) {
751            return false;
752        }
753        return x >= view.getLeft() && y >= view.getTop()
754                && x < view.getRight() && y < view.getBottom();
755    }
756
757    private boolean crossedDragHandle(float x, float y, float initialY) {
758
759        final int challengeTop = mChallengeView.getTop();
760        final boolean horizOk = x >= 0 && x < getWidth();
761
762        final boolean vertOk;
763        if (mChallengeShowing) {
764            vertOk = initialY < (challengeTop - getDragHandleSizeAbove()) &&
765                    y > challengeTop + getDragHandleSizeBelow();
766        } else {
767            vertOk = initialY > challengeTop + getDragHandleSizeBelow() &&
768                    y < challengeTop - getDragHandleSizeAbove();
769        }
770        return horizOk && vertOk;
771    }
772
773    private int makeChildMeasureSpec(int maxSize, int childDimen) {
774        final int mode;
775        final int size;
776        switch (childDimen) {
777            case LayoutParams.WRAP_CONTENT:
778                mode = MeasureSpec.AT_MOST;
779                size = maxSize;
780                break;
781            case LayoutParams.MATCH_PARENT:
782                mode = MeasureSpec.EXACTLY;
783                size = maxSize;
784                break;
785            default:
786                mode = MeasureSpec.EXACTLY;
787                size = Math.min(maxSize, childDimen);
788                break;
789        }
790        return MeasureSpec.makeMeasureSpec(size, mode);
791    }
792
793    @Override
794    protected void onMeasure(int widthSpec, int heightSpec) {
795        if (MeasureSpec.getMode(widthSpec) != MeasureSpec.EXACTLY ||
796                MeasureSpec.getMode(heightSpec) != MeasureSpec.EXACTLY) {
797            throw new IllegalArgumentException(
798                    "SlidingChallengeLayout must be measured with an exact size");
799        }
800
801        final int width = MeasureSpec.getSize(widthSpec);
802        final int height = MeasureSpec.getSize(heightSpec);
803        setMeasuredDimension(width, height);
804
805        // Find one and only one challenge view.
806        final View oldChallengeView = mChallengeView;
807        final View oldExpandChallengeView = mChallengeView;
808        mChallengeView = null;
809        mExpandChallengeView = null;
810        final int count = getChildCount();
811
812        // First iteration through the children finds special children and sets any associated
813        // state.
814        for (int i = 0; i < count; i++) {
815            final View child = getChildAt(i);
816            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
817            if (lp.childType == LayoutParams.CHILD_TYPE_CHALLENGE) {
818                if (mChallengeView != null) {
819                    throw new IllegalStateException(
820                            "There may only be one child with layout_isChallenge=\"true\"");
821                }
822                if (!(child instanceof KeyguardSecurityContainer)) {
823                            throw new IllegalArgumentException(
824                                    "Challenge must be a KeyguardSecurityContainer");
825                }
826                mChallengeView = (KeyguardSecurityContainer) child;
827                if (mChallengeView != oldChallengeView) {
828                    mChallengeView.setVisibility(mChallengeShowing ? VISIBLE : INVISIBLE);
829                }
830                // We're going to play silly games with the frame's background drawable later.
831                if (!mHasLayout) {
832                    // Set up the margin correctly based on our content for the first run.
833                    mHasGlowpad = child.findViewById(R.id.keyguard_selector_view) != null;
834                    lp.leftMargin = lp.rightMargin = getChallengeMargin(true);
835                }
836            } else if (lp.childType == LayoutParams.CHILD_TYPE_EXPAND_CHALLENGE_HANDLE) {
837                if (mExpandChallengeView != null) {
838                    throw new IllegalStateException(
839                            "There may only be one child with layout_childType"
840                            + "=\"expandChallengeHandle\"");
841                }
842                mExpandChallengeView = child;
843                if (mExpandChallengeView != oldExpandChallengeView) {
844                    mExpandChallengeView.setVisibility(mChallengeShowing ? INVISIBLE : VISIBLE);
845                    mExpandChallengeView.setOnClickListener(mExpandChallengeClickListener);
846                }
847            } else if (lp.childType == LayoutParams.CHILD_TYPE_SCRIM) {
848                setScrimView(child);
849            } else if (lp.childType == LayoutParams.CHILD_TYPE_WIDGETS) {
850                mWidgetsView = child;
851            }
852        }
853
854        // We want to measure the challenge view first, since the KeyguardWidgetPager
855        // needs to do things its measure pass that are dependent on the challenge view
856        // having been measured.
857        if (mChallengeView != null && mChallengeView.getVisibility() != View.GONE) {
858            // This one's a little funny. If the IME is present - reported in the form
859            // of insets on the root view - we only give the challenge the space it would
860            // have had if the IME wasn't there in order to keep the rest of the layout stable.
861            // We base this on the layout_maxHeight on the challenge view. If it comes out
862            // negative or zero, either we didn't have a maxHeight or we're totally out of space,
863            // so give up and measure as if this rule weren't there.
864            int challengeHeightSpec = heightSpec;
865            final View root = getRootView();
866            if (root != null) {
867                final LayoutParams lp = (LayoutParams) mChallengeView.getLayoutParams();
868                final int specSize = MeasureSpec.getSize(heightSpec);
869                final int windowHeight = mDisplayMetrics.heightPixels - root.getPaddingTop();
870                final int diff = windowHeight - specSize;
871                final int maxChallengeHeight = lp.maxHeight - diff;
872                if (maxChallengeHeight > 0) {
873                    challengeHeightSpec = makeChildMeasureSpec(maxChallengeHeight, lp.height);
874                }
875            }
876            measureChildWithMargins(mChallengeView, widthSpec, 0, challengeHeightSpec, 0);
877        }
878
879        // Measure the rest of the children
880        for (int i = 0; i < count; i++) {
881            final View child = getChildAt(i);
882            if (child.getVisibility() == GONE) {
883                continue;
884            }
885            // Don't measure the challenge view twice!
886            if (child == mChallengeView) continue;
887
888            // Measure children. Widget frame measures special, so that we can ignore
889            // insets for the IME.
890            int parentWidthSpec = widthSpec, parentHeightSpec = heightSpec;
891            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
892            if (lp.childType == LayoutParams.CHILD_TYPE_WIDGETS) {
893                final View root = getRootView();
894                if (root != null) {
895                    // This calculation is super dodgy and relies on several assumptions.
896                    // Specifically that the root of the window will be padded in for insets
897                    // and that the window is LAYOUT_IN_SCREEN.
898                    final int windowWidth = mDisplayMetrics.widthPixels;
899                    final int windowHeight = mDisplayMetrics.heightPixels - root.getPaddingTop();
900                    parentWidthSpec = MeasureSpec.makeMeasureSpec(
901                            windowWidth, MeasureSpec.EXACTLY);
902                    parentHeightSpec = MeasureSpec.makeMeasureSpec(
903                            windowHeight, MeasureSpec.EXACTLY);
904                }
905            }
906            measureChildWithMargins(child, parentWidthSpec, 0, parentHeightSpec, 0);
907        }
908    }
909
910    @Override
911    protected void onLayout(boolean changed, int l, int t, int r, int b) {
912        final int paddingLeft = getPaddingLeft();
913        final int paddingTop = getPaddingTop();
914        final int paddingRight = getPaddingRight();
915        final int paddingBottom = getPaddingBottom();
916        final int width = r - l;
917        final int height = b - t;
918
919        final int count = getChildCount();
920        for (int i = 0; i < count; i++) {
921            final View child = getChildAt(i);
922
923            if (child.getVisibility() == GONE) continue;
924
925            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
926
927            if (lp.childType == LayoutParams.CHILD_TYPE_CHALLENGE) {
928                // Challenge views pin to the bottom, offset by a portion of their height,
929                // and center horizontally.
930                final int center = (paddingLeft + width - paddingRight) / 2;
931                final int childWidth = child.getMeasuredWidth();
932                final int childHeight = child.getMeasuredHeight();
933                final int left = center - childWidth / 2;
934                final int layoutBottom = height - paddingBottom - lp.bottomMargin;
935                // We use the top of the challenge view to position the handle, so
936                // we never want less than the handle size showing at the bottom.
937                final int bottom = layoutBottom + (int) ((childHeight - mChallengeBottomBound)
938                        * (1 - mChallengeOffset));
939                child.setAlpha(getChallengeAlpha());
940                child.layout(left, bottom - childHeight, left + childWidth, bottom);
941            } else if (lp.childType == LayoutParams.CHILD_TYPE_EXPAND_CHALLENGE_HANDLE) {
942                final int center = (paddingLeft + width - paddingRight) / 2;
943                final int left = center - child.getMeasuredWidth() / 2;
944                final int right = left + child.getMeasuredWidth();
945                final int bottom = height - paddingBottom - lp.bottomMargin;
946                final int top = bottom - child.getMeasuredHeight();
947                child.layout(left, top, right, bottom);
948            } else {
949                // Non-challenge views lay out from the upper left, layered.
950                child.layout(paddingLeft + lp.leftMargin,
951                        paddingTop + lp.topMargin,
952                        paddingLeft + child.getMeasuredWidth(),
953                        paddingTop + child.getMeasuredHeight());
954            }
955        }
956
957        if (!mHasLayout) {
958            mHasLayout = true;
959        }
960    }
961
962    @Override
963    public void draw(Canvas c) {
964        super.draw(c);
965        if (DEBUG) {
966            final Paint debugPaint = new Paint();
967            debugPaint.setColor(0x40FF00CC);
968            // show the isInDragHandle() rect
969            c.drawRect(mDragHandleEdgeSlop,
970                    mChallengeView.getTop() - getDragHandleSizeAbove(),
971                    getWidth() - mDragHandleEdgeSlop,
972                    mChallengeView.getTop() + getDragHandleSizeBelow(),
973                    debugPaint);
974        }
975    }
976
977    public void computeScroll() {
978        super.computeScroll();
979
980        if (!mScroller.isFinished()) {
981            if (mChallengeView == null) {
982                // Can't scroll if the view is missing.
983                Log.e(TAG, "Challenge view missing in computeScroll");
984                mScroller.abortAnimation();
985                return;
986            }
987
988            mScroller.computeScrollOffset();
989            moveChallengeTo(mScroller.getCurrY());
990
991            if (mScroller.isFinished()) {
992                post(mEndScrollRunnable);
993            }
994        }
995    }
996
997    private void cancelTransitionsInProgress() {
998        if (!mScroller.isFinished()) {
999            mScroller.abortAnimation();
1000            completeChallengeScroll();
1001        }
1002        if (mFader != null) {
1003            mFader.cancel();
1004        }
1005    }
1006
1007    public void fadeInChallenge() {
1008        fadeChallenge(true);
1009    }
1010
1011    public void fadeOutChallenge() {
1012        fadeChallenge(false);
1013    }
1014
1015    public void fadeChallenge(final boolean show) {
1016        if (mChallengeView != null) {
1017
1018            cancelTransitionsInProgress();
1019            float alpha = show ? 1f : 0f;
1020            int duration = show ? CHALLENGE_FADE_IN_DURATION : CHALLENGE_FADE_OUT_DURATION;
1021            mFader = ObjectAnimator.ofFloat(mChallengeView, "alpha", alpha);
1022            mFader.addListener(new AnimatorListenerAdapter() {
1023                @Override
1024                public void onAnimationStart(Animator animation) {
1025                    onFadeStart(show);
1026                }
1027                @Override
1028                public void onAnimationEnd(Animator animation) {
1029                    onFadeEnd(show);
1030                }
1031            });
1032            mFader.setDuration(duration);
1033            mFader.start();
1034        }
1035    }
1036
1037    private int getMaxChallengeBottom() {
1038        if (mChallengeView == null) return 0;
1039        final int layoutBottom = getLayoutBottom();
1040        final int challengeHeight = mChallengeView.getMeasuredHeight();
1041
1042        return (layoutBottom + challengeHeight - mChallengeBottomBound);
1043    }
1044
1045    private int getMinChallengeBottom() {
1046        return getLayoutBottom();
1047    }
1048
1049
1050    private void onFadeStart(boolean show) {
1051        mChallengeInteractiveInternal = false;
1052        mChallengeView.setLayerType(LAYER_TYPE_HARDWARE, null);
1053
1054        if (show) {
1055            moveChallengeTo(getMinChallengeBottom());
1056        }
1057
1058        setScrollState(SCROLL_STATE_FADING);
1059    }
1060
1061    private void onFadeEnd(boolean show) {
1062        mChallengeInteractiveInternal = true;
1063        setChallengeShowing(show);
1064
1065        if (!show) {
1066            moveChallengeTo(getMaxChallengeBottom());
1067        }
1068
1069        mChallengeView.setLayerType(LAYER_TYPE_NONE, null);
1070        mFader = null;
1071        setScrollState(SCROLL_STATE_IDLE);
1072    }
1073
1074    public int getMaxChallengeTop() {
1075        if (mChallengeView == null) return 0;
1076
1077        final int layoutBottom = getLayoutBottom();
1078        final int challengeHeight = mChallengeView.getMeasuredHeight();
1079        return layoutBottom - challengeHeight;
1080    }
1081
1082    /**
1083     * Move the bottom edge of mChallengeView to a new position and notify the listener
1084     * if it represents a change in position. Changes made through this method will
1085     * be stable across layout passes. If this method is called before first layout of
1086     * this SlidingChallengeLayout it will have no effect.
1087     *
1088     * @param bottom New bottom edge in px in this SlidingChallengeLayout's coordinate system.
1089     * @return true if the challenge view was moved
1090     */
1091    private boolean moveChallengeTo(int bottom) {
1092        if (mChallengeView == null || !mHasLayout) {
1093            return false;
1094        }
1095
1096        final int layoutBottom = getLayoutBottom();
1097        final int challengeHeight = mChallengeView.getHeight();
1098
1099        bottom = Math.max(getMinChallengeBottom(),
1100                Math.min(bottom, getMaxChallengeBottom()));
1101
1102        float offset = 1.f - (float) (bottom - layoutBottom) /
1103                (challengeHeight - mChallengeBottomBound);
1104        mChallengeOffset = offset;
1105        if (offset > 0 && !mChallengeShowing) {
1106            setChallengeShowing(true);
1107        }
1108
1109        mChallengeView.layout(mChallengeView.getLeft(),
1110                bottom - mChallengeView.getHeight(), mChallengeView.getRight(), bottom);
1111
1112        mChallengeView.setAlpha(getChallengeAlpha());
1113        if (mScrollListener != null) {
1114            mScrollListener.onScrollPositionChanged(offset, mChallengeView.getTop());
1115        }
1116        postInvalidateOnAnimation();
1117        return true;
1118    }
1119
1120    /**
1121     * The bottom edge of this SlidingChallengeLayout's coordinate system; will coincide with
1122     * the bottom edge of mChallengeView when the challenge is fully opened.
1123     */
1124    private int getLayoutBottom() {
1125        final int bottomMargin = (mChallengeView == null)
1126                ? 0
1127                : ((LayoutParams) mChallengeView.getLayoutParams()).bottomMargin;
1128        final int layoutBottom = getMeasuredHeight() - getPaddingBottom() - bottomMargin;
1129        return layoutBottom;
1130    }
1131
1132    /**
1133     * The bottom edge of mChallengeView; essentially, where the sliding challenge 'is'.
1134     */
1135    private int getChallengeBottom() {
1136        if (mChallengeView == null) return 0;
1137
1138        return mChallengeView.getBottom();
1139    }
1140
1141    /**
1142     * Show or hide the challenge view, animating it if necessary.
1143     * @param show true to show, false to hide
1144     */
1145    public void showChallenge(boolean show) {
1146        showChallenge(show, 0);
1147        if (!show) {
1148            // Block any drags in progress so that callers can use this to disable dragging
1149            // for other touch interactions.
1150            mBlockDrag = true;
1151        }
1152    }
1153
1154    private void showChallenge(int velocity) {
1155        boolean show = false;
1156        if (Math.abs(velocity) > mMinVelocity) {
1157            show = velocity < 0;
1158        } else {
1159            show = mChallengeOffset >= 0.5f;
1160        }
1161        showChallenge(show, velocity);
1162    }
1163
1164    private void showChallenge(boolean show, int velocity) {
1165        if (mChallengeView == null) {
1166            setChallengeShowing(false);
1167            return;
1168        }
1169
1170        if (mHasLayout) {
1171            mChallengeShowingTargetState = show;
1172            final int layoutBottom = getLayoutBottom();
1173            animateChallengeTo(show ? layoutBottom :
1174                    layoutBottom + mChallengeView.getHeight() - mChallengeBottomBound, velocity);
1175        }
1176    }
1177
1178    @Override
1179    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
1180        return new LayoutParams(getContext(), attrs);
1181    }
1182
1183    @Override
1184    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
1185        return p instanceof LayoutParams ? new LayoutParams((LayoutParams) p) :
1186                p instanceof MarginLayoutParams ? new LayoutParams((MarginLayoutParams) p) :
1187                new LayoutParams(p);
1188    }
1189
1190    @Override
1191    protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
1192        return new LayoutParams();
1193    }
1194
1195    @Override
1196    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
1197        return p instanceof LayoutParams;
1198    }
1199
1200    public static class LayoutParams extends MarginLayoutParams {
1201        public int childType = CHILD_TYPE_NONE;
1202        public static final int CHILD_TYPE_NONE = 0;
1203        public static final int CHILD_TYPE_CHALLENGE = 2;
1204        public static final int CHILD_TYPE_SCRIM = 4;
1205        public static final int CHILD_TYPE_WIDGETS = 5;
1206        public static final int CHILD_TYPE_EXPAND_CHALLENGE_HANDLE = 6;
1207
1208        public int maxHeight;
1209
1210        public LayoutParams() {
1211            this(MATCH_PARENT, WRAP_CONTENT);
1212        }
1213
1214        public LayoutParams(int width, int height) {
1215            super(width, height);
1216        }
1217
1218        public LayoutParams(android.view.ViewGroup.LayoutParams source) {
1219            super(source);
1220        }
1221
1222        public LayoutParams(MarginLayoutParams source) {
1223            super(source);
1224        }
1225
1226        public LayoutParams(LayoutParams source) {
1227            super(source);
1228
1229            childType = source.childType;
1230        }
1231
1232        public LayoutParams(Context c, AttributeSet attrs) {
1233            super(c, attrs);
1234
1235            final TypedArray a = c.obtainStyledAttributes(attrs,
1236                    R.styleable.SlidingChallengeLayout_Layout);
1237            childType = a.getInt(R.styleable.SlidingChallengeLayout_Layout_layout_childType,
1238                    CHILD_TYPE_NONE);
1239            maxHeight = a.getDimensionPixelSize(
1240                    R.styleable.SlidingChallengeLayout_Layout_layout_maxHeight, 0);
1241            a.recycle();
1242        }
1243    }
1244}
1245