• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      */
AppWidgetHostView(Context context)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"})
AppWidgetHostView(Context context, int animationIn, int animationOut)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      */
setAppWidget(int appWidgetId, AppWidgetProviderInfo info)99     public void setAppWidget(int appWidgetId, AppWidgetProviderInfo info) {
100         mAppWidgetId = appWidgetId;
101         mInfo = info;
102     }
103 
getAppWidgetId()104     public int getAppWidgetId() {
105         return mAppWidgetId;
106     }
107 
getAppWidgetInfo()108     public AppWidgetProviderInfo getAppWidgetInfo() {
109         return mInfo;
110     }
111 
112     @Override
dispatchSaveInstanceState(SparseArray<Parcelable> container)113     protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
114         final ParcelableSparseArray jail = new ParcelableSparseArray();
115         super.dispatchSaveInstanceState(jail);
116         container.put(generateId(), jail);
117     }
118 
generateId()119     private int generateId() {
120         final int id = getId();
121         return id == View.NO_ID ? mAppWidgetId : id;
122     }
123 
124     @Override
dispatchRestoreInstanceState(SparseArray<Parcelable> container)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
generateLayoutParams(AttributeSet attrs)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      */
resetAppWidget(AppWidgetProviderInfo info)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      */
updateAppWidget(RemoteViews remoteViews)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      */
getRemoteContext(RemoteViews views)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 
drawChild(Canvas canvas, View child, long drawingTime)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      */
prepareView(View view)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      */
getDefaultView()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      */
getErrorView()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 {
describeContents()385         public int describeContents() {
386             return 0;
387         }
388 
writeToParcel(Parcel dest, int flags)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