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.Context;
     20 import android.content.pm.PackageManager;
     21 import android.content.pm.PackageManager.NameNotFoundException;
     22 import android.graphics.Bitmap;
     23 import android.graphics.Canvas;
     24 import android.graphics.Color;
     25 import android.graphics.Paint;
     26 import android.os.SystemClock;
     27 import android.os.Parcelable;
     28 import android.os.Parcel;
     29 import android.util.AttributeSet;
     30 import android.util.Log;
     31 import android.util.SparseArray;
     32 import android.view.Gravity;
     33 import android.view.LayoutInflater;
     34 import android.view.View;
     35 import android.widget.FrameLayout;
     36 import android.widget.RemoteViews;
     37 import android.widget.TextView;
     38 
     39 /**
     40  * Provides the glue to show AppWidget views. This class offers automatic animation
     41  * between updates, and will try recycling old views for each incoming
     42  * {@link RemoteViews}.
     43  */
     44 public class AppWidgetHostView extends FrameLayout {
     45     static final String TAG = "AppWidgetHostView";
     46     static final boolean LOGD = false;
     47     static final boolean CROSSFADE = false;
     48 
     49     static final int VIEW_MODE_NOINIT = 0;
     50     static final int VIEW_MODE_CONTENT = 1;
     51     static final int VIEW_MODE_ERROR = 2;
     52     static final int VIEW_MODE_DEFAULT = 3;
     53 
     54     static final int FADE_DURATION = 1000;
     55 
     56     // When we're inflating the initialLayout for a AppWidget, we only allow
     57     // views that are allowed in RemoteViews.
     58     static final LayoutInflater.Filter sInflaterFilter = new LayoutInflater.Filter() {
     59         public boolean onLoadClass(Class clazz) {
     60             return clazz.isAnnotationPresent(RemoteViews.RemoteView.class);
     61         }
     62     };
     63 
     64     Context mContext;
     65     Context mRemoteContext;
     66 
     67     int mAppWidgetId;
     68     AppWidgetProviderInfo mInfo;
     69     View mView;
     70     int mViewMode = VIEW_MODE_NOINIT;
     71     int mLayoutId = -1;
     72     long mFadeStartTime = -1;
     73     Bitmap mOld;
     74     Paint mOldPaint = new Paint();
     75 
     76     /**
     77      * Create a host view.  Uses default fade animations.
     78      */
     79     public AppWidgetHostView(Context context) {
     80         this(context, android.R.anim.fade_in, android.R.anim.fade_out);
     81     }
     82 
     83     /**
     84      * Create a host view. Uses specified animations when pushing
     85      * {@link #updateAppWidget(RemoteViews)}.
     86      *
     87      * @param animationIn Resource ID of in animation to use
     88      * @param animationOut Resource ID of out animation to use
     89      */
     90     @SuppressWarnings({"UnusedDeclaration"})
     91     public AppWidgetHostView(Context context, int animationIn, int animationOut) {
     92         super(context);
     93         mContext = context;
     94     }
     95 
     96     /**
     97      * Set the AppWidget that will be displayed by this view.
     98      */
     99     public void setAppWidget(int appWidgetId, AppWidgetProviderInfo info) {
    100         mAppWidgetId = appWidgetId;
    101         mInfo = info;
    102     }
    103 
    104     public int getAppWidgetId() {
    105         return mAppWidgetId;
    106     }
    107 
    108     public AppWidgetProviderInfo getAppWidgetInfo() {
    109         return mInfo;
    110     }
    111 
    112     @Override
    113     protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
    114         final ParcelableSparseArray jail = new ParcelableSparseArray();
    115         super.dispatchSaveInstanceState(jail);
    116         container.put(generateId(), jail);
    117     }
    118 
    119     private int generateId() {
    120         final int id = getId();
    121         return id == View.NO_ID ? mAppWidgetId : id;
    122     }
    123 
    124     @Override
    125     protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
    126         final Parcelable parcelable = container.get(generateId());
    127 
    128         ParcelableSparseArray jail = null;
    129         if (parcelable != null && parcelable instanceof ParcelableSparseArray) {
    130             jail = (ParcelableSparseArray) parcelable;
    131         }
    132 
    133         if (jail == null) jail = new ParcelableSparseArray();
    134 
    135         super.dispatchRestoreInstanceState(jail);
    136     }
    137 
    138     /** {@inheritDoc} */
    139     @Override
    140     public LayoutParams generateLayoutParams(AttributeSet attrs) {
    141         // We're being asked to inflate parameters, probably by a LayoutInflater
    142         // in a remote Context. To help resolve any remote references, we
    143         // inflate through our last mRemoteContext when it exists.
    144         final Context context = mRemoteContext != null ? mRemoteContext : mContext;
    145         return new FrameLayout.LayoutParams(context, attrs);
    146     }
    147 
    148     /**
    149      * Update the AppWidgetProviderInfo for this view, and reset it to the
    150      * initial layout.
    151      */
    152     void resetAppWidget(AppWidgetProviderInfo info) {
    153         mInfo = info;
    154         mViewMode = VIEW_MODE_NOINIT;
    155         updateAppWidget(null);
    156     }
    157 
    158     /**
    159      * Process a set of {@link RemoteViews} coming in as an update from the
    160      * AppWidget provider. Will animate into these new views as needed
    161      */
    162     public void updateAppWidget(RemoteViews remoteViews) {
    163         if (LOGD) Log.d(TAG, "updateAppWidget called mOld=" + mOld);
    164 
    165         boolean recycled = false;
    166         View content = null;
    167         Exception exception = null;
    168 
    169         // Capture the old view into a bitmap so we can do the crossfade.
    170         if (CROSSFADE) {
    171             if (mFadeStartTime < 0) {
    172                 if (mView != null) {
    173                     final int width = mView.getWidth();
    174                     final int height = mView.getHeight();
    175                     try {
    176                         mOld = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    177                     } catch (OutOfMemoryError e) {
    178                         // we just won't do the fade
    179                         mOld = null;
    180                     }
    181                     if (mOld != null) {
    182                         //mView.drawIntoBitmap(mOld);
    183                     }
    184                 }
    185             }
    186         }
    187 
    188         if (remoteViews == null) {
    189             if (mViewMode == VIEW_MODE_DEFAULT) {
    190                 // We've already done this -- nothing to do.
    191                 return;
    192             }
    193             content = getDefaultView();
    194             mLayoutId = -1;
    195             mViewMode = VIEW_MODE_DEFAULT;
    196         } else {
    197             // Prepare a local reference to the remote Context so we're ready to
    198             // inflate any requested LayoutParams.
    199             mRemoteContext = getRemoteContext(remoteViews);
    200             int layoutId = remoteViews.getLayoutId();
    201 
    202             // If our stale view has been prepared to match active, and the new
    203             // layout matches, try recycling it
    204             if (content == null && layoutId == mLayoutId) {
    205                 try {
    206                     remoteViews.reapply(mContext, mView);
    207                     content = mView;
    208                     recycled = true;
    209                     if (LOGD) Log.d(TAG, "was able to recycled existing layout");
    210                 } catch (RuntimeException e) {
    211                     exception = e;
    212                 }
    213             }
    214 
    215             // Try normal RemoteView inflation
    216             if (content == null) {
    217                 try {
    218                     content = remoteViews.apply(mContext, this);
    219                     if (LOGD) Log.d(TAG, "had to inflate new layout");
    220                 } catch (RuntimeException e) {
    221                     exception = e;
    222                 }
    223             }
    224 
    225             mLayoutId = layoutId;
    226             mViewMode = VIEW_MODE_CONTENT;
    227         }
    228 
    229         if (content == null) {
    230             if (mViewMode == VIEW_MODE_ERROR) {
    231                 // We've already done this -- nothing to do.
    232                 return ;
    233             }
    234             Log.w(TAG, "updateAppWidget couldn't find any view, using error view", exception);
    235             content = getErrorView();
    236             mViewMode = VIEW_MODE_ERROR;
    237         }
    238 
    239         if (!recycled) {
    240             prepareView(content);
    241             addView(content);
    242         }
    243 
    244         if (mView != content) {
    245             removeView(mView);
    246             mView = content;
    247         }
    248 
    249         if (CROSSFADE) {
    250             if (mFadeStartTime < 0) {
    251                 // if there is already an animation in progress, don't do anything --
    252                 // the new view will pop in on top of the old one during the cross fade,
    253                 // and that looks okay.
    254                 mFadeStartTime = SystemClock.uptimeMillis();
    255                 invalidate();
    256             }
    257         }
    258     }
    259 
    260     /**
    261      * Build a {@link Context} cloned into another package name, usually for the
    262      * purposes of reading remote resources.
    263      */
    264     private Context getRemoteContext(RemoteViews views) {
    265         // Bail if missing package name
    266         final String packageName = views.getPackage();
    267         if (packageName == null) return mContext;
    268 
    269         try {
    270             // Return if cloned successfully, otherwise default
    271             return mContext.createPackageContext(packageName, Context.CONTEXT_RESTRICTED);
    272         } catch (NameNotFoundException e) {
    273             Log.e(TAG, "Package name " + packageName + " not found");
    274             return mContext;
    275         }
    276     }
    277 
    278     protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    279         if (CROSSFADE) {
    280             int alpha;
    281             int l = child.getLeft();
    282             int t = child.getTop();
    283             if (mFadeStartTime > 0) {
    284                 alpha = (int)(((drawingTime-mFadeStartTime)*255)/FADE_DURATION);
    285                 if (alpha > 255) {
    286                     alpha = 255;
    287                 }
    288                 Log.d(TAG, "drawChild alpha=" + alpha + " l=" + l + " t=" + t
    289                         + " w=" + child.getWidth());
    290                 if (alpha != 255 && mOld != null) {
    291                     mOldPaint.setAlpha(255-alpha);
    292                     //canvas.drawBitmap(mOld, l, t, mOldPaint);
    293                 }
    294             } else {
    295                 alpha = 255;
    296             }
    297             int restoreTo = canvas.saveLayerAlpha(l, t, child.getWidth(), child.getHeight(), alpha,
    298                     Canvas.HAS_ALPHA_LAYER_SAVE_FLAG | Canvas.CLIP_TO_LAYER_SAVE_FLAG);
    299             boolean rv = super.drawChild(canvas, child, drawingTime);
    300             canvas.restoreToCount(restoreTo);
    301             if (alpha < 255) {
    302                 invalidate();
    303             } else {
    304                 mFadeStartTime = -1;
    305                 if (mOld != null) {
    306                     mOld.recycle();
    307                     mOld = null;
    308                 }
    309             }
    310             return rv;
    311         } else {
    312             return super.drawChild(canvas, child, drawingTime);
    313         }
    314     }
    315 
    316     /**
    317      * Prepare the given view to be shown. This might include adjusting
    318      * {@link FrameLayout.LayoutParams} before inserting.
    319      */
    320     protected void prepareView(View view) {
    321         // Take requested dimensions from child, but apply default gravity.
    322         FrameLayout.LayoutParams requested = (FrameLayout.LayoutParams)view.getLayoutParams();
    323         if (requested == null) {
    324             requested = new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT,
    325                     LayoutParams.MATCH_PARENT);
    326         }
    327 
    328         requested.gravity = Gravity.CENTER;
    329         view.setLayoutParams(requested);
    330     }
    331 
    332     /**
    333      * Inflate and return the default layout requested by AppWidget provider.
    334      */
    335     protected View getDefaultView() {
    336         if (LOGD) {
    337             Log.d(TAG, "getDefaultView");
    338         }
    339         View defaultView = null;
    340         Exception exception = null;
    341 
    342         try {
    343             if (mInfo != null) {
    344                 Context theirContext = mContext.createPackageContext(
    345                         mInfo.provider.getPackageName(), Context.CONTEXT_RESTRICTED);
    346                 mRemoteContext = theirContext;
    347                 LayoutInflater inflater = (LayoutInflater)
    348                         theirContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    349                 inflater = inflater.cloneInContext(theirContext);
    350                 inflater.setFilter(sInflaterFilter);
    351                 defaultView = inflater.inflate(mInfo.initialLayout, this, false);
    352             } else {
    353                 Log.w(TAG, "can't inflate defaultView because mInfo is missing");
    354             }
    355         } catch (PackageManager.NameNotFoundException e) {
    356             exception = e;
    357         } catch (RuntimeException e) {
    358             exception = e;
    359         }
    360 
    361         if (exception != null) {
    362             Log.w(TAG, "Error inflating AppWidget " + mInfo + ": " + exception.toString());
    363         }
    364 
    365         if (defaultView == null) {
    366             if (LOGD) Log.d(TAG, "getDefaultView couldn't find any view, so inflating error");
    367             defaultView = getErrorView();
    368         }
    369 
    370         return defaultView;
    371     }
    372 
    373     /**
    374      * Inflate and return a view that represents an error state.
    375      */
    376     protected View getErrorView() {
    377         TextView tv = new TextView(mContext);
    378         tv.setText(com.android.internal.R.string.gadget_host_error_inflating);
    379         // TODO: get this color from somewhere.
    380         tv.setBackgroundColor(Color.argb(127, 0, 0, 0));
    381         return tv;
    382     }
    383 
    384     private static class ParcelableSparseArray extends SparseArray<Parcelable> implements Parcelable {
    385         public int describeContents() {
    386             return 0;
    387         }
    388 
    389         public void writeToParcel(Parcel dest, int flags) {
    390             final int count = size();
    391             dest.writeInt(count);
    392             for (int i = 0; i < count; i++) {
    393                 dest.writeInt(keyAt(i));
    394                 dest.writeParcelable(valueAt(i), 0);
    395             }
    396         }
    397 
    398         public static final Parcelable.Creator<ParcelableSparseArray> CREATOR =
    399                 new Parcelable.Creator<ParcelableSparseArray>() {
    400                     public ParcelableSparseArray createFromParcel(Parcel source) {
    401                         final ParcelableSparseArray array = new ParcelableSparseArray();
    402                         final ClassLoader loader = array.getClass().getClassLoader();
    403                         final int count = source.readInt();
    404                         for (int i = 0; i < count; i++) {
    405                             array.put(source.readInt(), source.readParcelable(loader));
    406                         }
    407                         return array;
    408                     }
    409 
    410                     public ParcelableSparseArray[] newArray(int size) {
    411                         return new ParcelableSparseArray[size];
    412                     }
    413                 };
    414     }
    415 }
    416