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