AppWidgetHostView.java revision d2097ebacf3a3658624cad10669a4d98e8d7d846
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. This information 212 * gets embedded into the AppWidgetExtras and causes a callback to the AppWidgetProvider. 213 * @see AppWidgetProvider#onAppWidgetOptionsChanged(Context, AppWidgetManager, int, Bundle) 214 * 215 * @param options The bundle of options, in addition to the size information, 216 * can be null. 217 * @param minWidth The minimum width that the widget will be displayed at. 218 * @param minHeight The maximum height that the widget will be displayed at. 219 * @param maxWidth The maximum width that the widget will be displayed at. 220 * @param maxHeight The maximum height that the widget will be displayed at. 221 * 222 */ 223 public void updateAppWidgetSize(Bundle options, int minWidth, int minHeight, int maxWidth, 224 int maxHeight) { 225 if (options == null) { 226 options = new Bundle(); 227 } 228 options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, minWidth); 229 options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, minHeight); 230 options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, maxWidth); 231 options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, maxHeight); 232 updateAppWidgetOptions(options); 233 } 234 235 /** 236 * Specify some extra information for the widget provider. Causes a callback to the 237 * AppWidgetProvider. 238 * @see AppWidgetProvider#onAppWidgetOptionsChanged(Context, AppWidgetManager, int, Bundle) 239 * 240 * @param options The bundle of options information. 241 */ 242 public void updateAppWidgetOptions(Bundle options) { 243 AppWidgetManager.getInstance(mContext).updateAppWidgetOptions(mAppWidgetId, options); 244 } 245 246 /** {@inheritDoc} */ 247 @Override 248 public LayoutParams generateLayoutParams(AttributeSet attrs) { 249 // We're being asked to inflate parameters, probably by a LayoutInflater 250 // in a remote Context. To help resolve any remote references, we 251 // inflate through our last mRemoteContext when it exists. 252 final Context context = mRemoteContext != null ? mRemoteContext : mContext; 253 return new FrameLayout.LayoutParams(context, attrs); 254 } 255 256 /** 257 * Update the AppWidgetProviderInfo for this view, and reset it to the 258 * initial layout. 259 */ 260 void resetAppWidget(AppWidgetProviderInfo info) { 261 mInfo = info; 262 mViewMode = VIEW_MODE_NOINIT; 263 updateAppWidget(null); 264 } 265 266 /** 267 * Process a set of {@link RemoteViews} coming in as an update from the 268 * AppWidget provider. Will animate into these new views as needed 269 */ 270 public void updateAppWidget(RemoteViews remoteViews) { 271 if (LOGD) Log.d(TAG, "updateAppWidget called mOld=" + mOld); 272 273 boolean recycled = false; 274 View content = null; 275 Exception exception = null; 276 277 // Capture the old view into a bitmap so we can do the crossfade. 278 if (CROSSFADE) { 279 if (mFadeStartTime < 0) { 280 if (mView != null) { 281 final int width = mView.getWidth(); 282 final int height = mView.getHeight(); 283 try { 284 mOld = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 285 } catch (OutOfMemoryError e) { 286 // we just won't do the fade 287 mOld = null; 288 } 289 if (mOld != null) { 290 //mView.drawIntoBitmap(mOld); 291 } 292 } 293 } 294 } 295 296 if (remoteViews == null) { 297 if (mViewMode == VIEW_MODE_DEFAULT) { 298 // We've already done this -- nothing to do. 299 return; 300 } 301 content = getDefaultView(); 302 mLayoutId = -1; 303 mViewMode = VIEW_MODE_DEFAULT; 304 } else { 305 // Prepare a local reference to the remote Context so we're ready to 306 // inflate any requested LayoutParams. 307 mRemoteContext = getRemoteContext(remoteViews); 308 int layoutId = remoteViews.getLayoutId(); 309 310 // If our stale view has been prepared to match active, and the new 311 // layout matches, try recycling it 312 if (content == null && layoutId == mLayoutId) { 313 try { 314 remoteViews.reapply(mContext, mView); 315 content = mView; 316 recycled = true; 317 if (LOGD) Log.d(TAG, "was able to recycled existing layout"); 318 } catch (RuntimeException e) { 319 exception = e; 320 } 321 } 322 323 // Try normal RemoteView inflation 324 if (content == null) { 325 try { 326 content = remoteViews.apply(mContext, this); 327 if (LOGD) Log.d(TAG, "had to inflate new layout"); 328 } catch (RuntimeException e) { 329 exception = e; 330 } 331 } 332 333 mLayoutId = layoutId; 334 mViewMode = VIEW_MODE_CONTENT; 335 } 336 337 if (content == null) { 338 if (mViewMode == VIEW_MODE_ERROR) { 339 // We've already done this -- nothing to do. 340 return ; 341 } 342 Log.w(TAG, "updateAppWidget couldn't find any view, using error view", exception); 343 content = getErrorView(); 344 mViewMode = VIEW_MODE_ERROR; 345 } 346 347 if (!recycled) { 348 prepareView(content); 349 addView(content); 350 } 351 352 if (mView != content) { 353 removeView(mView); 354 mView = content; 355 } 356 357 if (CROSSFADE) { 358 if (mFadeStartTime < 0) { 359 // if there is already an animation in progress, don't do anything -- 360 // the new view will pop in on top of the old one during the cross fade, 361 // and that looks okay. 362 mFadeStartTime = SystemClock.uptimeMillis(); 363 invalidate(); 364 } 365 } 366 } 367 368 /** 369 * Process data-changed notifications for the specified view in the specified 370 * set of {@link RemoteViews} views. 371 */ 372 void viewDataChanged(int viewId) { 373 View v = findViewById(viewId); 374 if ((v != null) && (v instanceof AdapterView<?>)) { 375 AdapterView<?> adapterView = (AdapterView<?>) v; 376 Adapter adapter = adapterView.getAdapter(); 377 if (adapter instanceof BaseAdapter) { 378 BaseAdapter baseAdapter = (BaseAdapter) adapter; 379 baseAdapter.notifyDataSetChanged(); 380 } else if (adapter == null && adapterView instanceof RemoteAdapterConnectionCallback) { 381 // If the adapter is null, it may mean that the RemoteViewsAapter has not yet 382 // connected to its associated service, and hence the adapter hasn't been set. 383 // In this case, we need to defer the notify call until it has been set. 384 ((RemoteAdapterConnectionCallback) adapterView).deferNotifyDataSetChanged(); 385 } 386 } 387 } 388 389 /** 390 * Build a {@link Context} cloned into another package name, usually for the 391 * purposes of reading remote resources. 392 */ 393 private Context getRemoteContext(RemoteViews views) { 394 // Bail if missing package name 395 final String packageName = views.getPackage(); 396 if (packageName == null) return mContext; 397 398 try { 399 // Return if cloned successfully, otherwise default 400 return mContext.createPackageContext(packageName, Context.CONTEXT_RESTRICTED); 401 } catch (NameNotFoundException e) { 402 Log.e(TAG, "Package name " + packageName + " not found"); 403 return mContext; 404 } 405 } 406 407 @Override 408 protected boolean drawChild(Canvas canvas, View child, long drawingTime) { 409 if (CROSSFADE) { 410 int alpha; 411 int l = child.getLeft(); 412 int t = child.getTop(); 413 if (mFadeStartTime > 0) { 414 alpha = (int)(((drawingTime-mFadeStartTime)*255)/FADE_DURATION); 415 if (alpha > 255) { 416 alpha = 255; 417 } 418 Log.d(TAG, "drawChild alpha=" + alpha + " l=" + l + " t=" + t 419 + " w=" + child.getWidth()); 420 if (alpha != 255 && mOld != null) { 421 mOldPaint.setAlpha(255-alpha); 422 //canvas.drawBitmap(mOld, l, t, mOldPaint); 423 } 424 } else { 425 alpha = 255; 426 } 427 int restoreTo = canvas.saveLayerAlpha(l, t, child.getWidth(), child.getHeight(), alpha, 428 Canvas.HAS_ALPHA_LAYER_SAVE_FLAG | Canvas.CLIP_TO_LAYER_SAVE_FLAG); 429 boolean rv = super.drawChild(canvas, child, drawingTime); 430 canvas.restoreToCount(restoreTo); 431 if (alpha < 255) { 432 invalidate(); 433 } else { 434 mFadeStartTime = -1; 435 if (mOld != null) { 436 mOld.recycle(); 437 mOld = null; 438 } 439 } 440 return rv; 441 } else { 442 return super.drawChild(canvas, child, drawingTime); 443 } 444 } 445 446 /** 447 * Prepare the given view to be shown. This might include adjusting 448 * {@link FrameLayout.LayoutParams} before inserting. 449 */ 450 protected void prepareView(View view) { 451 // Take requested dimensions from child, but apply default gravity. 452 FrameLayout.LayoutParams requested = (FrameLayout.LayoutParams)view.getLayoutParams(); 453 if (requested == null) { 454 requested = new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, 455 LayoutParams.MATCH_PARENT); 456 } 457 458 requested.gravity = Gravity.CENTER; 459 view.setLayoutParams(requested); 460 } 461 462 /** 463 * Inflate and return the default layout requested by AppWidget provider. 464 */ 465 protected View getDefaultView() { 466 if (LOGD) { 467 Log.d(TAG, "getDefaultView"); 468 } 469 View defaultView = null; 470 Exception exception = null; 471 472 try { 473 if (mInfo != null) { 474 Context theirContext = mContext.createPackageContext( 475 mInfo.provider.getPackageName(), Context.CONTEXT_RESTRICTED); 476 mRemoteContext = theirContext; 477 LayoutInflater inflater = (LayoutInflater) 478 theirContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 479 inflater = inflater.cloneInContext(theirContext); 480 inflater.setFilter(sInflaterFilter); 481 defaultView = inflater.inflate(mInfo.initialLayout, this, false); 482 } else { 483 Log.w(TAG, "can't inflate defaultView because mInfo is missing"); 484 } 485 } catch (PackageManager.NameNotFoundException e) { 486 exception = e; 487 } catch (RuntimeException e) { 488 exception = e; 489 } 490 491 if (exception != null) { 492 Log.w(TAG, "Error inflating AppWidget " + mInfo + ": " + exception.toString()); 493 } 494 495 if (defaultView == null) { 496 if (LOGD) Log.d(TAG, "getDefaultView couldn't find any view, so inflating error"); 497 defaultView = getErrorView(); 498 } 499 500 return defaultView; 501 } 502 503 /** 504 * Inflate and return a view that represents an error state. 505 */ 506 protected View getErrorView() { 507 TextView tv = new TextView(mContext); 508 tv.setText(com.android.internal.R.string.gadget_host_error_inflating); 509 // TODO: get this color from somewhere. 510 tv.setBackgroundColor(Color.argb(127, 0, 0, 0)); 511 return tv; 512 } 513 514 private static class ParcelableSparseArray extends SparseArray<Parcelable> implements Parcelable { 515 public int describeContents() { 516 return 0; 517 } 518 519 public void writeToParcel(Parcel dest, int flags) { 520 final int count = size(); 521 dest.writeInt(count); 522 for (int i = 0; i < count; i++) { 523 dest.writeInt(keyAt(i)); 524 dest.writeParcelable(valueAt(i), 0); 525 } 526 } 527 528 public static final Parcelable.Creator<ParcelableSparseArray> CREATOR = 529 new Parcelable.Creator<ParcelableSparseArray>() { 530 public ParcelableSparseArray createFromParcel(Parcel source) { 531 final ParcelableSparseArray array = new ParcelableSparseArray(); 532 final ClassLoader loader = array.getClass().getClassLoader(); 533 final int count = source.readInt(); 534 for (int i = 0; i < count; i++) { 535 array.put(source.readInt(), source.readParcelable(loader)); 536 } 537 return array; 538 } 539 540 public ParcelableSparseArray[] newArray(int size) { 541 return new ParcelableSparseArray[size]; 542 } 543 }; 544 } 545} 546