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