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