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