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