• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.internal.widget;
18 
19 import java.util.ArrayList;
20 
21 import android.animation.ValueAnimator;
22 import android.content.Context;
23 import android.content.res.Resources;
24 import android.graphics.Bitmap;
25 import android.graphics.BitmapFactory;
26 import android.graphics.Canvas;
27 import android.graphics.drawable.BitmapDrawable;
28 import android.os.UserHandle;
29 import android.os.Vibrator;
30 import android.provider.Settings;
31 import android.text.TextUtils;
32 import android.util.AttributeSet;
33 import android.util.Log;
34 import android.view.MotionEvent;
35 import android.view.View;
36 import android.view.accessibility.AccessibilityEvent;
37 import android.view.accessibility.AccessibilityManager;
38 
39 import 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  */
45 public 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 
WaveView(Context context)106     public WaveView(Context context) {
107         this(context, null);
108     }
109 
WaveView(Context context, AttributeSet attrs)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
onSizeChanged(int w, int h, int oldw, int oldh)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
getSuggestedMinimumWidth()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
getSuggestedMinimumHeight()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
onMeasure(int widthMeasureSpec, int heightMeasureSpec)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 
initDrawables()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 
waveUpdateFrame(float mouseX, float mouseY, boolean fingerDown)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 
createDrawable(int resId)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
onDraw(Canvas canvas)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
onHoverEvent(MotionEvent event)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
onTouchEvent(MotionEvent event)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      */
tryTransitionToStartAttemptState(MotionEvent event)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      */
getScaledGrabHandleRadius()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      */
announceUnlockHandle()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      */
vibrate(long duration)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      */
setOnTriggerListener(OnTriggerListener listener)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      */
dispatchTriggerEvent(int whichHandle)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      */
setGrabbedState(int newState)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          */
onTrigger(View v, int whichHandle)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          */
onGrabbedStateChange(View v, int grabbedState)646         void onGrabbedStateChange(View v, int grabbedState);
647     }
648 
onAnimationUpdate(ValueAnimator animation)649     public void onAnimationUpdate(ValueAnimator animation) {
650         invalidate();
651     }
652 
reset()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