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