diff options
Diffstat (limited to 'tools')
30 files changed, 1265 insertions, 200 deletions
diff --git a/tools/layoutlib/.idea/encodings.xml b/tools/layoutlib/.idea/encodings.xml index e206d70..f758959 100644 --- a/tools/layoutlib/.idea/encodings.xml +++ b/tools/layoutlib/.idea/encodings.xml @@ -1,5 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> <project version="4"> - <component name="Encoding" useUTFGuessing="true" native2AsciiForPropertiesFiles="false" /> -</project> - + <component name="Encoding" useUTFGuessing="true" native2AsciiForPropertiesFiles="false"> + <file url="PROJECT" charset="UTF-8" /> + </component> +</project>
\ No newline at end of file diff --git a/tools/layoutlib/bridge/src/android/animation/AnimatorInflater_Delegate.java b/tools/layoutlib/bridge/src/android/animation/AnimatorInflater_Delegate.java deleted file mode 100644 index 4475fa4..0000000 --- a/tools/layoutlib/bridge/src/android/animation/AnimatorInflater_Delegate.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2014 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.animation; - -import com.android.tools.layoutlib.annotations.LayoutlibDelegate; - -import android.content.Context; -import android.content.res.Resources; -import android.content.res.Resources.NotFoundException; -import android.content.res.Resources.Theme; -import android.util.AttributeSet; - -/** - * Delegate providing alternate implementation to static methods in {@link AnimatorInflater}. - */ -public class AnimatorInflater_Delegate { - - @LayoutlibDelegate - /*package*/ static Animator loadAnimator(Context context, int id) - throws NotFoundException { - return loadAnimator(context.getResources(), context.getTheme(), id); - } - - @LayoutlibDelegate - /*package*/ static Animator loadAnimator(Resources resources, Theme theme, int id) - throws NotFoundException { - return loadAnimator(resources, theme, id, 1); - } - - @LayoutlibDelegate - /*package*/ static Animator loadAnimator(Resources resources, Theme theme, int id, - float pathErrorScale) throws NotFoundException { - // This is a temporary fix to http://b.android.com/77865. This skips loading the - // animation altogether. - // TODO: Remove this override when Path.approximate() is supported. - return new FakeAnimator(); - } - - @LayoutlibDelegate - /*package*/ static ValueAnimator loadAnimator(Resources res, Theme theme, - AttributeSet attrs, ValueAnimator anim, float pathErrorScale) - throws NotFoundException { - return AnimatorInflater.loadAnimator_Original(res, theme, attrs, anim, pathErrorScale); - } -} diff --git a/tools/layoutlib/bridge/src/android/content/res/BridgeResources.java b/tools/layoutlib/bridge/src/android/content/res/BridgeResources.java index 163fbcb..0e39243 100644 --- a/tools/layoutlib/bridge/src/android/content/res/BridgeResources.java +++ b/tools/layoutlib/bridge/src/android/content/res/BridgeResources.java @@ -18,6 +18,7 @@ package android.content.res; import com.android.SdkConstants; import com.android.ide.common.rendering.api.ArrayResourceValue; +import com.android.ide.common.rendering.api.DensityBasedResourceValue; import com.android.ide.common.rendering.api.LayoutLog; import com.android.ide.common.rendering.api.LayoutlibCallback; import com.android.ide.common.rendering.api.ResourceValue; @@ -48,9 +49,6 @@ import java.io.FileNotFoundException; import java.io.InputStream; import java.util.Iterator; -/** - * - */ public final class BridgeResources extends Resources { private BridgeContext mContext; @@ -278,7 +276,7 @@ public final class BridgeResources extends Resources { * always Strings. The ideal signature for the method should be <T super String>, but java * generics don't support it. */ - private <T extends CharSequence> T[] fillValues(ArrayResourceValue resValue, T[] values) { + <T extends CharSequence> T[] fillValues(ArrayResourceValue resValue, T[] values) { int i = 0; for (Iterator<String> iterator = resValue.iterator(); iterator.hasNext(); i++) { @SuppressWarnings("unchecked") @@ -404,7 +402,7 @@ public final class BridgeResources extends Resources { if (xml.isFile()) { // we need to create a pull parser around the layout XML file, and then // give that to our XmlBlockParser - parser = ParserFactory.create(xml); + parser = ParserFactory.create(xml, true); } } @@ -664,13 +662,18 @@ public final class BridgeResources extends Resources { Pair<String, ResourceValue> value = getResourceValue(id, mPlatformResourceFlag); if (value != null) { - String v = value.getSecond().getValue(); + ResourceValue resVal = value.getSecond(); + String v = resVal.getValue(); if (v != null) { if (ResourceHelper.parseFloatAttribute(value.getFirst(), v, outValue, false /*requireUnit*/)) { return; } + if (resVal instanceof DensityBasedResourceValue) { + outValue.density = + ((DensityBasedResourceValue) resVal).getResourceDensity().getDpiValue(); + } // else it's a string outValue.type = TypedValue.TYPE_STRING; diff --git a/tools/layoutlib/bridge/src/android/content/res/BridgeTypedArray.java b/tools/layoutlib/bridge/src/android/content/res/BridgeTypedArray.java index 6a61090..31dd3d9 100644 --- a/tools/layoutlib/bridge/src/android/content/res/BridgeTypedArray.java +++ b/tools/layoutlib/bridge/src/android/content/res/BridgeTypedArray.java @@ -16,6 +16,7 @@ package android.content.res; +import com.android.ide.common.rendering.api.ArrayResourceValue; import com.android.ide.common.rendering.api.AttrResourceValue; import com.android.ide.common.rendering.api.LayoutLog; import com.android.ide.common.rendering.api.RenderResources; @@ -33,6 +34,7 @@ import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import android.annotation.Nullable; +import android.content.res.Resources.NotFoundException; import android.content.res.Resources.Theme; import android.graphics.drawable.Drawable; import android.util.DisplayMetrics; @@ -740,12 +742,20 @@ public final class BridgeTypedArray extends TypedArray { */ @Override public CharSequence[] getTextArray(int index) { - String value = getString(index); - if (value != null) { - return new CharSequence[] { value }; + if (!hasValue(index)) { + return null; } - - return null; + ResourceValue resVal = mResourceData[index]; + if (resVal instanceof ArrayResourceValue) { + ArrayResourceValue array = (ArrayResourceValue) resVal; + int count = array.getElementCount(); + return count >= 0 ? mBridgeResources.fillValues(array, new CharSequence[count]) : null; + } + int id = getResourceId(index, 0); + String resIdMessage = id > 0 ? " (resource id 0x" + Integer.toHexString(id) + ')' : ""; + throw new NotFoundException( + String.format("%1$s in %2$s%3$s is not a valid array resource.", + resVal.getValue(), mNames[index], resIdMessage)); } @Override diff --git a/tools/layoutlib/bridge/src/android/graphics/BitmapFactory_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/BitmapFactory_Delegate.java index d858953..60514b6 100644 --- a/tools/layoutlib/bridge/src/android/graphics/BitmapFactory_Delegate.java +++ b/tools/layoutlib/bridge/src/android/graphics/BitmapFactory_Delegate.java @@ -59,6 +59,7 @@ import java.util.Set; if (opts.inPremultiplied) { bitmapCreateFlags.add(BitmapCreateFlags.PREMULTIPLIED); } + opts.inScaled = false; } try { diff --git a/tools/layoutlib/bridge/src/android/graphics/Canvas_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/Canvas_Delegate.java index f8b3739..64cd503 100644 --- a/tools/layoutlib/bridge/src/android/graphics/Canvas_Delegate.java +++ b/tools/layoutlib/bridge/src/android/graphics/Canvas_Delegate.java @@ -35,6 +35,8 @@ import java.awt.RenderingHints; import java.awt.Shape; import java.awt.geom.AffineTransform; import java.awt.geom.Arc2D; +import java.awt.geom.Path2D; +import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; @@ -707,6 +709,12 @@ public final class Canvas_Delegate { @Override public void draw(Graphics2D graphics, Paint_Delegate paintDelegate) { Shape shape = pathDelegate.getJavaShape(); + Rectangle2D bounds = shape.getBounds2D(); + if (bounds.isEmpty()) { + // Apple JRE 1.6 doesn't like drawing empty shapes. + // http://b.android.com/178278 + return; + } int style = paintDelegate.getStyle(); if (style == Paint.Style.FILL.nativeInt || diff --git a/tools/layoutlib/bridge/src/android/graphics/FontFamily_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/FontFamily_Delegate.java index 857e6d0..c7b24bc 100644 --- a/tools/layoutlib/bridge/src/android/graphics/FontFamily_Delegate.java +++ b/tools/layoutlib/bridge/src/android/graphics/FontFamily_Delegate.java @@ -178,7 +178,9 @@ public class FontFamily_Delegate { desiredStyle.mIsItalic = isItalic; FontInfo bestFont = null; int bestMatch = Integer.MAX_VALUE; - for (FontInfo font : mFonts) { + //noinspection ForLoopReplaceableByForEach (avoid iterator instantiation) + for (int i = 0, n = mFonts.size(); i < n; i++) { + FontInfo font = mFonts.get(i); int match = computeMatch(font, desiredStyle); if (match < bestMatch) { bestMatch = match; @@ -415,7 +417,9 @@ public class FontFamily_Delegate { boolean isItalic = fontInfo.mIsItalic; // The list is usually just two fonts big. So iterating over all isn't as bad as it looks. // It's biggest for roboto where the size is 12. - for (FontInfo font : mFonts) { + //noinspection ForLoopReplaceableByForEach (avoid iterator instantiation) + for (int i = 0, n = mFonts.size(); i < n; i++) { + FontInfo font = mFonts.get(i); if (font.mWeight == weight && font.mIsItalic == isItalic) { return false; } diff --git a/tools/layoutlib/bridge/src/android/graphics/Paint_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/Paint_Delegate.java index 65b65ec..a545283 100644 --- a/tools/layoutlib/bridge/src/android/graphics/Paint_Delegate.java +++ b/tools/layoutlib/bridge/src/android/graphics/Paint_Delegate.java @@ -480,8 +480,10 @@ public class Paint_Delegate { return; } - delegate.mTextSize = textSize; - delegate.updateFontObject(); + if (delegate.mTextSize != textSize) { + delegate.mTextSize = textSize; + delegate.updateFontObject(); + } } @LayoutlibDelegate @@ -503,8 +505,10 @@ public class Paint_Delegate { return; } - delegate.mTextScaleX = scaleX; - delegate.updateFontObject(); + if (delegate.mTextScaleX != scaleX) { + delegate.mTextScaleX = scaleX; + delegate.updateFontObject(); + } } @LayoutlibDelegate @@ -526,8 +530,10 @@ public class Paint_Delegate { return; } - delegate.mTextSkewX = skewX; - delegate.updateFontObject(); + if (delegate.mTextSkewX != skewX) { + delegate.mTextSkewX = skewX; + delegate.updateFontObject(); + } } @LayoutlibDelegate @@ -897,9 +903,12 @@ public class Paint_Delegate { return 0; } - delegate.mTypeface = Typeface_Delegate.getDelegate(typeface); - delegate.mNativeTypeface = typeface; - delegate.updateFontObject(); + Typeface_Delegate typefaceDelegate = Typeface_Delegate.getDelegate(typeface); + if (delegate.mTypeface != typefaceDelegate || delegate.mNativeTypeface != typeface) { + delegate.mTypeface = Typeface_Delegate.getDelegate(typeface); + delegate.mNativeTypeface = typeface; + delegate.updateFontObject(); + } return typeface; } @@ -1214,13 +1223,31 @@ public class Paint_Delegate { mCap = paint.mCap; mJoin = paint.mJoin; mTextAlign = paint.mTextAlign; - mTypeface = paint.mTypeface; - mNativeTypeface = paint.mNativeTypeface; + + boolean needsFontUpdate = false; + if (mTypeface != paint.mTypeface || mNativeTypeface != paint.mNativeTypeface) { + mTypeface = paint.mTypeface; + mNativeTypeface = paint.mNativeTypeface; + needsFontUpdate = true; + } + + if (mTextSize != paint.mTextSize) { + mTextSize = paint.mTextSize; + needsFontUpdate = true; + } + + if (mTextScaleX != paint.mTextScaleX) { + mTextScaleX = paint.mTextScaleX; + needsFontUpdate = true; + } + + if (mTextSkewX != paint.mTextSkewX) { + mTextSkewX = paint.mTextSkewX; + needsFontUpdate = true; + } + mStrokeWidth = paint.mStrokeWidth; mStrokeMiter = paint.mStrokeMiter; - mTextSize = paint.mTextSize; - mTextScaleX = paint.mTextScaleX; - mTextSkewX = paint.mTextSkewX; mXfermode = paint.mXfermode; mColorFilter = paint.mColorFilter; mShader = paint.mShader; @@ -1228,7 +1255,10 @@ public class Paint_Delegate { mMaskFilter = paint.mMaskFilter; mRasterizer = paint.mRasterizer; mHintingMode = paint.mHintingMode; - updateFontObject(); + + if (needsFontUpdate) { + updateFontObject(); + } } private void reset() { @@ -1264,10 +1294,18 @@ public class Paint_Delegate { // Get the fonts from the TypeFace object. List<Font> fonts = mTypeface.getFonts(mFontVariant); + if (fonts.isEmpty()) { + mFonts = Collections.emptyList(); + return; + } + // create new font objects as well as FontMetrics, based on the current text size // and skew info. - ArrayList<FontInfo> infoList = new ArrayList<FontInfo>(fonts.size()); - for (Font font : fonts) { + int nFonts = fonts.size(); + ArrayList<FontInfo> infoList = new ArrayList<FontInfo>(nFonts); + //noinspection ForLoopReplaceableByForEach (avoid iterator instantiation) + for (int i = 0; i < nFonts; i++) { + Font font = fonts.get(i); if (font == null) { // If the font is null, add null to infoList. When rendering the text, if this // null is reached, a warning will be logged. diff --git a/tools/layoutlib/bridge/src/android/graphics/PathMeasure_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/PathMeasure_Delegate.java new file mode 100644 index 0000000..dd2978f --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/PathMeasure_Delegate.java @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2015 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.ide.common.rendering.api.LayoutLog; +import com.android.layoutlib.bridge.Bridge; +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import java.awt.geom.PathIterator; +import java.awt.geom.Point2D; + +/** + * Delegate implementing the native methods of {@link android.graphics.PathMeasure} + * <p/> + * Through the layoutlib_create tool, the original native methods of PathMeasure have been + * replaced by + * calls to methods of the same name in this delegate class. + * <p/> + * This class behaves like the original native implementation, but in Java, keeping previously + * native data into its own objects and mapping them to int that are sent back and forth between it + * and the original PathMeasure class. + * + * @see DelegateManager + */ +public final class PathMeasure_Delegate { + // ---- delegate manager ---- + private static final DelegateManager<PathMeasure_Delegate> sManager = + new DelegateManager<PathMeasure_Delegate>(PathMeasure_Delegate.class); + + // ---- delegate data ---- + // This governs how accurate the approximation of the Path is. + private static final float PRECISION = 0.002f; + + /** + * Array containing the path points components. There are three components for each point: + * <ul> + * <li>Fraction along the length of the path that the point resides</li> + * <li>The x coordinate of the point</li> + * <li>The y coordinate of the point</li> + * </ul> + */ + private float mPathPoints[]; + private long mNativePath; + + private PathMeasure_Delegate(long native_path, boolean forceClosed) { + mNativePath = native_path; + if (forceClosed && mNativePath != 0) { + // Copy the path and call close + mNativePath = Path_Delegate.init2(native_path); + Path_Delegate.native_close(mNativePath); + } + + mPathPoints = + mNativePath != 0 ? Path_Delegate.native_approximate(mNativePath, PRECISION) : null; + } + + @LayoutlibDelegate + /*package*/ static long native_create(long native_path, boolean forceClosed) { + return sManager.addNewDelegate(new PathMeasure_Delegate(native_path, forceClosed)); + } + + @LayoutlibDelegate + /*package*/ static void native_destroy(long native_instance) { + sManager.removeJavaReferenceFor(native_instance); + } + + @LayoutlibDelegate + /*package*/ static boolean native_getPosTan(long native_instance, float distance, float pos[], + float tan[]) { + Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED, + "PathMeasure.getPostTan is not supported.", null, null); + return false; + } + + @LayoutlibDelegate + /*package*/ static boolean native_getMatrix(long native_instance, float distance, long + native_matrix, int flags) { + Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED, + "PathMeasure.getMatrix is not supported.", null, null); + return false; + } + + @LayoutlibDelegate + /*package*/ static boolean native_nextContour(long native_instance) { + Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED, + "PathMeasure.nextContour is not supported.", null, null); + return false; + } + + @LayoutlibDelegate + /*package*/ static void native_setPath(long native_instance, long native_path, boolean + forceClosed) { + PathMeasure_Delegate pathMeasure = sManager.getDelegate(native_instance); + assert pathMeasure != null; + + if (forceClosed && native_path != 0) { + // Copy the path and call close + native_path = Path_Delegate.init2(native_path); + Path_Delegate.native_close(native_path); + } + pathMeasure.mNativePath = native_path; + pathMeasure.mPathPoints = Path_Delegate.native_approximate(native_path, PRECISION); + } + + @LayoutlibDelegate + /*package*/ static float native_getLength(long native_instance) { + PathMeasure_Delegate pathMeasure = sManager.getDelegate(native_instance); + assert pathMeasure != null; + + if (pathMeasure.mPathPoints == null) { + return 0; + } + + float length = 0; + int nPoints = pathMeasure.mPathPoints.length / 3; + for (int i = 1; i < nPoints; i++) { + length += Point2D.distance( + pathMeasure.mPathPoints[(i - 1) * 3 + 1], + pathMeasure.mPathPoints[(i - 1) * 3 + 2], + pathMeasure.mPathPoints[i*3 + 1], + pathMeasure.mPathPoints[i*3 + 2]); + } + + return length; + } + + @LayoutlibDelegate + /*package*/ static boolean native_isClosed(long native_instance) { + PathMeasure_Delegate pathMeasure = sManager.getDelegate(native_instance); + assert pathMeasure != null; + + Path_Delegate path = Path_Delegate.getDelegate(pathMeasure.mNativePath); + if (path == null) { + return false; + } + + PathIterator pathIterator = path.getJavaShape().getPathIterator(null); + + int type = 0; + float segment[] = new float[6]; + while (!pathIterator.isDone()) { + type = pathIterator.currentSegment(segment); + pathIterator.next(); + } + + // A path is a closed path if the last element is SEG_CLOSE + return type == PathIterator.SEG_CLOSE; + } + + @LayoutlibDelegate + /*package*/ static boolean native_getSegment(long native_instance, float startD, float stopD, + long native_dst_path, boolean startWithMoveTo) { + if (startD < 0) { + startD = 0; + } + + if (startD >= stopD) { + return false; + } + + PathMeasure_Delegate pathMeasure = sManager.getDelegate(native_instance); + assert pathMeasure != null; + + if (pathMeasure.mPathPoints == null) { + return false; + } + + float accLength = 0; + boolean isZeroLength = true; // Whether the output has zero length or not + int nPoints = pathMeasure.mPathPoints.length / 3; + for (int i = 0; i < nPoints; i++) { + float x = pathMeasure.mPathPoints[i * 3 + 1]; + float y = pathMeasure.mPathPoints[i * 3 + 2]; + if (accLength >= startD && accLength <= stopD) { + if (startWithMoveTo) { + startWithMoveTo = false; + Path_Delegate.native_moveTo(native_dst_path, x, y); + } else { + isZeroLength = false; + Path_Delegate.native_lineTo(native_dst_path, x, y); + } + } + + if (i > 0) { + accLength += Point2D.distance( + pathMeasure.mPathPoints[(i - 1) * 3 + 1], + pathMeasure.mPathPoints[(i - 1) * 3 + 2], + pathMeasure.mPathPoints[i * 3 + 1], + pathMeasure.mPathPoints[i * 3 + 2]); + } + } + + return !isZeroLength; + } +} diff --git a/tools/layoutlib/bridge/src/android/graphics/Path_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/Path_Delegate.java index 3c9a062..a2a53fe 100644 --- a/tools/layoutlib/bridge/src/android/graphics/Path_Delegate.java +++ b/tools/layoutlib/bridge/src/android/graphics/Path_Delegate.java @@ -36,6 +36,7 @@ import java.awt.geom.PathIterator; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.geom.RoundRectangle2D; +import java.util.ArrayList; /** * Delegate implementing the native methods of android.graphics.Path @@ -173,11 +174,8 @@ public final class Path_Delegate { @LayoutlibDelegate /*package*/ static boolean native_isEmpty(long nPath) { Path_Delegate pathDelegate = sManager.getDelegate(nPath); - if (pathDelegate == null) { - return true; - } + return pathDelegate == null || pathDelegate.isEmpty(); - return pathDelegate.isEmpty(); } @LayoutlibDelegate @@ -488,54 +486,44 @@ public final class Path_Delegate { @LayoutlibDelegate /*package*/ static float[] native_approximate(long nPath, float error) { - Bridge.getLog().warning(LayoutLog.TAG_UNSUPPORTED, "Path.approximate() not fully supported", - null); Path_Delegate pathDelegate = sManager.getDelegate(nPath); if (pathDelegate == null) { return null; } - PathIterator pathIterator = pathDelegate.mPath.getPathIterator(null); - float[] tmp = new float[6]; - float[] coords = new float[6]; - boolean isFirstPoint = true; - while (!pathIterator.isDone()) { - int type = pathIterator.currentSegment(tmp); - switch (type) { - case PathIterator.SEG_MOVETO: - case PathIterator.SEG_LINETO: - store(tmp, coords, 1, isFirstPoint); - break; - case PathIterator.SEG_QUADTO: - store(tmp, coords, 2, isFirstPoint); - break; - case PathIterator.SEG_CUBICTO: - store(tmp, coords, 3, isFirstPoint); - break; - case PathIterator.SEG_CLOSE: - // No points returned. + // Get a FlatteningIterator + PathIterator iterator = pathDelegate.getJavaShape().getPathIterator(null, error); + + float segment[] = new float[6]; + float totalLength = 0; + ArrayList<Point2D.Float> points = new ArrayList<Point2D.Float>(); + Point2D.Float previousPoint = null; + while (!iterator.isDone()) { + int type = iterator.currentSegment(segment); + Point2D.Float currentPoint = new Point2D.Float(segment[0], segment[1]); + // MoveTo shouldn't affect the length + if (previousPoint != null && type != PathIterator.SEG_MOVETO) { + totalLength += currentPoint.distance(previousPoint); } - isFirstPoint = false; - pathIterator.next(); + previousPoint = currentPoint; + points.add(currentPoint); + iterator.next(); } - if (isFirstPoint) { - // No points found - return new float[0]; - } else { - return coords; - } - } - private static void store(float[] src, float[] dst, int count, boolean isFirst) { - if (isFirst) { - dst[0] = 0; // fraction - dst[1] = src[0]; // abscissa - dst[2] = src[1]; // ordinate - } - if (count > 1 || !isFirst) { - dst[3] = 1; - dst[4] = src[2 * count - 2]; - dst[5] = src[2 * count - 1]; + int nPoints = points.size(); + float[] result = new float[nPoints * 3]; + previousPoint = null; + for (int i = 0; i < nPoints; i++) { + Point2D.Float point = points.get(i); + float distance = previousPoint != null ? (float) previousPoint.distance(point) : .0f; + result[i * 3] = distance / totalLength; + result[i * 3 + 1] = point.x; + result[i * 3 + 2] = point.y; + + totalLength += distance; + previousPoint = point; } + + return result; } // ---- Private helper methods ---- @@ -735,6 +723,9 @@ public final class Path_Delegate { */ private void cubicTo(float x1, float y1, float x2, float y2, float x3, float y3) { + if (isEmpty()) { + mPath.moveTo(0, 0); + } mPath.curveTo(x1, y1, x2, y2, mLastX = x3, mLastY = y3); } diff --git a/tools/layoutlib/bridge/src/android/graphics/drawable/GradientDrawable_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/drawable/GradientDrawable_Delegate.java new file mode 100644 index 0000000..a3ad2aa --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/drawable/GradientDrawable_Delegate.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2015 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.drawable; + +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import android.graphics.Path; +import android.graphics.drawable.GradientDrawable.GradientState; + +import java.lang.reflect.Field; + +/** + * Delegate implementing the native methods of {@link GradientDrawable} + * + * Through the layoutlib_create tool, the original native methods of GradientDrawable have been + * replaced by calls to methods of the same name in this delegate class. + */ +public class GradientDrawable_Delegate { + + /** + * The ring can be built either by drawing full circles, or by drawing arcs in case the + * circle isn't complete. LayoutLib cannot handle drawing full circles (requires path + * subtraction). So, if we need to draw full circles, we switch to drawing 99% circle. + */ + @LayoutlibDelegate + /*package*/ static Path buildRing(GradientDrawable thisDrawable, GradientState st) { + boolean useLevel = st.mUseLevelForShape; + int level = thisDrawable.getLevel(); + // 10000 is the max level. See android.graphics.drawable.Drawable#getLevel() + float sweep = useLevel ? (360.0f * level / 10000.0f) : 360f; + Field mLevel = null; + if (sweep >= 360 || sweep <= -360) { + st.mUseLevelForShape = true; + // Use reflection to set the value of the field to prevent setting the drawable to + // dirty again. + try { + mLevel = Drawable.class.getDeclaredField("mLevel"); + mLevel.setAccessible(true); + mLevel.setInt(thisDrawable, 9999); // set to one less than max. + } catch (NoSuchFieldException e) { + // The field has been removed in a recent framework change. Fall back to old + // buggy behaviour. + } catch (IllegalAccessException e) { + // We've already set the field to be accessible. + assert false; + } + } + Path path = thisDrawable.buildRing_Original(st); + st.mUseLevelForShape = useLevel; + if (mLevel != null) { + try { + mLevel.setInt(thisDrawable, level); + } catch (IllegalAccessException e) { + assert false; + } + } + return path; + } +} diff --git a/tools/layoutlib/bridge/src/android/preference/Preference_Delegate.java b/tools/layoutlib/bridge/src/android/preference/Preference_Delegate.java index 49ee642..2e44a77 100644 --- a/tools/layoutlib/bridge/src/android/preference/Preference_Delegate.java +++ b/tools/layoutlib/bridge/src/android/preference/Preference_Delegate.java @@ -29,9 +29,6 @@ import android.view.View; import android.view.ViewGroup; import android.widget.ListView; -import java.util.HashMap; -import java.util.Map; - /** * Delegate that provides implementation for native methods in {@link Preference} * <p/> @@ -59,9 +56,9 @@ public class Preference_Delegate { */ public static View inflatePreference(Context context, XmlPullParser parser, ViewGroup root) { PreferenceManager pm = new PreferenceManager(context); - PreferenceScreen ps = pm.getPreferenceScreen(); PreferenceInflater inflater = new BridgePreferenceInflater(context, pm); - ps = (PreferenceScreen) inflater.inflate(parser, ps, true); + PreferenceScreen ps = (PreferenceScreen) inflater.inflate(parser, null, true); + pm.setPreferences(ps); ListView preferenceView = createContainerView(context, root); ps.bind(preferenceView); return preferenceView; diff --git a/tools/layoutlib/bridge/src/android/text/Hyphenator_Delegate.java b/tools/layoutlib/bridge/src/android/text/Hyphenator_Delegate.java index 5a59597..44ce731 100644 --- a/tools/layoutlib/bridge/src/android/text/Hyphenator_Delegate.java +++ b/tools/layoutlib/bridge/src/android/text/Hyphenator_Delegate.java @@ -20,9 +20,10 @@ import com.android.layoutlib.bridge.impl.DelegateManager; import com.android.tools.layoutlib.annotations.LayoutlibDelegate; import java.io.File; +import java.nio.ByteBuffer; /** - * Delegate that overrides implementation for certain methods in {@link android.text.StaticLayout} + * Delegate that overrides implementation for certain methods in {@link android.text.Hyphenator} * <p/> * Through the layoutlib_create tool, selected methods of StaticLayout have been replaced * by calls to methods of the same name in this delegate class. @@ -38,7 +39,7 @@ public class Hyphenator_Delegate { return null; } - /*package*/ static long loadHyphenator(String patternData) { + /*package*/ static long loadHyphenator(ByteBuffer buf, int offset) { return sDelegateManager.addNewDelegate(new Hyphenator_Delegate()); } } diff --git a/tools/layoutlib/bridge/src/android/text/StaticLayout_Delegate.java b/tools/layoutlib/bridge/src/android/text/StaticLayout_Delegate.java index 1b0ba51..65c0a07 100644 --- a/tools/layoutlib/bridge/src/android/text/StaticLayout_Delegate.java +++ b/tools/layoutlib/bridge/src/android/text/StaticLayout_Delegate.java @@ -13,6 +13,7 @@ import android.icu.util.ULocale; import android.text.Primitive.PrimitiveType; import android.text.StaticLayout.LineBreaks; +import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -52,8 +53,8 @@ public class StaticLayout_Delegate { } @LayoutlibDelegate - /*package*/ static long nLoadHyphenator(String patternData) { - return Hyphenator_Delegate.loadHyphenator(patternData); + /*package*/ static long nLoadHyphenator(ByteBuffer buf, int offset) { + return Hyphenator_Delegate.loadHyphenator(buf, offset); } @LayoutlibDelegate diff --git a/tools/layoutlib/bridge/src/android/view/BridgeInflater.java b/tools/layoutlib/bridge/src/android/view/BridgeInflater.java index 1e33e3a..723e827 100644 --- a/tools/layoutlib/bridge/src/android/view/BridgeInflater.java +++ b/tools/layoutlib/bridge/src/android/view/BridgeInflater.java @@ -23,6 +23,7 @@ import com.android.ide.common.rendering.api.ResourceReference; import com.android.ide.common.rendering.api.ResourceValue; import com.android.layoutlib.bridge.Bridge; import com.android.layoutlib.bridge.BridgeConstants; +import com.android.layoutlib.bridge.MockView; import com.android.layoutlib.bridge.android.BridgeContext; import com.android.layoutlib.bridge.android.BridgeXmlBlockParser; import com.android.layoutlib.bridge.android.support.DrawerLayoutUtil; @@ -36,6 +37,7 @@ import org.xmlpull.v1.XmlPullParser; import android.annotation.NonNull; import android.content.Context; +import android.content.res.TypedArray; import android.util.AttributeSet; import java.io.File; @@ -54,6 +56,9 @@ public final class BridgeInflater extends LayoutInflater { private ResourceReference mResourceReference; private Map<View, String> mOpenDrawerLayouts; + // Keep in sync with the same value in LayoutInflater. + private static final int[] ATTRS_THEME = new int[] {com.android.internal.R.attr.theme }; + /** * List of class prefixes which are tried first by default. * <p/> @@ -122,6 +127,9 @@ public final class BridgeInflater extends LayoutInflater { if (view == null) { view = loadCustomView(name, attrs); } + } catch (InflateException e) { + // Don't catch the InflateException below as that results in hiding the real cause. + throw e; } catch (Exception e) { // Wrap the real exception in a ClassNotFoundException, so that the calling method // can deal with it. @@ -135,24 +143,45 @@ public final class BridgeInflater extends LayoutInflater { @Override public View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, - boolean ignoreThemeAttrs) { + boolean ignoreThemeAttr) { View view; try { - view = super.createViewFromTag(parent, name, context, attrs, ignoreThemeAttrs); + view = super.createViewFromTag(parent, name, context, attrs, ignoreThemeAttr); } 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)) { - exception.initCause(e2); - } else { - exception.initCause(e); + // Creation of ContextThemeWrapper code is same as in the super method. + // Apply a theme wrapper, if allowed and one is specified. + if (!ignoreThemeAttr) { + final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME); + final int themeResId = ta.getResourceId(0, 0); + if (themeResId != 0) { + context = new ContextThemeWrapper(context, themeResId); + } + ta.recycle(); + } + if (!(e.getCause() instanceof ClassNotFoundException)) { + // There is some unknown inflation exception in inflating a View that was found. + view = new MockView(context, attrs); + ((MockView) view).setText(name); + Bridge.getLog().error(LayoutLog.TAG_BROKEN, e.getMessage(), e, null); + } else { + final Object lastContext = mConstructorArgs[0]; + mConstructorArgs[0] = context; + // 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)) { + exception.initCause(e2); + } else { + exception.initCause(e); + } + throw exception; + } finally { + mConstructorArgs[0] = lastContext; } - throw exception; } } @@ -188,7 +217,7 @@ public final class BridgeInflater extends LayoutInflater { File f = new File(value.getValue()); if (f.isFile()) { try { - XmlPullParser parser = ParserFactory.create(f); + XmlPullParser parser = ParserFactory.create(f, true); BridgeXmlBlockParser bridgeParser = new BridgeXmlBlockParser( parser, bridgeContext, value.isFramework()); diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/MockView.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/MockView.java index 44a9aad..d392f21 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/MockView.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/MockView.java @@ -17,39 +17,90 @@ package com.android.layoutlib.bridge; import android.content.Context; -import android.graphics.Canvas; import android.util.AttributeSet; import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; 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). + * <p/> + * FrameLayout with a single TextView. Doesn't allow adding any other views to itself. */ -public class MockView extends TextView { +public class MockView extends FrameLayout { + + private final TextView mView; + + public MockView(Context context) { + this(context, null); + } public MockView(Context context, AttributeSet attrs) { this(context, attrs, 0); } - public MockView(Context context, AttributeSet attrs, int defStyle) { - this(context, attrs, defStyle, 0); + public MockView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); } public MockView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); - - setText(this.getClass().getSimpleName()); - setTextColor(0xFF000000); + mView = new TextView(context, attrs); + mView.setTextColor(0xFF000000); setGravity(Gravity.CENTER); + setText(getClass().getSimpleName()); + addView(mView); + setBackgroundColor(0xFF7F7F7F); + } + + // Only allow adding one TextView. + @Override + public void addView(View child) { + if (child == mView) { + super.addView(child); + } + } + + @Override + public void addView(View child, int index) { + if (child == mView) { + super.addView(child, index); + } } @Override - public void onDraw(Canvas canvas) { - canvas.drawARGB(0xFF, 0x7F, 0x7F, 0x7F); + public void addView(View child, int width, int height) { + if (child == mView) { + super.addView(child, width, height); + } + } + + @Override + public void addView(View child, ViewGroup.LayoutParams params) { + if (child == mView) { + super.addView(child, params); + } + } + + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) { + if (child == mView) { + super.addView(child, index, params); + } + } + + // The following methods are called by the IDE via reflection, and should be considered part + // of the API. + // Historically, MockView used to be a textView and had these methods. Now, we simply delegate + // them to the contained textView. + + public void setText(CharSequence text) { + mView.setText(text); + } - super.onDraw(canvas); + public void setGravity(int gravity) { + mView.setGravity(gravity); } } diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java index 689e359..f2d214c 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java @@ -409,7 +409,7 @@ public final class BridgeContext extends Context { pushParser(blockParser); return Pair.of( mBridgeInflater.inflate(blockParser, parent, attachToRoot), - true); + Boolean.TRUE); } finally { popParser(); } @@ -436,7 +436,7 @@ public final class BridgeContext extends Context { // we need to create a pull parser around the layout XML file, and then // give that to our XmlBlockParser try { - XmlPullParser parser = ParserFactory.create(xml); + XmlPullParser parser = ParserFactory.create(xml, true); // set the resource ref to have correct view cookies mBridgeInflater.setResourceReference(resource); @@ -447,7 +447,7 @@ public final class BridgeContext extends Context { pushParser(blockParser); return Pair.of( mBridgeInflater.inflate(blockParser, parent, attachToRoot), - false); + Boolean.FALSE); } finally { popParser(); } @@ -470,7 +470,7 @@ public final class BridgeContext extends Context { resource.getName()), null); } - return Pair.of(null, false); + return Pair.of(null, Boolean.FALSE); } @SuppressWarnings("deprecation") diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/AppCompatActionBar.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/AppCompatActionBar.java index 868c6d3..cdcf0ea 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/AppCompatActionBar.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/AppCompatActionBar.java @@ -16,10 +16,13 @@ package com.android.layoutlib.bridge.bars; +import com.android.ide.common.rendering.api.LayoutLog; +import com.android.ide.common.rendering.api.LayoutlibCallback; import com.android.ide.common.rendering.api.RenderResources; import com.android.ide.common.rendering.api.ResourceValue; import com.android.ide.common.rendering.api.SessionParams; import com.android.ide.common.rendering.api.StyleResourceValue; +import com.android.layoutlib.bridge.Bridge; import com.android.layoutlib.bridge.android.BridgeContext; import com.android.layoutlib.bridge.impl.ResourceHelper; import com.android.resources.ResourceType; @@ -45,6 +48,8 @@ public class AppCompatActionBar extends BridgeActionBar { private Object mWindowDecorActionBar; private static final String WINDOW_ACTION_BAR_CLASS = "android.support.v7.internal.app.WindowDecorActionBar"; + // This is used on v23.1.1 and later. + private static final String WINDOW_ACTION_BAR_CLASS_NEW = "android.support.v7.app.WindowDecorActionBar"; private Class<?> mWindowActionBarClass; /** @@ -70,14 +75,25 @@ public class AppCompatActionBar extends BridgeActionBar { try { Class[] constructorParams = {View.class}; Object[] constructorArgs = {getDecorContent()}; - mWindowDecorActionBar = params.getLayoutlibCallback().loadView(WINDOW_ACTION_BAR_CLASS, - constructorParams, constructorArgs); + LayoutlibCallback callback = params.getLayoutlibCallback(); + + // Check if the old action bar class is present. + String actionBarClass = WINDOW_ACTION_BAR_CLASS; + try { + callback.findClass(actionBarClass); + } catch (ClassNotFoundException expected) { + // Failed to find the old class, use the newer one. + actionBarClass = WINDOW_ACTION_BAR_CLASS_NEW; + } + mWindowDecorActionBar = callback.loadView(actionBarClass, + constructorParams, constructorArgs); mWindowActionBarClass = mWindowDecorActionBar == null ? null : mWindowDecorActionBar.getClass(); setupActionBar(); } catch (Exception e) { - e.printStackTrace(); + Bridge.getLog().warning(LayoutLog.TAG_BROKEN, + "Failed to load AppCompat ActionBar with unknown error.", e); } } diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/CustomBar.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/CustomBar.java index b76ec17..a6e5fb8 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/CustomBar.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/CustomBar.java @@ -116,11 +116,11 @@ abstract class CustomBar extends LinearLayout { density = iconLoader.getDensity(); String path = iconLoader.getPath(); // look for a cached bitmap - Bitmap bitmap = Bridge.getCachedBitmap(path, true /*isFramework*/); + Bitmap bitmap = Bridge.getCachedBitmap(path, Boolean.TRUE /*isFramework*/); if (bitmap == null) { try { bitmap = Bitmap_Delegate.createBitmap(stream, false /*isMutable*/, density); - Bridge.setCachedBitmap(path, bitmap, true /*isFramework*/); + Bridge.setCachedBitmap(path, bitmap, Boolean.TRUE /*isFramework*/); } catch (IOException e) { return; } diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/LayoutParserWrapper.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/LayoutParserWrapper.java new file mode 100644 index 0000000..71e7fd2 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/LayoutParserWrapper.java @@ -0,0 +1,377 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.layoutlib.bridge.impl; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.annotation.Nullable; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * A wrapper around XmlPullParser that can peek forward to inspect if the file is a data-binding + * layout and some parts need to be stripped. + */ +public class LayoutParserWrapper implements XmlPullParser { + + // Data binding constants. + private static final String TAG_LAYOUT = "layout"; + private static final String TAG_DATA = "data"; + private static final String DEFAULT = "default="; + + private final XmlPullParser mDelegate; + + // Storage for peeked values. + private boolean mPeeked; + private int mEventType; + private int mDepth; + private int mNext; + private List<Attribute> mAttributes; + private String mText; + private String mName; + + // Used to end the document before the actual parser ends. + private int mFinalDepth = -1; + private boolean mEndNow; + + public LayoutParserWrapper(XmlPullParser delegate) { + mDelegate = delegate; + } + + public LayoutParserWrapper peekTillLayoutStart() throws IOException, XmlPullParserException { + final int STATE_LAYOUT_NOT_STARTED = 0; // <layout> tag not encountered yet. + final int STATE_ROOT_NOT_STARTED = 1; // the main view root not found yet. + final int STATE_INSIDE_DATA = 2; // START_TAG for <data> found, but not END_TAG. + + int state = STATE_LAYOUT_NOT_STARTED; + int dataDepth = -1; // depth of the <data> tag. Should be two. + while (true) { + int peekNext = peekNext(); + switch (peekNext) { + case START_TAG: + if (state == STATE_LAYOUT_NOT_STARTED) { + if (mName.equals(TAG_LAYOUT)) { + state = STATE_ROOT_NOT_STARTED; + } else { + return this; // no layout tag in the file. + } + } else if (state == STATE_ROOT_NOT_STARTED) { + if (mName.equals(TAG_DATA)) { + state = STATE_INSIDE_DATA; + dataDepth = mDepth; + } else { + mFinalDepth = mDepth; + return this; + } + } + break; + case END_TAG: + if (state == STATE_INSIDE_DATA) { + if (mDepth <= dataDepth) { + state = STATE_ROOT_NOT_STARTED; + } + } + break; + case END_DOCUMENT: + // No layout start found. + return this; + } + // consume the peeked tag. + next(); + } + } + + private int peekNext() throws IOException, XmlPullParserException { + if (mPeeked) { + return mNext; + } + mEventType = mDelegate.getEventType(); + mNext = mDelegate.next(); + if (mEventType == START_TAG) { + int count = mDelegate.getAttributeCount(); + mAttributes = count > 0 ? new ArrayList<Attribute>(count) : + Collections.<Attribute>emptyList(); + for (int i = 0; i < count; i++) { + mAttributes.add(new Attribute(mDelegate.getAttributeNamespace(i), + mDelegate.getAttributeName(i), mDelegate.getAttributeValue(i))); + } + } + mDepth = mDelegate.getDepth(); + mText = mDelegate.getText(); + mName = mDelegate.getName(); + mPeeked = true; + return mNext; + } + + private void reset() { + mAttributes = null; + mText = null; + mName = null; + mPeeked = false; + } + + @Override + public int next() throws XmlPullParserException, IOException { + int returnValue; + int depth; + if (mPeeked) { + returnValue = mNext; + depth = mDepth; + reset(); + } else if (mEndNow) { + return END_DOCUMENT; + } else { + returnValue = mDelegate.next(); + depth = getDepth(); + } + if (returnValue == END_TAG && depth <= mFinalDepth) { + mEndNow = true; + } + return returnValue; + } + + @Override + public int getEventType() throws XmlPullParserException { + return mPeeked ? mEventType : mDelegate.getEventType(); + } + + @Override + public int getDepth() { + return mPeeked ? mDepth : mDelegate.getDepth(); + } + + @Override + public String getName() { + return mPeeked ? mName : mDelegate.getName(); + } + + @Override + public String getText() { + return mPeeked ? mText : mDelegate.getText(); + } + + @Override + public String getAttributeValue(@Nullable String namespace, String name) { + String returnValue = null; + if (mPeeked) { + if (mAttributes == null) { + if (mEventType != START_TAG) { + throw new IndexOutOfBoundsException("getAttributeValue() called when not at START_TAG."); + } else { + return null; + } + } else { + for (Attribute attribute : mAttributes) { + //noinspection StringEquality for nullness check. + if (attribute.name.equals(name) && (attribute.namespace == namespace || + attribute.namespace != null && attribute.namespace.equals(namespace))) { + returnValue = attribute.value; + break; + } + } + } + } else { + returnValue = mDelegate.getAttributeValue(namespace, name); + } + // Check if the value is bound via data-binding, if yes get the default value. + if (returnValue != null && mFinalDepth >= 0 && returnValue.startsWith("@{")) { + // TODO: Improve the detection of default keyword. + int i = returnValue.lastIndexOf(DEFAULT); + return i > 0 ? returnValue.substring(i + DEFAULT.length(), returnValue.length() - 1) + : null; + } + return returnValue; + } + + private static class Attribute { + @Nullable + public final String namespace; + public final String name; + public final String value; + + public Attribute(@Nullable String namespace, String name, String value) { + this.namespace = namespace; + this.name = name; + this.value = value; + } + } + + // Not affected by peeking. + + @Override + public void setFeature(String s, boolean b) throws XmlPullParserException { + mDelegate.setFeature(s, b); + } + + @Override + public void setProperty(String s, Object o) throws XmlPullParserException { + mDelegate.setProperty(s, o); + } + + @Override + public void setInput(InputStream inputStream, String s) throws XmlPullParserException { + mDelegate.setInput(inputStream, s); + } + + @Override + public void setInput(Reader reader) throws XmlPullParserException { + mDelegate.setInput(reader); + } + + @Override + public String getInputEncoding() { + return mDelegate.getInputEncoding(); + } + + @Override + public String getNamespace(String s) { + return mDelegate.getNamespace(s); + } + + @Override + public String getPositionDescription() { + return mDelegate.getPositionDescription(); + } + + @Override + public int getLineNumber() { + return mDelegate.getLineNumber(); + } + + @Override + public String getNamespace() { + return mDelegate.getNamespace(); + } + + @Override + public int getColumnNumber() { + return mDelegate.getColumnNumber(); + } + + // -- We don't care much about the methods that follow. + + @Override + public void require(int i, String s, String s1) throws XmlPullParserException, IOException { + throw new UnsupportedOperationException("Only few parser methods are supported."); + } + + @Override + public boolean getFeature(String s) { + throw new UnsupportedOperationException("Only few parser methods are supported."); + } + + @Override + public void defineEntityReplacementText(String s, String s1) throws XmlPullParserException { + throw new UnsupportedOperationException("Only few parser methods are supported."); + } + + @Override + public Object getProperty(String s) { + throw new UnsupportedOperationException("Only few parser methods are supported."); + } + + @Override + public int nextToken() throws XmlPullParserException, IOException { + throw new UnsupportedOperationException("Only few parser methods are supported."); + } + + @Override + public int getNamespaceCount(int i) throws XmlPullParserException { + throw new UnsupportedOperationException("Only few parser methods are supported."); + } + + @Override + public String getNamespacePrefix(int i) throws XmlPullParserException { + throw new UnsupportedOperationException("Only few parser methods are supported."); + } + + @Override + public String getNamespaceUri(int i) throws XmlPullParserException { + throw new UnsupportedOperationException("Only few parser methods are supported."); + } + + @Override + public boolean isWhitespace() throws XmlPullParserException { + throw new UnsupportedOperationException("Only few parser methods are supported."); + } + + @Override + public char[] getTextCharacters(int[] ints) { + throw new UnsupportedOperationException("Only few parser methods are supported."); + } + + @Override + public String getPrefix() { + throw new UnsupportedOperationException("Only few parser methods are supported."); + } + + @Override + public boolean isEmptyElementTag() throws XmlPullParserException { + throw new UnsupportedOperationException("Only few parser methods are supported."); + } + + @Override + public int getAttributeCount() { + throw new UnsupportedOperationException("Only few parser methods are supported."); + } + + @Override + public String getAttributeNamespace(int i) { + throw new UnsupportedOperationException("Only few parser methods are supported."); + } + + @Override + public String getAttributeName(int i) { + throw new UnsupportedOperationException("Only few parser methods are supported."); + } + + @Override + public String getAttributePrefix(int i) { + throw new UnsupportedOperationException("Only few parser methods are supported."); + } + + @Override + public String getAttributeType(int i) { + throw new UnsupportedOperationException("Only few parser methods are supported."); + } + + @Override + public boolean isAttributeDefault(int i) { + throw new UnsupportedOperationException("Only few parser methods are supported."); + } + + @Override + public String getAttributeValue(int i) { + throw new UnsupportedOperationException("Only few parser methods are supported."); + } + + @Override + public String nextText() throws XmlPullParserException, IOException { + throw new UnsupportedOperationException("Only few parser methods are supported."); + } + + @Override + public int nextTag() throws XmlPullParserException, IOException { + throw new UnsupportedOperationException("Only few parser methods are supported."); + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/ParserFactory.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/ParserFactory.java index 6e67f59..e273b2c 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/ParserFactory.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/ParserFactory.java @@ -53,24 +53,35 @@ public class ParserFactory { @NonNull public static XmlPullParser create(@NonNull File f) throws XmlPullParserException, FileNotFoundException { - InputStream stream = new FileInputStream(f); - return create(stream, f.getName(), f.length()); + return create(f, false); } + public static XmlPullParser create(@NonNull File f, boolean isLayout) + throws XmlPullParserException, FileNotFoundException { + InputStream stream = new FileInputStream(f); + return create(stream, f.getName(), f.length(), isLayout); + } @NonNull public static XmlPullParser create(@NonNull InputStream stream, @Nullable String name) throws XmlPullParserException { - return create(stream, name, -1); + return create(stream, name, -1, false); } @NonNull private static XmlPullParser create(@NonNull InputStream stream, @Nullable String name, - long size) throws XmlPullParserException { + long size, boolean isLayout) throws XmlPullParserException { XmlPullParser parser = instantiateParser(name); stream = readAndClose(stream, name, size); parser.setInput(stream, ENCODING); + if (isLayout) { + try { + return new LayoutParserWrapper(parser).peekTillLayoutStart(); + } catch (IOException e) { + throw new XmlPullParserException(null, parser, e); + } + } return parser; } diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/PorterDuffUtility.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/PorterDuffUtility.java index 9588035..80d7c68 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/PorterDuffUtility.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/PorterDuffUtility.java @@ -21,6 +21,7 @@ import com.android.layoutlib.bridge.Bridge; import android.graphics.BlendComposite; import android.graphics.BlendComposite.BlendingMode; +import android.graphics.PorterDuff; import android.graphics.PorterDuff.Mode; import android.graphics.PorterDuffColorFilter_Delegate; import android.graphics.PorterDuffXfermode_Delegate; @@ -34,6 +35,8 @@ import java.awt.Composite; */ public final class PorterDuffUtility { + private static final int MODES_COUNT = Mode.values().length; + // Make the class non-instantiable. private PorterDuffUtility() { } @@ -43,12 +46,11 @@ public final class PorterDuffUtility { * {@link Mode#SRC_OVER} for invalid modes. */ public static Mode getPorterDuffMode(int porterDuffMode) { - Mode[] values = Mode.values(); - if (porterDuffMode >= 0 && porterDuffMode < values.length) { - return values[porterDuffMode]; + if (porterDuffMode >= 0 && porterDuffMode < MODES_COUNT) { + return PorterDuff.intToMode(porterDuffMode); } Bridge.getLog().error(LayoutLog.TAG_BROKEN, - String.format("Unknown PorterDuff.Mode: %1$d", porterDuffMode), null /*data*/); + String.format("Unknown PorterDuff.Mode: %1$d", porterDuffMode), null); assert false; return Mode.SRC_OVER; } diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java index ac7c409..0ffa357 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java @@ -1051,11 +1051,7 @@ public class RenderSessionImpl extends RenderAction<SessionParams> { } if (scrollPos != 0) { view.scrollBy(0, scrollPos); - } else { - view.scrollBy(0, scrollPos); } - } else { - view.scrollBy(0, scrollPos); } if (!(view instanceof ViewGroup)) { diff --git a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/impl/LayoutParserWrapperTest.java b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/impl/LayoutParserWrapperTest.java new file mode 100644 index 0000000..2c33862 --- /dev/null +++ b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/impl/LayoutParserWrapperTest.java @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.layoutlib.bridge.impl; + +import org.junit.Test; +import org.kxml2.io.KXmlParser; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.StringReader; + +import static com.android.SdkConstants.NS_RESOURCES; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; +import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT; +import static org.xmlpull.v1.XmlPullParser.END_TAG; +import static org.xmlpull.v1.XmlPullParser.START_TAG; + + +public class LayoutParserWrapperTest { + @Test + @SuppressWarnings("StatementWithEmptyBody") // some for loops need to be empty statements. + public void testDataBindingLayout() throws Exception { + LayoutParserWrapper parser = getParserFromString(sDataBindingLayout); + parser.peekTillLayoutStart(); + assertEquals("Expected START_TAG", START_TAG, parser.next()); + assertEquals("RelativeLayout", parser.getName()); + for (int next = parser.next(); next != START_TAG && next != END_DOCUMENT; + next = parser.next()); + assertEquals("Expected START_TAG", START_TAG, parser.getEventType()); + assertEquals("TextView", parser.getName()); + assertEquals("layout_width incorrect for first text view.", "wrap_content", + parser.getAttributeValue(NS_RESOURCES, "layout_width")); + // Ensure that data-binding part is stripped. + assertEquals("Bound attribute android:text incorrect", "World", + parser.getAttributeValue(NS_RESOURCES, "text")); + assertEquals("resource attribute 'id' for first text view incorrect.", "@+id/first", + parser.getAttributeValue(NS_RESOURCES, "id")); + for (int next = parser.next(); + (next != END_TAG || !"RelativeLayout".equals(parser.getName())) && next != END_DOCUMENT; + next = parser.next()); + assertNotSame("Unexpected end of document", END_DOCUMENT, parser.getEventType()); + assertEquals("Document didn't end when expected.", END_DOCUMENT, parser.next()); + } + + @Test + @SuppressWarnings("StatementWithEmptyBody") + public void testNonDataBindingLayout() throws Exception { + LayoutParserWrapper parser = getParserFromString(sNonDataBindingLayout); + parser.peekTillLayoutStart(); + assertEquals("Expected START_TAG", START_TAG, parser.next()); + assertEquals("RelativeLayout", parser.getName()); + for (int next = parser.next(); next != START_TAG && next != END_DOCUMENT; + next = parser.next()); + assertEquals("Expected START_TAG", START_TAG, parser.getEventType()); + assertEquals("TextView", parser.getName()); + assertEquals("layout_width incorrect for first text view.", "wrap_content", + parser.getAttributeValue(NS_RESOURCES, "layout_width")); + // Ensure that value isn't modified. + assertEquals("Bound attribute android:text incorrect", "@{user.firstName,default=World}", + parser.getAttributeValue(NS_RESOURCES, "text")); + assertEquals("resource attribute 'id' for first text view incorrect.", "@+id/first", + parser.getAttributeValue(NS_RESOURCES, "id")); + for (int next = parser.next(); + (next != END_TAG || !"RelativeLayout".equals(parser.getName())) && next != END_DOCUMENT; + next = parser.next()); + assertNotSame("Unexpected end of document", END_DOCUMENT, parser.getEventType()); + assertEquals("Document didn't end when expected.", END_DOCUMENT, parser.next()); + } + + private static LayoutParserWrapper getParserFromString(String layoutContent) throws + XmlPullParserException { + XmlPullParser parser = new KXmlParser(); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + parser.setInput(new StringReader(layoutContent)); + return new LayoutParserWrapper(parser); + } + + private static final String sDataBindingLayout = + //language=XML + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + + "<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" + + " xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n" + + " xmlns:tools=\"http://schemas.android.com/tools\"\n" + + " tools:context=\".MainActivity\"\n" + + " tools:showIn=\"@layout/activity_main\">\n" + + "\n" + + " <data>\n" + + "\n" + + " <variable\n" + + " name=\"user\"\n" + + " type=\"com.example.User\" />\n" + + " <variable\n" + + " name=\"activity\"\n" + + " type=\"com.example.MainActivity\" />\n" + + " </data>\n" + + "\n" + + " <RelativeLayout\n" + + " android:layout_width=\"match_parent\"\n" + + " android:layout_height=\"match_parent\"\n" + + " android:paddingBottom=\"@dimen/activity_vertical_margin\"\n" + + " android:paddingLeft=\"@dimen/activity_horizontal_margin\"\n" + + " android:paddingRight=\"@dimen/activity_horizontal_margin\"\n" + + " android:paddingTop=\"@dimen/activity_vertical_margin\"\n" + + " app:layout_behavior=\"@string/appbar_scrolling_view_behavior\"\n" + + " >\n" + + "\n" + + " <TextView\n" + + " android:id=\"@+id/first\"\n" + + " android:layout_width=\"wrap_content\"\n" + + " android:layout_alignParentStart=\"true\"\n" + + " android:layout_alignParentLeft=\"true\"\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:text=\"@{user.firstName,default=World}\" />\n" + + "\n" + + " <TextView\n" + + " android:id=\"@+id/last\"\n" + + " android:layout_width=\"wrap_content\"\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:layout_toEndOf=\"@id/first\"\n" + + " android:layout_toRightOf=\"@id/first\"\n" + + " android:text=\"@{user.lastName,default=Hello}\" />\n" + + "\n" + + " <Button\n" + + " android:layout_width=\"wrap_content\"\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:layout_below=\"@id/last\"\n" + + " android:text=\"Submit\"\n" + + " android:onClick=\"@{activity.onClick}\"/>\n" + + " </RelativeLayout>\n" + + "</layout>"; + + private static final String sNonDataBindingLayout = + //language=XML + "<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" + + " xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n" + + " android:layout_width=\"match_parent\"\n" + + " android:layout_height=\"match_parent\"\n" + + " android:paddingBottom=\"@dimen/activity_vertical_margin\"\n" + + " android:paddingLeft=\"@dimen/activity_horizontal_margin\"\n" + + " android:paddingRight=\"@dimen/activity_horizontal_margin\"\n" + + " android:paddingTop=\"@dimen/activity_vertical_margin\"\n" + + " app:layout_behavior=\"@string/appbar_scrolling_view_behavior\"\n" + + ">\n" + + "\n" + + " <TextView\n" + + " android:id=\"@+id/first\"\n" + + " android:layout_width=\"wrap_content\"\n" + + " android:layout_alignParentStart=\"true\"\n" + + " android:layout_alignParentLeft=\"true\"\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:text=\"@{user.firstName,default=World}\" />\n" + + "\n" + + " <TextView\n" + + " android:id=\"@+id/last\"\n" + + " android:layout_width=\"wrap_content\"\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:layout_toEndOf=\"@id/first\"\n" + + " android:layout_toRightOf=\"@id/first\"\n" + + " android:text=\"@{user.lastName,default=Hello}\" />\n" + + "\n" + + " <Button\n" + + " android:layout_width=\"wrap_content\"\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:layout_below=\"@id/last\"\n" + + " android:text=\"Submit\"\n" + + " android:onClick=\"@{activity.onClick}\"/>\n" + + "</RelativeLayout>"; +} diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/CreateInfo.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/CreateInfo.java index 484240f..c9bc62e 100644 --- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/CreateInfo.java +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/CreateInfo.java @@ -157,7 +157,6 @@ public final class CreateInfo implements ICreateInfo { * The list of methods to rewrite as delegates. */ public final static String[] DELEGATE_METHODS = new String[] { - "android.animation.AnimatorInflater#loadAnimator", // TODO: remove when Path.approximate() is supported. "android.app.Fragment#instantiate", //(Landroid/content/Context;Ljava/lang/String;Landroid/os/Bundle;)Landroid/app/Fragment;", "android.content.res.Resources$Theme#obtainStyledAttributes", "android.content.res.Resources$Theme#resolveAttribute", @@ -167,6 +166,7 @@ public final class CreateInfo implements ICreateInfo { "android.content.res.TypedArray#getValueAt", "android.content.res.TypedArray#obtain", "android.graphics.BitmapFactory#finishDecode", + "android.graphics.drawable.GradientDrawable#buildRing", "android.graphics.Typeface#getSystemFontConfigLocation", "android.os.Handler#sendMessageAtTime", "android.os.HandlerThread#run", @@ -235,6 +235,7 @@ public final class CreateInfo implements ICreateInfo { "android.graphics.Path", "android.graphics.PathDashPathEffect", "android.graphics.PathEffect", + "android.graphics.PathMeasure", "android.graphics.PixelXorXfermode", "android.graphics.PorterDuffColorFilter", "android.graphics.PorterDuffXfermode", diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DelegateClassAdapter.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DelegateClassAdapter.java index ae4a57d..7ef7566 100644 --- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DelegateClassAdapter.java +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DelegateClassAdapter.java @@ -17,6 +17,7 @@ package com.android.tools.layoutlib.create; import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; @@ -40,6 +41,7 @@ public class DelegateClassAdapter extends ClassVisitor { private final String mClassName; private final Set<String> mDelegateMethods; private final Log mLog; + private boolean mIsStaticInnerClass; /** * Creates a new {@link DelegateClassAdapter} that can transform some methods @@ -62,16 +64,30 @@ public class DelegateClassAdapter extends ClassVisitor { mLog = log; mClassName = className; mDelegateMethods = delegateMethods; + // If this is an inner class, by default, we assume it's static. If it's not we will detect + // by looking at the fields (see visitField) + mIsStaticInnerClass = className.contains("$"); } //---------------------------------- // Methods from the ClassAdapter @Override + public FieldVisitor visitField(int access, String name, String desc, String signature, + Object value) { + if (mIsStaticInnerClass && "this$0".equals(name)) { + // Having a "this$0" field, proves that this class is not a static inner class. + mIsStaticInnerClass = false; + } + + return super.visitField(access, name, desc, signature, value); + } + + @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { - boolean isStatic = (access & Opcodes.ACC_STATIC) != 0; + boolean isStaticMethod = (access & Opcodes.ACC_STATIC) != 0; boolean isNative = (access & Opcodes.ACC_NATIVE) != 0; boolean useDelegate = (isNative && mDelegateMethods.contains(ALL_NATIVES)) || @@ -96,7 +112,8 @@ public class DelegateClassAdapter extends ClassVisitor { MethodVisitor mwDelegate = super.visitMethod(access, name, desc, signature, exceptions); DelegateMethodAdapter a = new DelegateMethodAdapter( - mLog, null, mwDelegate, mClassName, name, desc, isStatic); + mLog, null, mwDelegate, mClassName, name, desc, isStaticMethod, + mIsStaticInnerClass); // A native has no code to visit, so we need to generate it directly. a.generateDelegateCode(); @@ -120,6 +137,7 @@ public class DelegateClassAdapter extends ClassVisitor { desc, signature, exceptions); return new DelegateMethodAdapter( - mLog, mwOriginal, mwDelegate, mClassName, name, desc, isStatic); + mLog, mwOriginal, mwDelegate, mClassName, name, desc, isStaticMethod, + mIsStaticInnerClass); } } diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DelegateMethodAdapter.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DelegateMethodAdapter.java index 12690db..cca9e57 100644 --- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DelegateMethodAdapter.java +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DelegateMethodAdapter.java @@ -85,6 +85,8 @@ class DelegateMethodAdapter extends MethodVisitor { private String mDesc; /** True if the original method is static. */ private final boolean mIsStatic; + /** True if the method is contained in a static inner class */ + private final boolean mIsStaticInnerClass; /** The internal class name (e.g. <code>com/android/SomeClass$InnerClass</code>.) */ private final String mClassName; /** The method name. */ @@ -120,7 +122,8 @@ class DelegateMethodAdapter extends MethodVisitor { String className, String methodName, String desc, - boolean isStatic) { + boolean isStatic, + boolean isStaticClass) { super(Opcodes.ASM4); mLog = log; mOrgWriter = mvOriginal; @@ -129,6 +132,7 @@ class DelegateMethodAdapter extends MethodVisitor { mMethodName = methodName; mDesc = desc; mIsStatic = isStatic; + mIsStaticInnerClass = isStaticClass; } /** @@ -206,7 +210,7 @@ class DelegateMethodAdapter extends MethodVisitor { // by the 'this' of any outer class, if any. if (!mIsStatic) { - if (outerType != null) { + if (outerType != null && !mIsStaticInnerClass) { // The first-level inner class has a package-protected member called 'this$0' // that points to the outer class. diff --git a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/DelegateClassAdapterTest.java b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/DelegateClassAdapterTest.java index 648cea4..e37a09b 100644 --- a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/DelegateClassAdapterTest.java +++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/DelegateClassAdapterTest.java @@ -27,6 +27,7 @@ import static org.junit.Assert.fail; import com.android.tools.layoutlib.create.dataclass.ClassWithNative; import com.android.tools.layoutlib.create.dataclass.OuterClass; import com.android.tools.layoutlib.create.dataclass.OuterClass.InnerClass; +import com.android.tools.layoutlib.create.dataclass.OuterClass.StaticInnerClass; import org.junit.Before; import org.junit.Test; @@ -56,6 +57,8 @@ public class DelegateClassAdapterTest { private static final String OUTER_CLASS_NAME = OuterClass.class.getCanonicalName(); private static final String INNER_CLASS_NAME = OuterClass.class.getCanonicalName() + "$" + InnerClass.class.getSimpleName(); + private static final String STATIC_INNER_CLASS_NAME = + OuterClass.class.getCanonicalName() + "$" + StaticInnerClass.class.getSimpleName(); @Before public void setUp() throws Exception { @@ -294,6 +297,61 @@ public class DelegateClassAdapterTest { } } + @Test + public void testDelegateStaticInner() throws Throwable { + // We'll delegate the "get" method of both the inner and outer class. + HashSet<String> delegateMethods = new HashSet<String>(); + delegateMethods.add("get"); + + // Generate the delegate for the outer class. + ClassWriter cwOuter = new ClassWriter(0 /*flags*/); + String outerClassName = OUTER_CLASS_NAME.replace('.', '/'); + DelegateClassAdapter cvOuter = new DelegateClassAdapter( + mLog, cwOuter, outerClassName, delegateMethods); + ClassReader cr = new ClassReader(OUTER_CLASS_NAME); + cr.accept(cvOuter, 0 /* flags */); + + // Generate the delegate for the static inner class. + ClassWriter cwInner = new ClassWriter(0 /*flags*/); + String innerClassName = STATIC_INNER_CLASS_NAME.replace('.', '/'); + DelegateClassAdapter cvInner = new DelegateClassAdapter( + mLog, cwInner, innerClassName, delegateMethods); + cr = new ClassReader(STATIC_INNER_CLASS_NAME); + cr.accept(cvInner, 0 /* flags */); + + // Load the generated classes in a different class loader and try them + ClassLoader2 cl2 = null; + try { + cl2 = new ClassLoader2() { + @Override + public void testModifiedInstance() throws Exception { + + // Check the outer class + Class<?> outerClazz2 = loadClass(OUTER_CLASS_NAME); + Object o2 = outerClazz2.newInstance(); + assertNotNull(o2); + + // Check the inner class. Since it's not a static inner class, we need + // to use the hidden constructor that takes the outer class as first parameter. + Class<?> innerClazz2 = loadClass(STATIC_INNER_CLASS_NAME); + Constructor<?> innerCons = innerClazz2.getConstructor(); + Object i2 = innerCons.newInstance(); + assertNotNull(i2); + + // The original StaticInner.get returns 100+10+20, + // but the delegate makes it return 6+10+20 + assertEquals(6+10+20, callGet(i2, 10, 20)); + assertEquals(100+10+20, callGet_Original(i2, 10, 20)); + } + }; + cl2.add(OUTER_CLASS_NAME, cwOuter.toByteArray()); + cl2.add(STATIC_INNER_CLASS_NAME, cwInner.toByteArray()); + cl2.testModifiedInstance(); + } catch (Throwable t) { + throw dumpGeneratedClass(t, cl2); + } + } + //------- /** diff --git a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/dataclass/OuterClass.java b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/dataclass/OuterClass.java index f083e76..6dfb816 100644 --- a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/dataclass/OuterClass.java +++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/dataclass/OuterClass.java @@ -45,6 +45,16 @@ public class OuterClass { } } + public static class StaticInnerClass { + public StaticInnerClass() { + } + + // StaticInnerClass.get returns 100 + a + b + public int get(int a, long b) { + return 100 + a + (int) b; + } + } + @SuppressWarnings("unused") private String privateMethod() { return "outerPrivateMethod"; diff --git a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/dataclass/OuterClass_StaticInnerClass_Delegate.java b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/dataclass/OuterClass_StaticInnerClass_Delegate.java new file mode 100644 index 0000000..a29439e --- /dev/null +++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/dataclass/OuterClass_StaticInnerClass_Delegate.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2015 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.dataclass; + +import com.android.tools.layoutlib.create.DelegateClassAdapterTest; +import com.android.tools.layoutlib.create.dataclass.OuterClass.StaticInnerClass; + +/** + * Used by {@link DelegateClassAdapterTest}. + */ +public class OuterClass_StaticInnerClass_Delegate { + // The delegate override of Inner.get return 6 + a + b + public static int get(StaticInnerClass inner, int a, long b) { + return 6 + a + (int) b; + } +} |