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