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