diff options
Diffstat (limited to 'tools/layoutlib')
82 files changed, 15216 insertions, 0 deletions
diff --git a/tools/layoutlib/Android.mk b/tools/layoutlib/Android.mk new file mode 100644 index 0000000..6d606a9 --- /dev/null +++ b/tools/layoutlib/Android.mk @@ -0,0 +1,67 @@ +# +# 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. +# +LOCAL_PATH := $(my-dir) +include $(CLEAR_VARS) + +# +# Define rules to build temp_layoutlib.jar, which contains a subset of +# the classes in framework.jar. The layoutlib_create tool is used to +# transform the framework jar into the temp_layoutlib jar. +# + +# We need to process the framework classes.jar file, but we can't +# depend directly on it (private vars won't be inherited correctly). +# So, we depend on framework's BUILT file. +built_framework_dep := \ + $(call intermediates-dir-for,JAVA_LIBRARIES,framework)/javalib.jar +built_framework_classes := \ + $(call intermediates-dir-for,JAVA_LIBRARIES,framework)/classes.jar + +built_core_dep := \ + $(call intermediates-dir-for,JAVA_LIBRARIES,core)/javalib.jar +built_core_classes := \ + $(call intermediates-dir-for,JAVA_LIBRARIES,core)/classes.jar + +built_layoutlib_create_jar := $(call intermediates-dir-for, \ + JAVA_LIBRARIES,layoutlib_create,HOST)/javalib.jar + +# This is mostly a copy of config/host_java_library.mk +LOCAL_MODULE := temp_layoutlib +LOCAL_MODULE_CLASS := JAVA_LIBRARIES +LOCAL_MODULE_SUFFIX := $(COMMON_JAVA_PACKAGE_SUFFIX) +LOCAL_IS_HOST_MODULE := true +LOCAL_BUILT_MODULE_STEM := javalib.jar + +####################################### +include $(BUILD_SYSTEM)/base_rules.mk +####################################### + +$(LOCAL_BUILT_MODULE): $(built_core_dep) \ + $(built_framework_dep) \ + $(built_layoutlib_create_jar) + @echo "host layoutlib_create: $@" + @mkdir -p $(dir $@) + @rm -f $@ + $(hide) java -jar $(built_layoutlib_create_jar) \ + $@ \ + $(built_core_classes) \ + $(built_framework_classes) + + +# +# Include the subdir makefiles. +# +include $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/tools/layoutlib/api/.classpath b/tools/layoutlib/api/.classpath new file mode 100644 index 0000000..a09ce5f --- /dev/null +++ b/tools/layoutlib/api/.classpath @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<classpath> + <classpathentry kind="src" path="src"/> + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/> + <classpathentry kind="var" path="ANDROID_SRC/prebuilt/common/kxml2/kxml2-2.3.0.jar" sourcepath="/ANDROID_SRC/dalvik/libcore/xml/src/main/java"/> + <classpathentry kind="output" path="bin"/> +</classpath> diff --git a/tools/layoutlib/api/.project b/tools/layoutlib/api/.project new file mode 100644 index 0000000..4e4ca3b --- /dev/null +++ b/tools/layoutlib/api/.project @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>layoutlib_api</name> + <comment></comment> + <projects> + </projects> + <buildSpec> + <buildCommand> + <name>org.eclipse.jdt.core.javabuilder</name> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>org.eclipse.jdt.core.javanature</nature> + </natures> +</projectDescription> diff --git a/tools/layoutlib/api/Android.mk b/tools/layoutlib/api/Android.mk new file mode 100644 index 0000000..d60987c --- /dev/null +++ b/tools/layoutlib/api/Android.mk @@ -0,0 +1,26 @@ +# +# 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. +# +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-java-files-under,src) + +LOCAL_JAVA_LIBRARIES := \ + kxml2-2.3.0 + +LOCAL_MODULE := layoutlib_api + +include $(BUILD_HOST_JAVA_LIBRARY) diff --git a/tools/layoutlib/api/src/com/android/layoutlib/api/ILayoutBridge.java b/tools/layoutlib/api/src/com/android/layoutlib/api/ILayoutBridge.java new file mode 100644 index 0000000..df1876d --- /dev/null +++ b/tools/layoutlib/api/src/com/android/layoutlib/api/ILayoutBridge.java @@ -0,0 +1,169 @@ +/* + * 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 com.android.layoutlib.api; + +import java.util.Map; + +/** + * Entry point of the Layout Lib. Implementations of this interface provide a method to compute + * and render a layout. + * <p/> + * <p/>{@link #getApiLevel()} gives the ability to know which methods are available. + * <p/> + * Changes in API level 3: + * <ul> + * <li>{@link #computeLayout(IXmlPullParser, Object, int, int, int, float, float, String, boolean, Map, Map, IProjectCallback, ILayoutLog)}</li> + * <li> deprecated {@link #computeLayout(IXmlPullParser, Object, int, int, String, boolean, Map, Map, IProjectCallback, ILayoutLog)}</li> + * </ul> + * Changes in API level 2: + * <ul> + * <li>{@link #getApiLevel()}</li> + * <li>{@link #computeLayout(IXmlPullParser, Object, int, int, String, boolean, Map, Map, IProjectCallback, ILayoutLog)}</li> + * <li>deprecated {@link #computeLayout(IXmlPullParser, Object, int, int, String, Map, Map, IProjectCallback, ILayoutLog)}</li> + * </ul> + */ +public interface ILayoutBridge { + + final int API_CURRENT = 3; + + /** + * Returns the API level of the layout library. + * While no methods will ever be removed, some may become deprecated, and some new ones + * will appear. + * <p/>If calling this method throws an {@link AbstractMethodError}, then the API level + * should be considered to be 1. + */ + int getApiLevel(); + + /** + * Initializes the Bridge object. + * @param fontOsLocation the location of the fonts. + * @param enumValueMap map attrName => { map enumFlagName => Integer value }. + * @return true if success. + * @since 1 + */ + boolean init(String fontOsLocation, Map<String, Map<String, Integer>> enumValueMap); + + /** + * Computes and renders a layout + * @param layoutDescription the {@link IXmlPullParser} letting the LayoutLib Bridge visit the + * layout file. + * @param projectKey An Object identifying the project. This is used for the cache mechanism. + * @param screenWidth the screen width + * @param screenHeight the screen height + * @param density the density factor for the screen. + * @param xdpi the screen actual dpi in X + * @param ydpi the screen actual dpi in Y + * @param themeName The name of the theme to use. + * @param isProjectTheme true if the theme is a project theme, false if it is a framework theme. + * @param projectResources the resources of the project. The map contains (String, map) pairs + * where the string is the type of the resource reference used in the layout file, and the + * map contains (String, {@link IResourceValue}) pairs where the key is the resource name, + * and the value is the resource value. + * @param frameworkResources the framework resources. The map contains (String, map) pairs + * where the string is the type of the resource reference used in the layout file, and the map + * contains (String, {@link IResourceValue}) pairs where the key is the resource name, and the + * value is the resource value. + * @param projectCallback The {@link IProjectCallback} object to get information from + * the project. + * @param logger the object responsible for displaying warning/errors to the user. + * @return an {@link ILayoutResult} object that contains the result of the layout. + * @since 3 + */ + ILayoutResult computeLayout(IXmlPullParser layoutDescription, + Object projectKey, + int screenWidth, int screenHeight, int density, float xdpi, float ydpi, + String themeName, boolean isProjectTheme, + Map<String, Map<String, IResourceValue>> projectResources, + Map<String, Map<String, IResourceValue>> frameworkResources, + IProjectCallback projectCallback, ILayoutLog logger); + + /** + * Computes and renders a layout + * @param layoutDescription the {@link IXmlPullParser} letting the LayoutLib Bridge visit the + * layout file. + * @param projectKey An Object identifying the project. This is used for the cache mechanism. + * @param screenWidth the screen width + * @param screenHeight the screen height + * @param themeName The name of the theme to use. + * @param isProjectTheme true if the theme is a project theme, false if it is a framework theme. + * @param projectResources the resources of the project. The map contains (String, map) pairs + * where the string is the type of the resource reference used in the layout file, and the + * map contains (String, {@link IResourceValue}) pairs where the key is the resource name, + * and the value is the resource value. + * @param frameworkResources the framework resources. The map contains (String, map) pairs + * where the string is the type of the resource reference used in the layout file, and the map + * contains (String, {@link IResourceValue}) pairs where the key is the resource name, and the + * value is the resource value. + * @param projectCallback The {@link IProjectCallback} object to get information from + * the project. + * @param logger the object responsible for displaying warning/errors to the user. + * @return an {@link ILayoutResult} object that contains the result of the layout. + * @deprecated Use {@link #computeLayout(IXmlPullParser, Object, int, int, int, float, float, String, boolean, Map, Map, IProjectCallback, ILayoutLog)} + * @since 2 + */ + @Deprecated + ILayoutResult computeLayout(IXmlPullParser layoutDescription, + Object projectKey, + int screenWidth, int screenHeight, String themeName, boolean isProjectTheme, + Map<String, Map<String, IResourceValue>> projectResources, + Map<String, Map<String, IResourceValue>> frameworkResources, + IProjectCallback projectCallback, ILayoutLog logger); + + /** + * Computes and renders a layout + * @param layoutDescription the {@link IXmlPullParser} letting the LayoutLib Bridge visit the + * layout file. + * @param projectKey An Object identifying the project. This is used for the cache mechanism. + * @param screenWidth + * @param screenHeight + * @param themeName The name of the theme to use. In order to differentiate project and platform + * themes sharing the same name, all project themes must be prepended with a '*' character. + * @param projectResources the resources of the project. The map contains (String, map) pairs + * where the string is the type of the resource reference used in the layout file, and the + * map contains (String, {@link IResourceValue}) pairs where the key is the resource name, + * and the value is the resource value. + * @param frameworkResources the framework resources. The map contains (String, map) pairs + * where the string is the type of the resource reference used in the layout file, and the map + * contains (String, {@link IResourceValue}) pairs where the key is the resource name, and the + * value is the resource value. + * @param projectCallback The {@link IProjectCallback} object to get information from + * the project. + * @param logger the object responsible for displaying warning/errors to the user. + * @return an {@link ILayoutResult} object that contains the result of the layout. + * @deprecated Use {@link #computeLayout(IXmlPullParser, Object, int, int, int, float, float, String, boolean, Map, Map, IProjectCallback, ILayoutLog)} + * @since 1 + */ + @Deprecated + ILayoutResult computeLayout(IXmlPullParser layoutDescription, + Object projectKey, + int screenWidth, int screenHeight, String themeName, + Map<String, Map<String, IResourceValue>> projectResources, + Map<String, Map<String, IResourceValue>> frameworkResources, + IProjectCallback projectCallback, ILayoutLog logger); + + /** + * 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 objectKey the key for the project. + * @since 1 + */ + void clearCaches(Object projectKey); +} diff --git a/tools/layoutlib/api/src/com/android/layoutlib/api/ILayoutLog.java b/tools/layoutlib/api/src/com/android/layoutlib/api/ILayoutLog.java new file mode 100644 index 0000000..cae15d3 --- /dev/null +++ b/tools/layoutlib/api/src/com/android/layoutlib/api/ILayoutLog.java @@ -0,0 +1,42 @@ +/* + * 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 com.android.layoutlib.api; + +/** + * Callback interface to display warnings/errors that happened during the computation and + * rendering of the layout. + */ +public interface ILayoutLog { + + /** + * Displays a warning message. + * @param message the message to display. + */ + void warning(String message); + + /** + * Displays an error message. + * @param message the message to display. + */ + void error(String message); + + /** + * Displays an exception + * @param t the {@link Throwable} to display. + */ + void error(Throwable t); +} diff --git a/tools/layoutlib/api/src/com/android/layoutlib/api/ILayoutResult.java b/tools/layoutlib/api/src/com/android/layoutlib/api/ILayoutResult.java new file mode 100644 index 0000000..5a06349 --- /dev/null +++ b/tools/layoutlib/api/src/com/android/layoutlib/api/ILayoutResult.java @@ -0,0 +1,97 @@ +/* + * 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 com.android.layoutlib.api; + +import java.awt.image.BufferedImage; + +/** + * The result of a layout computation through + * {@link ILayoutLibBridge#computeLayout(IXmlPullParser, int, int, String, java.util.Map, java.util.Map, java.util.Map, IFontLoader, ILayoutLibLog, ICustomViewLoader)} + */ +public interface ILayoutResult { + /** Sucess return code */ + final static int SUCCESS = 0; + /** Error return code. + * <p/>See {@link #getErrorMessage()} + */ + final static int ERROR = 1; + + /** + * Returns the result code. + * @see #SUCCESS + * @see #ERROR + */ + int getSuccess(); + + /** + * Returns the {@link ILayoutViewInfo} object for the top level view. + */ + ILayoutViewInfo getRootView(); + + /** + * Returns the rendering of the full layout. + */ + BufferedImage getImage(); + + /** + * Returns the error message. + * <p/>Only valid when {@link #getSuccess()} returns {@link #ERROR} + */ + String getErrorMessage(); + + /** + * Layout information for a specific view. + */ + public interface ILayoutViewInfo { + + /** + * Returns the list of children views. + */ + ILayoutViewInfo[] getChildren(); + + /** + * Returns the key associated with the node. + * @see IXmlPullParser#getViewKey() + */ + Object getViewKey(); + + /** + * Returns the name of the view. + */ + String getName(); + + /** + * Returns the left of the view bounds. + */ + int getLeft(); + + /** + * Returns the top of the view bounds. + */ + int getTop(); + + /** + * Returns the right of the view bounds. + */ + int getRight(); + + /** + * Returns the bottom of the view bounds. + */ + int getBottom(); + } +} diff --git a/tools/layoutlib/api/src/com/android/layoutlib/api/IProjectCallback.java b/tools/layoutlib/api/src/com/android/layoutlib/api/IProjectCallback.java new file mode 100644 index 0000000..5ad5082 --- /dev/null +++ b/tools/layoutlib/api/src/com/android/layoutlib/api/IProjectCallback.java @@ -0,0 +1,74 @@ +/* + * 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 com.android.layoutlib.api; + +/** + * Callback for project information needed by the Layout Library. + * Classes implementing this interface provide methods giving access to some project data, like + * resource resolution, namespace information, and instantiation of custom view. + */ +public interface IProjectCallback { + + /** + * Loads a custom view with the given constructor signature and arguments. + * @param name The fully qualified name of the class. + * @param constructorSignature The signature of the class to use + * @param constructorArgs The arguments to use on the constructor + * @return A newly instantiated android.view.View object. + * @throws ClassNotFoundException. + * @throws Exception + */ + @SuppressWarnings("unchecked") + Object loadView(String name, Class[] constructorSignature, Object[] constructorArgs) + throws ClassNotFoundException, Exception; + + /** + * Returns the namespace of the application. + * <p/>This lets the Layout Lib load custom attributes for custom views. + */ + String getNamespace(); + + /** + * Resolves the id of a resource Id. + * <p/>The resource id is the value of a <code>R.<type>.<name></code>, and + * this method will return both the type and name of the resource. + * @param id the Id to resolve. + * @return an array of 2 strings containing the resource name and type, or null if the id + * does not match any resource. + */ + String[] resolveResourceValue(int id); + + /** + * Resolves the id of a resource Id of type int[] + * <p/>The resource id is the value of a R.styleable.<name>, and this method will + * return the name of the resource. + * @param id the Id to resolve. + * @return the name of the resource or <code>null</code> if not found. + */ + String resolveResourceValue(int[] id); + + /** + * Returns the id of a resource. + * <p/>The provided type and name must match an existing constant defined as + * <code>R.<type>.<name></code>. + * @param type the type of the resource + * @param name the name of the resource + * @return an Integer containing the resource Id, or <code>null</code> if not found. + */ + Integer getResourceValue(String type, String name); + +} diff --git a/tools/layoutlib/api/src/com/android/layoutlib/api/IResourceValue.java b/tools/layoutlib/api/src/com/android/layoutlib/api/IResourceValue.java new file mode 100644 index 0000000..1da9508 --- /dev/null +++ b/tools/layoutlib/api/src/com/android/layoutlib/api/IResourceValue.java @@ -0,0 +1,44 @@ +/* + * 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 com.android.layoutlib.api; + +/** + * Represents an android resource with a name and a string value. + */ +public interface IResourceValue { + + /** + * Returns the type of the resource. For instance "drawable", "color", etc... + */ + String getType(); + + /** + * Returns the name of the resource, as defined in the XML. + */ + String getName(); + + /** + * Returns the value of the resource, as defined in the XML. This can be <code>null</code> + */ + String getValue(); + + /** + * Returns whether the resource is a framework resource (<code>true</code>) or a project + * resource (<code>false</false>). + */ + boolean isFramework(); +} diff --git a/tools/layoutlib/api/src/com/android/layoutlib/api/IStyleResourceValue.java b/tools/layoutlib/api/src/com/android/layoutlib/api/IStyleResourceValue.java new file mode 100644 index 0000000..2f17e69 --- /dev/null +++ b/tools/layoutlib/api/src/com/android/layoutlib/api/IStyleResourceValue.java @@ -0,0 +1,35 @@ +/* + * 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 com.android.layoutlib.api; + + +/** + * Represents an android style resources with a name and a list of children {@link IResourceValue}. + */ +public interface IStyleResourceValue extends IResourceValue { + + /** + * Returns the parent style name or <code>null</code> if unknown. + */ + String getParentStyle(); + + /** + * Find an item in the list by name + * @param name + */ + IResourceValue findItem(String name); +} diff --git a/tools/layoutlib/api/src/com/android/layoutlib/api/IXmlPullParser.java b/tools/layoutlib/api/src/com/android/layoutlib/api/IXmlPullParser.java new file mode 100644 index 0000000..cd43c56 --- /dev/null +++ b/tools/layoutlib/api/src/com/android/layoutlib/api/IXmlPullParser.java @@ -0,0 +1,36 @@ +/* + * 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 com.android.layoutlib.api; + +import com.android.layoutlib.api.ILayoutResult.ILayoutViewInfo; + +import org.xmlpull.v1.XmlPullParser; + +/** + * Extended version of {@link XmlPullParser} to use with + * {@link ILayoutLibBridge#computeLayout(XmlPullParser, int, int, String, java.util.Map, java.util.Map, java.util.Map, com.android.layoutlib.api.ILayoutLibBridge.IFontInfo)} + */ +public interface IXmlPullParser extends XmlPullParser { + + /** + * Returns a key for the current XML node. + * <p/>This key will be passed back in the {@link ILayoutViewInfo} objects, allowing association + * of a particular XML node with its result from the layout computation. + */ + Object getViewKey(); +} + diff --git a/tools/layoutlib/bridge/.classpath b/tools/layoutlib/bridge/.classpath new file mode 100644 index 0000000..175a98b --- /dev/null +++ b/tools/layoutlib/bridge/.classpath @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<classpath> + <classpathentry excluding="org/kxml2/io/" kind="src" path="src"/> + <classpathentry kind="src" path="tests"/> + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/> + <classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/3"/> + <classpathentry combineaccessrules="false" kind="src" path="/layoutlib_api"/> + <classpathentry kind="var" path="ANDROID_SRC/prebuilt/common/kxml2/kxml2-2.3.0.jar" sourcepath="/ANDROID_SRC/dalvik/libcore/xml/src/main/java"/> + <classpathentry kind="var" path="ANDROID_OUT_FRAMEWORK/layoutlib.jar" sourcepath="/ANDROID_SRC/frameworks/base/core/java"/> + <classpathentry kind="var" path="ANDROID_OUT_FRAMEWORK/ninepatch.jar" sourcepath="/ANDROID_SRC/development/tools/ninepatch/src"/> + <classpathentry kind="output" path="bin"/> +</classpath> diff --git a/tools/layoutlib/bridge/.project b/tools/layoutlib/bridge/.project new file mode 100644 index 0000000..e36e71b --- /dev/null +++ b/tools/layoutlib/bridge/.project @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>layoutlib_bridge</name> + <comment></comment> + <projects> + </projects> + <buildSpec> + <buildCommand> + <name>org.eclipse.jdt.core.javabuilder</name> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>org.eclipse.jdt.core.javanature</nature> + </natures> +</projectDescription> diff --git a/tools/layoutlib/bridge/Android.mk b/tools/layoutlib/bridge/Android.mk new file mode 100644 index 0000000..b2010d5 --- /dev/null +++ b/tools/layoutlib/bridge/Android.mk @@ -0,0 +1,31 @@ +# +# 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. +# +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-java-files-under,src) + +LOCAL_JAVA_LIBRARIES := \ + kxml2-2.3.0 \ + layoutlib_api \ + ninepatch + +LOCAL_STATIC_JAVA_LIBRARIES := temp_layoutlib + +LOCAL_MODULE := layoutlib + +include $(BUILD_HOST_JAVA_LIBRARY) + diff --git a/tools/layoutlib/bridge/src/android/graphics/Bitmap.java b/tools/layoutlib/bridge/src/android/graphics/Bitmap.java new file mode 100644 index 0000000..6bc01b1 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/Bitmap.java @@ -0,0 +1,242 @@ +/* + * 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.graphics; + +import com.android.layoutlib.bridge.BridgeCanvas; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; + +import javax.imageio.ImageIO; + +public final class Bitmap extends _Original_Bitmap { + + private BufferedImage mImage; + + public Bitmap(File input) throws IOException { + super(1, true, null); + + mImage = ImageIO.read(input); + } + + Bitmap(BufferedImage image) { + super(1, true, null); + mImage = image; + } + + public BufferedImage getImage() { + return mImage; + } + + // ----- overriden methods + + public enum Config { + // these native values must match up with the enum in SkBitmap.h + ALPHA_8 (2), + RGB_565 (4), + ARGB_4444 (5), + ARGB_8888 (6); + + Config(int ni) { + this.nativeInt = ni; + } + final int nativeInt; + + /* package */ static Config nativeToConfig(int ni) { + return sConfigs[ni]; + } + + private static Config sConfigs[] = { + null, null, ALPHA_8, null, RGB_565, ARGB_4444, ARGB_8888 + }; + } + + + @Override + public int getWidth() { + return mImage.getWidth(); + } + + @Override + public int getHeight() { + return mImage.getHeight(); + } + + /** + * Returns an immutable bitmap from the source bitmap. The new bitmap may + * be the same object as source, or a copy may have been made. + */ + public static Bitmap createBitmap(Bitmap src) { + return createBitmap(src, 0, 0, src.getWidth(), src.getHeight(), null, false); + } + + /** + * Returns an immutable bitmap from the specified subset of the source + * bitmap. The new bitmap may be the same object as source, or a copy may + * have been made. + * + * @param source The bitmap we are subsetting + * @param x The x coordinate of the first pixel in source + * @param y The y coordinate of the first pixel in source + * @param width The number of pixels in each row + * @param height The number of rows + */ + public static Bitmap createBitmap(Bitmap source, int x, int y, + int width, int height) { + return new Bitmap(source.mImage.getSubimage(x, y, width, height)); + } + + /** + * Returns an immutable bitmap from subset of the source bitmap, + * transformed by the optional matrix. + * + * @param source The bitmap we are subsetting + * @param x The x coordinate of the first pixel in source + * @param y The y coordinate of the first pixel in source + * @param width The number of pixels in each row + * @param height The number of rows + * @param m Option matrix to be applied to the pixels + * @param filter true if the source should be filtered. + * Only applies if the matrix contains more than just + * translation. + * @return A bitmap that represents the specified subset of source + * @throws IllegalArgumentException if the x, y, width, height values are + * outside of the dimensions of the source bitmap. + */ + public static Bitmap createBitmap(Bitmap source, int x, int y, int width, + int height, Matrix m, boolean filter) { + checkXYSign(x, y); + checkWidthHeight(width, height); + if (x + width > source.getWidth()) { + throw new IllegalArgumentException( + "x + width must be <= bitmap.width()"); + } + if (y + height > source.getHeight()) { + throw new IllegalArgumentException( + "y + height must be <= bitmap.height()"); + } + + // check if we can just return our argument unchanged + if (!source.isMutable() && x == 0 && y == 0 + && width == source.getWidth() && height == source.getHeight() + && (m == null || m.isIdentity())) { + return source; + } + + if (m == null || m.isIdentity()) { + return new Bitmap(source.mImage.getSubimage(x, y, width, height)); + } + + int neww = width; + int newh = height; + Paint paint; + + Rect srcR = new Rect(x, y, x + width, y + height); + RectF dstR = new RectF(0, 0, width, height); + + /* the dst should have alpha if the src does, or if our matrix + doesn't preserve rectness + */ + boolean hasAlpha = source.hasAlpha() || !m.rectStaysRect(); + RectF deviceR = new RectF(); + m.mapRect(deviceR, dstR); + neww = Math.round(deviceR.width()); + newh = Math.round(deviceR.height()); + + BridgeCanvas canvas = new BridgeCanvas(neww, newh); + + canvas.translate(-deviceR.left, -deviceR.top); + canvas.concat(m); + paint = new Paint(); + paint.setFilterBitmap(filter); + if (!m.rectStaysRect()) { + paint.setAntiAlias(true); + } + + canvas.drawBitmap(source, srcR, dstR, paint); + + return new Bitmap(canvas.getImage()); + } + + /** + * Returns a mutable bitmap with the specified width and height. + * + * @param width The width of the bitmap + * @param height The height of the bitmap + * @param config The bitmap config to create. + * @throws IllegalArgumentException if the width or height are <= 0 + */ + public static Bitmap createBitmap(int width, int height, Config config) { + return new Bitmap(new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB)); + } + + /** + * Returns a immutable bitmap with the specified width and height, with each + * pixel value set to the corresponding value in the colors array. + * + * @param colors Array of {@link Color} used to initialize the pixels. + * @param offset Number of values to skip before the first color in the + * array of colors. + * @param stride Number of colors in the array between rows (must be >= + * width or <= -width). + * @param width The width of the bitmap + * @param height The height of the bitmap + * @param config The bitmap config to create. If the config does not + * support per-pixel alpha (e.g. RGB_565), then the alpha + * bytes in the colors[] will be ignored (assumed to be FF) + * @throws IllegalArgumentException if the width or height are <= 0, or if + * the color array's length is less than the number of pixels. + */ + public static Bitmap createBitmap(int colors[], int offset, int stride, + int width, int height, Config config) { + checkWidthHeight(width, height); + if (Math.abs(stride) < width) { + throw new IllegalArgumentException("abs(stride) must be >= width"); + } + int lastScanline = offset + (height - 1) * stride; + int length = colors.length; + if (offset < 0 || (offset + width > length) + || lastScanline < 0 + || (lastScanline + width > length)) { + throw new ArrayIndexOutOfBoundsException(); + } + + // TODO: create an immutable bitmap... + throw new UnsupportedOperationException(); + } + + /** + * Returns a immutable bitmap with the specified width and height, with each + * pixel value set to the corresponding value in the colors array. + * + * @param colors Array of {@link Color} used to initialize the pixels. + * This array must be at least as large as width * height. + * @param width The width of the bitmap + * @param height The height of the bitmap + * @param config The bitmap config to create. If the config does not + * support per-pixel alpha (e.g. RGB_565), then the alpha + * bytes in the colors[] will be ignored (assumed to be FF) + * @throws IllegalArgumentException if the width or height are <= 0, or if + * the color array's length is less than the number of pixels. + */ + public static Bitmap createBitmap(int colors[], int width, int height, + Config config) { + return createBitmap(colors, 0, width, width, height, config); + } + +} diff --git a/tools/layoutlib/bridge/src/android/graphics/BitmapShader.java b/tools/layoutlib/bridge/src/android/graphics/BitmapShader.java new file mode 100644 index 0000000..8bf7fcc --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/BitmapShader.java @@ -0,0 +1,41 @@ +/* + * 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.graphics; + +public class BitmapShader extends Shader { + + // we hold on just for the GC, since our native counterpart is using it + private final Bitmap mBitmap; + + /** + * Call this to create a new shader that will draw with a bitmap. + * + * @param bitmap The bitmap to use inside the shader + * @param tileX The tiling mode for x to draw the bitmap in. + * @param tileY The tiling mode for y to draw the bitmap in. + */ + public BitmapShader(Bitmap bitmap, TileMode tileX, TileMode tileY) { + mBitmap = bitmap; + } + + //---------- Custom methods + + public Bitmap getBitmap() { + return mBitmap; + } +} + diff --git a/tools/layoutlib/bridge/src/android/graphics/ComposeShader.java b/tools/layoutlib/bridge/src/android/graphics/ComposeShader.java new file mode 100644 index 0000000..968a597 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/ComposeShader.java @@ -0,0 +1,46 @@ +/* + * 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.graphics; + +/** A subclass of shader that returns the composition of two other shaders, combined by + an {@link android.graphics.Xfermode} subclass. +*/ +public class ComposeShader extends Shader { + /** Create a new compose shader, given shaders A, B, and a combining mode. + When the mode is applied, it will be given the result from shader A as its + "dst", and the result of from shader B as its "src". + @param shaderA The colors from this shader are seen as the "dst" by the mode + @param shaderB The colors from this shader are seen as the "src" by the mode + @param mode The mode that combines the colors from the two shaders. If mode + is null, then SRC_OVER is assumed. + */ + public ComposeShader(Shader shaderA, Shader shaderB, Xfermode mode) { + // FIXME Implement shader + } + + /** Create a new compose shader, given shaders A, B, and a combining PorterDuff mode. + When the mode is applied, it will be given the result from shader A as its + "dst", and the result of from shader B as its "src". + @param shaderA The colors from this shader are seen as the "dst" by the mode + @param shaderB The colors from this shader are seen as the "src" by the mode + @param mode The PorterDuff mode that combines the colors from the two shaders. + */ + public ComposeShader(Shader shaderA, Shader shaderB, PorterDuff.Mode mode) { + // FIXME Implement shader + } +} + diff --git a/tools/layoutlib/bridge/src/android/graphics/LinearGradient.java b/tools/layoutlib/bridge/src/android/graphics/LinearGradient.java new file mode 100644 index 0000000..1a0dc05 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/LinearGradient.java @@ -0,0 +1,71 @@ +/* + * 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.graphics; + +import java.awt.GradientPaint; +import java.awt.Color; +import java.awt.Paint; + +public class LinearGradient extends Shader { + + private GradientPaint mGradientPaint; + + /** Create a shader that draws a linear gradient along a line. + @param x0 The x-coordinate for the start of the gradient line + @param y0 The y-coordinate for the start of the gradient line + @param x1 The x-coordinate for the end of the gradient line + @param y1 The y-coordinate for the end of the gradient line + @param colors The colors to be distributed along the gradient line + @param positions May be null. The relative positions [0..1] of + each corresponding color in the colors array. If this is null, + the the colors are distributed evenly along the gradient line. + @param tile The Shader tiling mode + */ + public LinearGradient(float x0, float y0, float x1, float y1, + int colors[], float positions[], TileMode tile) { + if (colors.length < 2) { + throw new IllegalArgumentException("needs >= 2 number of colors"); + } + if (positions != null && colors.length != positions.length) { + throw new IllegalArgumentException("color and position arrays must be of equal length"); + } + + // FIXME implement multi color linear gradient + } + + /** Create a shader that draws a linear gradient along a line. + @param x0 The x-coordinate for the start of the gradient line + @param y0 The y-coordinate for the start of the gradient line + @param x1 The x-coordinate for the end of the gradient line + @param y1 The y-coordinate for the end of the gradient line + @param color0 The color at the start of the gradient line. + @param color1 The color at the end of the gradient line. + @param tile The Shader tiling mode + */ + public LinearGradient(float x0, float y0, float x1, float y1, + int color0, int color1, TileMode tile) { + mGradientPaint = new GradientPaint(x0, y0, new Color(color0, true /* hasalpha */), + x1,y1, new Color(color1, true /* hasalpha */), tile != TileMode.CLAMP); + } + + //---------- Custom Methods + + public Paint getPaint() { + return mGradientPaint; + } +} + diff --git a/tools/layoutlib/bridge/src/android/graphics/Matrix.java b/tools/layoutlib/bridge/src/android/graphics/Matrix.java new file mode 100644 index 0000000..3f9a993 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/Matrix.java @@ -0,0 +1,984 @@ +/* + * 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.graphics; + +import java.awt.geom.AffineTransform; + + +/** + * A matrix implementation overridden by the LayoutLib bridge. + */ +public class Matrix extends _Original_Matrix { + + float mValues[] = new float[9]; + + /** + * Create an identity matrix + */ + public Matrix() { + reset(); + } + + /** + * Create a matrix that is a (deep) copy of src + * @param src The matrix to copy into this matrix + */ + public Matrix(Matrix src) { + set(src); + } + + /** + * Creates a Matrix object from the float array. The array becomes the internal storage + * of the object. + * @param data + */ + private Matrix(float[] data) { + assert data.length != 9; + mValues = data; + } + + @Override + public void finalize() throws Throwable { + // pass + } + + //---------- Custom Methods + + /** + * Adds the given transformation to the current Matrix + * <p/>This in effect does this = this*matrix + * @param matrix + */ + private void addTransform(float[] matrix) { + float[] tmp = new float[9]; + + // first row + tmp[0] = matrix[0] * mValues[0] + matrix[1] * mValues[3] + matrix[2] * mValues[6]; + tmp[1] = matrix[0] * mValues[1] + matrix[1] * mValues[4] + matrix[2] * mValues[7]; + tmp[2] = matrix[0] * mValues[2] + matrix[1] * mValues[5] + matrix[2] * mValues[8]; + + // 2nd row + tmp[3] = matrix[3] * mValues[0] + matrix[4] * mValues[3] + matrix[5] * mValues[6]; + tmp[4] = matrix[3] * mValues[1] + matrix[4] * mValues[4] + matrix[5] * mValues[7]; + tmp[5] = matrix[3] * mValues[2] + matrix[4] * mValues[5] + matrix[5] * mValues[8]; + + // 3rd row + tmp[6] = matrix[6] * mValues[0] + matrix[7] * mValues[3] + matrix[8] * mValues[6]; + tmp[7] = matrix[6] * mValues[1] + matrix[7] * mValues[4] + matrix[8] * mValues[7]; + tmp[8] = matrix[6] * mValues[2] + matrix[7] * mValues[5] + matrix[8] * mValues[8]; + + // copy the result over to mValues + mValues = tmp; + } + + public AffineTransform getTransform() { + return new AffineTransform(mValues[0], mValues[1], mValues[2], + mValues[3], mValues[4], mValues[5]); + } + + public boolean hasPerspective() { + return (mValues[6] != 0 || mValues[7] != 0 || mValues[8] != 1); + } + + //---------- + + /** + * Returns true if the matrix is identity. + * This maybe faster than testing if (getType() == 0) + */ + @Override + public boolean isIdentity() { + for (int i = 0, k = 0; i < 3; i++) { + for (int j = 0; j < 3; j++, k++) { + if (mValues[k] != ((i==j) ? 1 : 0)) { + return false; + } + } + } + + return true; + } + + /** + * Returns true if will map a rectangle to another rectangle. This can be + * true if the matrix is identity, scale-only, or rotates a multiple of 90 + * degrees. + */ + @Override + public boolean rectStaysRect() { + return (computeTypeMask() & kRectStaysRect_Mask) != 0; + } + + /** + * (deep) copy the src matrix into this matrix. If src is null, reset this + * matrix to the identity matrix. + */ + public void set(Matrix src) { + if (src == null) { + reset(); + } else { + System.arraycopy(src.mValues, 0, mValues, 0, mValues.length); + } + } + + @Override + public void set(_Original_Matrix src) { + throw new UnsupportedOperationException("CALL TO PARENT FORBIDDEN"); + } + + /** Returns true if obj is a Matrix and its values equal our values. + */ + @Override + public boolean equals(Object obj) { + if (obj != null && obj instanceof Matrix) { + Matrix matrix = (Matrix)obj; + for (int i = 0 ; i < 9 ; i++) { + if (mValues[i] != matrix.mValues[i]) { + return false; + } + } + + return true; + } + + return false; + } + + /** Set the matrix to identity */ + @Override + public void reset() { + for (int i = 0, k = 0; i < 3; i++) { + for (int j = 0; j < 3; j++, k++) { + mValues[k] = ((i==j) ? 1 : 0); + } + } + } + + /** Set the matrix to translate by (dx, dy). */ + @Override + public void setTranslate(float dx, float dy) { + mValues[0] = 1; + mValues[1] = 0; + mValues[2] = dx; + mValues[3] = 0; + mValues[4] = 1; + mValues[5] = dy; + mValues[6] = 0; + mValues[7] = 0; + mValues[7] = 1; + } + + /** + * Set the matrix to scale by sx and sy, with a pivot point at (px, py). + * The pivot point is the coordinate that should remain unchanged by the + * specified transformation. + */ + @Override + public void setScale(float sx, float sy, float px, float py) { + // TODO: do it in one pass + + // translate so that the pivot is in 0,0 + mValues[0] = 1; + mValues[1] = 0; + mValues[2] = -px; + mValues[3] = 0; + mValues[4] = 1; + mValues[5] = -py; + mValues[6] = 0; + mValues[7] = 0; + mValues[7] = 1; + + // scale + addTransform(new float[] { sx, 0, 0, 0, sy, 0, 0, 0, 1 }); + // translate back the pivot + addTransform(new float[] { 1, 0, px, 0, 1, py, 0, 0, 1 }); + } + + /** Set the matrix to scale by sx and sy. */ + @Override + public void setScale(float sx, float sy) { + mValues[0] = sx; + mValues[1] = 0; + mValues[2] = 0; + mValues[3] = 0; + mValues[4] = sy; + mValues[5] = 0; + mValues[6] = 0; + mValues[7] = 0; + mValues[7] = 1; + } + + /** + * Set the matrix to rotate by the specified number of degrees, with a pivot + * point at (px, py). The pivot point is the coordinate that should remain + * unchanged by the specified transformation. + */ + @Override + public void setRotate(float degrees, float px, float py) { + // TODO: do it in one pass + + // translate so that the pivot is in 0,0 + mValues[0] = 1; + mValues[1] = 0; + mValues[2] = -px; + mValues[3] = 0; + mValues[4] = 1; + mValues[5] = -py; + mValues[6] = 0; + mValues[7] = 0; + mValues[7] = 1; + + // scale + double rad = Math.toRadians(degrees); + float cos = (float)Math.cos(rad); + float sin = (float)Math.sin(rad); + addTransform(new float[] { cos, -sin, 0, sin, cos, 0, 0, 0, 1 }); + // translate back the pivot + addTransform(new float[] { 1, 0, px, 0, 1, py, 0, 0, 1 }); + } + + /** + * Set the matrix to rotate about (0,0) by the specified number of degrees. + */ + @Override + public void setRotate(float degrees) { + double rad = Math.toRadians(degrees); + float cos = (float)Math.cos(rad); + float sin = (float)Math.sin(rad); + + mValues[0] = cos; + mValues[1] = -sin; + mValues[2] = 0; + mValues[3] = sin; + mValues[4] = cos; + mValues[5] = 0; + mValues[6] = 0; + mValues[7] = 0; + mValues[7] = 1; + } + + /** + * Set the matrix to rotate by the specified sine and cosine values, with a + * pivot point at (px, py). The pivot point is the coordinate that should + * remain unchanged by the specified transformation. + */ + @Override + public void setSinCos(float sinValue, float cosValue, float px, float py) { + // TODO: do it in one pass + + // translate so that the pivot is in 0,0 + mValues[0] = 1; + mValues[1] = 0; + mValues[2] = -px; + mValues[3] = 0; + mValues[4] = 1; + mValues[5] = -py; + mValues[6] = 0; + mValues[7] = 0; + mValues[7] = 1; + + // scale + addTransform(new float[] { cosValue, -sinValue, 0, sinValue, cosValue, 0, 0, 0, 1 }); + // translate back the pivot + addTransform(new float[] { 1, 0, px, 0, 1, py, 0, 0, 1 }); + } + + /** Set the matrix to rotate by the specified sine and cosine values. */ + @Override + public void setSinCos(float sinValue, float cosValue) { + mValues[0] = cosValue; + mValues[1] = -sinValue; + mValues[2] = 0; + mValues[3] = sinValue; + mValues[4] = cosValue; + mValues[5] = 0; + mValues[6] = 0; + mValues[7] = 0; + mValues[7] = 1; + } + + /** + * Set the matrix to skew by sx and sy, with a pivot point at (px, py). + * The pivot point is the coordinate that should remain unchanged by the + * specified transformation. + */ + @Override + public void setSkew(float kx, float ky, float px, float py) { + // TODO: do it in one pass + + // translate so that the pivot is in 0,0 + mValues[0] = 1; + mValues[1] = 0; + mValues[2] = -px; + mValues[3] = 0; + mValues[4] = 1; + mValues[5] = -py; + mValues[6] = 0; + mValues[7] = 0; + mValues[7] = 1; + + // scale + addTransform(new float[] { 1, kx, 0, ky, 1, 0, 0, 0, 1 }); + // translate back the pivot + addTransform(new float[] { 1, 0, px, 0, 1, py, 0, 0, 1 }); + } + + /** Set the matrix to skew by sx and sy. */ + @Override + public void setSkew(float kx, float ky) { + mValues[0] = 1; + mValues[1] = kx; + mValues[2] = -0; + mValues[3] = ky; + mValues[4] = 1; + mValues[5] = 0; + mValues[6] = 0; + mValues[7] = 0; + mValues[7] = 1; + } + + /** + * Set the matrix to the concatenation of the two specified matrices, + * returning true if the the result can be represented. Either of the two + * matrices may also be the target matrix. this = a * b + */ + public boolean setConcat(Matrix a, Matrix b) { + if (a == this) { + preConcat(b); + } else if (b == this) { + postConcat(b); + } else { + Matrix tmp = new Matrix(b); + tmp.addTransform(a.mValues); + set(tmp); + } + + return true; + } + + @Override + public boolean setConcat(_Original_Matrix a, _Original_Matrix b) { + throw new UnsupportedOperationException("CALL TO PARENT FORBIDDEN"); + } + + /** + * Preconcats the matrix with the specified translation. + * M' = M * T(dx, dy) + */ + @Override + public boolean preTranslate(float dx, float dy) { + // create a matrix that will be multiply by this + Matrix m = new Matrix(new float[] { 1, 0, dx, 0, 1, dy, 0, 0, 1 }); + m.addTransform(this.mValues); + + System.arraycopy(m.mValues, 0, mValues, 0, 9); + return true; + } + + /** + * Preconcats the matrix with the specified scale. + * M' = M * S(sx, sy, px, py) + */ + @Override + public boolean preScale(float sx, float sy, float px, float py) { + Matrix m = new Matrix(); + m.setScale(sx, sy, px, py); + m.addTransform(mValues); + set(m); + + return true; + } + + /** + * Preconcats the matrix with the specified scale. + * M' = M * S(sx, sy) + */ + @Override + public boolean preScale(float sx, float sy) { + Matrix m = new Matrix(); + m.setScale(sx, sy); + m.addTransform(mValues); + set(m); + + return true; + } + + /** + * Preconcats the matrix with the specified rotation. + * M' = M * R(degrees, px, py) + */ + @Override + public boolean preRotate(float degrees, float px, float py) { + Matrix m = new Matrix(); + m.setRotate(degrees, px, py); + m.addTransform(mValues); + set(m); + + return true; + } + + /** + * Preconcats the matrix with the specified rotation. + * M' = M * R(degrees) + */ + @Override + public boolean preRotate(float degrees) { + Matrix m = new Matrix(); + m.setRotate(degrees); + m.addTransform(mValues); + set(m); + + return true; + } + + /** + * Preconcats the matrix with the specified skew. + * M' = M * K(kx, ky, px, py) + */ + @Override + public boolean preSkew(float kx, float ky, float px, float py) { + Matrix m = new Matrix(); + m.setSkew(kx, ky, px, py); + m.addTransform(mValues); + set(m); + + return true; + } + + /** + * Preconcats the matrix with the specified skew. + * M' = M * K(kx, ky) + */ + @Override + public boolean preSkew(float kx, float ky) { + Matrix m = new Matrix(); + m.setSkew(kx, ky); + m.addTransform(mValues); + set(m); + + return true; + } + + /** + * Preconcats the matrix with the specified matrix. + * M' = M * other + */ + public boolean preConcat(Matrix other) { + Matrix m = new Matrix(other); + other.addTransform(mValues); + set(m); + + return true; + } + + @Override + public boolean preConcat(_Original_Matrix other) { + throw new UnsupportedOperationException("CALL TO PARENT FORBIDDEN"); + } + + /** + * Postconcats the matrix with the specified translation. + * M' = T(dx, dy) * M + */ + @Override + public boolean postTranslate(float dx, float dy) { + addTransform(new float[] { 1, 0, dx, 0, 1, dy, 0, 0, 1 }); + return true; + } + + /** + * Postconcats the matrix with the specified scale. + * M' = S(sx, sy, px, py) * M + */ + @Override + public boolean postScale(float sx, float sy, float px, float py) { + // TODO: do it in one pass + // translate so that the pivot is in 0,0 + addTransform(new float[] { 1, 0, -px, 0, 1, py, 0, 0, 1 }); + // scale + addTransform(new float[] { sx, 0, 0, 0, sy, 0, 0, 0, 1 }); + // translate back the pivot + addTransform(new float[] { 1, 0, px, 0, 1, py, 0, 0, 1 }); + + return true; + } + + /** + * Postconcats the matrix with the specified scale. + * M' = S(sx, sy) * M + */ + @Override + public boolean postScale(float sx, float sy) { + addTransform(new float[] { sx, 0, 0, 0, sy, 0, 0, 0, 1 }); + return true; + } + + /** + * Postconcats the matrix with the specified rotation. + * M' = R(degrees, px, py) * M + */ + @Override + public boolean postRotate(float degrees, float px, float py) { + // TODO: do it in one pass + // translate so that the pivot is in 0,0 + addTransform(new float[] { 1, 0, -px, 0, 1, py, 0, 0, 1 }); + // scale + double rad = Math.toRadians(degrees); + float cos = (float)Math.cos(rad); + float sin = (float)Math.sin(rad); + addTransform(new float[] { cos, -sin, 0, sin, cos, 0, 0, 0, 1 }); + // translate back the pivot + addTransform(new float[] { 1, 0, px, 0, 1, py, 0, 0, 1 }); + + return true; + } + + /** + * Postconcats the matrix with the specified rotation. + * M' = R(degrees) * M + */ + @Override + public boolean postRotate(float degrees) { + double rad = Math.toRadians(degrees); + float cos = (float)Math.cos(rad); + float sin = (float)Math.sin(rad); + addTransform(new float[] { cos, -sin, 0, sin, cos, 0, 0, 0, 1 }); + + return true; + } + + /** + * Postconcats the matrix with the specified skew. + * M' = K(kx, ky, px, py) * M + */ + @Override + public boolean postSkew(float kx, float ky, float px, float py) { + // TODO: do it in one pass + // translate so that the pivot is in 0,0 + addTransform(new float[] { 1, 0, -px, 0, 1, py, 0, 0, 1 }); + // scale + addTransform(new float[] { 1, kx, 0, ky, 1, 0, 0, 0, 1 }); + // translate back the pivot + addTransform(new float[] { 1, 0, px, 0, 1, py, 0, 0, 1 }); + + return true; + } + + /** + * Postconcats the matrix with the specified skew. + * M' = K(kx, ky) * M + */ + @Override + public boolean postSkew(float kx, float ky) { + addTransform(new float[] { 1, kx, 0, ky, 1, 0, 0, 0, 1 }); + + return true; + } + + /** + * Postconcats the matrix with the specified matrix. + * M' = other * M + */ + public boolean postConcat(Matrix other) { + addTransform(other.mValues); + + return true; + } + + @Override + public boolean postConcat(_Original_Matrix other) { + throw new UnsupportedOperationException("CALL TO PARENT FORBIDDEN"); + } + + /** Controlls how the src rect should align into the dst rect for + setRectToRect(). + */ + public enum ScaleToFit { + /** + * Scale in X and Y independently, so that src matches dst exactly. + * This may change the aspect ratio of the src. + */ + FILL (0), + /** + * Compute a scale that will maintain the original src aspect ratio, + * but will also ensure that src fits entirely inside dst. At least one + * axis (X or Y) will fit exactly. START aligns the result to the + * left and top edges of dst. + */ + START (1), + /** + * Compute a scale that will maintain the original src aspect ratio, + * but will also ensure that src fits entirely inside dst. At least one + * axis (X or Y) will fit exactly. The result is centered inside dst. + */ + CENTER (2), + /** + * Compute a scale that will maintain the original src aspect ratio, + * but will also ensure that src fits entirely inside dst. At least one + * axis (X or Y) will fit exactly. END aligns the result to the + * right and bottom edges of dst. + */ + END (3); + + // the native values must match those in SkMatrix.h + ScaleToFit(int nativeInt) { + this.nativeInt = nativeInt; + } + final int nativeInt; + } + + /** + * Set the matrix to the scale and translate values that map the source + * rectangle to the destination rectangle, returning true if the result + * can be represented. + * + * @param src the source rectangle to map from. + * @param dst the destination rectangle to map to. + * @param stf the ScaleToFit option + * @return true if the matrix can be represented by the rectangle mapping. + */ + public boolean setRectToRect(RectF src, RectF dst, ScaleToFit stf) { + if (dst == null || src == null) { + throw new NullPointerException(); + } + + if (src.isEmpty()) { + reset(); + return false; + } + + if (dst.isEmpty()) { + mValues[0] = mValues[1] = mValues[2] = mValues[3] = mValues[4] = mValues[5] + = mValues[6] = mValues[7] = 0; + mValues[8] = 1; + } else { + float tx, sx = dst.width() / src.width(); + float ty, sy = dst.height() / src.height(); + boolean xLarger = false; + + if (stf != ScaleToFit.FILL) { + if (sx > sy) { + xLarger = true; + sx = sy; + } else { + sy = sx; + } + } + + tx = dst.left - src.left * sx; + ty = dst.top - src.top * sy; + if (stf == ScaleToFit.CENTER || stf == ScaleToFit.END) { + float diff; + + if (xLarger) { + diff = dst.width() - src.width() * sy; + } else { + diff = dst.height() - src.height() * sy; + } + + if (stf == ScaleToFit.CENTER) { + diff = diff / 2; + } + + if (xLarger) { + tx += diff; + } else { + ty += diff; + } + } + + mValues[0] = sx; + mValues[4] = sy; + mValues[2] = tx; + mValues[5] = ty; + mValues[1] = mValues[3] = mValues[6] = mValues[7] = 0; + + } + // shared cleanup + mValues[8] = 1; + return true; + } + + @Override + public boolean setRectToRect(RectF src, RectF dst, _Original_Matrix.ScaleToFit stf) { + throw new UnsupportedOperationException("CALL TO PARENT FORBIDDEN"); + } + + /** + * Set the matrix such that the specified src points would map to the + * specified dst points. The "points" are represented as an array of floats, + * order [x0, y0, x1, y1, ...], where each "point" is 2 float values. + * + * @param src The array of src [x,y] pairs (points) + * @param srcIndex Index of the first pair of src values + * @param dst The array of dst [x,y] pairs (points) + * @param dstIndex Index of the first pair of dst values + * @param pointCount The number of pairs/points to be used. Must be [0..4] + * @return true if the matrix was set to the specified transformation + */ + @Override + public boolean setPolyToPoly(float[] src, int srcIndex, + float[] dst, int dstIndex, + int pointCount) { + if (pointCount > 4) { + throw new IllegalArgumentException(); + } + checkPointArrays(src, srcIndex, dst, dstIndex, pointCount); + throw new UnsupportedOperationException("STUB NEEDED"); + } + + /** + * If this matrix can be inverted, return true and if inverse is not null, + * set inverse to be the inverse of this matrix. If this matrix cannot be + * inverted, ignore inverse and return false. + */ + public boolean invert(Matrix inverse) { + throw new UnsupportedOperationException("STUB NEEDED"); + } + + @Override + public boolean invert(_Original_Matrix inverse) { + throw new UnsupportedOperationException("CALL TO PARENT FORBIDDEN"); + } + + /** + * Apply this matrix to the array of 2D points specified by src, and write + * the transformed points into the array of points specified by dst. The + * two arrays represent their "points" as pairs of floats [x, y]. + * + * @param dst The array of dst points (x,y pairs) + * @param dstIndex The index of the first [x,y] pair of dst floats + * @param src The array of src points (x,y pairs) + * @param srcIndex The index of the first [x,y] pair of src floats + * @param pointCount The number of points (x,y pairs) to transform + */ + @Override + public void mapPoints(float[] dst, int dstIndex, float[] src, int srcIndex, + int pointCount) { + checkPointArrays(src, srcIndex, dst, dstIndex, pointCount); + throw new UnsupportedOperationException("STUB NEEDED"); + } + + /** + * Apply this matrix to the array of 2D vectors specified by src, and write + * the transformed vectors into the array of vectors specified by dst. The + * two arrays represent their "vectors" as pairs of floats [x, y]. + * + * @param dst The array of dst vectors (x,y pairs) + * @param dstIndex The index of the first [x,y] pair of dst floats + * @param src The array of src vectors (x,y pairs) + * @param srcIndex The index of the first [x,y] pair of src floats + * @param vectorCount The number of vectors (x,y pairs) to transform + */ + @Override + public void mapVectors(float[] dst, int dstIndex, float[] src, int srcIndex, + int vectorCount) { + checkPointArrays(src, srcIndex, dst, dstIndex, vectorCount); + throw new UnsupportedOperationException("STUB NEEDED"); + } + + /** + * Apply this matrix to the array of 2D points specified by src, and write + * the transformed points into the array of points specified by dst. The + * two arrays represent their "points" as pairs of floats [x, y]. + * + * @param dst The array of dst points (x,y pairs) + * @param src The array of src points (x,y pairs) + */ + @Override + public void mapPoints(float[] dst, float[] src) { + if (dst.length != src.length) { + throw new ArrayIndexOutOfBoundsException(); + } + mapPoints(dst, 0, src, 0, dst.length >> 1); + } + + /** + * Apply this matrix to the array of 2D vectors specified by src, and write + * the transformed vectors into the array of vectors specified by dst. The + * two arrays represent their "vectors" as pairs of floats [x, y]. + * + * @param dst The array of dst vectors (x,y pairs) + * @param src The array of src vectors (x,y pairs) + */ + @Override + public void mapVectors(float[] dst, float[] src) { + if (dst.length != src.length) { + throw new ArrayIndexOutOfBoundsException(); + } + mapVectors(dst, 0, src, 0, dst.length >> 1); + } + + /** + * Apply this matrix to the array of 2D points, and write the transformed + * points back into the array + * + * @param pts The array [x0, y0, x1, y1, ...] of points to transform. + */ + @Override + public void mapPoints(float[] pts) { + mapPoints(pts, 0, pts, 0, pts.length >> 1); + } + + /** + * Apply this matrix to the array of 2D vectors, and write the transformed + * vectors back into the array. + * @param vecs The array [x0, y0, x1, y1, ...] of vectors to transform. + */ + @Override + public void mapVectors(float[] vecs) { + mapVectors(vecs, 0, vecs, 0, vecs.length >> 1); + } + + /** + * Apply this matrix to the src rectangle, and write the transformed + * rectangle into dst. This is accomplished by transforming the 4 corners of + * src, and then setting dst to the bounds of those points. + * + * @param dst Where the transformed rectangle is written. + * @param src The original rectangle to be transformed. + * @return the result of calling rectStaysRect() + */ + @Override + public boolean mapRect(RectF dst, RectF src) { + if (dst == null || src == null) { + throw new NullPointerException(); + } + throw new UnsupportedOperationException("STUB NEEDED"); + } + + /** + * Apply this matrix to the rectangle, and write the transformed rectangle + * back into it. This is accomplished by transforming the 4 corners of rect, + * and then setting it to the bounds of those points + * + * @param rect The rectangle to transform. + * @return the result of calling rectStaysRect() + */ + @Override + public boolean mapRect(RectF rect) { + return mapRect(rect, rect); + } + + /** + * Return the mean radius of a circle after it has been mapped by + * this matrix. NOTE: in perspective this value assumes the circle + * has its center at the origin. + */ + @Override + public float mapRadius(float radius) { + throw new UnsupportedOperationException("STUB NEEDED"); + } + + /** Copy 9 values from the matrix into the array. + */ + @Override + public void getValues(float[] values) { + if (values.length < 9) { + throw new ArrayIndexOutOfBoundsException(); + } + System.arraycopy(mValues, 0, values, 0, mValues.length); + } + + /** Copy 9 values from the array into the matrix. + Depending on the implementation of Matrix, these may be + transformed into 16.16 integers in the Matrix, such that + a subsequent call to getValues() will not yield exactly + the same values. + */ + @Override + public void setValues(float[] values) { + if (values.length < 9) { + throw new ArrayIndexOutOfBoundsException(); + } + System.arraycopy(values, 0, mValues, 0, mValues.length); + } + + @SuppressWarnings("unused") + private final static int kIdentity_Mask = 0; + private final static int kTranslate_Mask = 0x01; //!< set if the matrix has translation + private final static int kScale_Mask = 0x02; //!< set if the matrix has X or Y scale + private final static int kAffine_Mask = 0x04; //!< set if the matrix skews or rotates + private final static int kPerspective_Mask = 0x08; //!< set if the matrix is in perspective + private final static int kRectStaysRect_Mask = 0x10; + @SuppressWarnings("unused") + private final static int kUnknown_Mask = 0x80; + + @SuppressWarnings("unused") + private final static int kAllMasks = kTranslate_Mask | + kScale_Mask | + kAffine_Mask | + kPerspective_Mask | + kRectStaysRect_Mask; + + // these guys align with the masks, so we can compute a mask from a variable 0/1 + @SuppressWarnings("unused") + private final static int kTranslate_Shift = 0; + @SuppressWarnings("unused") + private final static int kScale_Shift = 1; + @SuppressWarnings("unused") + private final static int kAffine_Shift = 2; + @SuppressWarnings("unused") + private final static int kPerspective_Shift = 3; + private final static int kRectStaysRect_Shift = 4; + + private int computeTypeMask() { + int mask = 0; + + if (mValues[6] != 0. || mValues[7] != 0. || mValues[8] != 1.) { + mask |= kPerspective_Mask; + } + + if (mValues[2] != 0. || mValues[5] != 0.) { + mask |= kTranslate_Mask; + } + + float m00 = mValues[0]; + float m01 = mValues[1]; + float m10 = mValues[3]; + float m11 = mValues[4]; + + if (m01 != 0. || m10 != 0.) { + mask |= kAffine_Mask; + } + + if (m00 != 1. || m11 != 1.) { + mask |= kScale_Mask; + } + + if ((mask & kPerspective_Mask) == 0) { + // map non-zero to 1 + int im00 = m00 != 0 ? 1 : 0; + int im01 = m01 != 0 ? 1 : 0; + int im10 = m10 != 0 ? 1 : 0; + int im11 = m11 != 0 ? 1 : 0; + + // record if the (p)rimary and (s)econdary diagonals are all 0 or + // all non-zero (answer is 0 or 1) + int dp0 = (im00 | im11) ^ 1; // true if both are 0 + int dp1 = im00 & im11; // true if both are 1 + int ds0 = (im01 | im10) ^ 1; // true if both are 0 + int ds1 = im01 & im10; // true if both are 1 + + // return 1 if primary is 1 and secondary is 0 or + // primary is 0 and secondary is 1 + mask |= ((dp0 & ds1) | (dp1 & ds0)) << kRectStaysRect_Shift; + } + + return mask; + } +} diff --git a/tools/layoutlib/bridge/src/android/graphics/Paint.java b/tools/layoutlib/bridge/src/android/graphics/Paint.java new file mode 100644 index 0000000..ade07d6 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/Paint.java @@ -0,0 +1,864 @@ +/* + * 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.graphics; + +import android.text.SpannableString; +import android.text.SpannableStringBuilder; +import android.text.SpannedString; +import android.text.TextUtils; + +import java.awt.Font; +import java.awt.Toolkit; +import java.awt.font.FontRenderContext; +import java.awt.geom.AffineTransform; +import java.awt.geom.Rectangle2D; + +/** + * A paint implementation overridden by the LayoutLib bridge. + */ +public class Paint extends _Original_Paint { + + private int mColor = 0xFFFFFFFF; + private float mTextSize = 20; + private float mScaleX = 1; + private float mSkewX = 0; + private Align mAlign = Align.LEFT; + private Style mStyle = Style.FILL; + private int mFlags = 0; + + private Font mFont; + private final FontRenderContext mFontContext = new FontRenderContext( + new AffineTransform(), true, true); + private java.awt.FontMetrics mMetrics; + + @SuppressWarnings("hiding") + public static final int ANTI_ALIAS_FLAG = _Original_Paint.ANTI_ALIAS_FLAG; + @SuppressWarnings("hiding") + public static final int FILTER_BITMAP_FLAG = _Original_Paint.FILTER_BITMAP_FLAG; + @SuppressWarnings("hiding") + public static final int DITHER_FLAG = _Original_Paint.DITHER_FLAG; + @SuppressWarnings("hiding") + public static final int UNDERLINE_TEXT_FLAG = _Original_Paint.UNDERLINE_TEXT_FLAG; + @SuppressWarnings("hiding") + public static final int STRIKE_THRU_TEXT_FLAG = _Original_Paint.STRIKE_THRU_TEXT_FLAG; + @SuppressWarnings("hiding") + public static final int FAKE_BOLD_TEXT_FLAG = _Original_Paint.FAKE_BOLD_TEXT_FLAG; + @SuppressWarnings("hiding") + public static final int LINEAR_TEXT_FLAG = _Original_Paint.LINEAR_TEXT_FLAG; + @SuppressWarnings("hiding") + public static final int SUBPIXEL_TEXT_FLAG = _Original_Paint.SUBPIXEL_TEXT_FLAG; + @SuppressWarnings("hiding") + public static final int DEV_KERN_TEXT_FLAG = _Original_Paint.DEV_KERN_TEXT_FLAG; + + public static class FontMetrics extends _Original_Paint.FontMetrics { + } + + public static class FontMetricsInt extends _Original_Paint.FontMetricsInt { + } + + /** + * The Style specifies if the primitive being drawn is filled, + * stroked, or both (in the same color). The default is FILL. + */ + public enum Style { + /** + * Geometry and text drawn with this style will be filled, ignoring all + * stroke-related settings in the paint. + */ + FILL (0), + /** + * Geometry and text drawn with this style will be stroked, respecting + * the stroke-related fields on the paint. + */ + STROKE (1), + /** + * Geometry and text drawn with this style will be both filled and + * stroked at the same time, respecting the stroke-related fields on + * the paint. + */ + FILL_AND_STROKE (2); + + Style(int nativeInt) { + this.nativeInt = nativeInt; + } + final int nativeInt; + } + + /** + * The Cap specifies the treatment for the beginning and ending of + * stroked lines and paths. The default is BUTT. + */ + public enum Cap { + /** + * The stroke ends with the path, and does not project beyond it. + */ + BUTT (0), + /** + * The stroke projects out as a square, with the center at the end + * of the path. + */ + ROUND (1), + /** + * The stroke projects out as a semicircle, with the center at the + * end of the path. + */ + SQUARE (2); + + private Cap(int nativeInt) { + this.nativeInt = nativeInt; + } + final int nativeInt; + } + + /** + * The Join specifies the treatment where lines and curve segments + * join on a stroked path. The default is MITER. + */ + public enum Join { + /** + * The outer edges of a join meet at a sharp angle + */ + MITER (0), + /** + * The outer edges of a join meet in a circular arc. + */ + ROUND (1), + /** + * The outer edges of a join meet with a straight line + */ + BEVEL (2); + + private Join(int nativeInt) { + this.nativeInt = nativeInt; + } + final int nativeInt; + } + + /** + * Align specifies how drawText aligns its text relative to the + * [x,y] coordinates. The default is LEFT. + */ + public enum Align { + /** + * The text is drawn to the right of the x,y origin + */ + LEFT (0), + /** + * The text is drawn centered horizontally on the x,y origin + */ + CENTER (1), + /** + * The text is drawn to the left of the x,y origin + */ + RIGHT (2); + + private Align(int nativeInt) { + this.nativeInt = nativeInt; + } + final int nativeInt; + } + + public Paint() { + this(0); + } + + public Paint(int flags) { + setFlags(flags | DEFAULT_PAINT_FLAGS); + initFont(); + } + + public Paint(Paint paint) { + set(paint); + initFont(); + } + + @Override + public void finalize() throws Throwable { + // pass + } + + /** + * Returns the {@link Font} object. + */ + public Font getFont() { + return mFont; + } + + private void initFont() { + mTypeface = Typeface.DEFAULT; + updateFontObject(); + } + + /** + * Update the {@link Font} object from the typeface, text size and scaling + */ + private void updateFontObject() { + if (mTypeface != null) { + // get the typeface font object, and get our font object from it, based on the current size + mFont = mTypeface.getFont().deriveFont(mTextSize); + if (mScaleX != 1.0 || mSkewX != 0) { + // TODO: support skew + mFont = mFont.deriveFont(new AffineTransform( + mScaleX, mSkewX, 0, 0, 1, 0)); + } + + mMetrics = Toolkit.getDefaultToolkit().getFontMetrics(mFont); + } + } + + //---------------------------------------- + + public void set(Paint src) { + if (this != src) { + mColor = src.mColor; + mTextSize = src.mTextSize; + mScaleX = src.mScaleX; + mSkewX = src.mSkewX; + mAlign = src.mAlign; + mStyle = src.mStyle; + mFlags = src.mFlags; + + super.set(src); + } + } + + @Override + public int getFlags() { + return mFlags; + } + + @Override + public void setFlags(int flags) { + mFlags = flags; + } + + /** + * Return the font's recommended interline spacing, given the Paint's + * settings for typeface, textSize, etc. If metrics is not null, return the + * fontmetric values in it. + * + * @param metrics If this object is not null, its fields are filled with + * the appropriate values given the paint's text attributes. + * @return the font's recommended interline spacing. + */ + public float getFontMetrics(FontMetrics metrics) { + if (mMetrics != null) { + if (metrics != null) { + // ascent stuff should be negatif, but awt returns them as positive. + metrics.top = - mMetrics.getMaxAscent(); + metrics.ascent = - mMetrics.getAscent(); + metrics.descent = mMetrics.getDescent(); + metrics.bottom = mMetrics.getMaxDescent(); + metrics.leading = mMetrics.getLeading(); + } + + return mMetrics.getHeight(); + } + + return 0; + } + + public int getFontMetricsInt(FontMetricsInt metrics) { + if (mMetrics != null) { + if (metrics != null) { + // ascent stuff should be negatif, but awt returns them as positive. + metrics.top = - mMetrics.getMaxAscent(); + metrics.ascent = - mMetrics.getAscent(); + metrics.descent = mMetrics.getDescent(); + metrics.bottom = mMetrics.getMaxDescent(); + metrics.leading = mMetrics.getLeading(); + } + + return mMetrics.getHeight(); + } + + return 0; + } + + /** + * Reimplemented to return Paint.FontMetrics instead of _Original_Paint.FontMetrics + */ + public FontMetrics getFontMetrics() { + FontMetrics fm = new FontMetrics(); + getFontMetrics(fm); + return fm; + } + + /** + * Reimplemented to return Paint.FontMetricsInt instead of _Original_Paint.FontMetricsInt + */ + public FontMetricsInt getFontMetricsInt() { + FontMetricsInt fm = new FontMetricsInt(); + getFontMetricsInt(fm); + return fm; + } + + + + @Override + public float getFontMetrics(_Original_Paint.FontMetrics metrics) { + // TODO implement if needed + throw new UnsupportedOperationException("CALL TO PARENT FORBIDDEN"); + } + + @Override + public int getFontMetricsInt(_Original_Paint.FontMetricsInt metrics) { + // TODO implement if needed + throw new UnsupportedOperationException("CALL TO PARENT FORBIDDEN"); + } + + @Override + public Typeface setTypeface(Typeface typeface) { + if (typeface != null) { + mTypeface = typeface; + } else { + mTypeface = Typeface.DEFAULT; + } + + updateFontObject(); + + return typeface; + } + + @Override + public int getColor() { + return mColor; + } + + @Override + public void setColor(int color) { + mColor = color; + } + + + @Override + public void setAlpha(int alpha) { + mColor = (alpha << 24) | (mColor & 0x00FFFFFF); + } + + @Override + public int getAlpha() { + return mColor >>> 24; + } + + /** + * Set or clear the shader object. + * <p /> + * Pass null to clear any previous shader. + * As a convenience, the parameter passed is also returned. + * + * @param shader May be null. the new shader to be installed in the paint + * @return shader + */ + @Override + public Shader setShader(Shader shader) { + return mShader = shader; + } + + /** + * Set or clear the paint's colorfilter, returning the parameter. + * + * @param filter May be null. The new filter to be installed in the paint + * @return filter + */ + @Override + public ColorFilter setColorFilter(ColorFilter filter) { + int filterNative = 0; + if (filter != null) + filterNative = filter.native_instance; + mColorFilter = filter; + return filter; + } + + /** + * Set or clear the xfermode object. + * <p /> + * Pass null to clear any previous xfermode. + * As a convenience, the parameter passed is also returned. + * + * @param xfermode May be null. The xfermode to be installed in the paint + * @return xfermode + */ + @Override + public Xfermode setXfermode(Xfermode xfermode) { + return mXfermode = xfermode; + } + + public void setTextAlign(Align align) { + mAlign = align; + } + + @Override + public void setTextAlign(android.graphics._Original_Paint.Align align) { + // TODO implement if needed + throw new UnsupportedOperationException("CALL TO PARENT FORBIDDEN"); + } + + public Align getTextAlign() { + return mAlign; + } + + public void setStyle(Style style) { + mStyle = style; + } + + @Override + public void setStyle(android.graphics._Original_Paint.Style style) { + // TODO implement if needed + throw new UnsupportedOperationException("CALL TO PARENT FORBIDDEN"); + } + + public Style getStyle() { + return mStyle; + } + + @Override + public void setDither(boolean dither) { + mFlags |= dither ? DITHER_FLAG : ~DITHER_FLAG; + } + + @Override + public void setAntiAlias(boolean aa) { + mFlags |= aa ? ANTI_ALIAS_FLAG : ~ANTI_ALIAS_FLAG; + } + + @Override + public void setFakeBoldText(boolean flag) { + mFlags |= flag ? FAKE_BOLD_TEXT_FLAG : ~FAKE_BOLD_TEXT_FLAG; + } + + /** + * Return the paint's text size. + * + * @return the paint's text size. + */ + @Override + public float getTextSize() { + return mTextSize; + } + + /** + * Set the paint's text size. This value must be > 0 + * + * @param textSize set the paint's text size. + */ + @Override + public void setTextSize(float textSize) { + mTextSize = textSize; + + updateFontObject(); + } + + /** + * Return the paint's horizontal scale factor for text. The default value + * is 1.0. + * + * @return the paint's scale factor in X for drawing/measuring text + */ + @Override + public float getTextScaleX() { + return mScaleX; + } + + /** + * Set the paint's horizontal scale factor for text. The default value + * is 1.0. Values > 1.0 will stretch the text wider. Values < 1.0 will + * stretch the text narrower. + * + * @param scaleX set the paint's scale in X for drawing/measuring text. + */ + @Override + public void setTextScaleX(float scaleX) { + mScaleX = scaleX; + + updateFontObject(); + } + + /** + * Return the paint's horizontal skew factor for text. The default value + * is 0. + * + * @return the paint's skew factor in X for drawing text. + */ + @Override + public float getTextSkewX() { + return mSkewX; + } + + /** + * Set the paint's horizontal skew factor for text. The default value + * is 0. For approximating oblique text, use values around -0.25. + * + * @param skewX set the paint's skew factor in X for drawing text. + */ + @Override + public void setTextSkewX(float skewX) { + mSkewX = skewX; + + updateFontObject(); + } + + /** + * Return the distance above (negative) the baseline (ascent) based on the + * current typeface and text size. + * + * @return the distance above (negative) the baseline (ascent) based on the + * current typeface and text size. + */ + @Override + public float ascent() { + if (mMetrics != null) { + // ascent stuff should be negatif, but awt returns them as positive. + return - mMetrics.getAscent(); + } + + return 0; + } + + /** + * Return the distance below (positive) the baseline (descent) based on the + * current typeface and text size. + * + * @return the distance below (positive) the baseline (descent) based on + * the current typeface and text size. + */ + @Override + public float descent() { + if (mMetrics != null) { + return mMetrics.getDescent(); + } + + return 0; + } + + /** + * Return the width of the text. + * + * @param text The text to measure + * @param index The index of the first character to start measuring + * @param count THe number of characters to measure, beginning with start + * @return The width of the text + */ + @Override + public float measureText(char[] text, int index, int count) { + if (mFont != null && text != null && text.length > 0) { + Rectangle2D bounds = mFont.getStringBounds(text, index, index + count, mFontContext); + + return (float)bounds.getWidth(); + } + + return 0; + } + + /** + * Return the width of the text. + * + * @param text The text to measure + * @param start The index of the first character to start measuring + * @param end 1 beyond the index of the last character to measure + * @return The width of the text + */ + @Override + public float measureText(String text, int start, int end) { + return measureText(text.toCharArray(), start, end - start); + } + + /** + * Return the width of the text. + * + * @param text The text to measure + * @return The width of the text + */ + @Override + public float measureText(String text) { + return measureText(text.toCharArray(), 0, text.length()); + } + + /* + * re-implement to call SpannableStringBuilder.measureText with a Paint object + * instead of an _Original_Paint + */ + @Override + public float measureText(CharSequence text, int start, int end) { + if (text instanceof String) { + return measureText((String)text, start, end); + } + if (text instanceof SpannedString || + text instanceof SpannableString) { + return measureText(text.toString(), start, end); + } + if (text instanceof SpannableStringBuilder) { + return ((SpannableStringBuilder)text).measureText(start, end, this); + } + + char[] buf = TemporaryBuffer.obtain(end - start); + TextUtils.getChars(text, start, end, buf, 0); + float result = measureText(buf, 0, end - start); + TemporaryBuffer.recycle(buf); + return result; + } + + /** + * Measure the text, stopping early if the measured width exceeds maxWidth. + * Return the number of chars that were measured, and if measuredWidth is + * not null, return in it the actual width measured. + * + * @param text The text to measure + * @param index The offset into text to begin measuring at + * @param count The number of maximum number of entries to measure. If count + * is negative, then the characters before index are measured + * in reverse order. This allows for measuring the end of + * string. + * @param maxWidth The maximum width to accumulate. + * @param measuredWidth Optional. If not null, returns the actual width + * measured. + * @return The number of chars that were measured. Will always be <= + * abs(count). + */ + @Override + public int breakText(char[] text, int index, int count, + float maxWidth, float[] measuredWidth) { + int inc = count > 0 ? 1 : -1; + + int measureIndex = 0; + float measureAcc = 0; + for (int i = index ; i != index + count ; i += inc, measureIndex++) { + int start, end; + if (i < index) { + start = i; + end = index; + } else { + start = index; + end = i; + } + + // measure from start to end + float res = measureText(text, start, end - start + 1); + + if (measuredWidth != null) { + measuredWidth[measureIndex] = res; + } + + measureAcc += res; + if (res > maxWidth) { + // we should not return this char index, but since it's 0-based and we need + // to return a count, we simply return measureIndex; + return measureIndex; + } + + } + + return measureIndex; + } + + /** + * Measure the text, stopping early if the measured width exceeds maxWidth. + * Return the number of chars that were measured, and if measuredWidth is + * not null, return in it the actual width measured. + * + * @param text The text to measure + * @param measureForwards If true, measure forwards, starting at index. + * Otherwise, measure backwards, starting with the + * last character in the string. + * @param maxWidth The maximum width to accumulate. + * @param measuredWidth Optional. If not null, returns the actual width + * measured. + * @return The number of chars that were measured. Will always be <= + * abs(count). + */ + @Override + public int breakText(String text, boolean measureForwards, + float maxWidth, float[] measuredWidth) { + // NOTE: javadoc doesn't match. Just a guess. + return breakText(text, + 0 /* start */, text.length() /* end */, + measureForwards, maxWidth, measuredWidth); + } + + /** + * Return the advance widths for the characters in the string. + * + * @param text The text to measure + * @param index The index of the first char to to measure + * @param count The number of chars starting with index to measure + * @param widths array to receive the advance widths of the characters. + * Must be at least a large as count. + * @return the actual number of widths returned. + */ + @Override + public int getTextWidths(char[] text, int index, int count, + float[] widths) { + if (mMetrics != null) { + if ((index | count) < 0 || index + count > text.length + || count > widths.length) { + throw new ArrayIndexOutOfBoundsException(); + } + + for (int i = 0; i < count; i++) { + widths[i] = mMetrics.charWidth(text[i + index]); + } + + return count; + } + + return 0; + } + + /** + * Return the advance widths for the characters in the string. + * + * @param text The text to measure + * @param start The index of the first char to to measure + * @param end The end of the text slice to measure + * @param widths array to receive the advance widths of the characters. + * Must be at least a large as the text. + * @return the number of unichars in the specified text. + */ + @Override + public int getTextWidths(String text, int start, int end, float[] widths) { + if ((start | end | (end - start) | (text.length() - end)) < 0) { + throw new IndexOutOfBoundsException(); + } + if (end - start > widths.length) { + throw new ArrayIndexOutOfBoundsException(); + } + + return getTextWidths(text.toCharArray(), start, end - start, widths); + } + + /* + * re-implement to call SpannableStringBuilder.getTextWidths with a Paint object + * instead of an _Original_Paint + */ + @Override + public int getTextWidths(CharSequence text, int start, int end, float[] widths) { + if (text instanceof String) { + return getTextWidths((String)text, start, end, widths); + } + if (text instanceof SpannedString || text instanceof SpannableString) { + return getTextWidths(text.toString(), start, end, widths); + } + if (text instanceof SpannableStringBuilder) { + return ((SpannableStringBuilder)text).getTextWidths(start, end, widths, this); + } + + char[] buf = TemporaryBuffer.obtain(end - start); + TextUtils.getChars(text, start, end, buf, 0); + int result = getTextWidths(buf, 0, end - start, widths); + TemporaryBuffer.recycle(buf); + return result; + } + + + /** + * Return the path (outline) for the specified text. + * Note: just like Canvas.drawText, this will respect the Align setting in + * the paint. + * + * @param text The text to retrieve the path from + * @param index The index of the first character in text + * @param count The number of characterss starting with index + * @param x The x coordinate of the text's origin + * @param y The y coordinate of the text's origin + * @param path The path to receive the data describing the text. Must + * be allocated by the caller. + */ + @Override + public void getTextPath(char[] text, int index, int count, + float x, float y, Path path) { + + // TODO this is the ORIGINAL implementation. REPLACE AS NEEDED OR REMOVE + + if ((index | count) < 0 || index + count > text.length) { + throw new ArrayIndexOutOfBoundsException(); + } + + // TODO native_getTextPath(mNativePaint, text, index, count, x, y, path.ni()); + + throw new UnsupportedOperationException("IMPLEMENT AS NEEDED"); + } + + /** + * Return the path (outline) for the specified text. + * Note: just like Canvas.drawText, this will respect the Align setting + * in the paint. + * + * @param text The text to retrieve the path from + * @param start The first character in the text + * @param end 1 past the last charcter in the text + * @param x The x coordinate of the text's origin + * @param y The y coordinate of the text's origin + * @param path The path to receive the data describing the text. Must + * be allocated by the caller. + */ + @Override + public void getTextPath(String text, int start, int end, + float x, float y, Path path) { + if ((start | end | (end - start) | (text.length() - end)) < 0) { + throw new IndexOutOfBoundsException(); + } + + getTextPath(text.toCharArray(), start, end - start, x, y, path); + } + + /** + * Return in bounds (allocated by the caller) the smallest rectangle that + * encloses all of the characters, with an implied origin at (0,0). + * + * @param text String to measure and return its bounds + * @param start Index of the first char in the string to measure + * @param end 1 past the last char in the string measure + * @param bounds Returns the unioned bounds of all the text. Must be + * allocated by the caller. + */ + @Override + public void getTextBounds(String text, int start, int end, Rect bounds) { + if ((start | end | (end - start) | (text.length() - end)) < 0) { + throw new IndexOutOfBoundsException(); + } + if (bounds == null) { + throw new NullPointerException("need bounds Rect"); + } + + getTextBounds(text.toCharArray(), start, end - start, bounds); + } + + /** + * Return in bounds (allocated by the caller) the smallest rectangle that + * encloses all of the characters, with an implied origin at (0,0). + * + * @param text Array of chars to measure and return their unioned bounds + * @param index Index of the first char in the array to measure + * @param count The number of chars, beginning at index, to measure + * @param bounds Returns the unioned bounds of all the text. Must be + * allocated by the caller. + */ + @Override + public void getTextBounds(char[] text, int index, int count, Rect bounds) { + if (mFont != null) { + if ((index | count) < 0 || index + count > text.length) { + throw new ArrayIndexOutOfBoundsException(); + } + if (bounds == null) { + throw new NullPointerException("need bounds Rect"); + } + + Rectangle2D rect = mFont.getStringBounds(text, index, index + count, mFontContext); + bounds.set(0, 0, (int)rect.getWidth(), (int)rect.getHeight()); + } + } +} diff --git a/tools/layoutlib/bridge/src/android/graphics/Path.java b/tools/layoutlib/bridge/src/android/graphics/Path.java new file mode 100644 index 0000000..12d2cde --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/Path.java @@ -0,0 +1,611 @@ +/* + * Copyright (C) 2006 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.graphics; + +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Ellipse2D; +import java.awt.geom.GeneralPath; +import java.awt.geom.PathIterator; +import java.awt.geom.Rectangle2D; + +/** + * The Path class encapsulates compound (multiple contour) geometric paths + * consisting of straight line segments, quadratic curves, and cubic curves. + * It can be drawn with canvas.drawPath(path, paint), either filled or stroked + * (based on the paint's Style), or it can be used for clipping or to draw + * text on a path. + */ +public class Path { + + private FillType mFillType = FillType.WINDING; + private GeneralPath mPath = new GeneralPath(); + + private float mLastX = 0; + private float mLastY = 0; + + //---------- Custom methods ---------- + + public Shape getAwtShape() { + return mPath; + } + + //---------- + + /** + * Create an empty path + */ + public Path() { + } + + /** + * Create a new path, copying the contents from the src path. + * + * @param src The path to copy from when initializing the new path + */ + public Path(Path src) { + mPath.append(src.mPath, false /* connect */); + } + + /** + * Clear any lines and curves from the path, making it empty. + * This does NOT change the fill-type setting. + */ + public void reset() { + mPath = new GeneralPath(); + } + + /** + * Rewinds the path: clears any lines and curves from the path but + * keeps the internal data structure for faster reuse. + */ + public void rewind() { + // FIXME + throw new UnsupportedOperationException(); + } + + /** Replace the contents of this with the contents of src. + */ + public void set(Path src) { + mPath.append(src.mPath, false /* connect */); + } + + /** Enum for the ways a path may be filled + */ + public enum FillType { + // these must match the values in SkPath.h + WINDING (GeneralPath.WIND_NON_ZERO, false), + EVEN_ODD (GeneralPath.WIND_EVEN_ODD, false), + INVERSE_WINDING (GeneralPath.WIND_NON_ZERO, true), + INVERSE_EVEN_ODD(GeneralPath.WIND_EVEN_ODD, true); + + FillType(int rule, boolean inverse) { + this.rule = rule; + this.inverse = inverse; + } + + final int rule; + final boolean inverse; + } + + /** + * Return the path's fill type. This defines how "inside" is + * computed. The default value is WINDING. + * + * @return the path's fill type + */ + public FillType getFillType() { + return mFillType; + } + + /** + * Set the path's fill type. This defines how "inside" is computed. + * + * @param ft The new fill type for this path + */ + public void setFillType(FillType ft) { + mFillType = ft; + mPath.setWindingRule(ft.rule); + } + + /** + * Returns true if the filltype is one of the INVERSE variants + * + * @return true if the filltype is one of the INVERSE variants + */ + public boolean isInverseFillType() { + return mFillType.inverse; + } + + /** + * Toggles the INVERSE state of the filltype + */ + public void toggleInverseFillType() { + switch (mFillType) { + case WINDING: + mFillType = FillType.INVERSE_WINDING; + break; + case EVEN_ODD: + mFillType = FillType.INVERSE_EVEN_ODD; + break; + case INVERSE_WINDING: + mFillType = FillType.WINDING; + break; + case INVERSE_EVEN_ODD: + mFillType = FillType.EVEN_ODD; + break; + } + } + + /** + * Returns true if the path is empty (contains no lines or curves) + * + * @return true if the path is empty (contains no lines or curves) + */ + public boolean isEmpty() { + return mPath.getCurrentPoint() == null; + } + + /** + * Returns true if the path specifies a rectangle. If so, and if rect is + * not null, set rect to the bounds of the path. If the path does not + * specify a rectangle, return false and ignore rect. + * + * @param rect If not null, returns the bounds of the path if it specifies + * a rectangle + * @return true if the path specifies a rectangle + */ + public boolean isRect(RectF rect) { + // FIXME + throw new UnsupportedOperationException(); + } + + /** + * Compute the bounds of the path, and write the answer into bounds. If the + * path contains 0 or 1 points, the bounds is set to (0,0,0,0) + * + * @param bounds Returns the computed bounds of the path + * @param exact If true, return the exact (but slower) bounds, else return + * just the bounds of all control points + */ + public void computeBounds(RectF bounds, boolean exact) { + Rectangle2D rect = mPath.getBounds2D(); + bounds.left = (float)rect.getMinX(); + bounds.right = (float)rect.getMaxX(); + bounds.top = (float)rect.getMinY(); + bounds.bottom = (float)rect.getMaxY(); + } + + /** + * Hint to the path to prepare for adding more points. This can allow the + * path to more efficiently allocate its storage. + * + * @param extraPtCount The number of extra points that may be added to this + * path + */ + public void incReserve(int extraPtCount) { + // pass + } + + /** + * Set the beginning of the next contour to the point (x,y). + * + * @param x The x-coordinate of the start of a new contour + * @param y The y-coordinate of the start of a new contour + */ + public void moveTo(float x, float y) { + mPath.moveTo(mLastX = x, mLastY = y); + } + + /** + * Set the beginning of the next contour relative to the last point on the + * previous contour. If there is no previous contour, this is treated the + * same as moveTo(). + * + * @param dx The amount to add to the x-coordinate of the end of the + * previous contour, to specify the start of a new contour + * @param dy The amount to add to the y-coordinate of the end of the + * previous contour, to specify the start of a new contour + */ + public void rMoveTo(float dx, float dy) { + dx += mLastX; + dy += mLastY; + mPath.moveTo(mLastX = dx, mLastY = dy); + } + + /** + * Add a line from the last point to the specified point (x,y). + * If no moveTo() call has been made for this contour, the first point is + * automatically set to (0,0). + * + * @param x The x-coordinate of the end of a line + * @param y The y-coordinate of the end of a line + */ + public void lineTo(float x, float y) { + mPath.lineTo(mLastX = x, mLastY = y); + } + + /** + * Same as lineTo, but the coordinates are considered relative to the last + * point on this contour. If there is no previous point, then a moveTo(0,0) + * is inserted automatically. + * + * @param dx The amount to add to the x-coordinate of the previous point on + * this contour, to specify a line + * @param dy The amount to add to the y-coordinate of the previous point on + * this contour, to specify a line + */ + public void rLineTo(float dx, float dy) { + if (isEmpty()) { + mPath.moveTo(mLastX = 0, mLastY = 0); + } + dx += mLastX; + dy += mLastY; + mPath.lineTo(mLastX = dx, mLastY = dy); + } + + /** + * Add a quadratic bezier from the last point, approaching control point + * (x1,y1), and ending at (x2,y2). If no moveTo() call has been made for + * this contour, the first point is automatically set to (0,0). + * + * @param x1 The x-coordinate of the control point on a quadratic curve + * @param y1 The y-coordinate of the control point on a quadratic curve + * @param x2 The x-coordinate of the end point on a quadratic curve + * @param y2 The y-coordinate of the end point on a quadratic curve + */ + public void quadTo(float x1, float y1, float x2, float y2) { + mPath.quadTo(x1, y1, mLastX = x2, mLastY = y2); + } + + /** + * Same as quadTo, but the coordinates are considered relative to the last + * point on this contour. If there is no previous point, then a moveTo(0,0) + * is inserted automatically. + * + * @param dx1 The amount to add to the x-coordinate of the last point on + * this contour, for the control point of a quadratic curve + * @param dy1 The amount to add to the y-coordinate of the last point on + * this contour, for the control point of a quadratic curve + * @param dx2 The amount to add to the x-coordinate of the last point on + * this contour, for the end point of a quadratic curve + * @param dy2 The amount to add to the y-coordinate of the last point on + * this contour, for the end point of a quadratic curve + */ + public void rQuadTo(float dx1, float dy1, float dx2, float dy2) { + if (isEmpty()) { + mPath.moveTo(mLastX = 0, mLastY = 0); + } + dx1 += mLastX; + dy1 += mLastY; + dx2 += mLastX; + dy2 += mLastY; + mPath.quadTo(dx1, dy1, mLastX = dx2, mLastY = dy2); + } + + /** + * Add a cubic bezier from the last point, approaching control points + * (x1,y1) and (x2,y2), and ending at (x3,y3). If no moveTo() call has been + * made for this contour, the first point is automatically set to (0,0). + * + * @param x1 The x-coordinate of the 1st control point on a cubic curve + * @param y1 The y-coordinate of the 1st control point on a cubic curve + * @param x2 The x-coordinate of the 2nd control point on a cubic curve + * @param y2 The y-coordinate of the 2nd control point on a cubic curve + * @param x3 The x-coordinate of the end point on a cubic curve + * @param y3 The y-coordinate of the end point on a cubic curve + */ + public void cubicTo(float x1, float y1, float x2, float y2, + float x3, float y3) { + mPath.curveTo(x1, y1, x2, y2, mLastX = x3, mLastY = y3); + } + + /** + * Same as cubicTo, but the coordinates are considered relative to the + * current point on this contour. If there is no previous point, then a + * moveTo(0,0) is inserted automatically. + */ + public void rCubicTo(float dx1, float dy1, float dx2, float dy2, + float dx3, float dy3) { + if (isEmpty()) { + mPath.moveTo(mLastX = 0, mLastY = 0); + } + dx1 += mLastX; + dy1 += mLastY; + dx2 += mLastX; + dy2 += mLastY; + dx3 += mLastX; + dy3 += mLastY; + mPath.curveTo(dx1, dy1, dx2, dy2, mLastX = dx3, mLastY = dy3); + } + + /** + * Append the specified arc to the path as a new contour. If the start of + * the path is different from the path's current last point, then an + * automatic lineTo() is added to connect the current contour to the + * start of the arc. However, if the path is empty, then we call moveTo() + * with the first point of the arc. The sweep angle is tread mod 360. + * + * @param oval The bounds of oval defining shape and size of the arc + * @param startAngle Starting angle (in degrees) where the arc begins + * @param sweepAngle Sweep angle (in degrees) measured clockwise, treated + * mod 360. + * @param forceMoveTo If true, always begin a new contour with the arc + */ + public void arcTo(RectF oval, float startAngle, float sweepAngle, + boolean forceMoveTo) { + throw new UnsupportedOperationException(); + } + + /** + * Append the specified arc to the path as a new contour. If the start of + * the path is different from the path's current last point, then an + * automatic lineTo() is added to connect the current contour to the + * start of the arc. However, if the path is empty, then we call moveTo() + * with the first point of the arc. + * + * @param oval The bounds of oval defining shape and size of the arc + * @param startAngle Starting angle (in degrees) where the arc begins + * @param sweepAngle Sweep angle (in degrees) measured clockwise + */ + public void arcTo(RectF oval, float startAngle, float sweepAngle) { + throw new UnsupportedOperationException(); + } + + /** + * Close the current contour. If the current point is not equal to the + * first point of the contour, a line segment is automatically added. + */ + public void close() { + mPath.closePath(); + } + + /** + * Specifies how closed shapes (e.g. rects, ovals) are oriented when they + * are added to a path. + */ + public enum Direction { + /** clockwise */ + CW (0), // must match enum in SkPath.h + /** counter-clockwise */ + CCW (1); // must match enum in SkPath.h + + Direction(int ni) { + nativeInt = ni; + } + final int nativeInt; + } + + /** + * Add a closed rectangle contour to the path + * + * @param rect The rectangle to add as a closed contour to the path + * @param dir The direction to wind the rectangle's contour + */ + public void addRect(RectF rect, Direction dir) { + if (rect == null) { + throw new NullPointerException("need rect parameter"); + } + + addRect(rect.left, rect.top, rect.right, rect.bottom, dir); + } + + /** + * Add a closed rectangle contour to the path + * + * @param left The left side of a rectangle to add to the path + * @param top The top of a rectangle to add to the path + * @param right The right side of a rectangle to add to the path + * @param bottom The bottom of a rectangle to add to the path + * @param dir The direction to wind the rectangle's contour + */ + public void addRect(float left, float top, float right, float bottom, + Direction dir) { + moveTo(left, top); + + switch (dir) { + case CW: + lineTo(right, top); + lineTo(right, bottom); + lineTo(left, bottom); + break; + case CCW: + lineTo(left, bottom); + lineTo(right, bottom); + lineTo(right, top); + break; + } + + close(); + } + + /** + * Add a closed oval contour to the path + * + * @param oval The bounds of the oval to add as a closed contour to the path + * @param dir The direction to wind the oval's contour + */ + public void addOval(RectF oval, Direction dir) { + if (oval == null) { + throw new NullPointerException("need oval parameter"); + } + + // FIXME Need to support direction + Ellipse2D ovalShape = new Ellipse2D.Float(oval.left, oval.top, oval.width(), oval.height()); + + mPath.append(ovalShape, false /* connect */); + } + + /** + * Add a closed circle contour to the path + * + * @param x The x-coordinate of the center of a circle to add to the path + * @param y The y-coordinate of the center of a circle to add to the path + * @param radius The radius of a circle to add to the path + * @param dir The direction to wind the circle's contour + */ + public void addCircle(float x, float y, float radius, Direction dir) { + // FIXME + throw new UnsupportedOperationException(); + } + + /** + * Add the specified arc to the path as a new contour. + * + * @param oval The bounds of oval defining the shape and size of the arc + * @param startAngle Starting angle (in degrees) where the arc begins + * @param sweepAngle Sweep angle (in degrees) measured clockwise + */ + public void addArc(RectF oval, float startAngle, float sweepAngle) { + if (oval == null) { + throw new NullPointerException("need oval parameter"); + } + // FIXME + throw new UnsupportedOperationException(); + } + + /** + * Add a closed round-rectangle contour to the path + * + * @param rect The bounds of a round-rectangle to add to the path + * @param rx The x-radius of the rounded corners on the round-rectangle + * @param ry The y-radius of the rounded corners on the round-rectangle + * @param dir The direction to wind the round-rectangle's contour + */ + public void addRoundRect(RectF rect, float rx, float ry, Direction dir) { + if (rect == null) { + throw new NullPointerException("need rect parameter"); + } + // FIXME + throw new UnsupportedOperationException(); + } + + /** + * Add a closed round-rectangle contour to the path. Each corner receives + * two radius values [X, Y]. The corners are ordered top-left, top-right, + * bottom-right, bottom-left + * + * @param rect The bounds of a round-rectangle to add to the path + * @param radii Array of 8 values, 4 pairs of [X,Y] radii + * @param dir The direction to wind the round-rectangle's contour + */ + public void addRoundRect(RectF rect, float[] radii, Direction dir) { + if (rect == null) { + throw new NullPointerException("need rect parameter"); + } + if (radii.length < 8) { + throw new ArrayIndexOutOfBoundsException("radii[] needs 8 values"); + } + // FIXME + throw new UnsupportedOperationException(); + } + + /** + * Add a copy of src to the path, offset by (dx,dy) + * + * @param src The path to add as a new contour + * @param dx The amount to translate the path in X as it is added + */ + public void addPath(Path src, float dx, float dy) { + PathIterator iterator = src.mPath.getPathIterator(new AffineTransform(0, 0, dx, 0, 0, dy)); + mPath.append(iterator, false /* connect */); + } + + /** + * Add a copy of src to the path + * + * @param src The path that is appended to the current path + */ + public void addPath(Path src) { + addPath(src, 0, 0); + } + + /** + * Add a copy of src to the path, transformed by matrix + * + * @param src The path to add as a new contour + */ + public void addPath(Path src, Matrix matrix) { + // FIXME + throw new UnsupportedOperationException(); + } + + /** + * Offset the path by (dx,dy), returning true on success + * + * @param dx The amount in the X direction to offset the entire path + * @param dy The amount in the Y direction to offset the entire path + * @param dst The translated path is written here. If this is null, then + * the original path is modified. + */ + public void offset(float dx, float dy, Path dst) { + GeneralPath newPath = new GeneralPath(); + + PathIterator iterator = mPath.getPathIterator(new AffineTransform(0, 0, dx, 0, 0, dy)); + + newPath.append(iterator, false /* connect */); + + if (dst != null) { + dst.mPath = newPath; + } else { + mPath = newPath; + } + } + + /** + * Offset the path by (dx,dy), returning true on success + * + * @param dx The amount in the X direction to offset the entire path + * @param dy The amount in the Y direction to offset the entire path + */ + public void offset(float dx, float dy) { + offset(dx, dy, null /* dst */); + } + + /** + * Sets the last point of the path. + * + * @param dx The new X coordinate for the last point + * @param dy The new Y coordinate for the last point + */ + public void setLastPoint(float dx, float dy) { + mLastX = dx; + mLastY = dy; + } + + /** + * Transform the points in this path by matrix, and write the answer + * into dst. If dst is null, then the the original path is modified. + * + * @param matrix The matrix to apply to the path + * @param dst The transformed path is written here. If dst is null, + * then the the original path is modified + */ + public void transform(Matrix matrix, Path dst) { + // FIXME + throw new UnsupportedOperationException(); + } + + /** + * Transform the points in this path by matrix. + * + * @param matrix The matrix to apply to the path + */ + public void transform(Matrix matrix) { + transform(matrix, null /* dst */); + } +} diff --git a/tools/layoutlib/bridge/src/android/graphics/PorterDuffXfermode.java b/tools/layoutlib/bridge/src/android/graphics/PorterDuffXfermode.java new file mode 100644 index 0000000..974ae49 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/PorterDuffXfermode.java @@ -0,0 +1,40 @@ +/* + * 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.graphics; + +import android.graphics.PorterDuff.Mode; + +public class PorterDuffXfermode extends Xfermode { + private final Mode mMode; + + /** + * Create an xfermode that uses the specified porter-duff mode. + * + * @param mode The porter-duff mode that is applied + */ + public PorterDuffXfermode(PorterDuff.Mode mode) { + mMode = mode; + } + + //---------- Custom Methods + + public PorterDuff.Mode getMode() { + return mMode; + } + + //---------- +} diff --git a/tools/layoutlib/bridge/src/android/graphics/RadialGradient.java b/tools/layoutlib/bridge/src/android/graphics/RadialGradient.java new file mode 100644 index 0000000..61b693a --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/RadialGradient.java @@ -0,0 +1,62 @@ +/* + * 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.graphics; + +public class RadialGradient extends Shader { + + /** Create a shader that draws a radial gradient given the center and radius. + @param x The x-coordinate of the center of the radius + @param y The y-coordinate of the center of the radius + @param radius Must be positive. The radius of the circle for this gradient + @param colors The colors to be distributed between the center and edge of the circle + @param positions May be NULL. The relative position of + each corresponding color in the colors array. If this is NULL, + the the colors are distributed evenly between the center and edge of the circle. + @param tile The Shader tiling mode + */ + public RadialGradient(float x, float y, float radius, + int colors[], float positions[], TileMode tile) { + if (radius <= 0) { + throw new IllegalArgumentException("radius must be > 0"); + } + if (colors.length < 2) { + throw new IllegalArgumentException("needs >= 2 number of colors"); + } + if (positions != null && colors.length != positions.length) { + throw new IllegalArgumentException("color and position arrays must be of equal length"); + } + + // FIXME Implement shader + } + + /** Create a shader that draws a radial gradient given the center and radius. + @param x The x-coordinate of the center of the radius + @param y The y-coordinate of the center of the radius + @param radius Must be positive. The radius of the circle for this gradient + @param color0 The color at the center of the circle. + @param color1 The color at the edge of the circle. + @param tile The Shader tiling mode + */ + public RadialGradient(float x, float y, float radius, + int color0, int color1, TileMode tile) { + if (radius <= 0) { + throw new IllegalArgumentException("radius must be > 0"); + } + // FIXME Implement shader + } +} + diff --git a/tools/layoutlib/bridge/src/android/graphics/Shader.java b/tools/layoutlib/bridge/src/android/graphics/Shader.java new file mode 100644 index 0000000..3a9fda5 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/Shader.java @@ -0,0 +1,76 @@ +/* + * 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.graphics; + +/** + * Shader is the based class for objects that return horizontal spans of colors + * during drawing. A subclass of Shader is installed in a Paint calling + * paint.setShader(shader). After that any object (other than a bitmap) that is + * drawn with that paint will get its color(s) from the shader. + */ +public class Shader { + + private final Matrix mMatrix = new Matrix(); + + public enum TileMode { + /** + * replicate the edge color if the shader draws outside of its + * original bounds + */ + CLAMP (0), + /** + * repeat the shader's image horizontally and vertically + */ + REPEAT (1), + /** + * repeat the shader's image horizontally and vertically, alternating + * mirror images so that adjacent images always seam + */ + MIRROR (2); + + TileMode(int nativeInt) { + this.nativeInt = nativeInt; + } + final int nativeInt; + } + + /** + * Return true if the shader has a non-identity local matrix. + * @param localM If not null, it is set to the shader's local matrix. + * @return true if the shader has a non-identity local matrix + */ + public boolean getLocalMatrix(Matrix localM) { + if (localM != null) { + localM.set(mMatrix); + } + + return !mMatrix.isIdentity(); + } + + /** + * Set the shader's local matrix. Passing null will reset the shader's + * matrix to identity + * @param localM The shader's new local matrix, or null to specify identity + */ + public void setLocalMatrix(Matrix localM) { + if (localM != null) { + mMatrix.set(localM); + } else { + mMatrix.reset(); + } + } +} diff --git a/tools/layoutlib/bridge/src/android/graphics/SweepGradient.java b/tools/layoutlib/bridge/src/android/graphics/SweepGradient.java new file mode 100644 index 0000000..e79e970 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/SweepGradient.java @@ -0,0 +1,60 @@ +/* + * 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.graphics; + +public class SweepGradient extends Shader { + + /** + * A subclass of Shader that draws a sweep gradient around a center point. + * + * @param cx The x-coordinate of the center + * @param cy The y-coordinate of the center + * @param colors The colors to be distributed between around the center. + * There must be at least 2 colors in the array. + * @param positions May be NULL. The relative position of + * each corresponding color in the colors array, beginning + * with 0 and ending with 1.0. If the values are not + * monotonic, the drawing may produce unexpected results. + * If positions is NULL, then the colors are automatically + * spaced evenly. + */ + public SweepGradient(float cx, float cy, + int colors[], float positions[]) { + if (colors.length < 2) { + throw new IllegalArgumentException("needs >= 2 number of colors"); + } + if (positions != null && colors.length != positions.length) { + throw new IllegalArgumentException( + "color and position arrays must be of equal length"); + } + + // FIXME Implement shader + } + + /** + * A subclass of Shader that draws a sweep gradient around a center point. + * + * @param cx The x-coordinate of the center + * @param cy The y-coordinate of the center + * @param color0 The color to use at the start of the sweep + * @param color1 The color to use at the end of the sweep + */ + public SweepGradient(float cx, float cy, int color0, int color1) { + // FIXME Implement shader + } +} + diff --git a/tools/layoutlib/bridge/src/android/graphics/Typeface.java b/tools/layoutlib/bridge/src/android/graphics/Typeface.java new file mode 100644 index 0000000..e878b04 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/Typeface.java @@ -0,0 +1,180 @@ +/* + * 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.graphics; + +import com.android.layoutlib.bridge.FontLoader; + +import android.content.res.AssetManager; + +import java.awt.Font; + +/** + * Re-implementation of Typeface over java.awt + */ +public class Typeface { + private static final String DEFAULT_FAMILY = "sans-serif"; + private static final int[] styleBuffer = new int[1]; + + /** The default NORMAL typeface object */ + public static Typeface DEFAULT; + /** + * The default BOLD typeface object. Note: this may be not actually be + * bold, depending on what fonts are installed. Call getStyle() to know + * for sure. + */ + public static Typeface DEFAULT_BOLD; + /** The NORMAL style of the default sans serif typeface. */ + public static Typeface SANS_SERIF; + /** The NORMAL style of the default serif typeface. */ + public static Typeface SERIF; + /** The NORMAL style of the default monospace typeface. */ + public static Typeface MONOSPACE; + + private static Typeface[] sDefaults; + private static FontLoader mFontLoader; + + private final int mStyle; + private final Font mFont; + private final String mFamily; + + // Style + public static final int NORMAL = _Original_Typeface.NORMAL; + public static final int BOLD = _Original_Typeface.BOLD; + public static final int ITALIC = _Original_Typeface.ITALIC; + public static final int BOLD_ITALIC = _Original_Typeface.BOLD_ITALIC; + + /** + * Returns the underlying {@link Font} object. + */ + public Font getFont() { + return mFont; + } + + /** Returns the typeface's intrinsic style attributes */ + public int getStyle() { + return mStyle; + } + + /** Returns true if getStyle() has the BOLD bit set. */ + public final boolean isBold() { + return (getStyle() & BOLD) != 0; + } + + /** Returns true if getStyle() has the ITALIC bit set. */ + public final boolean isItalic() { + return (getStyle() & ITALIC) != 0; + } + + /** + * Create a typeface object given a family name, and option style information. + * If null is passed for the name, then the "default" font will be chosen. + * The resulting typeface object can be queried (getStyle()) to discover what + * its "real" style characteristics are. + * + * @param familyName May be null. The name of the font family. + * @param style The style (normal, bold, italic) of the typeface. + * e.g. NORMAL, BOLD, ITALIC, BOLD_ITALIC + * @return The best matching typeface. + */ + public static Typeface create(String familyName, int style) { + styleBuffer[0] = style; + Font font = mFontLoader.getFont(familyName, styleBuffer); + if (font != null) { + return new Typeface(familyName, styleBuffer[0], font); + } + + return null; + } + + /** + * Create a typeface object that best matches the specified existing + * typeface and the specified Style. Use this call if you want to pick a new + * style from the same family of an existing typeface object. If family is + * null, this selects from the default font's family. + * + * @param family May be null. The name of the existing type face. + * @param style The style (normal, bold, italic) of the typeface. + * e.g. NORMAL, BOLD, ITALIC, BOLD_ITALIC + * @return The best matching typeface. + */ + public static Typeface create(Typeface family, int style) { + styleBuffer[0] = style; + Font font = mFontLoader.getFont(family.mFamily, styleBuffer); + if (font != null) { + return new Typeface(family.mFamily, styleBuffer[0], font); + } + + return null; + } + + /** + * Returns one of the default typeface objects, based on the specified style + * + * @return the default typeface that corresponds to the style + */ + public static Typeface defaultFromStyle(int style) { + return sDefaults[style]; + } + + /** + * Create a new typeface from the specified font data. + * @param mgr The application's asset manager + * @param path The file name of the font data in the assets directory + * @return The new typeface. + */ + public static Typeface createFromAsset(AssetManager mgr, String path) { + return null; + //return new Typeface(nativeCreateFromAsset(mgr, path)); + } + + // don't allow clients to call this directly + private Typeface(String family, int style, Font f) { + mFamily = family; + mFont = f; + mStyle = style; + } + + public static void init(FontLoader fontLoader) { + mFontLoader = fontLoader; + + DEFAULT = create(DEFAULT_FAMILY, NORMAL); + DEFAULT_BOLD = create(DEFAULT_FAMILY, BOLD); + SANS_SERIF = create("sans-serif", NORMAL); + SERIF = create("serif", NORMAL); + MONOSPACE = create("monospace", NORMAL); + sDefaults = new Typeface[] { + DEFAULT, + DEFAULT_BOLD, + create(DEFAULT_FAMILY, ITALIC), + create(DEFAULT_FAMILY, BOLD_ITALIC), + }; + + /* + DEFAULT = create((String)null, 0); + DEFAULT_BOLD = create((String)null, Typeface.BOLD); + SANS_SERIF = create("sans-serif", 0); + SERIF = create("serif", 0); + MONOSPACE = create("monospace", 0); + + sDefaults = new Typeface[] { + DEFAULT, + DEFAULT_BOLD, + create((String)null, Typeface.ITALIC), + create((String)null, Typeface.BOLD_ITALIC), + };*/ + } +} diff --git a/tools/layoutlib/bridge/src/android/util/FloatMath.java b/tools/layoutlib/bridge/src/android/util/FloatMath.java new file mode 100644 index 0000000..aae44f2 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/util/FloatMath.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2007 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.util; + +/** + * Reimplements _Original_FloatMath with the standard libraries. + * + * Math routines similar to those found in {@link java.lang.Math}. Performs + * computations on {@code float} values directly without incurring the overhead + * of conversions to and from {@code double}. + * + * <p>On one platform, {@code FloatMath.sqrt(100)} executes in one third of the + * time required by {@code java.lang.Math.sqrt(100)}.</p> + */ +public class FloatMath { + + /** Prevents instantiation. */ + private FloatMath() {} + + /** + * Returns the float conversion of the most positive (i.e. closest to + * positive infinity) integer value which is less than the argument. + * + * @param value to be converted + * @return the floor of value + */ + public static float floor(float value) { + return (float)Math.floor(value); + } + + /** + * Returns the float conversion of the most negative (i.e. closest to + * negative infinity) integer value which is greater than the argument. + * + * @param value to be converted + * @return the ceiling of value + */ + public static float ceil(float value) { + return (float)Math.ceil(value); + } + + /** + * Returns the closest float approximation of the sine of the argument. + * + * @param angle to compute the cosine of, in radians + * @return the sine of angle + */ + public static float sin(float angle) { + return (float)Math.sin(angle); + } + + /** + * Returns the closest float approximation of the cosine of the argument. + * + * @param angle to compute the cosine of, in radians + * @return the cosine of angle + */ + public static float cos(float angle) { + return (float)Math.cos(angle); + } + + /** + * Returns the closest float approximation of the square root of the + * argument. + * + * @param value to compute sqrt of + * @return the square root of value + */ + public static float sqrt(float value) { + return (float)Math.sqrt(value); + } +} diff --git a/tools/layoutlib/bridge/src/android/view/BridgeInflater.java b/tools/layoutlib/bridge/src/android/view/BridgeInflater.java new file mode 100644 index 0000000..0910d79 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/view/BridgeInflater.java @@ -0,0 +1,227 @@ +/* + * 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.view; + +import com.android.layoutlib.api.IProjectCallback; +import com.android.layoutlib.api.IResourceValue; +import com.android.layoutlib.bridge.Bridge; +import com.android.layoutlib.bridge.BridgeConstants; +import com.android.layoutlib.bridge.BridgeContext; +import com.android.layoutlib.bridge.BridgeXmlBlockParser; + +import org.kxml2.io.KXmlParser; +import org.xmlpull.v1.XmlPullParser; + +import android.content.Context; +import android.util.AttributeSet; + +import java.io.File; +import java.io.FileReader; + +/** + * Custom implementation of {@link LayoutInflater} to handle custom views. + */ +public final class BridgeInflater extends LayoutInflater { + + private final IProjectCallback mProjectCallback; + + /** + * List of class prefixes which are tried first by default. + * <p/> + * This should match the list in com.android.internal.policy.impl.PhoneLayoutInflater. + */ + private static final String[] sClassPrefixList = { + "android.widget.", + "android.webkit." + }; + + protected BridgeInflater(LayoutInflater original, Context newContext) { + super(original, newContext); + mProjectCallback = null; + } + + /** + * Instantiate a new BridgeInflater with an {@link IProjectCallback} object. + * + * @param context The Android application context. + * @param projectCallback the {@link IProjectCallback} object. + */ + public BridgeInflater(Context context, IProjectCallback projectCallback) { + super(context); + mProjectCallback = projectCallback; + mConstructorArgs[0] = context; + } + + @Override + public View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException { + View view = null; + + try { + // First try to find a class using the default Android prefixes + for (String prefix : sClassPrefixList) { + try { + view = createView(name, prefix, attrs); + if (view != null) { + break; + } + } catch (ClassNotFoundException e) { + // Ignore. We'll try again using the base class below. + } + } + + // Next try using the parent loader. This will most likely only work for + // fully-qualified class names. + try { + if (view == null) { + view = super.onCreateView(name, attrs); + } + } catch (ClassNotFoundException e) { + // Ignore. We'll try again using the custom view loader below. + } + + // Finally try again using the custom view loader + try { + if (view == null) { + view = loadCustomView(name, attrs); + } + } catch (ClassNotFoundException e) { + // If the class was not found, we throw the exception directly, because this + // method is already expected to throw it. + throw e; + } + } catch (Exception e) { + // Wrap the real exception in a ClassNotFoundException, so that the calling method + // can deal with it. + ClassNotFoundException exception = new ClassNotFoundException("onCreateView", e); + throw exception; + } + + setupViewInContext(view, attrs); + + return view; + } + + @Override + public View createViewFromTag(String name, AttributeSet attrs) { + View view = null; + try { + view = super.createViewFromTag(name, attrs); + } catch (InflateException e) { + // try to load the class from using the custom view loader + try { + view = loadCustomView(name, attrs); + } catch (Exception e2) { + // Wrap the real exception in an InflateException so that the calling + // method can deal with it. + InflateException exception = new InflateException(); + if (e2.getClass().equals(ClassNotFoundException.class) == false) { + exception.initCause(e2); + } else { + exception.initCause(e); + } + throw exception; + } + } + + setupViewInContext(view, attrs); + + return view; + } + + @Override + public View inflate(int resource, ViewGroup root) { + Context context = getContext(); + if (context instanceof BridgeContext) { + BridgeContext bridgeContext = (BridgeContext)context; + + IResourceValue value = null; + + String[] layoutInfo = Bridge.resolveResourceValue(resource); + if (layoutInfo != null) { + value = bridgeContext.getFrameworkResource(BridgeConstants.RES_LAYOUT, + layoutInfo[0]); + } else { + layoutInfo = mProjectCallback.resolveResourceValue(resource); + + if (layoutInfo != null) { + value = bridgeContext.getProjectResource(BridgeConstants.RES_LAYOUT, + layoutInfo[0]); + } + } + + if (value != null) { + File f = new File(value.getValue()); + if (f.isFile()) { + try { + KXmlParser parser = new KXmlParser(); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + parser.setInput(new FileReader(f)); + + BridgeXmlBlockParser bridgeParser = new BridgeXmlBlockParser( + parser, bridgeContext, false); + + return inflate(bridgeParser, root); + } catch (Exception e) { + bridgeContext.getLogger().error(e); + // return null below. + } + } + } + } + return null; + } + + private View loadCustomView(String name, AttributeSet attrs) throws ClassNotFoundException, + Exception{ + if (mProjectCallback != null) { + // first get the classname in case it's not the node name + if (name.equals("view")) { + name = attrs.getAttributeValue(null, "class"); + } + + mConstructorArgs[1] = attrs; + + Object customView = mProjectCallback.loadView(name, mConstructorSignature, + mConstructorArgs); + + if (customView instanceof View) { + return (View)customView; + } + } + + return null; + } + + + + private void setupViewInContext(View view, AttributeSet attrs) { + if (getContext() instanceof BridgeContext) { + BridgeContext bc = (BridgeContext) getContext(); + if (attrs instanceof BridgeXmlBlockParser) { + Object viewKey = ((BridgeXmlBlockParser) attrs).getViewKey(); + if (viewKey != null) { + bc.addViewKey(view, viewKey); + } + } + } + } + + @Override + public LayoutInflater cloneInContext(Context newContext) { + return new BridgeInflater(this, newContext); + } +} diff --git a/tools/layoutlib/bridge/src/android/view/SurfaceView.java b/tools/layoutlib/bridge/src/android/view/SurfaceView.java new file mode 100644 index 0000000..ce32da9 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/view/SurfaceView.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2006 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.view; + +import com.android.layoutlib.bridge.MockView; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.util.AttributeSet; + +/** + * Mock version of the SurfaceView. + * Only non override public methods from the real SurfaceView have been added in there. + * Methods that take an unknown class as parameter or as return object, have been removed for now. + * + * TODO: generate automatically. + * + */ +public class SurfaceView extends MockView { + + public SurfaceView(Context context) { + this(context, null); + } + + public SurfaceView(Context context, AttributeSet attrs) { + this(context, attrs , 0); + } + + public SurfaceView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public SurfaceHolder getHolder() { + return mSurfaceHolder; + } + + private SurfaceHolder mSurfaceHolder = new SurfaceHolder() { + + public boolean isCreating() { + return false; + } + + public void addCallback(Callback callback) { + } + + public void removeCallback(Callback callback) { + } + + public void setFixedSize(int width, int height) { + } + + public void setSizeFromLayout() { + } + + public void setFormat(int format) { + } + + public void setType(int type) { + } + + public void setKeepScreenOn(boolean screenOn) { + } + + public Canvas lockCanvas() { + return null; + } + + public Canvas lockCanvas(Rect dirty) { + return null; + } + + public void unlockCanvasAndPost(Canvas canvas) { + } + + public Surface getSurface() { + return null; + } + + public Rect getSurfaceFrame() { + return null; + } + }; +} + diff --git a/tools/layoutlib/bridge/src/android/webkit/WebView.java b/tools/layoutlib/bridge/src/android/webkit/WebView.java new file mode 100644 index 0000000..42e4dfa --- /dev/null +++ b/tools/layoutlib/bridge/src/android/webkit/WebView.java @@ -0,0 +1,261 @@ +/* + * 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.webkit; + +import com.android.layoutlib.bridge.MockView; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Picture; +import android.os.Bundle; +import android.os.Message; +import android.util.AttributeSet; +import android.view.View; + +/** + * Mock version of the WebView. + * Only non override public methods from the real WebView have been added in there. + * Methods that take an unknown class as parameter or as return object, have been removed for now. + * + * TODO: generate automatically. + * + */ +public class WebView extends MockView { + + /** + * Construct a new WebView with a Context object. + * @param context A Context object used to access application assets. + */ + public WebView(Context context) { + this(context, null); + } + + /** + * Construct a new WebView with layout parameters. + * @param context A Context object used to access application assets. + * @param attrs An AttributeSet passed to our parent. + */ + public WebView(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.webViewStyle); + } + + /** + * Construct a new WebView with layout parameters and a default style. + * @param context A Context object used to access application assets. + * @param attrs An AttributeSet passed to our parent. + * @param defStyle The default style resource ID. + */ + public WebView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + // START FAKE PUBLIC METHODS + + public void setHorizontalScrollbarOverlay(boolean overlay) { + } + + public void setVerticalScrollbarOverlay(boolean overlay) { + } + + public boolean overlayHorizontalScrollbar() { + return false; + } + + public boolean overlayVerticalScrollbar() { + return false; + } + + public void savePassword(String host, String username, String password) { + } + + public void setHttpAuthUsernamePassword(String host, String realm, + String username, String password) { + } + + public String[] getHttpAuthUsernamePassword(String host, String realm) { + return null; + } + + public void destroy() { + } + + public static void enablePlatformNotifications() { + } + + public static void disablePlatformNotifications() { + } + + public WebBackForwardList saveState(Bundle outState) { + return null; + } + + public WebBackForwardList restoreState(Bundle inState) { + return null; + } + + public void loadUrl(String url) { + } + + public void loadData(String data, String mimeType, String encoding) { + } + + public void loadDataWithBaseURL(String baseUrl, String data, + String mimeType, String encoding, String failUrl) { + } + + public void stopLoading() { + } + + public void reload() { + } + + public boolean canGoBack() { + return false; + } + + public void goBack() { + } + + public boolean canGoForward() { + return false; + } + + public void goForward() { + } + + public boolean canGoBackOrForward(int steps) { + return false; + } + + public void goBackOrForward(int steps) { + } + + public boolean pageUp(boolean top) { + return false; + } + + public boolean pageDown(boolean bottom) { + return false; + } + + public void clearView() { + } + + public Picture capturePicture() { + return null; + } + + public float getScale() { + return 0; + } + + public void setInitialScale(int scaleInPercent) { + } + + public void invokeZoomPicker() { + } + + public void requestFocusNodeHref(Message hrefMsg) { + } + + public void requestImageRef(Message msg) { + } + + public String getUrl() { + return null; + } + + public String getTitle() { + return null; + } + + public Bitmap getFavicon() { + return null; + } + + public int getProgress() { + return 0; + } + + public int getContentHeight() { + return 0; + } + + public void pauseTimers() { + } + + public void resumeTimers() { + } + + public void clearCache() { + } + + public void clearFormData() { + } + + public void clearHistory() { + } + + public void clearSslPreferences() { + } + + public WebBackForwardList copyBackForwardList() { + return null; + } + + public static String findAddress(String addr) { + return null; + } + + public void documentHasImages(Message response) { + } + + public void setWebViewClient(WebViewClient client) { + } + + public void setDownloadListener(DownloadListener listener) { + } + + public void setWebChromeClient(WebChromeClient client) { + } + + public void addJavascriptInterface(Object obj, String interfaceName) { + } + + public WebSettings getSettings() { + return null; + } + + public static synchronized PluginList getPluginList() { + return null; + } + + public void refreshPlugins(boolean reloadOpenPages) { + } + + public View getZoomControls() { + return null; + } + + public boolean zoomIn() { + return false; + } + + public boolean zoomOut() { + return false; + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/Bridge.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/Bridge.java new file mode 100644 index 0000000..6abc452d --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/Bridge.java @@ -0,0 +1,932 @@ +/* + * 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 com.android.layoutlib.bridge; + +import com.android.internal.util.XmlUtils; +import com.android.layoutlib.api.ILayoutBridge; +import com.android.layoutlib.api.ILayoutLog; +import com.android.layoutlib.api.ILayoutResult; +import com.android.layoutlib.api.IProjectCallback; +import com.android.layoutlib.api.IResourceValue; +import com.android.layoutlib.api.IStyleResourceValue; +import com.android.layoutlib.api.IXmlPullParser; +import com.android.layoutlib.api.ILayoutResult.ILayoutViewInfo; +import com.android.layoutlib.bridge.LayoutResult.LayoutViewInfo; +import com.android.ninepatch.NinePatch; +import com.android.tools.layoutlib.create.MethodAdapter; +import com.android.tools.layoutlib.create.OverrideMethod; + +import android.graphics.Bitmap; +import android.graphics.Rect; +import android.graphics.Region; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import android.view.BridgeInflater; +import android.view.IWindow; +import android.view.IWindowSession; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.Surface; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewGroup; +import android.view.View.AttachInfo; +import android.view.View.MeasureSpec; +import android.view.WindowManager.LayoutParams; +import android.widget.FrameLayout; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +/** + * Main entry point of the LayoutLib Bridge. + * <p/>To use this bridge, simply instantiate an object of type {@link Bridge} and call + * {@link #computeLayout(IXmlPullParser, Object, int, int, String, boolean, Map, Map, IProjectCallback, ILayoutLog)}. + */ +public final class Bridge implements ILayoutBridge { + + private static final int DEFAULT_TITLE_BAR_HEIGHT = 25; + private static final int DEFAULT_STATUS_BAR_HEIGHT = 25; + + public static class StaticMethodNotImplementedException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public StaticMethodNotImplementedException(String msg) { + super(msg); + } + } + + /** + * Maps from id to resource name/type. + */ + private final static Map<Integer, String[]> sRMap = new HashMap<Integer, String[]>(); + /** + * Same as sRMap except for int[] instead of int resources. + */ + private final static Map<int[], String> sRArrayMap = new HashMap<int[], String>(); + /** + * Reverse map compared to sRMap, resource type -> (resource name -> id) + */ + private final static Map<String, Map<String, Integer>> sRFullMap = + new HashMap<String, Map<String,Integer>>(); + + private final static Map<Object, Map<String, Bitmap>> sProjectBitmapCache = + new HashMap<Object, Map<String, Bitmap>>(); + private final static Map<Object, Map<String, NinePatch>> sProject9PatchCache = + new HashMap<Object, Map<String, NinePatch>>(); + + private final static Map<String, Bitmap> sFrameworkBitmapCache = new HashMap<String, Bitmap>(); + private final static Map<String, NinePatch> sFramework9PatchCache = + new HashMap<String, NinePatch>(); + + private static Map<String, Map<String, Integer>> sEnumValueMap; + + /** + * A default logger than prints to stdout/stderr. + */ + private final static ILayoutLog sDefaultLogger = new ILayoutLog() { + public void error(String message) { + System.err.println(message); + } + + public void error(Throwable t) { + String message = t.getMessage(); + if (message == null) { + message = t.getClass().getName(); + } + + System.err.println(message); + } + + public void warning(String message) { + System.out.println(message); + } + }; + + /** + * Logger defined during a compute layout operation. + * <p/> + * This logger is generally set to {@link #sDefaultLogger} except during rendering + * operations when it might be set to a specific provided logger. + * <p/> + * To change this value, use a block synchronized on {@link #sDefaultLogger}. + */ + private static ILayoutLog sLogger = sDefaultLogger; + + /* + * (non-Javadoc) + * @see com.android.layoutlib.api.ILayoutBridge#getApiLevel() + */ + public int getApiLevel() { + return API_CURRENT; + } + + /* + * (non-Javadoc) + * @see com.android.layoutlib.api.ILayoutLibBridge#init(java.lang.String, java.util.Map) + */ + public boolean init( + String fontOsLocation, Map<String, Map<String, Integer>> enumValueMap) { + + return sinit(fontOsLocation, enumValueMap); + } + + private static synchronized boolean sinit(String fontOsLocation, + Map<String, Map<String, Integer>> enumValueMap) { + + // When DEBUG_LAYOUT is set and is not 0 or false, setup a default listener + // on static (native) methods which prints the signature on the console and + // throws an exception. + // This is useful when testing the rendering in ADT to identify static native + // methods that are ignored -- layoutlib_create makes them returns 0/false/null + // which is generally OK yet might be a problem, so this is how you'd find out. + // + // Currently layoutlib_create only overrides static native method. + // Static non-natives are not overridden and thus do not get here. + final String debug = System.getenv("DEBUG_LAYOUT"); + if (debug != null && !debug.equals("0") && !debug.equals("false")) { + + OverrideMethod.setDefaultListener(new MethodAdapter() { + @Override + public void onInvokeV(String signature, boolean isNative, Object caller) { + if (sLogger != null) { + synchronized (sDefaultLogger) { + sLogger.error("Missing Stub: " + signature + + (isNative ? " (native)" : "")); + } + } + + if (debug.equalsIgnoreCase("throw")) { + // Throwing this exception doesn't seem that useful. It breaks + // the layout editor yet doesn't display anything meaningful to the + // user. Having the error in the console is just as useful. We'll + // throw it only if the environment variable is "throw" or "THROW". + throw new StaticMethodNotImplementedException(signature); + } + } + }); + } + + // Override View.isInEditMode to return true. + // + // This allows custom views that are drawn in the Graphical Layout Editor to adapt their + // rendering for preview. Most important this let custom views know that they can't expect + // the rest of their activities to be alive. + OverrideMethod.setMethodListener("android.view.View#isInEditMode()Z", + new MethodAdapter() { + @Override + public int onInvokeI(String signature, boolean isNative, Object caller) { + return 1; + } + } + ); + + // load the fonts. + FontLoader fontLoader = FontLoader.create(fontOsLocation); + if (fontLoader != null) { + Typeface.init(fontLoader); + } else { + return false; + } + + sEnumValueMap = enumValueMap; + + // now parse com.android.internal.R (and only this one as android.R is a subset of + // the internal version), and put the content in the maps. + try { + // WARNING: this only works because the class is already loaded, and therefore + // the objects returned by Field.get() are the same as the ones used by + // the code accessing the R class. + // int[] does not implement equals/hashCode, and if the parsing used a different class + // loader for the R class, this would NOT work. + Class<?> r = com.android.internal.R.class; + + for (Class<?> inner : r.getDeclaredClasses()) { + String resType = inner.getSimpleName(); + + Map<String, Integer> fullMap = new HashMap<String, Integer>(); + sRFullMap.put(resType, fullMap); + + for (Field f : inner.getDeclaredFields()) { + // only process static final fields. Since the final attribute may have + // been altered by layoutlib_create, we only check static + int modifiers = f.getModifiers(); + if (Modifier.isStatic(modifiers)) { + Class<?> type = f.getType(); + if (type.isArray() && type.getComponentType() == int.class) { + // if the object is an int[] we put it in sRArrayMap + sRArrayMap.put((int[]) f.get(null), f.getName()); + } else if (type == int.class) { + Integer value = (Integer) f.get(null); + sRMap.put(value, new String[] { f.getName(), resType }); + fullMap.put(f.getName(), value); + } else { + assert false; + } + } + } + } + } catch (IllegalArgumentException e) { + // FIXME: log/return the error (there's no logger object at this point!) + e.printStackTrace(); + return false; + } catch (IllegalAccessException e) { + e.printStackTrace(); + return false; + } + + return true; + } + + /* + * For compatilibty purposes, we implement the old deprecated version of computeLayout. + * (non-Javadoc) + * @see com.android.layoutlib.api.ILayoutBridge#computeLayout(com.android.layoutlib.api.IXmlPullParser, java.lang.Object, int, int, java.lang.String, java.util.Map, java.util.Map, com.android.layoutlib.api.IProjectCallback, com.android.layoutlib.api.ILayoutLog) + */ + @Deprecated + public ILayoutResult computeLayout(IXmlPullParser layoutDescription, + Object projectKey, + int screenWidth, int screenHeight, String themeName, + Map<String, Map<String, IResourceValue>> projectResources, + Map<String, Map<String, IResourceValue>> frameworkResources, + IProjectCallback customViewLoader, ILayoutLog logger) { + boolean isProjectTheme = false; + if (themeName.charAt(0) == '*') { + themeName = themeName.substring(1); + isProjectTheme = true; + } + + return computeLayout(layoutDescription, projectKey, + screenWidth, screenHeight, DisplayMetrics.DEFAULT_DENSITY, + DisplayMetrics.DEFAULT_DENSITY, DisplayMetrics.DEFAULT_DENSITY, + themeName, isProjectTheme, + projectResources, frameworkResources, customViewLoader, logger); + } + + /* + * For compatilibty purposes, we implement the old deprecated version of computeLayout. + * (non-Javadoc) + * @see com.android.layoutlib.api.ILayoutBridge#computeLayout(com.android.layoutlib.api.IXmlPullParser, java.lang.Object, int, int, java.lang.String, boolean, java.util.Map, java.util.Map, com.android.layoutlib.api.IProjectCallback, com.android.layoutlib.api.ILayoutLog) + */ + public ILayoutResult computeLayout(IXmlPullParser layoutDescription, Object projectKey, + int screenWidth, int screenHeight, String themeName, boolean isProjectTheme, + Map<String, Map<String, IResourceValue>> projectResources, + Map<String, Map<String, IResourceValue>> frameworkResources, + IProjectCallback customViewLoader, ILayoutLog logger) { + return computeLayout(layoutDescription, projectKey, + screenWidth, screenHeight, DisplayMetrics.DEFAULT_DENSITY, + DisplayMetrics.DEFAULT_DENSITY, DisplayMetrics.DEFAULT_DENSITY, + themeName, isProjectTheme, + projectResources, frameworkResources, customViewLoader, logger); + } + + /* + * (non-Javadoc) + * @see com.android.layoutlib.api.ILayoutBridge#computeLayout(com.android.layoutlib.api.IXmlPullParser, java.lang.Object, int, int, int, float, float, java.lang.String, boolean, java.util.Map, java.util.Map, com.android.layoutlib.api.IProjectCallback, com.android.layoutlib.api.ILayoutLog) + */ + public ILayoutResult computeLayout(IXmlPullParser layoutDescription, Object projectKey, + int screenWidth, int screenHeight, int density, float xdpi, float ydpi, + String themeName, boolean isProjectTheme, + Map<String, Map<String, IResourceValue>> projectResources, + Map<String, Map<String, IResourceValue>> frameworkResources, + IProjectCallback customViewLoader, ILayoutLog logger) { + if (logger == null) { + logger = sDefaultLogger; + } + + synchronized (sDefaultLogger) { + sLogger = logger; + } + + // find the current theme and compute the style inheritance map + Map<IStyleResourceValue, IStyleResourceValue> styleParentMap = + new HashMap<IStyleResourceValue, IStyleResourceValue>(); + + IStyleResourceValue currentTheme = computeStyleMaps(themeName, isProjectTheme, + projectResources.get(BridgeConstants.RES_STYLE), + frameworkResources.get(BridgeConstants.RES_STYLE), styleParentMap); + + BridgeContext context = null; + try { + // setup the display Metrics. + DisplayMetrics metrics = new DisplayMetrics(); + metrics.density = density / (float) DisplayMetrics.DEFAULT_DENSITY; + metrics.scaledDensity = metrics.density; + metrics.widthPixels = screenWidth; + metrics.heightPixels = screenHeight; + metrics.xdpi = xdpi; + metrics.ydpi = ydpi; + + context = new BridgeContext(projectKey, metrics, currentTheme, projectResources, + frameworkResources, styleParentMap, customViewLoader, logger); + BridgeInflater inflater = new BridgeInflater(context, customViewLoader); + context.setBridgeInflater(inflater); + + IResourceValue windowBackground = null; + int screenOffset = 0; + if (currentTheme != null) { + windowBackground = context.findItemInStyle(currentTheme, "windowBackground"); + windowBackground = context.resolveResValue(windowBackground); + + screenOffset = getScreenOffset(currentTheme, context); + } + + // we need to make sure the Looper has been initialized for this thread. + // this is required for View that creates Handler objects. + if (Looper.myLooper() == null) { + Looper.prepare(); + } + + BridgeXmlBlockParser parser = new BridgeXmlBlockParser(layoutDescription, + context, false /* platformResourceFlag */); + + ViewGroup root = new FrameLayout(context); + + View view = inflater.inflate(parser, root); + + // set the AttachInfo on the root view. + AttachInfo info = new AttachInfo(new WindowSession(), new Window(), + new Handler(), null); + info.mHasWindowFocus = true; + info.mWindowVisibility = View.VISIBLE; + info.mInTouchMode = false; // this is so that we can display selections. + root.dispatchAttachedToWindow(info, 0); + + // get the background drawable + if (windowBackground != null) { + Drawable d = ResourceHelper.getDrawable(windowBackground.getValue(), + context, true /* isFramework */); + root.setBackgroundDrawable(d); + } + + int w_spec = MeasureSpec.makeMeasureSpec(screenWidth, MeasureSpec.EXACTLY); + int h_spec = MeasureSpec.makeMeasureSpec(screenHeight - screenOffset, + MeasureSpec.EXACTLY); + + // measure the views + view.measure(w_spec, h_spec); + view.layout(0, screenOffset, screenWidth, screenHeight); + + // draw them + BridgeCanvas canvas = new BridgeCanvas(screenWidth, screenHeight - screenOffset, + logger); + + root.draw(canvas); + canvas.dispose(); + + return new LayoutResult(visit(((ViewGroup)view).getChildAt(0), context), + canvas.getImage()); + } catch (Throwable e) { + // get the real cause of the exception. + Throwable t = e; + while (t.getCause() != null) { + t = t.getCause(); + } + + // log it + logger.error(t); + + // then return with an ERROR status and the message from the real exception + return new LayoutResult(ILayoutResult.ERROR, + t.getClass().getSimpleName() + ": " + t.getMessage()); + } finally { + // Make sure to remove static references, otherwise we could not unload the lib + BridgeResources.clearSystem(); + BridgeAssetManager.clearSystem(); + + // Remove the global logger + synchronized (sDefaultLogger) { + sLogger = sDefaultLogger; + } + } + } + + /* + * (non-Javadoc) + * @see com.android.layoutlib.api.ILayoutLibBridge#clearCaches(java.lang.Object) + */ + public void clearCaches(Object projectKey) { + if (projectKey != null) { + sProjectBitmapCache.remove(projectKey); + sProject9PatchCache.remove(projectKey); + } + } + + /** + * Returns details of a framework resource from its integer value. + * @param value the integer value + * @return an array of 2 strings containing the resource name and type, or null if the id + * does not match any resource. + */ + public static String[] resolveResourceValue(int value) { + return sRMap.get(value); + + } + + /** + * Returns the name of a framework resource whose value is an int array. + * @param array + */ + public static String resolveResourceValue(int[] array) { + return sRArrayMap.get(array); + } + + /** + * Returns the integer id of a framework resource, from a given resource type and resource name. + * @param type the type of the resource + * @param name the name of the resource. + * @return an {@link Integer} containing the resource id, or null if no resource were found. + */ + public static Integer getResourceValue(String type, String name) { + Map<String, Integer> map = sRFullMap.get(type); + if (map != null) { + return map.get(name); + } + + return null; + } + + static Map<String, Integer> getEnumValues(String attributeName) { + if (sEnumValueMap != null) { + return sEnumValueMap.get(attributeName); + } + + return null; + } + + /** + * Visits a View and its children and generate a {@link ILayoutViewInfo} containing the + * bounds of all the views. + * @param view the root View + * @param context the context. + */ + private ILayoutViewInfo visit(View view, BridgeContext context) { + if (view == null) { + return null; + } + + LayoutViewInfo result = new LayoutViewInfo(view.getClass().getName(), + context.getViewKey(view), + view.getLeft(), view.getTop(), view.getRight(), view.getBottom()); + + if (view instanceof ViewGroup) { + ViewGroup group = ((ViewGroup) view); + int n = group.getChildCount(); + ILayoutViewInfo[] children = new ILayoutViewInfo[n]; + for (int i = 0; i < group.getChildCount(); i++) { + children[i] = visit(group.getChildAt(i), context); + } + result.setChildren(children); + } + + return result; + } + + /** + * 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 IStyleResourceValue} matching <var>themeName</var> + */ + private IStyleResourceValue computeStyleMaps( + String themeName, boolean isProjectTheme, Map<String, + IResourceValue> inProjectStyleMap, Map<String, IResourceValue> inFrameworkStyleMap, + Map<IStyleResourceValue, IStyleResourceValue> outInheritanceMap) { + + if (inProjectStyleMap != null && inFrameworkStyleMap != null) { + // first, get the theme + IResourceValue theme = null; + + // project theme names have been prepended with a * + if (isProjectTheme) { + theme = inProjectStyleMap.get(themeName); + } else { + theme = inFrameworkStyleMap.get(themeName); + } + + if (theme instanceof IStyleResourceValue) { + // compute the inheritance map for both the project and framework styles + computeStyleInheritance(inProjectStyleMap.values(), inProjectStyleMap, + inFrameworkStyleMap, outInheritanceMap); + + // Compute the style inheritance for the framework styles/themes. + // Since, for those, the style parent values do not contain 'android:' + // we want to force looking in the framework style only to avoid using + // similarly named styles from the project. + // To do this, we pass null in lieu of the project style map. + computeStyleInheritance(inFrameworkStyleMap.values(), null /*inProjectStyleMap */, + inFrameworkStyleMap, outInheritanceMap); + + return (IStyleResourceValue)theme; + } + } + + return null; + } + + /** + * Compute the parent style for all the styles in a given list. + * @param styles the styles for which we compute the parent. + * @param inProjectStyleMap the map of project styles. + * @param inFrameworkStyleMap the map of framework styles. + * @param outInheritanceMap the map of style inheritance. This is filled by the method. + */ + private void computeStyleInheritance(Collection<IResourceValue> styles, + Map<String, IResourceValue> inProjectStyleMap, + Map<String, IResourceValue> inFrameworkStyleMap, + Map<IStyleResourceValue, IStyleResourceValue> outInheritanceMap) { + for (IResourceValue value : styles) { + if (value instanceof IStyleResourceValue) { + IStyleResourceValue style = (IStyleResourceValue)value; + IStyleResourceValue parentStyle = null; + + // first look for a specified parent. + String parentName = style.getParentStyle(); + + // no specified parent? try to infer it from the name of the style. + if (parentName == null) { + parentName = getParentName(value.getName()); + } + + if (parentName != null) { + parentStyle = getStyle(parentName, inProjectStyleMap, inFrameworkStyleMap); + + if (parentStyle != null) { + outInheritanceMap.put(style, parentStyle); + } + } + } + } + } + + /** + * Searches for and returns the {@link IStyleResourceValue} from a given name. + * <p/>The format of the name can be: + * <ul> + * <li>[android:]<name></li> + * <li>[android:]style/<name></li> + * <li>@[android:]style/<name></li> + * </ul> + * @param parentName the name of the style. + * @param inProjectStyleMap the project style map. Can be <code>null</code> + * @param inFrameworkStyleMap the framework style map. + * @return The matching {@link IStyleResourceValue} object or <code>null</code> if not found. + */ + private IStyleResourceValue getStyle(String parentName, + Map<String, IResourceValue> inProjectStyleMap, + Map<String, IResourceValue> inFrameworkStyleMap) { + boolean frameworkOnly = false; + + String name = parentName; + + // remove the useless @ if it's there + if (name.startsWith(BridgeConstants.PREFIX_RESOURCE_REF)) { + name = name.substring(BridgeConstants.PREFIX_RESOURCE_REF.length()); + } + + // check for framework identifier. + if (name.startsWith(BridgeConstants.PREFIX_ANDROID)) { + frameworkOnly = true; + name = name.substring(BridgeConstants.PREFIX_ANDROID.length()); + } + + // at this point we could have the format style/<name>. we want only the name + if (name.startsWith(BridgeConstants.REFERENCE_STYLE)) { + name = name.substring(BridgeConstants.REFERENCE_STYLE.length()); + } + + IResourceValue parent = null; + + // if allowed, search in the project resources. + if (frameworkOnly == false && inProjectStyleMap != null) { + parent = inProjectStyleMap.get(name); + } + + // if not found, then look in the framework resources. + if (parent == null) { + parent = inFrameworkStyleMap.get(name); + } + + // make sure the result is the proper class type and return it. + if (parent instanceof IStyleResourceValue) { + return (IStyleResourceValue)parent; + } + + sLogger.error(String.format("Unable to resolve parent style name: ", parentName)); + + return null; + } + + /** + * Computes the name of the parent style, or <code>null</code> 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. + * @return the pixel height offset + */ + private int getScreenOffset(IStyleResourceValue currentTheme, BridgeContext context) { + int offset = 0; + + // get the title bar flag from the current theme. + IResourceValue value = context.findItemInStyle(currentTheme, "windowNoTitle"); + + // because it may reference something else, we resolve it. + value = context.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) { + // get value from the theme. + value = context.findItemInStyle(currentTheme, "windowTitleSize"); + + // resolve it + value = context.resolveResValue(value); + + // default value + offset = DEFAULT_TITLE_BAR_HEIGHT; + + // get the real value; + if (value != null) { + TypedValue typedValue = ResourceHelper.getValue(value.getValue()); + if (typedValue != null) { + offset = (int)typedValue.getDimension(context.getResources().mMetrics); + } + } + } + + // get the fullscreen flag from the current theme. + value = context.findItemInStyle(currentTheme, "windowFullscreen"); + + // because it may reference something else, we resolve it. + value = context.resolveResValue(value); + + if (value == null || value.getValue() == null || + XmlUtils.convertValueToBoolean(value.getValue(), false /* defValue */) == false) { + // FIXME: Right now this is hard-coded in the platform, but once there's a constant, we'll need to use it. + offset += DEFAULT_STATUS_BAR_HEIGHT; + } + + return offset; + } + + /** + * Returns the bitmap for a specific path, from a specific project cache, or from the + * framework cache. + * @param value the path of the bitmap + * @param projectKey the key of the project, or null to query the framework cache. + * @return the cached Bitmap or null if not found. + */ + static Bitmap getCachedBitmap(String value, Object projectKey) { + if (projectKey != null) { + Map<String, Bitmap> map = sProjectBitmapCache.get(projectKey); + if (map != null) { + return map.get(value); + } + + return null; + } + + return sFrameworkBitmapCache.get(value); + } + + /** + * Sets a bitmap in a project cache or in the framework cache. + * @param value the path of the bitmap + * @param bmp the Bitmap object + * @param projectKey the key of the project, or null to put the bitmap in the framework cache. + */ + static void setCachedBitmap(String value, Bitmap bmp, Object projectKey) { + if (projectKey != null) { + Map<String, Bitmap> map = sProjectBitmapCache.get(projectKey); + + if (map == null) { + map = new HashMap<String, Bitmap>(); + sProjectBitmapCache.put(projectKey, map); + } + + map.put(value, bmp); + } + + sFrameworkBitmapCache.put(value, bmp); + } + + /** + * Returns the 9 patch for a specific path, from a specific project cache, or from the + * framework cache. + * @param value the path of the 9 patch + * @param projectKey the key of the project, or null to query the framework cache. + * @return the cached 9 patch or null if not found. + */ + static NinePatch getCached9Patch(String value, Object projectKey) { + if (projectKey != null) { + Map<String, NinePatch> map = sProject9PatchCache.get(projectKey); + + if (map != null) { + return map.get(value); + } + + return null; + } + + return sFramework9PatchCache.get(value); + } + + /** + * Sets a 9 patch in a project cache or in the framework cache. + * @param value the path of the 9 patch + * @param ninePatch the 9 patch object + * @param projectKey the key of the project, or null to put the bitmap in the framework cache. + */ + static void setCached9Patch(String value, NinePatch ninePatch, Object projectKey) { + if (projectKey != null) { + Map<String, NinePatch> map = sProject9PatchCache.get(projectKey); + + if (map == null) { + map = new HashMap<String, NinePatch>(); + sProject9PatchCache.put(projectKey, map); + } + + map.put(value, ninePatch); + } + + sFramework9PatchCache.put(value, ninePatch); + } + + /** + * Implementation of {@link IWindowSession} so that mSession is not null in + * the {@link SurfaceView}. + */ + private static final class WindowSession implements IWindowSession { + + @SuppressWarnings("unused") + public int add(IWindow arg0, LayoutParams arg1, int arg2, Rect arg3) + throws RemoteException { + // pass for now. + return 0; + } + + @SuppressWarnings("unused") + public void finishDrawing(IWindow arg0) throws RemoteException { + // pass for now. + } + + @SuppressWarnings("unused") + public void finishKey(IWindow arg0) throws RemoteException { + // pass for now. + } + + @SuppressWarnings("unused") + public boolean getInTouchMode() throws RemoteException { + // pass for now. + return false; + } + + @SuppressWarnings("unused") + public boolean performHapticFeedback(IWindow window, int effectId, boolean always) { + // pass for now. + return false; + } + + @SuppressWarnings("unused") + public MotionEvent getPendingPointerMove(IWindow arg0) throws RemoteException { + // pass for now. + return null; + } + + @SuppressWarnings("unused") + public MotionEvent getPendingTrackballMove(IWindow arg0) throws RemoteException { + // pass for now. + return null; + } + + @SuppressWarnings("unused") + public int relayout(IWindow arg0, LayoutParams arg1, int arg2, int arg3, int arg4, + boolean arg4_5, Rect arg5, Rect arg6, Rect arg7, Surface arg8) + throws RemoteException { + // pass for now. + return 0; + } + + public void getDisplayFrame(IWindow window, Rect outDisplayFrame) { + // pass for now. + } + + @SuppressWarnings("unused") + public void remove(IWindow arg0) throws RemoteException { + // pass for now. + } + + @SuppressWarnings("unused") + public void setInTouchMode(boolean arg0) throws RemoteException { + // pass for now. + } + + @SuppressWarnings("unused") + public void setTransparentRegion(IWindow arg0, Region arg1) throws RemoteException { + // pass for now. + } + + public void setInsets(IWindow window, int touchable, Rect contentInsets, + Rect visibleInsets) { + // pass for now. + } + + public IBinder asBinder() { + // pass for now. + return null; + } + } + + /** + * Implementation of {@link IWindow} to pass to the {@link AttachInfo}. + */ + private static final class Window implements IWindow { + + @SuppressWarnings("unused") + public void dispatchAppVisibility(boolean arg0) throws RemoteException { + // pass for now. + } + + @SuppressWarnings("unused") + public void dispatchGetNewSurface() throws RemoteException { + // pass for now. + } + + @SuppressWarnings("unused") + public void dispatchKey(KeyEvent arg0) throws RemoteException { + // pass for now. + } + + @SuppressWarnings("unused") + public void dispatchPointer(MotionEvent arg0, long arg1) throws RemoteException { + // pass for now. + } + + @SuppressWarnings("unused") + public void dispatchTrackball(MotionEvent arg0, long arg1) throws RemoteException { + // pass for now. + } + + @SuppressWarnings("unused") + public void executeCommand(String arg0, String arg1, ParcelFileDescriptor arg2) + throws RemoteException { + // pass for now. + } + + @SuppressWarnings("unused") + public void resized(int arg0, int arg1, Rect arg2, Rect arg3, boolean arg4) + throws RemoteException { + // pass for now. + } + + @SuppressWarnings("unused") + public void windowFocusChanged(boolean arg0, boolean arg1) throws RemoteException { + // pass for now. + } + + public IBinder asBinder() { + // pass for now. + return null; + } + } + +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeAssetManager.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeAssetManager.java new file mode 100644 index 0000000..1fa11af --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeAssetManager.java @@ -0,0 +1,74 @@ +/* + * 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 com.android.layoutlib.bridge; + +import android.content.res.AssetManager; +import android.content.res.Configuration; + +import java.util.Locale; + +public class BridgeAssetManager extends AssetManager { + + /** + * This initializes the static field {@link AssetManager#mSystem} which is used + * by methods who get a global asset manager using {@link AssetManager#getSystem()}. + * <p/> + * They will end up using our bridge asset manager. + * <p/> + * {@link Bridge} calls this method after setting up a new bridge. + */ + /*package*/ static AssetManager initSystem() { + if (!(AssetManager.mSystem instanceof BridgeAssetManager)) { + // Note that AssetManager() creates a system AssetManager and we override it + // with our BridgeAssetManager. + AssetManager.mSystem = new BridgeAssetManager(); + AssetManager.mSystem.makeStringBlocks(false); + } + return AssetManager.mSystem; + } + + /** + * Clears the static {@link AssetManager#mSystem} to make sure we don't leave objects + * around that would prevent us from unloading the library. + */ + /*package*/ static void clearSystem() { + AssetManager.mSystem = null; + } + + private BridgeAssetManager() { + } + + /** + * Change the configuration used when retrieving resources. Not for use by applications. + */ + @Override + public void setConfiguration(int mcc, int mnc, String locale, + int orientation, int touchscreen, int density, int keyboard, + int keyboardHidden, int navigation, int screenWidth, int screenHeight, + int version) { + + Configuration c = new Configuration(); + c.mcc = mcc; + c.mnc = mnc; + c.locale = new Locale(locale); + c.touchscreen = touchscreen; + c.keyboard = keyboard; + c.keyboardHidden = keyboardHidden; + c.navigation = navigation; + c.orientation = orientation; + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeCanvas.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeCanvas.java new file mode 100644 index 0000000..70c26a7 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeCanvas.java @@ -0,0 +1,1099 @@ +/* + * 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 com.android.layoutlib.bridge; + +import com.android.layoutlib.api.ILayoutLog; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.DrawFilter; +import android.graphics.LinearGradient; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Picture; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Region; +import android.graphics.Shader; +import android.graphics.Xfermode; +import android.graphics.Paint.Align; +import android.graphics.Paint.Style; +import android.graphics.Region.Op; + +import java.awt.AlphaComposite; +import java.awt.Color; +import java.awt.Composite; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.image.BufferedImage; +import java.util.Stack; + +import javax.microedition.khronos.opengles.GL; + +/** + * Re-implementation of the Canvas, 100% in java on top of a BufferedImage. + */ +public class BridgeCanvas extends Canvas { + + private BufferedImage mBufferedImage; + private final Stack<Graphics2D> mGraphicsStack = new Stack<Graphics2D>(); + private final ILayoutLog mLogger; + + public BridgeCanvas(int width, int height, ILayoutLog logger) { + mLogger = logger; + mBufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + mGraphicsStack.push(mBufferedImage.createGraphics()); + } + + public BridgeCanvas(int width, int height) { + this(width, height, null /* logger*/); + } + + public BufferedImage getImage() { + return mBufferedImage; + } + + Graphics2D getGraphics2d() { + return mGraphicsStack.peek(); + } + + void dispose() { + while (mGraphicsStack.size() > 0) { + mGraphicsStack.pop().dispose(); + } + } + + /** + * Creates a new {@link Graphics2D} based on the {@link Paint} parameters. + * <p/>The object must be disposed ({@link Graphics2D#dispose()}) after being used. + */ + private Graphics2D getNewGraphics(Paint paint, Graphics2D g) { + // make new one + g = (Graphics2D)g.create(); + g.setColor(new Color(paint.getColor())); + int alpha = paint.getAlpha(); + float falpha = alpha / 255.f; + + Xfermode xfermode = paint.getXfermode(); + if (xfermode instanceof PorterDuffXfermode) { + PorterDuff.Mode mode = ((PorterDuffXfermode)xfermode).getMode(); + + setModeInGraphics(mode, g, falpha); + } else { + if (mLogger != null && xfermode != null) { + mLogger.warning(String.format( + "Xfermode '%1$s' is not supported in the Layout Editor.", + xfermode.getClass().getCanonicalName())); + } + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, falpha)); + } + + Shader shader = paint.getShader(); + if (shader instanceof LinearGradient) { + g.setPaint(((LinearGradient)shader).getPaint()); + } else { + if (mLogger != null && shader != null) { + mLogger.warning(String.format( + "Shader '%1$s' is not supported in the Layout Editor.", + shader.getClass().getCanonicalName())); + } + } + + return g; + } + + private void setModeInGraphics(PorterDuff.Mode mode, Graphics2D g, float falpha) { + switch (mode) { + case CLEAR: + g.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR, falpha)); + break; + case DARKEN: + break; + case DST: + g.setComposite(AlphaComposite.getInstance(AlphaComposite.DST, falpha)); + break; + case DST_ATOP: + g.setComposite(AlphaComposite.getInstance(AlphaComposite.DST_ATOP, falpha)); + break; + case DST_IN: + g.setComposite(AlphaComposite.getInstance(AlphaComposite.DST_IN, falpha)); + break; + case DST_OUT: + g.setComposite(AlphaComposite.getInstance(AlphaComposite.DST_OUT, falpha)); + break; + case DST_OVER: + g.setComposite(AlphaComposite.getInstance(AlphaComposite.DST_OVER, falpha)); + break; + case LIGHTEN: + break; + case MULTIPLY: + break; + case SCREEN: + break; + case SRC: + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC, falpha)); + break; + case SRC_ATOP: + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, falpha)); + break; + case SRC_IN: + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_IN, falpha)); + break; + case SRC_OUT: + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OUT, falpha)); + break; + case SRC_OVER: + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, falpha)); + break; + case XOR: + g.setComposite(AlphaComposite.getInstance(AlphaComposite.XOR, falpha)); + break; + } + } + + // -------------------- + + @Override + public void finalize() throws Throwable { + // pass + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#translate(float, float) + */ + @Override + public void translate(float dx, float dy) { + getGraphics2d().translate(dx, dy); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#save() + */ + @Override + public int save() { + Graphics2D g = (Graphics2D)getGraphics2d().create(); + mGraphicsStack.push(g); + + return mGraphicsStack.size() - 1; + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#save(int) + */ + @Override + public int save(int saveFlags) { + // For now we ignore saveFlags + return save(); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#restore() + */ + @Override + public void restore() { + mGraphicsStack.pop(); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#restoreToCount(int) + */ + @Override + public void restoreToCount(int saveCount) { + while (mGraphicsStack.size() > saveCount) { + mGraphicsStack.pop(); + } + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#getSaveCount() + */ + @Override + public int getSaveCount() { + return mGraphicsStack.size() - 1; + } + + + /* (non-Javadoc) + * @see android.graphics.Canvas#clipRect(float, float, float, float, android.graphics.Region.Op) + */ + @Override + public boolean clipRect(float left, float top, float right, float bottom, Op op) { + return clipRect(left, top, right, bottom); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#clipRect(float, float, float, float) + */ + @Override + public boolean clipRect(float left, float top, float right, float bottom) { + getGraphics2d().clipRect((int)left, (int)top, (int)(right-left), (int)(bottom-top)); + return true; + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#clipRect(int, int, int, int) + */ + @Override + public boolean clipRect(int left, int top, int right, int bottom) { + getGraphics2d().clipRect(left, top, right-left, bottom-top); + return true; + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#clipRect(android.graphics.Rect, android.graphics.Region.Op) + */ + @Override + public boolean clipRect(Rect rect, Op op) { + return clipRect(rect.left, rect.top, rect.right, rect.bottom); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#clipRect(android.graphics.Rect) + */ + @Override + public boolean clipRect(Rect rect) { + return clipRect(rect.left, rect.top, rect.right, rect.bottom); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#clipRect(android.graphics.RectF, android.graphics.Region.Op) + */ + @Override + public boolean clipRect(RectF rect, Op op) { + return clipRect(rect.left, rect.top, rect.right, rect.bottom); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#clipRect(android.graphics.RectF) + */ + @Override + public boolean clipRect(RectF rect) { + return clipRect(rect.left, rect.top, rect.right, rect.bottom); + } + + @Override + public boolean quickReject(RectF rect, EdgeType type) { + return false; + } + + @Override + public boolean quickReject(Path path, EdgeType type) { + return false; + } + + @Override + public boolean quickReject(float left, float top, float right, float bottom, + EdgeType type) { + return false; + } + + /** + * Retrieve the clip bounds, returning true if they are non-empty. + * + * @param bounds Return the clip bounds here. If it is null, ignore it but + * still return true if the current clip is non-empty. + * @return true if the current clip is non-empty. + */ + @Override + public boolean getClipBounds(Rect bounds) { + Rectangle rect = getGraphics2d().getClipBounds(); + if (rect != null) { + bounds.left = rect.x; + bounds.top = rect.y; + bounds.right = rect.x + rect.width; + bounds.bottom = rect.y + rect.height; + return true; + } + return false; + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawColor(int, android.graphics.PorterDuff.Mode) + */ + @Override + public void drawColor(int color, PorterDuff.Mode mode) { + Graphics2D g = getGraphics2d(); + + // save old color + Color c = g.getColor(); + + Composite composite = g.getComposite(); + + // get the alpha from the color + int alpha = color >>> 24; + float falpha = alpha / 255.f; + + setModeInGraphics(mode, g, falpha); + + g.setColor(new Color(color)); + + getGraphics2d().fillRect(0, 0, getWidth(), getHeight()); + + g.setComposite(composite); + + // restore color + g.setColor(c); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawColor(int) + */ + @Override + public void drawColor(int color) { + drawColor(color, PorterDuff.Mode.SRC_OVER); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawARGB(int, int, int, int) + */ + @Override + public void drawARGB(int a, int r, int g, int b) { + drawColor(a << 24 | r << 16 | g << 8 | b, PorterDuff.Mode.SRC_OVER); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawRGB(int, int, int) + */ + @Override + public void drawRGB(int r, int g, int b) { + drawColor(0xFF << 24 | r << 16 | g << 8 | b, PorterDuff.Mode.SRC_OVER); + } + + + /* (non-Javadoc) + * @see android.graphics.Canvas#getWidth() + */ + @Override + public int getWidth() { + return mBufferedImage.getWidth(); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#getHeight() + */ + @Override + public int getHeight() { + return mBufferedImage.getHeight(); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawPaint(android.graphics.Paint) + */ + @Override + public void drawPaint(Paint paint) { + drawColor(paint.getColor()); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawBitmap(android.graphics.Bitmap, float, float, android.graphics.Paint) + */ + @Override + public void drawBitmap(Bitmap bitmap, float left, float top, Paint paint) { + drawBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), + (int)left, (int)top, + (int)left+bitmap.getWidth(), (int)top+bitmap.getHeight(), paint); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawBitmap(android.graphics.Bitmap, android.graphics.Matrix, android.graphics.Paint) + */ + @Override + public void drawBitmap(Bitmap bitmap, Matrix matrix, Paint paint) { + throw new UnsupportedOperationException(); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawBitmap(android.graphics.Bitmap, android.graphics.Rect, android.graphics.Rect, android.graphics.Paint) + */ + @Override + public void drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint) { + if (src == null) { + drawBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), + dst.left, dst.top, dst.right, dst.bottom, paint); + } else { + drawBitmap(bitmap, src.left, src.top, src.width(), src.height(), + dst.left, dst.top, dst.right, dst.bottom, paint); + } + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawBitmap(android.graphics.Bitmap, android.graphics.Rect, android.graphics.RectF, android.graphics.Paint) + */ + @Override + public void drawBitmap(Bitmap bitmap, Rect src, RectF dst, Paint paint) { + if (src == null) { + drawBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), + (int)dst.left, (int)dst.top, (int)dst.right, (int)dst.bottom, paint); + } else { + drawBitmap(bitmap, src.left, src.top, src.width(), src.height(), + (int)dst.left, (int)dst.top, (int)dst.right, (int)dst.bottom, paint); + } + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawBitmap(int[], int, int, int, int, int, int, boolean, android.graphics.Paint) + */ + @Override + public void drawBitmap(int[] colors, int offset, int stride, int x, int y, int width, + int height, boolean hasAlpha, Paint paint) { + throw new UnsupportedOperationException(); + } + + private void drawBitmap(Bitmap bitmap, int sleft, int stop, int sright, int sbottom, int dleft, + int dtop, int dright, int dbottom, Paint paint) { + BufferedImage image = bitmap.getImage(); + + Graphics2D g = getGraphics2d(); + + Composite c = null; + + if (paint.isFilterBitmap()) { + g = (Graphics2D)g.create(); + g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_BILINEAR); + } + + if (paint.getAlpha() != 0xFF) { + c = g.getComposite(); + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, + paint.getAlpha()/255.f)); + } + + g.drawImage(image, dleft, dtop, dright, dbottom, + sleft, stop, sright, sbottom, null); + + if (paint.isFilterBitmap()) { + g.dispose(); + } + + if (c != null) { + g.setComposite(c); + } + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#rotate(float, float, float) + */ + @Override + public void rotate(float degrees, float px, float py) { + if (degrees != 0) { + Graphics2D g = getGraphics2d(); + g.translate(px, py); + g.rotate(Math.toRadians(degrees)); + g.translate(-px, -py); + } + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#rotate(float) + */ + @Override + public void rotate(float degrees) { + getGraphics2d().rotate(Math.toRadians(degrees)); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#scale(float, float, float, float) + */ + @Override + public void scale(float sx, float sy, float px, float py) { + Graphics2D g = getGraphics2d(); + g.translate(px, py); + g.scale(sx, sy); + g.translate(-px, -py); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#scale(float, float) + */ + @Override + public void scale(float sx, float sy) { + getGraphics2d().scale(sx, sy); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawText(char[], int, int, float, float, android.graphics.Paint) + */ + @Override + public void drawText(char[] text, int index, int count, float x, float y, Paint paint) { + Graphics2D g = getGraphics2d(); + + g = (Graphics2D)g.create(); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + g.setFont(paint.getFont()); + + // set the color. because this only handles RGB we have to handle the alpha separately + g.setColor(new Color(paint.getColor())); + int alpha = paint.getAlpha(); + float falpha = alpha / 255.f; + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, falpha)); + + // Paint.TextAlign indicates how the text is positioned relative to X. + // LEFT is the default and there's nothing to do. + if (paint.getTextAlign() != Align.LEFT) { + float m = paint.measureText(text, index, count); + if (paint.getTextAlign() == Align.CENTER) { + x -= m / 2; + } else if (paint.getTextAlign() == Align.RIGHT) { + x -= m; + } + } + + g.drawChars(text, index, count, (int)x, (int)y); + + g.dispose(); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, float, float, android.graphics.Paint) + */ + @Override + public void drawText(CharSequence text, int start, int end, float x, float y, Paint paint) { + drawText(text.toString().toCharArray(), start, end - start, x, y, paint); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawText(java.lang.String, float, float, android.graphics.Paint) + */ + @Override + public void drawText(String text, float x, float y, Paint paint) { + drawText(text.toCharArray(), 0, text.length(), x, y, paint); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawText(java.lang.String, int, int, float, float, android.graphics.Paint) + */ + @Override + public void drawText(String text, int start, int end, float x, float y, Paint paint) { + drawText(text.toCharArray(), start, end - start, x, y, paint); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawRect(android.graphics.RectF, android.graphics.Paint) + */ + @Override + public void drawRect(RectF rect, Paint paint) { + doDrawRect((int)rect.left, (int)rect.top, (int)rect.width(), (int)rect.height(), paint); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawRect(float, float, float, float, android.graphics.Paint) + */ + @Override + public void drawRect(float left, float top, float right, float bottom, Paint paint) { + doDrawRect((int)left, (int)top, (int)(right-left), (int)(bottom-top), paint); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawRect(android.graphics.Rect, android.graphics.Paint) + */ + @Override + public void drawRect(Rect r, Paint paint) { + doDrawRect(r.left, r.top, r.width(), r.height(), paint); + } + + private final void doDrawRect(int left, int top, int width, int height, Paint paint) { + // get current graphisc + Graphics2D g = getGraphics2d(); + + g = getNewGraphics(paint, g); + + Style style = paint.getStyle(); + + // draw + if (style == Style.FILL || style == Style.FILL_AND_STROKE) { + g.fillRect(left, top, width, height); + } + + if (style == Style.STROKE || style == Style.FILL_AND_STROKE) { + g.drawRect(left, top, width, height); + } + + // dispose Graphics2D object + g.dispose(); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawRoundRect(android.graphics.RectF, float, float, android.graphics.Paint) + */ + @Override + public void drawRoundRect(RectF rect, float rx, float ry, Paint paint) { + // get current graphisc + Graphics2D g = getGraphics2d(); + + g = getNewGraphics(paint, g); + + Style style = paint.getStyle(); + + // draw + + int arcWidth = (int)(rx * 2); + int arcHeight = (int)(ry * 2); + + if (style == Style.FILL || style == Style.FILL_AND_STROKE) { + g.fillRoundRect((int)rect.left, (int)rect.right, (int)rect.width(), (int)rect.height(), + arcWidth, arcHeight); + } + + if (style == Style.STROKE || style == Style.FILL_AND_STROKE) { + g.drawRoundRect((int)rect.left, (int)rect.right, (int)rect.width(), (int)rect.height(), + arcWidth, arcHeight); + } + + // dispose Graphics2D object + g.dispose(); + } + + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawLine(float, float, float, float, android.graphics.Paint) + */ + @Override + public void drawLine(float startX, float startY, float stopX, float stopY, Paint paint) { + // get current graphisc + Graphics2D g = getGraphics2d(); + + g = getNewGraphics(paint, g); + + g.drawLine((int)startX, (int)startY, (int)stopX, (int)stopY); + + // dispose Graphics2D object + g.dispose(); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawLines(float[], int, int, android.graphics.Paint) + */ + @Override + public void drawLines(float[] pts, int offset, int count, Paint paint) { + // get current graphisc + Graphics2D g = getGraphics2d(); + + g = getNewGraphics(paint, g); + + for (int i = 0 ; i < count ; i += 4) { + g.drawLine((int)pts[i + offset], (int)pts[i + offset + 1], + (int)pts[i + offset + 2], (int)pts[i + offset + 3]); + } + + // dispose Graphics2D object + g.dispose(); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawLines(float[], android.graphics.Paint) + */ + @Override + public void drawLines(float[] pts, Paint paint) { + drawLines(pts, 0, pts.length, paint); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawCircle(float, float, float, android.graphics.Paint) + */ + @Override + public void drawCircle(float cx, float cy, float radius, Paint paint) { + // get current graphisc + Graphics2D g = getGraphics2d(); + + g = getNewGraphics(paint, g); + + Style style = paint.getStyle(); + + int size = (int)(radius * 2); + + // draw + if (style == Style.FILL || style == Style.FILL_AND_STROKE) { + g.fillOval((int)(cx - radius), (int)(cy - radius), size, size); + } + + if (style == Style.STROKE || style == Style.FILL_AND_STROKE) { + g.drawOval((int)(cx - radius), (int)(cy - radius), size, size); + } + + // dispose Graphics2D object + g.dispose(); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawOval(android.graphics.RectF, android.graphics.Paint) + */ + @Override + public void drawOval(RectF oval, Paint paint) { + // get current graphics + Graphics2D g = getGraphics2d(); + + g = getNewGraphics(paint, g); + + Style style = paint.getStyle(); + + // draw + if (style == Style.FILL || style == Style.FILL_AND_STROKE) { + g.fillOval((int)oval.left, (int)oval.top, (int)oval.width(), (int)oval.height()); + } + + if (style == Style.STROKE || style == Style.FILL_AND_STROKE) { + g.drawOval((int)oval.left, (int)oval.top, (int)oval.width(), (int)oval.height()); + } + + // dispose Graphics2D object + g.dispose(); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawPath(android.graphics.Path, android.graphics.Paint) + */ + @Override + public void drawPath(Path path, Paint paint) { + // get current graphics + Graphics2D g = getGraphics2d(); + + g = getNewGraphics(paint, g); + + Style style = paint.getStyle(); + + // draw + if (style == Style.FILL || style == Style.FILL_AND_STROKE) { + g.fill(path.getAwtShape()); + } + + if (style == Style.STROKE || style == Style.FILL_AND_STROKE) { + g.draw(path.getAwtShape()); + } + + // dispose Graphics2D object + g.dispose(); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#setMatrix(android.graphics.Matrix) + */ + @Override + public void setMatrix(Matrix matrix) { + // since SetMatrix *replaces* all the other transformation, we have to restore/save + restore(); + save(); + + // get the new current graphics + Graphics2D g = getGraphics2d(); + + // and apply the matrix + g.setTransform(matrix.getTransform()); + + if (mLogger != null && matrix.hasPerspective()) { + mLogger.warning("android.graphics.Canvas#setMatrix(android.graphics.Matrix) only supports affine transformations in the Layout Editor."); + } + } + + // -------------------- + + /* (non-Javadoc) + * @see android.graphics.Canvas#clipPath(android.graphics.Path, android.graphics.Region.Op) + */ + @Override + public boolean clipPath(Path path, Op op) { + // TODO Auto-generated method stub + return super.clipPath(path, op); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#clipPath(android.graphics.Path) + */ + @Override + public boolean clipPath(Path path) { + // TODO Auto-generated method stub + return super.clipPath(path); + } + + + /* (non-Javadoc) + * @see android.graphics.Canvas#clipRegion(android.graphics.Region, android.graphics.Region.Op) + */ + @Override + public boolean clipRegion(Region region, Op op) { + // TODO Auto-generated method stub + return super.clipRegion(region, op); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#clipRegion(android.graphics.Region) + */ + @Override + public boolean clipRegion(Region region) { + // TODO Auto-generated method stub + return super.clipRegion(region); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#concat(android.graphics.Matrix) + */ + @Override + public void concat(Matrix matrix) { + // TODO Auto-generated method stub + super.concat(matrix); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawArc(android.graphics.RectF, float, float, boolean, android.graphics.Paint) + */ + @Override + public void drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter, + Paint paint) { + // TODO Auto-generated method stub + super.drawArc(oval, startAngle, sweepAngle, useCenter, paint); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawBitmapMesh(android.graphics.Bitmap, int, int, float[], int, int[], int, android.graphics.Paint) + */ + @Override + public void drawBitmapMesh(Bitmap bitmap, int meshWidth, int meshHeight, float[] verts, + int vertOffset, int[] colors, int colorOffset, Paint paint) { + // TODO Auto-generated method stub + super.drawBitmapMesh(bitmap, meshWidth, meshHeight, verts, vertOffset, colors, colorOffset, paint); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawPicture(android.graphics.Picture, android.graphics.Rect) + */ + @Override + public void drawPicture(Picture picture, Rect dst) { + // TODO Auto-generated method stub + super.drawPicture(picture, dst); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawPicture(android.graphics.Picture, android.graphics.RectF) + */ + @Override + public void drawPicture(Picture picture, RectF dst) { + // TODO Auto-generated method stub + super.drawPicture(picture, dst); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawPicture(android.graphics.Picture) + */ + @Override + public void drawPicture(Picture picture) { + // TODO Auto-generated method stub + super.drawPicture(picture); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawPoint(float, float, android.graphics.Paint) + */ + @Override + public void drawPoint(float x, float y, Paint paint) { + // TODO Auto-generated method stub + super.drawPoint(x, y, paint); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawPoints(float[], int, int, android.graphics.Paint) + */ + @Override + public void drawPoints(float[] pts, int offset, int count, Paint paint) { + // TODO Auto-generated method stub + super.drawPoints(pts, offset, count, paint); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawPoints(float[], android.graphics.Paint) + */ + @Override + public void drawPoints(float[] pts, Paint paint) { + // TODO Auto-generated method stub + super.drawPoints(pts, paint); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawPosText(char[], int, int, float[], android.graphics.Paint) + */ + @Override + public void drawPosText(char[] text, int index, int count, float[] pos, Paint paint) { + // TODO Auto-generated method stub + super.drawPosText(text, index, count, pos, paint); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawPosText(java.lang.String, float[], android.graphics.Paint) + */ + @Override + public void drawPosText(String text, float[] pos, Paint paint) { + // TODO Auto-generated method stub + super.drawPosText(text, pos, paint); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawTextOnPath(char[], int, int, android.graphics.Path, float, float, android.graphics.Paint) + */ + @Override + public void drawTextOnPath(char[] text, int index, int count, Path path, float offset, + float offset2, Paint paint) { + // TODO Auto-generated method stub + super.drawTextOnPath(text, index, count, path, offset, offset2, paint); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawTextOnPath(java.lang.String, android.graphics.Path, float, float, android.graphics.Paint) + */ + @Override + public void drawTextOnPath(String text, Path path, float offset, float offset2, Paint paint) { + // TODO Auto-generated method stub + super.drawTextOnPath(text, path, offset, offset2, paint); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawVertices(android.graphics.Canvas.VertexMode, int, float[], int, float[], int, int[], int, short[], int, int, android.graphics.Paint) + */ + @Override + public void drawVertices(VertexMode mode, int vertexCount, float[] verts, int vertOffset, + float[] texs, int texOffset, int[] colors, int colorOffset, short[] indices, + int indexOffset, int indexCount, Paint paint) { + // TODO Auto-generated method stub + super.drawVertices(mode, vertexCount, verts, vertOffset, texs, texOffset, colors, colorOffset, + indices, indexOffset, indexCount, paint); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#getDrawFilter() + */ + @Override + public DrawFilter getDrawFilter() { + // TODO Auto-generated method stub + return super.getDrawFilter(); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#getGL() + */ + @Override + public GL getGL() { + // TODO Auto-generated method stub + return super.getGL(); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#getMatrix() + */ + @Override + public Matrix getMatrix() { + // TODO Auto-generated method stub + return super.getMatrix(); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#getMatrix(android.graphics.Matrix) + */ + @Override + public void getMatrix(Matrix ctm) { + // TODO Auto-generated method stub + super.getMatrix(ctm); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#isOpaque() + */ + @Override + public boolean isOpaque() { + // TODO Auto-generated method stub + return super.isOpaque(); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#saveLayer(float, float, float, float, android.graphics.Paint, int) + */ + @Override + public int saveLayer(float left, float top, float right, float bottom, Paint paint, + int saveFlags) { + // TODO Auto-generated method stub + return super.saveLayer(left, top, right, bottom, paint, saveFlags); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#saveLayer(android.graphics.RectF, android.graphics.Paint, int) + */ + @Override + public int saveLayer(RectF bounds, Paint paint, int saveFlags) { + // TODO Auto-generated method stub + return super.saveLayer(bounds, paint, saveFlags); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#saveLayerAlpha(float, float, float, float, int, int) + */ + @Override + public int saveLayerAlpha(float left, float top, float right, float bottom, int alpha, + int saveFlags) { + // TODO Auto-generated method stub + return super.saveLayerAlpha(left, top, right, bottom, alpha, saveFlags); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#saveLayerAlpha(android.graphics.RectF, int, int) + */ + @Override + public int saveLayerAlpha(RectF bounds, int alpha, int saveFlags) { + // TODO Auto-generated method stub + return super.saveLayerAlpha(bounds, alpha, saveFlags); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#setBitmap(android.graphics.Bitmap) + */ + @Override + public void setBitmap(Bitmap bitmap) { + // TODO Auto-generated method stub + super.setBitmap(bitmap); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#setDrawFilter(android.graphics.DrawFilter) + */ + @Override + public void setDrawFilter(DrawFilter filter) { + // TODO Auto-generated method stub + super.setDrawFilter(filter); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#setViewport(int, int) + */ + @Override + public void setViewport(int width, int height) { + // TODO Auto-generated method stub + super.setViewport(width, height); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#skew(float, float) + */ + @Override + public void skew(float sx, float sy) { + // TODO Auto-generated method stub + super.skew(sx, sy); + } + + + +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeConstants.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeConstants.java new file mode 100644 index 0000000..b426247 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeConstants.java @@ -0,0 +1,64 @@ +/* + * 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 com.android.layoutlib.bridge; + +/** + * Constant definition class.<br> + * <br> + * Most constants have a prefix defining the content. + * <ul> + * <li><code>WS_</code> Workspace path constant. Those are absolute paths, + * from the project root.</li> + * <li><code>OS_</code> OS path constant. These paths are different depending on the platform.</li> + * <li><code>FN_</code> File name constant.</li> + * <li><code>FD_</code> Folder name constant.</li> + * <li><code>EXT_</code> File extension constant. This does NOT include a dot.</li> + * <li><code>DOT_</code> File extension constant. This start with a dot.</li> + * <li><code>RE_</code> Regexp constant.</li> + * <li><code>NS_</code> Namespace constant.</li> + * <li><code>CLASS_</code> Fully qualified class name.</li> + * </ul> + * + */ +public class BridgeConstants { + + /** Namespace for the resource XML */ + public final static String NS_RESOURCES = "http://schemas.android.com/apk/res/android"; + + public final static String R = "com.android.internal.R"; + + public final static String PREFIX_ANDROID_RESOURCE_REF = "@android:"; + public final static String PREFIX_RESOURCE_REF = "@"; + public final static String PREFIX_ANDROID_THEME_REF = "?android:"; + public final static String PREFIX_THEME_REF = "?"; + + public final static String PREFIX_ANDROID = "android:"; + + public final static String RES_STYLE = "style"; + public final static String RES_ATTR = "attr"; + public final static String RES_DRAWABLE = "drawable"; + public final static String RES_COLOR = "color"; + public final static String RES_LAYOUT = "layout"; + public final static String RES_STRING = "string"; + public final static String RES_ID = "id"; + + public final static String REFERENCE_STYLE = RES_STYLE + "/"; + public final static String REFERENCE_NULL = "@null"; + + public final static String FILL_PARENT = "fill_parent"; + public final static String WRAP_CONTENT = "wrap_content"; +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeContentResolver.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeContentResolver.java new file mode 100644 index 0000000..727d6f2 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeContentResolver.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2009 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; + +import android.content.ContentResolver; +import android.content.ContentServiceNative; +import android.content.Context; +import android.content.IContentProvider; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Bundle; + +/** + * A mock content resolver for the LayoutLib Bridge. + * <p/> + * It won't serve any actual data but it's good enough for all + * the widgets which expect to have a content resolver available via + * {@link BridgeContext#getContentResolver()}. + */ +public class BridgeContentResolver extends ContentResolver { + + public BridgeContentResolver(Context context) { + super(context); + } + + @Override + public IContentProvider acquireProvider(Context c, String name) { + // ignore + return null; + } + + @Override + public boolean releaseProvider(IContentProvider icp) { + // ignore + return false; + } + + /** + * Stub for the layoutlib bridge content resolver. + * <p/> + * The super implementation accesses the {@link ContentServiceNative#getDefault()} + * which returns null and would make the call crash. Instead we do nothing. + */ + @Override + public void registerContentObserver(Uri uri, boolean notifyForDescendents, + ContentObserver observer) { + // pass + } + + /** + * Stub for the layoutlib bridge content resolver. + * <p/> + * The super implementation accesses the {@link ContentServiceNative#getDefault()} + * which returns null and would make the call crash. Instead we do nothing. + */ + @Override + public void unregisterContentObserver(ContentObserver observer) { + // pass + } + + /** + * Stub for the layoutlib bridge content resolver. + * <p/> + * The super implementation accesses the {@link ContentServiceNative#getDefault()} + * which returns null and would make the call crash. Instead we do nothing. + */ + @Override + public void notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork) { + // pass + } + + /** + * Stub for the layoutlib bridge content resolver. + * <p/> + * The super implementation accesses the {@link ContentServiceNative#getDefault()} + * which returns null and would make the call crash. Instead we do nothing. + */ + @Override + public void startSync(Uri uri, Bundle extras) { + // pass + } + + /** + * Stub for the layoutlib bridge content resolver. + * <p/> + * The super implementation accesses the {@link ContentServiceNative#getDefault()} + * which returns null and would make the call crash. Instead we do nothing. + */ + @Override + public void cancelSync(Uri uri) { + // pass + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeContext.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeContext.java new file mode 100644 index 0000000..baa3d53 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeContext.java @@ -0,0 +1,1148 @@ +/* + * 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 com.android.layoutlib.bridge; + +import com.android.layoutlib.api.ILayoutLog; +import com.android.layoutlib.api.IProjectCallback; +import com.android.layoutlib.api.IResourceValue; +import com.android.layoutlib.api.IStyleResourceValue; + +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.ServiceConnection; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.content.res.AssetManager; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.content.res.Resources.Theme; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteDatabase.CursorFactory; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.view.BridgeInflater; +import android.view.View; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; +import java.util.Map.Entry; + +/** + * Custom implementation of Context to handle non compiled resources. + */ +public final class BridgeContext extends Context { + + private Resources mResources; + private Theme mTheme; + private HashMap<View, Object> mViewKeyMap = new HashMap<View, Object>(); + private IStyleResourceValue mThemeValues; + private final Object mProjectKey; + private Map<String, Map<String, IResourceValue>> mProjectResources; + private Map<String, Map<String, IResourceValue>> mFrameworkResources; + private Map<IStyleResourceValue, IStyleResourceValue> mStyleInheritanceMap; + + // maps for dynamically generated id representing style objects (IStyleResourceValue) + private Map<Integer, IStyleResourceValue> mDynamicIdToStyleMap; + private Map<IStyleResourceValue, Integer> mStyleToDynamicIdMap; + private int mDynamicIdGenerator = 0x01030000; // Base id for framework R.style + + // cache for TypedArray generated from IStyleResourceValue object + private Map<int[], Map<Integer, TypedArray>> mTypedArrayCache; + private BridgeInflater mInflater; + + private final IProjectCallback mProjectCallback; + private final ILayoutLog mLogger; + private BridgeContentResolver mContentResolver; + + /** + * @param projectKey An Object identifying the project. This is used for the cache mechanism. + * @param metrics the {@link DisplayMetrics}. + * @param themeName The name of the theme to use. + * @param projectResources the resources of the project. The map contains (String, map) pairs + * where the string is the type of the resource reference used in the layout file, and the + * map contains (String, {@link IResourceValue}) pairs where the key is the resource name, + * and the value is the resource value. + * @param frameworkResources the framework resources. The map contains (String, map) pairs + * where the string is the type of the resource reference used in the layout file, and the map + * contains (String, {@link IResourceValue}) pairs where the key is the resource name, and the + * value is the resource value. + * @param styleInheritanceMap + * @param customViewLoader + */ + public BridgeContext(Object projectKey, DisplayMetrics metrics, + IStyleResourceValue currentTheme, + Map<String, Map<String, IResourceValue>> projectResources, + Map<String, Map<String, IResourceValue>> frameworkResources, + Map<IStyleResourceValue, IStyleResourceValue> styleInheritanceMap, + IProjectCallback customViewLoader, ILayoutLog logger) { + mProjectKey = projectKey; + mProjectCallback = customViewLoader; + mLogger = logger; + Configuration config = new Configuration(); + + AssetManager assetManager = BridgeAssetManager.initSystem(); + mResources = BridgeResources.initSystem( + this, + assetManager, + metrics, + config, + customViewLoader); + + mTheme = mResources.newTheme(); + + mThemeValues = currentTheme; + mProjectResources = projectResources; + mFrameworkResources = frameworkResources; + mStyleInheritanceMap = styleInheritanceMap; + } + + public void setBridgeInflater(BridgeInflater inflater) { + mInflater = inflater; + } + + public void addViewKey(View view, Object viewKey) { + mViewKeyMap.put(view, viewKey); + } + + public Object getViewKey(View view) { + return mViewKeyMap.get(view); + } + + public Object getProjectKey() { + return mProjectKey; + } + + public IProjectCallback getProjectCallback() { + return mProjectCallback; + } + + public ILayoutLog getLogger() { + return mLogger; + } + + // ------------ Context methods + + @Override + public Resources getResources() { + return mResources; + } + + @Override + public Theme getTheme() { + return mTheme; + } + + @Override + public ClassLoader getClassLoader() { + return this.getClass().getClassLoader(); + } + + @Override + public Object getSystemService(String service) { + if (LAYOUT_INFLATER_SERVICE.equals(service)) { + return mInflater; + } + + // AutoCompleteTextView and MultiAutoCompleteTextView want a window + // service. We don't have any but it's not worth an exception. + if (WINDOW_SERVICE.equals(service)) { + return null; + } + + throw new UnsupportedOperationException("Unsupported Service: " + service); + } + + + @Override + public final TypedArray obtainStyledAttributes(int[] attrs) { + return createStyleBasedTypedArray(mThemeValues, attrs); + } + + @Override + public final TypedArray obtainStyledAttributes(int resid, int[] attrs) + throws Resources.NotFoundException { + // get the IStyleResourceValue based on the resId; + IStyleResourceValue style = getStyleByDynamicId(resid); + + if (style == null) { + throw new Resources.NotFoundException(); + } + + if (mTypedArrayCache == null) { + mTypedArrayCache = new HashMap<int[], Map<Integer,TypedArray>>(); + + Map<Integer, TypedArray> map = new HashMap<Integer, TypedArray>(); + mTypedArrayCache.put(attrs, map); + + BridgeTypedArray ta = createStyleBasedTypedArray(style, attrs); + map.put(resid, ta); + + return ta; + } + + // get the 2nd map + Map<Integer, TypedArray> map = mTypedArrayCache.get(attrs); + if (map == null) { + map = new HashMap<Integer, TypedArray>(); + mTypedArrayCache.put(attrs, map); + } + + // get the array from the 2nd map + TypedArray ta = map.get(resid); + + if (ta == null) { + ta = createStyleBasedTypedArray(style, attrs); + map.put(resid, ta); + } + + return ta; + } + + @Override + public final TypedArray obtainStyledAttributes(AttributeSet set, int[] attrs) { + return obtainStyledAttributes(set, attrs, 0, 0); + } + + @Override + public TypedArray obtainStyledAttributes(AttributeSet set, int[] attrs, + int defStyleAttr, int defStyleRes) { + + // Hint: for XmlPullParser, attach source //DEVICE_SRC/dalvik/libcore/xml/src/java + BridgeXmlBlockParser parser = null; + if (set instanceof BridgeXmlBlockParser) { + parser = (BridgeXmlBlockParser)set; + } else { + // reall this should not be happening since its instantiated in Bridge + mLogger.error("Parser is not a BridgeXmlBlockParser!"); + return null; + } + + boolean[] frameworkAttributes = new boolean[1]; + TreeMap<Integer, String> styleNameMap = searchAttrs(attrs, frameworkAttributes); + + BridgeTypedArray ta = ((BridgeResources) mResources).newTypeArray(attrs.length, + parser.isPlatformFile()); + + // resolve the defStyleAttr value into a IStyleResourceValue + IStyleResourceValue defStyleValues = null; + if (defStyleAttr != 0) { + // get the name from the int. + String defStyleName = searchAttr(defStyleAttr); + + // look for the style in the current theme, and its parent: + if (mThemeValues != null) { + IResourceValue item = findItemInStyle(mThemeValues, defStyleName); + + if (item != null) { + // item is a reference to a style entry. Search for it. + item = findResValue(item.getValue()); + + if (item instanceof IStyleResourceValue) { + defStyleValues = (IStyleResourceValue)item; + } + } else { + // TODO: log the error properly + System.out.println("Failed to find defStyle: " + defStyleName); + } + } + } + + if (defStyleRes != 0) { + // FIXME: See what we need to do with this. + throw new UnsupportedOperationException(); + } + + String namespace = BridgeConstants.NS_RESOURCES; + if (frameworkAttributes[0] == false) { + // need to use the application namespace + namespace = mProjectCallback.getNamespace(); + } + + if (styleNameMap != null) { + for (Entry<Integer, String> styleAttribute : styleNameMap.entrySet()) { + int index = styleAttribute.getKey().intValue(); + + String name = styleAttribute.getValue(); + String value = parser.getAttributeValue(namespace, name); + + // if there's no direct value for this attribute in the XML, we look for default + // values in the widget defStyle, and then in the theme. + if (value == null) { + IResourceValue resValue = null; + + // look for the value in the defStyle first (and its parent if needed) + if (defStyleValues != null) { + resValue = findItemInStyle(defStyleValues, name); + } + + // if the item is not present in the defStyle, we look in the main theme (and + // its parent themes) + if (resValue == null && mThemeValues != null) { + resValue = findItemInStyle(mThemeValues, name); + } + + // if we found a value, we make sure this doesn't reference another value. + // So we resolve it. + if (resValue != null) { + resValue = resolveResValue(resValue); + } + + ta.bridgeSetValue(index, name, resValue); + } else { + // there is a value in the XML, but we need to resolve it in case it's + // referencing another resource or a theme value. + ta.bridgeSetValue(index, name, resolveValue(null, name, value)); + } + } + } + + ta.sealArray(); + + return ta; + } + + + // ------------- private new methods + + /** + * Creates a {@link BridgeTypedArray} by filling the values defined by the int[] with the + * values found in the given style. + * @see #obtainStyledAttributes(int, int[]) + */ + private BridgeTypedArray createStyleBasedTypedArray(IStyleResourceValue style, int[] attrs) + throws Resources.NotFoundException { + TreeMap<Integer, String> styleNameMap = searchAttrs(attrs, null); + + BridgeTypedArray ta = ((BridgeResources) mResources).newTypeArray(attrs.length, + false /* platformResourceFlag */); + + // loop through all the values in the style map, and init the TypedArray with + // the style we got from the dynamic id + for (Entry<Integer, String> styleAttribute : styleNameMap.entrySet()) { + int index = styleAttribute.getKey().intValue(); + + String name = styleAttribute.getValue(); + + // get the value from the style, or its parent styles. + IResourceValue resValue = findItemInStyle(style, name); + + // resolve it to make sure there are no references left. + ta.bridgeSetValue(index, name, resolveResValue(resValue)); + } + + ta.sealArray(); + + return ta; + } + + + /** + * Resolves the value of a resource, if the value references a theme or resource value. + * <p/> + * This method ensures that it returns a {@link IResourceValue} object that does not + * reference another resource. + * If the resource cannot be resolved, it returns <code>null</code>. + * <p/> + * If a value that does not need to be resolved is given, the method will return a new + * instance of IResourceValue that contains the input value. + * + * @param type the type of the resource + * @param name the name of the attribute containing this value. + * @param value the resource value, or reference to resolve + * @return the resolved resource value or <code>null</code> if it failed to resolve it. + */ + private IResourceValue resolveValue(String type, String name, String value) { + if (value == null) { + return null; + } + + // get the IResourceValue referenced by this value + IResourceValue resValue = findResValue(value); + + // if resValue is null, but value is not null, this means it was not a reference. + // we return the name/value wrapper in a IResourceValue + if (resValue == null) { + return new ResourceValue(type, name, value); + } + + // we resolved a first reference, but we need to make sure this isn't a reference also. + return resolveResValue(resValue); + } + + /** + * Returns the {@link IResourceValue} referenced by the value of <var>value</var>. + * <p/> + * This method ensures that it returns a {@link IResourceValue} object that does not + * reference another resource. + * If the resource cannot be resolved, it returns <code>null</code>. + * <p/> + * If a value that does not need to be resolved is given, the method will return the input + * value. + * + * @param value the value containing the reference to resolve. + * @return a {@link IResourceValue} object or <code>null</code> + */ + IResourceValue resolveResValue(IResourceValue value) { + if (value == null) { + return null; + } + + // if the resource value is a style, we simply return it. + if (value instanceof IStyleResourceValue) { + return value; + } + + // else attempt to find another IResourceValue referenced by this one. + IResourceValue resolvedValue = findResValue(value.getValue()); + + // if the value did not reference anything, then we simply return the input value + if (resolvedValue == null) { + return value; + } + + // otherwise, we attempt to resolve this new value as well + return resolveResValue(resolvedValue); + } + + /** + * Searches for, and returns a {@link IResourceValue} by its reference. + * <p/> + * The reference format can be: + * <pre>@resType/resName</pre> + * <pre>@android:resType/resName</pre> + * <pre>@resType/android:resName</pre> + * <pre>?resType/resName</pre> + * <pre>?android:resType/resName</pre> + * <pre>?resType/android:resName</pre> + * Any other string format will return <code>null</code>. + * <p/> + * The actual format of a reference is <pre>@[namespace:]resType/resName</pre> but this method + * only support the android namespace. + * + * @param reference the resource reference to search for. + * @return a {@link IResourceValue} or <code>null</code>. + */ + IResourceValue findResValue(String reference) { + if (reference == null) { + return null; + } + if (reference.startsWith(BridgeConstants.PREFIX_THEME_REF)) { + // no theme? no need to go further! + if (mThemeValues == null) { + return null; + } + + boolean frameworkOnly = false; + + // eleminate the prefix from the string + if (reference.startsWith(BridgeConstants.PREFIX_ANDROID_THEME_REF)) { + frameworkOnly = true; + reference = reference.substring(BridgeConstants.PREFIX_ANDROID_THEME_REF.length()); + } else { + reference = reference.substring(BridgeConstants.PREFIX_THEME_REF.length()); + } + + // at this point, value can contain type/name (drawable/foo for instance). + // split it to make sure. + String[] segments = reference.split("\\/"); + + // we look for the referenced item name. + String referenceName = null; + + if (segments.length == 2) { + // there was a resType in the reference. If it's attr, we ignore it + // else, we assert for now. + if (BridgeConstants.RES_ATTR.equals(segments[0])) { + referenceName = segments[1]; + } else { + // At this time, no support for ?type/name where type is not "attr" + return null; + } + } else { + // it's just an item name. + referenceName = segments[0]; + } + + // now we look for android: in the referenceName in order to support format + // such as: ?attr/android:name + if (referenceName.startsWith(BridgeConstants.PREFIX_ANDROID)) { + frameworkOnly = true; + referenceName = referenceName.substring(BridgeConstants.PREFIX_ANDROID.length()); + } + + // Now look for the item in the theme, starting with the current one. + if (frameworkOnly) { + // FIXME for now we do the same as if it didn't specify android: + return findItemInStyle(mThemeValues, referenceName); + } + + return findItemInStyle(mThemeValues, referenceName); + } else if (reference.startsWith(BridgeConstants.PREFIX_RESOURCE_REF)) { + boolean frameworkOnly = false; + + // check for the specific null reference value. + if (BridgeConstants.REFERENCE_NULL.equals(reference)) { + return null; + } + + // Eliminate the prefix from the string. + if (reference.startsWith(BridgeConstants.PREFIX_ANDROID_RESOURCE_REF)) { + frameworkOnly = true; + reference = reference.substring( + BridgeConstants.PREFIX_ANDROID_RESOURCE_REF.length()); + } else { + reference = reference.substring(BridgeConstants.PREFIX_RESOURCE_REF.length()); + } + + // at this point, value contains type/[android:]name (drawable/foo for instance) + String[] segments = reference.split("\\/"); + + // now we look for android: in the resource name in order to support format + // such as: @drawable/android:name + if (segments[1].startsWith(BridgeConstants.PREFIX_ANDROID)) { + frameworkOnly = true; + segments[1] = segments[1].substring(BridgeConstants.PREFIX_ANDROID.length()); + } + + return findResValue(segments[0], segments[1], frameworkOnly); + } + + // Looks like the value didn't reference anything. Return null. + return null; + } + + /** + * Searches for, and returns a {@link IResourceValue} by its name, and type. + * @param resType the type of the resource + * @param resName the name of the resource + * @param frameworkOnly if <code>true</code>, the method does not search in the + * project resources + */ + private IResourceValue findResValue(String resType, String resName, boolean frameworkOnly) { + // map of IResouceValue for the given type + Map<String, IResourceValue> typeMap; + + // if allowed, search in the project resources first. + if (frameworkOnly == false) { + typeMap = mProjectResources.get(resType); + if (typeMap != null) { + IResourceValue item = typeMap.get(resName); + if (item != null) { + return item; + } + } + } + + // now search in the framework resources. + typeMap = mFrameworkResources.get(resType); + if (typeMap != null) { + IResourceValue item = typeMap.get(resName); + if (item != null) { + return item; + } + } + + // didn't find the resource anywhere. + return null; + } + + /** + * Returns a framework resource by type and name. The returned resource is resolved. + * @param resourceType the type of the resource + * @param resourceName the name of the resource + */ + public IResourceValue getFrameworkResource(String resourceType, String resourceName) { + return getResource(resourceType, resourceName, mFrameworkResources); + } + + /** + * Returns a project resource by type and name. The returned resource is resolved. + * @param resourceType the type of the resource + * @param resourceName the name of the resource + */ + public IResourceValue getProjectResource(String resourceType, String resourceName) { + return getResource(resourceType, resourceName, mProjectResources); + } + + IResourceValue getResource(String resourceType, String resourceName, + Map<String, Map<String, IResourceValue>> resourceRepository) { + Map<String, IResourceValue> typeMap = resourceRepository.get(resourceType); + if (typeMap != null) { + IResourceValue item = typeMap.get(resourceName); + if (item != null) { + item = resolveResValue(item); + return item; + } + } + + // didn't find the resource anywhere. + return null; + + } + + /** + * Returns the {@link IResourceValue} matching a given name in a given style. If the + * item is not directly available in the style, the method looks in its parent style. + * @param style the style to search in + * @param itemName the name of the item to search for. + * @return the {@link IResourceValue} object or <code>null</code> + */ + IResourceValue findItemInStyle(IStyleResourceValue style, String itemName) { + IResourceValue item = style.findItem(itemName); + + // if we didn't find it, we look in the parent style (if applicable) + if (item == null && mStyleInheritanceMap != null) { + IStyleResourceValue parentStyle = mStyleInheritanceMap.get(style); + if (parentStyle != null) { + return findItemInStyle(parentStyle, itemName); + } + } + + return item; + } + + /** + * The input int[] attrs is one of com.android.internal.R.styleable fields where the name + * of the field is the style being referenced and the array contains one index per attribute. + * <p/> + * searchAttrs() finds all the names of the attributes referenced so for example if + * attrs == com.android.internal.R.styleable.View, this returns the list of the "xyz" where + * there's a field com.android.internal.R.styleable.View_xyz and the field value is the index + * that is used to reference the attribute later in the TypedArray. + * + * @param attrs An attribute array reference given to obtainStyledAttributes. + * @return A sorted map Attribute-Value to Attribute-Name for all attributes declared by the + * attribute array. Returns null if nothing is found. + */ + private TreeMap<Integer,String> searchAttrs(int[] attrs, boolean[] outFrameworkFlag) { + // get the name of the array from the framework resources + String arrayName = Bridge.resolveResourceValue(attrs); + if (arrayName != null) { + // if we found it, get the name of each of the int in the array. + TreeMap<Integer,String> attributes = new TreeMap<Integer, String>(); + for (int i = 0 ; i < attrs.length ; i++) { + String[] info = Bridge.resolveResourceValue(attrs[i]); + if (info != null) { + attributes.put(i, info[0]); + } else { + // FIXME Not sure what we should be doing here... + attributes.put(i, null); + } + } + + if (outFrameworkFlag != null) { + outFrameworkFlag[0] = true; + } + + return attributes; + } + + // if the name was not found in the framework resources, look in the project + // resources + arrayName = mProjectCallback.resolveResourceValue(attrs); + if (arrayName != null) { + TreeMap<Integer,String> attributes = new TreeMap<Integer, String>(); + for (int i = 0 ; i < attrs.length ; i++) { + String[] info = mProjectCallback.resolveResourceValue(attrs[i]); + if (info != null) { + attributes.put(i, info[0]); + } else { + // FIXME Not sure what we should be doing here... + attributes.put(i, null); + } + } + + if (outFrameworkFlag != null) { + outFrameworkFlag[0] = false; + } + + return attributes; + } + + return null; + } + + /** + * Searches for the attribute referenced by its internal id. + * + * @param attr An attribute reference given to obtainStyledAttributes such as defStyle. + * @return The unique name of the attribute, if found, e.g. "buttonStyle". Returns null + * if nothing is found. + */ + public String searchAttr(int attr) { + String[] info = Bridge.resolveResourceValue(attr); + if (info != null) { + return info[0]; + } + + info = mProjectCallback.resolveResourceValue(attr); + if (info != null) { + return info[0]; + } + + return null; + } + + int getDynamicIdByStyle(IStyleResourceValue resValue) { + if (mDynamicIdToStyleMap == null) { + // create the maps. + mDynamicIdToStyleMap = new HashMap<Integer, IStyleResourceValue>(); + mStyleToDynamicIdMap = new HashMap<IStyleResourceValue, Integer>(); + } + + // look for an existing id + Integer id = mStyleToDynamicIdMap.get(resValue); + + if (id == null) { + // generate a new id + id = Integer.valueOf(++mDynamicIdGenerator); + + // and add it to the maps. + mDynamicIdToStyleMap.put(id, resValue); + mStyleToDynamicIdMap.put(resValue, id); + } + + return id; + } + + private IStyleResourceValue getStyleByDynamicId(int i) { + if (mDynamicIdToStyleMap != null) { + return mDynamicIdToStyleMap.get(i); + } + + return null; + } + + int getFrameworkIdValue(String idName, int defValue) { + Integer value = Bridge.getResourceValue(BridgeConstants.RES_ID, idName); + if (value != null) { + return value.intValue(); + } + + return defValue; + } + + int getProjectIdValue(String idName, int defValue) { + if (mProjectCallback != null) { + Integer value = mProjectCallback.getResourceValue(BridgeConstants.RES_ID, idName); + if (value != null) { + return value.intValue(); + } + } + + return defValue; + } + + //------------ NOT OVERRIDEN -------------------- + + @Override + public boolean bindService(Intent arg0, ServiceConnection arg1, int arg2) { + // TODO Auto-generated method stub + return false; + } + + @Override + public int checkCallingOrSelfPermission(String arg0) { + // TODO Auto-generated method stub + return 0; + } + + @Override + public int checkCallingOrSelfUriPermission(Uri arg0, int arg1) { + // TODO Auto-generated method stub + return 0; + } + + @Override + public int checkCallingPermission(String arg0) { + // TODO Auto-generated method stub + return 0; + } + + @Override + public int checkCallingUriPermission(Uri arg0, int arg1) { + // TODO Auto-generated method stub + return 0; + } + + @Override + public int checkPermission(String arg0, int arg1, int arg2) { + // TODO Auto-generated method stub + return 0; + } + + @Override + public int checkUriPermission(Uri arg0, int arg1, int arg2, int arg3) { + // TODO Auto-generated method stub + return 0; + } + + @Override + public int checkUriPermission(Uri arg0, String arg1, String arg2, int arg3, + int arg4, int arg5) { + // TODO Auto-generated method stub + return 0; + } + + @Override + public void clearWallpaper() { + // TODO Auto-generated method stub + + } + + @Override + public Context createPackageContext(String arg0, int arg1) { + // TODO Auto-generated method stub + return null; + } + + @Override + public String[] databaseList() { + // TODO Auto-generated method stub + return null; + } + + @Override + public boolean deleteDatabase(String arg0) { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean deleteFile(String arg0) { + // TODO Auto-generated method stub + return false; + } + + @Override + public void enforceCallingOrSelfPermission(String arg0, String arg1) { + // TODO Auto-generated method stub + + } + + @Override + public void enforceCallingOrSelfUriPermission(Uri arg0, int arg1, + String arg2) { + // TODO Auto-generated method stub + + } + + @Override + public void enforceCallingPermission(String arg0, String arg1) { + // TODO Auto-generated method stub + + } + + @Override + public void enforceCallingUriPermission(Uri arg0, int arg1, String arg2) { + // TODO Auto-generated method stub + + } + + @Override + public void enforcePermission(String arg0, int arg1, int arg2, String arg3) { + // TODO Auto-generated method stub + + } + + @Override + public void enforceUriPermission(Uri arg0, int arg1, int arg2, int arg3, + String arg4) { + // TODO Auto-generated method stub + + } + + @Override + public void enforceUriPermission(Uri arg0, String arg1, String arg2, + int arg3, int arg4, int arg5, String arg6) { + // TODO Auto-generated method stub + + } + + @Override + public String[] fileList() { + // TODO Auto-generated method stub + return null; + } + + @Override + public AssetManager getAssets() { + // TODO Auto-generated method stub + return null; + } + + @Override + public File getCacheDir() { + // TODO Auto-generated method stub + return null; + } + + @Override + public ContentResolver getContentResolver() { + if (mContentResolver == null) { + mContentResolver = new BridgeContentResolver(this); + } + return mContentResolver; + } + + @Override + public File getDatabasePath(String arg0) { + // TODO Auto-generated method stub + return null; + } + + @Override + public File getDir(String arg0, int arg1) { + // TODO Auto-generated method stub + return null; + } + + @Override + public File getFileStreamPath(String arg0) { + // TODO Auto-generated method stub + return null; + } + + @Override + public File getFilesDir() { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getPackageCodePath() { + // TODO Auto-generated method stub + return null; + } + + @Override + public PackageManager getPackageManager() { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getPackageName() { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getPackageResourcePath() { + // TODO Auto-generated method stub + return null; + } + + @Override + public SharedPreferences getSharedPreferences(String arg0, int arg1) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Drawable getWallpaper() { + // TODO Auto-generated method stub + return null; + } + + @Override + public int getWallpaperDesiredMinimumWidth() { + return -1; + } + + @Override + public int getWallpaperDesiredMinimumHeight() { + return -1; + } + + @Override + public void grantUriPermission(String arg0, Uri arg1, int arg2) { + // TODO Auto-generated method stub + + } + + @SuppressWarnings("unused") + @Override + public FileInputStream openFileInput(String arg0) + throws FileNotFoundException { + // TODO Auto-generated method stub + return null; + } + + @SuppressWarnings("unused") + @Override + public FileOutputStream openFileOutput(String arg0, int arg1) + throws FileNotFoundException { + // TODO Auto-generated method stub + return null; + } + + @Override + public SQLiteDatabase openOrCreateDatabase(String arg0, int arg1, + CursorFactory arg2) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Drawable peekWallpaper() { + // TODO Auto-generated method stub + return null; + } + + @Override + public Intent registerReceiver(BroadcastReceiver arg0, IntentFilter arg1) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Intent registerReceiver(BroadcastReceiver arg0, IntentFilter arg1, + String arg2, Handler arg3) { + // TODO Auto-generated method stub + return null; + } + + @Override + public void removeStickyBroadcast(Intent arg0) { + // TODO Auto-generated method stub + + } + + @Override + public void revokeUriPermission(Uri arg0, int arg1) { + // TODO Auto-generated method stub + + } + + @Override + public void sendBroadcast(Intent arg0) { + // TODO Auto-generated method stub + + } + + @Override + public void sendBroadcast(Intent arg0, String arg1) { + // TODO Auto-generated method stub + + } + + @Override + public void sendOrderedBroadcast(Intent arg0, String arg1) { + // TODO Auto-generated method stub + + } + + @Override + public void sendOrderedBroadcast(Intent arg0, String arg1, + BroadcastReceiver arg2, Handler arg3, int arg4, String arg5, + Bundle arg6) { + // TODO Auto-generated method stub + + } + + @Override + public void sendStickyBroadcast(Intent arg0) { + // TODO Auto-generated method stub + + } + + @Override + public void setTheme(int arg0) { + // TODO Auto-generated method stub + + } + + @SuppressWarnings("unused") + @Override + public void setWallpaper(Bitmap arg0) throws IOException { + // TODO Auto-generated method stub + + } + + @SuppressWarnings("unused") + @Override + public void setWallpaper(InputStream arg0) throws IOException { + // TODO Auto-generated method stub + + } + + @Override + public void startActivity(Intent arg0) { + // TODO Auto-generated method stub + + } + + @Override + public boolean startInstrumentation(ComponentName arg0, String arg1, + Bundle arg2) { + // TODO Auto-generated method stub + return false; + } + + @Override + public ComponentName startService(Intent arg0) { + // TODO Auto-generated method stub + return null; + } + + @Override + public boolean stopService(Intent arg0) { + // TODO Auto-generated method stub + return false; + } + + @Override + public void unbindService(ServiceConnection arg0) { + // TODO Auto-generated method stub + + } + + @Override + public void unregisterReceiver(BroadcastReceiver arg0) { + // TODO Auto-generated method stub + + } + + @Override + public Looper getMainLooper() { + throw new UnsupportedOperationException(); + } + + @Override + public Context getApplicationContext() { + throw new UnsupportedOperationException(); + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeResources.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeResources.java new file mode 100644 index 0000000..0bcc7fd --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeResources.java @@ -0,0 +1,498 @@ +/* + * 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 com.android.layoutlib.bridge; + +import com.android.layoutlib.api.IProjectCallback; +import com.android.layoutlib.api.IResourceValue; + +import org.kxml2.io.KXmlParser; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.content.res.AssetFileDescriptor; +import android.content.res.AssetManager; +import android.content.res.ColorStateList; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import android.view.ViewGroup.LayoutParams; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.InputStream; + +/** + * + */ +public final class BridgeResources extends Resources { + + private BridgeContext mContext; + private IProjectCallback mProjectCallback; + private boolean[] mPlatformResourceFlag = new boolean[1]; + + /** + * This initializes the static field {@link Resources#mSystem} which is used + * by methods who get global resources using {@link Resources#getSystem()}. + * <p/> + * They will end up using our bridge resources. + * <p/> + * {@link Bridge} calls this method after setting up a new bridge. + */ + /*package*/ static Resources initSystem(BridgeContext context, + AssetManager assets, + DisplayMetrics metrics, + Configuration config, + IProjectCallback projectCallback) { + if (!(Resources.mSystem instanceof BridgeResources)) { + Resources.mSystem = new BridgeResources(context, + assets, + metrics, + config, + projectCallback); + } + return Resources.mSystem; + } + + /** + * Clears the static {@link Resources#mSystem} to make sure we don't leave objects + * around that would prevent us from unloading the library. + */ + /*package*/ static void clearSystem() { + if (Resources.mSystem instanceof BridgeResources) { + ((BridgeResources)(Resources.mSystem)).mContext = null; + ((BridgeResources)(Resources.mSystem)).mProjectCallback = null; + } + Resources.mSystem = null; + } + + private BridgeResources(BridgeContext context, AssetManager assets, DisplayMetrics metrics, + Configuration config, IProjectCallback projectCallback) { + super(assets, metrics, config); + mContext = context; + mProjectCallback = projectCallback; + } + + public BridgeTypedArray newTypeArray(int numEntries, boolean platformFile) { + return new BridgeTypedArray(this, mContext, numEntries, platformFile); + } + + private IResourceValue getResourceValue(int id, boolean[] platformResFlag_out) { + // first get the String related to this id in the framework + String[] resourceInfo = Bridge.resolveResourceValue(id); + + if (resourceInfo != null) { + platformResFlag_out[0] = true; + return mContext.getFrameworkResource(resourceInfo[1], resourceInfo[0]); + } + + // didn't find a match in the framework? look in the project. + if (mProjectCallback != null) { + resourceInfo = mProjectCallback.resolveResourceValue(id); + + if (resourceInfo != null) { + platformResFlag_out[0] = false; + return mContext.getProjectResource(resourceInfo[1], resourceInfo[0]); + } + } + + return null; + } + + @Override + public Drawable getDrawable(int id) throws NotFoundException { + IResourceValue value = getResourceValue(id, mPlatformResourceFlag); + + if (value != null) { + return ResourceHelper.getDrawable(value.getValue(), mContext, value.isFramework()); + } + + // id was not found or not resolved. Throw a NotFoundException. + throwException(id); + + // this is not used since the method above always throws + return null; + } + + @Override + public int getColor(int id) throws NotFoundException { + IResourceValue value = getResourceValue(id, mPlatformResourceFlag); + + if (value != null) { + try { + return ResourceHelper.getColor(value.getValue()); + } catch (NumberFormatException e) { + return 0; + } + } + + // id was not found or not resolved. Throw a NotFoundException. + throwException(id); + + // this is not used since the method above always throws + return 0; + } + + @Override + public ColorStateList getColorStateList(int id) throws NotFoundException { + IResourceValue value = getResourceValue(id, mPlatformResourceFlag); + + if (value != null) { + try { + int color = ResourceHelper.getColor(value.getValue()); + return ColorStateList.valueOf(color); + } catch (NumberFormatException e) { + return null; + } + } + + // id was not found or not resolved. Throw a NotFoundException. + throwException(id); + + // this is not used since the method above always throws + return null; + } + + @Override + public CharSequence getText(int id) throws NotFoundException { + IResourceValue value = getResourceValue(id, mPlatformResourceFlag); + + if (value != null) { + return value.getValue(); + } + + // id was not found or not resolved. Throw a NotFoundException. + throwException(id); + + // this is not used since the method above always throws + return null; + } + + @Override + public XmlResourceParser getLayout(int id) throws NotFoundException { + IResourceValue value = getResourceValue(id, mPlatformResourceFlag); + + if (value != null) { + File xml = new File(value.getValue()); + if (xml.isFile()) { + // we need to create a pull parser around the layout XML file, and then + // give that to our XmlBlockParser + try { + KXmlParser parser = new KXmlParser(); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + parser.setInput(new FileReader(xml)); + + return new BridgeXmlBlockParser(parser, mContext, mPlatformResourceFlag[0]); + } catch (XmlPullParserException e) { + mContext.getLogger().error(e); + + // we'll return null below. + } catch (FileNotFoundException e) { + // this shouldn't happen since we check above. + } + } + } + + // id was not found or not resolved. Throw a NotFoundException. + throwException(id); + + // this is not used since the method above always throws + return null; + } + + @Override + public TypedArray obtainAttributes(AttributeSet set, int[] attrs) { + return mContext.obtainStyledAttributes(set, attrs); + } + + @Override + public TypedArray obtainTypedArray(int id) throws NotFoundException { + throw new UnsupportedOperationException(); + } + + + @Override + public float getDimension(int id) throws NotFoundException { + IResourceValue value = getResourceValue(id, mPlatformResourceFlag); + + if (value != null) { + String v = value.getValue(); + + if (v != null) { + if (v.equals(BridgeConstants.FILL_PARENT)) { + return LayoutParams.FILL_PARENT; + } else if (v.equals(BridgeConstants.WRAP_CONTENT)) { + return LayoutParams.WRAP_CONTENT; + } + + if (ResourceHelper.stringToFloat(v, mTmpValue) && + mTmpValue.type == TypedValue.TYPE_DIMENSION) { + return mTmpValue.getDimension(mMetrics); + } + } + } + + // id was not found or not resolved. Throw a NotFoundException. + throwException(id); + + // this is not used since the method above always throws + return 0; + } + + @Override + public int getDimensionPixelOffset(int id) throws NotFoundException { + IResourceValue value = getResourceValue(id, mPlatformResourceFlag); + + if (value != null) { + String v = value.getValue(); + + if (v != null) { + if (ResourceHelper.stringToFloat(v, mTmpValue) && + mTmpValue.type == TypedValue.TYPE_DIMENSION) { + return TypedValue.complexToDimensionPixelOffset(mTmpValue.data, mMetrics); + } + } + } + + // id was not found or not resolved. Throw a NotFoundException. + throwException(id); + + // this is not used since the method above always throws + return 0; + } + + @Override + public int getDimensionPixelSize(int id) throws NotFoundException { + IResourceValue value = getResourceValue(id, mPlatformResourceFlag); + + if (value != null) { + String v = value.getValue(); + + if (v != null) { + if (ResourceHelper.stringToFloat(v, mTmpValue) && + mTmpValue.type == TypedValue.TYPE_DIMENSION) { + return TypedValue.complexToDimensionPixelSize(mTmpValue.data, mMetrics); + } + } + } + + // id was not found or not resolved. Throw a NotFoundException. + throwException(id); + + // this is not used since the method above always throws + return 0; + } + + @Override + public int getInteger(int id) throws NotFoundException { + IResourceValue value = getResourceValue(id, mPlatformResourceFlag); + + if (value != null && value.getValue() != null) { + String v = value.getValue(); + int radix = 10; + if (v.startsWith("0x")) { + v = v.substring(2); + radix = 16; + } + try { + return Integer.parseInt(v, radix); + } catch (NumberFormatException e) { + // return exception below + } + } + + // id was not found or not resolved. Throw a NotFoundException. + throwException(id); + + // this is not used since the method above always throws + return 0; + } + + @Override + public String getResourceEntryName(int resid) throws NotFoundException { + throw new UnsupportedOperationException(); + } + + @Override + public String getResourceName(int resid) throws NotFoundException { + throw new UnsupportedOperationException(); + } + + @Override + public String getResourceTypeName(int resid) throws NotFoundException { + throw new UnsupportedOperationException(); + } + + @Override + public String getString(int id, Object... formatArgs) throws NotFoundException { + String s = getString(id); + if (s != null) { + return String.format(s, formatArgs); + + } + + // id was not found or not resolved. Throw a NotFoundException. + throwException(id); + + // this is not used since the method above always throws + return null; + } + + @Override + public String getString(int id) throws NotFoundException { + IResourceValue value = getResourceValue(id, mPlatformResourceFlag); + + if (value != null && value.getValue() != null) { + return value.getValue(); + } + + // id was not found or not resolved. Throw a NotFoundException. + throwException(id); + + // this is not used since the method above always throws + return null; + } + + @Override + public void getValue(int id, TypedValue outValue, boolean resolveRefs) + throws NotFoundException { + IResourceValue value = getResourceValue(id, mPlatformResourceFlag); + + if (value != null) { + String v = value.getValue(); + + if (v != null) { + if (ResourceHelper.stringToFloat(v, outValue)) { + return; + } + } + } + + // id was not found or not resolved. Throw a NotFoundException. + throwException(id); + } + + @Override + public void getValue(String name, TypedValue outValue, boolean resolveRefs) + throws NotFoundException { + throw new UnsupportedOperationException(); + } + + @Override + public XmlResourceParser getXml(int id) throws NotFoundException { + IResourceValue value = getResourceValue(id, mPlatformResourceFlag); + + if (value != null) { + String v = value.getValue(); + + if (v != null) { + // check this is a file + File f = new File(value.getValue()); + if (f.isFile()) { + try { + KXmlParser parser = new KXmlParser(); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + parser.setInput(new FileReader(f)); + + return new BridgeXmlBlockParser(parser, mContext, mPlatformResourceFlag[0]); + } catch (XmlPullParserException e) { + NotFoundException newE = new NotFoundException(); + newE.initCause(e); + throw newE; + } catch (FileNotFoundException e) { + NotFoundException newE = new NotFoundException(); + newE.initCause(e); + throw newE; + } + } + } + } + + // id was not found or not resolved. Throw a NotFoundException. + throwException(id); + + // this is not used since the method above always throws + return null; + } + + @Override + public InputStream openRawResource(int id) throws NotFoundException { + IResourceValue value = getResourceValue(id, mPlatformResourceFlag); + + if (value != null) { + String v = value.getValue(); + + if (v != null) { + // check this is a file + File f = new File(value.getValue()); + if (f.isFile()) { + try { + return new FileInputStream(f); + } catch (FileNotFoundException e) { + NotFoundException newE = new NotFoundException(); + newE.initCause(e); + throw newE; + } + } + } + } + + // id was not found or not resolved. Throw a NotFoundException. + throwException(id); + + // this is not used since the method above always throws + return null; + } + + @Override + public AssetFileDescriptor openRawResourceFd(int id) throws NotFoundException { + throw new UnsupportedOperationException(); + } + + /** + * Builds and throws a {@link Resources.NotFoundException} based on a resource id and a resource type. + * @param id the id of the resource + * @throws NotFoundException + */ + private void throwException(int id) throws NotFoundException { + // first get the String related to this id in the framework + String[] resourceInfo = Bridge.resolveResourceValue(id); + + // if the name is unknown in the framework, get it from the custom view loader. + if (resourceInfo == null && mProjectCallback != null) { + resourceInfo = mProjectCallback.resolveResourceValue(id); + } + + String message = null; + if (resourceInfo != null) { + message = String.format( + "Could not find %1$s resource matching value 0x%2$X (resolved name: %3$s) in current configuration.", + resourceInfo[1], id, resourceInfo[0]); + } else { + message = String.format( + "Could not resolve resource value: 0x%1$X.", id); + } + + throw new NotFoundException(message); + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeTypedArray.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeTypedArray.java new file mode 100644 index 0000000..f5da91d --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeTypedArray.java @@ -0,0 +1,781 @@ +/* + * 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 com.android.layoutlib.bridge; + +import com.android.internal.util.XmlUtils; +import com.android.layoutlib.api.IResourceValue; +import com.android.layoutlib.api.IStyleResourceValue; + +import org.kxml2.io.KXmlParser; +import org.xmlpull.v1.XmlPullParser; + +import android.content.res.ColorStateList; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import android.view.ViewGroup.LayoutParams; + +import java.io.File; +import java.io.FileReader; +import java.util.Map; + +/** + * TODO: describe. + */ +public final class BridgeTypedArray extends TypedArray { + + @SuppressWarnings("hiding") + private BridgeResources mResources; + private BridgeContext mContext; + @SuppressWarnings("hiding") + private IResourceValue[] mData; + private String[] mNames; + private final boolean mPlatformFile; + + public BridgeTypedArray(BridgeResources resources, BridgeContext context, int len, + boolean platformFile) { + super(null, null, null, 0); + mResources = resources; + mContext = context; + mPlatformFile = platformFile; + mData = new IResourceValue[len]; + mNames = new String[len]; + } + + /** A bridge-specific method that sets a value in the type array */ + public void bridgeSetValue(int index, String name, IResourceValue value) { + mData[index] = value; + mNames[index] = name; + } + + /** + * Seals the array after all calls to {@link #bridgeSetValue(int, String, IResourceValue)} have + * been done. + * <p/>This allows to compute the list of non default values, permitting + * {@link #getIndexCount()} to return the proper value. + */ + public void sealArray() { + // fills TypedArray.mIndices which is used to implement getIndexCount/getIndexAt + // first count the array size + int count = 0; + for (IResourceValue data : mData) { + if (data != null) { + count++; + } + } + + // allocate the table with an extra to store the size + mIndices = new int[count+1]; + mIndices[0] = count; + + // fill the array with the indices. + int index = 1; + for (int i = 0 ; i < mData.length ; i++) { + if (mData[i] != null) { + mIndices[index++] = i; + } + } + } + + /** + * Return the number of values in this array. + */ + @Override + public int length() { + return mData.length; + } + + /** + * Return the Resources object this array was loaded from. + */ + @Override + public Resources getResources() { + return mResources; + } + + /** + * Retrieve the styled string value for the attribute at <var>index</var>. + * + * @param index Index of attribute to retrieve. + * + * @return CharSequence holding string data. May be styled. Returns + * null if the attribute is not defined. + */ + @Override + public CharSequence getText(int index) { + if (mData[index] != null) { + // FIXME: handle styled strings! + return mData[index].getValue(); + } + + return null; + } + + /** + * Retrieve the string value for the attribute at <var>index</var>. + * + * @param index Index of attribute to retrieve. + * + * @return String holding string data. Any styling information is + * removed. Returns null if the attribute is not defined. + */ + @Override + public String getString(int index) { + if (mData[index] != null) { + return mData[index].getValue(); + } + + return null; + } + + /** + * Retrieve the boolean value for the attribute at <var>index</var>. + * + * @param index Index of attribute to retrieve. + * @param defValue Value to return if the attribute is not defined. + * + * @return Attribute boolean value, or defValue if not defined. + */ + @Override + public boolean getBoolean(int index, boolean defValue) { + if (mData[index] == null) { + return defValue; + } + + String s = mData[index].getValue(); + if (s != null) { + return XmlUtils.convertValueToBoolean(s, defValue); + } + + return defValue; + } + + /** + * Retrieve the integer value for the attribute at <var>index</var>. + * + * @param index Index of attribute to retrieve. + * @param defValue Value to return if the attribute is not defined. + * + * @return Attribute int value, or defValue if not defined. + */ + @Override + public int getInt(int index, int defValue) { + if (mData[index] == null) { + return defValue; + } + + String s = mData[index].getValue(); + + try { + return (s == null) ? defValue : XmlUtils.convertValueToInt(s, defValue); + } catch (NumberFormatException e) { + // pass + } + + // Field is not null and is not an integer. + // Check for possible constants and try to find them. + // Get the map of attribute-constant -> IntegerValue + Map<String, Integer> map = Bridge.getEnumValues(mNames[index]); + + if (map != null) { + // accumulator to store the value of the 1+ constants. + int result = 0; + + // split the value in case this is a mix of several flags. + String[] keywords = s.split("\\|"); + for (String keyword : keywords) { + Integer i = map.get(keyword.trim()); + if (i != null) { + result |= i.intValue(); + } else { + mContext.getLogger().warning(String.format( + "Unknown constant \"%s\" in attribute \"%2$s\"", + keyword, mNames[index])); + } + } + return result; + } + + return defValue; + } + + /** + * Retrieve the float value for the attribute at <var>index</var>. + * + * @param index Index of attribute to retrieve. + * + * @return Attribute float value, or defValue if not defined.. + */ + @Override + public float getFloat(int index, float defValue) { + if (mData[index] == null) { + return defValue; + } + + String s = mData[index].getValue(); + + if (s != null) { + try { + return Float.parseFloat(s); + } catch (NumberFormatException e) { + mContext.getLogger().warning(String.format( + "Unable to convert \"%s\" into a float in attribute \"%2$s\"", + s, mNames[index])); + + // we'll return the default value below. + } + } + return defValue; + } + + /** + * Retrieve the color value for the attribute at <var>index</var>. If + * the attribute references a color resource holding a complex + * {@link android.content.res.ColorStateList}, then the default color from + * the set is returned. + * + * @param index Index of attribute to retrieve. + * @param defValue Value to return if the attribute is not defined or + * not a resource. + * + * @return Attribute color value, or defValue if not defined. + */ + @Override + public int getColor(int index, int defValue) { + if (mData[index] == null) { + return defValue; + } + + String s = mData[index].getValue(); + try { + return ResourceHelper.getColor(s); + } catch (NumberFormatException e) { + mContext.getLogger().warning(String.format( + "Unable to convert \"%s\" into a color in attribute \"%2$s\"", + s, mNames[index])); + + // we'll return the default value below. + } + + return defValue; + } + + /** + * Retrieve the ColorStateList for the attribute at <var>index</var>. + * The value may be either a single solid color or a reference to + * a color or complex {@link android.content.res.ColorStateList} description. + * + * @param index Index of attribute to retrieve. + * + * @return ColorStateList for the attribute, or null if not defined. + */ + @Override + public ColorStateList getColorStateList(int index) { + if (mData[index] == null) { + return null; + } + + String value = mData[index].getValue(); + + if (value == null) { + return null; + } + + try { + int color = ResourceHelper.getColor(value); + return ColorStateList.valueOf(color); + } catch (NumberFormatException e) { + // if it's not a color value, we'll attempt to read the xml based color below. + } + + // let the framework inflate the ColorStateList from the XML file. + try { + File f = new File(value); + if (f.isFile()) { + KXmlParser parser = new KXmlParser(); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + parser.setInput(new FileReader(f)); + + ColorStateList colorStateList = ColorStateList.createFromXml( + mContext.getResources(), + // FIXME: we need to know if this resource is platform or not + new BridgeXmlBlockParser(parser, mContext, false)); + return colorStateList; + } + } catch (Exception e) { + // this is an error and not warning since the file existence is checked before + // attempting to parse it. + mContext.getLogger().error(e); + + // return null below. + } + + // looks like were unable to resolve the color value. + mContext.getLogger().warning(String.format( + "Unable to resolve color value \"%1$s\" in attribute \"%2$s\"", + value, mNames[index])); + + return null; + } + + /** + * Retrieve the integer value for the attribute at <var>index</var>. + * + * @param index Index of attribute to retrieve. + * @param defValue Value to return if the attribute is not defined or + * not a resource. + * + * @return Attribute integer value, or defValue if not defined. + */ + @Override + public int getInteger(int index, int defValue) { + if (mData[index] == null) { + return defValue; + } + + String s = mData[index].getValue(); + + if (s != null) { + try { + return Integer.parseInt(s); + } catch (NumberFormatException e) { + mContext.getLogger().warning(String.format( + "Unable to convert \"%s\" into a integer in attribute \"%2$s\"", + s, mNames[index])); + + // The default value is returned below. + } + } + + return defValue; + } + + /** + * Retrieve a dimensional unit attribute at <var>index</var>. Unit + * conversions are based on the current {@link DisplayMetrics} + * associated with the resources this {@link TypedArray} object + * came from. + * + * @param index Index of attribute to retrieve. + * @param defValue Value to return if the attribute is not defined or + * not a resource. + * + * @return Attribute dimension value multiplied by the appropriate + * metric, or defValue if not defined. + * + * @see #getDimensionPixelOffset + * @see #getDimensionPixelSize + */ + @Override + public float getDimension(int index, float defValue) { + if (mData[index] == null) { + return defValue; + } + + String s = mData[index].getValue(); + + if (s == null) { + return defValue; + } else if (s.equals(BridgeConstants.FILL_PARENT)) { + return LayoutParams.FILL_PARENT; + } else if (s.equals(BridgeConstants.WRAP_CONTENT)) { + return LayoutParams.WRAP_CONTENT; + } + + if (ResourceHelper.stringToFloat(s, mValue)) { + return mValue.getDimension(mResources.mMetrics); + } + + // looks like we were unable to resolve the dimension value + mContext.getLogger().warning(String.format( + "Unable to resolve dimension value \"%1$s\" in attribute \"%2$s\"", + s, mNames[index])); + + return defValue; + } + + /** + * Retrieve a dimensional unit attribute at <var>index</var> for use + * as an offset in raw pixels. This is the same as + * {@link #getDimension}, except the returned value is converted to + * integer pixels for you. An offset conversion involves simply + * truncating the base value to an integer. + * + * @param index Index of attribute to retrieve. + * @param defValue Value to return if the attribute is not defined or + * not a resource. + * + * @return Attribute dimension value multiplied by the appropriate + * metric and truncated to integer pixels, or defValue if not defined. + * + * @see #getDimension + * @see #getDimensionPixelSize + */ + @Override + public int getDimensionPixelOffset(int index, int defValue) { + return (int) getDimension(index, defValue); + } + + /** + * Retrieve a dimensional unit attribute at <var>index</var> for use + * as a size in raw pixels. This is the same as + * {@link #getDimension}, except the returned value is converted to + * integer pixels for use as a size. A size conversion involves + * rounding the base value, and ensuring that a non-zero base value + * is at least one pixel in size. + * + * @param index Index of attribute to retrieve. + * @param defValue Value to return if the attribute is not defined or + * not a resource. + * + * @return Attribute dimension value multiplied by the appropriate + * metric and truncated to integer pixels, or defValue if not defined. + * + * @see #getDimension + * @see #getDimensionPixelOffset + */ + @Override + public int getDimensionPixelSize(int index, int defValue) { + if (mData[index] == null) { + return defValue; + } + + String s = mData[index].getValue(); + + if (s == null) { + return defValue; + } else if (s.equals(BridgeConstants.FILL_PARENT)) { + return LayoutParams.FILL_PARENT; + } else if (s.equals(BridgeConstants.WRAP_CONTENT)) { + return LayoutParams.WRAP_CONTENT; + } + + // FIXME huh? + + float f = getDimension(index, defValue); + final int res = (int)(f+0.5f); + if (res != 0) return res; + if (f == 0) return 0; + if (f > 0) return 1; + + throw new UnsupportedOperationException("Can't convert to dimension: " + + Integer.toString(index)); + } + + /** + * Special version of {@link #getDimensionPixelSize} for retrieving + * {@link android.view.ViewGroup}'s layout_width and layout_height + * attributes. This is only here for performance reasons; applications + * should use {@link #getDimensionPixelSize}. + * + * @param index Index of the attribute to retrieve. + * @param name Textual name of attribute for error reporting. + * + * @return Attribute dimension value multiplied by the appropriate + * metric and truncated to integer pixels. + */ + @Override + public int getLayoutDimension(int index, String name) { + return getDimensionPixelSize(index, 0); + } + + /** + * Retrieve a fractional unit attribute at <var>index</var>. + * + * @param index Index of attribute to retrieve. + * @param base The base value of this fraction. In other words, a + * standard fraction is multiplied by this value. + * @param pbase The parent base value of this fraction. In other + * words, a parent fraction (nn%p) is multiplied by this + * value. + * @param defValue Value to return if the attribute is not defined or + * not a resource. + * + * @return Attribute fractional value multiplied by the appropriate + * base value, or defValue if not defined. + */ + @Override + public float getFraction(int index, int base, int pbase, float defValue) { + if (mData[index] == null) { + return defValue; + } + + String value = mData[index].getValue(); + if (value == null) { + return defValue; + } + + if (ResourceHelper.stringToFloat(value, mValue)) { + return mValue.getFraction(base, pbase); + } + + // looks like we were unable to resolve the fraction value + mContext.getLogger().warning(String.format( + "Unable to resolve fraction value \"%1$s\" in attribute \"%2$s\"", + value, mNames[index])); + + return defValue; + } + + /** + * Retrieve the resource identifier for the attribute at + * <var>index</var>. Note that attribute resource as resolved when + * the overall {@link TypedArray} object is retrieved. As a + * result, this function will return the resource identifier of the + * final resource value that was found, <em>not</em> necessarily the + * original resource that was specified by the attribute. + * + * @param index Index of attribute to retrieve. + * @param defValue Value to return if the attribute is not defined or + * not a resource. + * + * @return Attribute resource identifier, or defValue if not defined. + */ + @Override + public int getResourceId(int index, int defValue) { + // get the IResource for this index + IResourceValue resValue = mData[index]; + + // no data, return the default value. + if (resValue == null) { + return defValue; + } + + // check if this is a style resource + if (resValue instanceof IStyleResourceValue) { + // get the id that will represent this style. + return mContext.getDynamicIdByStyle((IStyleResourceValue)resValue); + } + + // if the attribute was a reference to an id, and not a declaration of an id (@+id), then + // the xml attribute value was "resolved" which leads us to a IResourceValue with + // getType() returning "id" and getName() returning the id name + // (and getValue() returning null!). We need to handle this! + if (resValue.getType() != null && resValue.getType().equals(BridgeConstants.RES_ID)) { + // if this is a framework id + if (mPlatformFile || resValue.isFramework()) { + // look for idName in the android R classes + return mContext.getFrameworkIdValue(resValue.getName(), defValue); + } + + // look for idName in the project R class. + return mContext.getProjectIdValue(resValue.getName(), defValue); + } + + // else, try to get the value, and resolve it somehow. + String value = resValue.getValue(); + if (value == null) { + return defValue; + } + + // if the value is just an integer, return it. + try { + int i = Integer.parseInt(value); + if (Integer.toString(i).equals(value)) { + return i; + } + } catch (NumberFormatException e) { + // pass + } + + // Handle the @id/<name>, @+id/<name> and @android:id/<name> + // We need to return the exact value that was compiled (from the various R classes), + // as these values can be reused internally with calls to findViewById(). + // There's a trick with platform layouts that not use "android:" but their IDs are in + // fact in the android.R and com.android.internal.R classes. + // The field mPlatformFile will indicate that all IDs are to be looked up in the android R + // classes exclusively. + + // if this is a reference to an id, find it. + if (value.startsWith("@id/") || value.startsWith("@+") || + value.startsWith("@android:id/")) { + + int pos = value.indexOf('/'); + String idName = value.substring(pos + 1); + + // if this is a framework id + if (mPlatformFile || value.startsWith("@android") || value.startsWith("@+android")) { + // look for idName in the android R classes + return mContext.getFrameworkIdValue(idName, defValue); + } + + // look for idName in the project R class. + return mContext.getProjectIdValue(idName, defValue); + } + + // not a direct id valid reference? resolve it + Integer idValue = null; + + if (resValue.isFramework()) { + idValue = Bridge.getResourceValue(resValue.getType(), resValue.getName()); + } else { + idValue = mContext.getProjectCallback().getResourceValue( + resValue.getType(), resValue.getName()); + } + + if (idValue != null) { + return idValue.intValue(); + } + + mContext.getLogger().warning(String.format( + "Unable to resolve id \"%1$s\" for attribute \"%2$s\"", value, mNames[index])); + return defValue; + } + + /** + * Retrieve the Drawable for the attribute at <var>index</var>. This + * gets the resource ID of the selected attribute, and uses + * {@link Resources#getDrawable Resources.getDrawable} of the owning + * Resources object to retrieve its Drawable. + * + * @param index Index of attribute to retrieve. + * + * @return Drawable for the attribute, or null if not defined. + */ + @Override + public Drawable getDrawable(int index) { + if (mData[index] == null) { + return null; + } + + String value = mData[index].getValue(); + if (value == null || BridgeConstants.REFERENCE_NULL.equals(value)) { + return null; + } + + Drawable d = ResourceHelper.getDrawable(value, mContext, mData[index].isFramework()); + + if (d != null) { + return d; + } + + // looks like we were unable to resolve the drawable + mContext.getLogger().warning(String.format( + "Unable to resolve drawable \"%1$s\" in attribute \"%2$s\"", value, mNames[index])); + + return null; + } + + + /** + * Retrieve the CharSequence[] for the attribute at <var>index</var>. + * This gets the resource ID of the selected attribute, and uses + * {@link Resources#getTextArray Resources.getTextArray} of the owning + * Resources object to retrieve its String[]. + * + * @param index Index of attribute to retrieve. + * + * @return CharSequence[] for the attribute, or null if not defined. + */ + @Override + public CharSequence[] getTextArray(int index) { + if (mData[index] == null) { + return null; + } + + String value = mData[index].getValue(); + if (value == null) { + return null; + } + + throw new UnsupportedOperationException( + String.format("BridgeTypedArray: UNKNOWN VALUE FOR getTextArray(%d) => %s", //DEBUG + index, value)); + + } + + /** + * Retrieve the raw TypedValue for the attribute at <var>index</var>. + * + * @param index Index of attribute to retrieve. + * @param outValue TypedValue object in which to place the attribute's + * data. + * + * @return Returns true if the value was retrieved, else false. + */ + @Override + public boolean getValue(int index, TypedValue outValue) { + if (mData[index] == null) { + return false; + } + + String s = mData[index].getValue(); + + return ResourceHelper.stringToFloat(s, outValue); + } + + /** + * Determines whether there is an attribute at <var>index</var>. + * + * @param index Index of attribute to retrieve. + * + * @return True if the attribute has a value, false otherwise. + */ + @Override + public boolean hasValue(int index) { + return mData[index] != null; + } + + /** + * Retrieve the raw TypedValue for the attribute at <var>index</var> + * and return a temporary object holding its data. This object is only + * valid until the next call on to {@link TypedArray}. + * + * @param index Index of attribute to retrieve. + * + * @return Returns a TypedValue object if the attribute is defined, + * containing its data; otherwise returns null. (You will not + * receive a TypedValue whose type is TYPE_NULL.) + */ + @Override + public TypedValue peekValue(int index) { + if (getValue(index, mValue)) { + return mValue; + } + + return null; + } + + /** + * Returns a message about the parser state suitable for printing error messages. + */ + @Override + public String getPositionDescription() { + return "<internal -- stub if needed>"; + } + + /** + * Give back a previously retrieved StyledAttributes, for later re-use. + */ + @Override + public void recycle() { + // pass + } + + @Override + public boolean getValueAt(int index, TypedValue outValue) { + // pass + return false; + } + + @Override + public String toString() { + return mData.toString(); + } + } diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeXmlBlockParser.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeXmlBlockParser.java new file mode 100644 index 0000000..d842a66 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeXmlBlockParser.java @@ -0,0 +1,378 @@ +/* + * 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 com.android.layoutlib.bridge; + +import com.android.layoutlib.api.IXmlPullParser; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.content.res.XmlResourceParser; +import android.util.AttributeSet; +import android.util.XmlPullAttributes; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; + +/** + * {@link BridgeXmlBlockParser} reimplements most of android.xml.XmlBlock.Parser. + * It delegates to both an instance of {@link XmlPullParser} and an instance of + * {@link XmlPullAttributes} (for the {@link AttributeSet} part). + */ +public class BridgeXmlBlockParser implements XmlResourceParser { + + private XmlPullParser mParser; + private XmlPullAttributes mAttrib; + + private boolean mStarted = false; + private boolean mDecNextDepth = false; + private int mDepth = 0; + private int mEventType = START_DOCUMENT; + private final boolean mPlatformFile; + + /** + * Builds a {@link BridgeXmlBlockParser}. + * @param parser The XmlPullParser to get the content from. + * @param context the Context. + * @param platformFile Indicates whether the the file is a platform file or not. + */ + public BridgeXmlBlockParser(XmlPullParser parser, BridgeContext context, boolean platformFile) { + mParser = parser; + mPlatformFile = platformFile; + mAttrib = new BridgeXmlPullAttributes(parser, context, mPlatformFile); + } + + public boolean isPlatformFile() { + return mPlatformFile; + } + + public Object getViewKey() { + if (mParser instanceof IXmlPullParser) { + return ((IXmlPullParser)mParser).getViewKey(); + } + + return null; + } + + + // ------- XmlResourceParser implementation + + public void setFeature(String name, boolean state) + throws XmlPullParserException { + if (FEATURE_PROCESS_NAMESPACES.equals(name) && state) { + return; + } + if (FEATURE_REPORT_NAMESPACE_ATTRIBUTES.equals(name) && state) { + return; + } + throw new XmlPullParserException("Unsupported feature: " + name); + } + + public boolean getFeature(String name) { + if (FEATURE_PROCESS_NAMESPACES.equals(name)) { + return true; + } + if (FEATURE_REPORT_NAMESPACE_ATTRIBUTES.equals(name)) { + return true; + } + return false; + } + + public void setProperty(String name, Object value) throws XmlPullParserException { + throw new XmlPullParserException("setProperty() not supported"); + } + + public Object getProperty(String name) { + return null; + } + + public void setInput(Reader in) throws XmlPullParserException { + mParser.setInput(in); + } + + public void setInput(InputStream inputStream, String inputEncoding) + throws XmlPullParserException { + mParser.setInput(inputStream, inputEncoding); + } + + public void defineEntityReplacementText(String entityName, + String replacementText) throws XmlPullParserException { + throw new XmlPullParserException( + "defineEntityReplacementText() not supported"); + } + + public String getNamespacePrefix(int pos) throws XmlPullParserException { + throw new XmlPullParserException("getNamespacePrefix() not supported"); + } + + public String getInputEncoding() { + return null; + } + + public String getNamespace(String prefix) { + throw new RuntimeException("getNamespace() not supported"); + } + + public int getNamespaceCount(int depth) throws XmlPullParserException { + throw new XmlPullParserException("getNamespaceCount() not supported"); + } + + public String getPositionDescription() { + return "Binary XML file line #" + getLineNumber(); + } + + public String getNamespaceUri(int pos) throws XmlPullParserException { + throw new XmlPullParserException("getNamespaceUri() not supported"); + } + + public int getColumnNumber() { + return -1; + } + + public int getDepth() { + return mDepth; + } + + public String getText() { + return mParser.getText(); + } + + public int getLineNumber() { + return mParser.getLineNumber(); + } + + public int getEventType() { + return mEventType; + } + + public boolean isWhitespace() throws XmlPullParserException { + // Original comment: whitespace was stripped by aapt. + return mParser.isWhitespace(); + } + + public String getPrefix() { + throw new RuntimeException("getPrefix not supported"); + } + + public char[] getTextCharacters(int[] holderForStartAndLength) { + String txt = getText(); + char[] chars = null; + if (txt != null) { + holderForStartAndLength[0] = 0; + holderForStartAndLength[1] = txt.length(); + chars = new char[txt.length()]; + txt.getChars(0, txt.length(), chars, 0); + } + return chars; + } + + public String getNamespace() { + return mParser.getNamespace(); + } + + public String getName() { + return mParser.getName(); + } + + public String getAttributeNamespace(int index) { + return mParser.getAttributeNamespace(index); + } + + public String getAttributeName(int index) { + return mParser.getAttributeName(index); + } + + public String getAttributePrefix(int index) { + throw new RuntimeException("getAttributePrefix not supported"); + } + + public boolean isEmptyElementTag() { + // XXX Need to detect this. + return false; + } + + public int getAttributeCount() { + return mParser.getAttributeCount(); + } + + public String getAttributeValue(int index) { + return mParser.getAttributeValue(index); + } + + public String getAttributeType(int index) { + return "CDATA"; + } + + public boolean isAttributeDefault(int index) { + return false; + } + + public int nextToken() throws XmlPullParserException, IOException { + return next(); + } + + public String getAttributeValue(String namespace, String name) { + return mParser.getAttributeValue(namespace, name); + } + + public int next() throws XmlPullParserException, IOException { + if (!mStarted) { + mStarted = true; + return START_DOCUMENT; + } + int ev = mParser.next(); + if (mDecNextDepth) { + mDepth--; + mDecNextDepth = false; + } + switch (ev) { + case START_TAG: + mDepth++; + break; + case END_TAG: + mDecNextDepth = true; + break; + } + mEventType = ev; + return ev; + } + + public void require(int type, String namespace, String name) + throws XmlPullParserException { + if (type != getEventType() + || (namespace != null && !namespace.equals(getNamespace())) + || (name != null && !name.equals(getName()))) + throw new XmlPullParserException("expected " + TYPES[type] + + getPositionDescription()); + } + + public String nextText() throws XmlPullParserException, IOException { + if (getEventType() != START_TAG) { + throw new XmlPullParserException(getPositionDescription() + + ": parser must be on START_TAG to read next text", this, + null); + } + int eventType = next(); + if (eventType == TEXT) { + String result = getText(); + eventType = next(); + if (eventType != END_TAG) { + throw new XmlPullParserException( + getPositionDescription() + + ": event TEXT it must be immediately followed by END_TAG", + this, null); + } + return result; + } else if (eventType == END_TAG) { + return ""; + } else { + throw new XmlPullParserException(getPositionDescription() + + ": parser must be on START_TAG or TEXT to read text", + this, null); + } + } + + public int nextTag() throws XmlPullParserException, IOException { + int eventType = next(); + if (eventType == TEXT && isWhitespace()) { // skip whitespace + eventType = next(); + } + if (eventType != START_TAG && eventType != END_TAG) { + throw new XmlPullParserException(getPositionDescription() + + ": expected start or end tag", this, null); + } + return eventType; + } + + // AttributeSet implementation + + + public void close() { + // pass + } + + public boolean getAttributeBooleanValue(int index, boolean defaultValue) { + return mAttrib.getAttributeBooleanValue(index, defaultValue); + } + + public boolean getAttributeBooleanValue(String namespace, String attribute, + boolean defaultValue) { + return mAttrib.getAttributeBooleanValue(namespace, attribute, defaultValue); + } + + public float getAttributeFloatValue(int index, float defaultValue) { + return mAttrib.getAttributeFloatValue(index, defaultValue); + } + + public float getAttributeFloatValue(String namespace, String attribute, float defaultValue) { + return mAttrib.getAttributeFloatValue(namespace, attribute, defaultValue); + } + + public int getAttributeIntValue(int index, int defaultValue) { + return mAttrib.getAttributeIntValue(index, defaultValue); + } + + public int getAttributeIntValue(String namespace, String attribute, int defaultValue) { + return mAttrib.getAttributeIntValue(namespace, attribute, defaultValue); + } + + public int getAttributeListValue(int index, String[] options, int defaultValue) { + return mAttrib.getAttributeListValue(index, options, defaultValue); + } + + public int getAttributeListValue(String namespace, String attribute, + String[] options, int defaultValue) { + return mAttrib.getAttributeListValue(namespace, attribute, options, defaultValue); + } + + public int getAttributeNameResource(int index) { + return mAttrib.getAttributeNameResource(index); + } + + public int getAttributeResourceValue(int index, int defaultValue) { + return mAttrib.getAttributeResourceValue(index, defaultValue); + } + + public int getAttributeResourceValue(String namespace, String attribute, int defaultValue) { + return mAttrib.getAttributeResourceValue(namespace, attribute, defaultValue); + } + + public int getAttributeUnsignedIntValue(int index, int defaultValue) { + return mAttrib.getAttributeUnsignedIntValue(index, defaultValue); + } + + public int getAttributeUnsignedIntValue(String namespace, String attribute, int defaultValue) { + return mAttrib.getAttributeUnsignedIntValue(namespace, attribute, defaultValue); + } + + public String getClassAttribute() { + return mAttrib.getClassAttribute(); + } + + public String getIdAttribute() { + return mAttrib.getIdAttribute(); + } + + public int getIdAttributeResourceValue(int defaultValue) { + return mAttrib.getIdAttributeResourceValue(defaultValue); + } + + public int getStyleAttribute() { + return mAttrib.getStyleAttribute(); + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeXmlPullAttributes.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeXmlPullAttributes.java new file mode 100644 index 0000000..4be6eab --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeXmlPullAttributes.java @@ -0,0 +1,122 @@ +/* + * 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 com.android.layoutlib.bridge; + +import com.android.layoutlib.api.IResourceValue; + +import org.xmlpull.v1.XmlPullParser; + +import android.util.AttributeSet; +import android.util.XmlPullAttributes; + +/** + * A correct implementation of the {@link AttributeSet} interface on top of a XmlPullParser + */ +public class BridgeXmlPullAttributes extends XmlPullAttributes { + + private final BridgeContext mContext; + private final boolean mPlatformFile; + + public BridgeXmlPullAttributes(XmlPullParser parser, BridgeContext context, + boolean platformFile) { + super(parser); + mContext = context; + mPlatformFile = platformFile; + } + + /* + * (non-Javadoc) + * @see android.util.XmlPullAttributes#getAttributeNameResource(int) + * + * This methods must return com.android.internal.R.attr.<name> matching + * the name of the attribute. + * It returns 0 if it doesn't find anything. + */ + @Override + public int getAttributeNameResource(int index) { + // get the attribute name. + String name = getAttributeName(index); + + // get the attribute namespace + String ns = mParser.getAttributeNamespace(index); + + if (BridgeConstants.NS_RESOURCES.equals(ns)) { + Integer v = Bridge.getResourceValue(BridgeConstants.RES_ATTR, name); + if (v != null) { + return v.intValue(); + } + + return 0; + } + + // this is not an attribute in the android namespace, we query the customviewloader, if + // the namespaces match. + if (mContext.getProjectCallback().getNamespace().equals(ns)) { + Integer v = mContext.getProjectCallback().getResourceValue(BridgeConstants.RES_ATTR, + name); + if (v != null) { + return v.intValue(); + } + } + + return 0; + } + + /* + * (non-Javadoc) + * @see android.util.XmlPullAttributes#getAttributeResourceValue(int, int) + */ + @Override + public int getAttributeResourceValue(int index, int defaultValue) { + String value = getAttributeValue(index); + + return resolveResourceValue(value, defaultValue); + } + + /* + * (non-Javadoc) + * @see android.util.XmlPullAttributes#getAttributeResourceValue(java.lang.String, java.lang.String, int) + */ + @Override + public int getAttributeResourceValue(String namespace, String attribute, int defaultValue) { + String value = getAttributeValue(namespace, attribute); + + return resolveResourceValue(value, defaultValue); + } + + private int resolveResourceValue(String value, int defaultValue) { + // now look for this particular value + IResourceValue resource = mContext.resolveResValue(mContext.findResValue(value)); + + if (resource != null) { + Integer id = null; + if (mPlatformFile || resource.isFramework()) { + id = Bridge.getResourceValue(resource.getType(), resource.getName()); + } else { + id = mContext.getProjectCallback().getResourceValue( + resource.getType(), resource.getName()); + } + + if (id != null) { + return id; + } + } + + return defaultValue; + } + +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/FontLoader.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/FontLoader.java new file mode 100644 index 0000000..1bdd1cc --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/FontLoader.java @@ -0,0 +1,349 @@ +/* + * 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 com.android.layoutlib.bridge; + +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +import android.graphics.Typeface; + +import java.awt.Font; +import java.awt.FontFormatException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +/** + * Provides {@link Font} object to the layout lib. + * <p/> + * The fonts are loaded from the SDK directory. Family/style mapping is done by parsing the + * fonts.xml file located alongside the ttf files. + */ +public final class FontLoader { + private static final String FONTS_DEFINITIONS = "fonts.xml"; + + private static final String NODE_FONTS = "fonts"; + private static final String NODE_FONT = "font"; + private static final String NODE_NAME = "name"; + + private static final String ATTR_TTF = "ttf"; + + private static final String[] NODE_LEVEL = { NODE_FONTS, NODE_FONT, NODE_NAME }; + + private static final String FONT_EXT = ".ttf"; + + private static final String[] FONT_STYLE_DEFAULT = { "", "-Regular" }; + private static final String[] FONT_STYLE_BOLD = { "-Bold" }; + private static final String[] FONT_STYLE_ITALIC = { "-Italic" }; + private static final String[] FONT_STYLE_BOLDITALIC = { "-BoldItalic" }; + + // list of font style, in the order matching the Typeface Font style + private static final String[][] FONT_STYLES = { + FONT_STYLE_DEFAULT, + FONT_STYLE_BOLD, + FONT_STYLE_ITALIC, + FONT_STYLE_BOLDITALIC + }; + + private final Map<String, String> mFamilyToTtf = new HashMap<String, String>(); + private final Map<String, Map<Integer, Font>> mTtfToFontMap = + new HashMap<String, Map<Integer, Font>>(); + + public static FontLoader create(String fontOsLocation) { + try { + SAXParserFactory parserFactory = SAXParserFactory.newInstance(); + parserFactory.setNamespaceAware(true); + + SAXParser parser = parserFactory.newSAXParser(); + File f = new File(fontOsLocation + File.separator + FONTS_DEFINITIONS); + + FontDefinitionParser definitionParser = new FontDefinitionParser( + fontOsLocation + File.separator); + parser.parse(new FileInputStream(f), definitionParser); + + return definitionParser.getFontLoader(); + } catch (ParserConfigurationException e) { + // return null below + } catch (SAXException e) { + // return null below + } catch (FileNotFoundException e) { + // return null below + } catch (IOException e) { + // return null below + } + + return null; + } + + private FontLoader(List<FontInfo> fontList) { + for (FontInfo info : fontList) { + for (String family : info.families) { + mFamilyToTtf.put(family, info.ttf); + } + } + } + + public synchronized Font getFont(String family, int[] style) { + if (family == null) { + return null; + } + + // get the ttf name from the family + String ttf = mFamilyToTtf.get(family); + + if (ttf == null) { + return null; + } + + // get the font from the ttf + Map<Integer, Font> styleMap = mTtfToFontMap.get(ttf); + + if (styleMap == null) { + styleMap = new HashMap<Integer, Font>(); + mTtfToFontMap.put(ttf, styleMap); + } + + Font f = styleMap.get(style); + + if (f != null) { + return f; + } + + // if it doesn't exist, we create it, and we can't, we try with a simpler style + switch (style[0]) { + case Typeface.NORMAL: + f = getFont(ttf, FONT_STYLES[Typeface.NORMAL]); + break; + case Typeface.BOLD: + case Typeface.ITALIC: + f = getFont(ttf, FONT_STYLES[style[0]]); + if (f == null) { + f = getFont(ttf, FONT_STYLES[Typeface.NORMAL]); + style[0] = Typeface.NORMAL; + } + break; + case Typeface.BOLD_ITALIC: + f = getFont(ttf, FONT_STYLES[style[0]]); + if (f == null) { + f = getFont(ttf, FONT_STYLES[Typeface.BOLD]); + if (f != null) { + style[0] = Typeface.BOLD; + } else { + f = getFont(ttf, FONT_STYLES[Typeface.ITALIC]); + if (f != null) { + style[0] = Typeface.ITALIC; + } else { + f = getFont(ttf, FONT_STYLES[Typeface.NORMAL]); + style[0] = Typeface.NORMAL; + } + } + } + break; + } + + if (f != null) { + styleMap.put(style[0], f); + return f; + } + + return null; + } + + private Font getFont(String ttf, String[] fontFileSuffix) { + for (String suffix : fontFileSuffix) { + String name = ttf + suffix + FONT_EXT; + + File f = new File(name); + if (f.isFile()) { + try { + Font font = Font.createFont(Font.TRUETYPE_FONT, f); + if (font != null) { + return font; + } + } catch (FontFormatException e) { + // skip this font name + } catch (IOException e) { + // skip this font name + } + } + } + + return null; + } + + private final static class FontInfo { + String ttf; + final Set<String> families; + + FontInfo() { + families = new HashSet<String>(); + } + } + + private final static class FontDefinitionParser extends DefaultHandler { + private final String mOsFontsLocation; + + private int mDepth = 0; + private FontInfo mFontInfo = null; + private final StringBuilder mBuilder = new StringBuilder(); + private final List<FontInfo> mFontList = new ArrayList<FontInfo>(); + + private FontDefinitionParser(String osFontsLocation) { + super(); + mOsFontsLocation = osFontsLocation; + } + + FontLoader getFontLoader() { + return new FontLoader(mFontList); + } + + /* (non-Javadoc) + * @see org.xml.sax.helpers.DefaultHandler#startElement(java.lang.String, java.lang.String, java.lang.String, org.xml.sax.Attributes) + */ + @Override + public void startElement(String uri, String localName, String name, Attributes attributes) + throws SAXException { + if (localName.equals(NODE_LEVEL[mDepth])) { + mDepth++; + + if (mDepth == 2) { // font level. + String ttf = attributes.getValue(ATTR_TTF); + if (ttf != null) { + mFontInfo = new FontInfo(); + mFontInfo.ttf = mOsFontsLocation + ttf; + mFontList.add(mFontInfo); + } + } + } + + super.startElement(uri, localName, name, attributes); + } + + /* (non-Javadoc) + * @see org.xml.sax.helpers.DefaultHandler#characters(char[], int, int) + */ + @SuppressWarnings("unused") + @Override + public void characters(char[] ch, int start, int length) throws SAXException { + if (mFontInfo != null) { + mBuilder.append(ch, start, length); + } + } + + /* (non-Javadoc) + * @see org.xml.sax.helpers.DefaultHandler#endElement(java.lang.String, java.lang.String, java.lang.String) + */ + @SuppressWarnings("unused") + @Override + public void endElement(String uri, String localName, String name) throws SAXException { + if (localName.equals(NODE_LEVEL[mDepth-1])) { + mDepth--; + if (mDepth == 2) { // end of a <name> node + if (mFontInfo != null) { + String family = trimXmlWhitespaces(mBuilder.toString()); + mFontInfo.families.add(family); + mBuilder.setLength(0); + } + } else if (mDepth == 1) { // end of a <font> node + mFontInfo = null; + } + } + } + + private String trimXmlWhitespaces(String value) { + if (value == null) { + return null; + } + + // look for carriage return and replace all whitespace around it by just 1 space. + int index; + + while ((index = value.indexOf('\n')) != -1) { + // look for whitespace on each side + int left = index - 1; + while (left >= 0) { + if (Character.isWhitespace(value.charAt(left))) { + left--; + } else { + break; + } + } + + int right = index + 1; + int count = value.length(); + while (right < count) { + if (Character.isWhitespace(value.charAt(right))) { + right++; + } else { + break; + } + } + + // remove all between left and right (non inclusive) and replace by a single space. + String leftString = null; + if (left >= 0) { + leftString = value.substring(0, left + 1); + } + String rightString = null; + if (right < count) { + rightString = value.substring(right); + } + + if (leftString != null) { + value = leftString; + if (rightString != null) { + value += " " + rightString; + } + } else { + value = rightString != null ? rightString : ""; + } + } + + // now we un-escape the string + int length = value.length(); + char[] buffer = value.toCharArray(); + + for (int i = 0 ; i < length ; i++) { + if (buffer[i] == '\\') { + if (buffer[i+1] == 'n') { + // replace the char with \n + buffer[i+1] = '\n'; + } + + // offset the rest of the buffer since we go from 2 to 1 char + System.arraycopy(buffer, i+1, buffer, i, length - i - 1); + length--; + } + } + + return new String(buffer, 0, length); + } + + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/LayoutResult.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/LayoutResult.java new file mode 100644 index 0000000..c4c5225 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/LayoutResult.java @@ -0,0 +1,126 @@ +/* + * 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 com.android.layoutlib.bridge; + +import com.android.layoutlib.api.ILayoutResult; + +import java.awt.image.BufferedImage; + +/** + * Implementation of {@link ILayoutResult} + */ +public final class LayoutResult implements ILayoutResult { + + private final ILayoutViewInfo mRootView; + private final BufferedImage mImage; + private final int mSuccess; + private final String mErrorMessage; + + /** + * Creates a {@link #SUCCESS} {@link ILayoutResult} with the specified params + * @param rootView + * @param image + */ + public LayoutResult(ILayoutViewInfo rootView, BufferedImage image) { + mSuccess = SUCCESS; + mErrorMessage = null; + mRootView = rootView; + mImage = image; + } + + /** + * Creates a LayoutResult with a specific success code and associated message + * @param code + * @param message + */ + public LayoutResult(int code, String message) { + mSuccess = code; + mErrorMessage = message; + mRootView = null; + mImage = null; + } + + public int getSuccess() { + return mSuccess; + } + + public String getErrorMessage() { + return mErrorMessage; + } + + public BufferedImage getImage() { + return mImage; + } + + public ILayoutViewInfo getRootView() { + return mRootView; + } + + /** + * Implementation of {@link ILayoutResult.ILayoutViewInfo} + */ + public static final class LayoutViewInfo implements ILayoutViewInfo { + private final Object mKey; + private final String mName; + private final int mLeft; + private final int mRight; + private final int mTop; + private final int mBottom; + private ILayoutViewInfo[] mChildren; + + public LayoutViewInfo(String name, Object key, int left, int top, int right, int bottom) { + mName = name; + mKey = key; + mLeft = left; + mRight = right; + mTop = top; + mBottom = bottom; + } + + public void setChildren(ILayoutViewInfo[] children) { + mChildren = children; + } + + public ILayoutViewInfo[] getChildren() { + return mChildren; + } + + public Object getViewKey() { + return mKey; + } + + public String getName() { + return mName; + } + + public int getLeft() { + return mLeft; + } + + public int getTop() { + return mTop; + } + + public int getRight() { + return mRight; + } + + public int getBottom() { + return mBottom; + } + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/MockView.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/MockView.java new file mode 100644 index 0000000..1ca3182 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/MockView.java @@ -0,0 +1,45 @@ +/* + * 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 com.android.layoutlib.bridge; + +import android.content.Context; +import android.graphics.Canvas; +import android.util.AttributeSet; +import android.widget.TextView; + +/** + * Base class for mocked views. + * + * TODO: implement onDraw and draw a rectangle in a random color with the name of the class + * (or better the id of the view). + */ +public class MockView extends TextView { + + public MockView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + setText(this.getClass().getSimpleName()); + setTextColor(0xFF000000); + } + + @Override + public void onDraw(Canvas canvas) { + canvas.drawARGB(0xFF, 0x7F, 0x7F, 0x7F); + + super.onDraw(canvas); + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/NinePatchDrawable.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/NinePatchDrawable.java new file mode 100644 index 0000000..5f0852e --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/NinePatchDrawable.java @@ -0,0 +1,113 @@ +/* + * 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 com.android.layoutlib.bridge; + +import com.android.ninepatch.NinePatch; + +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; + +public class NinePatchDrawable extends Drawable { + + private NinePatch m9Patch; + + NinePatchDrawable(NinePatch ninePatch) { + m9Patch = ninePatch; + } + + @Override + public int getMinimumWidth() { + return m9Patch.getWidth(); + } + + @Override + public int getMinimumHeight() { + return m9Patch.getHeight(); + } + + /** + * Return the intrinsic width of the underlying drawable object. Returns + * -1 if it has no intrinsic width, such as with a solid color. + */ + @Override + public int getIntrinsicWidth() { + return m9Patch.getWidth(); + } + + /** + * Return the intrinsic height of the underlying drawable object. Returns + * -1 if it has no intrinsic height, such as with a solid color. + */ + @Override + public int getIntrinsicHeight() { + return m9Patch.getHeight(); + } + + /** + * Return in padding the insets suggested by this Drawable for placing + * content inside the drawable's bounds. Positive values move toward the + * center of the Drawable (set Rect.inset). Returns true if this drawable + * actually has a padding, else false. When false is returned, the padding + * is always set to 0. + */ + @Override + public boolean getPadding(Rect padding) { + int[] padd = new int[4]; + m9Patch.getPadding(padd); + padding.left = padd[0]; + padding.top = padd[1]; + padding.right = padd[2]; + padding.bottom = padd[3]; + return true; + } + + @Override + public void draw(Canvas canvas) { + if (canvas instanceof BridgeCanvas) { + BridgeCanvas bridgeCanvas = (BridgeCanvas)canvas; + + Rect r = getBounds(); + m9Patch.draw(bridgeCanvas.getGraphics2d(), r.left, r.top, r.width(), r.height()); + + return; + } + + throw new UnsupportedOperationException(); + } + + + // ----------- Not implemented methods --------------- + + + @Override + public int getOpacity() { + // FIXME + return 0xFF; + } + + @Override + public void setAlpha(int arg0) { + // FIXME ! + } + + @Override + public void setColorFilter(ColorFilter arg0) { + // FIXME + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/ResourceHelper.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/ResourceHelper.java new file mode 100644 index 0000000..fbdf8dc --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/ResourceHelper.java @@ -0,0 +1,353 @@ +/* + * 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 com.android.layoutlib.bridge; + +import com.android.ninepatch.NinePatch; + +import org.kxml2.io.KXmlParser; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.util.TypedValue; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.net.MalformedURLException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Helper class to provide various convertion method used in handling android resources. + */ +public final class ResourceHelper { + + private final static Pattern sFloatPattern = Pattern.compile("(-?[0-9]+(?:\\.[0-9]+)?)(.*)"); + private final static float[] sFloatOut = new float[1]; + + private final static TypedValue mValue = new TypedValue(); + + /** + * Returns the color value represented by the given string value + * @param value the color value + * @return the color as an int + * @throw NumberFormatException if the conversion failed. + */ + static int getColor(String value) { + if (value != null) { + if (value.startsWith("#") == false) { + throw new NumberFormatException(); + } + + value = value.substring(1); + + // make sure it's not longer than 32bit + if (value.length() > 8) { + throw new NumberFormatException(); + } + + if (value.length() == 3) { // RGB format + char[] color = new char[8]; + color[0] = color[1] = 'F'; + color[2] = color[3] = value.charAt(0); + color[4] = color[5] = value.charAt(1); + color[6] = color[7] = value.charAt(2); + value = new String(color); + } else if (value.length() == 4) { // ARGB format + char[] color = new char[8]; + color[0] = color[1] = value.charAt(0); + color[2] = color[3] = value.charAt(1); + color[4] = color[5] = value.charAt(2); + color[6] = color[7] = value.charAt(3); + value = new String(color); + } else if (value.length() == 6) { + value = "FF" + value; + } + + // this is a RRGGBB or AARRGGBB value + + // Integer.parseInt will fail to parse strings like "ff191919", so we use + // a Long, but cast the result back into an int, since we know that we're only + // dealing with 32 bit values. + return (int)Long.parseLong(value, 16); + } + + throw new NumberFormatException(); + } + + /** + * Returns a drawable from the given value. + * @param value The value. A path to a 9 patch, a bitmap or a xml based drawable, + * or an hexadecimal color + * @param context + * @param isFramework indicates whether the resource is a framework resources. + * Framework resources are cached, and loaded only once. + */ + public static Drawable getDrawable(String value, BridgeContext context, boolean isFramework) { + Drawable d = null; + + String lowerCaseValue = value.toLowerCase(); + + if (lowerCaseValue.endsWith(NinePatch.EXTENSION_9PATCH)) { + File f = new File(value); + if (f.isFile()) { + NinePatch ninePatch = Bridge.getCached9Patch(value, + isFramework ? null : context.getProjectKey()); + + if (ninePatch == null) { + try { + ninePatch = NinePatch.load(new File(value).toURL(), false /* convert */); + + Bridge.setCached9Patch(value, ninePatch, + isFramework ? null : context.getProjectKey()); + } catch (MalformedURLException e) { + // URL is wrong, we'll return null below + } catch (IOException e) { + // failed to read the file, we'll return null below. + } + } + + if (ninePatch != null) { + return new NinePatchDrawable(ninePatch); + } + } + + return null; + } else if (lowerCaseValue.endsWith(".xml")) { + // create a blockparser for the file + File f = new File(value); + if (f.isFile()) { + try { + // let the framework inflate the Drawable from the XML file. + KXmlParser parser = new KXmlParser(); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + parser.setInput(new FileReader(f)); + + d = Drawable.createFromXml(context.getResources(), + // FIXME: we need to know if this resource is platform or not + new BridgeXmlBlockParser(parser, context, false)); + return d; + } catch (XmlPullParserException e) { + context.getLogger().error(e); + } catch (FileNotFoundException e) { + // will not happen, since we pre-check + } catch (IOException e) { + context.getLogger().error(e); + } + } + + return null; + } else { + File bmpFile = new File(value); + if (bmpFile.isFile()) { + try { + Bitmap bitmap = Bridge.getCachedBitmap(value, + isFramework ? null : context.getProjectKey()); + + if (bitmap == null) { + bitmap = new Bitmap(bmpFile); + Bridge.setCachedBitmap(value, bitmap, + isFramework ? null : context.getProjectKey()); + } + + return new BitmapDrawable(bitmap); + } catch (IOException e) { + // we'll return null below + // TODO: log the error. + } + } else { + // attempt to get a color from the value + try { + int color = getColor(value); + return new ColorDrawable(color); + } catch (NumberFormatException e) { + // we'll return null below. + // TODO: log the error + } + } + } + + return null; + } + + + // ------- TypedValue stuff + // This is taken from //device/libs/utils/ResourceTypes.cpp + + private static final class UnitEntry { + String name; + int type; + int unit; + float scale; + + UnitEntry(String name, int type, int unit, float scale) { + this.name = name; + this.type = type; + this.unit = unit; + this.scale = scale; + } + } + + private final static UnitEntry[] sUnitNames = new UnitEntry[] { + new UnitEntry("px", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_PX, 1.0f), + new UnitEntry("dip", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_DIP, 1.0f), + new UnitEntry("dp", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_DIP, 1.0f), + new UnitEntry("sp", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_SP, 1.0f), + new UnitEntry("pt", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_PT, 1.0f), + new UnitEntry("in", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_IN, 1.0f), + new UnitEntry("mm", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_MM, 1.0f), + new UnitEntry("%", TypedValue.TYPE_FRACTION, TypedValue.COMPLEX_UNIT_FRACTION, 1.0f/100), + new UnitEntry("%p", TypedValue.TYPE_FRACTION, TypedValue.COMPLEX_UNIT_FRACTION_PARENT, 1.0f/100), + }; + + /** + * Returns the raw value from the given string. + * This object is only valid until the next call on to {@link ResourceHelper}. + */ + public static TypedValue getValue(String s) { + if (stringToFloat(s, mValue)) { + return mValue; + } + + return null; + } + + /** + * Convert the string into a {@link TypedValue}. + * @param s + * @param outValue + * @return true if success. + */ + public static boolean stringToFloat(String s, TypedValue outValue) { + // remove the space before and after + s.trim(); + int len = s.length(); + + if (len <= 0) { + return false; + } + + // check that there's no non ascii characters. + char[] buf = s.toCharArray(); + for (int i = 0 ; i < len ; i++) { + if (buf[i] > 255) { + return false; + } + } + + // check the first character + if (buf[0] < '0' && buf[0] > '9' && buf[0] != '.') { + return false; + } + + // now look for the string that is after the float... + Matcher m = sFloatPattern.matcher(s); + if (m.matches()) { + String f_str = m.group(1); + String end = m.group(2); + + float f; + try { + f = Float.parseFloat(f_str); + } catch (NumberFormatException e) { + // this shouldn't happen with the regexp above. + return false; + } + + if (end.length() > 0 && end.charAt(0) != ' ') { + // Might be a unit... + if (parseUnit(end, outValue, sFloatOut)) { + + f *= sFloatOut[0]; + boolean neg = f < 0; + if (neg) { + f = -f; + } + long bits = (long)(f*(1<<23)+.5f); + int radix; + int shift; + if ((bits&0x7fffff) == 0) { + // Always use 23p0 if there is no fraction, just to make + // things easier to read. + radix = TypedValue.COMPLEX_RADIX_23p0; + shift = 23; + } else if ((bits&0xffffffffff800000L) == 0) { + // Magnitude is zero -- can fit in 0 bits of precision. + radix = TypedValue.COMPLEX_RADIX_0p23; + shift = 0; + } else if ((bits&0xffffffff80000000L) == 0) { + // Magnitude can fit in 8 bits of precision. + radix = TypedValue.COMPLEX_RADIX_8p15; + shift = 8; + } else if ((bits&0xffffff8000000000L) == 0) { + // Magnitude can fit in 16 bits of precision. + radix = TypedValue.COMPLEX_RADIX_16p7; + shift = 16; + } else { + // Magnitude needs entire range, so no fractional part. + radix = TypedValue.COMPLEX_RADIX_23p0; + shift = 23; + } + int mantissa = (int)( + (bits>>shift) & TypedValue.COMPLEX_MANTISSA_MASK); + if (neg) { + mantissa = (-mantissa) & TypedValue.COMPLEX_MANTISSA_MASK; + } + outValue.data |= + (radix<<TypedValue.COMPLEX_RADIX_SHIFT) + | (mantissa<<TypedValue.COMPLEX_MANTISSA_SHIFT); + return true; + } + return false; + } + + // make sure it's only spaces at the end. + end = end.trim(); + + if (end.length() == 0) { + if (outValue != null) { + outValue.type = TypedValue.TYPE_FLOAT; + outValue.data = Float.floatToIntBits(f); + return true; + } + } + } + + return false; + } + + private static boolean parseUnit(String str, TypedValue outValue, float[] outScale) { + str = str.trim(); + + for (UnitEntry unit : sUnitNames) { + if (unit.name.equals(str)) { + outValue.type = unit.type; + outValue.data = unit.unit << TypedValue.COMPLEX_UNIT_SHIFT; + outScale[0] = unit.scale; + + return true; + } + } + + return false; + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/ResourceValue.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/ResourceValue.java new file mode 100644 index 0000000..01a4871 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/ResourceValue.java @@ -0,0 +1,67 @@ +/* + * 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 com.android.layoutlib.bridge; + +import com.android.layoutlib.api.IResourceValue; + +/** + * Basic implementation of IResourceValue. + */ +class ResourceValue implements IResourceValue { + private final String mType; + private final String mName; + private String mValue = null; + + ResourceValue(String name) { + mType = null; + mName = name; + } + + public ResourceValue(String type, String name, String value) { + mType = type; + mName = name; + mValue = value; + } + + public String getType() { + return mType; + } + + public final String getName() { + return mName; + } + + public final String getValue() { + return mValue; + } + + public final void setValue(String value) { + mValue = value; + } + + public void replaceWith(ResourceValue value) { + mValue = value.mValue; + } + + public boolean isFramework() { + // ResourceValue object created directly in the framework are used to describe + // non resolvable coming from the XML. Since they will never be cached (as they can't + // be a value pointing to a bitmap, or they'd be resolvable.), the return value deoes + // not matter. + return false; + } +} diff --git a/tools/layoutlib/bridge/src/com/google/android/maps/MapView.java b/tools/layoutlib/bridge/src/com/google/android/maps/MapView.java new file mode 100644 index 0000000..6d013bb --- /dev/null +++ b/tools/layoutlib/bridge/src/com/google/android/maps/MapView.java @@ -0,0 +1,121 @@ +/* + * 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 com.google.android.maps; + +import com.android.layoutlib.bridge.MockView; + +import android.content.Context; +import android.os.Bundle; +import android.util.AttributeSet; +import android.view.View; + +/** + * Mock version of the MapView. + * Only non override public methods from the real MapView have been added in there. + * Methods that take an unknown class as parameter or as return object, have been removed for now. + * + * TODO: generate automatically. + * + */ +public class MapView extends MockView { + + /** + * Construct a new WebView with a Context object. + * @param context A Context object used to access application assets. + */ + public MapView(Context context) { + this(context, null); + } + + /** + * Construct a new WebView with layout parameters. + * @param context A Context object used to access application assets. + * @param attrs An AttributeSet passed to our parent. + */ + public MapView(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.mapViewStyle); + } + + /** + * Construct a new WebView with layout parameters and a default style. + * @param context A Context object used to access application assets. + * @param attrs An AttributeSet passed to our parent. + * @param defStyle The default style resource ID. + */ + public MapView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + // START FAKE PUBLIC METHODS + + public void displayZoomControls(boolean takeFocus) { + } + + public boolean canCoverCenter() { + return false; + } + + public void preLoad() { + } + + public int getZoomLevel() { + return 0; + } + + public void setSatellite(boolean on) { + } + + public boolean isSatellite() { + return false; + } + + public void setTraffic(boolean on) { + } + + public boolean isTraffic() { + return false; + } + + public void setStreetView(boolean on) { + } + + public boolean isStreetView() { + return false; + } + + public int getLatitudeSpan() { + return 0; + } + + public int getLongitudeSpan() { + return 0; + } + + public int getMaxZoomLevel() { + return 0; + } + + public void onSaveInstanceState(Bundle state) { + } + + public void onRestoreInstanceState(Bundle state) { + } + + public View getZoomControls() { + return null; + } +} diff --git a/tools/layoutlib/bridge/tests/com/android/layoutlib/bridge/AndroidGraphicsTests.java b/tools/layoutlib/bridge/tests/com/android/layoutlib/bridge/AndroidGraphicsTests.java new file mode 100644 index 0000000..ac144e7 --- /dev/null +++ b/tools/layoutlib/bridge/tests/com/android/layoutlib/bridge/AndroidGraphicsTests.java @@ -0,0 +1,73 @@ +/* + * 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 com.android.layoutlib.bridge; + +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics._Original_Paint; +import android.text.TextPaint; + +import junit.framework.TestCase; + +/** + * + */ +public class AndroidGraphicsTests extends TestCase { + + @Override + protected void setUp() throws Exception { + super.setUp(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + } + + public void testMatrix() { + Matrix m1 = new Matrix(); + + assertFalse(m1.isIdentity()); + + m1.setValues(new float[] { 1,0,0, 0,1,0, 0,0,1 }); + assertTrue(m1.isIdentity()); + + Matrix m2 = new Matrix(m1); + + float[] v1 = new float[9]; + float[] v2 = new float[9]; + m1.getValues(v1); + m2.getValues(v2); + + for (int i = 0 ; i < 9; i++) { + assertEquals(v1[i], v2[i]); + } + } + + public void testPaint() { + _Original_Paint o = new _Original_Paint(); + assertNotNull(o); + + Paint p = new Paint(); + assertNotNull(p); + } + + public void textTextPaint() { + TextPaint p = new TextPaint(); + assertNotNull(p); + } +} diff --git a/tools/layoutlib/bridge/tests/com/android/layoutlib/bridge/BridgeTest.java b/tools/layoutlib/bridge/tests/com/android/layoutlib/bridge/BridgeTest.java new file mode 100644 index 0000000..e424f1d --- /dev/null +++ b/tools/layoutlib/bridge/tests/com/android/layoutlib/bridge/BridgeTest.java @@ -0,0 +1,229 @@ +/* + * 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 com.android.layoutlib.bridge; + +import com.android.layoutlib.api.ILayoutResult; +import com.android.layoutlib.api.IResourceValue; +import com.android.layoutlib.api.IStyleResourceValue; +import com.android.layoutlib.api.IXmlPullParser; +import com.android.layoutlib.api.ILayoutResult.ILayoutViewInfo; + +import org.kxml2.io.KXmlParser; +import org.xmlpull.v1.XmlPullParser; + +import java.io.File; +import java.io.FileReader; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +import junit.framework.TestCase; + +public class BridgeTest extends TestCase { + + /** the class being tested */ + private Bridge mBridge; + /** the path to the sample layout.xml file */ + private String mLayoutXml1Path; + private String mTextOnlyXmlPath; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + mBridge = new Bridge(); + + // FIXME: need some fonts somewhere. + mBridge.init(null /* fontOsLocation */, getAttributeValues()); + + URL url = this.getClass().getClassLoader().getResource("data/layout1.xml"); + mLayoutXml1Path = url.getFile(); + + url = this.getClass().getClassLoader().getResource("data/textonly.xml"); + mTextOnlyXmlPath = url.getFile(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + } + + // --------------- + + /** + * Test parser that implements {@link IXmlPullParser}. + */ + private static class TestParser extends KXmlParser implements IXmlPullParser { + public Object getViewKey() { + return null; + } + } + + public void testComputeLayout() throws Exception { + + TestParser parser = new TestParser(); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + parser.setInput(new FileReader(new File(mLayoutXml1Path))); + + Map<String, Map<String, IResourceValue>> projectResources = getProjectResources(); + + Map<String, Map<String, IResourceValue>> frameworkResources = getFrameworkResources(); + + int screenWidth = 320; + int screenHeight = 480; + + // FIXME need a dummy font for the tests! + ILayoutResult result = mBridge.computeLayout(parser, new Integer(1) /* projectKey */, + screenWidth, screenHeight, + "Theme", projectResources, frameworkResources, null, null); + + display(result.getRootView(), ""); + } + + private Map<String, Map<String, Integer>> getAttributeValues() { + Map<String, Map<String, Integer>> attributeValues = + new HashMap<String, Map<String,Integer>>(); + + // lets create a map for the orientation attribute + Map<String, Integer> attributeMap = new HashMap<String, Integer>(); + + attributeMap.put("horizontal", Integer.valueOf(0)); + attributeMap.put("vertical", Integer.valueOf(1)); + + attributeValues.put("orientation", attributeMap); + + return attributeValues; + } + + private Map<String, Map<String, IResourceValue>> getFrameworkResources() { + Map<String, Map<String, IResourceValue>> frameworkResources = + new HashMap<String, Map<String, IResourceValue>>(); + + // create the style map + Map<String, IResourceValue> styleMap = new HashMap<String, IResourceValue>(); + frameworkResources.put("style", styleMap); + + // create a button style. + IStyleResourceValue style = createStyle("Widget.Button", + "background", "@android:drawable/something", + "focusable", "true", + "clickable", "true", + "textAppearance", "?android:attr/textAppearanceSmallInverse", + "textColor", "?android:attr/textColorBrightInverseNoDisable", + "gravity", "center_vertical|center_horizontal" + ); + styleMap.put(style.getName(), style); + + // create the parent style of button style + style = createStyle("Widget", + "textAppearance", "?textAppearance"); + styleMap.put(style.getName(), style); + + // link the buttonStyle info in the default theme. + style = createStyle("Theme", + BridgeConstants.RES_STYLE, "buttonStyle", "@android:style/Widget.Button", + BridgeConstants.RES_STYLE, "textAppearance", "@android:style/TextAppearance", + BridgeConstants.RES_STYLE, "textAppearanceSmallInverse", "@android:style/TextAppearance.Small.Inverse", + BridgeConstants.RES_COLOR, "textColorBrightInverseNoDisable", "@android:color/bright_text_light_nodisable" + ); + styleMap.put(style.getName(), style); + + // create a dummy drawable to go with it + Map<String, IResourceValue> drawableMap = new HashMap<String, IResourceValue>(); + frameworkResources.put("drawable", drawableMap); + + // get the 9 patch test location + URL url = this.getClass().getClassLoader().getResource("data/button.9.png"); + + IResourceValue drawable = new ResourceValue(BridgeConstants.RES_DRAWABLE, "something", + url.getPath()); + drawableMap.put(drawable.getName(), drawable); + return frameworkResources; + } + + private Map<String, Map<String, IResourceValue>> getProjectResources() { + Map<String, Map<String, IResourceValue>> projectResources = + new HashMap<String, Map<String, IResourceValue>>(); + + // create the style map (even empty there should be one) + Map<String, IResourceValue> styleMap = new HashMap<String, IResourceValue>(); + projectResources.put("style", styleMap); + + return projectResources; + } + + + private void display(ILayoutViewInfo result, String offset) { + + String msg = String.format("%s%s L:%d T:%d R:%d B:%d", + offset, + result.getName(), + result.getLeft(), result.getTop(), result.getRight(), result.getBottom()); + + System.out.println(msg); + ILayoutViewInfo[] children = result.getChildren(); + if (children != null) { + offset += "+-"; + for (ILayoutViewInfo child : children) { + display(child, offset); + } + } + } + + /** + * Creates a {@link IStyleResourceValue} based on the given values. + * @param styleName the name of the style. + * @param items An array of Strings. Even indices contain a style item name, and odd indices + * a style item value. If the number of string in the array is not even, an exception is thrown. + */ + private IStyleResourceValue createStyle(String styleName, String... items) { + StyleResourceValue value = new StyleResourceValue(styleName); + + if (items.length % 3 == 0) { + for (int i = 0 ; i < items.length;) { + value.addItem(new ResourceValue(items[i++], items[i++], items[i++])); + } + } else { + throw new IllegalArgumentException("Need a multiple of 3 for the number of strings"); + } + + return value; + } + + // --------------- + + public void testTextLayout() throws Exception { + + TestParser parser = new TestParser(); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + parser.setInput(new FileReader(new File(mTextOnlyXmlPath))); + + Map<String, Map<String, IResourceValue>> projectResources = getProjectResources(); + Map<String, Map<String, IResourceValue>> frameworkResources = getFrameworkResources(); + + int screenWidth = 320; + int screenHeight = 480; + + // FIXME need a dummy font for the tests! + ILayoutResult result = mBridge.computeLayout(parser, new Integer(1) /* projectKey */, + screenWidth, screenHeight, + "Theme", projectResources, frameworkResources, null, null); + + display(result.getRootView(), ""); + } + +} diff --git a/tools/layoutlib/bridge/tests/com/android/layoutlib/bridge/BridgeXmlBlockParserTest.java b/tools/layoutlib/bridge/tests/com/android/layoutlib/bridge/BridgeXmlBlockParserTest.java new file mode 100644 index 0000000..cac1f95 --- /dev/null +++ b/tools/layoutlib/bridge/tests/com/android/layoutlib/bridge/BridgeXmlBlockParserTest.java @@ -0,0 +1,150 @@ +/* + * 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 com.android.layoutlib.bridge; + +import org.kxml2.io.KXmlParser; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.xml.sax.SAXException; +import org.xmlpull.v1.XmlPullParser; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.net.URL; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import junit.framework.TestCase; + +public class BridgeXmlBlockParserTest extends TestCase { + + private String mXmlPath; + private Document mDoc; + + @Override + protected void setUp() throws Exception { + super.setUp(); + URL url = this.getClass().getClassLoader().getResource("data/layout1.xml"); + mXmlPath = url.getFile(); + mDoc = getXmlDocument(mXmlPath); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + } + + public void testXmlBlockParser() throws Exception { + XmlPullParser parser = new KXmlParser(); + parser = new BridgeXmlBlockParser(parser, null, false /* platformResourceFlag */); + parser.setInput(new FileReader(new File(mXmlPath))); + + assertEquals(XmlPullParser.START_DOCUMENT, parser.next()); + + assertEquals(XmlPullParser.START_TAG, parser.next()); + assertEquals("LinearLayout", parser.getName()); + + assertEquals(XmlPullParser.TEXT, parser.next()); + + assertEquals(XmlPullParser.START_TAG, parser.next()); + assertEquals("Button", parser.getName()); + assertEquals(XmlPullParser.TEXT, parser.next()); + assertEquals(XmlPullParser.END_TAG, parser.next()); + + assertEquals(XmlPullParser.TEXT, parser.next()); + + assertEquals(XmlPullParser.START_TAG, parser.next()); + assertEquals("View", parser.getName()); + assertEquals(XmlPullParser.END_TAG, parser.next()); + + assertEquals(XmlPullParser.TEXT, parser.next()); + + assertEquals(XmlPullParser.START_TAG, parser.next()); + assertEquals("TextView", parser.getName()); + assertEquals(XmlPullParser.END_TAG, parser.next()); + + assertEquals(XmlPullParser.TEXT, parser.next()); + + assertEquals(XmlPullParser.END_TAG, parser.next()); + assertEquals(XmlPullParser.END_DOCUMENT, parser.next()); + } + + //------------ + + private Document getXmlDocument(String xmlFilePath) + throws ParserConfigurationException, SAXException, IOException { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + + // keep comments + factory.setIgnoringComments(false); + // don't validate our bogus DTD + factory.setValidating(false); + // we want namespaces + factory.setNamespaceAware(true); + + DocumentBuilder builder = factory.newDocumentBuilder(); + return builder.parse(new File(xmlFilePath)); + } + + + /** + * Quick'n'dirty debug helper that dumps an XML structure to stdout. + */ + @SuppressWarnings("unused") + private void dump(Node node, String prefix) { + Node n; + + String[] types = { + "unknown", + "ELEMENT_NODE", + "ATTRIBUTE_NODE", + "TEXT_NODE", + "CDATA_SECTION_NODE", + "ENTITY_REFERENCE_NODE", + "ENTITY_NODE", + "PROCESSING_INSTRUCTION_NODE", + "COMMENT_NODE", + "DOCUMENT_NODE", + "DOCUMENT_TYPE_NODE", + "DOCUMENT_FRAGMENT_NODE", + "NOTATION_NODE" + }; + + String s = String.format("%s<%s> %s %s", + prefix, + types[node.getNodeType()], + node.getNodeName(), + node.getNodeValue() == null ? "" : node.getNodeValue().trim()); + + System.out.println(s); + + n = node.getFirstChild(); + if (n != null) { + dump(n, prefix + "- "); + } + + n = node.getNextSibling(); + if (n != null) { + dump(n, prefix); + } + + } + +} diff --git a/tools/layoutlib/bridge/tests/com/android/layoutlib/bridge/NinePatchTest.java b/tools/layoutlib/bridge/tests/com/android/layoutlib/bridge/NinePatchTest.java new file mode 100644 index 0000000..67ec5e1 --- /dev/null +++ b/tools/layoutlib/bridge/tests/com/android/layoutlib/bridge/NinePatchTest.java @@ -0,0 +1,35 @@ +package com.android.layoutlib.bridge; + +import com.android.ninepatch.NinePatch; + +import java.net.URL; + +import junit.framework.TestCase; + +public class NinePatchTest extends TestCase { + + private NinePatch mPatch; + + @Override + protected void setUp() throws Exception { + URL url = this.getClass().getClassLoader().getResource("data/button.9.png"); + + mPatch = NinePatch.load(url, false /* convert */); + } + + public void test9PatchLoad() throws Exception { + assertNotNull(mPatch); + } + + public void test9PatchMinSize() { + int[] padding = new int[4]; + mPatch.getPadding(padding); + assertEquals(13, padding[0]); + assertEquals(3, padding[1]); + assertEquals(13, padding[2]); + assertEquals(4, padding[3]); + assertEquals(38, mPatch.getWidth()); + assertEquals(27, mPatch.getHeight()); + } + +} diff --git a/tools/layoutlib/bridge/tests/com/android/layoutlib/bridge/StyleResourceValue.java b/tools/layoutlib/bridge/tests/com/android/layoutlib/bridge/StyleResourceValue.java new file mode 100644 index 0000000..84bdc2f --- /dev/null +++ b/tools/layoutlib/bridge/tests/com/android/layoutlib/bridge/StyleResourceValue.java @@ -0,0 +1,60 @@ +/* + * 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 com.android.layoutlib.bridge; + +import com.android.layoutlib.api.IResourceValue; +import com.android.layoutlib.api.IStyleResourceValue; + +import java.util.HashMap; + +class StyleResourceValue extends ResourceValue implements IStyleResourceValue { + + private String mParentStyle = null; + private HashMap<String, IResourceValue> mItems = new HashMap<String, IResourceValue>(); + + StyleResourceValue(String name) { + super(name); + } + + StyleResourceValue(String name, String parentStyle) { + super(name); + mParentStyle = parentStyle; + } + + public String getParentStyle() { + return mParentStyle; + } + + public IResourceValue findItem(String name) { + return mItems.get(name); + } + + public void addItem(IResourceValue value) { + mItems.put(value.getName(), value); + } + + @Override + public void replaceWith(ResourceValue value) { + super.replaceWith(value); + + if (value instanceof StyleResourceValue) { + mItems.clear(); + mItems.putAll(((StyleResourceValue)value).mItems); + } + } + +} diff --git a/tools/layoutlib/bridge/tests/data/button.9.png b/tools/layoutlib/bridge/tests/data/button.9.png Binary files differnew file mode 100644 index 0000000..9d52f40 --- /dev/null +++ b/tools/layoutlib/bridge/tests/data/button.9.png diff --git a/tools/layoutlib/bridge/tests/data/layout1.xml b/tools/layoutlib/bridge/tests/data/layout1.xml new file mode 100644 index 0000000..554f541 --- /dev/null +++ b/tools/layoutlib/bridge/tests/data/layout1.xml @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- + 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. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:orientation="vertical" +> + <Button + android:id="@+id/bouton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="My Button Text" + > + </Button> + <View + android:id="@+id/surface" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:layout_weight="2" + /> + <TextView + android:id="@+id/status" + android:paddingLeft="2dip" + android:layout_weight="0" + android:background="@drawable/black" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:lines="1" + android:gravity="center_vertical|center_horizontal" + android:text="My TextView Text" + /> +</LinearLayout> diff --git a/tools/layoutlib/create/.classpath b/tools/layoutlib/create/.classpath new file mode 100644 index 0000000..0c60f6a --- /dev/null +++ b/tools/layoutlib/create/.classpath @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> +<classpath> + <classpathentry kind="src" path="src"/> + <classpathentry excluding="mock_android/" kind="src" path="tests"/> + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/> + <classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/4"/> + <classpathentry kind="var" path="ANDROID_SRC/prebuilt/common/asm/asm-3.1.jar"/> + <classpathentry kind="output" path="bin"/> +</classpath> diff --git a/tools/layoutlib/create/.project b/tools/layoutlib/create/.project new file mode 100644 index 0000000..e100d17 --- /dev/null +++ b/tools/layoutlib/create/.project @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>layoutlib_create</name> + <comment></comment> + <projects> + </projects> + <buildSpec> + <buildCommand> + <name>org.eclipse.jdt.core.javabuilder</name> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>org.eclipse.jdt.core.javanature</nature> + </natures> +</projectDescription> diff --git a/tools/layoutlib/create/Android.mk b/tools/layoutlib/create/Android.mk new file mode 100644 index 0000000..310fae5 --- /dev/null +++ b/tools/layoutlib/create/Android.mk @@ -0,0 +1,28 @@ +# +# 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. +# +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-java-files-under,src) + +LOCAL_JAR_MANIFEST := manifest.txt +LOCAL_STATIC_JAVA_LIBRARIES := \ + asm-3.1 + +LOCAL_MODULE := layoutlib_create + +include $(BUILD_HOST_JAVA_LIBRARY) + diff --git a/tools/layoutlib/create/README.txt b/tools/layoutlib/create/README.txt new file mode 100644 index 0000000..09b392b --- /dev/null +++ b/tools/layoutlib/create/README.txt @@ -0,0 +1,71 @@ +# Copyright (C) 2008 The Android Open Source Project + + +- Description - +--------------- + +makeLayoutLib generates a library used by the Eclipse graphical layout editor +to perform layout. + + + +- Usage - +--------- + + ./makeLayoutLib path/to/android.jar destination.jar + + + +- Implementation Notes - +------------------------ + +The goal of makeLayoutLib is to list all the classes from the input jar and create a new +jar that only keeps certain classes and create stubs for all their dependencies. + +First the input jar is parsed to find all the classes defined. + +In the Main(), the following list of classes are hardcoded (TODO config file later): +- keep all classes that derive from android.view.View. +- keep all classes in the android.view and android.widget packages (sub-packages excluded). +- keep specific classes such as android.policy.PhoneLayoutInflater. + +For each class to keep, their dependencies are examined using BCEL. +A dependency is defined as a class needed to instantiate the given class that should be kept, +directly or indirectly. So a dependency is a class that is used by the input class, that is +defined in the input jar and that is not part of the current JRE. + +Dependencies are computed recursively. + +Once all dependencies are found, the final jar can be created. +There are three kind of classes to write: +- classes that are to be kept as-is. They are just dumped in the new jar unchanged. +- classes that are to be kept yet contain native methods or fields. +- classes that are just dependencies. We don't want to expose their implementation in the final + jar. + +The implementation of native methods and all methods of mock classes is replaced by a stub +that throws UnsupportedOperationException. + +Incidentally, the access level of native and mock classes needs to be changed in order for +native methods to be later overridden. Methods that are "final private native" must become +non-final, non-native and at most protected. Package-default access is changed to public. +Classes that are final are made non-final. Abstract methods are left untouched. + + + +---- +20080617 Replace Class + +Some classes are basically wrappers over native objects. +Subclassing doesn't work as most methods are either static or we don't +control object creation. In this scenario the idea is to be able to +replace classes in the final jar. + +Example: android.graphics.Paint would get renamed to OriginalPaint +in the generated jar. Then in the bridge we'll introduce a replacement +Paint class that derives from OriginalPaint. + +This won't rename/replace the inner static methods of a given class. + + + diff --git a/tools/layoutlib/create/manifest.txt b/tools/layoutlib/create/manifest.txt new file mode 100644 index 0000000..238e7f9 --- /dev/null +++ b/tools/layoutlib/create/manifest.txt @@ -0,0 +1 @@ +Main-Class: com.android.tools.layoutlib.create.Main diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmAnalyzer.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmAnalyzer.java new file mode 100644 index 0000000..b197ea7 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmAnalyzer.java @@ -0,0 +1,751 @@ +/* + * 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 com.android.tools.layoutlib.create; + +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.Attribute; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Type; +import org.objectweb.asm.signature.SignatureReader; +import org.objectweb.asm.signature.SignatureVisitor; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.Map.Entry; +import java.util.regex.Pattern; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +/** + * Analyzes the input JAR using the ASM java bytecode manipulation library + * to list the desired classes and their dependencies. + */ +public class AsmAnalyzer { + + // Note: a bunch of stuff has package-level access for unit tests. Consider it private. + + /** Output logger. */ + private final Log mLog; + /** The input source JAR to parse. */ + private final List<String> mOsSourceJar; + /** The generator to fill with the class list and dependency list. */ + private final AsmGenerator mGen; + /** Keep all classes that derive from these one (these included). */ + private final String[] mDeriveFrom; + /** Glob patterns of classes to keep, e.g. "com.foo.*" */ + private final String[] mIncludeGlobs; + + /** + * Creates a new analyzer. + * + * @param log The log output. + * @param osJarPath The input source JARs to parse. + * @param gen The generator to fill with the class list and dependency list. + * @param deriveFrom Keep all classes that derive from these one (these included). + * @param includeGlobs Glob patterns of classes to keep, e.g. "com.foo.*" + * ("*" does not matches dots whilst "**" does, "." and "$" are interpreted as-is) + */ + public AsmAnalyzer(Log log, List<String> osJarPath, AsmGenerator gen, + String[] deriveFrom, String[] includeGlobs) { + mLog = log; + mGen = gen; + mOsSourceJar = osJarPath != null ? osJarPath : new ArrayList<String>(); + mDeriveFrom = deriveFrom != null ? deriveFrom : new String[0]; + mIncludeGlobs = includeGlobs != null ? includeGlobs : new String[0]; + } + + /** + * Starts the analysis using parameters from the constructor. + * Fills the generator with classes & dependencies found. + */ + public void analyze() throws IOException, LogAbortException { + + AsmAnalyzer visitor = this; + + Map<String, ClassReader> zipClasses = parseZip(mOsSourceJar); + mLog.info("Found %d classes in input JAR%s.", zipClasses.size(), + mOsSourceJar.size() > 1 ? "s" : ""); + + Map<String, ClassReader> found = findIncludes(zipClasses); + Map<String, ClassReader> deps = findDeps(zipClasses, found); + + if (mGen != null) { + mGen.setKeep(found); + mGen.setDeps(deps); + } + } + + /** + * Parses a JAR file and returns a list of all classes founds using a map + * class name => ASM ClassReader. Class names are in the form "android.view.View". + */ + Map<String,ClassReader> parseZip(List<String> jarPathList) throws IOException { + TreeMap<String, ClassReader> classes = new TreeMap<String, ClassReader>(); + + for (String jarPath : jarPathList) { + ZipFile zip = new ZipFile(jarPath); + Enumeration<? extends ZipEntry> entries = zip.entries(); + ZipEntry entry; + while (entries.hasMoreElements()) { + entry = entries.nextElement(); + if (entry.getName().endsWith(".class")) { + ClassReader cr = new ClassReader(zip.getInputStream(entry)); + String className = classReaderToClassName(cr); + classes.put(className, cr); + } + } + } + + return classes; + } + + /** + * Utility that returns the fully qualified binary class name for a ClassReader. + * E.g. it returns something like android.view.View. + */ + static String classReaderToClassName(ClassReader classReader) { + if (classReader == null) { + return null; + } else { + return classReader.getClassName().replace('/', '.'); + } + } + + /** + * Utility that returns the fully qualified binary class name from a path-like FQCN. + * E.g. it returns android.view.View from android/view/View. + */ + static String internalToBinaryClassName(String className) { + if (className == null) { + return null; + } else { + return className.replace('/', '.'); + } + } + + /** + * Process the "includes" arrays. + * <p/> + * This updates the in_out_found map. + */ + Map<String, ClassReader> findIncludes(Map<String, ClassReader> zipClasses) + throws LogAbortException { + TreeMap<String, ClassReader> found = new TreeMap<String, ClassReader>(); + + mLog.debug("Find classes to include."); + + for (String s : mIncludeGlobs) { + findGlobs(s, zipClasses, found); + } + for (String s : mDeriveFrom) { + findClassesDerivingFrom(s, zipClasses, found); + } + + return found; + } + + + /** + * Uses ASM to find the class reader for the given FQCN class name. + * If found, insert it in the in_out_found map. + * Returns the class reader object. + */ + ClassReader findClass(String className, Map<String, ClassReader> zipClasses, + Map<String, ClassReader> inOutFound) throws LogAbortException { + ClassReader classReader = zipClasses.get(className); + if (classReader == null) { + throw new LogAbortException("Class %s not found by ASM in %s", + className, mOsSourceJar); + } + + inOutFound.put(className, classReader); + return classReader; + } + + /** + * Insert in the inOutFound map all classes found in zipClasses that match the + * given glob pattern. + * <p/> + * The glob pattern is not a regexp. It only accepts the "*" keyword to mean + * "anything but a period". The "." and "$" characters match themselves. + * The "**" keyword means everything including ".". + * <p/> + * Examples: + * <ul> + * <li>com.foo.* matches all classes in the package com.foo but NOT sub-packages. + * <li>com.foo*.*$Event matches all internal Event classes in a com.foo*.* class. + * </ul> + */ + void findGlobs(String globPattern, Map<String, ClassReader> zipClasses, + Map<String, ClassReader> inOutFound) throws LogAbortException { + // transforms the glob pattern in a regexp: + // - escape "." with "\." + // - replace "*" by "[^.]*" + // - escape "$" with "\$" + // - add end-of-line match $ + globPattern = globPattern.replaceAll("\\$", "\\\\\\$"); + globPattern = globPattern.replaceAll("\\.", "\\\\."); + // prevent ** from being altered by the next rule, then process the * rule and finally + // the real ** rule (which is now @) + globPattern = globPattern.replaceAll("\\*\\*", "@"); + globPattern = globPattern.replaceAll("\\*", "[^.]*"); + globPattern = globPattern.replaceAll("@", ".*"); + globPattern += "$"; + + Pattern regexp = Pattern.compile(globPattern); + + for (Entry<String, ClassReader> entry : zipClasses.entrySet()) { + String class_name = entry.getKey(); + if (regexp.matcher(class_name).matches()) { + findClass(class_name, zipClasses, inOutFound); + } + } + } + + /** + * Checks all the classes defined in the JarClassName instance and uses BCEL to + * determine if they are derived from the given FQCN super class name. + * Inserts the super class and all the class objects found in the map. + */ + void findClassesDerivingFrom(String super_name, Map<String, ClassReader> zipClasses, + Map<String, ClassReader> inOutFound) throws LogAbortException { + ClassReader super_clazz = findClass(super_name, zipClasses, inOutFound); + + for (Entry<String, ClassReader> entry : zipClasses.entrySet()) { + String className = entry.getKey(); + if (super_name.equals(className)) { + continue; + } + ClassReader classReader = entry.getValue(); + ClassReader parent_cr = classReader; + while (parent_cr != null) { + String parent_name = internalToBinaryClassName(parent_cr.getSuperName()); + if (parent_name == null) { + // not found + break; + } else if (super_name.equals(parent_name)) { + inOutFound.put(className, classReader); + break; + } + parent_cr = zipClasses.get(parent_name); + } + } + } + + /** + * Instantiates a new DependencyVisitor. Useful for unit tests. + */ + DependencyVisitor getVisitor(Map<String, ClassReader> zipClasses, + Map<String, ClassReader> inKeep, + Map<String, ClassReader> outKeep, + Map<String, ClassReader> inDeps, + Map<String, ClassReader> outDeps) { + return new DependencyVisitor(zipClasses, inKeep, outKeep, inDeps, outDeps); + } + + /** + * Finds all dependencies for all classes in keepClasses which are also + * listed in zipClasses. Returns a map of all the dependencies found. + */ + Map<String, ClassReader> findDeps(Map<String, ClassReader> zipClasses, + Map<String, ClassReader> inOutKeepClasses) { + + TreeMap<String, ClassReader> deps = new TreeMap<String, ClassReader>(); + TreeMap<String, ClassReader> new_deps = new TreeMap<String, ClassReader>(); + TreeMap<String, ClassReader> new_keep = new TreeMap<String, ClassReader>(); + TreeMap<String, ClassReader> temp = new TreeMap<String, ClassReader>(); + + DependencyVisitor visitor = getVisitor(zipClasses, + inOutKeepClasses, new_keep, + deps, new_deps); + + for (ClassReader cr : inOutKeepClasses.values()) { + cr.accept(visitor, 0 /* flags */); + } + + while (new_deps.size() > 0 || new_keep.size() > 0) { + deps.putAll(new_deps); + inOutKeepClasses.putAll(new_keep); + + temp.clear(); + temp.putAll(new_deps); + temp.putAll(new_keep); + new_deps.clear(); + new_keep.clear(); + mLog.debug("Found %1$d to keep, %2$d dependencies.", + inOutKeepClasses.size(), deps.size()); + + for (ClassReader cr : temp.values()) { + cr.accept(visitor, 0 /* flags */); + } + } + + mLog.info("Found %1$d classes to keep, %2$d class dependencies.", + inOutKeepClasses.size(), deps.size()); + + return deps; + } + + + + // ---------------------------------- + + /** + * Visitor to collect all the type dependencies from a class. + */ + public class DependencyVisitor + implements ClassVisitor, FieldVisitor, MethodVisitor, SignatureVisitor, AnnotationVisitor { + + /** All classes found in the source JAR. */ + private final Map<String, ClassReader> mZipClasses; + /** Classes from which dependencies are to be found. */ + private final Map<String, ClassReader> mInKeep; + /** Dependencies already known. */ + private final Map<String, ClassReader> mInDeps; + /** New dependencies found by this visitor. */ + private final Map<String, ClassReader> mOutDeps; + /** New classes to keep as-is found by this visitor. */ + private final Map<String, ClassReader> mOutKeep; + + /** + * Creates a new visitor that will find all the dependencies for the visited class. + * Types which are already in the zipClasses, keepClasses or inDeps are not marked. + * New dependencies are marked in outDeps. + * + * @param zipClasses All classes found in the source JAR. + * @param inKeep Classes from which dependencies are to be found. + * @param inDeps Dependencies already known. + * @param outDeps New dependencies found by this visitor. + */ + public DependencyVisitor(Map<String, ClassReader> zipClasses, + Map<String, ClassReader> inKeep, + Map<String, ClassReader> outKeep, + Map<String,ClassReader> inDeps, + Map<String,ClassReader> outDeps) { + mZipClasses = zipClasses; + mInKeep = inKeep; + mOutKeep = outKeep; + mInDeps = inDeps; + mOutDeps = outDeps; + } + + /** + * Considers the given class name as a dependency. + * If it does, add to the mOutDeps map. + */ + public void considerName(String className) { + if (className == null) { + return; + } + + className = internalToBinaryClassName(className); + + // exclude classes that have already been found + if (mInKeep.containsKey(className) || + mOutKeep.containsKey(className) || + mInDeps.containsKey(className) || + mOutDeps.containsKey(className)) { + return; + } + + // exclude classes that are not part of the JAR file being examined + ClassReader cr = mZipClasses.get(className); + if (cr == null) { + return; + } + + try { + // exclude classes that are part of the default JRE (the one executing this program) + if (getClass().getClassLoader().loadClass(className) != null) { + return; + } + } catch (ClassNotFoundException e) { + // ignore + } + + // accept this class: + // - android classes are added to dependencies + // - non-android classes are added to the list of classes to keep as-is (they don't need + // to be stubbed). + if (className.indexOf("android") >= 0) { // TODO make configurable + mOutDeps.put(className, cr); + } else { + mOutKeep.put(className, cr); + } + } + + /** + * Considers this array of names using considerName(). + */ + public void considerNames(String[] classNames) { + if (classNames != null) { + for (String className : classNames) { + considerName(className); + } + } + } + + /** + * Considers this signature or type signature by invoking the {@link SignatureVisitor} + * on it. + */ + public void considerSignature(String signature) { + if (signature != null) { + SignatureReader sr = new SignatureReader(signature); + // SignatureReader.accept will call accessType so we don't really have + // to differentiate where the signature comes from. + sr.accept(this); + } + } + + /** + * Considers this {@link Type}. For arrays, the element type is considered. + * If the type is an object, it's internal name is considered. + */ + public void considerType(Type t) { + if (t != null) { + if (t.getSort() == Type.ARRAY) { + t = t.getElementType(); + } + if (t.getSort() == Type.OBJECT) { + considerName(t.getInternalName()); + } + } + } + + /** + * Considers a descriptor string. The descriptor is converted to a {@link Type} + * and then considerType() is invoked. + */ + public void considerDesc(String desc) { + if (desc != null) { + try { + Type t = Type.getType(desc); + considerType(t); + } catch (ArrayIndexOutOfBoundsException e) { + // ignore, not a valid type. + } + } + } + + + // --------------------------------------------------- + // --- ClassVisitor, FieldVisitor + // --------------------------------------------------- + + // Visits a class header + public void visit(int version, int access, String name, + String signature, String superName, String[] interfaces) { + // signature is the signature of this class. May be null if the class is not a generic + // one, and does not extend or implement generic classes or interfaces. + + if (signature != null) { + considerSignature(signature); + } + + // superName is the internal of name of the super class (see getInternalName). + // For interfaces, the super class is Object. May be null but only for the Object class. + considerName(superName); + + // interfaces is the internal names of the class's interfaces (see getInternalName). + // May be null. + considerNames(interfaces); + } + + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + // desc is the class descriptor of the annotation class. + considerDesc(desc); + return this; // return this to visit annotion values + } + + public void visitAttribute(Attribute attr) { + // pass + } + + // Visits the end of a class + public void visitEnd() { + // pass + } + + public FieldVisitor visitField(int access, String name, String desc, + String signature, Object value) { + // desc is the field's descriptor (see Type). + considerDesc(desc); + + // signature is the field's signature. May be null if the field's type does not use + // generic types. + considerSignature(signature); + + return this; // a visitor to visit field annotations and attributes + } + + public void visitInnerClass(String name, String outerName, String innerName, int access) { + // name is the internal name of an inner class (see getInternalName). + considerName(name); + } + + public MethodVisitor visitMethod(int access, String name, String desc, + String signature, String[] exceptions) { + // desc is the method's descriptor (see Type). + considerDesc(desc); + // signature is the method's signature. May be null if the method parameters, return + // type and exceptions do not use generic types. + considerSignature(signature); + + return this; // returns this to visit the method + } + + public void visitOuterClass(String owner, String name, String desc) { + // pass + } + + public void visitSource(String source, String debug) { + // pass + } + + + // --------------------------------------------------- + // --- MethodVisitor + // --------------------------------------------------- + + public AnnotationVisitor visitAnnotationDefault() { + return this; // returns this to visit the default value + } + + + public void visitCode() { + // pass + } + + // field instruction + public void visitFieldInsn(int opcode, String owner, String name, String desc) { + // name is the field's name. + considerName(name); + // desc is the field's descriptor (see Type). + considerDesc(desc); + } + + public void visitFrame(int type, int local, Object[] local2, int stack, Object[] stack2) { + // pass + } + + public void visitIincInsn(int var, int increment) { + // pass -- an IINC instruction + } + + public void visitInsn(int opcode) { + // pass -- a zero operand instruction + } + + public void visitIntInsn(int opcode, int operand) { + // pass -- a single int operand instruction + } + + public void visitJumpInsn(int opcode, Label label) { + // pass -- a jump instruction + } + + public void visitLabel(Label label) { + // pass -- a label target + } + + // instruction to load a constant from the stack + public void visitLdcInsn(Object cst) { + if (cst instanceof Type) { + considerType((Type) cst); + } + } + + public void visitLineNumber(int line, Label start) { + // pass + } + + public void visitLocalVariable(String name, String desc, + String signature, Label start, Label end, int index) { + // desc is the type descriptor of this local variable. + considerDesc(desc); + // signature is the type signature of this local variable. May be null if the local + // variable type does not use generic types. + considerSignature(signature); + } + + public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) { + // pass -- a lookup switch instruction + } + + public void visitMaxs(int maxStack, int maxLocals) { + // pass + } + + // instruction that invokes a method + public void visitMethodInsn(int opcode, String owner, String name, String desc) { + + // owner is the internal name of the method's owner class + considerName(owner); + // desc is the method's descriptor (see Type). + considerDesc(desc); + } + + // instruction multianewarray, whatever that is + public void visitMultiANewArrayInsn(String desc, int dims) { + + // desc an array type descriptor. + considerDesc(desc); + } + + public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, + boolean visible) { + // desc is the class descriptor of the annotation class. + considerDesc(desc); + return this; // return this to visit annotation values + } + + public void visitTableSwitchInsn(int min, int max, Label dflt, Label[] labels) { + // pass -- table switch instruction + + } + + public void visitTryCatchBlock(Label start, Label end, Label handler, String type) { + // type is the internal name of the type of exceptions handled by the handler, + // or null to catch any exceptions (for "finally" blocks). + considerName(type); + } + + // type instruction + public void visitTypeInsn(int opcode, String type) { + // type is the operand of the instruction to be visited. This operand must be the + // internal name of an object or array class. + considerName(type); + } + + public void visitVarInsn(int opcode, int var) { + // pass -- local variable instruction + } + + + // --------------------------------------------------- + // --- SignatureVisitor + // --------------------------------------------------- + + private String mCurrentSignatureClass = null; + + // Starts the visit of a signature corresponding to a class or interface type + public void visitClassType(String name) { + mCurrentSignatureClass = name; + considerName(name); + } + + // Visits an inner class + public void visitInnerClassType(String name) { + if (mCurrentSignatureClass != null) { + mCurrentSignatureClass += "$" + name; + considerName(mCurrentSignatureClass); + } + } + + public SignatureVisitor visitArrayType() { + return this; // returns this to visit the signature of the array element type + } + + public void visitBaseType(char descriptor) { + // pass -- a primitive type, ignored + } + + public SignatureVisitor visitClassBound() { + return this; // returns this to visit the signature of the class bound + } + + public SignatureVisitor visitExceptionType() { + return this; // return this to visit the signature of the exception type. + } + + public void visitFormalTypeParameter(String name) { + // pass + } + + public SignatureVisitor visitInterface() { + return this; // returns this to visit the signature of the interface type + } + + public SignatureVisitor visitInterfaceBound() { + return this; // returns this to visit the signature of the interface bound + } + + public SignatureVisitor visitParameterType() { + return this; // returns this to visit the signature of the parameter type + } + + public SignatureVisitor visitReturnType() { + return this; // returns this to visit the signature of the return type + } + + public SignatureVisitor visitSuperclass() { + return this; // returns this to visit the signature of the super class type + } + + public SignatureVisitor visitTypeArgument(char wildcard) { + return this; // returns this to visit the signature of the type argument + } + + public void visitTypeVariable(String name) { + // pass + } + + public void visitTypeArgument() { + // pass + } + + + // --------------------------------------------------- + // --- AnnotationVisitor + // --------------------------------------------------- + + + // Visits a primitive value of an annotation + public void visit(String name, Object value) { + // value is the actual value, whose type must be Byte, Boolean, Character, Short, + // Integer, Long, Float, Double, String or Type + if (value instanceof Type) { + considerType((Type) value); + } + } + + public AnnotationVisitor visitAnnotation(String name, String desc) { + // desc is the class descriptor of the nested annotation class. + considerDesc(desc); + return this; // returns this to visit the actual nested annotation value + } + + public AnnotationVisitor visitArray(String name) { + return this; // returns this to visit the actual array value elements + } + + public void visitEnum(String name, String desc, String value) { + // desc is the class descriptor of the enumeration class. + considerDesc(desc); + } + + } +} diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmGenerator.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmGenerator.java new file mode 100644 index 0000000..1adcc17 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmGenerator.java @@ -0,0 +1,338 @@ +/* + * 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 com.android.tools.layoutlib.create; + +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; + +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.Map.Entry; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; + +/** + * Class that generates a new JAR from a list of classes, some of which are to be kept as-is + * and some of which are to be stubbed partially or totally. + */ +public class AsmGenerator { + + /** Output logger. */ + private final Log mLog; + /** The path of the destination JAR to create. */ + private final String mOsDestJar; + /** List of classes to inject in the final JAR from _this_ archive. */ + private final Class<?>[] mInjectClasses; + /** The set of methods to stub out. */ + private final Set<String> mStubMethods; + /** All classes to output as-is, except if they have native methods. */ + private Map<String, ClassReader> mKeep; + /** All dependencies that must be completely stubbed. */ + private Map<String, ClassReader> mDeps; + /** Counter of number of classes renamed during transform. */ + private int mRenameCount; + /** FQCN Names of the classes to rename: map old-FQCN => new-FQCN */ + private final HashMap<String, String> mRenameClasses; + /** FQCN Names of "old" classes that were NOT renamed. This starts with the full list of + * old-FQCN to rename and they get erased as they get renamed. At the end, classes still + * left here are not in the code base anymore and thus were not renamed. */ + private HashSet<String> mClassesNotRenamed; + /** A map { FQCN => map { list of return types to delete from the FQCN } }. */ + private HashMap<String, Set<String>> mDeleteReturns; + + /** + * Creates a new generator that can generate the output JAR with the stubbed classes. + * + * @param log Output logger. + * @param osDestJar The path of the destination JAR to create. + * @param stubMethods The list of methods to stub out. Each entry must be in the form + * "package.package.OuterClass$InnerClass#MethodName". + * @param renameClasses The list of classes to rename, must be an even list: the binary FQCN + * of class to replace followed by the new FQCN. + * @param deleteReturns List of classes for which the methods returning them should be deleted. + * The array contains a list of null terminated section starting with the name of the class + * to rename in which the methods are deleted, followed by a list of return types identifying + * the methods to delete. + */ + public AsmGenerator(Log log, String osDestJar, + Class<?>[] injectClasses, + String[] stubMethods, + String[] renameClasses, String[] deleteReturns) { + mLog = log; + mOsDestJar = osDestJar; + mInjectClasses = injectClasses != null ? injectClasses : new Class<?>[0]; + mStubMethods = stubMethods != null ? new HashSet<String>(Arrays.asList(stubMethods)) : + new HashSet<String>(); + + // Create the map of classes to rename. + mRenameClasses = new HashMap<String, String>(); + mClassesNotRenamed = new HashSet<String>(); + int n = renameClasses == null ? 0 : renameClasses.length; + for (int i = 0; i < n; i += 2) { + assert i + 1 < n; + // The ASM class names uses "/" separators, whereas regular FQCN use "." + String oldFqcn = binaryToInternalClassName(renameClasses[i]); + String newFqcn = binaryToInternalClassName(renameClasses[i + 1]); + mRenameClasses.put(oldFqcn, newFqcn); + mClassesNotRenamed.add(oldFqcn); + } + + // create the map of renamed class -> return type of method to delete. + mDeleteReturns = new HashMap<String, Set<String>>(); + if (deleteReturns != null) { + Set<String> returnTypes = null; + String renamedClass = null; + for (String className : deleteReturns) { + // if we reach the end of a section, add it to the main map + if (className == null) { + if (returnTypes != null) { + mDeleteReturns.put(renamedClass, returnTypes); + } + + renamedClass = null; + continue; + } + + // if the renamed class is null, this is the beginning of a section + if (renamedClass == null) { + renamedClass = binaryToInternalClassName(className); + continue; + } + + // just a standard return type, we add it to the list. + if (returnTypes == null) { + returnTypes = new HashSet<String>(); + } + returnTypes.add(binaryToInternalClassName(className)); + } + } + } + + /** + * Returns the list of classes that have not been renamed yet. + * <p/> + * The names are "internal class names" rather than FQCN, i.e. they use "/" instead "." + * as package separators. + */ + public Set<String> getClassesNotRenamed() { + return mClassesNotRenamed; + } + + /** + * Utility that returns the internal ASM class name from a fully qualified binary class + * name. E.g. it returns android/view/View from android.view.View. + */ + String binaryToInternalClassName(String className) { + if (className == null) { + return null; + } else { + return className.replace('.', '/'); + } + } + + /** Sets the map of classes to output as-is, except if they have native methods */ + public void setKeep(Map<String, ClassReader> keep) { + mKeep = keep; + } + + /** Sets the map of dependencies that must be completely stubbed */ + public void setDeps(Map<String, ClassReader> deps) { + mDeps = deps; + } + + /** Gets the map of classes to output as-is, except if they have native methods */ + public Map<String, ClassReader> getKeep() { + return mKeep; + } + + /** Gets the map of dependencies that must be completely stubbed */ + public Map<String, ClassReader> getDeps() { + return mDeps; + } + + /** Generates the final JAR */ + public void generate() throws FileNotFoundException, IOException { + TreeMap<String, byte[]> all = new TreeMap<String, byte[]>(); + + for (Class<?> clazz : mInjectClasses) { + String name = classToEntryPath(clazz); + InputStream is = ClassLoader.getSystemResourceAsStream(name); + ClassReader cr = new ClassReader(is); + byte[] b = transform(cr, true /* stubNativesOnly */); + name = classNameToEntryPath(transformName(cr.getClassName())); + all.put(name, b); + } + + for (Entry<String, ClassReader> entry : mDeps.entrySet()) { + ClassReader cr = entry.getValue(); + byte[] b = transform(cr, true /* stubNativesOnly */); + String name = classNameToEntryPath(transformName(cr.getClassName())); + all.put(name, b); + } + + for (Entry<String, ClassReader> entry : mKeep.entrySet()) { + ClassReader cr = entry.getValue(); + byte[] b = transform(cr, true /* stubNativesOnly */); + String name = classNameToEntryPath(transformName(cr.getClassName())); + all.put(name, b); + } + + mLog.info("# deps classes: %d", mDeps.size()); + mLog.info("# keep classes: %d", mKeep.size()); + mLog.info("# renamed : %d", mRenameCount); + + createJar(new FileOutputStream(mOsDestJar), all); + mLog.info("Created JAR file %s", mOsDestJar); + } + + /** + * Writes the JAR file. + * + * @param outStream The file output stream were to write the JAR. + * @param all The map of all classes to output. + * @throws IOException if an I/O error has occurred + */ + void createJar(FileOutputStream outStream, Map<String,byte[]> all) throws IOException { + JarOutputStream jar = new JarOutputStream(outStream); + for (Entry<String, byte[]> entry : all.entrySet()) { + String name = entry.getKey(); + JarEntry jar_entry = new JarEntry(name); + jar.putNextEntry(jar_entry); + jar.write(entry.getValue()); + jar.closeEntry(); + } + jar.flush(); + jar.close(); + } + + /** + * Utility method that converts a fully qualified java name into a JAR entry path + * e.g. for the input "android.view.View" it returns "android/view/View.class" + */ + String classNameToEntryPath(String className) { + return className.replaceAll("\\.", "/").concat(".class"); + } + + /** + * Utility method to get the JAR entry path from a Class name. + * e.g. it returns someting like "com/foo/OuterClass$InnerClass1$InnerClass2.class" + */ + private String classToEntryPath(Class<?> clazz) { + String name = ""; + Class<?> parent; + while ((parent = clazz.getEnclosingClass()) != null) { + name = "$" + clazz.getSimpleName() + name; + clazz = parent; + } + return classNameToEntryPath(clazz.getCanonicalName() + name); + } + + /** + * Transforms a class. + * <p/> + * There are 3 kind of transformations: + * + * 1- For "mock" dependencies classes, we want to remove all code from methods and replace + * by a stub. Native methods must be implemented with this stub too. Abstract methods are + * left intact. Modified classes must be overridable (non-private, non-final). + * Native methods must be made non-final, non-private. + * + * 2- For "keep" classes, we want to rewrite all native methods as indicated above. + * If a class has native methods, it must also be made non-private, non-final. + * + * Note that unfortunately static methods cannot be changed to non-static (since static and + * non-static are invoked differently.) + */ + byte[] transform(ClassReader cr, boolean stubNativesOnly) { + + boolean hasNativeMethods = hasNativeMethods(cr); + String className = cr.getClassName(); + + String newName = transformName(className); + // transformName returns its input argument if there's no need to rename the class + if (newName != className) { + mRenameCount++; + // This class is being renamed, so remove it from the list of classes not renamed. + mClassesNotRenamed.remove(className); + } + + mLog.debug("Transform %s%s%s%s", className, + newName == className ? "" : " (renamed to " + newName + ")", + hasNativeMethods ? " -- has natives" : "", + stubNativesOnly ? " -- stub natives only" : ""); + + // Rewrite the new class from scratch, without reusing the constant pool from the + // original class reader. + ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); + + ClassVisitor rv = cw; + if (newName != className) { + rv = new RenameClassAdapter(cw, className, newName); + } + + TransformClassAdapter cv = new TransformClassAdapter(mLog, mStubMethods, + mDeleteReturns.get(className), + newName, rv, + stubNativesOnly, stubNativesOnly || hasNativeMethods); + cr.accept(cv, 0 /* flags */); + return cw.toByteArray(); + } + + /** + * Should this class be renamed, this returns the new name. Otherwise it returns the + * original name. + * + * @param className The internal ASM name of the class that may have to be renamed + * @return A new transformed name or the original input argument. + */ + String transformName(String className) { + String newName = mRenameClasses.get(className); + if (newName != null) { + return newName; + } + int pos = className.indexOf('$'); + if (pos > 0) { + // Is this an inner class of a renamed class? + String base = className.substring(0, pos); + newName = mRenameClasses.get(base); + if (newName != null) { + return newName + className.substring(pos); + } + } + + return className; + } + + /** + * Returns true if a class has any native methods. + */ + boolean hasNativeMethods(ClassReader cr) { + ClassHasNativeVisitor cv = new ClassHasNativeVisitor(); + cr.accept(cv, 0 /* flags */); + return cv.hasNativeMethods(); + } + +} diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/ClassHasNativeVisitor.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/ClassHasNativeVisitor.java new file mode 100644 index 0000000..5424efa --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/ClassHasNativeVisitor.java @@ -0,0 +1,80 @@ +/* + * 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 com.android.tools.layoutlib.create; + +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.Attribute; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +/** + * Indicates if a class contains any native methods. + */ +public class ClassHasNativeVisitor implements ClassVisitor { + + private boolean mHasNativeMethods = false; + + public boolean hasNativeMethods() { + return mHasNativeMethods; + } + + public void visit(int version, int access, String name, String signature, + String superName, String[] interfaces) { + // pass + } + + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + // pass + return null; + } + + public void visitAttribute(Attribute attr) { + // pass + } + + public void visitEnd() { + // pass + } + + public FieldVisitor visitField(int access, String name, String desc, + String signature, Object value) { + // pass + return null; + } + + public void visitInnerClass(String name, String outerName, + String innerName, int access) { + // pass + } + + public MethodVisitor visitMethod(int access, String name, String desc, + String signature, String[] exceptions) { + mHasNativeMethods |= ((access & Opcodes.ACC_NATIVE) != 0); + return null; + } + + public void visitOuterClass(String owner, String name, String desc) { + // pass + } + + public void visitSource(String source, String debug) { + // pass + } + +} diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/Log.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/Log.java new file mode 100644 index 0000000..8efd871 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/Log.java @@ -0,0 +1,64 @@ +/* + * 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 com.android.tools.layoutlib.create; + +import java.io.PrintWriter; +import java.io.StringWriter; + +public class Log { + + private boolean mVerbose = false; + + public void setVerbose(boolean verbose) { + mVerbose = verbose; + } + + public void debug(String format, Object... args) { + if (mVerbose) { + info(format, args); + } + } + + public void info(String format, Object... args) { + String s = String.format(format, args); + outPrintln(s); + } + + public void error(String format, Object... args) { + String s = String.format(format, args); + errPrintln(s); + } + + public void exception(Throwable t, String format, Object... args) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + t.printStackTrace(pw); + pw.flush(); + error(format + "\n" + sw.toString(), args); + } + + /** for unit testing */ + protected void errPrintln(String msg) { + System.err.println(msg); + } + + /** for unit testing */ + protected void outPrintln(String msg) { + System.out.println(msg); + } + +} diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/LogAbortException.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/LogAbortException.java new file mode 100644 index 0000000..dc4b4a7 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/LogAbortException.java @@ -0,0 +1,32 @@ +/* + * 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 com.android.tools.layoutlib.create; + +public class LogAbortException extends Exception { + + private final String mFormat; + private final Object[] mArgs; + + public LogAbortException(String format, Object... args) { + mFormat = format; + mArgs = args; + } + + public void error(Log log) { + log.error(mFormat, mArgs); + } +} diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/Main.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/Main.java new file mode 100644 index 0000000..76bd8d4 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/Main.java @@ -0,0 +1,174 @@ +/* + * 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 com.android.tools.layoutlib.create; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Set; + + + +public class Main { + + public static void main(String[] args) { + + Log log = new Log(); + + ArrayList<String> osJarPath = new ArrayList<String>(); + String[] osDestJar = { null }; + + if (!processArgs(log, args, osJarPath, osDestJar)) { + log.error("Usage: layoutlib_create [-v] output.jar input.jar ..."); + System.exit(1); + } + + log.info("Output: %1$s", osDestJar[0]); + for (String path : osJarPath) { + log.info("Input : %1$s", path); + } + + try { + AsmGenerator agen = new AsmGenerator(log, osDestJar[0], + new Class<?>[] { // classes to inject in the final JAR + OverrideMethod.class, + MethodListener.class, + MethodAdapter.class + }, + new String[] { // methods to force override + "android.view.View#isInEditMode", + "android.content.res.Resources$Theme#obtainStyledAttributes", + }, + new String[] { // classes to rename (so that we can replace them in layoutlib) + // original-platform-class-name ======> renamed-class-name + "android.graphics.Matrix", "android.graphics._Original_Matrix", + "android.graphics.Paint", "android.graphics._Original_Paint", + "android.graphics.Typeface", "android.graphics._Original_Typeface", + "android.graphics.Bitmap", "android.graphics._Original_Bitmap", + "android.graphics.Path", "android.graphics._Original_Path", + "android.graphics.PorterDuffXfermode", "android.graphics._Original_PorterDuffXfermode", + "android.graphics.Shader", "android.graphics._Original_Shader", + "android.graphics.LinearGradient", "android.graphics._Original_LinearGradient", + "android.graphics.BitmapShader", "android.graphics._Original_BitmapShader", + "android.graphics.ComposeShader", "android.graphics._Original_ComposeShader", + "android.graphics.RadialGradient", "android.graphics._Original_RadialGradient", + "android.graphics.SweepGradient", "android.graphics._Original_SweepGradient", + "android.util.FloatMath", "android.util._Original_FloatMath", + "android.view.SurfaceView", "android.view._Original_SurfaceView", + }, + new String[] { // methods deleted from their return type. + "android.graphics.Paint", // class to delete method from + "android.graphics.Paint$Align", // list of type identifying methods to delete + "android.graphics.Paint$Style", + "android.graphics.Paint$Join", + "android.graphics.Paint$Cap", + "android.graphics.Paint$FontMetrics", + "android.graphics.Paint$FontMetricsInt", + null } + ); + + AsmAnalyzer aa = new AsmAnalyzer(log, osJarPath, agen, + new String[] { "android.view.View" }, // derived from + new String[] { // include classes + "android.*", // for android.R + "android.util.*", + "com.android.internal.util.*", + "android.view.*", + "android.widget.*", + "com.android.internal.widget.*", + "android.text.**", + "android.graphics.*", + "android.graphics.drawable.*", + "android.content.*", + "android.content.res.*", + "org.apache.harmony.xml.*", + "com.android.internal.R**", + "android.pim.*", // for datepicker + "android.os.*", // for android.os.Handler + }); + aa.analyze(); + agen.generate(); + + // Throw an error if any class failed to get renamed by the generator + // + // IMPORTANT: if you're building the platform and you get this error message, + // it means the renameClasses[] array in AsmGenerator needs to be updated: some + // class should have been renamed but it was not found in the input JAR files. + Set<String> notRenamed = agen.getClassesNotRenamed(); + if (notRenamed.size() > 0) { + // (80-column guide below for error formatting) + // 01234567890123456789012345678901234567890123456789012345678901234567890123456789 + log.error( + "ERROR when running layoutlib_create: the following classes are referenced\n" + + "by tools/layoutlib/create but were not actually found in the input JAR files.\n" + + "This may be due to some platform classes having been renamed."); + for (String fqcn : notRenamed) { + log.error("- Class not found: %s", fqcn.replace('/', '.')); + } + for (String path : osJarPath) { + log.info("- Input JAR : %1$s", path); + } + System.exit(1); + } + + System.exit(0); + } catch (IOException e) { + log.exception(e, "Failed to load jar"); + } catch (LogAbortException e) { + e.error(log); + } + + System.exit(1); + } + + /** + * Returns true if args where properly parsed. + * Returns false if program should exit with command-line usage. + * <p/> + * Note: the String[0] is an output parameter wrapped in an array, since there is no + * "out" parameter support. + */ + private static boolean processArgs(Log log, String[] args, + ArrayList<String> osJarPath, String[] osDestJar) { + for (int i = 0; i < args.length; i++) { + String s = args[i]; + if (s.equals("-v")) { + log.setVerbose(true); + } else if (!s.startsWith("-")) { + if (osDestJar[0] == null) { + osDestJar[0] = s; + } else { + osJarPath.add(s); + } + } else { + log.error("Unknow argument: %s", s); + return false; + } + } + + if (osJarPath.isEmpty()) { + log.error("Missing parameter: path to input jar"); + return false; + } + if (osDestJar[0] == null) { + log.error("Missing parameter: path to output jar"); + return false; + } + + return true; + } + +} diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/MethodAdapter.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/MethodAdapter.java new file mode 100644 index 0000000..627ea17 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/MethodAdapter.java @@ -0,0 +1,91 @@ +/* + * 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 com.android.tools.layoutlib.create; + + +/** + * An adapter to make it easier to use {@link MethodListener}. + * <p/> + * The adapter calls the void {@link #onInvokeV(String, boolean, Object)} listener + * for all types (I, L, F, D and A), returning 0 or null as appropriate. + */ +public class MethodAdapter implements MethodListener { + /** + * A stub method is being invoked. + * <p/> + * Known limitation: caller arguments are not available. + * + * @param signature The signature of the method being invoked, composed of the + * binary class name followed by the method descriptor (aka argument + * types). Example: "com/foo/MyClass/InnerClass/printInt(I)V". + * @param isNative True if the method was a native method. + * @param caller The calling object. Null for static methods, "this" for instance methods. + */ + public void onInvokeV(String signature, boolean isNative, Object caller) { + } + + /** + * Same as {@link #onInvokeV(String, boolean, Object)} but returns an integer or similar. + * @see #onInvokeV(String, boolean, Object) + * @return an integer, or a boolean, or a short or a byte. + */ + public int onInvokeI(String signature, boolean isNative, Object caller) { + onInvokeV(signature, isNative, caller); + return 0; + } + + /** + * Same as {@link #onInvokeV(String, boolean, Object)} but returns a long. + * @see #onInvokeV(String, boolean, Object) + * @return a long. + */ + public long onInvokeL(String signature, boolean isNative, Object caller) { + onInvokeV(signature, isNative, caller); + return 0; + } + + /** + * Same as {@link #onInvokeV(String, boolean, Object)} but returns a float. + * @see #onInvokeV(String, boolean, Object) + * @return a float. + */ + public float onInvokeF(String signature, boolean isNative, Object caller) { + onInvokeV(signature, isNative, caller); + return 0; + } + + /** + * Same as {@link #onInvokeV(String, boolean, Object)} but returns a double. + * @see #onInvokeV(String, boolean, Object) + * @return a double. + */ + public double onInvokeD(String signature, boolean isNative, Object caller) { + onInvokeV(signature, isNative, caller); + return 0; + } + + /** + * Same as {@link #onInvokeV(String, boolean, Object)} but returns an object. + * @see #onInvokeV(String, boolean, Object) + * @return an object. + */ + public Object onInvokeA(String signature, boolean isNative, Object caller) { + onInvokeV(signature, isNative, caller); + return null; + } +} + diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/MethodListener.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/MethodListener.java new file mode 100644 index 0000000..6fc2b24 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/MethodListener.java @@ -0,0 +1,76 @@ +/* + * 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 com.android.tools.layoutlib.create; + + +/** + * Interface to allow a method invocation to be listened upon. + * <p/> + * This is used by {@link OverrideMethod} to register a listener for methods that + * have been stubbed by the {@link AsmGenerator}. At runtime the stub will call either a + * default global listener or a specific listener based on the method signature. + */ +public interface MethodListener { + /** + * A stub method is being invoked. + * <p/> + * Known limitation: caller arguments are not available. + * + * @param signature The signature of the method being invoked, composed of the + * binary class name followed by the method descriptor (aka argument + * types). Example: "com/foo/MyClass/InnerClass/printInt(I)V". + * @param isNative True if the method was a native method. + * @param caller The calling object. Null for static methods, "this" for instance methods. + */ + public void onInvokeV(String signature, boolean isNative, Object caller); + + /** + * Same as {@link #onInvokeV(String, boolean, Object)} but returns an integer or similar. + * @see #onInvokeV(String, boolean, Object) + * @return an integer, or a boolean, or a short or a byte. + */ + public int onInvokeI(String signature, boolean isNative, Object caller); + + /** + * Same as {@link #onInvokeV(String, boolean, Object)} but returns a long. + * @see #onInvokeV(String, boolean, Object) + * @return a long. + */ + public long onInvokeL(String signature, boolean isNative, Object caller); + + /** + * Same as {@link #onInvokeV(String, boolean, Object)} but returns a float. + * @see #onInvokeV(String, boolean, Object) + * @return a float. + */ + public float onInvokeF(String signature, boolean isNative, Object caller); + + /** + * Same as {@link #onInvokeV(String, boolean, Object)} but returns a double. + * @see #onInvokeV(String, boolean, Object) + * @return a double. + */ + public double onInvokeD(String signature, boolean isNative, Object caller); + + /** + * Same as {@link #onInvokeV(String, boolean, Object)} but returns an object. + * @see #onInvokeV(String, boolean, Object) + * @return an object. + */ + public Object onInvokeA(String signature, boolean isNative, Object caller); +} + diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/OverrideMethod.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/OverrideMethod.java new file mode 100644 index 0000000..a6aff99 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/OverrideMethod.java @@ -0,0 +1,151 @@ +/* + * 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 com.android.tools.layoutlib.create; + +import java.util.HashMap; + +/** + * Allows stub methods from LayoutLib to be overriden at runtime. + * <p/> + * Implementation note: all types required by this class(inner/outer classes & interfaces) + * must be referenced by the injectClass argument to {@link AsmGenerator} in Main.java; + * Otherwise they won't be accessible in layoutlib.jar at runtime. + */ +public final class OverrideMethod { + + /** Map of method overridden. */ + private static HashMap<String, MethodListener> sMethods = new HashMap<String, MethodListener>(); + /** Default listener for all method not listed in sMethods. Nothing if null. */ + private static MethodListener sDefaultListener = null; + + /** + * Sets the default listener for all methods not specifically handled. + * Null means to do nothing. + */ + public static void setDefaultListener(MethodListener listener) { + sDefaultListener = listener; + } + + /** + * Defines or reset a listener for the given method signature. + * + * @param signature The signature of the method being invoked, composed of the + * binary class name followed by the method descriptor (aka argument + * types). Example: "com/foo/MyClass/InnerClass/printInt(I)V" + * @param listener The new listener. Removes it if null. + */ + public static void setMethodListener(String signature, MethodListener listener) { + if (listener == null) { + sMethods.remove(signature); + } else { + sMethods.put(signature, listener); + } + } + + /** + * Invokes the specific listener for the given signature or the default one if defined. + * <p/> + * This version invokes the method listener for the void return type. + * <p/> + * Note: this is not intended to be used by the LayoutLib Bridge. It is intended to be called + * by the stubbed methods generated by the LayoutLib_create tool. + * + * @param signature The signature of the method being invoked, composed of the + * binary class name followed by the method descriptor (aka argument + * types). Example: "com/foo/MyClass/InnerClass/printInt(I)V". + * @param isNative True if the method was a native method. + * @param caller The calling object. Null for static methods, "this" for instance methods. + */ + public static void invokeV(String signature, boolean isNative, Object caller) { + MethodListener i = sMethods.get(signature); + if (i != null) { + i.onInvokeV(signature, isNative, caller); + } else if (sDefaultListener != null) { + sDefaultListener.onInvokeV(signature, isNative, caller); + } + } + + /** + * Invokes the specific listener for the int return type. + * @see #invokeV(String, boolean, Object) + */ + public static int invokeI(String signature, boolean isNative, Object caller) { + MethodListener i = sMethods.get(signature); + if (i != null) { + return i.onInvokeI(signature, isNative, caller); + } else if (sDefaultListener != null) { + return sDefaultListener.onInvokeI(signature, isNative, caller); + } + return 0; + } + + /** + * Invokes the specific listener for the long return type. + * @see #invokeV(String, boolean, Object) + */ + public static long invokeL(String signature, boolean isNative, Object caller) { + MethodListener i = sMethods.get(signature); + if (i != null) { + return i.onInvokeL(signature, isNative, caller); + } else if (sDefaultListener != null) { + return sDefaultListener.onInvokeL(signature, isNative, caller); + } + return 0; + } + + /** + * Invokes the specific listener for the float return type. + * @see #invokeV(String, boolean, Object) + */ + public static float invokeF(String signature, boolean isNative, Object caller) { + MethodListener i = sMethods.get(signature); + if (i != null) { + return i.onInvokeF(signature, isNative, caller); + } else if (sDefaultListener != null) { + return sDefaultListener.onInvokeF(signature, isNative, caller); + } + return 0; + } + + /** + * Invokes the specific listener for the double return type. + * @see #invokeV(String, boolean, Object) + */ + public static double invokeD(String signature, boolean isNative, Object caller) { + MethodListener i = sMethods.get(signature); + if (i != null) { + return i.onInvokeD(signature, isNative, caller); + } else if (sDefaultListener != null) { + return sDefaultListener.onInvokeD(signature, isNative, caller); + } + return 0; + } + + /** + * Invokes the specific listener for the object return type. + * @see #invokeV(String, boolean, Object) + */ + public static Object invokeA(String signature, boolean isNative, Object caller) { + MethodListener i = sMethods.get(signature); + if (i != null) { + return i.onInvokeA(signature, isNative, caller); + } else if (sDefaultListener != null) { + return sDefaultListener.onInvokeA(signature, isNative, caller); + } + return null; + } +} diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/RenameClassAdapter.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/RenameClassAdapter.java new file mode 100644 index 0000000..0956b92 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/RenameClassAdapter.java @@ -0,0 +1,446 @@ +/* + * 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 com.android.tools.layoutlib.create; + +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.ClassAdapter; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodAdapter; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Type; +import org.objectweb.asm.signature.SignatureReader; +import org.objectweb.asm.signature.SignatureVisitor; +import org.objectweb.asm.signature.SignatureWriter; + +/** + * This class visitor renames a class from a given old name to a given new name. + * The class visitor will also rename all inner classes and references in the methods. + * <p/> + * + * For inner classes, this handles only the case where the outer class name changes. + * The inner class name should remain the same. + */ +public class RenameClassAdapter extends ClassAdapter { + + + private final String mOldName; + private final String mNewName; + private String mOldBase; + private String mNewBase; + + /** + * Creates a class visitor that renames a class from a given old name to a given new name. + * The class visitor will also rename all inner classes and references in the methods. + * The names must be full qualified internal ASM names (e.g. com/blah/MyClass$InnerClass). + */ + public RenameClassAdapter(ClassWriter cv, String oldName, String newName) { + super(cv); + mOldBase = mOldName = oldName; + mNewBase = mNewName = newName; + + int pos = mOldName.indexOf('$'); + if (pos > 0) { + mOldBase = mOldName.substring(0, pos); + } + pos = mNewName.indexOf('$'); + if (pos > 0) { + mNewBase = mNewName.substring(0, pos); + } + + assert (mOldBase == null && mNewBase == null) || (mOldBase != null && mNewBase != null); + } + + + /** + * Renames a type descriptor, e.g. "Lcom.package.MyClass;" + * If the type doesn't need to be renamed, returns the input string as-is. + */ + String renameTypeDesc(String desc) { + if (desc == null) { + return null; + } + + return renameType(Type.getType(desc)); + } + + /** + * Renames an object type, e.g. "Lcom.package.MyClass;" or an array type that has an + * object element, e.g. "[Lcom.package.MyClass;" + * If the type doesn't need to be renamed, returns the internal name of the input type. + */ + String renameType(Type type) { + if (type == null) { + return null; + } + + if (type.getSort() == Type.OBJECT) { + String in = type.getInternalName(); + return "L" + renameInternalType(in) + ";"; + } else if (type.getSort() == Type.ARRAY) { + StringBuilder sb = new StringBuilder(); + for (int n = type.getDimensions(); n > 0; n--) { + sb.append('['); + } + sb.append(renameType(type.getElementType())); + return sb.toString(); + } + return type.getDescriptor(); + } + + /** + * Renames an object type, e.g. "Lcom.package.MyClass;" or an array type that has an + * object element, e.g. "[Lcom.package.MyClass;". + * This is like renameType() except that it returns a Type object. + * If the type doesn't need to be renamed, returns the input type object. + */ + Type renameTypeAsType(Type type) { + if (type == null) { + return null; + } + + if (type.getSort() == Type.OBJECT) { + String in = type.getInternalName(); + String newIn = renameInternalType(in); + if (newIn != in) { + return Type.getType("L" + newIn + ";"); + } + } else if (type.getSort() == Type.ARRAY) { + StringBuilder sb = new StringBuilder(); + for (int n = type.getDimensions(); n > 0; n--) { + sb.append('['); + } + sb.append(renameType(type.getElementType())); + return Type.getType(sb.toString()); + } + return type; + } + + /** + * Renames an internal type name, e.g. "com.package.MyClass". + * If the type doesn't need to be renamed, returns the input string as-is. + * <p/> + * The internal type of some of the MethodVisitor turns out to be a type + descriptor sometimes so descriptors are renamed too. + */ + String renameInternalType(String type) { + if (type == null) { + return null; + } + + if (type.equals(mOldName)) { + return mNewName; + } + + if (mOldBase != mOldName && type.equals(mOldBase)) { + return mNewBase; + } + + int pos = type.indexOf('$'); + if (pos == mOldBase.length() && type.startsWith(mOldBase)) { + return mNewBase + type.substring(pos); + } + + // The internal type of some of the MethodVisitor turns out to be a type + // descriptor sometimes. This is the case with visitTypeInsn(type) and + // visitMethodInsn(owner). We try to detect it and adjust it here. + if (type.indexOf(';') > 0) { + type = renameTypeDesc(type); + } + + return type; + } + + /** + * Renames a method descriptor, i.e. applies renameType to all arguments and to the + * return value. + */ + String renameMethodDesc(String desc) { + if (desc == null) { + return null; + } + + Type[] args = Type.getArgumentTypes(desc); + + StringBuilder sb = new StringBuilder("("); + for (Type arg : args) { + String name = renameType(arg); + sb.append(name); + } + sb.append(')'); + + Type ret = Type.getReturnType(desc); + String name = renameType(ret); + sb.append(name); + + return sb.toString(); + } + + + /** + * Renames the ClassSignature handled by ClassVisitor.visit + * or the MethodTypeSignature handled by ClassVisitor.visitMethod. + */ + String renameTypeSignature(String sig) { + if (sig == null) { + return null; + } + SignatureReader reader = new SignatureReader(sig); + SignatureWriter writer = new SignatureWriter(); + reader.accept(new RenameSignatureAdapter(writer)); + sig = writer.toString(); + return sig; + } + + + /** + * Renames the FieldTypeSignature handled by ClassVisitor.visitField + * or MethodVisitor.visitLocalVariable. + */ + String renameFieldSignature(String sig) { + if (sig == null) { + return null; + } + SignatureReader reader = new SignatureReader(sig); + SignatureWriter writer = new SignatureWriter(); + reader.acceptType(new RenameSignatureAdapter(writer)); + sig = writer.toString(); + return sig; + } + + + //---------------------------------- + // Methods from the ClassAdapter + + @Override + public void visit(int version, int access, String name, String signature, + String superName, String[] interfaces) { + name = renameInternalType(name); + superName = renameInternalType(superName); + signature = renameTypeSignature(signature); + + super.visit(version, access, name, signature, superName, interfaces); + } + + @Override + public void visitInnerClass(String name, String outerName, String innerName, int access) { + assert outerName.equals(mOldName); + outerName = renameInternalType(outerName); + name = outerName + "$" + innerName; + super.visitInnerClass(name, outerName, innerName, access); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String desc, + String signature, String[] exceptions) { + desc = renameMethodDesc(desc); + signature = renameTypeSignature(signature); + MethodVisitor mw = super.visitMethod(access, name, desc, signature, exceptions); + return new RenameMethodAdapter(mw); + } + + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + desc = renameTypeDesc(desc); + return super.visitAnnotation(desc, visible); + } + + @Override + public FieldVisitor visitField(int access, String name, String desc, + String signature, Object value) { + desc = renameTypeDesc(desc); + signature = renameFieldSignature(signature); + return super.visitField(access, name, desc, signature, value); + } + + + //---------------------------------- + + /** + * A method visitor that renames all references from an old class name to a new class name. + */ + public class RenameMethodAdapter extends MethodAdapter { + + /** + * Creates a method visitor that renames all references from a given old name to a given new + * name. The method visitor will also rename all inner classes. + * The names must be full qualified internal ASM names (e.g. com/blah/MyClass$InnerClass). + */ + public RenameMethodAdapter(MethodVisitor mv) { + super(mv); + } + + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + desc = renameTypeDesc(desc); + + return super.visitAnnotation(desc, visible); + } + + @Override + public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible) { + desc = renameTypeDesc(desc); + + return super.visitParameterAnnotation(parameter, desc, visible); + } + + @Override + public void visitTypeInsn(int opcode, String type) { + type = renameInternalType(type); + + super.visitTypeInsn(opcode, type); + } + + @Override + public void visitFieldInsn(int opcode, String owner, String name, String desc) { + owner = renameInternalType(owner); + desc = renameTypeDesc(desc); + + super.visitFieldInsn(opcode, owner, name, desc); + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String desc) { + owner = renameInternalType(owner); + desc = renameMethodDesc(desc); + + super.visitMethodInsn(opcode, owner, name, desc); + } + + @Override + public void visitLdcInsn(Object cst) { + // If cst is a Type, this means the code is trying to pull the .class constant + // for this class, so it needs to be renamed too. + if (cst instanceof Type) { + cst = renameTypeAsType((Type) cst); + } + super.visitLdcInsn(cst); + } + + @Override + public void visitMultiANewArrayInsn(String desc, int dims) { + desc = renameTypeDesc(desc); + + super.visitMultiANewArrayInsn(desc, dims); + } + + @Override + public void visitTryCatchBlock(Label start, Label end, Label handler, String type) { + type = renameInternalType(type); + + super.visitTryCatchBlock(start, end, handler, type); + } + + @Override + public void visitLocalVariable(String name, String desc, String signature, + Label start, Label end, int index) { + desc = renameTypeDesc(desc); + signature = renameFieldSignature(signature); + + super.visitLocalVariable(name, desc, signature, start, end, index); + } + + } + + //---------------------------------- + + public class RenameSignatureAdapter implements SignatureVisitor { + + private final SignatureVisitor mSv; + + public RenameSignatureAdapter(SignatureVisitor sv) { + mSv = sv; + } + + public void visitClassType(String name) { + name = renameInternalType(name); + mSv.visitClassType(name); + } + + public void visitInnerClassType(String name) { + name = renameInternalType(name); + mSv.visitInnerClassType(name); + } + + public SignatureVisitor visitArrayType() { + SignatureVisitor sv = mSv.visitArrayType(); + return new RenameSignatureAdapter(sv); + } + + public void visitBaseType(char descriptor) { + mSv.visitBaseType(descriptor); + } + + public SignatureVisitor visitClassBound() { + SignatureVisitor sv = mSv.visitClassBound(); + return new RenameSignatureAdapter(sv); + } + + public void visitEnd() { + mSv.visitEnd(); + } + + public SignatureVisitor visitExceptionType() { + SignatureVisitor sv = mSv.visitExceptionType(); + return new RenameSignatureAdapter(sv); + } + + public void visitFormalTypeParameter(String name) { + mSv.visitFormalTypeParameter(name); + } + + public SignatureVisitor visitInterface() { + SignatureVisitor sv = mSv.visitInterface(); + return new RenameSignatureAdapter(sv); + } + + public SignatureVisitor visitInterfaceBound() { + SignatureVisitor sv = mSv.visitInterfaceBound(); + return new RenameSignatureAdapter(sv); + } + + public SignatureVisitor visitParameterType() { + SignatureVisitor sv = mSv.visitParameterType(); + return new RenameSignatureAdapter(sv); + } + + public SignatureVisitor visitReturnType() { + SignatureVisitor sv = mSv.visitReturnType(); + return new RenameSignatureAdapter(sv); + } + + public SignatureVisitor visitSuperclass() { + SignatureVisitor sv = mSv.visitSuperclass(); + return new RenameSignatureAdapter(sv); + } + + public void visitTypeArgument() { + mSv.visitTypeArgument(); + } + + public SignatureVisitor visitTypeArgument(char wildcard) { + SignatureVisitor sv = mSv.visitTypeArgument(wildcard); + return new RenameSignatureAdapter(sv); + } + + public void visitTypeVariable(String name) { + mSv.visitTypeVariable(name); + } + + } +} diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/StubMethodAdapter.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/StubMethodAdapter.java new file mode 100644 index 0000000..9a57a4a --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/StubMethodAdapter.java @@ -0,0 +1,350 @@ +/* + * 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 com.android.tools.layoutlib.create; + +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.Attribute; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; + +/** + * This method adapter rewrites a method by discarding the original code and generating + * a stub depending on the return type. Original annotations are passed along unchanged. + */ +class StubMethodAdapter implements MethodVisitor { + + private static String CONSTRUCTOR = "<init>"; + private static String CLASS_INIT = "<clinit>"; + + /** The parent method writer */ + private MethodVisitor mParentVisitor; + /** The method return type. Can be null. */ + private Type mReturnType; + /** Message to be printed by stub methods. */ + private String mInvokeSignature; + /** Flag to output the first line number. */ + private boolean mOutputFirstLineNumber = true; + /** Flag that is true when implementing a constructor, to accept all original + * code calling the original super constructor. */ + private boolean mIsInitMethod = false; + + private boolean mMessageGenerated; + private final boolean mIsStatic; + private final boolean mIsNative; + + public StubMethodAdapter(MethodVisitor mv, String methodName, Type returnType, + String invokeSignature, boolean isStatic, boolean isNative) { + mParentVisitor = mv; + mReturnType = returnType; + mInvokeSignature = invokeSignature; + mIsStatic = isStatic; + mIsNative = isNative; + + if (CONSTRUCTOR.equals(methodName) || CLASS_INIT.equals(methodName)) { + mIsInitMethod = true; + } + } + + private void generateInvoke() { + /* Generates the code: + * OverrideMethod.invoke("signature", mIsNative ? true : false, null or this); + */ + mParentVisitor.visitLdcInsn(mInvokeSignature); + // push true or false + mParentVisitor.visitInsn(mIsNative ? Opcodes.ICONST_1 : Opcodes.ICONST_0); + // push null or this + if (mIsStatic) { + mParentVisitor.visitInsn(Opcodes.ACONST_NULL); + } else { + mParentVisitor.visitVarInsn(Opcodes.ALOAD, 0); + } + + int sort = mReturnType != null ? mReturnType.getSort() : Type.VOID; + switch(sort) { + case Type.VOID: + mParentVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, + "com/android/tools/layoutlib/create/OverrideMethod", + "invokeV", + "(Ljava/lang/String;ZLjava/lang/Object;)V"); + mParentVisitor.visitInsn(Opcodes.RETURN); + break; + case Type.BOOLEAN: + case Type.CHAR: + case Type.BYTE: + case Type.SHORT: + case Type.INT: + mParentVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, + "com/android/tools/layoutlib/create/OverrideMethod", + "invokeI", + "(Ljava/lang/String;ZLjava/lang/Object;)I"); + switch(sort) { + case Type.BOOLEAN: + Label l1 = new Label(); + mParentVisitor.visitJumpInsn(Opcodes.IFEQ, l1); + mParentVisitor.visitInsn(Opcodes.ICONST_1); + mParentVisitor.visitInsn(Opcodes.IRETURN); + mParentVisitor.visitLabel(l1); + mParentVisitor.visitInsn(Opcodes.ICONST_0); + break; + case Type.CHAR: + mParentVisitor.visitInsn(Opcodes.I2C); + break; + case Type.BYTE: + mParentVisitor.visitInsn(Opcodes.I2B); + break; + case Type.SHORT: + mParentVisitor.visitInsn(Opcodes.I2S); + break; + } + mParentVisitor.visitInsn(Opcodes.IRETURN); + break; + case Type.LONG: + mParentVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, + "com/android/tools/layoutlib/create/OverrideMethod", + "invokeL", + "(Ljava/lang/String;ZLjava/lang/Object;)J"); + mParentVisitor.visitInsn(Opcodes.LRETURN); + break; + case Type.FLOAT: + mParentVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, + "com/android/tools/layoutlib/create/OverrideMethod", + "invokeF", + "(Ljava/lang/String;ZLjava/lang/Object;)F"); + mParentVisitor.visitInsn(Opcodes.FRETURN); + break; + case Type.DOUBLE: + mParentVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, + "com/android/tools/layoutlib/create/OverrideMethod", + "invokeD", + "(Ljava/lang/String;ZLjava/lang/Object;)D"); + mParentVisitor.visitInsn(Opcodes.DRETURN); + break; + case Type.ARRAY: + case Type.OBJECT: + mParentVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, + "com/android/tools/layoutlib/create/OverrideMethod", + "invokeA", + "(Ljava/lang/String;ZLjava/lang/Object;)Ljava/lang/Object;"); + mParentVisitor.visitTypeInsn(Opcodes.CHECKCAST, mReturnType.getInternalName()); + mParentVisitor.visitInsn(Opcodes.ARETURN); + break; + } + + } + + private void generatePop() { + /* Pops the stack, depending on the return type. + */ + switch(mReturnType != null ? mReturnType.getSort() : Type.VOID) { + case Type.VOID: + break; + case Type.BOOLEAN: + case Type.CHAR: + case Type.BYTE: + case Type.SHORT: + case Type.INT: + case Type.FLOAT: + case Type.ARRAY: + case Type.OBJECT: + mParentVisitor.visitInsn(Opcodes.POP); + break; + case Type.LONG: + case Type.DOUBLE: + mParentVisitor.visitInsn(Opcodes.POP2); + break; + } + } + + /* Pass down to visitor writer. In this implementation, either do nothing. */ + public void visitCode() { + mParentVisitor.visitCode(); + } + + /* + * visitMaxs is called just before visitEnd if there was any code to rewrite. + * For non-constructor, generate the messaging code and the return statement + * if it hasn't been done before. + */ + public void visitMaxs(int maxStack, int maxLocals) { + if (!mIsInitMethod && !mMessageGenerated) { + generateInvoke(); + mMessageGenerated = true; + } + mParentVisitor.visitMaxs(maxStack, maxLocals); + } + + /** + * End of visiting. + * For non-constructor, generate the messaging code and the return statement + * if it hasn't been done before. + */ + public void visitEnd() { + if (!mIsInitMethod && !mMessageGenerated) { + generateInvoke(); + mMessageGenerated = true; + mParentVisitor.visitMaxs(1, 1); + } + mParentVisitor.visitEnd(); + } + + /* Writes all annotation from the original method. */ + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + return mParentVisitor.visitAnnotation(desc, visible); + } + + /* Writes all annotation default values from the original method. */ + public AnnotationVisitor visitAnnotationDefault() { + return mParentVisitor.visitAnnotationDefault(); + } + + public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, + boolean visible) { + return mParentVisitor.visitParameterAnnotation(parameter, desc, visible); + } + + /* Writes all attributes from the original method. */ + public void visitAttribute(Attribute attr) { + mParentVisitor.visitAttribute(attr); + } + + /* + * Only writes the first line number present in the original code so that source + * viewers can direct to the correct method, even if the content doesn't match. + */ + public void visitLineNumber(int line, Label start) { + if (mIsInitMethod || mOutputFirstLineNumber) { + mParentVisitor.visitLineNumber(line, start); + mOutputFirstLineNumber = false; + } + } + + /** + * For non-constructor, rewrite existing "return" instructions to write the message. + */ + public void visitInsn(int opcode) { + if (mIsInitMethod) { + switch (opcode) { + case Opcodes.RETURN: + case Opcodes.ARETURN: + case Opcodes.DRETURN: + case Opcodes.FRETURN: + case Opcodes.IRETURN: + case Opcodes.LRETURN: + // Pop the last word from the stack since invoke will generate its own return. + generatePop(); + generateInvoke(); + mMessageGenerated = true; + default: + mParentVisitor.visitInsn(opcode); + } + } + } + + public void visitLabel(Label label) { + if (mIsInitMethod) { + mParentVisitor.visitLabel(label); + } + } + + public void visitTryCatchBlock(Label start, Label end, Label handler, String type) { + if (mIsInitMethod) { + mParentVisitor.visitTryCatchBlock(start, end, handler, type); + } + } + + public void visitMethodInsn(int opcode, String owner, String name, String desc) { + if (mIsInitMethod) { + mParentVisitor.visitMethodInsn(opcode, owner, name, desc); + } + } + + public void visitFieldInsn(int opcode, String owner, String name, String desc) { + if (mIsInitMethod) { + mParentVisitor.visitFieldInsn(opcode, owner, name, desc); + } + } + + public void visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack) { + if (mIsInitMethod) { + mParentVisitor.visitFrame(type, nLocal, local, nStack, stack); + } + } + + public void visitIincInsn(int var, int increment) { + if (mIsInitMethod) { + mParentVisitor.visitIincInsn(var, increment); + } + } + + public void visitIntInsn(int opcode, int operand) { + if (mIsInitMethod) { + mParentVisitor.visitIntInsn(opcode, operand); + } + } + + public void visitJumpInsn(int opcode, Label label) { + if (mIsInitMethod) { + mParentVisitor.visitJumpInsn(opcode, label); + } + } + + public void visitLdcInsn(Object cst) { + if (mIsInitMethod) { + mParentVisitor.visitLdcInsn(cst); + } + } + + public void visitLocalVariable(String name, String desc, String signature, + Label start, Label end, int index) { + if (mIsInitMethod) { + mParentVisitor.visitLocalVariable(name, desc, signature, start, end, index); + } + } + + public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) { + if (mIsInitMethod) { + mParentVisitor.visitLookupSwitchInsn(dflt, keys, labels); + } + } + + public void visitMultiANewArrayInsn(String desc, int dims) { + if (mIsInitMethod) { + mParentVisitor.visitMultiANewArrayInsn(desc, dims); + } + } + + public void visitTableSwitchInsn(int min, int max, Label dflt, Label[] labels) { + if (mIsInitMethod) { + mParentVisitor.visitTableSwitchInsn(min, max, dflt, labels); + } + } + + public void visitTypeInsn(int opcode, String type) { + if (mIsInitMethod) { + mParentVisitor.visitTypeInsn(opcode, type); + } + } + + public void visitVarInsn(int opcode, int var) { + if (mIsInitMethod) { + mParentVisitor.visitVarInsn(opcode, var); + } + } + +} diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/TransformClassAdapter.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/TransformClassAdapter.java new file mode 100644 index 0000000..e294d56 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/TransformClassAdapter.java @@ -0,0 +1,177 @@ +/* + * 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 com.android.tools.layoutlib.create; + +import org.objectweb.asm.ClassAdapter; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; + +import java.util.Set; + +/** + * Class adapter that can stub some or all of the methods of the class. + */ +class TransformClassAdapter extends ClassAdapter { + + /** True if all methods should be stubbed, false if only native ones must be stubbed. */ + private final boolean mStubAll; + /** True if the class is an interface. */ + private boolean mIsInterface; + private final String mClassName; + private final Log mLog; + private final Set<String> mStubMethods; + private Set<String> mDeleteReturns; + + /** + * Creates a new class adapter that will stub some or all methods. + * @param logger + * @param stubMethods + * @param deleteReturns list of types that trigger the deletion of methods returning them. + * @param className The name of the class being modified + * @param cv The parent class writer visitor + * @param stubNativesOnly True if only native methods should be stubbed. False if all + * methods should be stubbed. + * @param hasNative True if the method has natives, in which case its access should be + * changed. + */ + public TransformClassAdapter(Log logger, Set<String> stubMethods, + Set<String> deleteReturns, String className, ClassVisitor cv, + boolean stubNativesOnly, boolean hasNative) { + super(cv); + mLog = logger; + mStubMethods = stubMethods; + mClassName = className; + mStubAll = !stubNativesOnly; + mIsInterface = false; + mDeleteReturns = deleteReturns; + } + + /* Visits the class header. */ + @Override + public void visit(int version, int access, String name, + String signature, String superName, String[] interfaces) { + + // This class might be being renamed. + name = mClassName; + + // remove protected or private and set as public + access = access & ~(Opcodes.ACC_PRIVATE | Opcodes.ACC_PROTECTED); + access |= Opcodes.ACC_PUBLIC; + // remove final + access = access & ~Opcodes.ACC_FINAL; + // note: leave abstract classes as such + // don't try to implement stub for interfaces + + mIsInterface = ((access & Opcodes.ACC_INTERFACE) != 0); + super.visit(version, access, name, signature, superName, interfaces); + } + + /* Visits the header of an inner class. */ + @Override + public void visitInnerClass(String name, String outerName, String innerName, int access) { + // remove protected or private and set as public + access = access & ~(Opcodes.ACC_PRIVATE | Opcodes.ACC_PROTECTED); + access |= Opcodes.ACC_PUBLIC; + // remove final + access = access & ~Opcodes.ACC_FINAL; + // note: leave abstract classes as such + // don't try to implement stub for interfaces + + super.visitInnerClass(name, outerName, innerName, access); + } + + /* Visits a method. */ + @Override + public MethodVisitor visitMethod(int access, String name, String desc, + String signature, String[] exceptions) { + + if (mDeleteReturns != null) { + Type t = Type.getReturnType(desc); + if (t.getSort() == Type.OBJECT) { + String returnType = t.getInternalName(); + if (returnType != null) { + if (mDeleteReturns.contains(returnType)) { + return null; + } + } + } + } + + String methodSignature = mClassName.replace('/', '.') + "#" + name; + + // change access to public + access &= ~(Opcodes.ACC_PROTECTED | Opcodes.ACC_PRIVATE); + access |= Opcodes.ACC_PUBLIC; + + // remove final + access = access & ~Opcodes.ACC_FINAL; + + // stub this method if they are all to be stubbed or if it is a native method + // and don't try to stub interfaces nor abstract non-native methods. + if (!mIsInterface && + ((access & (Opcodes.ACC_ABSTRACT | Opcodes.ACC_NATIVE)) != Opcodes.ACC_ABSTRACT) && + (mStubAll || + (access & Opcodes.ACC_NATIVE) != 0) || + mStubMethods.contains(methodSignature)) { + + boolean isStatic = (access & Opcodes.ACC_STATIC) != 0; + boolean isNative = (access & Opcodes.ACC_NATIVE) != 0; + + // remove abstract, final and native + access = access & ~(Opcodes.ACC_ABSTRACT | Opcodes.ACC_FINAL | Opcodes.ACC_NATIVE); + + String invokeSignature = methodSignature + desc; + mLog.debug(" Stub: %s (%s)", invokeSignature, isNative ? "native" : ""); + + MethodVisitor mw = super.visitMethod(access, name, desc, signature, exceptions); + return new StubMethodAdapter(mw, name, returnType(desc), invokeSignature, + isStatic, isNative); + + } else { + mLog.debug(" Keep: %s %s", name, desc); + return super.visitMethod(access, name, desc, signature, exceptions); + } + } + + /* Visits a field. Makes it public. */ + @Override + public FieldVisitor visitField(int access, String name, String desc, String signature, + Object value) { + // change access to public + access &= ~(Opcodes.ACC_PROTECTED | Opcodes.ACC_PRIVATE); + access |= Opcodes.ACC_PUBLIC; + + return super.visitField(access, name, desc, signature, value); + } + + /** + * Extracts the return {@link Type} of this descriptor. + */ + Type returnType(String desc) { + if (desc != null) { + try { + return Type.getReturnType(desc); + } catch (ArrayIndexOutOfBoundsException e) { + // ignore, not a valid type. + } + } + return null; + } +} diff --git a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/AsmAnalyzerTest.java b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/AsmAnalyzerTest.java new file mode 100644 index 0000000..603284e --- /dev/null +++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/AsmAnalyzerTest.java @@ -0,0 +1,228 @@ +/* + * 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 com.android.tools.layoutlib.create; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import com.android.tools.layoutlib.create.AsmAnalyzer.DependencyVisitor; +import com.android.tools.layoutlib.create.LogTest.MockLog; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.objectweb.asm.ClassReader; + +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Map; +import java.util.TreeMap; + +/** + * Unit tests for some methods of {@link AsmAnalyzer}. + */ +public class AsmAnalyzerTest { + + private MockLog mLog; + private ArrayList<String> mOsJarPath; + private AsmAnalyzer mAa; + + @Before + public void setUp() throws Exception { + mLog = new LogTest.MockLog(); + URL url = this.getClass().getClassLoader().getResource("data/mock_android.jar"); + + mOsJarPath = new ArrayList<String>(); + mOsJarPath.add(url.getFile()); + + mAa = new AsmAnalyzer(mLog, mOsJarPath, null /* gen */, + null /* deriveFrom */, null /* includeGlobs */ ); + } + + @After + public void tearDown() throws Exception { + } + + @Test + public void testParseZip() throws IOException { + Map<String, ClassReader> map = mAa.parseZip(mOsJarPath); + + assertArrayEquals(new String[] { + "mock_android.dummy.InnerTest", + "mock_android.dummy.InnerTest$DerivingClass", + "mock_android.dummy.InnerTest$MyGenerics1", + "mock_android.dummy.InnerTest$MyIntEnum", + "mock_android.dummy.InnerTest$MyStaticInnerClass", + "mock_android.dummy.InnerTest$NotStaticInner1", + "mock_android.dummy.InnerTest$NotStaticInner2", + "mock_android.view.View", + "mock_android.view.ViewGroup", + "mock_android.view.ViewGroup$LayoutParams", + "mock_android.view.ViewGroup$MarginLayoutParams", + "mock_android.widget.LinearLayout", + "mock_android.widget.LinearLayout$LayoutParams", + "mock_android.widget.TableLayout", + "mock_android.widget.TableLayout$LayoutParams" + }, + map.keySet().toArray()); + } + + @Test + public void testFindClass() throws IOException, LogAbortException { + Map<String, ClassReader> zipClasses = mAa.parseZip(mOsJarPath); + TreeMap<String, ClassReader> found = new TreeMap<String, ClassReader>(); + + ClassReader cr = mAa.findClass("mock_android.view.ViewGroup$LayoutParams", + zipClasses, found); + + assertNotNull(cr); + assertEquals("mock_android/view/ViewGroup$LayoutParams", cr.getClassName()); + assertArrayEquals(new String[] { "mock_android.view.ViewGroup$LayoutParams" }, + found.keySet().toArray()); + assertArrayEquals(new ClassReader[] { cr }, found.values().toArray()); + } + + @Test + public void testFindGlobs() throws IOException, LogAbortException { + Map<String, ClassReader> zipClasses = mAa.parseZip(mOsJarPath); + TreeMap<String, ClassReader> found = new TreeMap<String, ClassReader>(); + + // this matches classes, a package match returns nothing + found.clear(); + mAa.findGlobs("mock_android.view", zipClasses, found); + + assertArrayEquals(new String[] { }, + found.keySet().toArray()); + + // a complex glob search. * is a search pattern that matches names, not dots + mAa.findGlobs("mock_android.*.*Group$*Layout*", zipClasses, found); + + assertArrayEquals(new String[] { + "mock_android.view.ViewGroup$LayoutParams", + "mock_android.view.ViewGroup$MarginLayoutParams" + }, + found.keySet().toArray()); + + // a complex glob search. ** is a search pattern that matches names including dots + mAa.findGlobs("mock_android.**Group*", zipClasses, found); + + assertArrayEquals(new String[] { + "mock_android.view.ViewGroup", + "mock_android.view.ViewGroup$LayoutParams", + "mock_android.view.ViewGroup$MarginLayoutParams" + }, + found.keySet().toArray()); + + // matches a single class + found.clear(); + mAa.findGlobs("mock_android.view.View", zipClasses, found); + + assertArrayEquals(new String[] { + "mock_android.view.View" + }, + found.keySet().toArray()); + + // matches everyting inside the given package but not sub-packages + found.clear(); + mAa.findGlobs("mock_android.view.*", zipClasses, found); + + assertArrayEquals(new String[] { + "mock_android.view.View", + "mock_android.view.ViewGroup", + "mock_android.view.ViewGroup$LayoutParams", + "mock_android.view.ViewGroup$MarginLayoutParams" + }, + found.keySet().toArray()); + + for (String key : found.keySet()) { + ClassReader value = found.get(key); + assertNotNull("No value for " + key, value); + assertEquals(key, AsmAnalyzer.classReaderToClassName(value)); + } + } + + @Test + public void testFindClassesDerivingFrom() throws LogAbortException, IOException { + Map<String, ClassReader> zipClasses = mAa.parseZip(mOsJarPath); + TreeMap<String, ClassReader> found = new TreeMap<String, ClassReader>(); + + mAa.findClassesDerivingFrom("mock_android.view.View", zipClasses, found); + + assertArrayEquals(new String[] { + "mock_android.view.View", + "mock_android.view.ViewGroup", + "mock_android.widget.LinearLayout", + "mock_android.widget.TableLayout", + }, + found.keySet().toArray()); + + for (String key : found.keySet()) { + ClassReader value = found.get(key); + assertNotNull("No value for " + key, value); + assertEquals(key, AsmAnalyzer.classReaderToClassName(value)); + } + } + + @Test + public void testDependencyVisitor() throws IOException, LogAbortException { + Map<String, ClassReader> zipClasses = mAa.parseZip(mOsJarPath); + TreeMap<String, ClassReader> keep = new TreeMap<String, ClassReader>(); + TreeMap<String, ClassReader> new_keep = new TreeMap<String, ClassReader>(); + TreeMap<String, ClassReader> in_deps = new TreeMap<String, ClassReader>(); + TreeMap<String, ClassReader> out_deps = new TreeMap<String, ClassReader>(); + + ClassReader cr = mAa.findClass("mock_android.widget.TableLayout", zipClasses, keep); + DependencyVisitor visitor = mAa.getVisitor(zipClasses, keep, new_keep, in_deps, out_deps); + + // get first level dependencies + cr.accept(visitor, 0 /* flags */); + + assertArrayEquals(new String[] { + "mock_android.view.ViewGroup", + "mock_android.widget.TableLayout$LayoutParams", + }, + out_deps.keySet().toArray()); + + in_deps.putAll(out_deps); + out_deps.clear(); + + // get second level dependencies + for (ClassReader cr2 : in_deps.values()) { + cr2.accept(visitor, 0 /* flags */); + } + + assertArrayEquals(new String[] { + "mock_android.view.View", + "mock_android.view.ViewGroup$LayoutParams", + "mock_android.view.ViewGroup$MarginLayoutParams", + }, + out_deps.keySet().toArray()); + + in_deps.putAll(out_deps); + out_deps.clear(); + + // get third level dependencies (there are none) + for (ClassReader cr2 : in_deps.values()) { + cr2.accept(visitor, 0 /* flags */); + } + + assertArrayEquals(new String[] { }, out_deps.keySet().toArray()); + } +} diff --git a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/AsmGeneratorTest.java b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/AsmGeneratorTest.java new file mode 100644 index 0000000..7cdf79a --- /dev/null +++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/AsmGeneratorTest.java @@ -0,0 +1,90 @@ +/* + * 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 com.android.tools.layoutlib.create; + + +import static org.junit.Assert.assertArrayEquals; + +import com.android.tools.layoutlib.create.LogTest.MockLog; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Set; + +/** + * Unit tests for some methods of {@link AsmGenerator}. + */ +public class AsmGeneratorTest { + + private MockLog mLog; + private ArrayList<String> mOsJarPath; + private String mOsDestJar; + private File mTempFile; + + @Before + public void setUp() throws Exception { + mLog = new LogTest.MockLog(); + URL url = this.getClass().getClassLoader().getResource("data/mock_android.jar"); + + mOsJarPath = new ArrayList<String>(); + mOsJarPath.add(url.getFile()); + + mTempFile = File.createTempFile("mock", "jar"); + mOsDestJar = mTempFile.getAbsolutePath(); + mTempFile.deleteOnExit(); + } + + @After + public void tearDown() throws Exception { + if (mTempFile != null) { + mTempFile.delete(); + mTempFile = null; + } + } + + @Test + public void testClassRenaming() throws IOException, LogAbortException { + + AsmGenerator agen = new AsmGenerator(mLog, mOsDestJar, + null, // classes to inject in the final JAR + null, // methods to force override + new String[] { // classes to rename (so that we can replace them) + "mock_android.view.View", "mock_android.view._Original_View", + "not.an.actual.ClassName", "anoter.fake.NewClassName", + }, + null // methods deleted from their return type. + ); + + AsmAnalyzer aa = new AsmAnalyzer(mLog, mOsJarPath, agen, + null, // derived from + new String[] { // include classes + "**" + }); + aa.analyze(); + agen.generate(); + + Set<String> notRenamed = agen.getClassesNotRenamed(); + assertArrayEquals(new String[] { "not/an/actual/ClassName" }, notRenamed.toArray()); + } +} diff --git a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/LogTest.java b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/LogTest.java new file mode 100644 index 0000000..3f13158 --- /dev/null +++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/LogTest.java @@ -0,0 +1,113 @@ +/* + * 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 com.android.tools.layoutlib.create; + +import static org.junit.Assert.*; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class LogTest { + + public static class MockLog extends Log { + StringBuilder mOut = new StringBuilder(); + StringBuilder mErr = new StringBuilder(); + + public String getOut() { + return mOut.toString(); + } + + public String getErr() { + return mErr.toString(); + } + + @Override + protected void outPrintln(String msg) { + mOut.append(msg); + mOut.append('\n'); + } + + @Override + protected void errPrintln(String msg) { + mErr.append(msg); + mErr.append('\n'); + } + } + + private MockLog mLog; + + @Before + public void setUp() throws Exception { + mLog = new MockLog(); + } + + @After + public void tearDown() throws Exception { + // pass + } + + @Test + public void testDebug() { + assertEquals("", mLog.getOut()); + assertEquals("", mLog.getErr()); + + mLog.setVerbose(false); + mLog.debug("Test %d", 42); + assertEquals("", mLog.getOut()); + + mLog.setVerbose(true); + mLog.debug("Test %d", 42); + + assertEquals("Test 42\n", mLog.getOut()); + assertEquals("", mLog.getErr()); + } + + @Test + public void testInfo() { + assertEquals("", mLog.getOut()); + assertEquals("", mLog.getErr()); + + mLog.info("Test %d", 43); + + assertEquals("Test 43\n", mLog.getOut()); + assertEquals("", mLog.getErr()); + } + + @Test + public void testError() { + assertEquals("", mLog.getOut()); + assertEquals("", mLog.getErr()); + + mLog.error("Test %d", 44); + + assertEquals("", mLog.getOut()); + assertEquals("Test 44\n", mLog.getErr()); + } + + @Test + public void testException() { + assertEquals("", mLog.getOut()); + assertEquals("", mLog.getErr()); + + Exception e = new Exception("My Exception"); + mLog.exception(e, "Test %d", 44); + + assertEquals("", mLog.getOut()); + assertTrue(mLog.getErr().startsWith("Test 44\njava.lang.Exception: My Exception")); + } +} diff --git a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/RenameClassAdapterTest.java b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/RenameClassAdapterTest.java new file mode 100644 index 0000000..90c6a9c --- /dev/null +++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/RenameClassAdapterTest.java @@ -0,0 +1,120 @@ +/* + * 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 com.android.tools.layoutlib.create; + +import static org.junit.Assert.*; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * + */ +public class RenameClassAdapterTest { + + private RenameClassAdapter mOuter; + private RenameClassAdapter mInner; + + @Before + public void setUp() throws Exception { + mOuter = new RenameClassAdapter(null, // cv + "com.pack.Old", + "org.blah.New"); + + mInner = new RenameClassAdapter(null, // cv + "com.pack.Old$Inner", + "org.blah.New$Inner"); + } + + @After + public void tearDown() throws Exception { + } + + /** + * Renames a type, e.g. "Lcom.package.My;" + * If the type doesn't need to be renamed, returns the input string as-is. + */ + @Test + public void testRenameTypeDesc() { + + // primitive types are left untouched + assertEquals("I", mOuter.renameTypeDesc("I")); + assertEquals("D", mOuter.renameTypeDesc("D")); + assertEquals("V", mOuter.renameTypeDesc("V")); + + // object types that need no renaming are left untouched + assertEquals("Lcom.package.MyClass;", mOuter.renameTypeDesc("Lcom.package.MyClass;")); + assertEquals("Lcom.package.MyClass;", mInner.renameTypeDesc("Lcom.package.MyClass;")); + + // object types that match the requirements + assertEquals("Lorg.blah.New;", mOuter.renameTypeDesc("Lcom.pack.Old;")); + assertEquals("Lorg.blah.New$Inner;", mInner.renameTypeDesc("Lcom.pack.Old$Inner;")); + // inner classes match the base type which is being renamed + assertEquals("Lorg.blah.New$Other;", mOuter.renameTypeDesc("Lcom.pack.Old$Other;")); + assertEquals("Lorg.blah.New$Other;", mInner.renameTypeDesc("Lcom.pack.Old$Other;")); + + // arrays + assertEquals("[Lorg.blah.New;", mOuter.renameTypeDesc("[Lcom.pack.Old;")); + assertEquals("[[Lorg.blah.New;", mOuter.renameTypeDesc("[[Lcom.pack.Old;")); + + assertEquals("[Lorg.blah.New;", mInner.renameTypeDesc("[Lcom.pack.Old;")); + assertEquals("[[Lorg.blah.New;", mInner.renameTypeDesc("[[Lcom.pack.Old;")); + } + + /** + * Renames an object type, e.g. "Lcom.package.MyClass;" or an array type that has an + * object element, e.g. "[Lcom.package.MyClass;" + * If the type doesn't need to be renamed, returns the internal name of the input type. + */ + @Test + public void testRenameType() { + // Skip. This is actually tested by testRenameTypeDesc above. + } + + /** + * Renames an internal type name, e.g. "com.package.MyClass". + * If the type doesn't need to be renamed, returns the input string as-is. + */ + @Test + public void testRenameInternalType() { + // a descriptor is not left untouched + assertEquals("Lorg.blah.New;", mOuter.renameInternalType("Lcom.pack.Old;")); + assertEquals("Lorg.blah.New$Inner;", mOuter.renameInternalType("Lcom.pack.Old$Inner;")); + + // an actual FQCN + assertEquals("org.blah.New", mOuter.renameInternalType("com.pack.Old")); + assertEquals("org.blah.New$Inner", mOuter.renameInternalType("com.pack.Old$Inner")); + + assertEquals("org.blah.New$Other", mInner.renameInternalType("com.pack.Old$Other")); + assertEquals("org.blah.New$Other", mInner.renameInternalType("com.pack.Old$Other")); + } + + /** + * Renames a method descriptor, i.e. applies renameType to all arguments and to the + * return value. + */ + @Test + public void testRenameMethodDesc() { + assertEquals("(IDLorg.blah.New;[Lorg.blah.New$Inner;)Lorg.blah.New$Other;", + mOuter.renameMethodDesc("(IDLcom.pack.Old;[Lcom.pack.Old$Inner;)Lcom.pack.Old$Other;")); + } + + + +} diff --git a/tools/layoutlib/create/tests/data/mock_android.jar b/tools/layoutlib/create/tests/data/mock_android.jar Binary files differnew file mode 100644 index 0000000..a7ea74f --- /dev/null +++ b/tools/layoutlib/create/tests/data/mock_android.jar diff --git a/tools/layoutlib/create/tests/data/mock_android.jardesc b/tools/layoutlib/create/tests/data/mock_android.jardesc new file mode 100644 index 0000000..95f7591 --- /dev/null +++ b/tools/layoutlib/create/tests/data/mock_android.jardesc @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="WINDOWS-1252" standalone="no"?> +<jardesc> + <jar path="C:/ralf/google/src/raphael-lapdroid/device/tools/layoutlib/create/tests/data/mock_android.jar"/> + <options buildIfNeeded="true" compress="true" descriptionLocation="/layoutlib_create/tests/data/mock_android.jardesc" exportErrors="true" exportWarnings="true" includeDirectoryEntries="false" overwrite="false" saveDescription="true" storeRefactorings="false" useSourceFolders="false"/> + <storedRefactorings deprecationInfo="true" structuralOnly="false"/> + <selectedProjects/> + <manifest generateManifest="true" manifestLocation="" manifestVersion="1.0" reuseManifest="false" saveManifest="false" usesManifest="true"> + <sealing sealJar="false"> + <packagesToSeal/> + <packagesToUnSeal/> + </sealing> + </manifest> + <selectedElements exportClassFiles="true" exportJavaFiles="false" exportOutputFolder="false"> + <javaElement handleIdentifier="=layoutlib_create/tests<mock_android.widget"/> + <javaElement handleIdentifier="=layoutlib_create/tests<mock_android.view"/> + <javaElement handleIdentifier="=layoutlib_create/tests<mock_android.dummy"/> + </selectedElements> +</jardesc> diff --git a/tools/layoutlib/create/tests/mock_android/dummy/InnerTest.java b/tools/layoutlib/create/tests/mock_android/dummy/InnerTest.java new file mode 100644 index 0000000..e355ead --- /dev/null +++ b/tools/layoutlib/create/tests/mock_android/dummy/InnerTest.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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 mock_android.dummy; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; + +public class InnerTest { + + private int mSomeField; + private MyStaticInnerClass mInnerInstance; + private MyIntEnum mTheIntEnum; + private MyGenerics1<int[][], InnerTest, MyIntEnum, float[]> mGeneric1; + + public class NotStaticInner2 extends NotStaticInner1 { + + } + + public class NotStaticInner1 { + + public void someThing() { + mSomeField = 2; + mInnerInstance = null; + } + + } + + private static class MyStaticInnerClass { + + } + + private static class DerivingClass extends InnerTest { + + } + + // enums are a kind of inner static class + public enum MyIntEnum { + VALUE0(0), + VALUE1(1), + VALUE2(2); + + MyIntEnum(int myInt) { + this.myInt = myInt; + } + final int myInt; + } + + public static class MyGenerics1<T, U, V, W> { + public MyGenerics1() { + int a = 1; + } + } + + public <X> void genericMethod1(X a, X[] a) { + } + + public <X, Y> void genericMethod2(X a, List<Y> b) { + } + + public <X, Y> void genericMethod3(X a, List<Y extends InnerTest> b) { + } + + public <T extends InnerTest> void genericMethod4(T[] a, Collection<T> b, Collection<?> c) { + Iterator<T> i = b.iterator(); + } + + public void someMethod(InnerTest self) { + mSomeField = self.mSomeField; + MyStaticInnerClass m = new MyStaticInnerClass(); + mInnerInstance = m; + mTheIntEnum = null; + mGeneric1 = new MyGenerics1(); + genericMethod(new DerivingClass[0], new ArrayList<DerivingClass>(), new ArrayList<InnerTest>()); + } +} diff --git a/tools/layoutlib/create/tests/mock_android/view/View.java b/tools/layoutlib/create/tests/mock_android/view/View.java new file mode 100644 index 0000000..a80a98d --- /dev/null +++ b/tools/layoutlib/create/tests/mock_android/view/View.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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 mock_android.view; + +public class View { + +} diff --git a/tools/layoutlib/create/tests/mock_android/view/ViewGroup.java b/tools/layoutlib/create/tests/mock_android/view/ViewGroup.java new file mode 100644 index 0000000..466470f --- /dev/null +++ b/tools/layoutlib/create/tests/mock_android/view/ViewGroup.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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 mock_android.view; + +public class ViewGroup extends View { + + public class MarginLayoutParams extends LayoutParams { + + } + + public class LayoutParams { + + } + +} diff --git a/tools/layoutlib/create/tests/mock_android/widget/LinearLayout.java b/tools/layoutlib/create/tests/mock_android/widget/LinearLayout.java new file mode 100644 index 0000000..3870a63 --- /dev/null +++ b/tools/layoutlib/create/tests/mock_android/widget/LinearLayout.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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 mock_android.widget; + +import mock_android.view.ViewGroup; + +public class LinearLayout extends ViewGroup { + + public class LayoutParams extends mock_android.view.ViewGroup.LayoutParams { + + } + +} diff --git a/tools/layoutlib/create/tests/mock_android/widget/TableLayout.java b/tools/layoutlib/create/tests/mock_android/widget/TableLayout.java new file mode 100644 index 0000000..e455e7d --- /dev/null +++ b/tools/layoutlib/create/tests/mock_android/widget/TableLayout.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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 mock_android.widget; + +import mock_android.view.ViewGroup; + +public class TableLayout extends ViewGroup { + + public class LayoutParams extends MarginLayoutParams { + + } + +} |