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