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