/* * 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.ide.common.resources.ResourceResolver; import com.android.ide.common.resources.ResourceResolver.IFrameworkResourceIdProvider; import com.android.internal.util.XmlUtils; import com.android.layoutlib.bridge.Bridge; 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.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 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(); // create the resource resolver ResourceResolver resolver = ResourceResolver.create( new IFrameworkResourceIdProvider() { public Integer getId(String resType, String resName) { return Bridge.getResourceValue(resType, resName); } }, mParams.getProjectResources(), mParams.getFrameworkResources(), mParams.getThemeName(), mParams.isProjectTheme(), mParams.getLog()); // build the context mContext = new BridgeContext(mParams.getProjectKey(), metrics, resolver, mParams.getProjectCallback()); // set the current rendering context sCurrentContext = mContext; // make sure the Resources object references the context (and other objects) for this // scene mContext.initResources(); // get the screen offset and window-background resource mWindowBackground = null; mScreenOffset = 0; StyleResourceValue theme = resolver.getTheme(); if (theme != null && mParams.isBgColorOverridden() == false) { mWindowBackground = resolver.findItemInTheme("windowBackground"); mWindowBackground = resolver.resolveResValue(mWindowBackground); mScreenOffset = getScreenOffset(resolver, metrics); } // build the inflater and parser. mInflater = new BridgeInflater(mContext, mParams.getProjectCallback()); mContext.setBridgeInflater(mInflater); mInflater.setFactory2(mContext); mBlockParser = new BridgeXmlBlockParser(mParams.getLayoutDescription(), mContext, false /* platformResourceFlag */); return SUCCESS.createResult(); } /** * Prepares the scene for action. *
* 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(); } 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(); } 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.getResolver().getFrameworkResource( ResourceResolver.RES_ANIMATOR, animationName); if (animationResource != null) { animationId = Bridge.getResourceValue(ResourceResolver.RES_ANIMATOR, animationName); } } else { animationResource = mContext.getResolver().getProjectResource( ResourceResolver.RES_ANIMATOR, animationName); if (animationResource != null) { animationId = mContext.getProjectCallback().getResourceValue( ResourceResolver.RES_ANIMATOR, 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");
}
}
/**
* Returns the top screen offset. This depends on whether the current theme defines the user
* of the title and status bars.
* @param resolver The {@link ResourceResolver}
* @param metrics The display metrics
* @return the pixel height offset
*/
private int getScreenOffset(ResourceResolver resolver, DisplayMetrics metrics) {
int offset = 0;
// get the title bar flag from the current theme.
ResourceValue value = resolver.findItemInTheme("windowNoTitle");
// because it may reference something else, we resolve it.
value = resolver.resolveResValue(value);
// if there's a value and it's true (default is false)
if (value == null || value.getValue() == null ||
XmlUtils.convertValueToBoolean(value.getValue(), false /* defValue */) == false) {
// default size of the window title bar
int defaultOffset = DEFAULT_TITLE_BAR_HEIGHT;
// get value from the theme.
value = resolver.findItemInTheme("windowTitleSize");
// resolve it
value = resolver.resolveResValue(value);
if (value != null) {
// get the numerical value, if available
TypedValue typedValue = ResourceHelper.getValue(value.getValue());
if (typedValue != null) {
// compute the pixel value based on the display metrics
defaultOffset = (int)typedValue.getDimension(metrics);
}
}
offset += defaultOffset;
}
// get the fullscreen flag from the current theme.
value = resolver.findItemInTheme("windowFullscreen");
// because it may reference something else, we resolve it.
value = resolver.resolveResValue(value);
if (value == null || value.getValue() == null ||
XmlUtils.convertValueToBoolean(value.getValue(), false /* defValue */) == false) {
// default value
int defaultOffset = DEFAULT_STATUS_BAR_HEIGHT;
// get the real value
value = resolver.getFrameworkResource(ResourceResolver.RES_DIMEN, "status_bar_height");
if (value != null) {
TypedValue typedValue = ResourceHelper.getValue(value.getValue());
if (typedValue != null) {
// compute the pixel value based on the display metrics
defaultOffset = (int)typedValue.getDimension(metrics);
}
}
// add the computed offset.
offset += defaultOffset;
}
return offset;
}
/**
* Post process on a view hierachy that was just inflated.
*