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