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