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