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