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