AppWidgetHostView.java revision d2097ebacf3a3658624cad10669a4d98e8d7d846
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. This information
212     * gets embedded into the AppWidgetExtras and causes a callback to the AppWidgetProvider.
213     * @see AppWidgetProvider#onAppWidgetOptionsChanged(Context, AppWidgetManager, int, Bundle)
214     *
215     * @param options The bundle of options, in addition to the size information,
216     *          can be null.
217     * @param minWidth The minimum width that the widget will be displayed at.
218     * @param minHeight The maximum height that the widget will be displayed at.
219     * @param maxWidth The maximum width that the widget will be displayed at.
220     * @param maxHeight The maximum height that the widget will be displayed at.
221     *
222     */
223    public void updateAppWidgetSize(Bundle options, int minWidth, int minHeight, int maxWidth,
224            int maxHeight) {
225        if (options == null) {
226            options = new Bundle();
227        }
228        options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, minWidth);
229        options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, minHeight);
230        options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, maxWidth);
231        options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, maxHeight);
232        updateAppWidgetOptions(options);
233    }
234
235    /**
236     * Specify some extra information for the widget provider. Causes a callback to the
237     * AppWidgetProvider.
238     * @see AppWidgetProvider#onAppWidgetOptionsChanged(Context, AppWidgetManager, int, Bundle)
239     *
240     * @param options The bundle of options information.
241     */
242    public void updateAppWidgetOptions(Bundle options) {
243        AppWidgetManager.getInstance(mContext).updateAppWidgetOptions(mAppWidgetId, options);
244    }
245
246    /** {@inheritDoc} */
247    @Override
248    public LayoutParams generateLayoutParams(AttributeSet attrs) {
249        // We're being asked to inflate parameters, probably by a LayoutInflater
250        // in a remote Context. To help resolve any remote references, we
251        // inflate through our last mRemoteContext when it exists.
252        final Context context = mRemoteContext != null ? mRemoteContext : mContext;
253        return new FrameLayout.LayoutParams(context, attrs);
254    }
255
256    /**
257     * Update the AppWidgetProviderInfo for this view, and reset it to the
258     * initial layout.
259     */
260    void resetAppWidget(AppWidgetProviderInfo info) {
261        mInfo = info;
262        mViewMode = VIEW_MODE_NOINIT;
263        updateAppWidget(null);
264    }
265
266    /**
267     * Process a set of {@link RemoteViews} coming in as an update from the
268     * AppWidget provider. Will animate into these new views as needed
269     */
270    public void updateAppWidget(RemoteViews remoteViews) {
271        if (LOGD) Log.d(TAG, "updateAppWidget called mOld=" + mOld);
272
273        boolean recycled = false;
274        View content = null;
275        Exception exception = null;
276
277        // Capture the old view into a bitmap so we can do the crossfade.
278        if (CROSSFADE) {
279            if (mFadeStartTime < 0) {
280                if (mView != null) {
281                    final int width = mView.getWidth();
282                    final int height = mView.getHeight();
283                    try {
284                        mOld = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
285                    } catch (OutOfMemoryError e) {
286                        // we just won't do the fade
287                        mOld = null;
288                    }
289                    if (mOld != null) {
290                        //mView.drawIntoBitmap(mOld);
291                    }
292                }
293            }
294        }
295
296        if (remoteViews == null) {
297            if (mViewMode == VIEW_MODE_DEFAULT) {
298                // We've already done this -- nothing to do.
299                return;
300            }
301            content = getDefaultView();
302            mLayoutId = -1;
303            mViewMode = VIEW_MODE_DEFAULT;
304        } else {
305            // Prepare a local reference to the remote Context so we're ready to
306            // inflate any requested LayoutParams.
307            mRemoteContext = getRemoteContext(remoteViews);
308            int layoutId = remoteViews.getLayoutId();
309
310            // If our stale view has been prepared to match active, and the new
311            // layout matches, try recycling it
312            if (content == null && layoutId == mLayoutId) {
313                try {
314                    remoteViews.reapply(mContext, mView);
315                    content = mView;
316                    recycled = true;
317                    if (LOGD) Log.d(TAG, "was able to recycled existing layout");
318                } catch (RuntimeException e) {
319                    exception = e;
320                }
321            }
322
323            // Try normal RemoteView inflation
324            if (content == null) {
325                try {
326                    content = remoteViews.apply(mContext, this);
327                    if (LOGD) Log.d(TAG, "had to inflate new layout");
328                } catch (RuntimeException e) {
329                    exception = e;
330                }
331            }
332
333            mLayoutId = layoutId;
334            mViewMode = VIEW_MODE_CONTENT;
335        }
336
337        if (content == null) {
338            if (mViewMode == VIEW_MODE_ERROR) {
339                // We've already done this -- nothing to do.
340                return ;
341            }
342            Log.w(TAG, "updateAppWidget couldn't find any view, using error view", exception);
343            content = getErrorView();
344            mViewMode = VIEW_MODE_ERROR;
345        }
346
347        if (!recycled) {
348            prepareView(content);
349            addView(content);
350        }
351
352        if (mView != content) {
353            removeView(mView);
354            mView = content;
355        }
356
357        if (CROSSFADE) {
358            if (mFadeStartTime < 0) {
359                // if there is already an animation in progress, don't do anything --
360                // the new view will pop in on top of the old one during the cross fade,
361                // and that looks okay.
362                mFadeStartTime = SystemClock.uptimeMillis();
363                invalidate();
364            }
365        }
366    }
367
368    /**
369     * Process data-changed notifications for the specified view in the specified
370     * set of {@link RemoteViews} views.
371     */
372    void viewDataChanged(int viewId) {
373        View v = findViewById(viewId);
374        if ((v != null) && (v instanceof AdapterView<?>)) {
375            AdapterView<?> adapterView = (AdapterView<?>) v;
376            Adapter adapter = adapterView.getAdapter();
377            if (adapter instanceof BaseAdapter) {
378                BaseAdapter baseAdapter = (BaseAdapter) adapter;
379                baseAdapter.notifyDataSetChanged();
380            }  else if (adapter == null && adapterView instanceof RemoteAdapterConnectionCallback) {
381                // If the adapter is null, it may mean that the RemoteViewsAapter has not yet
382                // connected to its associated service, and hence the adapter hasn't been set.
383                // In this case, we need to defer the notify call until it has been set.
384                ((RemoteAdapterConnectionCallback) adapterView).deferNotifyDataSetChanged();
385            }
386        }
387    }
388
389    /**
390     * Build a {@link Context} cloned into another package name, usually for the
391     * purposes of reading remote resources.
392     */
393    private Context getRemoteContext(RemoteViews views) {
394        // Bail if missing package name
395        final String packageName = views.getPackage();
396        if (packageName == null) return mContext;
397
398        try {
399            // Return if cloned successfully, otherwise default
400            return mContext.createPackageContext(packageName, Context.CONTEXT_RESTRICTED);
401        } catch (NameNotFoundException e) {
402            Log.e(TAG, "Package name " + packageName + " not found");
403            return mContext;
404        }
405    }
406
407    @Override
408    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
409        if (CROSSFADE) {
410            int alpha;
411            int l = child.getLeft();
412            int t = child.getTop();
413            if (mFadeStartTime > 0) {
414                alpha = (int)(((drawingTime-mFadeStartTime)*255)/FADE_DURATION);
415                if (alpha > 255) {
416                    alpha = 255;
417                }
418                Log.d(TAG, "drawChild alpha=" + alpha + " l=" + l + " t=" + t
419                        + " w=" + child.getWidth());
420                if (alpha != 255 && mOld != null) {
421                    mOldPaint.setAlpha(255-alpha);
422                    //canvas.drawBitmap(mOld, l, t, mOldPaint);
423                }
424            } else {
425                alpha = 255;
426            }
427            int restoreTo = canvas.saveLayerAlpha(l, t, child.getWidth(), child.getHeight(), alpha,
428                    Canvas.HAS_ALPHA_LAYER_SAVE_FLAG | Canvas.CLIP_TO_LAYER_SAVE_FLAG);
429            boolean rv = super.drawChild(canvas, child, drawingTime);
430            canvas.restoreToCount(restoreTo);
431            if (alpha < 255) {
432                invalidate();
433            } else {
434                mFadeStartTime = -1;
435                if (mOld != null) {
436                    mOld.recycle();
437                    mOld = null;
438                }
439            }
440            return rv;
441        } else {
442            return super.drawChild(canvas, child, drawingTime);
443        }
444    }
445
446    /**
447     * Prepare the given view to be shown. This might include adjusting
448     * {@link FrameLayout.LayoutParams} before inserting.
449     */
450    protected void prepareView(View view) {
451        // Take requested dimensions from child, but apply default gravity.
452        FrameLayout.LayoutParams requested = (FrameLayout.LayoutParams)view.getLayoutParams();
453        if (requested == null) {
454            requested = new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT,
455                    LayoutParams.MATCH_PARENT);
456        }
457
458        requested.gravity = Gravity.CENTER;
459        view.setLayoutParams(requested);
460    }
461
462    /**
463     * Inflate and return the default layout requested by AppWidget provider.
464     */
465    protected View getDefaultView() {
466        if (LOGD) {
467            Log.d(TAG, "getDefaultView");
468        }
469        View defaultView = null;
470        Exception exception = null;
471
472        try {
473            if (mInfo != null) {
474                Context theirContext = mContext.createPackageContext(
475                        mInfo.provider.getPackageName(), Context.CONTEXT_RESTRICTED);
476                mRemoteContext = theirContext;
477                LayoutInflater inflater = (LayoutInflater)
478                        theirContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
479                inflater = inflater.cloneInContext(theirContext);
480                inflater.setFilter(sInflaterFilter);
481                defaultView = inflater.inflate(mInfo.initialLayout, this, false);
482            } else {
483                Log.w(TAG, "can't inflate defaultView because mInfo is missing");
484            }
485        } catch (PackageManager.NameNotFoundException e) {
486            exception = e;
487        } catch (RuntimeException e) {
488            exception = e;
489        }
490
491        if (exception != null) {
492            Log.w(TAG, "Error inflating AppWidget " + mInfo + ": " + exception.toString());
493        }
494
495        if (defaultView == null) {
496            if (LOGD) Log.d(TAG, "getDefaultView couldn't find any view, so inflating error");
497            defaultView = getErrorView();
498        }
499
500        return defaultView;
501    }
502
503    /**
504     * Inflate and return a view that represents an error state.
505     */
506    protected View getErrorView() {
507        TextView tv = new TextView(mContext);
508        tv.setText(com.android.internal.R.string.gadget_host_error_inflating);
509        // TODO: get this color from somewhere.
510        tv.setBackgroundColor(Color.argb(127, 0, 0, 0));
511        return tv;
512    }
513
514    private static class ParcelableSparseArray extends SparseArray<Parcelable> implements Parcelable {
515        public int describeContents() {
516            return 0;
517        }
518
519        public void writeToParcel(Parcel dest, int flags) {
520            final int count = size();
521            dest.writeInt(count);
522            for (int i = 0; i < count; i++) {
523                dest.writeInt(keyAt(i));
524                dest.writeParcelable(valueAt(i), 0);
525            }
526        }
527
528        public static final Parcelable.Creator<ParcelableSparseArray> CREATOR =
529                new Parcelable.Creator<ParcelableSparseArray>() {
530                    public ParcelableSparseArray createFromParcel(Parcel source) {
531                        final ParcelableSparseArray array = new ParcelableSparseArray();
532                        final ClassLoader loader = array.getClass().getClassLoader();
533                        final int count = source.readInt();
534                        for (int i = 0; i < count; i++) {
535                            array.put(source.readInt(), source.readParcelable(loader));
536                        }
537                        return array;
538                    }
539
540                    public ParcelableSparseArray[] newArray(int size) {
541                        return new ParcelableSparseArray[size];
542                    }
543                };
544    }
545}
546