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