• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package android.appwidget;
18 
19 import android.content.ComponentName;
20 import android.content.Context;
21 import android.content.pm.ApplicationInfo;
22 import android.content.pm.PackageManager;
23 import android.content.pm.PackageManager.NameNotFoundException;
24 import android.content.res.Resources;
25 import android.graphics.Bitmap;
26 import android.graphics.Canvas;
27 import android.graphics.Color;
28 import android.graphics.Paint;
29 import android.graphics.Rect;
30 import android.os.Build;
31 import android.os.Bundle;
32 import android.os.Parcel;
33 import android.os.Parcelable;
34 import android.os.SystemClock;
35 import android.util.AttributeSet;
36 import android.util.Log;
37 import android.util.SparseArray;
38 import android.view.Gravity;
39 import android.view.LayoutInflater;
40 import android.view.View;
41 import android.view.accessibility.AccessibilityNodeInfo;
42 import android.widget.Adapter;
43 import android.widget.AdapterView;
44 import android.widget.BaseAdapter;
45 import android.widget.FrameLayout;
46 import android.widget.RemoteViews;
47 import android.widget.RemoteViewsAdapter.RemoteAdapterConnectionCallback;
48 import 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  */
55 public 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      */
AppWidgetHostView(Context context)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"})
AppWidgetHostView(Context context, int animationIn, int animationOut)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      */
setAppWidget(int appWidgetId, AppWidgetProviderInfo info)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      */
getDefaultPaddingForWidget(Context context, ComponentName component, Rect padding)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 
getAppWidgetId()177     public int getAppWidgetId() {
178         return mAppWidgetId;
179     }
180 
getAppWidgetInfo()181     public AppWidgetProviderInfo getAppWidgetInfo() {
182         return mInfo;
183     }
184 
185     @Override
dispatchSaveInstanceState(SparseArray<Parcelable> container)186     protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
187         final ParcelableSparseArray jail = new ParcelableSparseArray();
188         super.dispatchSaveInstanceState(jail);
189         container.put(generateId(), jail);
190     }
191 
generateId()192     private int generateId() {
193         final int id = getId();
194         return id == View.NO_ID ? mAppWidgetId : id;
195     }
196 
197     @Override
dispatchRestoreInstanceState(SparseArray<Parcelable> container)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      */
updateAppWidgetSize(Bundle options, int minWidth, int minHeight, int maxWidth, int maxHeight)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      */
updateAppWidgetOptions(Bundle options)255     public void updateAppWidgetOptions(Bundle options) {
256         AppWidgetManager.getInstance(mContext).updateAppWidgetOptions(mAppWidgetId, options);
257     }
258 
259     /** {@inheritDoc} */
260     @Override
generateLayoutParams(AttributeSet attrs)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      */
resetAppWidget(AppWidgetProviderInfo info)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      */
updateAppWidget(RemoteViews remoteViews)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      */
viewDataChanged(int viewId)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      */
getRemoteContext(RemoteViews views)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
drawChild(Canvas canvas, View child, long drawingTime)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      */
prepareView(View view)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      */
getDefaultView()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      */
getErrorView()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
onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)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 {
describeContents()534         public int describeContents() {
535             return 0;
536         }
537 
writeToParcel(Parcel dest, int flags)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