Home | History | Annotate | Download | only in multiwaveview
      1 /*
      2  * Copyright (C) 2012 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.incallui.widget.multiwaveview;
     18 
     19 import android.animation.Animator;
     20 import android.animation.Animator.AnimatorListener;
     21 import android.animation.AnimatorListenerAdapter;
     22 import android.animation.TimeInterpolator;
     23 import android.animation.ValueAnimator;
     24 import android.animation.ValueAnimator.AnimatorUpdateListener;
     25 import android.content.ComponentName;
     26 import android.content.Context;
     27 import android.content.pm.PackageManager;
     28 import android.content.pm.PackageManager.NameNotFoundException;
     29 import android.content.res.Resources;
     30 import android.content.res.TypedArray;
     31 import android.graphics.Canvas;
     32 import android.graphics.drawable.Drawable;
     33 import android.os.Bundle;
     34 import android.os.Vibrator;
     35 import android.text.TextUtils;
     36 import android.util.AttributeSet;
     37 import android.util.Log;
     38 import android.util.TypedValue;
     39 import android.view.Gravity;
     40 import android.view.MotionEvent;
     41 import android.view.View;
     42 import android.view.accessibility.AccessibilityManager;
     43 
     44 import com.android.incallui.R;
     45 
     46 import java.util.ArrayList;
     47 
     48 /**
     49  * This is a copy of com.android.internal.widget.multiwaveview.GlowPadView with minor changes
     50  * to remove dependencies on private api's.
     51  *
     52  * Incoporated the scaling functionality.
     53  *
     54  * A re-usable widget containing a center, outer ring and wave animation.
     55  */
     56 public class GlowPadView extends View {
     57     private static final String TAG = "GlowPadView";
     58     private static final boolean DEBUG = false;
     59 
     60     // Wave state machine
     61     private static final int STATE_IDLE = 0;
     62     private static final int STATE_START = 1;
     63     private static final int STATE_FIRST_TOUCH = 2;
     64     private static final int STATE_TRACKING = 3;
     65     private static final int STATE_SNAP = 4;
     66     private static final int STATE_FINISH = 5;
     67 
     68     // Animation properties.
     69     private static final float SNAP_MARGIN_DEFAULT = 20.0f; // distance to ring before we snap to it
     70 
     71     public interface OnTriggerListener {
     72         int NO_HANDLE = 0;
     73         int CENTER_HANDLE = 1;
     74         public void onGrabbed(View v, int handle);
     75         public void onReleased(View v, int handle);
     76         public void onTrigger(View v, int target);
     77         public void onGrabbedStateChange(View v, int handle);
     78         public void onFinishFinalAnimation();
     79     }
     80 
     81     // Tuneable parameters for animation
     82     private static final int WAVE_ANIMATION_DURATION = 1350;
     83     private static final int RETURN_TO_HOME_DELAY = 1200;
     84     private static final int RETURN_TO_HOME_DURATION = 200;
     85     private static final int HIDE_ANIMATION_DELAY = 200;
     86     private static final int HIDE_ANIMATION_DURATION = 200;
     87     private static final int SHOW_ANIMATION_DURATION = 200;
     88     private static final int SHOW_ANIMATION_DELAY = 50;
     89     private static final int INITIAL_SHOW_HANDLE_DURATION = 200;
     90     private static final int REVEAL_GLOW_DELAY = 0;
     91     private static final int REVEAL_GLOW_DURATION = 0;
     92 
     93     private static final float TAP_RADIUS_SCALE_ACCESSIBILITY_ENABLED = 1.3f;
     94     private static final float TARGET_SCALE_EXPANDED = 1.0f;
     95     private static final float TARGET_SCALE_COLLAPSED = 0.8f;
     96     private static final float RING_SCALE_EXPANDED = 1.0f;
     97     private static final float RING_SCALE_COLLAPSED = 0.5f;
     98 
     99     private ArrayList<TargetDrawable> mTargetDrawables = new ArrayList<TargetDrawable>();
    100     private AnimationBundle mWaveAnimations = new AnimationBundle();
    101     private AnimationBundle mTargetAnimations = new AnimationBundle();
    102     private AnimationBundle mGlowAnimations = new AnimationBundle();
    103     private ArrayList<String> mTargetDescriptions;
    104     private ArrayList<String> mDirectionDescriptions;
    105     private OnTriggerListener mOnTriggerListener;
    106     private TargetDrawable mHandleDrawable;
    107     private TargetDrawable mOuterRing;
    108     private Vibrator mVibrator;
    109 
    110     private int mFeedbackCount = 3;
    111     private int mVibrationDuration = 0;
    112     private int mGrabbedState;
    113     private int mActiveTarget = -1;
    114     private float mGlowRadius;
    115     private float mWaveCenterX;
    116     private float mWaveCenterY;
    117     private int mMaxTargetHeight;
    118     private int mMaxTargetWidth;
    119     private float mRingScaleFactor = 1f;
    120     private boolean mAllowScaling;
    121 
    122     private float mOuterRadius = 0.0f;
    123     private float mSnapMargin = 0.0f;
    124     private boolean mDragging;
    125     private int mNewTargetResources;
    126 
    127     private class AnimationBundle extends ArrayList<Tweener> {
    128         private static final long serialVersionUID = 0xA84D78726F127468L;
    129         private boolean mSuspended;
    130 
    131         public void start() {
    132             if (mSuspended) return; // ignore attempts to start animations
    133             final int count = size();
    134             for (int i = 0; i < count; i++) {
    135                 Tweener anim = get(i);
    136                 anim.animator.start();
    137             }
    138         }
    139 
    140         public void cancel() {
    141             final int count = size();
    142             for (int i = 0; i < count; i++) {
    143                 Tweener anim = get(i);
    144                 anim.animator.cancel();
    145             }
    146             clear();
    147         }
    148 
    149         public void stop() {
    150             final int count = size();
    151             for (int i = 0; i < count; i++) {
    152                 Tweener anim = get(i);
    153                 anim.animator.end();
    154             }
    155             clear();
    156         }
    157 
    158         public void setSuspended(boolean suspend) {
    159             mSuspended = suspend;
    160         }
    161     };
    162 
    163     private AnimatorListener mResetListener = new AnimatorListenerAdapter() {
    164         public void onAnimationEnd(Animator animator) {
    165             switchToState(STATE_IDLE, mWaveCenterX, mWaveCenterY);
    166             dispatchOnFinishFinalAnimation();
    167         }
    168     };
    169 
    170     private AnimatorListener mResetListenerWithPing = new AnimatorListenerAdapter() {
    171         public void onAnimationEnd(Animator animator) {
    172             ping();
    173             switchToState(STATE_IDLE, mWaveCenterX, mWaveCenterY);
    174             dispatchOnFinishFinalAnimation();
    175         }
    176     };
    177 
    178     private AnimatorUpdateListener mUpdateListener = new AnimatorUpdateListener() {
    179         public void onAnimationUpdate(ValueAnimator animation) {
    180             invalidate();
    181         }
    182     };
    183 
    184     private boolean mAnimatingTargets;
    185     private AnimatorListener mTargetUpdateListener = new AnimatorListenerAdapter() {
    186         public void onAnimationEnd(Animator animator) {
    187             if (mNewTargetResources != 0) {
    188                 internalSetTargetResources(mNewTargetResources);
    189                 mNewTargetResources = 0;
    190                 hideTargets(false, false);
    191             }
    192             mAnimatingTargets = false;
    193         }
    194     };
    195     private int mTargetResourceId;
    196     private int mTargetDescriptionsResourceId;
    197     private int mDirectionDescriptionsResourceId;
    198     private boolean mAlwaysTrackFinger;
    199     private int mHorizontalInset;
    200     private int mVerticalInset;
    201     private int mGravity = Gravity.TOP;
    202     private boolean mInitialLayout = true;
    203     private Tweener mBackgroundAnimator;
    204     private PointCloud mPointCloud;
    205     private float mInnerRadius;
    206     private int mPointerId;
    207 
    208     public GlowPadView(Context context) {
    209         this(context, null);
    210     }
    211 
    212     public GlowPadView(Context context, AttributeSet attrs) {
    213         super(context, attrs);
    214         Resources res = context.getResources();
    215 
    216         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.GlowPadView);
    217         mInnerRadius = a.getDimension(R.styleable.GlowPadView_innerRadius, mInnerRadius);
    218         mOuterRadius = a.getDimension(R.styleable.GlowPadView_outerRadius, mOuterRadius);
    219         mSnapMargin = a.getDimension(R.styleable.GlowPadView_snapMargin, mSnapMargin);
    220         mVibrationDuration = a.getInt(R.styleable.GlowPadView_vibrationDuration,
    221                 mVibrationDuration);
    222         mFeedbackCount = a.getInt(R.styleable.GlowPadView_feedbackCount,
    223                 mFeedbackCount);
    224         mAllowScaling = a.getBoolean(R.styleable.GlowPadView_allowScaling, false);
    225         TypedValue handle = a.peekValue(R.styleable.GlowPadView_handleDrawable);
    226         setHandleDrawable(handle != null ? handle.resourceId : R.drawable.ic_incall_audio_handle);
    227         mOuterRing = new TargetDrawable(res,
    228                 getResourceId(a, R.styleable.GlowPadView_outerRingDrawable), 1);
    229 
    230         mAlwaysTrackFinger = a.getBoolean(R.styleable.GlowPadView_alwaysTrackFinger, false);
    231 
    232         int pointId = getResourceId(a, R.styleable.GlowPadView_pointDrawable);
    233         Drawable pointDrawable = pointId != 0 ? res.getDrawable(pointId) : null;
    234         mGlowRadius = a.getDimension(R.styleable.GlowPadView_glowRadius, 0.0f);
    235 
    236         TypedValue outValue = new TypedValue();
    237 
    238         // Read array of target drawables
    239         if (a.getValue(R.styleable.GlowPadView_targetDrawables, outValue)) {
    240             internalSetTargetResources(outValue.resourceId);
    241         }
    242         if (mTargetDrawables == null || mTargetDrawables.size() == 0) {
    243             throw new IllegalStateException("Must specify at least one target drawable");
    244         }
    245 
    246         // Read array of target descriptions
    247         if (a.getValue(R.styleable.GlowPadView_targetDescriptions, outValue)) {
    248             final int resourceId = outValue.resourceId;
    249             if (resourceId == 0) {
    250                 throw new IllegalStateException("Must specify target descriptions");
    251             }
    252             setTargetDescriptionsResourceId(resourceId);
    253         }
    254 
    255         // Read array of direction descriptions
    256         if (a.getValue(R.styleable.GlowPadView_directionDescriptions, outValue)) {
    257             final int resourceId = outValue.resourceId;
    258             if (resourceId == 0) {
    259                 throw new IllegalStateException("Must specify direction descriptions");
    260             }
    261             setDirectionDescriptionsResourceId(resourceId);
    262         }
    263 
    264         // Use gravity attribute from LinearLayout
    265         //a = context.obtainStyledAttributes(attrs, R.styleable.LinearLayout);
    266         mGravity = a.getInt(R.styleable.GlowPadView_android_gravity, Gravity.TOP);
    267         a.recycle();
    268 
    269 
    270         setVibrateEnabled(mVibrationDuration > 0);
    271 
    272         assignDefaultsIfNeeded();
    273 
    274         mPointCloud = new PointCloud(pointDrawable);
    275         mPointCloud.makePointCloud(mInnerRadius, mOuterRadius);
    276         mPointCloud.glowManager.setRadius(mGlowRadius);
    277     }
    278 
    279     private int getResourceId(TypedArray a, int id) {
    280         TypedValue tv = a.peekValue(id);
    281         return tv == null ? 0 : tv.resourceId;
    282     }
    283 
    284     private void dump() {
    285         Log.v(TAG, "Outer Radius = " + mOuterRadius);
    286         Log.v(TAG, "SnapMargin = " + mSnapMargin);
    287         Log.v(TAG, "FeedbackCount = " + mFeedbackCount);
    288         Log.v(TAG, "VibrationDuration = " + mVibrationDuration);
    289         Log.v(TAG, "GlowRadius = " + mGlowRadius);
    290         Log.v(TAG, "WaveCenterX = " + mWaveCenterX);
    291         Log.v(TAG, "WaveCenterY = " + mWaveCenterY);
    292     }
    293 
    294     public void suspendAnimations() {
    295         mWaveAnimations.setSuspended(true);
    296         mTargetAnimations.setSuspended(true);
    297         mGlowAnimations.setSuspended(true);
    298     }
    299 
    300     public void resumeAnimations() {
    301         mWaveAnimations.setSuspended(false);
    302         mTargetAnimations.setSuspended(false);
    303         mGlowAnimations.setSuspended(false);
    304         mWaveAnimations.start();
    305         mTargetAnimations.start();
    306         mGlowAnimations.start();
    307     }
    308 
    309     @Override
    310     protected int getSuggestedMinimumWidth() {
    311         // View should be large enough to contain the background + handle and
    312         // target drawable on either edge.
    313         return (int) (Math.max(mOuterRing.getWidth(), 2 * mOuterRadius) + mMaxTargetWidth);
    314     }
    315 
    316     @Override
    317     protected int getSuggestedMinimumHeight() {
    318         // View should be large enough to contain the unlock ring + target and
    319         // target drawable on either edge
    320         return (int) (Math.max(mOuterRing.getHeight(), 2 * mOuterRadius) + mMaxTargetHeight);
    321     }
    322 
    323     /**
    324      * This gets the suggested width accounting for the ring's scale factor.
    325      */
    326     protected int getScaledSuggestedMinimumWidth() {
    327         return (int) (mRingScaleFactor * Math.max(mOuterRing.getWidth(), 2 * mOuterRadius)
    328                 + mMaxTargetWidth);
    329     }
    330 
    331     /**
    332      * This gets the suggested height accounting for the ring's scale factor.
    333      */
    334     protected int getScaledSuggestedMinimumHeight() {
    335         return (int) (mRingScaleFactor * Math.max(mOuterRing.getHeight(), 2 * mOuterRadius)
    336                 + mMaxTargetHeight);
    337     }
    338 
    339     private int resolveMeasured(int measureSpec, int desired)
    340     {
    341         int result = 0;
    342         int specSize = MeasureSpec.getSize(measureSpec);
    343         switch (MeasureSpec.getMode(measureSpec)) {
    344             case MeasureSpec.UNSPECIFIED:
    345                 result = desired;
    346                 break;
    347             case MeasureSpec.AT_MOST:
    348                 result = Math.min(specSize, desired);
    349                 break;
    350             case MeasureSpec.EXACTLY:
    351             default:
    352                 result = specSize;
    353         }
    354         return result;
    355     }
    356 
    357     @Override
    358     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    359         final int minimumWidth = getSuggestedMinimumWidth();
    360         final int minimumHeight = getSuggestedMinimumHeight();
    361         int computedWidth = resolveMeasured(widthMeasureSpec, minimumWidth);
    362         int computedHeight = resolveMeasured(heightMeasureSpec, minimumHeight);
    363 
    364         mRingScaleFactor = computeScaleFactor(minimumWidth, minimumHeight,
    365                 computedWidth, computedHeight);
    366 
    367         int scaledWidth = getScaledSuggestedMinimumWidth();
    368         int scaledHeight = getScaledSuggestedMinimumHeight();
    369 
    370         computeInsets(computedWidth - scaledWidth, computedHeight - scaledHeight);
    371         setMeasuredDimension(computedWidth, computedHeight);
    372     }
    373 
    374     private void switchToState(int state, float x, float y) {
    375         switch (state) {
    376             case STATE_IDLE:
    377                 deactivateTargets();
    378                 hideGlow(0, 0, 0.0f, null);
    379                 startBackgroundAnimation(0, 0.0f);
    380                 mHandleDrawable.setState(TargetDrawable.STATE_INACTIVE);
    381                 mHandleDrawable.setAlpha(1.0f);
    382                 break;
    383 
    384             case STATE_START:
    385                 startBackgroundAnimation(0, 0.0f);
    386                 break;
    387 
    388             case STATE_FIRST_TOUCH:
    389                 mHandleDrawable.setAlpha(0.0f);
    390                 deactivateTargets();
    391                 showTargets(true);
    392                 startBackgroundAnimation(INITIAL_SHOW_HANDLE_DURATION, 1.0f);
    393                 setGrabbedState(OnTriggerListener.CENTER_HANDLE);
    394 
    395                 final AccessibilityManager accessibilityManager =
    396                     (AccessibilityManager) getContext().getSystemService(
    397                             Context.ACCESSIBILITY_SERVICE);
    398                 if (accessibilityManager.isEnabled()) {
    399                     announceTargets();
    400                 }
    401                 break;
    402 
    403             case STATE_TRACKING:
    404                 mHandleDrawable.setAlpha(0.0f);
    405                 showGlow(REVEAL_GLOW_DURATION , REVEAL_GLOW_DELAY, 1.0f, null);
    406                 break;
    407 
    408             case STATE_SNAP:
    409                 // TODO: Add transition states (see list_selector_background_transition.xml)
    410                 mHandleDrawable.setAlpha(0.0f);
    411                 showGlow(REVEAL_GLOW_DURATION , REVEAL_GLOW_DELAY, 0.0f, null);
    412                 break;
    413 
    414             case STATE_FINISH:
    415                 doFinish();
    416                 break;
    417         }
    418     }
    419 
    420     private void showGlow(int duration, int delay, float finalAlpha,
    421             AnimatorListener finishListener) {
    422         mGlowAnimations.cancel();
    423         mGlowAnimations.add(Tweener.to(mPointCloud.glowManager, duration,
    424                 "ease", Ease.Cubic.easeIn,
    425                 "delay", delay,
    426                 "alpha", finalAlpha,
    427                 "onUpdate", mUpdateListener,
    428                 "onComplete", finishListener));
    429         mGlowAnimations.start();
    430     }
    431 
    432     private void hideGlow(int duration, int delay, float finalAlpha,
    433             AnimatorListener finishListener) {
    434         mGlowAnimations.cancel();
    435         mGlowAnimations.add(Tweener.to(mPointCloud.glowManager, duration,
    436                 "ease", Ease.Quart.easeOut,
    437                 "delay", delay,
    438                 "alpha", finalAlpha,
    439                 "x", 0.0f,
    440                 "y", 0.0f,
    441                 "onUpdate", mUpdateListener,
    442                 "onComplete", finishListener));
    443         mGlowAnimations.start();
    444     }
    445 
    446     private void deactivateTargets() {
    447         final int count = mTargetDrawables.size();
    448         for (int i = 0; i < count; i++) {
    449             TargetDrawable target = mTargetDrawables.get(i);
    450             target.setState(TargetDrawable.STATE_INACTIVE);
    451         }
    452         mActiveTarget = -1;
    453     }
    454 
    455     /**
    456      * Dispatches a trigger event to listener. Ignored if a listener is not set.
    457      * @param whichTarget the target that was triggered.
    458      */
    459     private void dispatchTriggerEvent(int whichTarget) {
    460         vibrate();
    461         if (mOnTriggerListener != null) {
    462             mOnTriggerListener.onTrigger(this, whichTarget);
    463         }
    464     }
    465 
    466     private void dispatchOnFinishFinalAnimation() {
    467         if (mOnTriggerListener != null) {
    468             mOnTriggerListener.onFinishFinalAnimation();
    469         }
    470     }
    471 
    472     private void doFinish() {
    473         final int activeTarget = mActiveTarget;
    474         final boolean targetHit =  activeTarget != -1;
    475 
    476         if (targetHit) {
    477             if (DEBUG) Log.v(TAG, "Finish with target hit = " + targetHit);
    478 
    479             highlightSelected(activeTarget);
    480 
    481             // Inform listener of any active targets.  Typically only one will be active.
    482             hideGlow(RETURN_TO_HOME_DURATION, RETURN_TO_HOME_DELAY, 0.0f, mResetListener);
    483             dispatchTriggerEvent(activeTarget);
    484             if (!mAlwaysTrackFinger) {
    485                 // Force ring and targets to finish animation to final expanded state
    486                 mTargetAnimations.stop();
    487             }
    488         } else {
    489             // Animate handle back to the center based on current state.
    490             hideGlow(HIDE_ANIMATION_DURATION, 0, 0.0f, mResetListenerWithPing);
    491             hideTargets(true, false);
    492         }
    493 
    494         setGrabbedState(OnTriggerListener.NO_HANDLE);
    495     }
    496 
    497     private void highlightSelected(int activeTarget) {
    498         // Highlight the given target and fade others
    499         mTargetDrawables.get(activeTarget).setState(TargetDrawable.STATE_ACTIVE);
    500         hideUnselected(activeTarget);
    501     }
    502 
    503     private void hideUnselected(int active) {
    504         for (int i = 0; i < mTargetDrawables.size(); i++) {
    505             if (i != active) {
    506                 mTargetDrawables.get(i).setAlpha(0.0f);
    507             }
    508         }
    509     }
    510 
    511     private void hideTargets(boolean animate, boolean expanded) {
    512         mTargetAnimations.cancel();
    513         // Note: these animations should complete at the same time so that we can swap out
    514         // the target assets asynchronously from the setTargetResources() call.
    515         mAnimatingTargets = animate;
    516         final int duration = animate ? HIDE_ANIMATION_DURATION : 0;
    517         final int delay = animate ? HIDE_ANIMATION_DELAY : 0;
    518 
    519         final float targetScale = expanded ?
    520                 TARGET_SCALE_EXPANDED : TARGET_SCALE_COLLAPSED;
    521         final int length = mTargetDrawables.size();
    522         final TimeInterpolator interpolator = Ease.Cubic.easeOut;
    523         for (int i = 0; i < length; i++) {
    524             TargetDrawable target = mTargetDrawables.get(i);
    525             target.setState(TargetDrawable.STATE_INACTIVE);
    526             mTargetAnimations.add(Tweener.to(target, duration,
    527                     "ease", interpolator,
    528                     "alpha", 0.0f,
    529                     "scaleX", targetScale,
    530                     "scaleY", targetScale,
    531                     "delay", delay,
    532                     "onUpdate", mUpdateListener));
    533         }
    534 
    535         float ringScaleTarget = expanded ?
    536                 RING_SCALE_EXPANDED : RING_SCALE_COLLAPSED;
    537         ringScaleTarget *= mRingScaleFactor;
    538         mTargetAnimations.add(Tweener.to(mOuterRing, duration,
    539                 "ease", interpolator,
    540                 "alpha", 0.0f,
    541                 "scaleX", ringScaleTarget,
    542                 "scaleY", ringScaleTarget,
    543                 "delay", delay,
    544                 "onUpdate", mUpdateListener,
    545                 "onComplete", mTargetUpdateListener));
    546 
    547         mTargetAnimations.start();
    548     }
    549 
    550     private void showTargets(boolean animate) {
    551         mTargetAnimations.stop();
    552         mAnimatingTargets = animate;
    553         final int delay = animate ? SHOW_ANIMATION_DELAY : 0;
    554         final int duration = animate ? SHOW_ANIMATION_DURATION : 0;
    555         final int length = mTargetDrawables.size();
    556         for (int i = 0; i < length; i++) {
    557             TargetDrawable target = mTargetDrawables.get(i);
    558             target.setState(TargetDrawable.STATE_INACTIVE);
    559             mTargetAnimations.add(Tweener.to(target, duration,
    560                     "ease", Ease.Cubic.easeOut,
    561                     "alpha", 1.0f,
    562                     "scaleX", 1.0f,
    563                     "scaleY", 1.0f,
    564                     "delay", delay,
    565                     "onUpdate", mUpdateListener));
    566         }
    567         float ringScale = mRingScaleFactor * RING_SCALE_EXPANDED;
    568         mTargetAnimations.add(Tweener.to(mOuterRing, duration,
    569                 "ease", Ease.Cubic.easeOut,
    570                 "alpha", 1.0f,
    571                 "scaleX", ringScale,
    572                 "scaleY", ringScale,
    573                 "delay", delay,
    574                 "onUpdate", mUpdateListener,
    575                 "onComplete", mTargetUpdateListener));
    576 
    577         mTargetAnimations.start();
    578     }
    579 
    580     private void vibrate() {
    581         if (mVibrator != null) {
    582             mVibrator.vibrate(mVibrationDuration);
    583         }
    584     }
    585 
    586     private ArrayList<TargetDrawable> loadDrawableArray(int resourceId) {
    587         Resources res = getContext().getResources();
    588         TypedArray array = res.obtainTypedArray(resourceId);
    589         final int count = array.length();
    590         ArrayList<TargetDrawable> drawables = new ArrayList<TargetDrawable>(count);
    591         for (int i = 0; i < count; i++) {
    592             TypedValue value = array.peekValue(i);
    593             TargetDrawable target = new TargetDrawable(res, value != null ? value.resourceId : 0, 3);
    594             drawables.add(target);
    595         }
    596         array.recycle();
    597         return drawables;
    598     }
    599 
    600     private void internalSetTargetResources(int resourceId) {
    601         final ArrayList<TargetDrawable> targets = loadDrawableArray(resourceId);
    602         mTargetDrawables = targets;
    603         mTargetResourceId = resourceId;
    604 
    605         int maxWidth = mHandleDrawable.getWidth();
    606         int maxHeight = mHandleDrawable.getHeight();
    607         final int count = targets.size();
    608         for (int i = 0; i < count; i++) {
    609             TargetDrawable target = targets.get(i);
    610             maxWidth = Math.max(maxWidth, target.getWidth());
    611             maxHeight = Math.max(maxHeight, target.getHeight());
    612         }
    613         if (mMaxTargetWidth != maxWidth || mMaxTargetHeight != maxHeight) {
    614             mMaxTargetWidth = maxWidth;
    615             mMaxTargetHeight = maxHeight;
    616             requestLayout(); // required to resize layout and call updateTargetPositions()
    617         } else {
    618             updateTargetPositions(mWaveCenterX, mWaveCenterY);
    619             updatePointCloudPosition(mWaveCenterX, mWaveCenterY);
    620         }
    621     }
    622     /**
    623      * Loads an array of drawables from the given resourceId.
    624      *
    625      * @param resourceId
    626      */
    627     public void setTargetResources(int resourceId) {
    628         if (mAnimatingTargets) {
    629             // postpone this change until we return to the initial state
    630             mNewTargetResources = resourceId;
    631         } else {
    632             internalSetTargetResources(resourceId);
    633         }
    634     }
    635 
    636     public int getTargetResourceId() {
    637         return mTargetResourceId;
    638     }
    639 
    640     /**
    641      * Sets teh handle drawable to the drawable specified by the resource ID.
    642      * @param resourceId
    643      */
    644     public void setHandleDrawable(int resourceId) {
    645         mHandleDrawable = new TargetDrawable(getResources(), resourceId, 2);
    646         mHandleDrawable.setState(TargetDrawable.STATE_INACTIVE);
    647     }
    648 
    649     /**
    650      * Sets the resource id specifying the target descriptions for accessibility.
    651      *
    652      * @param resourceId The resource id.
    653      */
    654     public void setTargetDescriptionsResourceId(int resourceId) {
    655         mTargetDescriptionsResourceId = resourceId;
    656         if (mTargetDescriptions != null) {
    657             mTargetDescriptions.clear();
    658         }
    659     }
    660 
    661     /**
    662      * Gets the resource id specifying the target descriptions for accessibility.
    663      *
    664      * @return The resource id.
    665      */
    666     public int getTargetDescriptionsResourceId() {
    667         return mTargetDescriptionsResourceId;
    668     }
    669 
    670     /**
    671      * Sets the resource id specifying the target direction descriptions for accessibility.
    672      *
    673      * @param resourceId The resource id.
    674      */
    675     public void setDirectionDescriptionsResourceId(int resourceId) {
    676         mDirectionDescriptionsResourceId = resourceId;
    677         if (mDirectionDescriptions != null) {
    678             mDirectionDescriptions.clear();
    679         }
    680     }
    681 
    682     /**
    683      * Gets the resource id specifying the target direction descriptions.
    684      *
    685      * @return The resource id.
    686      */
    687     public int getDirectionDescriptionsResourceId() {
    688         return mDirectionDescriptionsResourceId;
    689     }
    690 
    691     /**
    692      * Enable or disable vibrate on touch.
    693      *
    694      * @param enabled
    695      */
    696     public void setVibrateEnabled(boolean enabled) {
    697         if (enabled && mVibrator == null) {
    698             mVibrator = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE);
    699         } else {
    700             mVibrator = null;
    701         }
    702     }
    703 
    704     /**
    705      * Starts wave animation.
    706      *
    707      */
    708     public void ping() {
    709         if (mFeedbackCount > 0) {
    710             boolean doWaveAnimation = true;
    711             final AnimationBundle waveAnimations = mWaveAnimations;
    712 
    713             // Don't do a wave if there's already one in progress
    714             if (waveAnimations.size() > 0 && waveAnimations.get(0).animator.isRunning()) {
    715                 long t = waveAnimations.get(0).animator.getCurrentPlayTime();
    716                 if (t < WAVE_ANIMATION_DURATION/2) {
    717                     doWaveAnimation = false;
    718                 }
    719             }
    720 
    721             if (doWaveAnimation) {
    722                 startWaveAnimation();
    723             }
    724         }
    725     }
    726 
    727     private void stopAndHideWaveAnimation() {
    728         mWaveAnimations.cancel();
    729         mPointCloud.waveManager.setAlpha(0.0f);
    730     }
    731 
    732     private void startWaveAnimation() {
    733         mWaveAnimations.cancel();
    734         mPointCloud.waveManager.setAlpha(1.0f);
    735         mPointCloud.waveManager.setRadius(mHandleDrawable.getWidth()/2.0f);
    736         mWaveAnimations.add(Tweener.to(mPointCloud.waveManager, WAVE_ANIMATION_DURATION,
    737                 "ease", Ease.Quad.easeOut,
    738                 "delay", 0,
    739                 "radius", 2.0f * mOuterRadius,
    740                 "onUpdate", mUpdateListener,
    741                 "onComplete",
    742                 new AnimatorListenerAdapter() {
    743                     public void onAnimationEnd(Animator animator) {
    744                         mPointCloud.waveManager.setRadius(0.0f);
    745                         mPointCloud.waveManager.setAlpha(0.0f);
    746                     }
    747                 }));
    748         mWaveAnimations.start();
    749     }
    750 
    751     /**
    752      * Resets the widget to default state and cancels all animation. If animate is 'true', will
    753      * animate objects into place. Otherwise, objects will snap back to place.
    754      *
    755      * @param animate
    756      */
    757     public void reset(boolean animate) {
    758         mGlowAnimations.stop();
    759         mTargetAnimations.stop();
    760         startBackgroundAnimation(0, 0.0f);
    761         stopAndHideWaveAnimation();
    762         hideTargets(animate, false);
    763         hideGlow(0, 0, 0.0f, null);
    764         Tweener.reset();
    765     }
    766 
    767     private void startBackgroundAnimation(int duration, float alpha) {
    768         final Drawable background = getBackground();
    769         if (mAlwaysTrackFinger && background != null) {
    770             if (mBackgroundAnimator != null) {
    771                 mBackgroundAnimator.animator.cancel();
    772             }
    773             mBackgroundAnimator = Tweener.to(background, duration,
    774                     "ease", Ease.Cubic.easeIn,
    775                     "alpha", (int)(255.0f * alpha),
    776                     "delay", SHOW_ANIMATION_DELAY);
    777             mBackgroundAnimator.animator.start();
    778         }
    779     }
    780 
    781     @Override
    782     public boolean onTouchEvent(MotionEvent event) {
    783         final int action = event.getActionMasked();
    784         boolean handled = false;
    785         switch (action) {
    786             case MotionEvent.ACTION_POINTER_DOWN:
    787             case MotionEvent.ACTION_DOWN:
    788                 if (DEBUG) Log.v(TAG, "*** DOWN ***");
    789                 handleDown(event);
    790                 handleMove(event);
    791                 handled = true;
    792                 break;
    793 
    794             case MotionEvent.ACTION_MOVE:
    795                 if (DEBUG) Log.v(TAG, "*** MOVE ***");
    796                 handleMove(event);
    797                 handled = true;
    798                 break;
    799 
    800             case MotionEvent.ACTION_POINTER_UP:
    801             case MotionEvent.ACTION_UP:
    802                 if (DEBUG) Log.v(TAG, "*** UP ***");
    803                 handleMove(event);
    804                 handleUp(event);
    805                 handled = true;
    806                 break;
    807 
    808             case MotionEvent.ACTION_CANCEL:
    809                 if (DEBUG) Log.v(TAG, "*** CANCEL ***");
    810                 handleMove(event);
    811                 handleCancel(event);
    812                 handled = true;
    813                 break;
    814         }
    815         invalidate();
    816         return handled ? true : super.onTouchEvent(event);
    817     }
    818 
    819     private void updateGlowPosition(float x, float y) {
    820         float dx = x - mOuterRing.getX();
    821         float dy = y - mOuterRing.getY();
    822         dx *= 1f / mRingScaleFactor;
    823         dy *= 1f / mRingScaleFactor;
    824         mPointCloud.glowManager.setX(mOuterRing.getX() + dx);
    825         mPointCloud.glowManager.setY(mOuterRing.getY() + dy);
    826     }
    827 
    828     private void handleDown(MotionEvent event) {
    829         int actionIndex = event.getActionIndex();
    830         float eventX = event.getX(actionIndex);
    831         float eventY = event.getY(actionIndex);
    832         switchToState(STATE_START, eventX, eventY);
    833         if (!trySwitchToFirstTouchState(eventX, eventY)) {
    834             mDragging = false;
    835         } else {
    836             mPointerId = event.getPointerId(actionIndex);
    837             updateGlowPosition(eventX, eventY);
    838         }
    839     }
    840 
    841     private void handleUp(MotionEvent event) {
    842         if (DEBUG && mDragging) Log.v(TAG, "** Handle RELEASE");
    843         int actionIndex = event.getActionIndex();
    844         if (event.getPointerId(actionIndex) == mPointerId) {
    845             switchToState(STATE_FINISH, event.getX(actionIndex), event.getY(actionIndex));
    846         }
    847     }
    848 
    849     private void handleCancel(MotionEvent event) {
    850         if (DEBUG && mDragging) Log.v(TAG, "** Handle CANCEL");
    851 
    852         // We should drop the active target here but it interferes with
    853         // moving off the screen in the direction of the navigation bar. At some point we may
    854         // want to revisit how we handle this. For now we'll allow a canceled event to
    855         // activate the current target.
    856 
    857         // mActiveTarget = -1; // Drop the active target if canceled.
    858 
    859         int actionIndex = event.findPointerIndex(mPointerId);
    860         actionIndex = actionIndex == -1 ? 0 : actionIndex;
    861         switchToState(STATE_FINISH, event.getX(actionIndex), event.getY(actionIndex));
    862     }
    863 
    864     private void handleMove(MotionEvent event) {
    865         int activeTarget = -1;
    866         final int historySize = event.getHistorySize();
    867         ArrayList<TargetDrawable> targets = mTargetDrawables;
    868         int ntargets = targets.size();
    869         float x = 0.0f;
    870         float y = 0.0f;
    871         int actionIndex = event.findPointerIndex(mPointerId);
    872 
    873         if (actionIndex == -1) {
    874             return;  // no data for this pointer
    875         }
    876 
    877         for (int k = 0; k < historySize + 1; k++) {
    878             float eventX = k < historySize ? event.getHistoricalX(actionIndex, k)
    879                     : event.getX(actionIndex);
    880             float eventY = k < historySize ? event.getHistoricalY(actionIndex, k)
    881                     :event.getY(actionIndex);
    882             // tx and ty are relative to wave center
    883             float tx = eventX - mWaveCenterX;
    884             float ty = eventY - mWaveCenterY;
    885             float touchRadius = (float) Math.sqrt(dist2(tx, ty));
    886             final float scale = touchRadius > mOuterRadius ? mOuterRadius / touchRadius : 1.0f;
    887             float limitX = tx * scale;
    888             float limitY = ty * scale;
    889             double angleRad = Math.atan2(-ty, tx);
    890 
    891             if (!mDragging) {
    892                 trySwitchToFirstTouchState(eventX, eventY);
    893             }
    894 
    895             if (mDragging) {
    896                 // For multiple targets, snap to the one that matches
    897                 final float snapRadius = mRingScaleFactor * mOuterRadius - mSnapMargin;
    898                 final float snapDistance2 = snapRadius * snapRadius;
    899                 // Find first target in range
    900                 for (int i = 0; i < ntargets; i++) {
    901                     TargetDrawable target = targets.get(i);
    902 
    903                     double targetMinRad = (i - 0.5) * 2 * Math.PI / ntargets;
    904                     double targetMaxRad = (i + 0.5) * 2 * Math.PI / ntargets;
    905                     if (target.isEnabled()) {
    906                         boolean angleMatches =
    907                             (angleRad > targetMinRad && angleRad <= targetMaxRad) ||
    908                             (angleRad + 2 * Math.PI > targetMinRad &&
    909                              angleRad + 2 * Math.PI <= targetMaxRad);
    910                         if (angleMatches && (dist2(tx, ty) > snapDistance2)) {
    911                             activeTarget = i;
    912                         }
    913                     }
    914                 }
    915             }
    916             x = limitX;
    917             y = limitY;
    918         }
    919 
    920         if (!mDragging) {
    921             return;
    922         }
    923 
    924         if (activeTarget != -1) {
    925             switchToState(STATE_SNAP, x,y);
    926             updateGlowPosition(x, y);
    927         } else {
    928             switchToState(STATE_TRACKING, x, y);
    929             updateGlowPosition(x, y);
    930         }
    931 
    932         if (mActiveTarget != activeTarget) {
    933             // Defocus the old target
    934             if (mActiveTarget != -1) {
    935                 TargetDrawable target = targets.get(mActiveTarget);
    936                 target.setState(TargetDrawable.STATE_INACTIVE);
    937             }
    938             // Focus the new target
    939             if (activeTarget != -1) {
    940                 TargetDrawable target = targets.get(activeTarget);
    941                 target.setState(TargetDrawable.STATE_FOCUSED);
    942                 final AccessibilityManager accessibilityManager =
    943                         (AccessibilityManager) getContext().getSystemService(
    944                                 Context.ACCESSIBILITY_SERVICE);
    945                 if (accessibilityManager.isEnabled()) {
    946                     String targetContentDescription = getTargetDescription(activeTarget);
    947                     announceForAccessibility(targetContentDescription);
    948                 }
    949             }
    950         }
    951         mActiveTarget = activeTarget;
    952     }
    953 
    954     @Override
    955     public boolean onHoverEvent(MotionEvent event) {
    956         final AccessibilityManager accessibilityManager =
    957                 (AccessibilityManager) getContext().getSystemService(
    958                         Context.ACCESSIBILITY_SERVICE);
    959         if (accessibilityManager.isTouchExplorationEnabled()) {
    960             final int action = event.getAction();
    961             switch (action) {
    962                 case MotionEvent.ACTION_HOVER_ENTER:
    963                     event.setAction(MotionEvent.ACTION_DOWN);
    964                     break;
    965                 case MotionEvent.ACTION_HOVER_MOVE:
    966                     event.setAction(MotionEvent.ACTION_MOVE);
    967                     break;
    968                 case MotionEvent.ACTION_HOVER_EXIT:
    969                     event.setAction(MotionEvent.ACTION_UP);
    970                     break;
    971             }
    972             onTouchEvent(event);
    973             event.setAction(action);
    974         }
    975         super.onHoverEvent(event);
    976         return true;
    977     }
    978 
    979     /**
    980      * Sets the current grabbed state, and dispatches a grabbed state change
    981      * event to our listener.
    982      */
    983     private void setGrabbedState(int newState) {
    984         if (newState != mGrabbedState) {
    985             if (newState != OnTriggerListener.NO_HANDLE) {
    986                 vibrate();
    987             }
    988             mGrabbedState = newState;
    989             if (mOnTriggerListener != null) {
    990                 if (newState == OnTriggerListener.NO_HANDLE) {
    991                     mOnTriggerListener.onReleased(this, OnTriggerListener.CENTER_HANDLE);
    992                 } else {
    993                     mOnTriggerListener.onGrabbed(this, OnTriggerListener.CENTER_HANDLE);
    994                 }
    995                 mOnTriggerListener.onGrabbedStateChange(this, newState);
    996             }
    997         }
    998     }
    999 
   1000     private boolean trySwitchToFirstTouchState(float x, float y) {
   1001         final float tx = x - mWaveCenterX;
   1002         final float ty = y - mWaveCenterY;
   1003         if (mAlwaysTrackFinger || dist2(tx,ty) <= getScaledGlowRadiusSquared()) {
   1004             if (DEBUG) Log.v(TAG, "** Handle HIT");
   1005             switchToState(STATE_FIRST_TOUCH, x, y);
   1006             updateGlowPosition(tx, ty);
   1007             mDragging = true;
   1008             return true;
   1009         }
   1010         return false;
   1011     }
   1012 
   1013     private void assignDefaultsIfNeeded() {
   1014         if (mOuterRadius == 0.0f) {
   1015             mOuterRadius = Math.max(mOuterRing.getWidth(), mOuterRing.getHeight())/2.0f;
   1016         }
   1017         if (mSnapMargin == 0.0f) {
   1018             mSnapMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
   1019                     SNAP_MARGIN_DEFAULT, getContext().getResources().getDisplayMetrics());
   1020         }
   1021         if (mInnerRadius == 0.0f) {
   1022             mInnerRadius = mHandleDrawable.getWidth() / 10.0f;
   1023         }
   1024     }
   1025 
   1026     private void computeInsets(int dx, int dy) {
   1027         final int layoutDirection = getLayoutDirection();
   1028         final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
   1029 
   1030         switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
   1031             case Gravity.LEFT:
   1032                 mHorizontalInset = 0;
   1033                 break;
   1034             case Gravity.RIGHT:
   1035                 mHorizontalInset = dx;
   1036                 break;
   1037             case Gravity.CENTER_HORIZONTAL:
   1038             default:
   1039                 mHorizontalInset = dx / 2;
   1040                 break;
   1041         }
   1042         switch (absoluteGravity & Gravity.VERTICAL_GRAVITY_MASK) {
   1043             case Gravity.TOP:
   1044                 mVerticalInset = 0;
   1045                 break;
   1046             case Gravity.BOTTOM:
   1047                 mVerticalInset = dy;
   1048                 break;
   1049             case Gravity.CENTER_VERTICAL:
   1050             default:
   1051                 mVerticalInset = dy / 2;
   1052                 break;
   1053         }
   1054     }
   1055 
   1056     /**
   1057      * Given the desired width and height of the ring and the allocated width and height, compute
   1058      * how much we need to scale the ring.
   1059      */
   1060     private float computeScaleFactor(int desiredWidth, int desiredHeight,
   1061             int actualWidth, int actualHeight) {
   1062 
   1063         // Return unity if scaling is not allowed.
   1064         if (!mAllowScaling) return 1f;
   1065 
   1066         final int layoutDirection = getLayoutDirection();
   1067         final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
   1068 
   1069         float scaleX = 1f;
   1070         float scaleY = 1f;
   1071 
   1072         // We use the gravity as a cue for whether we want to scale on a particular axis.
   1073         // We only scale to fit horizontally if we're not pinned to the left or right. Likewise,
   1074         // we only scale to fit vertically if we're not pinned to the top or bottom. In these
   1075         // cases, we want the ring to hang off the side or top/bottom, respectively.
   1076         switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
   1077             case Gravity.LEFT:
   1078             case Gravity.RIGHT:
   1079                 break;
   1080             case Gravity.CENTER_HORIZONTAL:
   1081             default:
   1082                 if (desiredWidth > actualWidth) {
   1083                     scaleX = (1f * actualWidth - mMaxTargetWidth) /
   1084                             (desiredWidth - mMaxTargetWidth);
   1085                 }
   1086                 break;
   1087         }
   1088         switch (absoluteGravity & Gravity.VERTICAL_GRAVITY_MASK) {
   1089             case Gravity.TOP:
   1090             case Gravity.BOTTOM:
   1091                 break;
   1092             case Gravity.CENTER_VERTICAL:
   1093             default:
   1094                 if (desiredHeight > actualHeight) {
   1095                     scaleY = (1f * actualHeight - mMaxTargetHeight) /
   1096                             (desiredHeight - mMaxTargetHeight);
   1097                 }
   1098                 break;
   1099         }
   1100         return Math.min(scaleX, scaleY);
   1101     }
   1102 
   1103     private float getRingWidth() {
   1104         return mRingScaleFactor * Math.max(mOuterRing.getWidth(), 2 * mOuterRadius);
   1105     }
   1106 
   1107     private float getRingHeight() {
   1108         return mRingScaleFactor * Math.max(mOuterRing.getHeight(), 2 * mOuterRadius);
   1109     }
   1110 
   1111     @Override
   1112     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
   1113         super.onLayout(changed, left, top, right, bottom);
   1114         final int width = right - left;
   1115         final int height = bottom - top;
   1116 
   1117         // Target placement width/height. This puts the targets on the greater of the ring
   1118         // width or the specified outer radius.
   1119         final float placementWidth = getRingWidth();
   1120         final float placementHeight = getRingHeight();
   1121         float newWaveCenterX = mHorizontalInset
   1122                 + (mMaxTargetWidth + placementWidth) / 2;
   1123         float newWaveCenterY = mVerticalInset
   1124                 + (mMaxTargetHeight + placementHeight) / 2;
   1125 
   1126         if (mInitialLayout) {
   1127             stopAndHideWaveAnimation();
   1128             hideTargets(false, false);
   1129             mInitialLayout = false;
   1130         }
   1131 
   1132         mOuterRing.setPositionX(newWaveCenterX);
   1133         mOuterRing.setPositionY(newWaveCenterY);
   1134 
   1135         mPointCloud.setScale(mRingScaleFactor);
   1136 
   1137         mHandleDrawable.setPositionX(newWaveCenterX);
   1138         mHandleDrawable.setPositionY(newWaveCenterY);
   1139 
   1140         updateTargetPositions(newWaveCenterX, newWaveCenterY);
   1141         updatePointCloudPosition(newWaveCenterX, newWaveCenterY);
   1142         updateGlowPosition(newWaveCenterX, newWaveCenterY);
   1143 
   1144         mWaveCenterX = newWaveCenterX;
   1145         mWaveCenterY = newWaveCenterY;
   1146 
   1147         if (DEBUG) dump();
   1148     }
   1149 
   1150     private void updateTargetPositions(float centerX, float centerY) {
   1151         // Reposition the target drawables if the view changed.
   1152         ArrayList<TargetDrawable> targets = mTargetDrawables;
   1153         final int size = targets.size();
   1154         final float alpha = (float) (-2.0f * Math.PI / size);
   1155         for (int i = 0; i < size; i++) {
   1156             final TargetDrawable targetIcon = targets.get(i);
   1157             final float angle = alpha * i;
   1158             targetIcon.setPositionX(centerX);
   1159             targetIcon.setPositionY(centerY);
   1160             targetIcon.setX(getRingWidth() / 2 * (float) Math.cos(angle));
   1161             targetIcon.setY(getRingHeight() / 2 * (float) Math.sin(angle));
   1162         }
   1163     }
   1164 
   1165     private void updatePointCloudPosition(float centerX, float centerY) {
   1166         mPointCloud.setCenter(centerX, centerY);
   1167     }
   1168 
   1169     @Override
   1170     protected void onDraw(Canvas canvas) {
   1171         mPointCloud.draw(canvas);
   1172         mOuterRing.draw(canvas);
   1173         final int ntargets = mTargetDrawables.size();
   1174         for (int i = 0; i < ntargets; i++) {
   1175             TargetDrawable target = mTargetDrawables.get(i);
   1176             if (target != null) {
   1177                 target.draw(canvas);
   1178             }
   1179         }
   1180         mHandleDrawable.draw(canvas);
   1181     }
   1182 
   1183     public void setOnTriggerListener(OnTriggerListener listener) {
   1184         mOnTriggerListener = listener;
   1185     }
   1186 
   1187     private float square(float d) {
   1188         return d * d;
   1189     }
   1190 
   1191     private float dist2(float dx, float dy) {
   1192         return dx*dx + dy*dy;
   1193     }
   1194 
   1195     private float getScaledGlowRadiusSquared() {
   1196         final float scaledTapRadius;
   1197         final AccessibilityManager accessibilityManager =
   1198                 (AccessibilityManager) getContext().getSystemService(
   1199                         Context.ACCESSIBILITY_SERVICE);
   1200         if (accessibilityManager.isEnabled()) {
   1201             scaledTapRadius = TAP_RADIUS_SCALE_ACCESSIBILITY_ENABLED * mGlowRadius;
   1202         } else {
   1203             scaledTapRadius = mGlowRadius;
   1204         }
   1205         return square(scaledTapRadius);
   1206     }
   1207 
   1208     private void announceTargets() {
   1209         StringBuilder utterance = new StringBuilder();
   1210         final int targetCount = mTargetDrawables.size();
   1211         for (int i = 0; i < targetCount; i++) {
   1212             String targetDescription = getTargetDescription(i);
   1213             String directionDescription = getDirectionDescription(i);
   1214             if (!TextUtils.isEmpty(targetDescription)
   1215                     && !TextUtils.isEmpty(directionDescription)) {
   1216                 String text = String.format(directionDescription, targetDescription);
   1217                 utterance.append(text);
   1218             }
   1219         }
   1220         if (utterance.length() > 0) {
   1221             announceForAccessibility(utterance.toString());
   1222         }
   1223     }
   1224 
   1225     private String getTargetDescription(int index) {
   1226         if (mTargetDescriptions == null || mTargetDescriptions.isEmpty()) {
   1227             mTargetDescriptions = loadDescriptions(mTargetDescriptionsResourceId);
   1228             if (mTargetDrawables.size() != mTargetDescriptions.size()) {
   1229                 Log.w(TAG, "The number of target drawables must be"
   1230                         + " equal to the number of target descriptions.");
   1231                 return null;
   1232             }
   1233         }
   1234         return mTargetDescriptions.get(index);
   1235     }
   1236 
   1237     private String getDirectionDescription(int index) {
   1238         if (mDirectionDescriptions == null || mDirectionDescriptions.isEmpty()) {
   1239             mDirectionDescriptions = loadDescriptions(mDirectionDescriptionsResourceId);
   1240             if (mTargetDrawables.size() != mDirectionDescriptions.size()) {
   1241                 Log.w(TAG, "The number of target drawables must be"
   1242                         + " equal to the number of direction descriptions.");
   1243                 return null;
   1244             }
   1245         }
   1246         return mDirectionDescriptions.get(index);
   1247     }
   1248 
   1249     private ArrayList<String> loadDescriptions(int resourceId) {
   1250         TypedArray array = getContext().getResources().obtainTypedArray(resourceId);
   1251         final int count = array.length();
   1252         ArrayList<String> targetContentDescriptions = new ArrayList<String>(count);
   1253         for (int i = 0; i < count; i++) {
   1254             String contentDescription = array.getString(i);
   1255             targetContentDescriptions.add(contentDescription);
   1256         }
   1257         array.recycle();
   1258         return targetContentDescriptions;
   1259     }
   1260 
   1261     public int getResourceIdForTarget(int index) {
   1262         final TargetDrawable drawable = mTargetDrawables.get(index);
   1263         return drawable == null ? 0 : drawable.getResourceId();
   1264     }
   1265 
   1266     public void setEnableTarget(int resourceId, boolean enabled) {
   1267         for (int i = 0; i < mTargetDrawables.size(); i++) {
   1268             final TargetDrawable target = mTargetDrawables.get(i);
   1269             if (target.getResourceId() == resourceId) {
   1270                 target.setEnabled(enabled);
   1271                 break; // should never be more than one match
   1272             }
   1273         }
   1274     }
   1275 
   1276     /**
   1277      * Gets the position of a target in the array that matches the given resource.
   1278      * @param resourceId
   1279      * @return the index or -1 if not found
   1280      */
   1281     public int getTargetPosition(int resourceId) {
   1282         for (int i = 0; i < mTargetDrawables.size(); i++) {
   1283             final TargetDrawable target = mTargetDrawables.get(i);
   1284             if (target.getResourceId() == resourceId) {
   1285                 return i; // should never be more than one match
   1286             }
   1287         }
   1288         return -1;
   1289     }
   1290 
   1291     private boolean replaceTargetDrawables(Resources res, int existingResourceId,
   1292             int newResourceId) {
   1293         if (existingResourceId == 0 || newResourceId == 0) {
   1294             return false;
   1295         }
   1296 
   1297         boolean result = false;
   1298         final ArrayList<TargetDrawable> drawables = mTargetDrawables;
   1299         final int size = drawables.size();
   1300         for (int i = 0; i < size; i++) {
   1301             final TargetDrawable target = drawables.get(i);
   1302             if (target != null && target.getResourceId() == existingResourceId) {
   1303                 target.setDrawable(res, newResourceId);
   1304                 result = true;
   1305             }
   1306         }
   1307 
   1308         if (result) {
   1309             requestLayout(); // in case any given drawable's size changes
   1310         }
   1311 
   1312         return result;
   1313     }
   1314 
   1315     /**
   1316      * Searches the given package for a resource to use to replace the Drawable on the
   1317      * target with the given resource id
   1318      * @param component of the .apk that contains the resource
   1319      * @param name of the metadata in the .apk
   1320      * @param existingResId the resource id of the target to search for
   1321      * @return true if found in the given package and replaced at least one target Drawables
   1322      */
   1323     public boolean replaceTargetDrawablesIfPresent(ComponentName component, String name,
   1324                 int existingResId) {
   1325         if (existingResId == 0) return false;
   1326 
   1327         boolean replaced = false;
   1328         if (component != null) {
   1329             try {
   1330                 PackageManager packageManager = getContext().getPackageManager();
   1331                 // Look for the search icon specified in the activity meta-data
   1332                 Bundle metaData = packageManager.getActivityInfo(
   1333                         component, PackageManager.GET_META_DATA).metaData;
   1334                 if (metaData != null) {
   1335                     int iconResId = metaData.getInt(name);
   1336                     if (iconResId != 0) {
   1337                         Resources res = packageManager.getResourcesForActivity(component);
   1338                         replaced = replaceTargetDrawables(res, existingResId, iconResId);
   1339                     }
   1340                 }
   1341             } catch (NameNotFoundException e) {
   1342                 Log.w(TAG, "Failed to swap drawable; "
   1343                         + component.flattenToShortString() + " not found", e);
   1344             } catch (Resources.NotFoundException nfe) {
   1345                 Log.w(TAG, "Failed to swap drawable from "
   1346                         + component.flattenToShortString(), nfe);
   1347             }
   1348         }
   1349         if (!replaced) {
   1350             // Restore the original drawable
   1351             replaceTargetDrawables(getContext().getResources(), existingResId, existingResId);
   1352         }
   1353         return replaced;
   1354     }
   1355 }
   1356