AppWidgetHostView.java revision 6394c0e52cf641d93f678fd052499aa952e3595d
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.Adapter;
36import android.widget.AdapterView;
37import android.widget.BaseAdapter;
38import android.widget.FrameLayout;
39import android.widget.RemoteViews;
40import android.widget.TextView;
41
42/**
43 * Provides the glue to show AppWidget views. This class offers automatic animation
44 * between updates, and will try recycling old views for each incoming
45 * {@link RemoteViews}.
46 */
47public class AppWidgetHostView extends FrameLayout {
48    static final String TAG = "AppWidgetHostView";
49    static final boolean LOGD = false;
50    static final boolean CROSSFADE = false;
51
52    static final int VIEW_MODE_NOINIT = 0;
53    static final int VIEW_MODE_CONTENT = 1;
54    static final int VIEW_MODE_ERROR = 2;
55    static final int VIEW_MODE_DEFAULT = 3;
56
57    static final int FADE_DURATION = 1000;
58
59    // When we're inflating the initialLayout for a AppWidget, we only allow
60    // views that are allowed in RemoteViews.
61    static final LayoutInflater.Filter sInflaterFilter = new LayoutInflater.Filter() {
62        public boolean onLoadClass(Class clazz) {
63            return clazz.isAnnotationPresent(RemoteViews.RemoteView.class);
64        }
65    };
66
67    Context mContext;
68    Context mRemoteContext;
69
70    int mAppWidgetId;
71    AppWidgetProviderInfo mInfo;
72    View mView;
73    int mViewMode = VIEW_MODE_NOINIT;
74    int mLayoutId = -1;
75    long mFadeStartTime = -1;
76    Bitmap mOld;
77    Paint mOldPaint = new Paint();
78
79    /**
80     * Create a host view.  Uses default fade animations.
81     */
82    public AppWidgetHostView(Context context) {
83        this(context, android.R.anim.fade_in, android.R.anim.fade_out);
84    }
85
86    /**
87     * Create a host view. Uses specified animations when pushing
88     * {@link #updateAppWidget(RemoteViews)}.
89     *
90     * @param animationIn Resource ID of in animation to use
91     * @param animationOut Resource ID of out animation to use
92     */
93    @SuppressWarnings({"UnusedDeclaration"})
94    public AppWidgetHostView(Context context, int animationIn, int animationOut) {
95        super(context);
96        mContext = context;
97    }
98
99    /**
100     * Set the AppWidget that will be displayed by this view.
101     */
102    public void setAppWidget(int appWidgetId, AppWidgetProviderInfo info) {
103        mAppWidgetId = appWidgetId;
104        mInfo = info;
105    }
106
107    public int getAppWidgetId() {
108        return mAppWidgetId;
109    }
110
111    public AppWidgetProviderInfo getAppWidgetInfo() {
112        return mInfo;
113    }
114
115    @Override
116    protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
117        final ParcelableSparseArray jail = new ParcelableSparseArray();
118        super.dispatchSaveInstanceState(jail);
119        container.put(generateId(), jail);
120    }
121
122    private int generateId() {
123        final int id = getId();
124        return id == View.NO_ID ? mAppWidgetId : id;
125    }
126
127    @Override
128    protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
129        final Parcelable parcelable = container.get(generateId());
130
131        ParcelableSparseArray jail = null;
132        if (parcelable != null && parcelable instanceof ParcelableSparseArray) {
133            jail = (ParcelableSparseArray) parcelable;
134        }
135
136        if (jail == null) jail = new ParcelableSparseArray();
137
138        super.dispatchRestoreInstanceState(jail);
139    }
140
141    /** {@inheritDoc} */
142    @Override
143    public LayoutParams generateLayoutParams(AttributeSet attrs) {
144        // We're being asked to inflate parameters, probably by a LayoutInflater
145        // in a remote Context. To help resolve any remote references, we
146        // inflate through our last mRemoteContext when it exists.
147        final Context context = mRemoteContext != null ? mRemoteContext : mContext;
148        return new FrameLayout.LayoutParams(context, attrs);
149    }
150
151    /**
152     * Update the AppWidgetProviderInfo for this view, and reset it to the
153     * initial layout.
154     */
155    void resetAppWidget(AppWidgetProviderInfo info) {
156        mInfo = info;
157        mViewMode = VIEW_MODE_NOINIT;
158        updateAppWidget(null);
159    }
160
161    /**
162     * Process a set of {@link RemoteViews} coming in as an update from the
163     * AppWidget provider. Will animate into these new views as needed
164     */
165    public void updateAppWidget(RemoteViews remoteViews) {
166        if (LOGD) Log.d(TAG, "updateAppWidget called mOld=" + mOld);
167
168        boolean recycled = false;
169        View content = null;
170        Exception exception = null;
171
172        // Capture the old view into a bitmap so we can do the crossfade.
173        if (CROSSFADE) {
174            if (mFadeStartTime < 0) {
175                if (mView != null) {
176                    final int width = mView.getWidth();
177                    final int height = mView.getHeight();
178                    try {
179                        mOld = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
180                    } catch (OutOfMemoryError e) {
181                        // we just won't do the fade
182                        mOld = null;
183                    }
184                    if (mOld != null) {
185                        //mView.drawIntoBitmap(mOld);
186                    }
187                }
188            }
189        }
190
191        if (remoteViews == null) {
192            if (mViewMode == VIEW_MODE_DEFAULT) {
193                // We've already done this -- nothing to do.
194                return;
195            }
196            content = getDefaultView();
197            mLayoutId = -1;
198            mViewMode = VIEW_MODE_DEFAULT;
199        } else {
200            // Prepare a local reference to the remote Context so we're ready to
201            // inflate any requested LayoutParams.
202            mRemoteContext = getRemoteContext(remoteViews);
203            int layoutId = remoteViews.getLayoutId();
204
205            // If our stale view has been prepared to match active, and the new
206            // layout matches, try recycling it
207            if (content == null && layoutId == mLayoutId) {
208                try {
209                    remoteViews.reapply(mContext, mView);
210                    content = mView;
211                    recycled = true;
212                    if (LOGD) Log.d(TAG, "was able to recycled existing layout");
213                } catch (RuntimeException e) {
214                    exception = e;
215                }
216            }
217
218            // Try normal RemoteView inflation
219            if (content == null) {
220                try {
221                    content = remoteViews.apply(mContext, this);
222                    if (LOGD) Log.d(TAG, "had to inflate new layout");
223                } catch (RuntimeException e) {
224                    exception = e;
225                }
226            }
227
228            mLayoutId = layoutId;
229            mViewMode = VIEW_MODE_CONTENT;
230        }
231
232        if (content == null) {
233            if (mViewMode == VIEW_MODE_ERROR) {
234                // We've already done this -- nothing to do.
235                return ;
236            }
237            Log.w(TAG, "updateAppWidget couldn't find any view, using error view", exception);
238            content = getErrorView();
239            mViewMode = VIEW_MODE_ERROR;
240        }
241
242        if (!recycled) {
243            prepareView(content);
244            addView(content);
245        }
246
247        if (mView != content) {
248            removeView(mView);
249            mView = content;
250        }
251
252        if (CROSSFADE) {
253            if (mFadeStartTime < 0) {
254                // if there is already an animation in progress, don't do anything --
255                // the new view will pop in on top of the old one during the cross fade,
256                // and that looks okay.
257                mFadeStartTime = SystemClock.uptimeMillis();
258                invalidate();
259            }
260        }
261    }
262
263    /**
264     * Process data-changed notifications for the specified view in the specified
265     * set of {@link RemoteViews} views.
266     */
267    void viewDataChanged(int viewId) {
268        View v = findViewById(viewId);
269        if ((v != null) && (v instanceof AdapterView<?>)) {
270            AdapterView<?> adapterView = (AdapterView<?>) v;
271            Adapter adapter = adapterView.getAdapter();
272            if (adapter instanceof BaseAdapter) {
273                BaseAdapter baseAdapter = (BaseAdapter) adapter;
274                baseAdapter.notifyDataSetChanged();
275            }
276        }
277    }
278
279    /**
280     * Build a {@link Context} cloned into another package name, usually for the
281     * purposes of reading remote resources.
282     */
283    private Context getRemoteContext(RemoteViews views) {
284        // Bail if missing package name
285        final String packageName = views.getPackage();
286        if (packageName == null) return mContext;
287
288        try {
289            // Return if cloned successfully, otherwise default
290            return mContext.createPackageContext(packageName, Context.CONTEXT_RESTRICTED);
291        } catch (NameNotFoundException e) {
292            Log.e(TAG, "Package name " + packageName + " not found");
293            return mContext;
294        }
295    }
296
297    @Override
298    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
299        if (CROSSFADE) {
300            int alpha;
301            int l = child.getLeft();
302            int t = child.getTop();
303            if (mFadeStartTime > 0) {
304                alpha = (int)(((drawingTime-mFadeStartTime)*255)/FADE_DURATION);
305                if (alpha > 255) {
306                    alpha = 255;
307                }
308                Log.d(TAG, "drawChild alpha=" + alpha + " l=" + l + " t=" + t
309                        + " w=" + child.getWidth());
310                if (alpha != 255 && mOld != null) {
311                    mOldPaint.setAlpha(255-alpha);
312                    //canvas.drawBitmap(mOld, l, t, mOldPaint);
313                }
314            } else {
315                alpha = 255;
316            }
317            int restoreTo = canvas.saveLayerAlpha(l, t, child.getWidth(), child.getHeight(), alpha,
318                    Canvas.HAS_ALPHA_LAYER_SAVE_FLAG | Canvas.CLIP_TO_LAYER_SAVE_FLAG);
319            boolean rv = super.drawChild(canvas, child, drawingTime);
320            canvas.restoreToCount(restoreTo);
321            if (alpha < 255) {
322                invalidate();
323            } else {
324                mFadeStartTime = -1;
325                if (mOld != null) {
326                    mOld.recycle();
327                    mOld = null;
328                }
329            }
330            return rv;
331        } else {
332            return super.drawChild(canvas, child, drawingTime);
333        }
334    }
335
336    /**
337     * Prepare the given view to be shown. This might include adjusting
338     * {@link FrameLayout.LayoutParams} before inserting.
339     */
340    protected void prepareView(View view) {
341        // Take requested dimensions from child, but apply default gravity.
342        FrameLayout.LayoutParams requested = (FrameLayout.LayoutParams)view.getLayoutParams();
343        if (requested == null) {
344            requested = new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT,
345                    LayoutParams.MATCH_PARENT);
346        }
347
348        requested.gravity = Gravity.CENTER;
349        view.setLayoutParams(requested);
350    }
351
352    /**
353     * Inflate and return the default layout requested by AppWidget provider.
354     */
355    protected View getDefaultView() {
356        if (LOGD) {
357            Log.d(TAG, "getDefaultView");
358        }
359        View defaultView = null;
360        Exception exception = null;
361
362        try {
363            if (mInfo != null) {
364                Context theirContext = mContext.createPackageContext(
365                        mInfo.provider.getPackageName(), Context.CONTEXT_RESTRICTED);
366                mRemoteContext = theirContext;
367                LayoutInflater inflater = (LayoutInflater)
368                        theirContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
369                inflater = inflater.cloneInContext(theirContext);
370                inflater.setFilter(sInflaterFilter);
371                defaultView = inflater.inflate(mInfo.initialLayout, this, false);
372            } else {
373                Log.w(TAG, "can't inflate defaultView because mInfo is missing");
374            }
375        } catch (PackageManager.NameNotFoundException e) {
376            exception = e;
377        } catch (RuntimeException e) {
378            exception = e;
379        }
380
381        if (exception != null) {
382            Log.w(TAG, "Error inflating AppWidget " + mInfo + ": " + exception.toString());
383        }
384
385        if (defaultView == null) {
386            if (LOGD) Log.d(TAG, "getDefaultView couldn't find any view, so inflating error");
387            defaultView = getErrorView();
388        }
389
390        return defaultView;
391    }
392
393    /**
394     * Inflate and return a view that represents an error state.
395     */
396    protected View getErrorView() {
397        TextView tv = new TextView(mContext);
398        tv.setText(com.android.internal.R.string.gadget_host_error_inflating);
399        // TODO: get this color from somewhere.
400        tv.setBackgroundColor(Color.argb(127, 0, 0, 0));
401        return tv;
402    }
403
404    private static class ParcelableSparseArray extends SparseArray<Parcelable> implements Parcelable {
405        public int describeContents() {
406            return 0;
407        }
408
409        public void writeToParcel(Parcel dest, int flags) {
410            final int count = size();
411            dest.writeInt(count);
412            for (int i = 0; i < count; i++) {
413                dest.writeInt(keyAt(i));
414                dest.writeParcelable(valueAt(i), 0);
415            }
416        }
417
418        public static final Parcelable.Creator<ParcelableSparseArray> CREATOR =
419                new Parcelable.Creator<ParcelableSparseArray>() {
420                    public ParcelableSparseArray createFromParcel(Parcel source) {
421                        final ParcelableSparseArray array = new ParcelableSparseArray();
422                        final ClassLoader loader = array.getClass().getClassLoader();
423                        final int count = source.readInt();
424                        for (int i = 0; i < count; i++) {
425                            array.put(source.readInt(), source.readParcelable(loader));
426                        }
427                        return array;
428                    }
429
430                    public ParcelableSparseArray[] newArray(int size) {
431                        return new ParcelableSparseArray[size];
432                    }
433                };
434    }
435}
436