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