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