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