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