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