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