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 
17 package com.android.incallui.widget.multiwaveview;
18 
19 import android.animation.Animator;
20 import android.animation.Animator.AnimatorListener;
21 import android.animation.AnimatorListenerAdapter;
22 import android.animation.TimeInterpolator;
23 import android.animation.ValueAnimator;
24 import android.animation.ValueAnimator.AnimatorUpdateListener;
25 import android.content.ComponentName;
26 import android.content.Context;
27 import android.content.pm.PackageManager;
28 import android.content.pm.PackageManager.NameNotFoundException;
29 import android.content.res.Resources;
30 import android.content.res.TypedArray;
31 import android.graphics.Canvas;
32 import android.graphics.Rect;
33 import android.graphics.drawable.Drawable;
34 import android.os.Bundle;
35 import android.os.Vibrator;
36 import android.support.v4.view.ViewCompat;
37 import android.support.v4.view.accessibility.AccessibilityEventCompat;
38 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
39 import android.support.v4.widget.ExploreByTouchHelper;
40 import android.text.TextUtils;
41 import android.util.AttributeSet;
42 import android.util.Log;
43 import android.util.TypedValue;
44 import android.view.Gravity;
45 import android.view.MotionEvent;
46 import android.view.View;
47 import android.view.accessibility.AccessibilityEvent;
48 import android.view.accessibility.AccessibilityManager;
49 import android.view.accessibility.AccessibilityNodeInfo;
50 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
51 import android.view.accessibility.AccessibilityNodeProvider;
52 
53 import com.android.dialer.R;
54 
55 import java.util.ArrayList;
56 import 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  */
66 public 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;
onGrabbed(View v, int handle)84         public void onGrabbed(View v, int handle);
onReleased(View v, int handle)85         public void onReleased(View v, int handle);
onTrigger(View v, int target)86         public void onTrigger(View v, int target);
onGrabbedStateChange(View v, int handle)87         public void onGrabbedStateChange(View v, int handle);
onFinishFinalAnimation()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 
start()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 
cancel()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 
stop()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 
setSuspended(boolean suspend)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 
GlowPadView(Context context)221     public GlowPadView(Context context) {
222         this(context, null);
223     }
224 
GlowPadView(Context context, AttributeSet attrs)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 
getResourceId(TypedArray a, int id)295     private int getResourceId(TypedArray a, int id) {
296         TypedValue tv = a.peekValue(id);
297         return tv == null ? 0 : tv.resourceId;
298     }
299 
dump()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 
suspendAnimations()310     public void suspendAnimations() {
311         mWaveAnimations.setSuspended(true);
312         mTargetAnimations.setSuspended(true);
313         mGlowAnimations.setSuspended(true);
314     }
315 
resumeAnimations()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
getSuggestedMinimumWidth()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
getSuggestedMinimumHeight()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      */
getScaledSuggestedMinimumWidth()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      */
getScaledSuggestedMinimumHeight()350     protected int getScaledSuggestedMinimumHeight() {
351         return (int) (mRingScaleFactor * Math.max(mOuterRing.getHeight(), 2 * mOuterRadius)
352                 + mMaxTargetHeight);
353     }
354 
resolveMeasured(int measureSpec, int desired)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
onMeasure(int widthMeasureSpec, int heightMeasureSpec)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 
switchToState(int state, float x, float y)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 
showGlow(int duration, int delay, float finalAlpha, AnimatorListener finishListener)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 
hideGlow(int duration, int delay, float finalAlpha, AnimatorListener finishListener)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 
deactivateTargets()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      */
dispatchTriggerEvent(int whichTarget)474     private void dispatchTriggerEvent(int whichTarget) {
475         vibrate();
476         if (mOnTriggerListener != null) {
477             mOnTriggerListener.onTrigger(this, whichTarget);
478         }
479     }
480 
dispatchOnFinishFinalAnimation()481     private void dispatchOnFinishFinalAnimation() {
482         if (mOnTriggerListener != null) {
483             mOnTriggerListener.onFinishFinalAnimation();
484         }
485     }
486 
doFinish()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 
highlightSelected(int activeTarget)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 
hideUnselected(int active)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 
hideTargets(boolean animate, boolean expanded)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 
showTargets(boolean animate)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 
vibrate()595     private void vibrate() {
596         if (mVibrator != null) {
597             mVibrator.vibrate(mVibrationDuration);
598         }
599     }
600 
loadDrawableArray(int resourceId)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 
internalSetTargetResources(int resourceId)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      */
setTargetResources(int resourceId)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 
getTargetResourceId()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      */
setHandleDrawable(int resourceId)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      */
setTargetDescriptionsResourceId(int resourceId)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      */
getTargetDescriptionsResourceId()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      */
setDirectionDescriptionsResourceId(int resourceId)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      */
getDirectionDescriptionsResourceId()706     public int getDirectionDescriptionsResourceId() {
707         return mDirectionDescriptionsResourceId;
708     }
709 
710     /**
711      * Enable or disable vibrate on touch.
712      *
713      * @param enabled
714      */
setVibrateEnabled(boolean enabled)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      */
ping()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 
stopAndHideWaveAnimation()746     private void stopAndHideWaveAnimation() {
747         mWaveAnimations.cancel();
748         mPointCloud.waveManager.setAlpha(0.0f);
749     }
750 
startWaveAnimation()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      */
reset(boolean animate)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 
startBackgroundAnimation(int duration, float alpha)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
onTouchEvent(MotionEvent event)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 
updateGlowPosition(float x, float y)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 
handleDown(MotionEvent event)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 
handleUp(MotionEvent event)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 
handleCancel(MotionEvent event)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 
handleMove(MotionEvent event)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 
getRingWidth()1122     private float getRingWidth() {
1123         return mRingScaleFactor * Math.max(mOuterRing.getWidth(), 2 * mOuterRadius);
1124     }
1125 
getRingHeight()1126     private float getRingHeight() {
1127         return mRingScaleFactor * Math.max(mOuterRing.getHeight(), 2 * mOuterRadius);
1128     }
1129 
1130     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)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 
updateTargetPositions(float centerX, float centerY)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 
updatePointCloudPosition(float centerX, float centerY)1184     private void updatePointCloudPosition(float centerX, float centerY) {
1185         mPointCloud.setCenter(centerX, centerY);
1186     }
1187 
1188     @Override
onDraw(Canvas canvas)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 
setOnTriggerListener(OnTriggerListener listener)1202     public void setOnTriggerListener(OnTriggerListener listener) {
1203         mOnTriggerListener = listener;
1204     }
1205 
square(float d)1206     private float square(float d) {
1207         return d * d;
1208     }
1209 
dist2(float dx, float dy)1210     private float dist2(float dx, float dy) {
1211         return dx*dx + dy*dy;
1212     }
1213 
getScaledGlowRadiusSquared()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 
announceTargets()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 
getTargetDescription(int index)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 
getDirectionDescription(int index)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 
loadDescriptions(int resourceId)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 
getResourceIdForTarget(int index)1280     public int getResourceIdForTarget(int index) {
1281         final TargetDrawable drawable = mTargetDrawables.get(index);
1282         return drawable == null ? 0 : drawable.getResourceId();
1283     }
1284 
setEnableTarget(int resourceId, boolean enabled)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      */
getTargetPosition(int resourceId)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 
replaceTargetDrawables(Resources res, int existingResourceId, int newResourceId)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      */
replaceTargetDrawablesIfPresent(ComponentName component, String name, int existingResId)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 
GlowpadExploreByTouchHelper(View forView)1379         public GlowpadExploreByTouchHelper(View forView) {
1380             super(forView);
1381         }
1382 
1383         @Override
getVirtualViewAt(float x, float y)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
getVisibleVirtualViews(List<Integer> virtualViewIds)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
onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event)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
onInitializeAccessibilityEvent(View host, AccessibilityEvent event)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
onPopulateNodeForHost(AccessibilityNodeInfoCompat node)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
performAccessibilityAction(View host, int action, Bundle args)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
onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfoCompat node)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
onPerformActionForVirtualView(int virtualViewId, int action, Bundle arguments)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