/* * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.appwidget; import android.content.Context; import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.os.Handler; import android.os.Message; import android.os.SystemClock; import android.util.Config; import android.util.Log; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.animation.Animation; import android.widget.FrameLayout; import android.widget.RemoteViews; import android.widget.TextView; /** * Provides the glue to show AppWidget views. This class offers automatic animation * between updates, and will try recycling old views for each incoming * {@link RemoteViews}. */ public class AppWidgetHostView extends FrameLayout { static final String TAG = "AppWidgetHostView"; static final boolean LOGD = false; static final boolean CROSSFADE = false; static final int VIEW_MODE_NOINIT = 0; static final int VIEW_MODE_CONTENT = 1; static final int VIEW_MODE_ERROR = 2; static final int VIEW_MODE_DEFAULT = 3; static final int FADE_DURATION = 1000; // When we're inflating the initialLayout for a AppWidget, we only allow // views that are allowed in RemoteViews. static final LayoutInflater.Filter sInflaterFilter = new LayoutInflater.Filter() { public boolean onLoadClass(Class clazz) { return clazz.isAnnotationPresent(RemoteViews.RemoteView.class); } }; Context mContext; int mAppWidgetId; AppWidgetProviderInfo mInfo; View mView; int mViewMode = VIEW_MODE_NOINIT; int mLayoutId = -1; long mFadeStartTime = -1; Bitmap mOld; Paint mOldPaint = new Paint(); /** * Create a host view. Uses default fade animations. */ public AppWidgetHostView(Context context) { this(context, android.R.anim.fade_in, android.R.anim.fade_out); } /** * Create a host view. Uses specified animations when pushing * {@link #updateAppWidget(RemoteViews)}. * * @param animationIn Resource ID of in animation to use * @param animationOut Resource ID of out animation to use */ public AppWidgetHostView(Context context, int animationIn, int animationOut) { super(context); mContext = context; } /** * Set the AppWidget that will be displayed by this view. */ public void setAppWidget(int appWidgetId, AppWidgetProviderInfo info) { mAppWidgetId = appWidgetId; mInfo = info; } public int getAppWidgetId() { return mAppWidgetId; } public AppWidgetProviderInfo getAppWidgetInfo() { return mInfo; } /** * Process a set of {@link RemoteViews} coming in as an update from the * AppWidget provider. Will animate into these new views as needed. */ public void updateAppWidget(RemoteViews remoteViews) { if (LOGD) Log.d(TAG, "updateAppWidget called mOld=" + mOld); boolean recycled = false; View content = null; Exception exception = null; // Capture the old view into a bitmap so we can do the crossfade. if (CROSSFADE) { if (mFadeStartTime < 0) { if (mView != null) { final int width = mView.getWidth(); final int height = mView.getHeight(); try { mOld = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); } catch (OutOfMemoryError e) { // we just won't do the fade mOld = null; } if (mOld != null) { //mView.drawIntoBitmap(mOld); } } } } if (remoteViews == null) { if (mViewMode == VIEW_MODE_DEFAULT) { // We've already done this -- nothing to do. return; } content = getDefaultView(); mLayoutId = -1; mViewMode = VIEW_MODE_DEFAULT; } else { int layoutId = remoteViews.getLayoutId(); // If our stale view has been prepared to match active, and the new // layout matches, try recycling it if (content == null && layoutId == mLayoutId) { try { remoteViews.reapply(mContext, mView); content = mView; recycled = true; if (LOGD) Log.d(TAG, "was able to recycled existing layout"); } catch (RuntimeException e) { exception = e; } } // Try normal RemoteView inflation if (content == null) { try { content = remoteViews.apply(mContext, this); if (LOGD) Log.d(TAG, "had to inflate new layout"); } catch (RuntimeException e) { exception = e; } } mLayoutId = layoutId; mViewMode = VIEW_MODE_CONTENT; } if (content == null) { if (mViewMode == VIEW_MODE_ERROR) { // We've already done this -- nothing to do. return ; } Log.w(TAG, "updateAppWidget couldn't find any view, using error view", exception); content = getErrorView(); mViewMode = VIEW_MODE_ERROR; } if (!recycled) { prepareView(content); addView(content); } if (mView != content) { removeView(mView); mView = content; } if (CROSSFADE) { if (mFadeStartTime < 0) { // if there is already an animation in progress, don't do anything -- // the new view will pop in on top of the old one during the cross fade, // and that looks okay. mFadeStartTime = SystemClock.uptimeMillis(); invalidate(); } } } protected boolean drawChild(Canvas canvas, View child, long drawingTime) { if (CROSSFADE) { int alpha; int l = child.getLeft(); int t = child.getTop(); if (mFadeStartTime > 0) { alpha = (int)(((drawingTime-mFadeStartTime)*255)/FADE_DURATION); if (alpha > 255) { alpha = 255; } Log.d(TAG, "drawChild alpha=" + alpha + " l=" + l + " t=" + t + " w=" + child.getWidth()); if (alpha != 255 && mOld != null) { mOldPaint.setAlpha(255-alpha); //canvas.drawBitmap(mOld, l, t, mOldPaint); } } else { alpha = 255; } int restoreTo = canvas.saveLayerAlpha(l, t, child.getWidth(), child.getHeight(), alpha, Canvas.HAS_ALPHA_LAYER_SAVE_FLAG | Canvas.CLIP_TO_LAYER_SAVE_FLAG); boolean rv = super.drawChild(canvas, child, drawingTime); canvas.restoreToCount(restoreTo); if (alpha < 255) { invalidate(); } else { mFadeStartTime = -1; if (mOld != null) { mOld.recycle(); mOld = null; } } return rv; } else { return super.drawChild(canvas, child, drawingTime); } } /** * Prepare the given view to be shown. This might include adjusting * {@link FrameLayout.LayoutParams} before inserting. */ protected void prepareView(View view) { // Take requested dimensions from parent, but apply default gravity. ViewGroup.LayoutParams requested = view.getLayoutParams(); if (requested == null) { requested = new FrameLayout.LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT); } FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(requested.width, requested.height); params.gravity = Gravity.CENTER; view.setLayoutParams(params); } /** * Inflate and return the default layout requested by AppWidget provider. */ protected View getDefaultView() { View defaultView = null; Exception exception = null; try { if (mInfo != null) { Context theirContext = mContext.createPackageContext( mInfo.provider.getPackageName(), 0 /* no flags */); LayoutInflater inflater = (LayoutInflater) theirContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); inflater = inflater.cloneInContext(theirContext); inflater.setFilter(sInflaterFilter); defaultView = inflater.inflate(mInfo.initialLayout, this, false); } else { Log.w(TAG, "can't inflate defaultView because mInfo is missing"); } } catch (PackageManager.NameNotFoundException e) { exception = e; } catch (RuntimeException e) { exception = e; } if (exception != null && LOGD) { Log.w(TAG, "Error inflating AppWidget " + mInfo, exception); } if (defaultView == null) { if (LOGD) Log.d(TAG, "getDefaultView couldn't find any view, so inflating error"); defaultView = getErrorView(); } return defaultView; } /** * Inflate and return a view that represents an error state. */ protected View getErrorView() { TextView tv = new TextView(mContext); tv.setText(com.android.internal.R.string.gadget_host_error_inflating); // TODO: get this color from somewhere. tv.setBackgroundColor(Color.argb(127, 0, 0, 0)); return tv; } }