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 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 protected boolean drawChild(Canvas canvas, View child, long drawingTime) { 280 if (CROSSFADE) { 281 int alpha; 282 int l = child.getLeft(); 283 int t = child.getTop(); 284 if (mFadeStartTime > 0) { 285 alpha = (int)(((drawingTime-mFadeStartTime)*255)/FADE_DURATION); 286 if (alpha > 255) { 287 alpha = 255; 288 } 289 Log.d(TAG, "drawChild alpha=" + alpha + " l=" + l + " t=" + t 290 + " w=" + child.getWidth()); 291 if (alpha != 255 && mOld != null) { 292 mOldPaint.setAlpha(255-alpha); 293 //canvas.drawBitmap(mOld, l, t, mOldPaint); 294 } 295 } else { 296 alpha = 255; 297 } 298 int restoreTo = canvas.saveLayerAlpha(l, t, child.getWidth(), child.getHeight(), alpha, 299 Canvas.HAS_ALPHA_LAYER_SAVE_FLAG | Canvas.CLIP_TO_LAYER_SAVE_FLAG); 300 boolean rv = super.drawChild(canvas, child, drawingTime); 301 canvas.restoreToCount(restoreTo); 302 if (alpha < 255) { 303 invalidate(); 304 } else { 305 mFadeStartTime = -1; 306 if (mOld != null) { 307 mOld.recycle(); 308 mOld = null; 309 } 310 } 311 return rv; 312 } else { 313 return super.drawChild(canvas, child, drawingTime); 314 } 315 } 316 317 /** 318 * Prepare the given view to be shown. This might include adjusting 319 * {@link FrameLayout.LayoutParams} before inserting. 320 */ 321 protected void prepareView(View view) { 322 // Take requested dimensions from child, but apply default gravity. 323 FrameLayout.LayoutParams requested = (FrameLayout.LayoutParams)view.getLayoutParams(); 324 if (requested == null) { 325 requested = new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, 326 LayoutParams.MATCH_PARENT); 327 } 328 329 requested.gravity = Gravity.CENTER; 330 view.setLayoutParams(requested); 331 } 332 333 /** 334 * Inflate and return the default layout requested by AppWidget provider. 335 */ 336 protected View getDefaultView() { 337 if (LOGD) { 338 Log.d(TAG, "getDefaultView"); 339 } 340 View defaultView = null; 341 Exception exception = null; 342 343 try { 344 if (mInfo != null) { 345 Context theirContext = mContext.createPackageContext( 346 mInfo.provider.getPackageName(), Context.CONTEXT_RESTRICTED); 347 mRemoteContext = theirContext; 348 LayoutInflater inflater = (LayoutInflater) 349 theirContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 350 inflater = inflater.cloneInContext(theirContext); 351 inflater.setFilter(sInflaterFilter); 352 defaultView = inflater.inflate(mInfo.initialLayout, this, false); 353 } else { 354 Log.w(TAG, "can't inflate defaultView because mInfo is missing"); 355 } 356 } catch (PackageManager.NameNotFoundException e) { 357 exception = e; 358 } catch (RuntimeException e) { 359 exception = e; 360 } 361 362 if (exception != null) { 363 Log.w(TAG, "Error inflating AppWidget " + mInfo + ": " + exception.toString()); 364 } 365 366 if (defaultView == null) { 367 if (LOGD) Log.d(TAG, "getDefaultView couldn't find any view, so inflating error"); 368 defaultView = getErrorView(); 369 } 370 371 return defaultView; 372 } 373 374 /** 375 * Inflate and return a view that represents an error state. 376 */ 377 protected View getErrorView() { 378 TextView tv = new TextView(mContext); 379 tv.setText(com.android.internal.R.string.gadget_host_error_inflating); 380 // TODO: get this color from somewhere. 381 tv.setBackgroundColor(Color.argb(127, 0, 0, 0)); 382 return tv; 383 } 384 385 private static class ParcelableSparseArray extends SparseArray<Parcelable> implements Parcelable { 386 public int describeContents() { 387 return 0; 388 } 389 390 public void writeToParcel(Parcel dest, int flags) { 391 final int count = size(); 392 dest.writeInt(count); 393 for (int i = 0; i < count; i++) { 394 dest.writeInt(keyAt(i)); 395 dest.writeParcelable(valueAt(i), 0); 396 } 397 } 398 399 public static final Parcelable.Creator<ParcelableSparseArray> CREATOR = 400 new Parcelable.Creator<ParcelableSparseArray>() { 401 public ParcelableSparseArray createFromParcel(Parcel source) { 402 final ParcelableSparseArray array = new ParcelableSparseArray(); 403 final ClassLoader loader = array.getClass().getClassLoader(); 404 final int count = source.readInt(); 405 for (int i = 0; i < count; i++) { 406 array.put(source.readInt(), source.readParcelable(loader)); 407 } 408 return array; 409 } 410 411 public ParcelableSparseArray[] newArray(int size) { 412 return new ParcelableSparseArray[size]; 413 } 414 }; 415 } 416 } 417