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