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