blob: 0a353f9e1d64b6ce824593b018efec302fcee7ec [file] [log] [blame]
/*
* 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.ide.common.rendering;
import static com.android.ide.common.rendering.api.Result.Status.ERROR_REFLECTION;
import com.android.ide.common.rendering.api.Bridge;
import com.android.ide.common.rendering.api.Capability;
import com.android.ide.common.rendering.api.DrawableParams;
import com.android.ide.common.rendering.api.ILayoutPullParser;
import com.android.ide.common.rendering.api.LayoutLog;
import com.android.ide.common.rendering.api.RenderSession;
import com.android.ide.common.rendering.api.ResourceValue;
import com.android.ide.common.rendering.api.Result;
import com.android.ide.common.rendering.api.Result.Status;
import com.android.ide.common.rendering.api.SessionParams;
import com.android.ide.common.rendering.api.SessionParams.RenderingMode;
import com.android.ide.common.rendering.api.ViewInfo;
import com.android.ide.common.rendering.legacy.ILegacyPullParser;
import com.android.ide.common.rendering.legacy.LegacyCallback;
import com.android.ide.common.resources.ResourceResolver;
import com.android.ide.common.sdk.LoadStatus;
import com.android.layoutlib.api.ILayoutBridge;
import com.android.layoutlib.api.ILayoutLog;
import com.android.layoutlib.api.ILayoutResult;
import com.android.layoutlib.api.ILayoutResult.ILayoutViewInfo;
import com.android.layoutlib.api.IProjectCallback;
import com.android.layoutlib.api.IResourceValue;
import com.android.layoutlib.api.IXmlPullParser;
import com.android.resources.ResourceType;
import com.android.utils.ILogger;
import java.awt.image.BufferedImage;
import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
/**
* Class to use the Layout library.
* <p/>
* Use {@link #load(String, ILogger)} to load the jar file.
* <p/>
* Use the layout library with:
* {@link #init(String, Map)}, {@link #supports(Capability)}, {@link #createSession(SessionParams)},
* {@link #dispose()}, {@link #clearCaches(Object)}.
*
* <p/>
* For client wanting to access both new and old (pre API level 5) layout libraries, it is
* important that the following interfaces be used:<br>
* {@link ILegacyPullParser} instead of {@link ILayoutPullParser}<br>
* {@link LegacyCallback} instead of {@link com.android.ide.common.rendering.api.IProjectCallback}.
* <p/>
* These interfaces will ensure that both new and older Layout libraries can be accessed.
*/
@SuppressWarnings("deprecation")
public class LayoutLibrary {
public final static String CLASS_BRIDGE = "com.android.layoutlib.bridge.Bridge"; //$NON-NLS-1$
/** Link to the layout bridge */
private final Bridge mBridge;
/** Link to a ILayoutBridge in case loaded an older library */
private final ILayoutBridge mLegacyBridge;
/** Status of the layoutlib.jar loading */
private final LoadStatus mStatus;
/** Message associated with the {@link LoadStatus}. This is mostly used when
* {@link #getStatus()} returns {@link LoadStatus#FAILED}.
*/
private final String mLoadMessage;
/** classloader used to load the jar file */
private final ClassLoader mClassLoader;
// Reflection data for older Layout Libraries.
private Method mViewGetParentMethod;
private Method mViewGetBaselineMethod;
private Method mViewParentIndexOfChildMethod;
private Class<?> mMarginLayoutParamClass;
private Field mLeftMarginField;
private Field mTopMarginField;
private Field mRightMarginField;
private Field mBottomMarginField;
/**
* Returns the {@link LoadStatus} of the loading of the layoutlib jar file.
*/
public LoadStatus getStatus() {
return mStatus;
}
/** Returns the message associated with the {@link LoadStatus}. This is mostly used when
* {@link #getStatus()} returns {@link LoadStatus#FAILED}.
*/
public String getLoadMessage() {
return mLoadMessage;
}
/**
* Returns the classloader used to load the classes in the layoutlib jar file.
*/
public ClassLoader getClassLoader() {
return mClassLoader;
}
/**
* Loads the layoutlib.jar file located at the given path and returns a {@link LayoutLibrary}
* object representing the result.
* <p/>
* If loading failed {@link #getStatus()} will reflect this, and {@link #getBridge()} will
* return null.
*
* @param layoutLibJarOsPath the path of the jar file
* @param log an optional log file.
* @return a {@link LayoutLibrary} object always.
*/
public static LayoutLibrary load(String layoutLibJarOsPath, ILogger log, String toolName) {
LoadStatus status = LoadStatus.LOADING;
String message = null;
Bridge bridge = null;
ILayoutBridge legacyBridge = null;
ClassLoader classLoader = null;
try {
// get the URL for the file.
File f = new File(layoutLibJarOsPath);
if (f.isFile() == false) {
if (log != null) {
log.error(null, "layoutlib.jar is missing!"); //$NON-NLS-1$
}
} else {
URI uri = f.toURI();
URL url = uri.toURL();
// create a class loader. Because this jar reference interfaces
// that are in the editors plugin, it's important to provide
// a parent class loader.
classLoader = new URLClassLoader(
new URL[] { url },
LayoutLibrary.class.getClassLoader());
// load the class
Class<?> clazz = classLoader.loadClass(CLASS_BRIDGE);
if (clazz != null) {
// instantiate an object of the class.
Constructor<?> constructor = clazz.getConstructor();
if (constructor != null) {
Object bridgeObject = constructor.newInstance();
if (bridgeObject instanceof Bridge) {
bridge = (Bridge)bridgeObject;
} else if (bridgeObject instanceof ILayoutBridge) {
legacyBridge = (ILayoutBridge) bridgeObject;
}
}
}
if (bridge == null && legacyBridge == null) {
status = LoadStatus.FAILED;
message = "Failed to load " + CLASS_BRIDGE; //$NON-NLS-1$
if (log != null) {
log.error(null,
"Failed to load " + //$NON-NLS-1$
CLASS_BRIDGE +
" from " + //$NON-NLS-1$
layoutLibJarOsPath);
}
} else {
// mark the lib as loaded, unless it's overridden below.
status = LoadStatus.LOADED;
// check the API, only if it's not a legacy bridge
if (bridge != null) {
int api = bridge.getApiLevel();
if (api > Bridge.API_CURRENT) {
status = LoadStatus.FAILED;
message = String.format(
"This version of the rendering library is more recent than your version of %1$s. Please update %1$s", toolName);
}
}
}
}
} catch (Throwable t) {
status = LoadStatus.FAILED;
Throwable cause = t;
while (cause.getCause() != null) {
cause = cause.getCause();
}
message = "Failed to load the LayoutLib: " + cause.getMessage();
// log the error.
if (log != null) {
log.error(t, message);
}
}
return new LayoutLibrary(bridge, legacyBridge, classLoader, status, message);
}
// ------ Layout Lib API proxy
/**
* Returns the API level of the layout library.
*/
public int getApiLevel() {
if (mBridge != null) {
return mBridge.getApiLevel();
}
if (mLegacyBridge != null) {
return getLegacyApiLevel();
}
return 0;
}
/**
* Returns the revision of the library inside a given (layoutlib) API level.
* The true version number of the library is {@link #getApiLevel()}.{@link #getRevision()}
*/
public int getRevision() {
if (mBridge != null) {
return mBridge.getRevision();
}
return 0;
}
/**
* Returns whether the LayoutLibrary supports a given {@link Capability}.
* @return true if it supports it.
*
* @see Bridge#getCapabilities()
*
*/
public boolean supports(Capability capability) {
if (mBridge != null) {
return mBridge.getCapabilities().contains(capability);
}
if (mLegacyBridge != null) {
switch (capability) {
case UNBOUND_RENDERING:
// legacy stops at 4. 5 is new API.
return getLegacyApiLevel() == 4;
}
}
return false;
}
/**
* Initializes the Layout Library object. This must be called before any other action is taken
* on the instance.
*
* @param platformProperties The build properties for the platform.
* @param fontLocation the location of the fonts in the SDK target.
* @param enumValueMap map attrName => { map enumFlagName => Integer value }. This is typically
* read from attrs.xml in the SDK target.
* @param log a {@link LayoutLog} object. Can be null.
* @return true if success.
*
* @see Bridge#init(String, Map)
*/
public boolean init(Map<String, String> platformProperties,
File fontLocation,
Map<String, Map<String, Integer>> enumValueMap,
LayoutLog log) {
if (mBridge != null) {
return mBridge.init(platformProperties, fontLocation, enumValueMap, log);
} else if (mLegacyBridge != null) {
return mLegacyBridge.init(fontLocation.getAbsolutePath(), enumValueMap);
}
return false;
}
/**
* Prepares the layoutlib to unloaded.
*
* @see Bridge#dispose()
*/
public boolean dispose() {
if (mBridge != null) {
return mBridge.dispose();
}
return true;
}
/**
* Starts a layout session by inflating and rendering it. The method returns a
* {@link RenderSession} on which further actions can be taken.
* <p/>
* Before taking further actions on the scene, it is recommended to use
* {@link #supports(Capability)} to check what the scene can do.
*
* @return a new {@link ILayoutScene} object that contains the result of the scene creation and
* first rendering or null if {@link #getStatus()} doesn't return {@link LoadStatus#LOADED}.
*
* @see Bridge#createSession(SessionParams)
*/
public RenderSession createSession(SessionParams params) {
if (mBridge != null) {
RenderSession session = mBridge.createSession(params);
if (params.getExtendedViewInfoMode() &&
mBridge.getCapabilities().contains(Capability.EXTENDED_VIEWINFO) == false) {
// Extended view info was requested but the layoutlib does not support it.
// Add it manually.
List<ViewInfo> infoList = session.getRootViews();
if (infoList != null) {
for (ViewInfo info : infoList) {
addExtendedViewInfo(info);
}
}
}
return session;
} else if (mLegacyBridge != null) {
return createLegacySession(params);
}
return null;
}
/**
* Renders a Drawable. If the rendering is successful, the result image is accessible through
* {@link Result#getData()}. It is of type {@link BufferedImage}
* @param params the rendering parameters.
* @return the result of the action.
*/
public Result renderDrawable(DrawableParams params) {
if (mBridge != null) {
return mBridge.renderDrawable(params);
}
return Status.NOT_IMPLEMENTED.createResult();
}
/**
* Clears the resource cache for a specific project.
* <p/>This cache contains bitmaps and nine patches that are loaded from the disk and reused
* until this method is called.
* <p/>The cache is not configuration dependent and should only be cleared when a
* resource changes (at this time only bitmaps and 9 patches go into the cache).
*
* @param projectKey the key for the project.
*
* @see Bridge#clearCaches(Object)
*/
public void clearCaches(Object projectKey) {
if (mBridge != null) {
mBridge.clearCaches(projectKey);
} else if (mLegacyBridge != null) {
mLegacyBridge.clearCaches(projectKey);
}
}
/**
* Utility method returning the parent of a given view object.
*
* @param viewObject the object for which to return the parent.
*
* @return a {@link Result} indicating the status of the action, and if success, the parent
* object in {@link Result#getData()}
*/
public Result getViewParent(Object viewObject) {
if (mBridge != null) {
Result r = mBridge.getViewParent(viewObject);
if (r.isSuccess()) {
return r;
}
}
return getViewParentWithReflection(viewObject);
}
/**
* Utility method returning the index of a given view in its parent.
* @param viewObject the object for which to return the index.
*
* @return a {@link Result} indicating the status of the action, and if success, the index in
* the parent in {@link Result#getData()}
*/
public Result getViewIndex(Object viewObject) {
if (mBridge != null) {
Result r = mBridge.getViewIndex(viewObject);
if (r.isSuccess()) {
return r;
}
}
return getViewIndexReflection(viewObject);
}
// ------ Implementation
private LayoutLibrary(Bridge bridge, ILayoutBridge legacyBridge, ClassLoader classLoader,
LoadStatus status, String message) {
mBridge = bridge;
mLegacyBridge = legacyBridge;
mClassLoader = classLoader;
mStatus = status;
mLoadMessage = message;
}
/**
* Returns the API level of the legacy bridge.
* <p/>
* This handles the case where ILayoutBridge does not have a {@link ILayoutBridge#getApiLevel()}
* (at API level 1).
* <p/>
* {@link ILayoutBridge#getApiLevel()} should never called directly.
*
* @return the api level of {@link #mLegacyBridge}.
*/
private int getLegacyApiLevel() {
int apiLevel = 1;
try {
apiLevel = mLegacyBridge.getApiLevel();
} catch (AbstractMethodError e) {
// the first version of the api did not have this method
// so this is 1
}
return apiLevel;
}
private RenderSession createLegacySession(SessionParams params) {
if (params.getLayoutDescription() instanceof IXmlPullParser == false) {
throw new IllegalArgumentException("Parser must be of type ILegacyPullParser");
}
if (params.getProjectCallback() instanceof
com.android.layoutlib.api.IProjectCallback == false) {
throw new IllegalArgumentException("Project callback must be of type ILegacyCallback");
}
if (params.getResources() instanceof ResourceResolver == false) {
throw new IllegalArgumentException("RenderResources object must be of type ResourceResolver");
}
ResourceResolver resources = (ResourceResolver) params.getResources();
int apiLevel = getLegacyApiLevel();
// create a log wrapper since the older api requires a ILayoutLog
final LayoutLog log = params.getLog();
ILayoutLog logWrapper = new ILayoutLog() {
@Override
public void warning(String message) {
log.warning(null, message, null /*data*/);
}
@Override
public void error(Throwable t) {
log.error(null, "error!", t, null /*data*/);
}
@Override
public void error(String message) {
log.error(null, message, null /*data*/);
}
};
// convert the map of ResourceValue into IResourceValue. Super ugly but works.
Map<String, Map<String, IResourceValue>> projectMap = convertMap(
resources.getProjectResources());
Map<String, Map<String, IResourceValue>> frameworkMap = convertMap(
resources.getFrameworkResources());
ILayoutResult result = null;
if (apiLevel == 4) {
// Final ILayoutBridge API added support for "render full height"
result = mLegacyBridge.computeLayout(
(IXmlPullParser) params.getLayoutDescription(),
params.getProjectKey(),
params.getScreenWidth(), params.getScreenHeight(),
params.getRenderingMode() == RenderingMode.FULL_EXPAND ? true : false,
params.getDensity().getDpiValue(), params.getXdpi(), params.getYdpi(),
resources.getThemeName(), resources.isProjectTheme(),
projectMap, frameworkMap,
(IProjectCallback) params.getProjectCallback(),
logWrapper);
} else if (apiLevel == 3) {
// api 3 add density support.
result = mLegacyBridge.computeLayout(
(IXmlPullParser) params.getLayoutDescription(), params.getProjectKey(),
params.getScreenWidth(), params.getScreenHeight(),
params.getDensity().getDpiValue(), params.getXdpi(), params.getYdpi(),
resources.getThemeName(), resources.isProjectTheme(),
projectMap, frameworkMap,
(IProjectCallback) params.getProjectCallback(), logWrapper);
} else if (apiLevel == 2) {
// api 2 added boolean for separation of project/framework theme
result = mLegacyBridge.computeLayout(
(IXmlPullParser) params.getLayoutDescription(), params.getProjectKey(),
params.getScreenWidth(), params.getScreenHeight(),
resources.getThemeName(), resources.isProjectTheme(),
projectMap, frameworkMap,
(IProjectCallback) params.getProjectCallback(), logWrapper);
} else {
// First api with no density/dpi, and project theme boolean mixed
// into the theme name.
// change the string if it's a custom theme to make sure we can
// differentiate them
String themeName = resources.getThemeName();
if (resources.isProjectTheme()) {
themeName = "*" + themeName; //$NON-NLS-1$
}
result = mLegacyBridge.computeLayout(
(IXmlPullParser) params.getLayoutDescription(), params.getProjectKey(),
params.getScreenWidth(), params.getScreenHeight(),
themeName,
projectMap, frameworkMap,
(IProjectCallback) params.getProjectCallback(), logWrapper);
}
// clean up that is not done by the ILayoutBridge itself
legacyCleanUp();
return convertToScene(result);
}
@SuppressWarnings("unchecked")
private Map<String, Map<String, IResourceValue>> convertMap(
Map<ResourceType, Map<String, ResourceValue>> map) {
Map<String, Map<String, IResourceValue>> result =
new HashMap<String, Map<String, IResourceValue>>();
for (Entry<ResourceType, Map<String, ResourceValue>> entry : map.entrySet()) {
// ugly case but works.
result.put(entry.getKey().getName(),
(Map) entry.getValue());
}
return result;
}
/**
* Converts a {@link ILayoutResult} to a {@link RenderSession}.
*/
private RenderSession convertToScene(ILayoutResult result) {
Result sceneResult;
ViewInfo rootViewInfo = null;
if (result.getSuccess() == ILayoutResult.SUCCESS) {
sceneResult = Status.SUCCESS.createResult();
ILayoutViewInfo oldRootView = result.getRootView();
if (oldRootView != null) {
rootViewInfo = convertToViewInfo(oldRootView);
}
} else {
sceneResult = Status.ERROR_UNKNOWN.createResult(result.getErrorMessage());
}
// create a BasicLayoutScene. This will return the given values but return the default
// implementation for all method.
// ADT should gracefully handle the default implementations of LayoutScene
return new StaticRenderSession(sceneResult, rootViewInfo, result.getImage());
}
/**
* Converts a {@link ILayoutViewInfo} (and its children) to a {@link ViewInfo}.
*/
private ViewInfo convertToViewInfo(ILayoutViewInfo view) {
// create the view info.
ViewInfo viewInfo = new ViewInfo(view.getName(), view.getViewKey(),
view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
// then convert the children
ILayoutViewInfo[] children = view.getChildren();
if (children != null) {
ArrayList<ViewInfo> convertedChildren = new ArrayList<ViewInfo>(children.length);
for (ILayoutViewInfo child : children) {
convertedChildren.add(convertToViewInfo(child));
}
viewInfo.setChildren(convertedChildren);
}
return viewInfo;
}
/**
* Post rendering clean-up that must be done here because it's not done in any layoutlib using
* {@link ILayoutBridge}.
*/
private void legacyCleanUp() {
try {
Class<?> looperClass = mClassLoader.loadClass("android.os.Looper"); //$NON-NLS-1$
Field threadLocalField = looperClass.getField("sThreadLocal"); //$NON-NLS-1$
if (threadLocalField != null) {
threadLocalField.setAccessible(true);
// get object. Field is static so no need to pass an object
ThreadLocal<?> threadLocal = (ThreadLocal<?>) threadLocalField.get(null);
if (threadLocal != null) {
threadLocal.remove();
}
}
} catch (Exception e) {
// do nothing.
}
}
private Result getViewParentWithReflection(Object viewObject) {
// default implementation using reflection.
try {
if (mViewGetParentMethod == null) {
Class<?> viewClass = Class.forName("android.view.View");
mViewGetParentMethod = viewClass.getMethod("getParent");
}
return Status.SUCCESS.createResult(mViewGetParentMethod.invoke(viewObject));
} catch (Exception e) {
// Catch all for the reflection calls.
return ERROR_REFLECTION.createResult(null, e);
}
}
/**
* Utility method returning the index of a given view in its parent.
* @param viewObject the object for which to return the index.
*
* @return a {@link Result} indicating the status of the action, and if success, the index in
* the parent in {@link Result#getData()}
*/
private Result getViewIndexReflection(Object viewObject) {
// default implementation using reflection.
try {
Class<?> viewClass = Class.forName("android.view.View");
if (mViewGetParentMethod == null) {
mViewGetParentMethod = viewClass.getMethod("getParent");
}
Object parentObject = mViewGetParentMethod.invoke(viewObject);
if (mViewParentIndexOfChildMethod == null) {
Class<?> viewParentClass = Class.forName("android.view.ViewParent");
mViewParentIndexOfChildMethod = viewParentClass.getMethod("indexOfChild",
viewClass);
}
return Status.SUCCESS.createResult(
mViewParentIndexOfChildMethod.invoke(parentObject, viewObject));
} catch (Exception e) {
// Catch all for the reflection calls.
return ERROR_REFLECTION.createResult(null, e);
}
}
private void addExtendedViewInfo(ViewInfo info) {
computeExtendedViewInfo(info);
List<ViewInfo> children = info.getChildren();
for (ViewInfo child : children) {
addExtendedViewInfo(child);
}
}
private void computeExtendedViewInfo(ViewInfo info) {
Object viewObject = info.getViewObject();
Object params = info.getLayoutParamsObject();
int baseLine = getViewBaselineReflection(viewObject);
int leftMargin = 0;
int topMargin = 0;
int rightMargin = 0;
int bottomMargin = 0;
try {
if (mMarginLayoutParamClass == null) {
mMarginLayoutParamClass = Class.forName(
"android.view.ViewGroup$MarginLayoutParams");
mLeftMarginField = mMarginLayoutParamClass.getField("leftMargin");
mTopMarginField = mMarginLayoutParamClass.getField("topMargin");
mRightMarginField = mMarginLayoutParamClass.getField("rightMargin");
mBottomMarginField = mMarginLayoutParamClass.getField("bottomMargin");
}
if (mMarginLayoutParamClass.isAssignableFrom(params.getClass())) {
leftMargin = (Integer)mLeftMarginField.get(params);
topMargin = (Integer)mTopMarginField.get(params);
rightMargin = (Integer)mRightMarginField.get(params);
bottomMargin = (Integer)mBottomMarginField.get(params);
}
} catch (Exception e) {
// just use 'unknown' value.
leftMargin = Integer.MIN_VALUE;
topMargin = Integer.MIN_VALUE;
rightMargin = Integer.MIN_VALUE;
bottomMargin = Integer.MIN_VALUE;
}
info.setExtendedInfo(baseLine, leftMargin, topMargin, rightMargin, bottomMargin);
}
/**
* Utility method returning the baseline value for a given view object. This basically returns
* View.getBaseline().
*
* @param viewObject the object for which to return the index.
*
* @return the baseline value or -1 if not applicable to the view object or if this layout
* library does not implement this method.
*/
private int getViewBaselineReflection(Object viewObject) {
// default implementation using reflection.
try {
if (mViewGetBaselineMethod == null) {
Class<?> viewClass = Class.forName("android.view.View");
mViewGetBaselineMethod = viewClass.getMethod("getBaseline");
}
Object result = mViewGetBaselineMethod.invoke(viewObject);
if (result instanceof Integer) {
return ((Integer)result).intValue();
}
} catch (Exception e) {
// Catch all for the reflection calls.
}
return Integer.MIN_VALUE;
}
}