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