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