/* * Copyright (C) 2010 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 com.android.layoutlib.bridge.impl; import static com.android.ide.common.rendering.api.Result.Status.ERROR_ANIM_NOT_FOUND; import static com.android.ide.common.rendering.api.Result.Status.ERROR_INFLATION; import static com.android.ide.common.rendering.api.Result.Status.ERROR_LOCK_INTERRUPTED; import static com.android.ide.common.rendering.api.Result.Status.ERROR_NOT_INFLATED; import static com.android.ide.common.rendering.api.Result.Status.ERROR_TIMEOUT; import static com.android.ide.common.rendering.api.Result.Status.ERROR_UNKNOWN; import static com.android.ide.common.rendering.api.Result.Status.ERROR_VIEWGROUP_NO_CHILDREN; import static com.android.ide.common.rendering.api.Result.Status.SUCCESS; import com.android.ide.common.rendering.api.IAnimationListener; import com.android.ide.common.rendering.api.ILayoutPullParser; import com.android.ide.common.rendering.api.IProjectCallback; import com.android.ide.common.rendering.api.LayoutLog; import com.android.ide.common.rendering.api.Params; import com.android.ide.common.rendering.api.RenderSession; import com.android.ide.common.rendering.api.ResourceDensity; import com.android.ide.common.rendering.api.ResourceValue; import com.android.ide.common.rendering.api.Result; import com.android.ide.common.rendering.api.StyleResourceValue; import com.android.ide.common.rendering.api.ViewInfo; import com.android.ide.common.rendering.api.Params.RenderingMode; import com.android.ide.common.rendering.api.Result.Status; import com.android.internal.util.XmlUtils; import com.android.layoutlib.bridge.Bridge; import com.android.layoutlib.bridge.BridgeConstants; import com.android.layoutlib.bridge.android.BridgeContext; import com.android.layoutlib.bridge.android.BridgeInflater; import com.android.layoutlib.bridge.android.BridgeLayoutParamsMapAttributes; import com.android.layoutlib.bridge.android.BridgeWindow; import com.android.layoutlib.bridge.android.BridgeWindowSession; import com.android.layoutlib.bridge.android.BridgeXmlBlockParser; import android.animation.Animator; import android.animation.AnimatorInflater; import android.animation.LayoutTransition; import android.animation.LayoutTransition.TransitionListener; import android.app.Fragment_Delegate; import android.graphics.Bitmap; import android.graphics.Bitmap_Delegate; import android.graphics.Canvas; import android.graphics.drawable.Drawable; import android.os.Handler; import android.util.DisplayMetrics; import android.util.TypedValue; import android.view.View; import android.view.ViewGroup; import android.view.View.AttachInfo; import android.view.View.MeasureSpec; import android.view.ViewGroup.LayoutParams; import android.widget.FrameLayout; import android.widget.TabHost; import android.widget.TabWidget; import java.awt.Color; import java.awt.Graphics2D; import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; /** * Class implementing the render session. * * A session is a stateful representation of a layout file. It is initialized with data coming * through the {@link Bridge} API to inflate the layout. Further actions and rendering can then * be done on the layout. * */ public class RenderSessionImpl { private static final int DEFAULT_TITLE_BAR_HEIGHT = 25; private static final int DEFAULT_STATUS_BAR_HEIGHT = 25; /** * The current context being rendered. This is set through {@link #acquire(long)} and * {@link #init(long)}, and unset in {@link #release()}. */ private static BridgeContext sCurrentContext = null; private final Params mParams; // scene state private RenderSession mScene; private BridgeContext mContext; private BridgeXmlBlockParser mBlockParser; private BridgeInflater mInflater; private StyleResourceValue mCurrentTheme; private int mScreenOffset; private ResourceValue mWindowBackground; private FrameLayout mViewRoot; private Canvas mCanvas; private int mMeasuredScreenWidth = -1; private int mMeasuredScreenHeight = -1; // information being returned through the API private BufferedImage mImage; private ViewInfo mViewInfo; private static final class PostInflateException extends Exception { private static final long serialVersionUID = 1L; public PostInflateException(String message) { super(message); } } /** * Creates a layout scene with all the information coming from the layout bridge API. *
* This must be followed by a call to {@link RenderSessionImpl#init()}, which act as a
* call to {@link RenderSessionImpl#acquire(long)}
*
* @see LayoutBridge#createScene(com.android.layoutlib.api.SceneParams)
*/
public RenderSessionImpl(Params params) {
// copy the params.
mParams = new Params(params);
}
/**
* Initializes and acquires the scene, creating various Android objects such as context,
* inflater, and parser.
*
* @param timeout the time to wait if another rendering is happening.
*
* @return whether the scene was prepared
*
* @see #acquire(long)
* @see #release()
*/
public Result init(long timeout) {
// acquire the lock. if the result is null, lock was just acquired, otherwise, return
// the result.
Result result = acquireLock(timeout);
if (result != null) {
return result;
}
Bridge.setLog(mParams.getLog());
// setup the display Metrics.
DisplayMetrics metrics = new DisplayMetrics();
metrics.densityDpi = mParams.getDensity();
metrics.density = mParams.getDensity() / (float) DisplayMetrics.DENSITY_DEFAULT;
metrics.scaledDensity = metrics.density;
metrics.widthPixels = mParams.getScreenWidth();
metrics.heightPixels = mParams.getScreenHeight();
metrics.xdpi = mParams.getXdpi();
metrics.ydpi = mParams.getYdpi();
// find the current theme and compute the style inheritance map
Map
* This call is blocking if another rendering/inflating is currently happening, and will return
* whether the preparation worked.
*
* The preparation can fail if another rendering took too long and the timeout was elapsed.
*
* More than one call to this from the same thread will have no effect and will return
* {@link Result#SUCCESS}.
*
* After scene actions have taken place, only one call to {@link #release()} must be
* done.
*
* @param timeout the time to wait if another rendering is happening.
*
* @return whether the scene was prepared
*
* @see #release()
*
* @throws IllegalStateException if {@link #init(long)} was never called.
*/
public Result acquire(long timeout) {
if (mContext == null) {
throw new IllegalStateException("After scene creation, #init() must be called");
}
// acquire the lock. if the result is null, lock was just acquired, otherwise, return
// the result.
Result result = acquireLock(timeout);
if (result != null) {
return result;
}
// make sure the Resources object references the context (and other objects) for this
// scene
mContext.initResources();
sCurrentContext = mContext;
Bridge.setLog(mParams.getLog());
return SUCCESS.createResult();
}
/**
* Acquire the lock so that the scene can be acted upon.
*
* This returns null if the lock was just acquired, otherwise it returns
* {@link Result#SUCCESS} if the lock already belonged to that thread, or another
* instance (see {@link Result#getStatus()}) if an error occurred.
*
* @param timeout the time to wait if another rendering is happening.
* @return null if the lock was just acquire or another result depending on the state.
*
* @throws IllegalStateException if the current context is different than the one owned by
* the scene.
*/
private Result acquireLock(long timeout) {
ReentrantLock lock = Bridge.getLock();
if (lock.isHeldByCurrentThread() == false) {
try {
boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS);
if (acquired == false) {
return ERROR_TIMEOUT.createResult();
}
} catch (InterruptedException e) {
return ERROR_LOCK_INTERRUPTED.createResult();
}
} else {
// This thread holds the lock already. Checks that this wasn't for a different context.
// If this is called by init, mContext will be null and so should sCurrentContext
// anyway
if (mContext != sCurrentContext) {
throw new IllegalStateException("Acquiring different scenes from same thread without releases");
}
return SUCCESS.createResult();
}
return null;
}
/**
* Cleans up the scene after an action.
*/
public void release() {
ReentrantLock lock = Bridge.getLock();
// with the use of finally blocks, it is possible to find ourself calling this
// without a successful call to prepareScene. This test makes sure that unlock() will
// not throw IllegalMonitorStateException.
if (lock.isHeldByCurrentThread()) {
// Make sure to remove static references, otherwise we could not unload the lib
mContext.disposeResources();
Bridge.setLog(null);
sCurrentContext = null;
lock.unlock();
}
}
/**
* Inflates the layout.
*
* {@link #acquire(long)} must have been called before this.
*
* @throws IllegalStateException if the current context is different than the one owned by
* the scene, or if {@link #init(long)} was not called.
*/
public Result inflate() {
checkLock();
try {
mViewRoot = new FrameLayout(mContext);
// Sets the project callback (custom view loader) to the fragment delegate so that
// it can instantiate the custom Fragment.
Fragment_Delegate.setProjectCallback(mParams.getProjectCallback());
View view = mInflater.inflate(mBlockParser, mViewRoot);
// post-inflate process. For now this supports TabHost/TabWidget
postInflateProcess(view, mParams.getProjectCallback());
Fragment_Delegate.setProjectCallback(null);
// set the AttachInfo on the root view.
AttachInfo info = new AttachInfo(new BridgeWindowSession(), new BridgeWindow(),
new Handler(), null);
info.mHasWindowFocus = true;
info.mWindowVisibility = View.VISIBLE;
info.mInTouchMode = false; // this is so that we can display selections.
info.mHardwareAccelerated = false;
mViewRoot.dispatchAttachedToWindow(info, 0);
// get the background drawable
if (mWindowBackground != null) {
Drawable d = ResourceHelper.getDrawable(mWindowBackground,
mContext, true /* isFramework */);
mViewRoot.setBackgroundDrawable(d);
}
return SUCCESS.createResult();
} catch (PostInflateException e) {
return ERROR_INFLATION.createResult(e.getMessage(), e);
} catch (Throwable e) {
// get the real cause of the exception.
Throwable t = e;
while (t.getCause() != null) {
t = t.getCause();
}
// log it
mParams.getLog().error("Scene inflate failed", t);
return ERROR_INFLATION.createResult(t.getMessage(), t);
}
}
/**
* Renders the scene.
*
* {@link #acquire(long)} must have been called before this.
*
* @throws IllegalStateException if the current context is different than the one owned by
* the scene, or if {@link #acquire(long)} was not called.
*
* @see SceneParams#getRenderingMode()
* @see LayoutScene#render(long)
*/
public Result render() {
checkLock();
try {
if (mViewRoot == null) {
return ERROR_NOT_INFLATED.createResult();
}
// measure the views
int w_spec, h_spec;
RenderingMode renderingMode = mParams.getRenderingMode();
// only do the screen measure when needed.
boolean newRenderSize = false;
if (mMeasuredScreenWidth == -1) {
newRenderSize = true;
mMeasuredScreenWidth = mParams.getScreenWidth();
mMeasuredScreenHeight = mParams.getScreenHeight();
if (renderingMode != RenderingMode.NORMAL) {
// measure the full size needed by the layout.
w_spec = MeasureSpec.makeMeasureSpec(mMeasuredScreenWidth,
renderingMode.isHorizExpand() ?
MeasureSpec.UNSPECIFIED // this lets us know the actual needed size
: MeasureSpec.EXACTLY);
h_spec = MeasureSpec.makeMeasureSpec(mMeasuredScreenHeight - mScreenOffset,
renderingMode.isVertExpand() ?
MeasureSpec.UNSPECIFIED // this lets us know the actual needed size
: MeasureSpec.EXACTLY);
mViewRoot.measure(w_spec, h_spec);
if (renderingMode.isHorizExpand()) {
int neededWidth = mViewRoot.getChildAt(0).getMeasuredWidth();
if (neededWidth > mMeasuredScreenWidth) {
mMeasuredScreenWidth = neededWidth;
}
}
if (renderingMode.isVertExpand()) {
int neededHeight = mViewRoot.getChildAt(0).getMeasuredHeight();
if (neededHeight > mMeasuredScreenHeight - mScreenOffset) {
mMeasuredScreenHeight = neededHeight + mScreenOffset;
}
}
}
}
// remeasure with the size we need
// This must always be done before the call to layout
w_spec = MeasureSpec.makeMeasureSpec(mMeasuredScreenWidth, MeasureSpec.EXACTLY);
h_spec = MeasureSpec.makeMeasureSpec(mMeasuredScreenHeight - mScreenOffset,
MeasureSpec.EXACTLY);
mViewRoot.measure(w_spec, h_spec);
// now do the layout.
mViewRoot.layout(0, mScreenOffset, mMeasuredScreenWidth, mMeasuredScreenHeight);
// draw the views
// create the BufferedImage into which the layout will be rendered.
if (newRenderSize || mCanvas == null) {
if (mParams.getImageFactory() != null) {
mImage = mParams.getImageFactory().getImage(mMeasuredScreenWidth,
mMeasuredScreenHeight - mScreenOffset);
} else {
mImage = new BufferedImage(mMeasuredScreenWidth,
mMeasuredScreenHeight - mScreenOffset, BufferedImage.TYPE_INT_ARGB);
}
if (mParams.isBgColorOverridden()) {
Graphics2D gc = mImage.createGraphics();
gc.setColor(new Color(mParams.getOverrideBgColor(), true));
gc.fillRect(0, 0, mMeasuredScreenWidth, mMeasuredScreenHeight - mScreenOffset);
gc.dispose();
}
// create an Android bitmap around the BufferedImage
Bitmap bitmap = Bitmap_Delegate.createBitmap(mImage,
true /*isMutable*/,
ResourceDensity.getEnum(mParams.getDensity()));
// create a Canvas around the Android bitmap
mCanvas = new Canvas(bitmap);
mCanvas.setDensity(mParams.getDensity());
}
mViewRoot.draw(mCanvas);
mViewInfo = visit(((ViewGroup)mViewRoot).getChildAt(0), mContext);
// success!
return SUCCESS.createResult();
} catch (Throwable e) {
// get the real cause of the exception.
Throwable t = e;
while (t.getCause() != null) {
t = t.getCause();
}
// log it
mParams.getLog().error("Scene Render failed", t);
return ERROR_UNKNOWN.createResult(t.getMessage(), t);
}
}
/**
* Animate an object
*
* {@link #acquire(long)} must have been called before this.
*
* @throws IllegalStateException if the current context is different than the one owned by
* the scene, or if {@link #acquire(long)} was not called.
*
* @see LayoutScene#animate(Object, String, boolean, IAnimationListener)
*/
public Result animate(Object targetObject, String animationName,
boolean isFrameworkAnimation, IAnimationListener listener) {
checkLock();
// find the animation file.
ResourceValue animationResource = null;
int animationId = 0;
if (isFrameworkAnimation) {
animationResource = mContext.getFrameworkResource("anim", animationName);
if (animationResource != null) {
animationId = Bridge.getResourceValue("anim", animationName);
}
} else {
animationResource = mContext.getProjectResource("anim", animationName);
if (animationResource != null) {
animationId = mContext.getProjectCallback().getResourceValue("anim", animationName);
}
}
if (animationResource != null) {
try {
Animator anim = AnimatorInflater.loadAnimator(mContext, animationId);
if (anim != null) {
anim.setTarget(targetObject);
new PlayAnimationThread(anim, this, animationName, listener).start();
return SUCCESS.createResult();
}
} catch (Exception e) {
// get the real cause of the exception.
Throwable t = e;
while (t.getCause() != null) {
t = t.getCause();
}
return ERROR_UNKNOWN.createResult(t.getMessage(), t);
}
}
return ERROR_ANIM_NOT_FOUND.createResult();
}
/**
* Insert a new child into an existing parent.
*
* {@link #acquire(long)} must have been called before this.
*
* @throws IllegalStateException if the current context is different than the one owned by
* the scene, or if {@link #acquire(long)} was not called.
*
* @see LayoutScene#insertChild(Object, ILayoutPullParser, int, IAnimationListener)
*/
public Result insertChild(final ViewGroup parentView, ILayoutPullParser childXml,
final int index, IAnimationListener listener) {
checkLock();
// create a block parser for the XML
BridgeXmlBlockParser blockParser = new BridgeXmlBlockParser(childXml, mContext,
false /* platformResourceFlag */);
// inflate the child without adding it to the root since we want to control where it'll
// get added. We do pass the parentView however to ensure that the layoutParams will
// be created correctly.
final View child = mInflater.inflate(blockParser, parentView, false /*attachToRoot*/);
invalidateRenderingSize();
if (listener != null) {
new AnimationThread(this, "insertChild", listener) {
@Override
public Result preAnimation() {
parentView.setLayoutTransition(new LayoutTransition());
return addView(parentView, child, index);
}
@Override
public void postAnimation() {
parentView.setLayoutTransition(null);
}
}.start();
// always return success since the real status will come through the listener.
return SUCCESS.createResult(child);
}
// add it to the parentView in the correct location
Result result = addView(parentView, child, index);
if (result.isSuccess() == false) {
return result;
}
result = render();
if (result.isSuccess()) {
result = result.getCopyWithData(child);
}
return result;
}
/**
* Adds a given view to a given parent at a given index.
*
* @param parent the parent to receive the view
* @param view the view to add to the parent
* @param index the index where to do the add.
*
* @return a Result with {@link Status#SUCCESS} or
* {@link Status#ERROR_VIEWGROUP_NO_CHILDREN} if the given parent doesn't support
* adding views.
*/
private Result addView(ViewGroup parent, View view, int index) {
try {
parent.addView(view, index);
return SUCCESS.createResult();
} catch (UnsupportedOperationException e) {
// looks like this is a view class that doesn't support children manipulation!
return ERROR_VIEWGROUP_NO_CHILDREN.createResult();
}
}
/**
* Moves a view to a new parent at a given location
*
* {@link #acquire(long)} must have been called before this.
*
* @throws IllegalStateException if the current context is different than the one owned by
* the scene, or if {@link #acquire(long)} was not called.
*
* @see LayoutScene#moveChild(Object, Object, int, Map, IAnimationListener)
*/
public Result moveChild(final ViewGroup newParentView, final View childView, final int index,
Map
* {@link #acquire(long)} must have been called before this.
*
* @throws IllegalStateException if the current context is different than the one owned by
* the scene, or if {@link #acquire(long)} was not called.
*
* @see LayoutScene#removeChild(Object, IAnimationListener)
*/
public Result removeChild(final View childView, IAnimationListener listener) {
checkLock();
invalidateRenderingSize();
final ViewGroup parent = (ViewGroup) childView.getParent();
if (listener != null) {
new AnimationThread(this, "moveChild", listener) {
@Override
public Result preAnimation() {
parent.setLayoutTransition(new LayoutTransition());
return removeView(parent, childView);
}
@Override
public void postAnimation() {
parent.setLayoutTransition(null);
}
}.start();
// always return success since the real status will come through the listener.
return SUCCESS.createResult();
}
Result result = removeView(parent, childView);
if (result.isSuccess() == false) {
return result;
}
return render();
}
/**
* Removes a given view from its current parent.
*
* @param view the view to remove from its parent
*
* @return a Result with {@link Status#SUCCESS} or
* {@link Status#ERROR_VIEWGROUP_NO_CHILDREN} if the given parent doesn't support
* adding views.
*/
private Result removeView(ViewGroup parent, View view) {
try {
parent.removeView(view);
return SUCCESS.createResult();
} catch (UnsupportedOperationException e) {
// looks like this is a view class that doesn't support children manipulation!
return ERROR_VIEWGROUP_NO_CHILDREN.createResult();
}
}
/**
* Returns the log associated with the session.
* @return the log or null if there are none.
*/
public LayoutLog getLog() {
if (mParams != null) {
return mParams.getLog();
}
return null;
}
/**
* Checks that the lock is owned by the current thread and that the current context is the one
* from this scene.
*
* @throws IllegalStateException if the current context is different than the one owned by
* the scene, or if {@link #acquire(long)} was not called.
*/
private void checkLock() {
ReentrantLock lock = Bridge.getLock();
if (lock.isHeldByCurrentThread() == false) {
throw new IllegalStateException("scene must be acquired first. see #acquire(long)");
}
if (sCurrentContext != mContext) {
throw new IllegalStateException("Thread acquired a scene but is rendering a different one");
}
}
/**
* Compute style information from the given list of style for the project and framework.
* @param themeName the name of the current theme. In order to differentiate project and
* platform themes sharing the same name, all project themes must be prepended with
* a '*' character.
* @param isProjectTheme Is this a project theme
* @param inProjectStyleMap the project style map
* @param inFrameworkStyleMap the framework style map
* @param outInheritanceMap the map of style inheritance. This is filled by the method
* @return the {@link StyleResourceValue} matching themeName
*/
private StyleResourceValue computeStyleMaps(
String themeName, boolean isProjectTheme, Map
*
* @param parentName the name of the style.
* @param inProjectStyleMap the project style map. Can be null
* @param inFrameworkStyleMap the framework style map.
* @return The matching {@link StyleResourceValue} object or null
if not found.
*/
private StyleResourceValue getStyle(String parentName,
Mapnull
if the style is a root style.
*/
private String getParentName(String styleName) {
int index = styleName.lastIndexOf('.');
if (index != -1) {
return styleName.substring(0, index);
}
return null;
}
/**
* Returns the top screen offset. This depends on whether the current theme defines the user
* of the title and status bars.
* @param frameworkResources The framework resources
* @param currentTheme The current theme
* @param context The context
* @return the pixel height offset
*/
private int getScreenOffset(Map