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