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