1/*
2 * Copyright (C) 2010 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.widget;
18
19import java.util.ArrayList;
20
21import android.animation.ValueAnimator;
22import android.content.Context;
23import android.content.res.Resources;
24import android.graphics.Bitmap;
25import android.graphics.BitmapFactory;
26import android.graphics.Canvas;
27import android.graphics.drawable.BitmapDrawable;
28import android.os.UserHandle;
29import android.os.Vibrator;
30import android.provider.Settings;
31import android.text.TextUtils;
32import android.util.AttributeSet;
33import android.util.Log;
34import android.view.MotionEvent;
35import android.view.View;
36import android.view.accessibility.AccessibilityEvent;
37import android.view.accessibility.AccessibilityManager;
38
39import com.android.internal.R;
40
41/**
42 * A special widget containing a center and outer ring. Moving the center ring to the outer ring
43 * causes an event that can be caught by implementing OnTriggerListener.
44 */
45public class WaveView extends View implements ValueAnimator.AnimatorUpdateListener {
46    private static final String TAG = "WaveView";
47    private static final boolean DBG = false;
48    private static final int WAVE_COUNT = 20; // default wave count
49    private static final long VIBRATE_SHORT = 20;  // msec
50    private static final long VIBRATE_LONG = 20;  // msec
51
52    // Lock state machine states
53    private static final int STATE_RESET_LOCK = 0;
54    private static final int STATE_READY = 1;
55    private static final int STATE_START_ATTEMPT = 2;
56    private static final int STATE_ATTEMPTING = 3;
57    private static final int STATE_UNLOCK_ATTEMPT = 4;
58    private static final int STATE_UNLOCK_SUCCESS = 5;
59
60    // Animation properties.
61    private static final long DURATION = 300; // duration of transitional animations
62    private static final long FINAL_DURATION = 200; // duration of final animations when unlocking
63    private static final long RING_DELAY = 1300; // when to start fading animated rings
64    private static final long FINAL_DELAY = 200; // delay for unlock success animation
65    private static final long SHORT_DELAY = 100; // for starting one animation after another.
66    private static final long WAVE_DURATION = 2000; // amount of time for way to expand/decay
67    private static final long RESET_TIMEOUT = 3000; // elapsed time of inactivity before we reset
68    private static final long DELAY_INCREMENT = 15; // increment per wave while tracking motion
69    private static final long DELAY_INCREMENT2 = 12; // increment per wave while not tracking
70    private static final long WAVE_DELAY = WAVE_DURATION / WAVE_COUNT; // initial propagation delay
71
72    /**
73     * The scale by which to multiply the unlock handle width to compute the radius
74     * in which it can be grabbed when accessibility is disabled.
75     */
76    private static final float GRAB_HANDLE_RADIUS_SCALE_ACCESSIBILITY_DISABLED = 0.5f;
77
78    /**
79     * The scale by which to multiply the unlock handle width to compute the radius
80     * in which it can be grabbed when accessibility is enabled (more generous).
81     */
82    private static final float GRAB_HANDLE_RADIUS_SCALE_ACCESSIBILITY_ENABLED = 1.0f;
83
84    private Vibrator mVibrator;
85    private OnTriggerListener mOnTriggerListener;
86    private ArrayList<DrawableHolder> mDrawables = new ArrayList<DrawableHolder>(3);
87    private ArrayList<DrawableHolder> mLightWaves = new ArrayList<DrawableHolder>(WAVE_COUNT);
88    private boolean mFingerDown = false;
89    private float mRingRadius = 182.0f; // Radius of bitmap ring. Used to snap halo to it
90    private int mSnapRadius = 136; // minimum threshold for drag unlock
91    private int mWaveCount = WAVE_COUNT;  // number of waves
92    private long mWaveTimerDelay = WAVE_DELAY;
93    private int mCurrentWave = 0;
94    private float mLockCenterX; // center of widget as dictated by widget size
95    private float mLockCenterY;
96    private float mMouseX; // current mouse position as of last touch event
97    private float mMouseY;
98    private DrawableHolder mUnlockRing;
99    private DrawableHolder mUnlockDefault;
100    private DrawableHolder mUnlockHalo;
101    private int mLockState = STATE_RESET_LOCK;
102    private int mGrabbedState = OnTriggerListener.NO_HANDLE;
103    private boolean mWavesRunning;
104    private boolean mFinishWaves;
105
106    public WaveView(Context context) {
107        this(context, null);
108    }
109
110    public WaveView(Context context, AttributeSet attrs) {
111        super(context, attrs);
112
113        // TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.WaveView);
114        // mOrientation = a.getInt(R.styleable.WaveView_orientation, HORIZONTAL);
115        // a.recycle();
116
117        initDrawables();
118    }
119
120    @Override
121    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
122        mLockCenterX = 0.5f * w;
123        mLockCenterY = 0.5f * h;
124        super.onSizeChanged(w, h, oldw, oldh);
125    }
126
127    @Override
128    protected int getSuggestedMinimumWidth() {
129        // View should be large enough to contain the unlock ring + halo
130        return mUnlockRing.getWidth() + mUnlockHalo.getWidth();
131    }
132
133    @Override
134    protected int getSuggestedMinimumHeight() {
135        // View should be large enough to contain the unlock ring + halo
136        return mUnlockRing.getHeight() + mUnlockHalo.getHeight();
137    }
138
139    @Override
140    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
141        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
142        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
143        int widthSpecSize =  MeasureSpec.getSize(widthMeasureSpec);
144        int heightSpecSize =  MeasureSpec.getSize(heightMeasureSpec);
145        int width;
146        int height;
147
148        if (widthSpecMode == MeasureSpec.AT_MOST) {
149            width = Math.min(widthSpecSize, getSuggestedMinimumWidth());
150        } else if (widthSpecMode == MeasureSpec.EXACTLY) {
151            width = widthSpecSize;
152        } else {
153            width = getSuggestedMinimumWidth();
154        }
155
156        if (heightSpecMode == MeasureSpec.AT_MOST) {
157            height = Math.min(heightSpecSize, getSuggestedMinimumWidth());
158        } else if (heightSpecMode == MeasureSpec.EXACTLY) {
159            height = heightSpecSize;
160        } else {
161            height = getSuggestedMinimumHeight();
162        }
163
164        setMeasuredDimension(width, height);
165    }
166
167    private void initDrawables() {
168        mUnlockRing = new DrawableHolder(createDrawable(R.drawable.unlock_ring));
169        mUnlockRing.setX(mLockCenterX);
170        mUnlockRing.setY(mLockCenterY);
171        mUnlockRing.setScaleX(0.1f);
172        mUnlockRing.setScaleY(0.1f);
173        mUnlockRing.setAlpha(0.0f);
174        mDrawables.add(mUnlockRing);
175
176        mUnlockDefault = new DrawableHolder(createDrawable(R.drawable.unlock_default));
177        mUnlockDefault.setX(mLockCenterX);
178        mUnlockDefault.setY(mLockCenterY);
179        mUnlockDefault.setScaleX(0.1f);
180        mUnlockDefault.setScaleY(0.1f);
181        mUnlockDefault.setAlpha(0.0f);
182        mDrawables.add(mUnlockDefault);
183
184        mUnlockHalo = new DrawableHolder(createDrawable(R.drawable.unlock_halo));
185        mUnlockHalo.setX(mLockCenterX);
186        mUnlockHalo.setY(mLockCenterY);
187        mUnlockHalo.setScaleX(0.1f);
188        mUnlockHalo.setScaleY(0.1f);
189        mUnlockHalo.setAlpha(0.0f);
190        mDrawables.add(mUnlockHalo);
191
192        BitmapDrawable wave = createDrawable(R.drawable.unlock_wave);
193        for (int i = 0; i < mWaveCount; i++) {
194            DrawableHolder holder = new DrawableHolder(wave);
195            mLightWaves.add(holder);
196            holder.setAlpha(0.0f);
197        }
198    }
199
200    private void waveUpdateFrame(float mouseX, float mouseY, boolean fingerDown) {
201        double distX = mouseX - mLockCenterX;
202        double distY = mouseY - mLockCenterY;
203        int dragDistance = (int) Math.ceil(Math.hypot(distX, distY));
204        double touchA = Math.atan2(distX, distY);
205        float ringX = (float) (mLockCenterX + mRingRadius * Math.sin(touchA));
206        float ringY = (float) (mLockCenterY + mRingRadius * Math.cos(touchA));
207
208        switch (mLockState) {
209            case STATE_RESET_LOCK:
210                if (DBG) Log.v(TAG, "State RESET_LOCK");
211                mWaveTimerDelay = WAVE_DELAY;
212                for (int i = 0; i < mLightWaves.size(); i++) {
213                    DrawableHolder holder = mLightWaves.get(i);
214                    holder.addAnimTo(300, 0, "alpha", 0.0f, false);
215                }
216                for (int i = 0; i < mLightWaves.size(); i++) {
217                    mLightWaves.get(i).startAnimations(this);
218                }
219
220                mUnlockRing.addAnimTo(DURATION, 0, "x", mLockCenterX, true);
221                mUnlockRing.addAnimTo(DURATION, 0, "y", mLockCenterY, true);
222                mUnlockRing.addAnimTo(DURATION, 0, "scaleX", 0.1f, true);
223                mUnlockRing.addAnimTo(DURATION, 0, "scaleY", 0.1f, true);
224                mUnlockRing.addAnimTo(DURATION, 0, "alpha", 0.0f, true);
225
226                mUnlockDefault.removeAnimationFor("x");
227                mUnlockDefault.removeAnimationFor("y");
228                mUnlockDefault.removeAnimationFor("scaleX");
229                mUnlockDefault.removeAnimationFor("scaleY");
230                mUnlockDefault.removeAnimationFor("alpha");
231                mUnlockDefault.setX(mLockCenterX);
232                mUnlockDefault.setY(mLockCenterY);
233                mUnlockDefault.setScaleX(0.1f);
234                mUnlockDefault.setScaleY(0.1f);
235                mUnlockDefault.setAlpha(0.0f);
236                mUnlockDefault.addAnimTo(DURATION, SHORT_DELAY, "scaleX", 1.0f, true);
237                mUnlockDefault.addAnimTo(DURATION, SHORT_DELAY, "scaleY", 1.0f, true);
238                mUnlockDefault.addAnimTo(DURATION, SHORT_DELAY, "alpha", 1.0f, true);
239
240                mUnlockHalo.removeAnimationFor("x");
241                mUnlockHalo.removeAnimationFor("y");
242                mUnlockHalo.removeAnimationFor("scaleX");
243                mUnlockHalo.removeAnimationFor("scaleY");
244                mUnlockHalo.removeAnimationFor("alpha");
245                mUnlockHalo.setX(mLockCenterX);
246                mUnlockHalo.setY(mLockCenterY);
247                mUnlockHalo.setScaleX(0.1f);
248                mUnlockHalo.setScaleY(0.1f);
249                mUnlockHalo.setAlpha(0.0f);
250                mUnlockHalo.addAnimTo(DURATION, SHORT_DELAY, "x", mLockCenterX, true);
251                mUnlockHalo.addAnimTo(DURATION, SHORT_DELAY, "y", mLockCenterY, true);
252                mUnlockHalo.addAnimTo(DURATION, SHORT_DELAY, "scaleX", 1.0f, true);
253                mUnlockHalo.addAnimTo(DURATION, SHORT_DELAY, "scaleY", 1.0f, true);
254                mUnlockHalo.addAnimTo(DURATION, SHORT_DELAY, "alpha", 1.0f, true);
255
256                removeCallbacks(mLockTimerActions);
257
258                mLockState = STATE_READY;
259                break;
260
261            case STATE_READY:
262                if (DBG) Log.v(TAG, "State READY");
263                mWaveTimerDelay = WAVE_DELAY;
264                break;
265
266            case STATE_START_ATTEMPT:
267                if (DBG) Log.v(TAG, "State START_ATTEMPT");
268                mUnlockDefault.removeAnimationFor("x");
269                mUnlockDefault.removeAnimationFor("y");
270                mUnlockDefault.removeAnimationFor("scaleX");
271                mUnlockDefault.removeAnimationFor("scaleY");
272                mUnlockDefault.removeAnimationFor("alpha");
273                mUnlockDefault.setX(mLockCenterX + 182);
274                mUnlockDefault.setY(mLockCenterY);
275                mUnlockDefault.setScaleX(0.1f);
276                mUnlockDefault.setScaleY(0.1f);
277                mUnlockDefault.setAlpha(0.0f);
278
279                mUnlockDefault.addAnimTo(DURATION, SHORT_DELAY, "scaleX", 1.0f, false);
280                mUnlockDefault.addAnimTo(DURATION, SHORT_DELAY, "scaleY", 1.0f, false);
281                mUnlockDefault.addAnimTo(DURATION, SHORT_DELAY, "alpha", 1.0f, false);
282
283                mUnlockRing.addAnimTo(DURATION, 0, "scaleX", 1.0f, true);
284                mUnlockRing.addAnimTo(DURATION, 0, "scaleY", 1.0f, true);
285                mUnlockRing.addAnimTo(DURATION, 0, "alpha", 1.0f, true);
286
287                mLockState = STATE_ATTEMPTING;
288                break;
289
290            case STATE_ATTEMPTING:
291                if (DBG) Log.v(TAG, "State ATTEMPTING (fingerDown = " + fingerDown + ")");
292                if (dragDistance > mSnapRadius) {
293                    mFinishWaves = true; // don't start any more waves.
294                    if (fingerDown) {
295                        mUnlockHalo.addAnimTo(0, 0, "x", ringX, true);
296                        mUnlockHalo.addAnimTo(0, 0, "y", ringY, true);
297                        mUnlockHalo.addAnimTo(0, 0, "scaleX", 1.0f, true);
298                        mUnlockHalo.addAnimTo(0, 0, "scaleY", 1.0f, true);
299                        mUnlockHalo.addAnimTo(0, 0, "alpha", 1.0f, true);
300                    }  else {
301                        if (DBG) Log.v(TAG, "up detected, moving to STATE_UNLOCK_ATTEMPT");
302                        mLockState = STATE_UNLOCK_ATTEMPT;
303                    }
304                } else {
305                    // If waves have stopped, we need to kick them off again...
306                    if (!mWavesRunning) {
307                        mWavesRunning = true;
308                        mFinishWaves = false;
309                        // mWaveTimerDelay = WAVE_DELAY;
310                        postDelayed(mAddWaveAction, mWaveTimerDelay);
311                    }
312                    mUnlockHalo.addAnimTo(0, 0, "x", mouseX, true);
313                    mUnlockHalo.addAnimTo(0, 0, "y", mouseY, true);
314                    mUnlockHalo.addAnimTo(0, 0, "scaleX", 1.0f, true);
315                    mUnlockHalo.addAnimTo(0, 0, "scaleY", 1.0f, true);
316                    mUnlockHalo.addAnimTo(0, 0, "alpha", 1.0f, true);
317                }
318                break;
319
320            case STATE_UNLOCK_ATTEMPT:
321                if (DBG) Log.v(TAG, "State UNLOCK_ATTEMPT");
322                if (dragDistance > mSnapRadius) {
323                    for (int n = 0; n < mLightWaves.size(); n++) {
324                        DrawableHolder wave = mLightWaves.get(n);
325                        long delay = 1000L*(6 + n - mCurrentWave)/10L;
326                        wave.addAnimTo(FINAL_DURATION, delay, "x", ringX, true);
327                        wave.addAnimTo(FINAL_DURATION, delay, "y", ringY, true);
328                        wave.addAnimTo(FINAL_DURATION, delay, "scaleX", 0.1f, true);
329                        wave.addAnimTo(FINAL_DURATION, delay, "scaleY", 0.1f, true);
330                        wave.addAnimTo(FINAL_DURATION, delay, "alpha", 0.0f, true);
331                    }
332                    for (int i = 0; i < mLightWaves.size(); i++) {
333                        mLightWaves.get(i).startAnimations(this);
334                    }
335
336                    mUnlockRing.addAnimTo(FINAL_DURATION, 0, "x", ringX, false);
337                    mUnlockRing.addAnimTo(FINAL_DURATION, 0, "y", ringY, false);
338                    mUnlockRing.addAnimTo(FINAL_DURATION, 0, "scaleX", 0.1f, false);
339                    mUnlockRing.addAnimTo(FINAL_DURATION, 0, "scaleY", 0.1f, false);
340                    mUnlockRing.addAnimTo(FINAL_DURATION, 0, "alpha", 0.0f, false);
341
342                    mUnlockRing.addAnimTo(FINAL_DURATION, FINAL_DELAY, "alpha", 0.0f, false);
343
344                    mUnlockDefault.removeAnimationFor("x");
345                    mUnlockDefault.removeAnimationFor("y");
346                    mUnlockDefault.removeAnimationFor("scaleX");
347                    mUnlockDefault.removeAnimationFor("scaleY");
348                    mUnlockDefault.removeAnimationFor("alpha");
349                    mUnlockDefault.setX(ringX);
350                    mUnlockDefault.setY(ringY);
351                    mUnlockDefault.setScaleX(0.1f);
352                    mUnlockDefault.setScaleY(0.1f);
353                    mUnlockDefault.setAlpha(0.0f);
354
355                    mUnlockDefault.addAnimTo(FINAL_DURATION, 0, "x", ringX, true);
356                    mUnlockDefault.addAnimTo(FINAL_DURATION, 0, "y", ringY, true);
357                    mUnlockDefault.addAnimTo(FINAL_DURATION, 0, "scaleX", 1.0f, true);
358                    mUnlockDefault.addAnimTo(FINAL_DURATION, 0, "scaleY", 1.0f, true);
359                    mUnlockDefault.addAnimTo(FINAL_DURATION, 0, "alpha", 1.0f, true);
360
361                    mUnlockDefault.addAnimTo(FINAL_DURATION, FINAL_DELAY, "scaleX", 3.0f, false);
362                    mUnlockDefault.addAnimTo(FINAL_DURATION, FINAL_DELAY, "scaleY", 3.0f, false);
363                    mUnlockDefault.addAnimTo(FINAL_DURATION, FINAL_DELAY, "alpha", 0.0f, false);
364
365                    mUnlockHalo.addAnimTo(FINAL_DURATION, 0, "x", ringX, false);
366                    mUnlockHalo.addAnimTo(FINAL_DURATION, 0, "y", ringY, false);
367
368                    mUnlockHalo.addAnimTo(FINAL_DURATION, FINAL_DELAY, "scaleX", 3.0f, false);
369                    mUnlockHalo.addAnimTo(FINAL_DURATION, FINAL_DELAY, "scaleY", 3.0f, false);
370                    mUnlockHalo.addAnimTo(FINAL_DURATION, FINAL_DELAY, "alpha", 0.0f, false);
371
372                    removeCallbacks(mLockTimerActions);
373
374                    postDelayed(mLockTimerActions, RESET_TIMEOUT);
375
376                    dispatchTriggerEvent(OnTriggerListener.CENTER_HANDLE);
377                    mLockState = STATE_UNLOCK_SUCCESS;
378                } else {
379                    mLockState = STATE_RESET_LOCK;
380                }
381                break;
382
383            case STATE_UNLOCK_SUCCESS:
384                if (DBG) Log.v(TAG, "State UNLOCK_SUCCESS");
385                removeCallbacks(mAddWaveAction);
386                break;
387
388            default:
389                if (DBG) Log.v(TAG, "Unknown state " + mLockState);
390                break;
391        }
392        mUnlockDefault.startAnimations(this);
393        mUnlockHalo.startAnimations(this);
394        mUnlockRing.startAnimations(this);
395    }
396
397    BitmapDrawable createDrawable(int resId) {
398        Resources res = getResources();
399        Bitmap bitmap = BitmapFactory.decodeResource(res, resId);
400        return new BitmapDrawable(res, bitmap);
401    }
402
403    @Override
404    protected void onDraw(Canvas canvas) {
405        waveUpdateFrame(mMouseX, mMouseY, mFingerDown);
406        for (int i = 0; i < mDrawables.size(); ++i) {
407            mDrawables.get(i).draw(canvas);
408        }
409        for (int i = 0; i < mLightWaves.size(); ++i) {
410            mLightWaves.get(i).draw(canvas);
411        }
412    }
413
414    private final Runnable mLockTimerActions = new Runnable() {
415        public void run() {
416            if (DBG) Log.v(TAG, "LockTimerActions");
417            // reset lock after inactivity
418            if (mLockState == STATE_ATTEMPTING) {
419                if (DBG) Log.v(TAG, "Timer resets to STATE_RESET_LOCK");
420                mLockState = STATE_RESET_LOCK;
421            }
422            // for prototype, reset after successful unlock
423            if (mLockState == STATE_UNLOCK_SUCCESS) {
424                if (DBG) Log.v(TAG, "Timer resets to STATE_RESET_LOCK after success");
425                mLockState = STATE_RESET_LOCK;
426            }
427            invalidate();
428        }
429    };
430
431    private final Runnable mAddWaveAction = new Runnable() {
432        public void run() {
433            double distX = mMouseX - mLockCenterX;
434            double distY = mMouseY - mLockCenterY;
435            int dragDistance = (int) Math.ceil(Math.hypot(distX, distY));
436            if (mLockState == STATE_ATTEMPTING && dragDistance < mSnapRadius
437                    && mWaveTimerDelay >= WAVE_DELAY) {
438                mWaveTimerDelay = Math.min(WAVE_DURATION, mWaveTimerDelay + DELAY_INCREMENT);
439
440                DrawableHolder wave = mLightWaves.get(mCurrentWave);
441                wave.setAlpha(0.0f);
442                wave.setScaleX(0.2f);
443                wave.setScaleY(0.2f);
444                wave.setX(mMouseX);
445                wave.setY(mMouseY);
446
447                wave.addAnimTo(WAVE_DURATION, 0, "x", mLockCenterX, true);
448                wave.addAnimTo(WAVE_DURATION, 0, "y", mLockCenterY, true);
449                wave.addAnimTo(WAVE_DURATION*2/3, 0, "alpha", 1.0f, true);
450                wave.addAnimTo(WAVE_DURATION, 0, "scaleX", 1.0f, true);
451                wave.addAnimTo(WAVE_DURATION, 0, "scaleY", 1.0f, true);
452
453                wave.addAnimTo(1000, RING_DELAY, "alpha", 0.0f, false);
454                wave.startAnimations(WaveView.this);
455
456                mCurrentWave = (mCurrentWave+1) % mWaveCount;
457                if (DBG) Log.v(TAG, "WaveTimerDelay: start new wave in " + mWaveTimerDelay);
458            } else {
459                mWaveTimerDelay += DELAY_INCREMENT2;
460            }
461            if (mFinishWaves) {
462                // sentinel used to restart the waves after they've stopped
463                mWavesRunning = false;
464            } else {
465                postDelayed(mAddWaveAction, mWaveTimerDelay);
466            }
467        }
468    };
469
470    @Override
471    public boolean onHoverEvent(MotionEvent event) {
472        if (AccessibilityManager.getInstance(mContext).isTouchExplorationEnabled()) {
473            final int action = event.getAction();
474            switch (action) {
475                case MotionEvent.ACTION_HOVER_ENTER:
476                    event.setAction(MotionEvent.ACTION_DOWN);
477                    break;
478                case MotionEvent.ACTION_HOVER_MOVE:
479                    event.setAction(MotionEvent.ACTION_MOVE);
480                    break;
481                case MotionEvent.ACTION_HOVER_EXIT:
482                    event.setAction(MotionEvent.ACTION_UP);
483                    break;
484            }
485            onTouchEvent(event);
486            event.setAction(action);
487        }
488        return super.onHoverEvent(event);
489    }
490
491    @Override
492    public boolean onTouchEvent(MotionEvent event) {
493        final int action = event.getAction();
494        mMouseX = event.getX();
495        mMouseY = event.getY();
496        boolean handled = false;
497        switch (action) {
498            case MotionEvent.ACTION_DOWN:
499                removeCallbacks(mLockTimerActions);
500                mFingerDown = true;
501                tryTransitionToStartAttemptState(event);
502                handled = true;
503                break;
504
505            case MotionEvent.ACTION_MOVE:
506                tryTransitionToStartAttemptState(event);
507                handled = true;
508                break;
509
510            case MotionEvent.ACTION_UP:
511                if (DBG) Log.v(TAG, "ACTION_UP");
512                mFingerDown = false;
513                postDelayed(mLockTimerActions, RESET_TIMEOUT);
514                setGrabbedState(OnTriggerListener.NO_HANDLE);
515                // Normally the state machine is driven by user interaction causing redraws.
516                // However, when there's no more user interaction and no running animations,
517                // the state machine stops advancing because onDraw() never gets called.
518                // The following ensures we advance to the next state in this case,
519                // either STATE_UNLOCK_ATTEMPT or STATE_RESET_LOCK.
520                waveUpdateFrame(mMouseX, mMouseY, mFingerDown);
521                handled = true;
522                break;
523
524            case MotionEvent.ACTION_CANCEL:
525                mFingerDown = false;
526                handled = true;
527                break;
528        }
529        invalidate();
530        return handled ? true : super.onTouchEvent(event);
531    }
532
533    /**
534     * Tries to transition to start attempt state.
535     *
536     * @param event A motion event.
537     */
538    private void tryTransitionToStartAttemptState(MotionEvent event) {
539        final float dx = event.getX() - mUnlockHalo.getX();
540        final float dy = event.getY() - mUnlockHalo.getY();
541        float dist = (float) Math.hypot(dx, dy);
542        if (dist <= getScaledGrabHandleRadius()) {
543            setGrabbedState(OnTriggerListener.CENTER_HANDLE);
544            if (mLockState == STATE_READY) {
545                mLockState = STATE_START_ATTEMPT;
546                if (AccessibilityManager.getInstance(mContext).isEnabled()) {
547                    announceUnlockHandle();
548                }
549            }
550        }
551    }
552
553    /**
554     * @return The radius in which the handle is grabbed scaled based on
555     *     whether accessibility is enabled.
556     */
557    private float getScaledGrabHandleRadius() {
558        if (AccessibilityManager.getInstance(mContext).isEnabled()) {
559            return GRAB_HANDLE_RADIUS_SCALE_ACCESSIBILITY_ENABLED * mUnlockHalo.getWidth();
560        } else {
561            return GRAB_HANDLE_RADIUS_SCALE_ACCESSIBILITY_DISABLED * mUnlockHalo.getWidth();
562        }
563    }
564
565    /**
566     * Announces the unlock handle if accessibility is enabled.
567     */
568    private void announceUnlockHandle() {
569        setContentDescription(mContext.getString(R.string.description_target_unlock_tablet));
570        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
571        setContentDescription(null);
572    }
573
574    /**
575     * Triggers haptic feedback.
576     */
577    private synchronized void vibrate(long duration) {
578        final boolean hapticEnabled = Settings.System.getIntForUser(
579                mContext.getContentResolver(), Settings.System.HAPTIC_FEEDBACK_ENABLED, 1,
580                UserHandle.USER_CURRENT) != 0;
581        if (hapticEnabled) {
582            if (mVibrator == null) {
583                mVibrator = (android.os.Vibrator) getContext()
584                        .getSystemService(Context.VIBRATOR_SERVICE);
585            }
586            mVibrator.vibrate(duration);
587        }
588    }
589
590    /**
591     * Registers a callback to be invoked when the user triggers an event.
592     *
593     * @param listener the OnDialTriggerListener to attach to this view
594     */
595    public void setOnTriggerListener(OnTriggerListener listener) {
596        mOnTriggerListener = listener;
597    }
598
599    /**
600     * Dispatches a trigger event to listener. Ignored if a listener is not set.
601     * @param whichHandle the handle that triggered the event.
602     */
603    private void dispatchTriggerEvent(int whichHandle) {
604        vibrate(VIBRATE_LONG);
605        if (mOnTriggerListener != null) {
606            mOnTriggerListener.onTrigger(this, whichHandle);
607        }
608    }
609
610    /**
611     * Sets the current grabbed state, and dispatches a grabbed state change
612     * event to our listener.
613     */
614    private void setGrabbedState(int newState) {
615        if (newState != mGrabbedState) {
616            mGrabbedState = newState;
617            if (mOnTriggerListener != null) {
618                mOnTriggerListener.onGrabbedStateChange(this, mGrabbedState);
619            }
620        }
621    }
622
623    public interface OnTriggerListener {
624        /**
625         * Sent when the user releases the handle.
626         */
627        public static final int NO_HANDLE = 0;
628
629        /**
630         * Sent when the user grabs the center handle
631         */
632        public static final int CENTER_HANDLE = 10;
633
634        /**
635         * Called when the user drags the center ring beyond a threshold.
636         */
637        void onTrigger(View v, int whichHandle);
638
639        /**
640         * Called when the "grabbed state" changes (i.e. when the user either grabs or releases
641         * one of the handles.)
642         *
643         * @param v the view that was triggered
644         * @param grabbedState the new state: {@link #NO_HANDLE}, {@link #CENTER_HANDLE},
645         */
646        void onGrabbedStateChange(View v, int grabbedState);
647    }
648
649    public void onAnimationUpdate(ValueAnimator animation) {
650        invalidate();
651    }
652
653    public void reset() {
654        if (DBG) Log.v(TAG, "reset() : resets state to STATE_RESET_LOCK");
655        mLockState = STATE_RESET_LOCK;
656        invalidate();
657    }
658}
659