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