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