blob: a985bd415559cafadaff35e2133fc87f1192626d [file] [log] [blame]
/*
* 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.gadget;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Color;
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;
import android.widget.ViewAnimator;
/**
* Provides the glue to show gadget views. This class offers automatic animation
* between updates, and will try recycling old views for each incoming
* {@link RemoteViews}.
*/
public class GadgetHostView extends ViewAnimator implements Animation.AnimationListener {
static final String TAG = "GadgetHostView";
static final boolean LOGD = Config.LOGD || true;
// When we're inflating the initialLayout for a gadget, 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 mLocalContext;
int mGadgetId;
GadgetProviderInfo mInfo;
View mActiveView = null;
View mStaleView = null;
int mActiveLayoutId = -1;
int mStaleLayoutId = -1;
/**
* Last set of {@link RemoteViews} applied to {@link #mActiveView}
*/
RemoteViews mActiveActions = null;
/**
* Flag indicating that {@link #mActiveActions} has been applied to
* {@link #mStaleView}, meaning it's readyto recycle.
*/
boolean mStalePrepared = false;
/**
* Create a host view. Uses default fade animations.
*/
public GadgetHostView(Context context) {
this(context, android.R.anim.fade_in, android.R.anim.fade_out);
}
/**
* Create a host view. Uses specified animations when pushing
* {@link #updateGadget(RemoteViews)}.
*
* @param animationIn Resource ID of in animation to use
* @param animationOut Resource ID of out animation to use
*/
public GadgetHostView(Context context, int animationIn, int animationOut) {
super(context);
mLocalContext = context;
// Prepare our default transition animations
setAnimateFirstView(true);
setInAnimation(context, animationIn);
setOutAnimation(context, animationOut);
// Watch for animation events to prepare recycling
Animation inAnimation = getInAnimation();
if (inAnimation != null) {
inAnimation.setAnimationListener(this);
}
}
/**
* Set the gadget that will be displayed by this view.
*/
public void setGadget(int gadgetId, GadgetProviderInfo info) {
if (mInfo != null) {
// TODO: remove the old view, or whatever
}
mGadgetId = gadgetId;
mInfo = info;
}
public int getGadgetId() {
return mGadgetId;
}
public GadgetProviderInfo getGadgetInfo() {
return mInfo;
}
public void onAnimationEnd(Animation animation) {
// When our transition animation finishes, we should try bringing our
// newly-stale view up to the current view.
if (mActiveActions != null &&
mStaleLayoutId == mActiveActions.getLayoutId()) {
if (LOGD) Log.d(TAG, "after animation, layoutId matched so we're recycling old view");
mActiveActions.reapply(mLocalContext, mStaleView);
mStalePrepared = true;
}
}
public void onAnimationRepeat(Animation animation) {
}
public void onAnimationStart(Animation animation) {
}
/**
* Process a set of {@link RemoteViews} coming in as an update from the
* gadget provider. Will animate into these new views as needed.
*/
public void updateGadget(RemoteViews remoteViews) {
if (LOGD) Log.d(TAG, "updateGadget called");
boolean recycled = false;
View newContent = null;
Exception exception = null;
if (remoteViews == null) {
newContent = getDefaultView();
}
// If our stale view has been prepared to match active, and the new
// layout matches, try recycling it
if (newContent == null && mStalePrepared &&
remoteViews.getLayoutId() == mStaleLayoutId) {
try {
remoteViews.reapply(mLocalContext, mStaleView);
newContent = mStaleView;
recycled = true;
if (LOGD) Log.d(TAG, "was able to recycled existing layout");
} catch (RuntimeException e) {
exception = e;
}
}
// Try normal RemoteView inflation
if (newContent == null) {
try {
newContent = remoteViews.apply(mLocalContext, this);
if (LOGD) Log.d(TAG, "had to inflate new layout");
} catch (RuntimeException e) {
exception = e;
}
}
if (exception != null && LOGD) {
Log.w(TAG, "Error inflating gadget " + getGadgetInfo(), exception);
}
if (newContent == null) {
// TODO: Should we throw an exception here for the host activity to catch?
// Maybe we should show a generic error widget.
if (LOGD) Log.d(TAG, "updateGadget couldn't find any view, so inflating error");
newContent = getErrorView();
}
if (!recycled) {
prepareView(newContent);
addView(newContent);
}
showNext();
if (!recycled) {
removeView(mStaleView);
}
mStalePrepared = false;
mActiveActions = remoteViews;
mStaleView = mActiveView;
mActiveView = newContent;
mStaleLayoutId = mActiveLayoutId;
mActiveLayoutId = (remoteViews == null) ? -1 : remoteViews.getLayoutId();
}
/**
* 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 gadget provider.
*/
protected View getDefaultView() {
View defaultView = null;
Exception exception = null;
try {
if (mInfo != null) {
Context theirContext = mLocalContext.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 gadget " + 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(mLocalContext);
// TODO: move this error string and background color into resources
tv.setText("Error inflating gadget");
tv.setBackgroundColor(Color.argb(127, 0, 0, 0));
return tv;
}
}