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