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