• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 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 android.appwidget;
18 
19 import android.content.ComponentName;
20 import android.content.Context;
21 import android.content.pm.ApplicationInfo;
22 import android.content.pm.PackageManager;
23 import android.content.pm.PackageManager.NameNotFoundException;
24 import android.content.res.Resources;
25 import android.graphics.Bitmap;
26 import android.graphics.Canvas;
27 import android.graphics.Color;
28 import android.graphics.Paint;
29 import android.graphics.Rect;
30 import android.os.Build;
31 import android.os.Bundle;
32 import android.os.Parcel;
33 import android.os.Parcelable;
34 import android.os.Process;
35 import android.os.SystemClock;
36 import android.os.UserHandle;
37 import android.util.AttributeSet;
38 import android.util.Log;
39 import android.util.SparseArray;
40 import android.view.Gravity;
41 import android.view.LayoutInflater;
42 import android.view.View;
43 import android.view.accessibility.AccessibilityNodeInfo;
44 import android.widget.Adapter;
45 import android.widget.AdapterView;
46 import android.widget.BaseAdapter;
47 import android.widget.FrameLayout;
48 import android.widget.RemoteViews;
49 import android.widget.RemoteViews.OnClickHandler;
50 import android.widget.RemoteViewsAdapter.RemoteAdapterConnectionCallback;
51 import android.widget.TextView;
52 
53 /**
54  * Provides the glue to show AppWidget views. This class offers automatic animation
55  * between updates, and will try recycling old views for each incoming
56  * {@link RemoteViews}.
57  */
58 public class AppWidgetHostView extends FrameLayout {
59     static final String TAG = "AppWidgetHostView";
60     static final boolean LOGD = false;
61     static final boolean CROSSFADE = false;
62 
63     static final int VIEW_MODE_NOINIT = 0;
64     static final int VIEW_MODE_CONTENT = 1;
65     static final int VIEW_MODE_ERROR = 2;
66     static final int VIEW_MODE_DEFAULT = 3;
67 
68     static final int FADE_DURATION = 1000;
69 
70     // When we're inflating the initialLayout for a AppWidget, we only allow
71     // views that are allowed in RemoteViews.
72     static final LayoutInflater.Filter sInflaterFilter = new LayoutInflater.Filter() {
73         public boolean onLoadClass(Class clazz) {
74             return clazz.isAnnotationPresent(RemoteViews.RemoteView.class);
75         }
76     };
77 
78     Context mContext;
79     Context mRemoteContext;
80 
81     int mAppWidgetId;
82     AppWidgetProviderInfo mInfo;
83     View mView;
84     int mViewMode = VIEW_MODE_NOINIT;
85     int mLayoutId = -1;
86     long mFadeStartTime = -1;
87     Bitmap mOld;
88     Paint mOldPaint = new Paint();
89     private OnClickHandler mOnClickHandler;
90     private UserHandle mUser;
91 
92     /**
93      * Create a host view.  Uses default fade animations.
94      */
AppWidgetHostView(Context context)95     public AppWidgetHostView(Context context) {
96         this(context, android.R.anim.fade_in, android.R.anim.fade_out);
97     }
98 
99     /**
100      * @hide
101      */
AppWidgetHostView(Context context, OnClickHandler handler)102     public AppWidgetHostView(Context context, OnClickHandler handler) {
103         this(context, android.R.anim.fade_in, android.R.anim.fade_out);
104         mOnClickHandler = handler;
105     }
106 
107     /**
108      * Create a host view. Uses specified animations when pushing
109      * {@link #updateAppWidget(RemoteViews)}.
110      *
111      * @param animationIn Resource ID of in animation to use
112      * @param animationOut Resource ID of out animation to use
113      */
114     @SuppressWarnings({"UnusedDeclaration"})
AppWidgetHostView(Context context, int animationIn, int animationOut)115     public AppWidgetHostView(Context context, int animationIn, int animationOut) {
116         super(context);
117         mContext = context;
118         mUser = Process.myUserHandle();
119         // We want to segregate the view ids within AppWidgets to prevent
120         // problems when those ids collide with view ids in the AppWidgetHost.
121         setIsRootNamespace(true);
122     }
123 
124     /** @hide */
setUserId(int userId)125     public void setUserId(int userId) {
126         mUser = new UserHandle(userId);
127     }
128 
129     /**
130      * Pass the given handler to RemoteViews when updating this widget. Unless this
131      * is done immediatly after construction, a call to {@link #updateAppWidget(RemoteViews)}
132      * should be made.
133      * @param handler
134      * @hide
135      */
setOnClickHandler(OnClickHandler handler)136     public void setOnClickHandler(OnClickHandler handler) {
137         mOnClickHandler = handler;
138     }
139 
140     /**
141      * Set the AppWidget that will be displayed by this view. This method also adds default padding
142      * to widgets, as described in {@link #getDefaultPaddingForWidget(Context, ComponentName, Rect)}
143      * and can be overridden in order to add custom padding.
144      */
setAppWidget(int appWidgetId, AppWidgetProviderInfo info)145     public void setAppWidget(int appWidgetId, AppWidgetProviderInfo info) {
146         mAppWidgetId = appWidgetId;
147         mInfo = info;
148 
149         // Sometimes the AppWidgetManager returns a null AppWidgetProviderInfo object for
150         // a widget, eg. for some widgets in safe mode.
151         if (info != null) {
152             // We add padding to the AppWidgetHostView if necessary
153             Rect padding = getDefaultPaddingForWidget(mContext, info.provider, null);
154             setPadding(padding.left, padding.top, padding.right, padding.bottom);
155             setContentDescription(info.label);
156         }
157     }
158 
159     /**
160      * As of ICE_CREAM_SANDWICH we are automatically adding padding to widgets targeting
161      * ICE_CREAM_SANDWICH and higher. The new widget design guidelines strongly recommend
162      * that widget developers do not add extra padding to their widgets. This will help
163      * achieve consistency among widgets.
164      *
165      * Note: this method is only needed by developers of AppWidgetHosts. The method is provided in
166      * order for the AppWidgetHost to account for the automatic padding when computing the number
167      * of cells to allocate to a particular widget.
168      *
169      * @param context the current context
170      * @param component the component name of the widget
171      * @param padding Rect in which to place the output, if null, a new Rect will be allocated and
172      *                returned
173      * @return default padding for this widget, in pixels
174      */
getDefaultPaddingForWidget(Context context, ComponentName component, Rect padding)175     public static Rect getDefaultPaddingForWidget(Context context, ComponentName component,
176             Rect padding) {
177         PackageManager packageManager = context.getPackageManager();
178         ApplicationInfo appInfo;
179 
180         if (padding == null) {
181             padding = new Rect(0, 0, 0, 0);
182         } else {
183             padding.set(0, 0, 0, 0);
184         }
185 
186         try {
187             appInfo = packageManager.getApplicationInfo(component.getPackageName(), 0);
188         } catch (NameNotFoundException e) {
189             // if we can't find the package, return 0 padding
190             return padding;
191         }
192 
193         if (appInfo.targetSdkVersion >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
194             Resources r = context.getResources();
195             padding.left = r.getDimensionPixelSize(com.android.internal.
196                     R.dimen.default_app_widget_padding_left);
197             padding.right = r.getDimensionPixelSize(com.android.internal.
198                     R.dimen.default_app_widget_padding_right);
199             padding.top = r.getDimensionPixelSize(com.android.internal.
200                     R.dimen.default_app_widget_padding_top);
201             padding.bottom = r.getDimensionPixelSize(com.android.internal.
202                     R.dimen.default_app_widget_padding_bottom);
203         }
204         return padding;
205     }
206 
getAppWidgetId()207     public int getAppWidgetId() {
208         return mAppWidgetId;
209     }
210 
getAppWidgetInfo()211     public AppWidgetProviderInfo getAppWidgetInfo() {
212         return mInfo;
213     }
214 
215     @Override
dispatchSaveInstanceState(SparseArray<Parcelable> container)216     protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
217         final ParcelableSparseArray jail = new ParcelableSparseArray();
218         super.dispatchSaveInstanceState(jail);
219         container.put(generateId(), jail);
220     }
221 
generateId()222     private int generateId() {
223         final int id = getId();
224         return id == View.NO_ID ? mAppWidgetId : id;
225     }
226 
227     @Override
dispatchRestoreInstanceState(SparseArray<Parcelable> container)228     protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
229         final Parcelable parcelable = container.get(generateId());
230 
231         ParcelableSparseArray jail = null;
232         if (parcelable != null && parcelable instanceof ParcelableSparseArray) {
233             jail = (ParcelableSparseArray) parcelable;
234         }
235 
236         if (jail == null) jail = new ParcelableSparseArray();
237 
238         try  {
239             super.dispatchRestoreInstanceState(jail);
240         } catch (Exception e) {
241             Log.e(TAG, "failed to restoreInstanceState for widget id: " + mAppWidgetId + ", "
242                     + (mInfo == null ? "null" : mInfo.provider), e);
243         }
244     }
245 
246     /**
247      * Provide guidance about the size of this widget to the AppWidgetManager. The widths and
248      * heights should correspond to the full area the AppWidgetHostView is given. Padding added by
249      * the framework will be accounted for automatically. This information gets embedded into the
250      * AppWidget options and causes a callback to the AppWidgetProvider.
251      * @see AppWidgetProvider#onAppWidgetOptionsChanged(Context, AppWidgetManager, int, Bundle)
252      *
253      * @param newOptions The bundle of options, in addition to the size information,
254      *          can be null.
255      * @param minWidth The minimum width in dips that the widget will be displayed at.
256      * @param minHeight The maximum height in dips that the widget will be displayed at.
257      * @param maxWidth The maximum width in dips that the widget will be displayed at.
258      * @param maxHeight The maximum height in dips that the widget will be displayed at.
259      *
260      */
updateAppWidgetSize(Bundle newOptions, int minWidth, int minHeight, int maxWidth, int maxHeight)261     public void updateAppWidgetSize(Bundle newOptions, int minWidth, int minHeight, int maxWidth,
262             int maxHeight) {
263         updateAppWidgetSize(newOptions, minWidth, minHeight, maxWidth, maxHeight, false);
264     }
265 
266     /**
267      * @hide
268      */
updateAppWidgetSize(Bundle newOptions, int minWidth, int minHeight, int maxWidth, int maxHeight, boolean ignorePadding)269     public void updateAppWidgetSize(Bundle newOptions, int minWidth, int minHeight, int maxWidth,
270             int maxHeight, boolean ignorePadding) {
271         if (newOptions == null) {
272             newOptions = new Bundle();
273         }
274 
275         Rect padding = new Rect();
276         if (mInfo != null) {
277             padding = getDefaultPaddingForWidget(mContext, mInfo.provider, padding);
278         }
279         float density = getResources().getDisplayMetrics().density;
280 
281         int xPaddingDips = (int) ((padding.left + padding.right) / density);
282         int yPaddingDips = (int) ((padding.top + padding.bottom) / density);
283 
284         int newMinWidth = minWidth - (ignorePadding ? 0 : xPaddingDips);
285         int newMinHeight = minHeight - (ignorePadding ? 0 : yPaddingDips);
286         int newMaxWidth = maxWidth - (ignorePadding ? 0 : xPaddingDips);
287         int newMaxHeight = maxHeight - (ignorePadding ? 0 : yPaddingDips);
288 
289         AppWidgetManager widgetManager = AppWidgetManager.getInstance(mContext);
290 
291         // We get the old options to see if the sizes have changed
292         Bundle oldOptions = widgetManager.getAppWidgetOptions(mAppWidgetId);
293         boolean needsUpdate = false;
294         if (newMinWidth != oldOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH) ||
295                 newMinHeight != oldOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT) ||
296                 newMaxWidth != oldOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH) ||
297                 newMaxHeight != oldOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT)) {
298             needsUpdate = true;
299         }
300 
301         if (needsUpdate) {
302             newOptions.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, newMinWidth);
303             newOptions.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, newMinHeight);
304             newOptions.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, newMaxWidth);
305             newOptions.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, newMaxHeight);
306             updateAppWidgetOptions(newOptions);
307         }
308     }
309 
310     /**
311      * Specify some extra information for the widget provider. Causes a callback to the
312      * AppWidgetProvider.
313      * @see AppWidgetProvider#onAppWidgetOptionsChanged(Context, AppWidgetManager, int, Bundle)
314      *
315      * @param options The bundle of options information.
316      */
updateAppWidgetOptions(Bundle options)317     public void updateAppWidgetOptions(Bundle options) {
318         AppWidgetManager.getInstance(mContext).updateAppWidgetOptions(mAppWidgetId, options);
319     }
320 
321     /** {@inheritDoc} */
322     @Override
generateLayoutParams(AttributeSet attrs)323     public LayoutParams generateLayoutParams(AttributeSet attrs) {
324         // We're being asked to inflate parameters, probably by a LayoutInflater
325         // in a remote Context. To help resolve any remote references, we
326         // inflate through our last mRemoteContext when it exists.
327         final Context context = mRemoteContext != null ? mRemoteContext : mContext;
328         return new FrameLayout.LayoutParams(context, attrs);
329     }
330 
331     /**
332      * Update the AppWidgetProviderInfo for this view, and reset it to the
333      * initial layout.
334      */
resetAppWidget(AppWidgetProviderInfo info)335     void resetAppWidget(AppWidgetProviderInfo info) {
336         mInfo = info;
337         mViewMode = VIEW_MODE_NOINIT;
338         updateAppWidget(null);
339     }
340 
341     /**
342      * Process a set of {@link RemoteViews} coming in as an update from the
343      * AppWidget provider. Will animate into these new views as needed
344      */
updateAppWidget(RemoteViews remoteViews)345     public void updateAppWidget(RemoteViews remoteViews) {
346 
347         if (LOGD) Log.d(TAG, "updateAppWidget called mOld=" + mOld);
348 
349         boolean recycled = false;
350         View content = null;
351         Exception exception = null;
352 
353         // Capture the old view into a bitmap so we can do the crossfade.
354         if (CROSSFADE) {
355             if (mFadeStartTime < 0) {
356                 if (mView != null) {
357                     final int width = mView.getWidth();
358                     final int height = mView.getHeight();
359                     try {
360                         mOld = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
361                     } catch (OutOfMemoryError e) {
362                         // we just won't do the fade
363                         mOld = null;
364                     }
365                     if (mOld != null) {
366                         //mView.drawIntoBitmap(mOld);
367                     }
368                 }
369             }
370         }
371 
372         if (remoteViews == null) {
373             if (mViewMode == VIEW_MODE_DEFAULT) {
374                 // We've already done this -- nothing to do.
375                 return;
376             }
377             content = getDefaultView();
378             mLayoutId = -1;
379             mViewMode = VIEW_MODE_DEFAULT;
380         } else {
381             // Prepare a local reference to the remote Context so we're ready to
382             // inflate any requested LayoutParams.
383             mRemoteContext = getRemoteContext(remoteViews);
384             int layoutId = remoteViews.getLayoutId();
385 
386             // If our stale view has been prepared to match active, and the new
387             // layout matches, try recycling it
388             if (content == null && layoutId == mLayoutId) {
389                 try {
390                     remoteViews.reapply(mContext, mView, mOnClickHandler);
391                     content = mView;
392                     recycled = true;
393                     if (LOGD) Log.d(TAG, "was able to recycled existing layout");
394                 } catch (RuntimeException e) {
395                     exception = e;
396                 }
397             }
398 
399             // Try normal RemoteView inflation
400             if (content == null) {
401                 try {
402                     content = remoteViews.apply(mContext, this, mOnClickHandler);
403                     if (LOGD) Log.d(TAG, "had to inflate new layout");
404                 } catch (RuntimeException e) {
405                     exception = e;
406                 }
407             }
408 
409             mLayoutId = layoutId;
410             mViewMode = VIEW_MODE_CONTENT;
411         }
412 
413         if (content == null) {
414             if (mViewMode == VIEW_MODE_ERROR) {
415                 // We've already done this -- nothing to do.
416                 return ;
417             }
418             Log.w(TAG, "updateAppWidget couldn't find any view, using error view", exception);
419             content = getErrorView();
420             mViewMode = VIEW_MODE_ERROR;
421         }
422 
423         if (!recycled) {
424             prepareView(content);
425             addView(content);
426         }
427 
428         if (mView != content) {
429             removeView(mView);
430             mView = content;
431         }
432 
433         if (CROSSFADE) {
434             if (mFadeStartTime < 0) {
435                 // if there is already an animation in progress, don't do anything --
436                 // the new view will pop in on top of the old one during the cross fade,
437                 // and that looks okay.
438                 mFadeStartTime = SystemClock.uptimeMillis();
439                 invalidate();
440             }
441         }
442     }
443 
444     /**
445      * Process data-changed notifications for the specified view in the specified
446      * set of {@link RemoteViews} views.
447      */
viewDataChanged(int viewId)448     void viewDataChanged(int viewId) {
449         View v = findViewById(viewId);
450         if ((v != null) && (v instanceof AdapterView<?>)) {
451             AdapterView<?> adapterView = (AdapterView<?>) v;
452             Adapter adapter = adapterView.getAdapter();
453             if (adapter instanceof BaseAdapter) {
454                 BaseAdapter baseAdapter = (BaseAdapter) adapter;
455                 baseAdapter.notifyDataSetChanged();
456             }  else if (adapter == null && adapterView instanceof RemoteAdapterConnectionCallback) {
457                 // If the adapter is null, it may mean that the RemoteViewsAapter has not yet
458                 // connected to its associated service, and hence the adapter hasn't been set.
459                 // In this case, we need to defer the notify call until it has been set.
460                 ((RemoteAdapterConnectionCallback) adapterView).deferNotifyDataSetChanged();
461             }
462         }
463     }
464 
465     /**
466      * Build a {@link Context} cloned into another package name, usually for the
467      * purposes of reading remote resources.
468      */
getRemoteContext(RemoteViews views)469     private Context getRemoteContext(RemoteViews views) {
470         // Bail if missing package name
471         final String packageName = views.getPackage();
472         if (packageName == null) return mContext;
473 
474         try {
475             // Return if cloned successfully, otherwise default
476             return mContext.createPackageContextAsUser(packageName, Context.CONTEXT_RESTRICTED,
477                     mUser);
478         } catch (NameNotFoundException e) {
479             Log.e(TAG, "Package name " + packageName + " not found");
480             return mContext;
481         }
482     }
483 
484     @Override
drawChild(Canvas canvas, View child, long drawingTime)485     protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
486         if (CROSSFADE) {
487             int alpha;
488             int l = child.getLeft();
489             int t = child.getTop();
490             if (mFadeStartTime > 0) {
491                 alpha = (int)(((drawingTime-mFadeStartTime)*255)/FADE_DURATION);
492                 if (alpha > 255) {
493                     alpha = 255;
494                 }
495                 Log.d(TAG, "drawChild alpha=" + alpha + " l=" + l + " t=" + t
496                         + " w=" + child.getWidth());
497                 if (alpha != 255 && mOld != null) {
498                     mOldPaint.setAlpha(255-alpha);
499                     //canvas.drawBitmap(mOld, l, t, mOldPaint);
500                 }
501             } else {
502                 alpha = 255;
503             }
504             int restoreTo = canvas.saveLayerAlpha(l, t, child.getWidth(), child.getHeight(), alpha,
505                     Canvas.HAS_ALPHA_LAYER_SAVE_FLAG | Canvas.CLIP_TO_LAYER_SAVE_FLAG);
506             boolean rv = super.drawChild(canvas, child, drawingTime);
507             canvas.restoreToCount(restoreTo);
508             if (alpha < 255) {
509                 invalidate();
510             } else {
511                 mFadeStartTime = -1;
512                 if (mOld != null) {
513                     mOld.recycle();
514                     mOld = null;
515                 }
516             }
517             return rv;
518         } else {
519             return super.drawChild(canvas, child, drawingTime);
520         }
521     }
522 
523     /**
524      * Prepare the given view to be shown. This might include adjusting
525      * {@link FrameLayout.LayoutParams} before inserting.
526      */
prepareView(View view)527     protected void prepareView(View view) {
528         // Take requested dimensions from child, but apply default gravity.
529         FrameLayout.LayoutParams requested = (FrameLayout.LayoutParams)view.getLayoutParams();
530         if (requested == null) {
531             requested = new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT,
532                     LayoutParams.MATCH_PARENT);
533         }
534 
535         requested.gravity = Gravity.CENTER;
536         view.setLayoutParams(requested);
537     }
538 
539     /**
540      * Inflate and return the default layout requested by AppWidget provider.
541      */
getDefaultView()542     protected View getDefaultView() {
543         if (LOGD) {
544             Log.d(TAG, "getDefaultView");
545         }
546         View defaultView = null;
547         Exception exception = null;
548 
549         try {
550             if (mInfo != null) {
551                 Context theirContext = mContext.createPackageContextAsUser(
552                         mInfo.provider.getPackageName(), Context.CONTEXT_RESTRICTED, mUser);
553                 mRemoteContext = theirContext;
554                 LayoutInflater inflater = (LayoutInflater)
555                         theirContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
556                 inflater = inflater.cloneInContext(theirContext);
557                 inflater.setFilter(sInflaterFilter);
558                 AppWidgetManager manager = AppWidgetManager.getInstance(mContext);
559                 Bundle options = manager.getAppWidgetOptions(mAppWidgetId);
560 
561                 int layoutId = mInfo.initialLayout;
562                 if (options.containsKey(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY)) {
563                     int category = options.getInt(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY);
564                     if (category == AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD) {
565                         int kgLayoutId = mInfo.initialKeyguardLayout;
566                         // If a default keyguard layout is not specified, use the standard
567                         // default layout.
568                         layoutId = kgLayoutId == 0 ? layoutId : kgLayoutId;
569                     }
570                 }
571                 defaultView = inflater.inflate(layoutId, this, false);
572             } else {
573                 Log.w(TAG, "can't inflate defaultView because mInfo is missing");
574             }
575         } catch (PackageManager.NameNotFoundException e) {
576             exception = e;
577         } catch (RuntimeException e) {
578             exception = e;
579         }
580 
581         if (exception != null) {
582             Log.w(TAG, "Error inflating AppWidget " + mInfo + ": " + exception.toString());
583         }
584 
585         if (defaultView == null) {
586             if (LOGD) Log.d(TAG, "getDefaultView couldn't find any view, so inflating error");
587             defaultView = getErrorView();
588         }
589 
590         return defaultView;
591     }
592 
593     /**
594      * Inflate and return a view that represents an error state.
595      */
getErrorView()596     protected View getErrorView() {
597         TextView tv = new TextView(mContext);
598         tv.setText(com.android.internal.R.string.gadget_host_error_inflating);
599         // TODO: get this color from somewhere.
600         tv.setBackgroundColor(Color.argb(127, 0, 0, 0));
601         return tv;
602     }
603 
604     @Override
onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)605     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
606         super.onInitializeAccessibilityNodeInfo(info);
607         info.setClassName(AppWidgetHostView.class.getName());
608     }
609 
610     private static class ParcelableSparseArray extends SparseArray<Parcelable> implements Parcelable {
describeContents()611         public int describeContents() {
612             return 0;
613         }
614 
writeToParcel(Parcel dest, int flags)615         public void writeToParcel(Parcel dest, int flags) {
616             final int count = size();
617             dest.writeInt(count);
618             for (int i = 0; i < count; i++) {
619                 dest.writeInt(keyAt(i));
620                 dest.writeParcelable(valueAt(i), 0);
621             }
622         }
623 
624         public static final Parcelable.Creator<ParcelableSparseArray> CREATOR =
625                 new Parcelable.Creator<ParcelableSparseArray>() {
626                     public ParcelableSparseArray createFromParcel(Parcel source) {
627                         final ParcelableSparseArray array = new ParcelableSparseArray();
628                         final ClassLoader loader = array.getClass().getClassLoader();
629                         final int count = source.readInt();
630                         for (int i = 0; i < count; i++) {
631                             array.put(source.readInt(), source.readParcelable(loader));
632                         }
633                         return array;
634                     }
635 
636                     public ParcelableSparseArray[] newArray(int size) {
637                         return new ParcelableSparseArray[size];
638                     }
639                 };
640     }
641 }
642