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