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.Process;
     35 import android.os.SystemClock;
     36 import android.os.UserHandle;
     37 import android.util.AttributeSet;
     38 import android.util.Log;
     39 import android.util.SparseArray;
     40 import android.view.Gravity;
     41 import android.view.LayoutInflater;
     42 import android.view.View;
     43 import android.view.accessibility.AccessibilityNodeInfo;
     44 import android.widget.Adapter;
     45 import android.widget.AdapterView;
     46 import android.widget.BaseAdapter;
     47 import android.widget.FrameLayout;
     48 import android.widget.RemoteViews;
     49 import android.widget.RemoteViews.OnClickHandler;
     50 import android.widget.RemoteViewsAdapter.RemoteAdapterConnectionCallback;
     51 import 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  */
     58 public 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