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