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