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