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