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 setContentDescription(info.label); 148 } 149 } 150 151 /** 152 * As of ICE_CREAM_SANDWICH we are automatically adding padding to widgets targeting 153 * ICE_CREAM_SANDWICH and higher. The new widget design guidelines strongly recommend 154 * that widget developers do not add extra padding to their widgets. This will help 155 * achieve consistency among widgets. 156 * 157 * Note: this method is only needed by developers of AppWidgetHosts. The method is provided in 158 * order for the AppWidgetHost to account for the automatic padding when computing the number 159 * of cells to allocate to a particular widget. 160 * 161 * @param context the current context 162 * @param component the component name of the widget 163 * @param padding Rect in which to place the output, if null, a new Rect will be allocated and 164 * returned 165 * @return default padding for this widget, in pixels 166 */ 167 public static Rect getDefaultPaddingForWidget(Context context, ComponentName component, 168 Rect padding) { 169 PackageManager packageManager = context.getPackageManager(); 170 ApplicationInfo appInfo; 171 172 if (padding == null) { 173 padding = new Rect(0, 0, 0, 0); 174 } else { 175 padding.set(0, 0, 0, 0); 176 } 177 178 try { 179 appInfo = packageManager.getApplicationInfo(component.getPackageName(), 0); 180 } catch (NameNotFoundException e) { 181 // if we can't find the package, return 0 padding 182 return padding; 183 } 184 185 if (appInfo.targetSdkVersion >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 186 Resources r = context.getResources(); 187 padding.left = r.getDimensionPixelSize(com.android.internal. 188 R.dimen.default_app_widget_padding_left); 189 padding.right = r.getDimensionPixelSize(com.android.internal. 190 R.dimen.default_app_widget_padding_right); 191 padding.top = r.getDimensionPixelSize(com.android.internal. 192 R.dimen.default_app_widget_padding_top); 193 padding.bottom = r.getDimensionPixelSize(com.android.internal. 194 R.dimen.default_app_widget_padding_bottom); 195 } 196 return padding; 197 } 198 199 public int getAppWidgetId() { 200 return mAppWidgetId; 201 } 202 203 public AppWidgetProviderInfo getAppWidgetInfo() { 204 return mInfo; 205 } 206 207 @Override 208 protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) { 209 final ParcelableSparseArray jail = new ParcelableSparseArray(); 210 super.dispatchSaveInstanceState(jail); 211 container.put(generateId(), jail); 212 } 213 214 private int generateId() { 215 final int id = getId(); 216 return id == View.NO_ID ? mAppWidgetId : id; 217 } 218 219 @Override 220 protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) { 221 final Parcelable parcelable = container.get(generateId()); 222 223 ParcelableSparseArray jail = null; 224 if (parcelable != null && parcelable instanceof ParcelableSparseArray) { 225 jail = (ParcelableSparseArray) parcelable; 226 } 227 228 if (jail == null) jail = new ParcelableSparseArray(); 229 230 try { 231 super.dispatchRestoreInstanceState(jail); 232 } catch (Exception e) { 233 Log.e(TAG, "failed to restoreInstanceState for widget id: " + mAppWidgetId + ", " 234 + (mInfo == null ? "null" : mInfo.provider), e); 235 } 236 } 237 238 /** 239 * Provide guidance about the size of this widget to the AppWidgetManager. The widths and 240 * heights should correspond to the full area the AppWidgetHostView is given. Padding added by 241 * the framework will be accounted for automatically. This information gets embedded into the 242 * AppWidget options and causes a callback to the AppWidgetProvider. 243 * @see AppWidgetProvider#onAppWidgetOptionsChanged(Context, AppWidgetManager, int, Bundle) 244 * 245 * @param newOptions The bundle of options, in addition to the size information, 246 * can be null. 247 * @param minWidth The minimum width in dips that the widget will be displayed at. 248 * @param minHeight The maximum height in dips that the widget will be displayed at. 249 * @param maxWidth The maximum width in dips that the widget will be displayed at. 250 * @param maxHeight The maximum height in dips that the widget will be displayed at. 251 * 252 */ 253 public void updateAppWidgetSize(Bundle newOptions, int minWidth, int minHeight, int maxWidth, 254 int maxHeight) { 255 updateAppWidgetSize(newOptions, minWidth, minHeight, maxWidth, maxHeight, false); 256 } 257 258 /** 259 * @hide 260 */ 261 public void updateAppWidgetSize(Bundle newOptions, int minWidth, int minHeight, int maxWidth, 262 int maxHeight, boolean ignorePadding) { 263 if (newOptions == null) { 264 newOptions = new Bundle(); 265 } 266 267 Rect padding = new Rect(); 268 if (mInfo != null) { 269 padding = getDefaultPaddingForWidget(mContext, mInfo.provider, padding); 270 } 271 float density = getResources().getDisplayMetrics().density; 272 273 int xPaddingDips = (int) ((padding.left + padding.right) / density); 274 int yPaddingDips = (int) ((padding.top + padding.bottom) / density); 275 276 int newMinWidth = minWidth - (ignorePadding ? 0 : xPaddingDips); 277 int newMinHeight = minHeight - (ignorePadding ? 0 : yPaddingDips); 278 int newMaxWidth = maxWidth - (ignorePadding ? 0 : xPaddingDips); 279 int newMaxHeight = maxHeight - (ignorePadding ? 0 : yPaddingDips); 280 281 AppWidgetManager widgetManager = AppWidgetManager.getInstance(mContext); 282 283 // We get the old options to see if the sizes have changed 284 Bundle oldOptions = widgetManager.getAppWidgetOptions(mAppWidgetId); 285 boolean needsUpdate = false; 286 if (newMinWidth != oldOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH) || 287 newMinHeight != oldOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT) || 288 newMaxWidth != oldOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH) || 289 newMaxHeight != oldOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT)) { 290 needsUpdate = true; 291 } 292 293 if (needsUpdate) { 294 newOptions.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, newMinWidth); 295 newOptions.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, newMinHeight); 296 newOptions.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, newMaxWidth); 297 newOptions.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, newMaxHeight); 298 updateAppWidgetOptions(newOptions); 299 } 300 } 301 302 /** 303 * Specify some extra information for the widget provider. Causes a callback to the 304 * AppWidgetProvider. 305 * @see AppWidgetProvider#onAppWidgetOptionsChanged(Context, AppWidgetManager, int, Bundle) 306 * 307 * @param options The bundle of options information. 308 */ 309 public void updateAppWidgetOptions(Bundle options) { 310 AppWidgetManager.getInstance(mContext).updateAppWidgetOptions(mAppWidgetId, options); 311 } 312 313 /** {@inheritDoc} */ 314 @Override 315 public LayoutParams generateLayoutParams(AttributeSet attrs) { 316 // We're being asked to inflate parameters, probably by a LayoutInflater 317 // in a remote Context. To help resolve any remote references, we 318 // inflate through our last mRemoteContext when it exists. 319 final Context context = mRemoteContext != null ? mRemoteContext : mContext; 320 return new FrameLayout.LayoutParams(context, attrs); 321 } 322 323 /** 324 * Update the AppWidgetProviderInfo for this view, and reset it to the 325 * initial layout. 326 */ 327 void resetAppWidget(AppWidgetProviderInfo info) { 328 mInfo = info; 329 mViewMode = VIEW_MODE_NOINIT; 330 updateAppWidget(null); 331 } 332 333 /** 334 * Process a set of {@link RemoteViews} coming in as an update from the 335 * AppWidget provider. Will animate into these new views as needed 336 */ 337 public void updateAppWidget(RemoteViews remoteViews) { 338 339 if (LOGD) Log.d(TAG, "updateAppWidget called mOld=" + mOld); 340 341 boolean recycled = false; 342 View content = null; 343 Exception exception = null; 344 345 // Capture the old view into a bitmap so we can do the crossfade. 346 if (CROSSFADE) { 347 if (mFadeStartTime < 0) { 348 if (mView != null) { 349 final int width = mView.getWidth(); 350 final int height = mView.getHeight(); 351 try { 352 mOld = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 353 } catch (OutOfMemoryError e) { 354 // we just won't do the fade 355 mOld = null; 356 } 357 if (mOld != null) { 358 //mView.drawIntoBitmap(mOld); 359 } 360 } 361 } 362 } 363 364 if (remoteViews == null) { 365 if (mViewMode == VIEW_MODE_DEFAULT) { 366 // We've already done this -- nothing to do. 367 return; 368 } 369 content = getDefaultView(); 370 mLayoutId = -1; 371 mViewMode = VIEW_MODE_DEFAULT; 372 } else { 373 // Prepare a local reference to the remote Context so we're ready to 374 // inflate any requested LayoutParams. 375 mRemoteContext = getRemoteContext(remoteViews); 376 int layoutId = remoteViews.getLayoutId(); 377 378 // If our stale view has been prepared to match active, and the new 379 // layout matches, try recycling it 380 if (content == null && layoutId == mLayoutId) { 381 try { 382 remoteViews.reapply(mContext, mView, mOnClickHandler); 383 content = mView; 384 recycled = true; 385 if (LOGD) Log.d(TAG, "was able to recycled existing layout"); 386 } catch (RuntimeException e) { 387 exception = e; 388 } 389 } 390 391 // Try normal RemoteView inflation 392 if (content == null) { 393 try { 394 content = remoteViews.apply(mContext, this, mOnClickHandler); 395 if (LOGD) Log.d(TAG, "had to inflate new layout"); 396 } catch (RuntimeException e) { 397 exception = e; 398 } 399 } 400 401 mLayoutId = layoutId; 402 mViewMode = VIEW_MODE_CONTENT; 403 } 404 405 if (content == null) { 406 if (mViewMode == VIEW_MODE_ERROR) { 407 // We've already done this -- nothing to do. 408 return ; 409 } 410 Log.w(TAG, "updateAppWidget couldn't find any view, using error view", exception); 411 content = getErrorView(); 412 mViewMode = VIEW_MODE_ERROR; 413 } 414 415 if (!recycled) { 416 prepareView(content); 417 addView(content); 418 } 419 420 if (mView != content) { 421 removeView(mView); 422 mView = content; 423 } 424 425 if (CROSSFADE) { 426 if (mFadeStartTime < 0) { 427 // if there is already an animation in progress, don't do anything -- 428 // the new view will pop in on top of the old one during the cross fade, 429 // and that looks okay. 430 mFadeStartTime = SystemClock.uptimeMillis(); 431 invalidate(); 432 } 433 } 434 } 435 436 /** 437 * Process data-changed notifications for the specified view in the specified 438 * set of {@link RemoteViews} views. 439 */ 440 void viewDataChanged(int viewId) { 441 View v = findViewById(viewId); 442 if ((v != null) && (v instanceof AdapterView<?>)) { 443 AdapterView<?> adapterView = (AdapterView<?>) v; 444 Adapter adapter = adapterView.getAdapter(); 445 if (adapter instanceof BaseAdapter) { 446 BaseAdapter baseAdapter = (BaseAdapter) adapter; 447 baseAdapter.notifyDataSetChanged(); 448 } else if (adapter == null && adapterView instanceof RemoteAdapterConnectionCallback) { 449 // If the adapter is null, it may mean that the RemoteViewsAapter has not yet 450 // connected to its associated service, and hence the adapter hasn't been set. 451 // In this case, we need to defer the notify call until it has been set. 452 ((RemoteAdapterConnectionCallback) adapterView).deferNotifyDataSetChanged(); 453 } 454 } 455 } 456 457 /** 458 * Build a {@link Context} cloned into another package name, usually for the 459 * purposes of reading remote resources. 460 */ 461 private Context getRemoteContext(RemoteViews views) { 462 // Bail if missing package name 463 final String packageName = views.getPackage(); 464 if (packageName == null) return mContext; 465 466 try { 467 // Return if cloned successfully, otherwise default 468 return mContext.createPackageContext(packageName, Context.CONTEXT_RESTRICTED); 469 } catch (NameNotFoundException e) { 470 Log.e(TAG, "Package name " + packageName + " not found"); 471 return mContext; 472 } 473 } 474 475 @Override 476 protected boolean drawChild(Canvas canvas, View child, long drawingTime) { 477 if (CROSSFADE) { 478 int alpha; 479 int l = child.getLeft(); 480 int t = child.getTop(); 481 if (mFadeStartTime > 0) { 482 alpha = (int)(((drawingTime-mFadeStartTime)*255)/FADE_DURATION); 483 if (alpha > 255) { 484 alpha = 255; 485 } 486 Log.d(TAG, "drawChild alpha=" + alpha + " l=" + l + " t=" + t 487 + " w=" + child.getWidth()); 488 if (alpha != 255 && mOld != null) { 489 mOldPaint.setAlpha(255-alpha); 490 //canvas.drawBitmap(mOld, l, t, mOldPaint); 491 } 492 } else { 493 alpha = 255; 494 } 495 int restoreTo = canvas.saveLayerAlpha(l, t, child.getWidth(), child.getHeight(), alpha, 496 Canvas.HAS_ALPHA_LAYER_SAVE_FLAG | Canvas.CLIP_TO_LAYER_SAVE_FLAG); 497 boolean rv = super.drawChild(canvas, child, drawingTime); 498 canvas.restoreToCount(restoreTo); 499 if (alpha < 255) { 500 invalidate(); 501 } else { 502 mFadeStartTime = -1; 503 if (mOld != null) { 504 mOld.recycle(); 505 mOld = null; 506 } 507 } 508 return rv; 509 } else { 510 return super.drawChild(canvas, child, drawingTime); 511 } 512 } 513 514 /** 515 * Prepare the given view to be shown. This might include adjusting 516 * {@link FrameLayout.LayoutParams} before inserting. 517 */ 518 protected void prepareView(View view) { 519 // Take requested dimensions from child, but apply default gravity. 520 FrameLayout.LayoutParams requested = (FrameLayout.LayoutParams)view.getLayoutParams(); 521 if (requested == null) { 522 requested = new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, 523 LayoutParams.MATCH_PARENT); 524 } 525 526 requested.gravity = Gravity.CENTER; 527 view.setLayoutParams(requested); 528 } 529 530 /** 531 * Inflate and return the default layout requested by AppWidget provider. 532 */ 533 protected View getDefaultView() { 534 if (LOGD) { 535 Log.d(TAG, "getDefaultView"); 536 } 537 View defaultView = null; 538 Exception exception = null; 539 540 try { 541 if (mInfo != null) { 542 Context theirContext = mContext.createPackageContext( 543 mInfo.provider.getPackageName(), Context.CONTEXT_RESTRICTED); 544 mRemoteContext = theirContext; 545 LayoutInflater inflater = (LayoutInflater) 546 theirContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 547 inflater = inflater.cloneInContext(theirContext); 548 inflater.setFilter(sInflaterFilter); 549 AppWidgetManager manager = AppWidgetManager.getInstance(mContext); 550 Bundle options = manager.getAppWidgetOptions(mAppWidgetId); 551 552 int layoutId = mInfo.initialLayout; 553 if (options.containsKey(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY)) { 554 int category = options.getInt(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY); 555 if (category == AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD) { 556 int kgLayoutId = mInfo.initialKeyguardLayout; 557 // If a default keyguard layout is not specified, use the standard 558 // default layout. 559 layoutId = kgLayoutId == 0 ? layoutId : kgLayoutId; 560 } 561 } 562 defaultView = inflater.inflate(layoutId, this, false); 563 } else { 564 Log.w(TAG, "can't inflate defaultView because mInfo is missing"); 565 } 566 } catch (PackageManager.NameNotFoundException e) { 567 exception = e; 568 } catch (RuntimeException e) { 569 exception = e; 570 } 571 572 if (exception != null) { 573 Log.w(TAG, "Error inflating AppWidget " + mInfo + ": " + exception.toString()); 574 } 575 576 if (defaultView == null) { 577 if (LOGD) Log.d(TAG, "getDefaultView couldn't find any view, so inflating error"); 578 defaultView = getErrorView(); 579 } 580 581 return defaultView; 582 } 583 584 /** 585 * Inflate and return a view that represents an error state. 586 */ 587 protected View getErrorView() { 588 TextView tv = new TextView(mContext); 589 tv.setText(com.android.internal.R.string.gadget_host_error_inflating); 590 // TODO: get this color from somewhere. 591 tv.setBackgroundColor(Color.argb(127, 0, 0, 0)); 592 return tv; 593 } 594 595 @Override 596 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 597 super.onInitializeAccessibilityNodeInfo(info); 598 info.setClassName(AppWidgetHostView.class.getName()); 599 } 600 601 private static class ParcelableSparseArray extends SparseArray<Parcelable> implements Parcelable { 602 public int describeContents() { 603 return 0; 604 } 605 606 public void writeToParcel(Parcel dest, int flags) { 607 final int count = size(); 608 dest.writeInt(count); 609 for (int i = 0; i < count; i++) { 610 dest.writeInt(keyAt(i)); 611 dest.writeParcelable(valueAt(i), 0); 612 } 613 } 614 615 public static final Parcelable.Creator<ParcelableSparseArray> CREATOR = 616 new Parcelable.Creator<ParcelableSparseArray>() { 617 public ParcelableSparseArray createFromParcel(Parcel source) { 618 final ParcelableSparseArray array = new ParcelableSparseArray(); 619 final ClassLoader loader = array.getClass().getClassLoader(); 620 final int count = source.readInt(); 621 for (int i = 0; i < count; i++) { 622 array.put(source.readInt(), source.readParcelable(loader)); 623 } 624 return array; 625 } 626 627 public ParcelableSparseArray[] newArray(int size) { 628 return new ParcelableSparseArray[size]; 629 } 630 }; 631 } 632} 633