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