AppWidgetHostView.java revision a21bf0c6ea0205abced7bfdcaf552535f395f34f
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.view.accessibility.AccessibilityNodeInfo; 42import android.widget.Adapter; 43import android.widget.AdapterView; 44import android.widget.BaseAdapter; 45import android.widget.FrameLayout; 46import android.widget.RemoteViews; 47import android.widget.RemoteViews.OnClickHandler; 48import android.widget.RemoteViewsAdapter.RemoteAdapterConnectionCallback; 49import 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 */ 56public 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 */ 92 public AppWidgetHostView(Context context) { 93 this(context, android.R.anim.fade_in, android.R.anim.fade_out); 94 } 95 96 /** 97 * @hide 98 */ 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"}) 112 public AppWidgetHostView(Context context, int animationIn, int animationOut) { 113 super(context); 114 mContext = context; 115 116 // We want to segregate the view ids within AppWidgets to prevent 117 // problems when those ids collide with view ids in the AppWidgetHost. 118 setIsRootNamespace(true); 119 } 120 121 /** 122 * Pass the given handler to RemoteViews when updating this widget. Unless this 123 * is done immediatly after construction, a call to {@link #updateAppWidget(RemoteViews)} 124 * should be made. 125 * @param handler 126 * @hide 127 */ 128 public void setOnClickHandler(OnClickHandler handler) { 129 mOnClickHandler = handler; 130 } 131 132 /** 133 * Set the AppWidget that will be displayed by this view. This method also adds default padding 134 * to widgets, as described in {@link #getDefaultPaddingForWidget(Context, ComponentName, Rect)} 135 * and can be overridden in order to add custom padding. 136 */ 137 public void setAppWidget(int appWidgetId, AppWidgetProviderInfo info) { 138 mAppWidgetId = appWidgetId; 139 mInfo = info; 140 141 // Sometimes the AppWidgetManager returns a null AppWidgetProviderInfo object for 142 // a widget, eg. for some widgets in safe mode. 143 if (info != null) { 144 // We add padding to the AppWidgetHostView if necessary 145 Rect padding = getDefaultPaddingForWidget(mContext, info.provider, null); 146 setPadding(padding.left, padding.top, padding.right, padding.bottom); 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 */ 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 198 public int getAppWidgetId() { 199 return mAppWidgetId; 200 } 201 202 public AppWidgetProviderInfo getAppWidgetInfo() { 203 return mInfo; 204 } 205 206 @Override 207 protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) { 208 final ParcelableSparseArray jail = new ParcelableSparseArray(); 209 super.dispatchSaveInstanceState(jail); 210 container.put(generateId(), jail); 211 } 212 213 private int generateId() { 214 final int id = getId(); 215 return id == View.NO_ID ? mAppWidgetId : id; 216 } 217 218 @Override 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 that the widget will be displayed at. 247 * @param minHeight The maximum height that the widget will be displayed at. 248 * @param maxWidth The maximum width that the widget will be displayed at. 249 * @param maxHeight The maximum height that the widget will be displayed at. 250 * 251 */ 252 public void updateAppWidgetSize(Bundle newOptions, int minWidth, int minHeight, int maxWidth, 253 int maxHeight) { 254 if (newOptions == null) { 255 newOptions = new Bundle(); 256 } 257 258 Rect padding = new Rect(); 259 if (mInfo != null) { 260 padding = getDefaultPaddingForWidget(mContext, mInfo.provider, padding); 261 } 262 float density = getResources().getDisplayMetrics().density; 263 264 int xPaddingDips = (int) ((padding.left + padding.right) / density); 265 int yPaddingDips = (int) ((padding.top + padding.bottom) / density); 266 267 int newMinWidth = minWidth - xPaddingDips; 268 int newMinHeight = minHeight - yPaddingDips; 269 int newMaxWidth = maxWidth - xPaddingDips; 270 int newMaxHeight = maxHeight - yPaddingDips; 271 272 AppWidgetManager widgetManager = AppWidgetManager.getInstance(mContext); 273 274 // We get the old options to see if the sizes have changed 275 Bundle oldOptions = widgetManager.getAppWidgetOptions(mAppWidgetId); 276 boolean needsUpdate = false; 277 if (newMinWidth != oldOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH) || 278 newMinHeight != oldOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT) || 279 newMaxWidth != oldOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH) || 280 newMaxHeight != oldOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT)) { 281 needsUpdate = true; 282 } 283 284 if (needsUpdate) { 285 newOptions.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, newMinWidth); 286 newOptions.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, newMinHeight); 287 newOptions.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, newMaxWidth); 288 newOptions.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, newMaxHeight); 289 updateAppWidgetOptions(newOptions); 290 } 291 } 292 293 /** 294 * Specify some extra information for the widget provider. Causes a callback to the 295 * AppWidgetProvider. 296 * @see AppWidgetProvider#onAppWidgetOptionsChanged(Context, AppWidgetManager, int, Bundle) 297 * 298 * @param options The bundle of options information. 299 */ 300 public void updateAppWidgetOptions(Bundle options) { 301 AppWidgetManager.getInstance(mContext).updateAppWidgetOptions(mAppWidgetId, options); 302 } 303 304 /** {@inheritDoc} */ 305 @Override 306 public LayoutParams generateLayoutParams(AttributeSet attrs) { 307 // We're being asked to inflate parameters, probably by a LayoutInflater 308 // in a remote Context. To help resolve any remote references, we 309 // inflate through our last mRemoteContext when it exists. 310 final Context context = mRemoteContext != null ? mRemoteContext : mContext; 311 return new FrameLayout.LayoutParams(context, attrs); 312 } 313 314 /** 315 * Update the AppWidgetProviderInfo for this view, and reset it to the 316 * initial layout. 317 */ 318 void resetAppWidget(AppWidgetProviderInfo info) { 319 mInfo = info; 320 mViewMode = VIEW_MODE_NOINIT; 321 updateAppWidget(null); 322 } 323 324 /** 325 * Process a set of {@link RemoteViews} coming in as an update from the 326 * AppWidget provider. Will animate into these new views as needed 327 */ 328 public void updateAppWidget(RemoteViews remoteViews) { 329 330 if (LOGD) Log.d(TAG, "updateAppWidget called mOld=" + mOld); 331 332 boolean recycled = false; 333 View content = null; 334 Exception exception = null; 335 336 // Capture the old view into a bitmap so we can do the crossfade. 337 if (CROSSFADE) { 338 if (mFadeStartTime < 0) { 339 if (mView != null) { 340 final int width = mView.getWidth(); 341 final int height = mView.getHeight(); 342 try { 343 mOld = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 344 } catch (OutOfMemoryError e) { 345 // we just won't do the fade 346 mOld = null; 347 } 348 if (mOld != null) { 349 //mView.drawIntoBitmap(mOld); 350 } 351 } 352 } 353 } 354 355 if (remoteViews == null) { 356 if (mViewMode == VIEW_MODE_DEFAULT) { 357 // We've already done this -- nothing to do. 358 return; 359 } 360 content = getDefaultView(); 361 mLayoutId = -1; 362 mViewMode = VIEW_MODE_DEFAULT; 363 } else { 364 // Prepare a local reference to the remote Context so we're ready to 365 // inflate any requested LayoutParams. 366 mRemoteContext = getRemoteContext(remoteViews); 367 int layoutId = remoteViews.getLayoutId(); 368 369 // If our stale view has been prepared to match active, and the new 370 // layout matches, try recycling it 371 if (content == null && layoutId == mLayoutId) { 372 try { 373 remoteViews.reapply(mContext, mView, mOnClickHandler); 374 content = mView; 375 recycled = true; 376 if (LOGD) Log.d(TAG, "was able to recycled existing layout"); 377 } catch (RuntimeException e) { 378 exception = e; 379 } 380 } 381 382 // Try normal RemoteView inflation 383 if (content == null) { 384 try { 385 content = remoteViews.apply(mContext, this, mOnClickHandler); 386 if (LOGD) Log.d(TAG, "had to inflate new layout"); 387 } catch (RuntimeException e) { 388 exception = e; 389 } 390 } 391 392 mLayoutId = layoutId; 393 mViewMode = VIEW_MODE_CONTENT; 394 } 395 396 if (content == null) { 397 if (mViewMode == VIEW_MODE_ERROR) { 398 // We've already done this -- nothing to do. 399 return ; 400 } 401 Log.w(TAG, "updateAppWidget couldn't find any view, using error view", exception); 402 content = getErrorView(); 403 mViewMode = VIEW_MODE_ERROR; 404 } 405 406 if (!recycled) { 407 prepareView(content); 408 addView(content); 409 } 410 411 if (mView != content) { 412 removeView(mView); 413 mView = content; 414 } 415 416 if (CROSSFADE) { 417 if (mFadeStartTime < 0) { 418 // if there is already an animation in progress, don't do anything -- 419 // the new view will pop in on top of the old one during the cross fade, 420 // and that looks okay. 421 mFadeStartTime = SystemClock.uptimeMillis(); 422 invalidate(); 423 } 424 } 425 } 426 427 /** 428 * Process data-changed notifications for the specified view in the specified 429 * set of {@link RemoteViews} views. 430 */ 431 void viewDataChanged(int viewId) { 432 View v = findViewById(viewId); 433 if ((v != null) && (v instanceof AdapterView<?>)) { 434 AdapterView<?> adapterView = (AdapterView<?>) v; 435 Adapter adapter = adapterView.getAdapter(); 436 if (adapter instanceof BaseAdapter) { 437 BaseAdapter baseAdapter = (BaseAdapter) adapter; 438 baseAdapter.notifyDataSetChanged(); 439 } else if (adapter == null && adapterView instanceof RemoteAdapterConnectionCallback) { 440 // If the adapter is null, it may mean that the RemoteViewsAapter has not yet 441 // connected to its associated service, and hence the adapter hasn't been set. 442 // In this case, we need to defer the notify call until it has been set. 443 ((RemoteAdapterConnectionCallback) adapterView).deferNotifyDataSetChanged(); 444 } 445 } 446 } 447 448 /** 449 * Build a {@link Context} cloned into another package name, usually for the 450 * purposes of reading remote resources. 451 */ 452 private Context getRemoteContext(RemoteViews views) { 453 // Bail if missing package name 454 final String packageName = views.getPackage(); 455 if (packageName == null) return mContext; 456 457 try { 458 // Return if cloned successfully, otherwise default 459 return mContext.createPackageContext(packageName, Context.CONTEXT_RESTRICTED); 460 } catch (NameNotFoundException e) { 461 Log.e(TAG, "Package name " + packageName + " not found"); 462 return mContext; 463 } 464 } 465 466 @Override 467 protected boolean drawChild(Canvas canvas, View child, long drawingTime) { 468 if (CROSSFADE) { 469 int alpha; 470 int l = child.getLeft(); 471 int t = child.getTop(); 472 if (mFadeStartTime > 0) { 473 alpha = (int)(((drawingTime-mFadeStartTime)*255)/FADE_DURATION); 474 if (alpha > 255) { 475 alpha = 255; 476 } 477 Log.d(TAG, "drawChild alpha=" + alpha + " l=" + l + " t=" + t 478 + " w=" + child.getWidth()); 479 if (alpha != 255 && mOld != null) { 480 mOldPaint.setAlpha(255-alpha); 481 //canvas.drawBitmap(mOld, l, t, mOldPaint); 482 } 483 } else { 484 alpha = 255; 485 } 486 int restoreTo = canvas.saveLayerAlpha(l, t, child.getWidth(), child.getHeight(), alpha, 487 Canvas.HAS_ALPHA_LAYER_SAVE_FLAG | Canvas.CLIP_TO_LAYER_SAVE_FLAG); 488 boolean rv = super.drawChild(canvas, child, drawingTime); 489 canvas.restoreToCount(restoreTo); 490 if (alpha < 255) { 491 invalidate(); 492 } else { 493 mFadeStartTime = -1; 494 if (mOld != null) { 495 mOld.recycle(); 496 mOld = null; 497 } 498 } 499 return rv; 500 } else { 501 return super.drawChild(canvas, child, drawingTime); 502 } 503 } 504 505 /** 506 * Prepare the given view to be shown. This might include adjusting 507 * {@link FrameLayout.LayoutParams} before inserting. 508 */ 509 protected void prepareView(View view) { 510 // Take requested dimensions from child, but apply default gravity. 511 FrameLayout.LayoutParams requested = (FrameLayout.LayoutParams)view.getLayoutParams(); 512 if (requested == null) { 513 requested = new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, 514 LayoutParams.MATCH_PARENT); 515 } 516 517 requested.gravity = Gravity.CENTER; 518 view.setLayoutParams(requested); 519 } 520 521 /** 522 * Inflate and return the default layout requested by AppWidget provider. 523 */ 524 protected View getDefaultView() { 525 if (LOGD) { 526 Log.d(TAG, "getDefaultView"); 527 } 528 View defaultView = null; 529 Exception exception = null; 530 531 try { 532 if (mInfo != null) { 533 Context theirContext = mContext.createPackageContext( 534 mInfo.provider.getPackageName(), Context.CONTEXT_RESTRICTED); 535 mRemoteContext = theirContext; 536 LayoutInflater inflater = (LayoutInflater) 537 theirContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 538 inflater = inflater.cloneInContext(theirContext); 539 inflater.setFilter(sInflaterFilter); 540 AppWidgetManager manager = AppWidgetManager.getInstance(mContext); 541 Bundle options = manager.getAppWidgetOptions(mAppWidgetId); 542 543 int layoutId = mInfo.initialLayout; 544 if (options.containsKey(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY)) { 545 int category = options.getInt(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY); 546 if (category == AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD) { 547 layoutId = mInfo.initialKeyguardLayout; 548 } 549 } 550 defaultView = inflater.inflate(layoutId, this, false); 551 } else { 552 Log.w(TAG, "can't inflate defaultView because mInfo is missing"); 553 } 554 } catch (PackageManager.NameNotFoundException e) { 555 exception = e; 556 } catch (RuntimeException e) { 557 exception = e; 558 } 559 560 if (exception != null) { 561 Log.w(TAG, "Error inflating AppWidget " + mInfo + ": " + exception.toString()); 562 } 563 564 if (defaultView == null) { 565 if (LOGD) Log.d(TAG, "getDefaultView couldn't find any view, so inflating error"); 566 defaultView = getErrorView(); 567 } 568 569 return defaultView; 570 } 571 572 /** 573 * Inflate and return a view that represents an error state. 574 */ 575 protected View getErrorView() { 576 TextView tv = new TextView(mContext); 577 tv.setText(com.android.internal.R.string.gadget_host_error_inflating); 578 // TODO: get this color from somewhere. 579 tv.setBackgroundColor(Color.argb(127, 0, 0, 0)); 580 return tv; 581 } 582 583 @Override 584 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 585 super.onInitializeAccessibilityNodeInfo(info); 586 info.setClassName(AppWidgetHostView.class.getName()); 587 } 588 589 private static class ParcelableSparseArray extends SparseArray<Parcelable> implements Parcelable { 590 public int describeContents() { 591 return 0; 592 } 593 594 public void writeToParcel(Parcel dest, int flags) { 595 final int count = size(); 596 dest.writeInt(count); 597 for (int i = 0; i < count; i++) { 598 dest.writeInt(keyAt(i)); 599 dest.writeParcelable(valueAt(i), 0); 600 } 601 } 602 603 public static final Parcelable.Creator<ParcelableSparseArray> CREATOR = 604 new Parcelable.Creator<ParcelableSparseArray>() { 605 public ParcelableSparseArray createFromParcel(Parcel source) { 606 final ParcelableSparseArray array = new ParcelableSparseArray(); 607 final ClassLoader loader = array.getClass().getClassLoader(); 608 final int count = source.readInt(); 609 for (int i = 0; i < count; i++) { 610 array.put(source.readInt(), source.readParcelable(loader)); 611 } 612 return array; 613 } 614 615 public ParcelableSparseArray[] newArray(int size) { 616 return new ParcelableSparseArray[size]; 617 } 618 }; 619 } 620} 621