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