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