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