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