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