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