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