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