diff options
author | Xin Li <delphij@google.com> | 2020-08-31 21:21:38 -0700 |
---|---|---|
committer | Xin Li <delphij@google.com> | 2020-08-31 21:21:38 -0700 |
commit | 628590d7ec80e10a3fc24b1c18a1afb55cca10a8 (patch) | |
tree | 4b1c3f52d86d7fb53afbe9e9438468588fa489f8 /graphics/java | |
parent | b11b8ec3aec8bb42f2c07e1c5ac7942da293baa8 (diff) | |
parent | d2d3a20624d968199353ccf6ddbae6f3ac39c9af (diff) |
Merge Android R (rvc-dev-plus-aosp-without-vendor@6692709)
Bug: 166295507
Merged-In: I3d92a6de21a938f6b352ec26dc23420c0fe02b27
Change-Id: Ifdb80563ef042738778ebb8a7581a97c4e3d96e2
Diffstat (limited to 'graphics/java')
36 files changed, 2378 insertions, 317 deletions
diff --git a/graphics/java/android/graphics/BLASTBufferQueue.java b/graphics/java/android/graphics/BLASTBufferQueue.java new file mode 100644 index 000000000000..4c7e960eb0a4 --- /dev/null +++ b/graphics/java/android/graphics/BLASTBufferQueue.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2019 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.view.Surface; +import android.view.SurfaceControl; + +/** + * @hide + */ +public final class BLASTBufferQueue { + // Note: This field is accessed by native code. + private long mNativeObject; // BLASTBufferQueue* + + private static native long nativeCreate(long surfaceControl, long width, long height, + boolean tripleBufferingEnabled); + private static native void nativeDestroy(long ptr); + private static native Surface nativeGetSurface(long ptr); + private static native void nativeSetNextTransaction(long ptr, long transactionPtr); + private static native void nativeUpdate(long ptr, long surfaceControl, long width, long height); + + /** Create a new connection with the surface flinger. */ + public BLASTBufferQueue(SurfaceControl sc, int width, int height, + boolean tripleBufferingEnabled) { + mNativeObject = nativeCreate(sc.mNativeObject, width, height, tripleBufferingEnabled); + } + + public void destroy() { + nativeDestroy(mNativeObject); + } + + public Surface getSurface() { + return nativeGetSurface(mNativeObject); + } + + public void setNextTransaction(SurfaceControl.Transaction t) { + nativeSetNextTransaction(mNativeObject, t.mNativeObject); + } + + public void update(SurfaceControl sc, int width, int height) { + nativeUpdate(mNativeObject, sc.mNativeObject, width, height); + } + + @Override + protected void finalize() throws Throwable { + try { + if (mNativeObject != 0) { + nativeDestroy(mNativeObject); + } + } finally { + super.finalize(); + } + } +} diff --git a/graphics/java/android/graphics/Bitmap.java b/graphics/java/android/graphics/Bitmap.java index a8b7e1fa0113..f7877590869a 100644 --- a/graphics/java/android/graphics/Bitmap.java +++ b/graphics/java/android/graphics/Bitmap.java @@ -1359,9 +1359,44 @@ public final class Bitmap implements Parcelable { * Specifies the known formats a bitmap can be compressed into */ public enum CompressFormat { - JPEG (0), - PNG (1), - WEBP (2); + /** + * Compress to the JPEG format. {@code quality} of {@code 0} means + * compress for the smallest size. {@code 100} means compress for max + * visual quality. + */ + JPEG (0), + /** + * Compress to the PNG format. PNG is lossless, so {@code quality} is + * ignored. + */ + PNG (1), + /** + * Compress to the WEBP format. {@code quality} of {@code 0} means + * compress for the smallest size. {@code 100} means compress for max + * visual quality. As of {@link android.os.Build.VERSION_CODES#Q}, a + * value of {@code 100} results in a file in the lossless WEBP format. + * Otherwise the file will be in the lossy WEBP format. + * + * @deprecated in favor of the more explicit + * {@link CompressFormat#WEBP_LOSSY} and + * {@link CompressFormat#WEBP_LOSSLESS}. + */ + @Deprecated + WEBP (2), + /** + * Compress to the WEBP lossy format. {@code quality} of {@code 0} means + * compress for the smallest size. {@code 100} means compress for max + * visual quality. + */ + WEBP_LOSSY (3), + /** + * Compress to the WEBP lossless format. {@code quality} refers to how + * much effort to put into compression. A value of {@code 0} means to + * compress quickly, resulting in a relatively large file size. + * {@code 100} means to spend more time compressing, resulting in a + * smaller file. + */ + WEBP_LOSSLESS (4); CompressFormat(int nativeInt) { this.nativeInt = nativeInt; @@ -1385,10 +1420,8 @@ public final class Bitmap implements Parcelable { * pixels). * * @param format The format of the compressed image - * @param quality Hint to the compressor, 0-100. 0 meaning compress for - * small size, 100 meaning compress for max quality. Some - * formats, like PNG which is lossless, will ignore the - * quality setting + * @param quality Hint to the compressor, 0-100. The value is interpreted + * differently depending on the {@link CompressFormat}. * @param stream The outputstream to write the compressed data. * @return true if successfully compressed to the specified stream. */ @@ -2099,7 +2132,7 @@ public final class Bitmap implements Parcelable { public void writeToParcel(Parcel p, int flags) { checkRecycled("Can't parcel a recycled bitmap"); noteHardwareBitmapSlowCall(); - if (!nativeWriteToParcel(mNativePtr, isMutable(), mDensity, p)) { + if (!nativeWriteToParcel(mNativePtr, mDensity, p)) { throw new RuntimeException("native writeToParcel failed"); } } @@ -2207,7 +2240,20 @@ public final class Bitmap implements Parcelable { */ @UnsupportedAppUsage public GraphicBuffer createGraphicBufferHandle() { - return nativeCreateGraphicBufferHandle(mNativePtr); + return GraphicBuffer.createFromHardwareBuffer(getHardwareBuffer()); + } + + /** + * @return {@link HardwareBuffer} which is internally used by hardware bitmap + * + * Note: the HardwareBuffer does *not* have an associated {@link ColorSpace}. + * To render this object the same as its rendered with this Bitmap, you + * should also call {@link getColorSpace}. + * + * @hide + */ + public HardwareBuffer getHardwareBuffer() { + return nativeGetHardwareBuffer(mNativePtr); } //////////// native methods @@ -2252,7 +2298,6 @@ public final class Bitmap implements Parcelable { private static native Bitmap nativeCreateFromParcel(Parcel p); // returns true on success private static native boolean nativeWriteToParcel(long nativeBitmap, - boolean isMutable, int density, Parcel p); // returns a new bitmap built from the native bitmap's alpha, and the paint @@ -2275,7 +2320,7 @@ public final class Bitmap implements Parcelable { private static native Bitmap nativeCopyPreserveInternalConfig(long nativeBitmap); private static native Bitmap nativeWrapHardwareBufferBitmap(HardwareBuffer buffer, long nativeColorSpace); - private static native GraphicBuffer nativeCreateGraphicBufferHandle(long nativeBitmap); + private static native HardwareBuffer nativeGetHardwareBuffer(long nativeBitmap); private static native ColorSpace nativeComputeColorSpace(long nativePtr); private static native void nativeSetColorSpace(long nativePtr, long nativeColorSpace); private static native boolean nativeIsSRGB(long nativePtr); diff --git a/graphics/java/android/graphics/Canvas.java b/graphics/java/android/graphics/Canvas.java index 9a0ca3e4ad9b..d949444d44d6 100644 --- a/graphics/java/android/graphics/Canvas.java +++ b/graphics/java/android/graphics/Canvas.java @@ -1162,6 +1162,7 @@ public class Canvas extends BaseCanvas { * @see #quickReject(float, float, float, float, EdgeType) * @see #quickReject(Path, EdgeType) * @see #quickReject(RectF, EdgeType) + * @deprecated quickReject no longer uses this. */ public enum EdgeType { @@ -1197,13 +1198,30 @@ public class Canvas extends BaseCanvas { * non-antialiased ({@link Canvas.EdgeType#BW}). * @return true if the rect (transformed by the canvas' matrix) * does not intersect with the canvas' clip + * @deprecated The EdgeType is ignored. Use {@link #quickReject(RectF)} instead. */ + @Deprecated public boolean quickReject(@NonNull RectF rect, @NonNull EdgeType type) { return nQuickReject(mNativeCanvasWrapper, rect.left, rect.top, rect.right, rect.bottom); } /** + * Return true if the specified rectangle, after being transformed by the + * current matrix, would lie completely outside of the current clip. Call + * this to check if an area you intend to draw into is clipped out (and + * therefore you can skip making the draw calls). + * + * @param rect the rect to compare with the current clip + * @return true if the rect (transformed by the canvas' matrix) + * does not intersect with the canvas' clip + */ + public boolean quickReject(@NonNull RectF rect) { + return nQuickReject(mNativeCanvasWrapper, + rect.left, rect.top, rect.right, rect.bottom); + } + + /** * Return true if the specified path, after being transformed by the * current matrix, would lie completely outside of the current clip. Call * this to check if an area you intend to draw into is clipped out (and @@ -1217,12 +1235,30 @@ public class Canvas extends BaseCanvas { * non-antialiased ({@link Canvas.EdgeType#BW}). * @return true if the path (transformed by the canvas' matrix) * does not intersect with the canvas' clip + * @deprecated The EdgeType is ignored. Use {@link #quickReject(Path)} instead. */ + @Deprecated public boolean quickReject(@NonNull Path path, @NonNull EdgeType type) { return nQuickReject(mNativeCanvasWrapper, path.readOnlyNI()); } /** + * Return true if the specified path, after being transformed by the + * current matrix, would lie completely outside of the current clip. Call + * this to check if an area you intend to draw into is clipped out (and + * therefore you can skip making the draw calls). Note: for speed it may + * return false even if the path itself might not intersect the clip + * (i.e. the bounds of the path intersects, but the path does not). + * + * @param path The path to compare with the current clip + * @return true if the path (transformed by the canvas' matrix) + * does not intersect with the canvas' clip + */ + public boolean quickReject(@NonNull Path path) { + return nQuickReject(mNativeCanvasWrapper, path.readOnlyNI()); + } + + /** * Return true if the specified rectangle, after being transformed by the * current matrix, would lie completely outside of the current clip. Call * this to check if an area you intend to draw into is clipped out (and @@ -1241,13 +1277,37 @@ public class Canvas extends BaseCanvas { * non-antialiased ({@link Canvas.EdgeType#BW}). * @return true if the rect (transformed by the canvas' matrix) * does not intersect with the canvas' clip + * @deprecated The EdgeType is ignored. Use {@link #quickReject(float, float, float, float)} + * instead. */ + @Deprecated public boolean quickReject(float left, float top, float right, float bottom, @NonNull EdgeType type) { return nQuickReject(mNativeCanvasWrapper, left, top, right, bottom); } /** + * Return true if the specified rectangle, after being transformed by the + * current matrix, would lie completely outside of the current clip. Call + * this to check if an area you intend to draw into is clipped out (and + * therefore you can skip making the draw calls). + * + * @param left The left side of the rectangle to compare with the + * current clip + * @param top The top of the rectangle to compare with the current + * clip + * @param right The right side of the rectangle to compare with the + * current clip + * @param bottom The bottom of the rectangle to compare with the + * current clip + * @return true if the rect (transformed by the canvas' matrix) + * does not intersect with the canvas' clip + */ + public boolean quickReject(float left, float top, float right, float bottom) { + return nQuickReject(mNativeCanvasWrapper, left, top, right, bottom); + } + + /** * Return the bounds of the current clip (in local coordinates) in the * bounds parameter, and return true if it is non-empty. This can be useful * in a way similar to quickReject, in that it tells you that drawing diff --git a/graphics/java/android/graphics/ColorSpace.java b/graphics/java/android/graphics/ColorSpace.java index 9c4b5e8b0165..1aeafa391b41 100644 --- a/graphics/java/android/graphics/ColorSpace.java +++ b/graphics/java/android/graphics/ColorSpace.java @@ -199,6 +199,11 @@ public abstract class ColorSpace { private static final float[] SRGB_PRIMARIES = { 0.640f, 0.330f, 0.300f, 0.600f, 0.150f, 0.060f }; private static final float[] NTSC_1953_PRIMARIES = { 0.67f, 0.33f, 0.21f, 0.71f, 0.14f, 0.08f }; + /** + * A gray color space does not have meaningful primaries, so we use this arbitrary set. + */ + private static final float[] GRAY_PRIMARIES = { 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f }; + private static final float[] ILLUMINANT_D50_XYZ = { 0.964212f, 1.0f, 0.825188f }; private static final Rgb.TransferParameters SRGB_TRANSFER_PARAMETERS = @@ -867,7 +872,8 @@ public abstract class ColorSpace { } } - private ColorSpace( + /** @hide */ + ColorSpace( @NonNull String name, @NonNull Model model, @IntRange(from = MIN_ID, to = MAX_ID) int id) { @@ -1380,9 +1386,9 @@ public abstract class ColorSpace { */ @NonNull static ColorSpace get(@IntRange(from = MIN_ID, to = MAX_ID) int index) { - if (index < 0 || index >= Named.values().length) { + if (index < 0 || index >= sNamedColorSpaces.length) { throw new IllegalArgumentException("Invalid ID, must be in the range [0.." + - Named.values().length + ")"); + sNamedColorSpaces.length + ")"); } return sNamedColorSpaces[index]; } @@ -1456,6 +1462,7 @@ public abstract class ColorSpace { "sRGB IEC61966-2.1", SRGB_PRIMARIES, ILLUMINANT_D65, + null, SRGB_TRANSFER_PARAMETERS, Named.SRGB.ordinal() ); @@ -1490,6 +1497,7 @@ public abstract class ColorSpace { "Rec. ITU-R BT.709-5", new float[] { 0.640f, 0.330f, 0.300f, 0.600f, 0.150f, 0.060f }, ILLUMINANT_D65, + null, new Rgb.TransferParameters(1 / 1.099, 0.099 / 1.099, 1 / 4.5, 0.081, 1 / 0.45), Named.BT709.ordinal() ); @@ -1497,6 +1505,7 @@ public abstract class ColorSpace { "Rec. ITU-R BT.2020-1", new float[] { 0.708f, 0.292f, 0.170f, 0.797f, 0.131f, 0.046f }, ILLUMINANT_D65, + null, new Rgb.TransferParameters(1 / 1.0993, 0.0993 / 1.0993, 1 / 4.5, 0.08145, 1 / 0.45), Named.BT2020.ordinal() ); @@ -1512,6 +1521,7 @@ public abstract class ColorSpace { "Display P3", new float[] { 0.680f, 0.320f, 0.265f, 0.690f, 0.150f, 0.060f }, ILLUMINANT_D65, + null, SRGB_TRANSFER_PARAMETERS, Named.DISPLAY_P3.ordinal() ); @@ -1519,6 +1529,7 @@ public abstract class ColorSpace { "NTSC (1953)", NTSC_1953_PRIMARIES, ILLUMINANT_C, + null, new Rgb.TransferParameters(1 / 1.099, 0.099 / 1.099, 1 / 4.5, 0.081, 1 / 0.45), Named.NTSC_1953.ordinal() ); @@ -1526,6 +1537,7 @@ public abstract class ColorSpace { "SMPTE-C RGB", new float[] { 0.630f, 0.340f, 0.310f, 0.595f, 0.155f, 0.070f }, ILLUMINANT_D65, + null, new Rgb.TransferParameters(1 / 1.099, 0.099 / 1.099, 1 / 4.5, 0.081, 1 / 0.45), Named.SMPTE_C.ordinal() ); @@ -1541,6 +1553,7 @@ public abstract class ColorSpace { "ROMM RGB ISO 22028-2:2013", new float[] { 0.7347f, 0.2653f, 0.1596f, 0.8404f, 0.0366f, 0.0001f }, ILLUMINANT_D50, + null, new Rgb.TransferParameters(1.0, 0.0, 1 / 16.0, 0.031248, 1.8), Named.PRO_PHOTO_RGB.ordinal() ); @@ -2470,7 +2483,11 @@ public abstract class ColorSpace { @NonNull @Size(min = 1) String name, @NonNull @Size(9) float[] toXYZ, @NonNull TransferParameters function) { - this(name, computePrimaries(toXYZ), computeWhitePoint(toXYZ), function, MIN_ID); + // Note: when isGray() returns false, this passes null for the transform for + // consistency with other constructors, which compute the transform from the primaries + // and white point. + this(name, isGray(toXYZ) ? GRAY_PRIMARIES : computePrimaries(toXYZ), + computeWhitePoint(toXYZ), isGray(toXYZ) ? toXYZ : null, function, MIN_ID); } /** @@ -2510,7 +2527,7 @@ public abstract class ColorSpace { @NonNull @Size(min = 6, max = 9) float[] primaries, @NonNull @Size(min = 2, max = 3) float[] whitePoint, @NonNull TransferParameters function) { - this(name, primaries, whitePoint, function, MIN_ID); + this(name, primaries, whitePoint, null, function, MIN_ID); } /** @@ -2533,6 +2550,8 @@ public abstract class ColorSpace { * @param name Name of the color space, cannot be null, its length must be >= 1 * @param primaries RGB primaries as an array of 6 (xy) or 9 (XYZ) floats * @param whitePoint Reference white as an array of 2 (xy) or 3 (XYZ) floats + * @param transform Computed transform matrix that converts from RGB to XYZ, or + * {@code null} to compute it from {@code primaries} and {@code whitePoint}. * @param function Parameters for the transfer functions * @param id ID of this color space as an integer between {@link #MIN_ID} and {@link #MAX_ID} * @@ -2551,9 +2570,10 @@ public abstract class ColorSpace { @NonNull @Size(min = 1) String name, @NonNull @Size(min = 6, max = 9) float[] primaries, @NonNull @Size(min = 2, max = 3) float[] whitePoint, + @Nullable @Size(9) float[] transform, @NonNull TransferParameters function, @IntRange(from = MIN_ID, to = MAX_ID) int id) { - this(name, primaries, whitePoint, null, + this(name, primaries, whitePoint, transform, function.e == 0.0 && function.f == 0.0 ? x -> rcpResponse(x, function.a, function.b, function.c, function.d, function.g) : @@ -2845,7 +2865,7 @@ public abstract class ColorSpace { * * @return The destination array passed as a parameter * - * @see #getWhitePoint(float[]) + * @see #getWhitePoint() */ @NonNull @Size(min = 2) @@ -2863,7 +2883,7 @@ public abstract class ColorSpace { * * @return A new non-null array of 2 floats * - * @see #getWhitePoint() + * @see #getWhitePoint(float[]) */ @NonNull @Size(2) @@ -2877,12 +2897,16 @@ public abstract class ColorSpace { * destination. The x and y components of the first primary are written * in the array at positions 0 and 1 respectively. * + * <p>Note: Some ColorSpaces represent gray profiles. The concept of + * primaries for such a ColorSpace does not make sense, so we use a special + * set of primaries that are all 1s.</p> + * * @param primaries The destination array, cannot be null, its length * must be >= 6 * * @return The destination array passed as a parameter * - * @see #getPrimaries(float[]) + * @see #getPrimaries() */ @NonNull @Size(min = 6) @@ -2897,9 +2921,13 @@ public abstract class ColorSpace { * the destination. The x and y components of the first primary are * written in the array at positions 0 and 1 respectively. * + * <p>Note: Some ColorSpaces represent gray profiles. The concept of + * primaries for such a ColorSpace does not make sense, so we use a special + * set of primaries that are all 1s.</p> + * * @return A new non-null array of 2 floats * - * @see #getWhitePoint() + * @see #getPrimaries(float[]) */ @NonNull @Size(6) @@ -2921,7 +2949,7 @@ public abstract class ColorSpace { * * @return The destination array passed as a parameter * - * @see #getInverseTransform() + * @see #getTransform() */ @NonNull @Size(min = 9) @@ -2941,7 +2969,7 @@ public abstract class ColorSpace { * * @return A new array of 9 floats * - * @see #getInverseTransform(float[]) + * @see #getTransform(float[]) */ @NonNull @Size(9) @@ -2963,7 +2991,7 @@ public abstract class ColorSpace { * * @return The destination array passed as a parameter * - * @see #getTransform() + * @see #getInverseTransform() */ @NonNull @Size(min = 9) @@ -2983,7 +3011,7 @@ public abstract class ColorSpace { * * @return A new array of 9 floats * - * @see #getTransform(float[]) + * @see #getInverseTransform(float[]) */ @NonNull @Size(9) @@ -3286,6 +3314,16 @@ public abstract class ColorSpace { return true; } + /** + * Report whether this matrix is a special gray matrix. + * @param toXYZ A XYZD50 matrix. Skia uses a special form for a gray profile. + * @return true if this is a special gray matrix. + */ + private static boolean isGray(@NonNull @Size(9) float[] toXYZ) { + return toXYZ.length == 9 && toXYZ[1] == 0 && toXYZ[2] == 0 && toXYZ[3] == 0 + && toXYZ[5] == 0 && toXYZ[6] == 0 && toXYZ[7] == 0; + } + private static boolean compare(double point, @NonNull DoubleUnaryOperator a, @NonNull DoubleUnaryOperator b) { double rA = a.applyAsDouble(point); diff --git a/graphics/java/android/graphics/FontFamily.java b/graphics/java/android/graphics/FontFamily.java index 254892013d51..f50de1665453 100644 --- a/graphics/java/android/graphics/FontFamily.java +++ b/graphics/java/android/graphics/FontFamily.java @@ -19,9 +19,10 @@ package android.graphics; import android.annotation.Nullable; import android.compat.annotation.UnsupportedAppUsage; import android.content.res.AssetManager; +import android.graphics.fonts.Font; import android.graphics.fonts.FontVariationAxis; +import android.os.Build; import android.text.TextUtils; -import android.util.Log; import dalvik.annotation.optimization.CriticalNative; @@ -59,7 +60,8 @@ public class FontFamily { * * This cannot be deleted because it's in use by AndroidX. */ - @UnsupportedAppUsage(trackingBug = 123768928) + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.Q, + publicAlternatives = "Use {@link android.graphics.fonts.FontFamily} instead.") public long mNativePtr; // Points native font family builder. Must be zero after freezing this family. @@ -68,7 +70,8 @@ public class FontFamily { /** * This cannot be deleted because it's in use by AndroidX. */ - @UnsupportedAppUsage(trackingBug = 123768928) + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.Q, + publicAlternatives = "Use {@link android.graphics.fonts.FontFamily} instead.") public FontFamily() { mBuilderPtr = nInitBuilder(null, 0); mNativeBuilderCleaner = sBuilderRegistry.registerNativeAllocation(this, mBuilderPtr); @@ -77,7 +80,8 @@ public class FontFamily { /** * This cannot be deleted because it's in use by AndroidX. */ - @UnsupportedAppUsage(trackingBug = 123768928) + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.Q, + publicAlternatives = "Use {@link android.graphics.fonts.FontFamily} instead.") public FontFamily(@Nullable String[] langs, int variant) { final String langsString; if (langs == null || langs.length == 0) { @@ -99,7 +103,8 @@ public class FontFamily { * * This cannot be deleted because it's in use by AndroidX. */ - @UnsupportedAppUsage(trackingBug = 123768928) + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.Q, + publicAlternatives = "Use {@link android.graphics.fonts.FontFamily} instead.") public boolean freeze() { if (mBuilderPtr == 0) { throw new IllegalStateException("This FontFamily is already frozen"); @@ -116,7 +121,8 @@ public class FontFamily { /** * This cannot be deleted because it's in use by AndroidX. */ - @UnsupportedAppUsage(trackingBug = 123768928) + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.Q, + publicAlternatives = "Use {@link android.graphics.fonts.FontFamily} instead.") public void abortCreation() { if (mBuilderPtr == 0) { throw new IllegalStateException("This FontFamily is already frozen or abandoned"); @@ -128,7 +134,8 @@ public class FontFamily { /** * This cannot be deleted because it's in use by AndroidX. */ - @UnsupportedAppUsage(trackingBug = 123768928) + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.Q, + publicAlternatives = "Use {@link android.graphics.fonts.FontFamily} instead.") public boolean addFont(String path, int ttcIndex, FontVariationAxis[] axes, int weight, int italic) { if (mBuilderPtr == 0) { @@ -145,7 +152,6 @@ public class FontFamily { } return nAddFont(mBuilderPtr, fontBuffer, ttcIndex, weight, italic); } catch (IOException e) { - Log.e(TAG, "Error mapping font file " + path); return false; } } @@ -153,7 +159,8 @@ public class FontFamily { /** * This cannot be deleted because it's in use by AndroidX. */ - @UnsupportedAppUsage(trackingBug = 123768928) + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.Q, + publicAlternatives = "Use {@link android.graphics.fonts.FontFamily} instead.") public boolean addFontFromBuffer(ByteBuffer font, int ttcIndex, FontVariationAxis[] axes, int weight, int italic) { if (mBuilderPtr == 0) { @@ -181,25 +188,21 @@ public class FontFamily { * * This cannot be deleted because it's in use by AndroidX. */ - @UnsupportedAppUsage(trackingBug = 123768928) + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.Q, + publicAlternatives = "Use {@link android.graphics.fonts.FontFamily} instead.") public boolean addFontFromAssetManager(AssetManager mgr, String path, int cookie, boolean isAsset, int ttcIndex, int weight, int isItalic, FontVariationAxis[] axes) { if (mBuilderPtr == 0) { throw new IllegalStateException("Unable to call addFontFromAsset after freezing."); } - if (axes != null) { - for (FontVariationAxis axis : axes) { - nAddAxisValue(mBuilderPtr, axis.getOpenTypeTagValue(), axis.getStyleValue()); - } - } - return nAddFontFromAssetManager(mBuilderPtr, mgr, path, cookie, isAsset, ttcIndex, weight, - isItalic); - } - // TODO: Remove once internal user stop using private API. - private static boolean nAddFont(long builderPtr, ByteBuffer font, int ttcIndex) { - return nAddFont(builderPtr, font, ttcIndex, -1, -1); + try { + ByteBuffer buffer = Font.Builder.createBuffer(mgr, path, isAsset, cookie); + return addFontFromBuffer(buffer, ttcIndex, axes, weight, isItalic); + } catch (IOException e) { + return false; + } } private static native long nInitBuilder(String langs, int variant); @@ -218,8 +221,6 @@ public class FontFamily { int weight, int isItalic); private static native boolean nAddFontWeightStyle(long builderPtr, ByteBuffer font, int ttcIndex, int weight, int isItalic); - private static native boolean nAddFontFromAssetManager(long builderPtr, AssetManager mgr, - String path, int cookie, boolean isAsset, int ttcIndex, int weight, int isItalic); // The added axis values are only valid for the next nAddFont* method call. @CriticalNative diff --git a/graphics/java/android/graphics/GraphicBuffer.java b/graphics/java/android/graphics/GraphicBuffer.java index 99fa5eef7bbd..0430847857b6 100644 --- a/graphics/java/android/graphics/GraphicBuffer.java +++ b/graphics/java/android/graphics/GraphicBuffer.java @@ -17,6 +17,7 @@ package android.graphics; import android.compat.annotation.UnsupportedAppUsage; +import android.hardware.HardwareBuffer; import android.os.Parcel; import android.os.Parcelable; @@ -110,6 +111,14 @@ public class GraphicBuffer implements Parcelable { } /** + * For Bitmap until all usages are updated to AHB + * @hide + */ + public static final GraphicBuffer createFromHardwareBuffer(HardwareBuffer buffer) { + return nCreateFromHardwareBuffer(buffer); + } + + /** * Returns the width of this buffer in pixels. */ public int getWidth() { @@ -305,4 +314,5 @@ public class GraphicBuffer implements Parcelable { private static native boolean nLockCanvas(long nativeObject, Canvas canvas, Rect dirty); private static native boolean nUnlockCanvasAndPost(long nativeObject, Canvas canvas); private static native long nWrapGraphicBuffer(long nativeObject); + private static native GraphicBuffer nCreateFromHardwareBuffer(HardwareBuffer buffer); } diff --git a/graphics/java/android/graphics/GraphicsStatsService.java b/graphics/java/android/graphics/GraphicsStatsService.java new file mode 100644 index 000000000000..2d6848b618a1 --- /dev/null +++ b/graphics/java/android/graphics/GraphicsStatsService.java @@ -0,0 +1,564 @@ +/* + * 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 android.app.AlarmManager; +import android.app.AppOpsManager; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.Binder; +import android.os.Environment; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Message; +import android.os.ParcelFileDescriptor; +import android.os.Process; +import android.os.RemoteException; +import android.os.SharedMemory; +import android.os.Trace; +import android.os.UserHandle; +import android.system.ErrnoException; +import android.util.Log; +import android.view.IGraphicsStats; +import android.view.IGraphicsStatsCallback; + +import com.android.internal.util.DumpUtils; +import com.android.internal.util.FastPrintWriter; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.HashSet; +import java.util.TimeZone; + +/** + * This service's job is to collect aggregate rendering profile data. It + * does this by allowing rendering processes to request an ashmem buffer + * to place their stats into. + * + * Buffers are rotated on a daily (in UTC) basis and only the 3 most-recent days + * are kept. + * + * The primary consumer of this is incident reports and automated metric checking. It is not + * intended for end-developer consumption, for that we have gfxinfo. + * + * Buffer rotation process: + * 1) Alarm fires + * 2) onRotateGraphicsStatsBuffer() is sent to all active processes + * 3) Upon receiving the callback, the process will stop using the previous ashmem buffer and + * request a new one. + * 4) When that request is received we now know that the ashmem region is no longer in use so + * it gets queued up for saving to disk and a new ashmem region is created and returned + * for the process to use. + * + * @hide */ +public class GraphicsStatsService extends IGraphicsStats.Stub { + public static final String GRAPHICS_STATS_SERVICE = "graphicsstats"; + + private static final String TAG = "GraphicsStatsService"; + + private static final int SAVE_BUFFER = 1; + private static final int DELETE_OLD = 2; + + private static final int AID_STATSD = 1066; // Statsd uid is set to 1066 forever. + + // This isn't static because we need this to happen after registerNativeMethods, however + // the class is loaded (and thus static ctor happens) before that occurs. + private final int mAshmemSize = nGetAshmemSize(); + private final byte[] mZeroData = new byte[mAshmemSize]; + + private final Context mContext; + private final AppOpsManager mAppOps; + private final AlarmManager mAlarmManager; + private final Object mLock = new Object(); + private ArrayList<ActiveBuffer> mActive = new ArrayList<>(); + private File mGraphicsStatsDir; + private final Object mFileAccessLock = new Object(); + private Handler mWriteOutHandler; + private boolean mRotateIsScheduled = false; + + public GraphicsStatsService(Context context) { + mContext = context; + mAppOps = context.getSystemService(AppOpsManager.class); + mAlarmManager = context.getSystemService(AlarmManager.class); + File systemDataDir = new File(Environment.getDataDirectory(), "system"); + mGraphicsStatsDir = new File(systemDataDir, "graphicsstats"); + mGraphicsStatsDir.mkdirs(); + if (!mGraphicsStatsDir.exists()) { + throw new IllegalStateException("Graphics stats directory does not exist: " + + mGraphicsStatsDir.getAbsolutePath()); + } + HandlerThread bgthread = new HandlerThread("GraphicsStats-disk", + Process.THREAD_PRIORITY_BACKGROUND); + bgthread.start(); + + mWriteOutHandler = new Handler(bgthread.getLooper(), new Handler.Callback() { + @Override + public boolean handleMessage(Message msg) { + switch (msg.what) { + case SAVE_BUFFER: + saveBuffer((HistoricalBuffer) msg.obj); + break; + case DELETE_OLD: + deleteOldBuffers(); + break; + } + return true; + } + }); + nativeInit(); + } + + /** + * Current rotation policy is to rotate at midnight UTC. We don't specify RTC_WAKEUP because + * rotation can be delayed if there's otherwise no activity. However exact is used because + * we don't want the system to delay it by TOO much. + */ + private void scheduleRotateLocked() { + if (mRotateIsScheduled) { + return; + } + mRotateIsScheduled = true; + Calendar calendar = normalizeDate(System.currentTimeMillis()); + calendar.add(Calendar.DATE, 1); + mAlarmManager.setExact(AlarmManager.RTC, calendar.getTimeInMillis(), TAG, this::onAlarm, + mWriteOutHandler); + } + + private void onAlarm() { + // We need to make a copy since some of the callbacks won't be proxy and thus + // can result in a re-entrant acquisition of mLock that would result in a modification + // of mActive during iteration. + ActiveBuffer[] activeCopy; + synchronized (mLock) { + mRotateIsScheduled = false; + scheduleRotateLocked(); + activeCopy = mActive.toArray(new ActiveBuffer[0]); + } + for (ActiveBuffer active : activeCopy) { + try { + active.mCallback.onRotateGraphicsStatsBuffer(); + } catch (RemoteException e) { + Log.w(TAG, String.format("Failed to notify '%s' (pid=%d) to rotate buffers", + active.mInfo.mPackageName, active.mPid), e); + } + } + // Give a few seconds for everyone to rotate before doing the cleanup + mWriteOutHandler.sendEmptyMessageDelayed(DELETE_OLD, 10000); + } + + @Override + public ParcelFileDescriptor requestBufferForProcess(String packageName, + IGraphicsStatsCallback token) throws RemoteException { + int uid = Binder.getCallingUid(); + int pid = Binder.getCallingPid(); + ParcelFileDescriptor pfd = null; + long callingIdentity = Binder.clearCallingIdentity(); + try { + mAppOps.checkPackage(uid, packageName); + PackageInfo info = mContext.getPackageManager().getPackageInfoAsUser( + packageName, + 0, + UserHandle.getUserId(uid)); + synchronized (mLock) { + pfd = requestBufferForProcessLocked(token, uid, pid, packageName, + info.getLongVersionCode()); + } + } catch (PackageManager.NameNotFoundException ex) { + throw new RemoteException("Unable to find package: '" + packageName + "'"); + } finally { + Binder.restoreCallingIdentity(callingIdentity); + } + return pfd; + } + + // If lastFullDay is true, pullGraphicsStats returns stats for the last complete day/24h period + // that does not include today. If lastFullDay is false, pullGraphicsStats returns stats for the + // current day. + // This method is invoked from native code only. + @SuppressWarnings({"UnusedDeclaration"}) + private void pullGraphicsStats(boolean lastFullDay, long pulledData) throws RemoteException { + int uid = Binder.getCallingUid(); + + // DUMP and PACKAGE_USAGE_STATS permissions are required to invoke this method. + // TODO: remove exception for statsd daemon after required permissions are granted. statsd + // TODO: should have these permissions granted by data/etc/platform.xml, but it does not. + if (uid != AID_STATSD) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new FastPrintWriter(sw); + if (!DumpUtils.checkDumpAndUsageStatsPermission(mContext, TAG, pw)) { + pw.flush(); + throw new RemoteException(sw.toString()); + } + } + + long callingIdentity = Binder.clearCallingIdentity(); + try { + pullGraphicsStatsImpl(lastFullDay, pulledData); + } finally { + Binder.restoreCallingIdentity(callingIdentity); + } + } + + private void pullGraphicsStatsImpl(boolean lastFullDay, long pulledData) { + long targetDay; + if (lastFullDay) { + // Get stats from yesterday. Stats stay constant, because the day is over. + targetDay = normalizeDate(System.currentTimeMillis() - 86400000).getTimeInMillis(); + } else { + // Get stats from today. Stats may change as more apps are run today. + targetDay = normalizeDate(System.currentTimeMillis()).getTimeInMillis(); + } + + // Find active buffers for targetDay. + ArrayList<HistoricalBuffer> buffers; + synchronized (mLock) { + buffers = new ArrayList<>(mActive.size()); + for (int i = 0; i < mActive.size(); i++) { + ActiveBuffer buffer = mActive.get(i); + if (buffer.mInfo.mStartTime == targetDay) { + try { + buffers.add(new HistoricalBuffer(buffer)); + } catch (IOException ex) { + // Ignore + } + } + } + } + + // Dump active and historic buffers for targetDay in a serialized + // GraphicsStatsServiceDumpProto proto. + long dump = nCreateDump(-1, true); + try { + synchronized (mFileAccessLock) { + HashSet<File> skipList = dumpActiveLocked(dump, buffers); + buffers.clear(); + String subPath = String.format("%d", targetDay); + File dateDir = new File(mGraphicsStatsDir, subPath); + if (dateDir.exists()) { + for (File pkg : dateDir.listFiles()) { + for (File version : pkg.listFiles()) { + File data = new File(version, "total"); + if (skipList.contains(data)) { + continue; + } + nAddToDump(dump, data.getAbsolutePath()); + } + } + } + } + } finally { + nFinishDumpInMemory(dump, pulledData, lastFullDay); + } + } + + private ParcelFileDescriptor requestBufferForProcessLocked(IGraphicsStatsCallback token, + int uid, int pid, String packageName, long versionCode) throws RemoteException { + ActiveBuffer buffer = fetchActiveBuffersLocked(token, uid, pid, packageName, versionCode); + scheduleRotateLocked(); + return buffer.getPfd(); + } + + private Calendar normalizeDate(long timestamp) { + Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + calendar.setTimeInMillis(timestamp); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + return calendar; + } + + private File pathForApp(BufferInfo info) { + String subPath = String.format("%d/%s/%d/total", + normalizeDate(info.mStartTime).getTimeInMillis(), info.mPackageName, + info.mVersionCode); + return new File(mGraphicsStatsDir, subPath); + } + + private void saveBuffer(HistoricalBuffer buffer) { + if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) { + Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, + "saving graphicsstats for " + buffer.mInfo.mPackageName); + } + synchronized (mFileAccessLock) { + File path = pathForApp(buffer.mInfo); + File parent = path.getParentFile(); + parent.mkdirs(); + if (!parent.exists()) { + Log.w(TAG, "Unable to create path: '" + parent.getAbsolutePath() + "'"); + return; + } + nSaveBuffer(path.getAbsolutePath(), buffer.mInfo.mPackageName, + buffer.mInfo.mVersionCode, buffer.mInfo.mStartTime, buffer.mInfo.mEndTime, + buffer.mData); + } + Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER); + } + + private void deleteRecursiveLocked(File file) { + if (file.isDirectory()) { + for (File child : file.listFiles()) { + deleteRecursiveLocked(child); + } + } + if (!file.delete()) { + Log.w(TAG, "Failed to delete '" + file.getAbsolutePath() + "'!"); + } + } + + private void deleteOldBuffers() { + Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "deleting old graphicsstats buffers"); + synchronized (mFileAccessLock) { + File[] files = mGraphicsStatsDir.listFiles(); + if (files == null || files.length <= 3) { + return; + } + long[] sortedDates = new long[files.length]; + for (int i = 0; i < files.length; i++) { + try { + sortedDates[i] = Long.parseLong(files[i].getName()); + } catch (NumberFormatException ex) { + // Skip unrecognized folders + } + } + if (sortedDates.length <= 3) { + return; + } + Arrays.sort(sortedDates); + for (int i = 0; i < sortedDates.length - 3; i++) { + deleteRecursiveLocked(new File(mGraphicsStatsDir, Long.toString(sortedDates[i]))); + } + } + Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER); + } + + private void addToSaveQueue(ActiveBuffer buffer) { + try { + HistoricalBuffer data = new HistoricalBuffer(buffer); + Message.obtain(mWriteOutHandler, SAVE_BUFFER, data).sendToTarget(); + } catch (IOException e) { + Log.w(TAG, "Failed to copy graphicsstats from " + buffer.mInfo.mPackageName, e); + } + buffer.closeAllBuffers(); + } + + private void processDied(ActiveBuffer buffer) { + synchronized (mLock) { + mActive.remove(buffer); + } + addToSaveQueue(buffer); + } + + private ActiveBuffer fetchActiveBuffersLocked(IGraphicsStatsCallback token, int uid, int pid, + String packageName, long versionCode) throws RemoteException { + int size = mActive.size(); + long today = normalizeDate(System.currentTimeMillis()).getTimeInMillis(); + for (int i = 0; i < size; i++) { + ActiveBuffer buffer = mActive.get(i); + if (buffer.mPid == pid + && buffer.mUid == uid) { + // If the buffer is too old we remove it and return a new one + if (buffer.mInfo.mStartTime < today) { + buffer.binderDied(); + break; + } else { + return buffer; + } + } + } + // Didn't find one, need to create it + try { + ActiveBuffer buffers = new ActiveBuffer(token, uid, pid, packageName, versionCode); + mActive.add(buffers); + return buffers; + } catch (IOException ex) { + throw new RemoteException("Failed to allocate space"); + } + } + + private HashSet<File> dumpActiveLocked(long dump, ArrayList<HistoricalBuffer> buffers) { + HashSet<File> skipFiles = new HashSet<>(buffers.size()); + for (int i = 0; i < buffers.size(); i++) { + HistoricalBuffer buffer = buffers.get(i); + File path = pathForApp(buffer.mInfo); + skipFiles.add(path); + nAddToDump(dump, path.getAbsolutePath(), buffer.mInfo.mPackageName, + buffer.mInfo.mVersionCode, buffer.mInfo.mStartTime, buffer.mInfo.mEndTime, + buffer.mData); + } + return skipFiles; + } + + private void dumpHistoricalLocked(long dump, HashSet<File> skipFiles) { + for (File date : mGraphicsStatsDir.listFiles()) { + for (File pkg : date.listFiles()) { + for (File version : pkg.listFiles()) { + File data = new File(version, "total"); + if (skipFiles.contains(data)) { + continue; + } + nAddToDump(dump, data.getAbsolutePath()); + } + } + } + } + + @Override + protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) { + if (!DumpUtils.checkDumpAndUsageStatsPermission(mContext, TAG, fout)) return; + boolean dumpProto = false; + for (String str : args) { + if ("--proto".equals(str)) { + dumpProto = true; + break; + } + } + ArrayList<HistoricalBuffer> buffers; + synchronized (mLock) { + buffers = new ArrayList<>(mActive.size()); + for (int i = 0; i < mActive.size(); i++) { + try { + buffers.add(new HistoricalBuffer(mActive.get(i))); + } catch (IOException ex) { + // Ignore + } + } + } + long dump = nCreateDump(fd.getInt$(), dumpProto); + try { + synchronized (mFileAccessLock) { + HashSet<File> skipList = dumpActiveLocked(dump, buffers); + buffers.clear(); + dumpHistoricalLocked(dump, skipList); + } + } finally { + nFinishDump(dump); + } + } + + @Override + protected void finalize() throws Throwable { + nativeDestructor(); + } + + private native void nativeInit(); + private static native void nativeDestructor(); + + private static native int nGetAshmemSize(); + private static native long nCreateDump(int outFd, boolean isProto); + private static native void nAddToDump(long dump, String path, String packageName, + long versionCode, long startTime, long endTime, byte[] data); + private static native void nAddToDump(long dump, String path); + private static native void nFinishDump(long dump); + private static native void nFinishDumpInMemory(long dump, long pulledData, boolean lastFullDay); + private static native void nSaveBuffer(String path, String packageName, long versionCode, + long startTime, long endTime, byte[] data); + + private final class BufferInfo { + final String mPackageName; + final long mVersionCode; + long mStartTime; + long mEndTime; + + BufferInfo(String packageName, long versionCode, long startTime) { + this.mPackageName = packageName; + this.mVersionCode = versionCode; + this.mStartTime = startTime; + } + } + + private final class ActiveBuffer implements DeathRecipient { + final BufferInfo mInfo; + final int mUid; + final int mPid; + final IGraphicsStatsCallback mCallback; + final IBinder mToken; + SharedMemory mProcessBuffer; + ByteBuffer mMapping; + + ActiveBuffer(IGraphicsStatsCallback token, int uid, int pid, String packageName, + long versionCode) + throws RemoteException, IOException { + mInfo = new BufferInfo(packageName, versionCode, System.currentTimeMillis()); + mUid = uid; + mPid = pid; + mCallback = token; + mToken = mCallback.asBinder(); + mToken.linkToDeath(this, 0); + try { + mProcessBuffer = SharedMemory.create("GFXStats-" + pid, mAshmemSize); + mMapping = mProcessBuffer.mapReadWrite(); + } catch (ErrnoException ex) { + ex.rethrowAsIOException(); + } + mMapping.position(0); + mMapping.put(mZeroData, 0, mAshmemSize); + } + + @Override + public void binderDied() { + mToken.unlinkToDeath(this, 0); + processDied(this); + } + + void closeAllBuffers() { + if (mMapping != null) { + SharedMemory.unmap(mMapping); + mMapping = null; + } + if (mProcessBuffer != null) { + mProcessBuffer.close(); + mProcessBuffer = null; + } + } + + ParcelFileDescriptor getPfd() { + try { + return mProcessBuffer.getFdDup(); + } catch (IOException ex) { + throw new IllegalStateException("Failed to get PFD from memory file", ex); + } + } + + void readBytes(byte[] buffer, int count) throws IOException { + if (mMapping == null) { + throw new IOException("SharedMemory has been deactivated"); + } + mMapping.position(0); + mMapping.get(buffer, 0, count); + } + } + + private final class HistoricalBuffer { + final BufferInfo mInfo; + final byte[] mData = new byte[mAshmemSize]; + HistoricalBuffer(ActiveBuffer active) throws IOException { + mInfo = active.mInfo; + mInfo.mEndTime = System.currentTimeMillis(); + active.readBytes(mData, mAshmemSize); + } + } +} diff --git a/graphics/java/android/graphics/HardwareRenderer.java b/graphics/java/android/graphics/HardwareRenderer.java index b6b2d4e1c46a..d08bfcf45a5c 100644 --- a/graphics/java/android/graphics/HardwareRenderer.java +++ b/graphics/java/android/graphics/HardwareRenderer.java @@ -28,7 +28,6 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.util.Log; import android.util.TimeUtils; -import android.view.FrameMetricsObserver; import android.view.IGraphicsStats; import android.view.IGraphicsStatsCallback; import android.view.NativeVectorDrawableAnimator; @@ -38,8 +37,6 @@ import android.view.SurfaceHolder; import android.view.TextureLayer; import android.view.animation.AnimationUtils; -import com.android.internal.util.VirtualRefBasePtr; - import java.io.File; import java.io.FileDescriptor; import java.lang.annotation.Retention; @@ -160,7 +157,7 @@ public class HardwareRenderer { public HardwareRenderer() { mRootNode = RenderNode.adopt(nCreateRootRenderNode()); mRootNode.setClipToBounds(false); - mNativeProxy = nCreateProxy(!mOpaque, mRootNode.mNativeRenderNode); + mNativeProxy = nCreateProxy(!mOpaque, mIsWideGamut, mRootNode.mNativeRenderNode); if (mNativeProxy == 0) { throw new OutOfMemoryError("Unable to create hardware renderer"); } @@ -286,10 +283,24 @@ public class HardwareRenderer { * non-null then {@link Surface#isValid()} must be true. */ public void setSurface(@Nullable Surface surface) { + setSurface(surface, false); + } + + /** + * See {@link #setSurface(Surface)} + * + * @hide + * @param discardBuffer determines whether the surface will attempt to preserve its contents + * between frames. If set to true the renderer will attempt to preserve + * the contents of the buffer between frames if the implementation allows + * it. If set to false no attempt will be made to preserve the buffer's + * contents between frames. + */ + public void setSurface(@Nullable Surface surface, boolean discardBuffer) { if (surface != null && !surface.isValid()) { throw new IllegalArgumentException("Surface is invalid. surface.isValid() == false."); } - nSetSurface(mNativeProxy, surface); + nSetSurface(mNativeProxy, surface, discardBuffer); } /** @@ -584,9 +595,8 @@ public class HardwareRenderer { * * @hide */ - public void addFrameMetricsObserver(FrameMetricsObserver observer) { - long nativeObserver = nAddFrameMetricsObserver(mNativeProxy, observer); - observer.mNative = new VirtualRefBasePtr(nativeObserver); + public void addObserver(HardwareRendererObserver observer) { + nAddObserver(mNativeProxy, observer.getNativeInstance()); } /** @@ -594,9 +604,8 @@ public class HardwareRenderer { * * @hide */ - public void removeFrameMetricsObserver(FrameMetricsObserver observer) { - nRemoveFrameMetricsObserver(mNativeProxy, observer.mNative.get()); - observer.mNative = null; + public void removeObserver(HardwareRendererObserver observer) { + nRemoveObserver(mNativeProxy, observer.getNativeInstance()); } /** @@ -1076,7 +1085,8 @@ public class HardwareRenderer { private static native long nCreateRootRenderNode(); - private static native long nCreateProxy(boolean translucent, long rootRenderNode); + private static native long nCreateProxy(boolean translucent, boolean isWideGamut, + long rootRenderNode); private static native void nDeleteProxy(long nativeProxy); @@ -1084,7 +1094,7 @@ public class HardwareRenderer { private static native void nSetName(long nativeProxy, String name); - private static native void nSetSurface(long nativeProxy, Surface window); + private static native void nSetSurface(long nativeProxy, Surface window, boolean discardBuffer); private static native boolean nPause(long nativeProxy); @@ -1156,10 +1166,9 @@ public class HardwareRenderer { private static native void nSetFrameCompleteCallback(long nativeProxy, FrameCompleteCallback callback); - private static native long nAddFrameMetricsObserver(long nativeProxy, - FrameMetricsObserver observer); + private static native void nAddObserver(long nativeProxy, long nativeObserver); - private static native void nRemoveFrameMetricsObserver(long nativeProxy, long nativeObserver); + private static native void nRemoveObserver(long nativeProxy, long nativeObserver); private static native int nCopySurfaceInto(Surface surface, int srcLeft, int srcTop, int srcRight, int srcBottom, long bitmapHandle); diff --git a/graphics/java/android/graphics/HardwareRendererObserver.java b/graphics/java/android/graphics/HardwareRendererObserver.java new file mode 100644 index 000000000000..da9d03c639f7 --- /dev/null +++ b/graphics/java/android/graphics/HardwareRendererObserver.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2019 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.annotation.NonNull; +import android.os.Handler; + +import com.android.internal.util.VirtualRefBasePtr; + +/** + * Provides streaming access to frame stats information from HardwareRenderer to apps. + * + * @hide + */ +public class HardwareRendererObserver { + private final long[] mFrameMetrics; + private final Handler mHandler; + private final OnFrameMetricsAvailableListener mListener; + private VirtualRefBasePtr mNativePtr; + + /** + * Interface for clients that want frame timing information for each frame rendered. + * @hide + */ + public interface OnFrameMetricsAvailableListener { + /** + * Called when information is available for the previously rendered frame. + * + * Reports can be dropped if this callback takes too long to execute, as the report producer + * cannot wait for the consumer to complete. + * + * It is highly recommended that clients copy the metrics array within this method + * and defer additional computation or storage to another thread to avoid unnecessarily + * dropping reports. + * + * @param dropCountSinceLastInvocation the number of reports dropped since the last time + * this callback was invoked. + */ + void onFrameMetricsAvailable(int dropCountSinceLastInvocation); + } + + /** + * Creates a FrameMetricsObserver + * + * @param frameMetrics the available metrics. This array is reused on every call to the listener + * and thus <strong>this reference should only be used within the scope of the listener callback + * as data is not guaranteed to be valid outside the scope of that method</strong>. + * @param handler the Handler to use when invoking callbacks + */ + public HardwareRendererObserver(@NonNull OnFrameMetricsAvailableListener listener, + @NonNull long[] frameMetrics, @NonNull Handler handler) { + if (handler == null || handler.getLooper() == null) { + throw new NullPointerException("handler and its looper cannot be null"); + } + + if (handler.getLooper().getQueue() == null) { + throw new IllegalStateException("invalid looper, null message queue\n"); + } + + mFrameMetrics = frameMetrics; + mHandler = handler; + mListener = listener; + mNativePtr = new VirtualRefBasePtr(nCreateObserver()); + } + + /*package*/ long getNativeInstance() { + return mNativePtr.get(); + } + + // Called by native on the provided Handler + @SuppressWarnings("unused") + private void notifyDataAvailable() { + mHandler.post(() -> { + boolean hasMoreData = true; + while (hasMoreData) { + // a drop count of -1 is a sentinel that no more buffers are available + int dropCount = nGetNextBuffer(mNativePtr.get(), mFrameMetrics); + if (dropCount >= 0) { + mListener.onFrameMetricsAvailable(dropCount); + } else { + hasMoreData = false; + } + } + }); + } + + private native long nCreateObserver(); + private static native int nGetNextBuffer(long nativePtr, long[] data); +} diff --git a/graphics/java/android/graphics/ImageDecoder.java b/graphics/java/android/graphics/ImageDecoder.java index dbdb6971c6c5..c8f065ad094c 100644 --- a/graphics/java/android/graphics/ImageDecoder.java +++ b/graphics/java/android/graphics/ImageDecoder.java @@ -70,9 +70,9 @@ import java.util.concurrent.atomic.AtomicBoolean; * {@link Bitmap} objects. * * <p>To use it, first create a {@link Source Source} using one of the - * {@code createSource} overloads. For example, to decode from a {@link File}, call - * {@link #createSource(File)} and pass the result to {@link #decodeDrawable(Source)} - * or {@link #decodeBitmap(Source)}: + * {@code createSource} overloads. For example, to decode from a {@link Uri}, call + * {@link #createSource(ContentResolver, Uri)} and pass the result to + * {@link #decodeDrawable(Source)} or {@link #decodeBitmap(Source)}: * * <pre class="prettyprint"> * File file = new File(...); @@ -214,7 +214,7 @@ public final class ImageDecoder implements AutoCloseable { /* @hide */ @NonNull - abstract ImageDecoder createImageDecoder() throws IOException; + abstract ImageDecoder createImageDecoder(boolean preferAnimation) throws IOException; }; private static class ByteArraySource extends Source { @@ -228,8 +228,8 @@ public final class ImageDecoder implements AutoCloseable { private final int mLength; @Override - public ImageDecoder createImageDecoder() throws IOException { - return nCreate(mData, mOffset, mLength, this); + public ImageDecoder createImageDecoder(boolean preferAnimation) throws IOException { + return nCreate(mData, mOffset, mLength, preferAnimation, this); } } @@ -240,14 +240,14 @@ public final class ImageDecoder implements AutoCloseable { private final ByteBuffer mBuffer; @Override - public ImageDecoder createImageDecoder() throws IOException { + public ImageDecoder createImageDecoder(boolean preferAnimation) throws IOException { if (!mBuffer.isDirect() && mBuffer.hasArray()) { int offset = mBuffer.arrayOffset() + mBuffer.position(); int length = mBuffer.limit() - mBuffer.position(); - return nCreate(mBuffer.array(), offset, length, this); + return nCreate(mBuffer.array(), offset, length, preferAnimation, this); } ByteBuffer buffer = mBuffer.slice(); - return nCreate(buffer, buffer.position(), buffer.limit(), this); + return nCreate(buffer, buffer.position(), buffer.limit(), preferAnimation, this); } } @@ -267,16 +267,20 @@ public final class ImageDecoder implements AutoCloseable { Resources getResources() { return mResources; } @Override - public ImageDecoder createImageDecoder() throws IOException { + public ImageDecoder createImageDecoder(boolean preferAnimation) throws IOException { AssetFileDescriptor assetFd = null; try { - if (mUri.getScheme() == ContentResolver.SCHEME_CONTENT) { + if (ContentResolver.SCHEME_CONTENT.equals(mUri.getScheme())) { assetFd = mResolver.openTypedAssetFileDescriptor(mUri, "image/*", null); } else { assetFd = mResolver.openAssetFileDescriptor(mUri, "r"); } } catch (FileNotFoundException e) { + // Handled below, along with the case where assetFd was set to null. + } + + if (assetFd == null) { // Some images cannot be opened as AssetFileDescriptors (e.g. // bmp, ico). Open them as InputStreams. InputStream is = mResolver.openInputStream(mUri); @@ -284,26 +288,27 @@ public final class ImageDecoder implements AutoCloseable { throw new FileNotFoundException(mUri.toString()); } - return createFromStream(is, true, this); + return createFromStream(is, true, preferAnimation, this); } - return createFromAssetFileDescriptor(assetFd, this); + + return createFromAssetFileDescriptor(assetFd, preferAnimation, this); } } @NonNull private static ImageDecoder createFromFile(@NonNull File file, - @NonNull Source source) throws IOException { + boolean preferAnimation, @NonNull Source source) throws IOException { FileInputStream stream = new FileInputStream(file); FileDescriptor fd = stream.getFD(); try { Os.lseek(fd, 0, SEEK_CUR); } catch (ErrnoException e) { - return createFromStream(stream, true, source); + return createFromStream(stream, true, preferAnimation, source); } ImageDecoder decoder = null; try { - decoder = nCreate(fd, source); + decoder = nCreate(fd, preferAnimation, source); } finally { if (decoder == null) { IoUtils.closeQuietly(stream); @@ -317,12 +322,12 @@ public final class ImageDecoder implements AutoCloseable { @NonNull private static ImageDecoder createFromStream(@NonNull InputStream is, - boolean closeInputStream, Source source) throws IOException { + boolean closeInputStream, boolean preferAnimation, Source source) throws IOException { // Arbitrary size matches BitmapFactory. byte[] storage = new byte[16 * 1024]; ImageDecoder decoder = null; try { - decoder = nCreate(is, storage, source); + decoder = nCreate(is, storage, preferAnimation, source); } finally { if (decoder == null) { if (closeInputStream) { @@ -340,7 +345,10 @@ public final class ImageDecoder implements AutoCloseable { @NonNull private static ImageDecoder createFromAssetFileDescriptor(@NonNull AssetFileDescriptor assetFd, - Source source) throws IOException { + boolean preferAnimation, Source source) throws IOException { + if (assetFd == null) { + throw new FileNotFoundException(); + } final FileDescriptor fd = assetFd.getFileDescriptor(); final long offset = assetFd.getStartOffset(); @@ -348,9 +356,9 @@ public final class ImageDecoder implements AutoCloseable { try { try { Os.lseek(fd, offset, SEEK_SET); - decoder = nCreate(fd, source); + decoder = nCreate(fd, preferAnimation, source); } catch (ErrnoException e) { - decoder = createFromStream(new FileInputStream(fd), true, source); + decoder = createFromStream(new FileInputStream(fd), true, preferAnimation, source); } } finally { if (decoder == null) { @@ -388,7 +396,7 @@ public final class ImageDecoder implements AutoCloseable { public int getDensity() { return mInputDensity; } @Override - public ImageDecoder createImageDecoder() throws IOException { + public ImageDecoder createImageDecoder(boolean preferAnimation) throws IOException { synchronized (this) { if (mInputStream == null) { @@ -396,7 +404,7 @@ public final class ImageDecoder implements AutoCloseable { } InputStream is = mInputStream; mInputStream = null; - return createFromStream(is, false, this); + return createFromStream(is, false, preferAnimation, this); } } } @@ -434,14 +442,14 @@ public final class ImageDecoder implements AutoCloseable { } @Override - public ImageDecoder createImageDecoder() throws IOException { + public ImageDecoder createImageDecoder(boolean preferAnimation) throws IOException { synchronized (this) { if (mAssetInputStream == null) { throw new IOException("Cannot reuse AssetInputStreamSource"); } AssetInputStream ais = mAssetInputStream; mAssetInputStream = null; - return createFromAsset(ais, this); + return createFromAsset(ais, preferAnimation, this); } } } @@ -469,7 +477,7 @@ public final class ImageDecoder implements AutoCloseable { } @Override - public ImageDecoder createImageDecoder() throws IOException { + public ImageDecoder createImageDecoder(boolean preferAnimation) throws IOException { TypedValue value = new TypedValue(); // This is just used in order to access the underlying Asset and // keep it alive. @@ -483,7 +491,7 @@ public final class ImageDecoder implements AutoCloseable { } } - return createFromAsset((AssetInputStream) is, this); + return createFromAsset((AssetInputStream) is, preferAnimation, this); } } @@ -491,11 +499,11 @@ public final class ImageDecoder implements AutoCloseable { * ImageDecoder will own the AssetInputStream. */ private static ImageDecoder createFromAsset(AssetInputStream ais, - Source source) throws IOException { + boolean preferAnimation, Source source) throws IOException { ImageDecoder decoder = null; try { long asset = ais.getNativeAsset(); - decoder = nCreate(asset, source); + decoder = nCreate(asset, preferAnimation, source); } finally { if (decoder == null) { IoUtils.closeQuietly(ais); @@ -517,9 +525,9 @@ public final class ImageDecoder implements AutoCloseable { private final String mFileName; @Override - public ImageDecoder createImageDecoder() throws IOException { + public ImageDecoder createImageDecoder(boolean preferAnimation) throws IOException { InputStream is = mAssets.open(mFileName); - return createFromAsset((AssetInputStream) is, this); + return createFromAsset((AssetInputStream) is, preferAnimation, this); } } @@ -531,8 +539,8 @@ public final class ImageDecoder implements AutoCloseable { private final File mFile; @Override - public ImageDecoder createImageDecoder() throws IOException { - return createFromFile(mFile, this); + public ImageDecoder createImageDecoder(boolean preferAnimation) throws IOException { + return createFromFile(mFile, preferAnimation, this); } } @@ -544,7 +552,7 @@ public final class ImageDecoder implements AutoCloseable { private final Callable<AssetFileDescriptor> mCallable; @Override - public ImageDecoder createImageDecoder() throws IOException { + public ImageDecoder createImageDecoder(boolean preferAnimation) throws IOException { AssetFileDescriptor assetFd = null; try { assetFd = mCallable.call(); @@ -555,7 +563,7 @@ public final class ImageDecoder implements AutoCloseable { throw new IOException(e); } } - return createFromAssetFileDescriptor(assetFd, this); + return createFromAssetFileDescriptor(assetFd, preferAnimation, this); } } @@ -1024,7 +1032,11 @@ public final class ImageDecoder implements AutoCloseable { /** * Create a new {@link Source Source} from a {@link java.io.File}. - * + * <p> + * This method should only be used for files that you have direct access to; + * if you'd like to work with files hosted outside your app, use an API like + * {@link #createSource(Callable)} or + * {@link #createSource(ContentResolver, Uri)}. * @return a new Source object, which can be passed to * {@link #decodeDrawable decodeDrawable} or * {@link #decodeBitmap decodeBitmap}. @@ -1669,6 +1681,9 @@ public final class ImageDecoder implements AutoCloseable { if (r == null) { return; } + if (r.width() <= 0 || r.height() <= 0) { + throw new IllegalStateException("Subset " + r + " is empty/unsorted"); + } if (r.left < 0 || r.top < 0 || r.right > width || r.bottom > height) { throw new IllegalStateException("Subset " + r + " not contained by " + "scaled image bounds: (" + width + " x " + height + ")"); @@ -1740,7 +1755,7 @@ public final class ImageDecoder implements AutoCloseable { @NonNull private static Drawable decodeDrawableImpl(@NonNull Source src, @Nullable OnHeaderDecodedListener listener) throws IOException { - try (ImageDecoder decoder = src.createImageDecoder()) { + try (ImageDecoder decoder = src.createImageDecoder(true /*preferAnimation*/)) { decoder.mSource = src; decoder.callHeaderDecoded(listener, src); @@ -1844,7 +1859,7 @@ public final class ImageDecoder implements AutoCloseable { @NonNull private static Bitmap decodeBitmapImpl(@NonNull Source src, @Nullable OnHeaderDecodedListener listener) throws IOException { - try (ImageDecoder decoder = src.createImageDecoder()) { + try (ImageDecoder decoder = src.createImageDecoder(false /*preferAnimation*/)) { decoder.mSource = src; decoder.callHeaderDecoded(listener, src); @@ -1971,15 +1986,17 @@ public final class ImageDecoder implements AutoCloseable { } } - private static native ImageDecoder nCreate(long asset, Source src) throws IOException; - private static native ImageDecoder nCreate(ByteBuffer buffer, int position, - int limit, Source src) throws IOException; + private static native ImageDecoder nCreate(long asset, + boolean preferAnimation, Source src) throws IOException; + private static native ImageDecoder nCreate(ByteBuffer buffer, int position, int limit, + boolean preferAnimation, Source src) throws IOException; private static native ImageDecoder nCreate(byte[] data, int offset, int length, - Source src) throws IOException; + boolean preferAnimation, Source src) throws IOException; private static native ImageDecoder nCreate(InputStream is, byte[] storage, - Source src) throws IOException; + boolean preferAnimation, Source src) throws IOException; // The fd must be seekable. - private static native ImageDecoder nCreate(FileDescriptor fd, Source src) throws IOException; + private static native ImageDecoder nCreate(FileDescriptor fd, + boolean preferAnimation, Source src) throws IOException; @NonNull private static native Bitmap nDecodeBitmap(long nativePtr, @NonNull ImageDecoder decoder, diff --git a/graphics/java/android/graphics/Outline.java b/graphics/java/android/graphics/Outline.java index 91a60c327bf0..5858e3988486 100644 --- a/graphics/java/android/graphics/Outline.java +++ b/graphics/java/android/graphics/Outline.java @@ -43,7 +43,7 @@ public final class Outline { /** @hide */ public static final int MODE_ROUND_RECT = 1; /** @hide */ - public static final int MODE_CONVEX_PATH = 2; + public static final int MODE_PATH = 2; /** @hide */ @Retention(RetentionPolicy.SOURCE) @@ -51,7 +51,7 @@ public final class Outline { value = { MODE_EMPTY, MODE_ROUND_RECT, - MODE_CONVEX_PATH, + MODE_PATH, }) public @interface Mode {} @@ -60,7 +60,7 @@ public final class Outline { public int mMode = MODE_EMPTY; /** - * Only guaranteed to be non-null when mode == MODE_CONVEX_PATH + * Only guaranteed to be non-null when mode == MODE_PATH * * @hide */ @@ -124,7 +124,7 @@ public final class Outline { * @see android.view.View#setClipToOutline(boolean) */ public boolean canClip() { - return mMode != MODE_CONVEX_PATH; + return mMode != MODE_PATH; } /** @@ -157,7 +157,7 @@ public final class Outline { */ public void set(@NonNull Outline src) { mMode = src.mMode; - if (src.mMode == MODE_CONVEX_PATH) { + if (src.mMode == MODE_PATH) { if (mPath == null) { mPath = new Path(); } @@ -194,7 +194,7 @@ public final class Outline { return; } - if (mMode == MODE_CONVEX_PATH) { + if (mMode == MODE_PATH) { // rewind here to avoid thrashing the allocations, but could alternately clear ref mPath.rewind(); } @@ -213,7 +213,7 @@ public final class Outline { /** * Populates {@code outBounds} with the outline bounds, if set, and returns * {@code true}. If no outline bounds are set, or if a path has been set - * via {@link #setConvexPath(Path)}, returns {@code false}. + * via {@link #setPath(Path)}, returns {@code false}. * * @param outRect the rect to populate with the outline bounds, if set * @return {@code true} if {@code outBounds} was populated with outline @@ -229,7 +229,7 @@ public final class Outline { /** * Returns the rounded rect radius, if set, or a value less than 0 if a path has - * been set via {@link #setConvexPath(Path)}. A return value of {@code 0} + * been set via {@link #setPath(Path)}. A return value of {@code 0} * indicates a non-rounded rect. * * @return the rounded rect radius, or value < 0 @@ -259,7 +259,7 @@ public final class Outline { mPath.rewind(); } - mMode = MODE_CONVEX_PATH; + mMode = MODE_PATH; mPath.addOval(left, top, right, bottom, Path.Direction.CW); mRect.setEmpty(); mRadius = RADIUS_UNDEFINED; @@ -279,9 +279,24 @@ public final class Outline { * @param convexPath used to construct the Outline. As of * {@link android.os.Build.VERSION_CODES#Q}, it is no longer required to be * convex. + * + * @deprecated As of {@link android.os.Build.VERSION_CODES#Q}, the restriction + * that the path must be convex is removed. However, the API is misnamed until + * {@link android.os.Build.VERSION_CODES#R}, when {@link #setPath} is + * introduced. Use {@link #setPath} instead. */ + @Deprecated public void setConvexPath(@NonNull Path convexPath) { - if (convexPath.isEmpty()) { + setPath(convexPath); + } + + /** + * Sets the Outline to a {@link android.graphics.Path path}. + * + * @param path used to construct the Outline. + */ + public void setPath(@NonNull Path path) { + if (path.isEmpty()) { setEmpty(); return; } @@ -290,8 +305,8 @@ public final class Outline { mPath = new Path(); } - mMode = MODE_CONVEX_PATH; - mPath.set(convexPath); + mMode = MODE_PATH; + mPath.set(path); mRect.setEmpty(); mRadius = RADIUS_UNDEFINED; } @@ -302,7 +317,7 @@ public final class Outline { public void offset(int dx, int dy) { if (mMode == MODE_ROUND_RECT) { mRect.offset(dx, dy); - } else if (mMode == MODE_CONVEX_PATH) { + } else if (mMode == MODE_PATH) { mPath.offset(dx, dy); } } diff --git a/graphics/java/android/graphics/ParcelableColorSpace.java b/graphics/java/android/graphics/ParcelableColorSpace.java new file mode 100644 index 000000000000..f9033a53d7e6 --- /dev/null +++ b/graphics/java/android/graphics/ParcelableColorSpace.java @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2020 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.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * A {@link Parcelable} {@link ColorSpace}. In order to enable parceling, the ColorSpace + * must be either a {@link ColorSpace.Named Named} ColorSpace or a {@link ColorSpace.Rgb} instance + * that has an ICC parametric transfer function as returned by {@link Rgb#getTransferParameters()}. + * TODO: Make public + * @hide + */ +public final class ParcelableColorSpace extends ColorSpace implements Parcelable { + private final ColorSpace mColorSpace; + + /** + * Checks if the given ColorSpace is able to be parceled. A ColorSpace can only be + * parceled if it is a {@link ColorSpace.Named Named} ColorSpace or a {@link ColorSpace.Rgb} + * instance that has an ICC parametric transfer function as returned by + * {@link Rgb#getTransferParameters()} + */ + public static boolean isParcelable(@NonNull ColorSpace colorSpace) { + if (colorSpace.getId() == ColorSpace.MIN_ID) { + if (!(colorSpace instanceof ColorSpace.Rgb)) { + return false; + } + ColorSpace.Rgb rgb = (ColorSpace.Rgb) colorSpace; + if (rgb.getTransferParameters() == null) { + return false; + } + } + return true; + } + + /** + * Constructs a new ParcelableColorSpace that wraps the provided ColorSpace. + * + * @param colorSpace The ColorSpace to wrap. The ColorSpace must be either named or be an + * RGB ColorSpace with an ICC parametric transfer function. + * @throws IllegalArgumentException If the provided ColorSpace does not satisfy the requirements + * to be parceled. See {@link #isParcelable(ColorSpace)}. + */ + public ParcelableColorSpace(@NonNull ColorSpace colorSpace) { + super(colorSpace.getName(), colorSpace.getModel(), colorSpace.getId()); + mColorSpace = colorSpace; + + if (mColorSpace.getId() == ColorSpace.MIN_ID) { + if (!(mColorSpace instanceof ColorSpace.Rgb)) { + throw new IllegalArgumentException( + "Unable to parcel unknown ColorSpaces that are not ColorSpace.Rgb"); + } + ColorSpace.Rgb rgb = (ColorSpace.Rgb) mColorSpace; + if (rgb.getTransferParameters() == null) { + throw new IllegalArgumentException("ColorSpace must use an ICC " + + "parametric transfer function to be parcelable"); + } + } + } + + public @NonNull ColorSpace getColorSpace() { + return mColorSpace; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + final int id = mColorSpace.getId(); + dest.writeInt(id); + if (id == ColorSpace.MIN_ID) { + // Not a named color space. We have to actually write, like, stuff. And things. Ugh. + // Cast is safe because this was asserted in the constructor + ColorSpace.Rgb rgb = (ColorSpace.Rgb) mColorSpace; + dest.writeString(rgb.getName()); + dest.writeFloatArray(rgb.getPrimaries()); + dest.writeFloatArray(rgb.getWhitePoint()); + ColorSpace.Rgb.TransferParameters transferParameters = rgb.getTransferParameters(); + dest.writeDouble(transferParameters.a); + dest.writeDouble(transferParameters.b); + dest.writeDouble(transferParameters.c); + dest.writeDouble(transferParameters.d); + dest.writeDouble(transferParameters.e); + dest.writeDouble(transferParameters.f); + dest.writeDouble(transferParameters.g); + } + } + + @NonNull + public static final Parcelable.Creator<ParcelableColorSpace> CREATOR = + new Parcelable.Creator<ParcelableColorSpace>() { + + public @NonNull ParcelableColorSpace createFromParcel(@NonNull Parcel in) { + final int id = in.readInt(); + if (id == ColorSpace.MIN_ID) { + String name = in.readString(); + float[] primaries = in.createFloatArray(); + float[] whitePoint = in.createFloatArray(); + double a = in.readDouble(); + double b = in.readDouble(); + double c = in.readDouble(); + double d = in.readDouble(); + double e = in.readDouble(); + double f = in.readDouble(); + double g = in.readDouble(); + ColorSpace.Rgb.TransferParameters function = + new ColorSpace.Rgb.TransferParameters(a, b, c, d, e, f, g); + return new ParcelableColorSpace( + new ColorSpace.Rgb(name, primaries, whitePoint, function)); + } else { + return new ParcelableColorSpace(ColorSpace.get(id)); + } + } + + public ParcelableColorSpace[] newArray(int size) { + return new ParcelableColorSpace[size]; + } + }; + + @Override + public boolean isWideGamut() { + return mColorSpace.isWideGamut(); + } + + @Override + public float getMinValue(int component) { + return mColorSpace.getMinValue(component); + } + + @Override + public float getMaxValue(int component) { + return mColorSpace.getMaxValue(component); + } + + @Override + public @NonNull float[] toXyz(@NonNull float[] v) { + return mColorSpace.toXyz(v); + } + + @Override + public @NonNull float[] fromXyz(@NonNull float[] v) { + return mColorSpace.fromXyz(v); + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ParcelableColorSpace other = (ParcelableColorSpace) o; + return mColorSpace.equals(other.mColorSpace); + } + + @Override + public int hashCode() { + return mColorSpace.hashCode(); + } + + /** @hide */ + @Override + long getNativeInstance() { + return mColorSpace.getNativeInstance(); + } +} diff --git a/graphics/java/android/graphics/Path.java b/graphics/java/android/graphics/Path.java index 1362fd864d29..84fe39290681 100644 --- a/graphics/java/android/graphics/Path.java +++ b/graphics/java/android/graphics/Path.java @@ -209,7 +209,13 @@ public class Path { * points, and cache the result. * * @return True if the path is convex. + * + * @deprecated This method is not reliable. The way convexity is computed may change from + * release to release, and convexity could change based on a matrix as well. This method was + * useful when non-convex Paths were unable to be used in certain contexts, but that is no + * longer the case. */ + @Deprecated public boolean isConvex() { return nIsConvex(mNativePath); } diff --git a/graphics/java/android/graphics/Point.java b/graphics/java/android/graphics/Point.java index 3614f3bcb3be..9f71a0fedd05 100644 --- a/graphics/java/android/graphics/Point.java +++ b/graphics/java/android/graphics/Point.java @@ -132,7 +132,7 @@ public class Point implements Parcelable { * @param fieldId Field Id of the Rect as defined in the parent message * @hide */ - public void writeToProto(@NonNull ProtoOutputStream protoOutputStream, long fieldId) { + public void dumpDebug(@NonNull ProtoOutputStream protoOutputStream, long fieldId) { final long token = protoOutputStream.start(fieldId); protoOutputStream.write(PointProto.X, x); protoOutputStream.write(PointProto.Y, y); diff --git a/graphics/java/android/graphics/PointF.java b/graphics/java/android/graphics/PointF.java index f1f48adc7018..ed9df145f880 100644 --- a/graphics/java/android/graphics/PointF.java +++ b/graphics/java/android/graphics/PointF.java @@ -38,6 +38,18 @@ public class PointF implements Parcelable { this.x = p.x; this.y = p.y; } + + /** + * Create a new PointF initialized with the values in the specified + * PointF (which is left unmodified). + * + * @param p The point whose values are copied into the new + * point. + */ + public PointF(@NonNull PointF p) { + this.x = p.x; + this.y = p.y; + } /** * Set the point's x and y coordinates diff --git a/graphics/java/android/graphics/PorterDuff.java b/graphics/java/android/graphics/PorterDuff.java index 1275cb9ca4f9..eb940e2f9017 100644 --- a/graphics/java/android/graphics/PorterDuff.java +++ b/graphics/java/android/graphics/PorterDuff.java @@ -30,8 +30,6 @@ public class PorterDuff { /** * {@usesMathJax} * - * <h3>Porter-Duff</h3> - * * <p>The name of the parent class is an homage to the work of Thomas Porter and * Tom Duff, presented in their seminal 1984 paper titled "Compositing Digital Images". * In this paper, the authors describe 12 compositing operators that govern how to diff --git a/graphics/java/android/graphics/Rect.java b/graphics/java/android/graphics/Rect.java index 270725ab9805..081b851d1333 100644 --- a/graphics/java/android/graphics/Rect.java +++ b/graphics/java/android/graphics/Rect.java @@ -239,7 +239,7 @@ public final class Rect implements Parcelable { * @param fieldId Field Id of the Rect as defined in the parent message * @hide */ - public void writeToProto(@NonNull ProtoOutputStream protoOutputStream, long fieldId) { + public void dumpDebug(@NonNull ProtoOutputStream protoOutputStream, long fieldId) { final long token = protoOutputStream.start(fieldId); protoOutputStream.write(RectProto.LEFT, left); protoOutputStream.write(RectProto.TOP, top); diff --git a/graphics/java/android/graphics/RenderNode.java b/graphics/java/android/graphics/RenderNode.java index c3c56dbbc6aa..22f5489be3b1 100644 --- a/graphics/java/android/graphics/RenderNode.java +++ b/graphics/java/android/graphics/RenderNode.java @@ -22,8 +22,8 @@ import android.annotation.FloatRange; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.graphics.animation.RenderNodeAnimator; import android.view.NativeVectorDrawableAnimator; -import android.view.RenderNodeAnimator; import android.view.Surface; import android.view.View; @@ -63,7 +63,7 @@ import java.lang.annotation.RetentionPolicy; * <h3>Creating a RenderNode</h3> * <pre class="prettyprint"> * RenderNode renderNode = new RenderNode("myRenderNode"); - * renderNode.setLeftTopRightBottom(0, 0, 50, 50); // Set the size to 50x50 + * renderNode.setPosition(0, 0, 50, 50); // Set the size to 50x50 * RecordingCanvas canvas = renderNode.beginRecording(); * try { * // Draw with the canvas @@ -104,7 +104,7 @@ import java.lang.annotation.RetentionPolicy; * <pre class="prettyprint"> * private void createDisplayList() { * mRenderNode = new RenderNode("MyRenderNode"); - * mRenderNode.setLeftTopRightBottom(0, 0, width, height); + * mRenderNode.setPosition(0, 0, width, height); * RecordingCanvas canvas = mRenderNode.beginRecording(); * try { * for (Bitmap b : mBitmaps) { @@ -687,8 +687,8 @@ public final class RenderNode { outline.mRect.left, outline.mRect.top, outline.mRect.right, outline.mRect.bottom, outline.mRadius, outline.mAlpha); - case Outline.MODE_CONVEX_PATH: - return nSetOutlineConvexPath(mNativeRenderNode, outline.mPath.mNativePath, + case Outline.MODE_PATH: + return nSetOutlinePath(mNativeRenderNode, outline.mPath.mNativePath, outline.mAlpha); } @@ -1380,7 +1380,22 @@ public final class RenderNode { * @return Approximate memory usage in bytes. */ public @BytesLong long computeApproximateMemoryUsage() { - return nGetDebugSize(mNativeRenderNode); + return nGetUsageSize(mNativeRenderNode); + } + + /** + * Gets the approximate amount of memory allocated for the RenderNode for debug purposes. + * Does not include the memory allocated by any child RenderNodes nor any bitmaps, only the + * memory allocated for this RenderNode and any data it owns. + * + * The difference between this and {@link #computeApproximateMemoryUsage()} is this includes + * memory allocated but not used. In particular structures such as DisplayLists are similar + * to things like ArrayLists - they need to resize as commands are added to them. As such, + * memory used can be less than memory allocated. + * + * @hide */ + public @BytesLong long computeApproximateMemoryAllocated() { + return nGetAllocatedSize(mNativeRenderNode); } /** @@ -1485,7 +1500,8 @@ public final class RenderNode { private static native void nOutput(long renderNode); - private static native int nGetDebugSize(long renderNode); + private static native int nGetUsageSize(long renderNode); + private static native int nGetAllocatedSize(long renderNode); private static native void nRequestPositionUpdates(long renderNode, PositionUpdateListener callback); @@ -1604,7 +1620,7 @@ public final class RenderNode { int right, int bottom, float radius, float alpha); @CriticalNative - private static native boolean nSetOutlineConvexPath(long renderNode, long nativePath, + private static native boolean nSetOutlinePath(long renderNode, long nativePath, float alpha); @CriticalNative diff --git a/graphics/java/android/graphics/RuntimeShader.java b/graphics/java/android/graphics/RuntimeShader.java new file mode 100644 index 000000000000..5a3f2a96e31d --- /dev/null +++ b/graphics/java/android/graphics/RuntimeShader.java @@ -0,0 +1,90 @@ +/* + * Copyright 2019 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.annotation.NonNull; +import android.annotation.Nullable; + +import libcore.util.NativeAllocationRegistry; + +/** + * Shader that calculates pixel output with a program (fragment shader) running on a GPU. + * @hide + */ +public class RuntimeShader extends Shader { + + private static class NoImagePreloadHolder { + public static final NativeAllocationRegistry sRegistry = + NativeAllocationRegistry.createMalloced( + RuntimeShader.class.getClassLoader(), nativeGetFinalizer()); + } + + private byte[] mUniforms; + private boolean mIsOpaque; + + /** + * Current native shader factory instance. + */ + private long mNativeInstanceRuntimeShaderFactory; + + /** + * Creates a new RuntimeShader. + * + * @param sksl The text of SKSL program to run on the GPU. + * @param uniforms Array of parameters passed by the SKSL shader. Array size depends + * on number of uniforms declared by sksl. + * @param isOpaque True if all pixels have alpha 1.0f. + */ + public RuntimeShader(@NonNull String sksl, @Nullable byte[] uniforms, boolean isOpaque) { + this(sksl, uniforms, isOpaque, ColorSpace.get(ColorSpace.Named.SRGB)); + } + + private RuntimeShader(@NonNull String sksl, @Nullable byte[] uniforms, boolean isOpaque, + ColorSpace colorSpace) { + super(colorSpace); + mUniforms = uniforms; + mIsOpaque = isOpaque; + mNativeInstanceRuntimeShaderFactory = nativeCreateShaderFactory(sksl); + NoImagePreloadHolder.sRegistry.registerNativeAllocation(this, + mNativeInstanceRuntimeShaderFactory); + } + + /** + * Sets new value for shader parameters. + * + * @param uniforms Array of parameters passed by the SKSL shader. Array size depends + * on number of uniforms declared by mSksl. + */ + public void updateUniforms(@Nullable byte[] uniforms) { + mUniforms = uniforms; + discardNativeInstance(); + } + + @Override + long createNativeInstance(long nativeMatrix) { + return nativeCreate(mNativeInstanceRuntimeShaderFactory, nativeMatrix, mUniforms, + colorSpace().getNativeInstance(), mIsOpaque); + } + + private static native long nativeCreate(long shaderFactory, long matrix, byte[] inputs, + long colorSpaceHandle, boolean isOpaque); + + private static native long nativeCreateShaderFactory(String sksl); + + private static native long nativeGetFinalizer(); +} + diff --git a/graphics/java/android/graphics/SurfaceTexture.java b/graphics/java/android/graphics/SurfaceTexture.java index 697daa8b7b70..228d03a1dd10 100644 --- a/graphics/java/android/graphics/SurfaceTexture.java +++ b/graphics/java/android/graphics/SurfaceTexture.java @@ -120,7 +120,7 @@ public class SurfaceTexture { /** * Construct a new SurfaceTexture to stream images to a given OpenGL texture. - * + * <p> * In single buffered mode the application is responsible for serializing access to the image * content buffer. Each time the image content is to be updated, the * {@link #releaseTexImage()} method must be called before the image content producer takes @@ -143,7 +143,7 @@ public class SurfaceTexture { /** * Construct a new SurfaceTexture to stream images to a given OpenGL texture. - * + * <p> * In single buffered mode the application is responsible for serializing access to the image * content buffer. Each time the image content is to be updated, the * {@link #releaseTexImage()} method must be called before the image content producer takes @@ -152,7 +152,7 @@ public class SurfaceTexture { * must be called before each ANativeWindow_lock, or that call will fail. When producing * image content with OpenGL ES, {@link #releaseTexImage()} must be called before the first * OpenGL ES function call each frame. - * + * <p> * Unlike {@link #SurfaceTexture(int, boolean)}, which takes an OpenGL texture object name, * this constructor creates the SurfaceTexture in detached mode. A texture name must be passed * in using {@link #attachToGLContext} before calling {@link #releaseTexImage()} and producing @@ -222,15 +222,15 @@ public class SurfaceTexture { * method. Both video and camera based image producers do override the size. This method may * be used to set the image size when producing images with {@link android.graphics.Canvas} (via * {@link android.view.Surface#lockCanvas}), or OpenGL ES (via an EGLSurface). - * + * <p> * The new default buffer size will take effect the next time the image producer requests a * buffer to fill. For {@link android.graphics.Canvas} this will be the next time {@link * android.view.Surface#lockCanvas} is called. For OpenGL ES, the EGLSurface should be * destroyed (via eglDestroySurface), made not-current (via eglMakeCurrent), and then recreated - * (via eglCreateWindowSurface) to ensure that the new default size has taken effect. - * + * (via {@code eglCreateWindowSurface}) to ensure that the new default size has taken effect. + * <p> * The width and height parameters must be no greater than the minimum of - * GL_MAX_VIEWPORT_DIMS and GL_MAX_TEXTURE_SIZE (see + * {@code GL_MAX_VIEWPORT_DIMS} and {@code GL_MAX_TEXTURE_SIZE} (see * {@link javax.microedition.khronos.opengles.GL10#glGetIntegerv glGetIntegerv}). * An error due to invalid dimensions might not be reported until * updateTexImage() is called. @@ -242,7 +242,7 @@ public class SurfaceTexture { /** * Update the texture image to the most recent frame from the image stream. This may only be * called while the OpenGL ES context that owns the texture is current on the calling thread. - * It will implicitly bind its texture to the GL_TEXTURE_EXTERNAL_OES texture target. + * It will implicitly bind its texture to the {@code GL_TEXTURE_EXTERNAL_OES} texture target. */ public void updateTexImage() { nativeUpdateTexImage(); @@ -251,6 +251,7 @@ public class SurfaceTexture { /** * Releases the the texture content. This is needed in single buffered mode to allow the image * content producer to take ownership of the image buffer. + * <p> * For more information see {@link #SurfaceTexture(int, boolean)}. */ public void releaseTexImage() { @@ -263,7 +264,7 @@ public class SurfaceTexture { * ES texture object will be deleted as a result of this call. After calling this method all * calls to {@link #updateTexImage} will throw an {@link java.lang.IllegalStateException} until * a successful call to {@link #attachToGLContext} is made. - * + * <p> * This can be used to access the SurfaceTexture image contents from multiple OpenGL ES * contexts. Note, however, that the image contents are only accessible from one OpenGL ES * context at a time. @@ -279,8 +280,8 @@ public class SurfaceTexture { * Attach the SurfaceTexture to the OpenGL ES context that is current on the calling thread. A * new OpenGL ES texture object is created and populated with the SurfaceTexture image frame * that was current at the time of the last call to {@link #detachFromGLContext}. This new - * texture is bound to the GL_TEXTURE_EXTERNAL_OES texture target. - * + * texture is bound to the {@code GL_TEXTURE_EXTERNAL_OES} texture target. + * <p> * This can be used to access the SurfaceTexture image contents from multiple OpenGL ES * contexts. Note, however, that the image contents are only accessible from one OpenGL ES * context at a time. @@ -297,15 +298,20 @@ public class SurfaceTexture { /** * Retrieve the 4x4 texture coordinate transform matrix associated with the texture image set by - * the most recent call to updateTexImage. - * + * the most recent call to {@link #updateTexImage}. + * <p> * This transform matrix maps 2D homogeneous texture coordinates of the form (s, t, 0, 1) with s * and t in the inclusive range [0, 1] to the texture coordinate that should be used to sample * that location from the texture. Sampling the texture outside of the range of this transform * is undefined. - * + * <p> * The matrix is stored in column-major order so that it may be passed directly to OpenGL ES via - * the glLoadMatrixf or glUniformMatrix4fv functions. + * the {@code glLoadMatrixf} or {@code glUniformMatrix4fv} functions. + * <p> + * If the underlying buffer has a crop associated with it, the transformation will also include + * a slight scale to cut off a 1-texel border around the edge of the crop. This ensures that + * when the texture is bilinear sampled that no texels outside of the buffer's valid region + * are accessed by the GPU, avoiding any sampling artifacts when scaling. * * @param mtx the array into which the 4x4 matrix will be stored. The array must have exactly * 16 elements. @@ -321,7 +327,7 @@ public class SurfaceTexture { /** * Retrieve the timestamp associated with the texture image set by the most recent call to - * updateTexImage. + * {@link #updateTexImage}. * * <p>This timestamp is in nanoseconds, and is normally monotonically increasing. The timestamp * should be unaffected by time-of-day adjustments. The specific meaning and zero point of the @@ -332,8 +338,8 @@ public class SurfaceTexture { * * <p>For camera sources, timestamps should be strictly monotonic. Timestamps from MediaPlayer * sources may be reset when the playback position is set. For EGL and Vulkan producers, the - * timestamp is the desired present time set with the EGL_ANDROID_presentation_time or - * VK_GOOGLE_display_timing extensions.</p> + * timestamp is the desired present time set with the {@code EGL_ANDROID_presentation_time} or + * {@code VK_GOOGLE_display_timing} extensions.</p> */ public long getTimestamp() { @@ -341,16 +347,17 @@ public class SurfaceTexture { } /** - * release() frees all the buffers and puts the SurfaceTexture into the + * {@code release()} frees all the buffers and puts the SurfaceTexture into the * 'abandoned' state. Once put in this state the SurfaceTexture can never * leave it. When in the 'abandoned' state, all methods of the - * IGraphicBufferProducer interface will fail with the NO_INIT error. - * + * {@code IGraphicBufferProducer} interface will fail with the {@code NO_INIT} + * error. + * <p> * Note that while calling this method causes all the buffers to be freed * from the perspective of the the SurfaceTexture, if there are additional * references on the buffers (e.g. if a buffer is referenced by a client or * by OpenGL ES as a texture) then those buffer will remain allocated. - * + * <p> * Always call this method when you are done with SurfaceTexture. Failing * to do so may delay resource deallocation for a significant amount of * time. @@ -362,7 +369,7 @@ public class SurfaceTexture { } /** - * Returns true if the SurfaceTexture was released. + * Returns {@code true} if the SurfaceTexture was released. * * @see #release() */ @@ -395,7 +402,7 @@ public class SurfaceTexture { } /** - * Returns true if the SurfaceTexture is single-buffered + * Returns {@code true} if the SurfaceTexture is single-buffered. * @hide */ public boolean isSingleBuffered() { diff --git a/graphics/java/android/graphics/animation/FallbackLUTInterpolator.java b/graphics/java/android/graphics/animation/FallbackLUTInterpolator.java new file mode 100644 index 000000000000..36062c141ed2 --- /dev/null +++ b/graphics/java/android/graphics/animation/FallbackLUTInterpolator.java @@ -0,0 +1,80 @@ +/* + * 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.graphics.animation; + +import android.animation.TimeInterpolator; +import android.util.TimeUtils; +import android.view.Choreographer; + +/** + * Interpolator that builds a lookup table to use. This is a fallback for + * building a native interpolator from a TimeInterpolator that is not marked + * with {@link HasNativeInterpolator} + * + * This implements TimeInterpolator to allow for easier interop with Animators + * @hide + */ +@HasNativeInterpolator +public class FallbackLUTInterpolator implements NativeInterpolator, TimeInterpolator { + + // If the duration of an animation is more than 300 frames, we cap the sample size to 300. + private static final int MAX_SAMPLE_POINTS = 300; + private TimeInterpolator mSourceInterpolator; + private final float[] mLut; + + /** + * Used to cache the float[] LUT for use across multiple native + * interpolator creation + */ + public FallbackLUTInterpolator(TimeInterpolator interpolator, long duration) { + mSourceInterpolator = interpolator; + mLut = createLUT(interpolator, duration); + } + + private static float[] createLUT(TimeInterpolator interpolator, long duration) { + long frameIntervalNanos = Choreographer.getInstance().getFrameIntervalNanos(); + int animIntervalMs = (int) (frameIntervalNanos / TimeUtils.NANOS_PER_MS); + // We need 2 frame values as the minimal. + int numAnimFrames = Math.max(2, (int) Math.ceil(((double) duration) / animIntervalMs)); + numAnimFrames = Math.min(numAnimFrames, MAX_SAMPLE_POINTS); + float[] values = new float[numAnimFrames]; + float lastFrame = numAnimFrames - 1; + for (int i = 0; i < numAnimFrames; i++) { + float inValue = i / lastFrame; + values[i] = interpolator.getInterpolation(inValue); + } + return values; + } + + @Override + public long createNativeInterpolator() { + return NativeInterpolatorFactory.createLutInterpolator(mLut); + } + + /** + * Used to create a one-shot float[] LUT & native interpolator + */ + public static long createNativeInterpolator(TimeInterpolator interpolator, long duration) { + float[] lut = createLUT(interpolator, duration); + return NativeInterpolatorFactory.createLutInterpolator(lut); + } + + @Override + public float getInterpolation(float input) { + return mSourceInterpolator.getInterpolation(input); + } +} diff --git a/graphics/java/android/graphics/animation/HasNativeInterpolator.java b/graphics/java/android/graphics/animation/HasNativeInterpolator.java new file mode 100644 index 000000000000..c53d5a42524d --- /dev/null +++ b/graphics/java/android/graphics/animation/HasNativeInterpolator.java @@ -0,0 +1,37 @@ +/* + * 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.graphics.animation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This is a class annotation that signals that it is safe to create + * a native interpolator counterpart via {@link NativeInterpolator} + * + * The idea here is to prevent subclasses of interpolators from being treated as a + * NativeInterpolator, and instead have them fall back to the LUT & LERP + * method like a custom interpolator. + * + * @hide + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +public @interface HasNativeInterpolator { +} diff --git a/graphics/java/android/graphics/animation/NativeInterpolator.java b/graphics/java/android/graphics/animation/NativeInterpolator.java new file mode 100644 index 000000000000..1e6fea8da466 --- /dev/null +++ b/graphics/java/android/graphics/animation/NativeInterpolator.java @@ -0,0 +1,29 @@ +/* + * 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.graphics.animation; + +/** + * @hide + */ +public interface NativeInterpolator { + /** + * Generates a native interpolator object that can be used by HardwareRenderer to draw + * RenderNodes. + * @return ptr to native object + */ + long createNativeInterpolator(); +} diff --git a/graphics/java/android/graphics/animation/NativeInterpolatorFactory.java b/graphics/java/android/graphics/animation/NativeInterpolatorFactory.java new file mode 100644 index 000000000000..38866145ee50 --- /dev/null +++ b/graphics/java/android/graphics/animation/NativeInterpolatorFactory.java @@ -0,0 +1,67 @@ +/* + * 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.graphics.animation; + +import android.animation.TimeInterpolator; + +/** + * Static utility class for constructing native interpolators to keep the + * JNI simpler + * + * @hide + */ +public final class NativeInterpolatorFactory { + private NativeInterpolatorFactory() {} + + /** + * Create a native interpolator from the provided param generating a LUT variant if a native + * implementation does not exist. + */ + public static long createNativeInterpolator(TimeInterpolator interpolator, long + duration) { + if (interpolator == null) { + return createLinearInterpolator(); + } else if (RenderNodeAnimator.isNativeInterpolator(interpolator)) { + return ((NativeInterpolator) interpolator).createNativeInterpolator(); + } else { + return FallbackLUTInterpolator.createNativeInterpolator(interpolator, duration); + } + } + + /** Creates a specialized native interpolator for Accelerate/Decelerate */ + public static native long createAccelerateDecelerateInterpolator(); + /** Creates a specialized native interpolator for Accelerate */ + public static native long createAccelerateInterpolator(float factor); + /** Creates a specialized native interpolator for Anticipate */ + public static native long createAnticipateInterpolator(float tension); + /** Creates a specialized native interpolator for Anticipate with Overshoot */ + public static native long createAnticipateOvershootInterpolator(float tension); + /** Creates a specialized native interpolator for Bounce */ + public static native long createBounceInterpolator(); + /** Creates a specialized native interpolator for Cycle */ + public static native long createCycleInterpolator(float cycles); + /** Creates a specialized native interpolator for Decelerate */ + public static native long createDecelerateInterpolator(float factor); + /** Creates a specialized native interpolator for Linear interpolation */ + public static native long createLinearInterpolator(); + /** Creates a specialized native interpolator for Overshoot */ + public static native long createOvershootInterpolator(float tension); + /** Creates a specialized native interpolator for along traveling along a Path */ + public static native long createPathInterpolator(float[] x, float[] y); + /** Creates a specialized native interpolator for LUT */ + public static native long createLutInterpolator(float[] values); +} diff --git a/graphics/java/android/graphics/animation/RenderNodeAnimator.java b/graphics/java/android/graphics/animation/RenderNodeAnimator.java new file mode 100644 index 000000000000..282b2f959fc4 --- /dev/null +++ b/graphics/java/android/graphics/animation/RenderNodeAnimator.java @@ -0,0 +1,513 @@ +/* + * 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.graphics.animation; + +import android.animation.Animator; +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.graphics.CanvasProperty; +import android.graphics.Paint; +import android.graphics.RecordingCanvas; +import android.graphics.RenderNode; +import android.os.Handler; +import android.os.Looper; +import android.view.Choreographer; + +import com.android.internal.util.VirtualRefBasePtr; + +import java.util.ArrayList; + +/** + * @hide + */ +public class RenderNodeAnimator extends Animator { + // Keep in sync with enum RenderProperty in Animator.h + public static final int TRANSLATION_X = 0; + public static final int TRANSLATION_Y = 1; + public static final int TRANSLATION_Z = 2; + public static final int SCALE_X = 3; + public static final int SCALE_Y = 4; + public static final int ROTATION = 5; + public static final int ROTATION_X = 6; + public static final int ROTATION_Y = 7; + public static final int X = 8; + public static final int Y = 9; + public static final int Z = 10; + public static final int ALPHA = 11; + // The last value in the enum, used for array size initialization + public static final int LAST_VALUE = ALPHA; + + // Keep in sync with enum PaintFields in Animator.h + public static final int PAINT_STROKE_WIDTH = 0; + + /** + * Field for the Paint alpha channel, which should be specified as a value + * between 0 and 255. + */ + public static final int PAINT_ALPHA = 1; + + private VirtualRefBasePtr mNativePtr; + + private Handler mHandler; + private RenderNode mTarget; + private ViewListener mViewListener; + private int mRenderProperty = -1; + private float mFinalValue; + private TimeInterpolator mInterpolator; + + private static final int STATE_PREPARE = 0; + private static final int STATE_DELAYED = 1; + private static final int STATE_RUNNING = 2; + private static final int STATE_FINISHED = 3; + private int mState = STATE_PREPARE; + + private long mUnscaledDuration = 300; + private long mUnscaledStartDelay = 0; + // If this is true, we will run any start delays on the UI thread. This is + // the safe default, and is necessary to ensure start listeners fire at + // the correct time. Animators created by RippleDrawable (the + // CanvasProperty<> ones) do not have this expectation, and as such will + // set this to false so that the renderthread handles the startdelay instead + private final boolean mUiThreadHandlesDelay; + private long mStartDelay = 0; + private long mStartTime; + + /** + * Interface used by the view system to update the view hierarchy in conjunction + * with this animator. + */ + public interface ViewListener { + /** notify the listener that an alpha animation has begun. */ + void onAlphaAnimationStart(float finalAlpha); + /** notify the listener that the animator has mutated a value that requires invalidation */ + void invalidateParent(boolean forceRedraw); + } + + public RenderNodeAnimator(int property, float finalValue) { + mRenderProperty = property; + mFinalValue = finalValue; + mUiThreadHandlesDelay = true; + init(nCreateAnimator(property, finalValue)); + } + + public RenderNodeAnimator(CanvasProperty<Float> property, float finalValue) { + init(nCreateCanvasPropertyFloatAnimator( + property.getNativeContainer(), finalValue)); + mUiThreadHandlesDelay = false; + } + + /** + * Creates a new render node animator for a field on a Paint property. + * + * @param property The paint property to target + * @param paintField Paint field to animate, one of {@link #PAINT_ALPHA} or + * {@link #PAINT_STROKE_WIDTH} + * @param finalValue The target value for the property + */ + public RenderNodeAnimator(CanvasProperty<Paint> property, int paintField, float finalValue) { + init(nCreateCanvasPropertyPaintAnimator( + property.getNativeContainer(), paintField, finalValue)); + mUiThreadHandlesDelay = false; + } + + public RenderNodeAnimator(int x, int y, float startRadius, float endRadius) { + init(nCreateRevealAnimator(x, y, startRadius, endRadius)); + mUiThreadHandlesDelay = true; + } + + private void init(long ptr) { + mNativePtr = new VirtualRefBasePtr(ptr); + } + + private void checkMutable() { + if (mState != STATE_PREPARE) { + throw new IllegalStateException("Animator has already started, cannot change it now!"); + } + if (mNativePtr == null) { + throw new IllegalStateException("Animator's target has been destroyed " + + "(trying to modify an animation after activity destroy?)"); + } + } + + static boolean isNativeInterpolator(TimeInterpolator interpolator) { + return interpolator.getClass().isAnnotationPresent(HasNativeInterpolator.class); + } + + private void applyInterpolator() { + if (mInterpolator == null || mNativePtr == null) return; + + long ni; + if (isNativeInterpolator(mInterpolator)) { + ni = ((NativeInterpolator) mInterpolator).createNativeInterpolator(); + } else { + long duration = nGetDuration(mNativePtr.get()); + ni = FallbackLUTInterpolator.createNativeInterpolator(mInterpolator, duration); + } + nSetInterpolator(mNativePtr.get(), ni); + } + + @Override + public void start() { + if (mTarget == null) { + throw new IllegalStateException("Missing target!"); + } + + if (mState != STATE_PREPARE) { + throw new IllegalStateException("Already started!"); + } + + mState = STATE_DELAYED; + if (mHandler == null) { + mHandler = new Handler(true); + } + applyInterpolator(); + + if (mNativePtr == null) { + // It's dead, immediately cancel + cancel(); + } else if (mStartDelay <= 0 || !mUiThreadHandlesDelay) { + nSetStartDelay(mNativePtr.get(), mStartDelay); + doStart(); + } else { + getHelper().addDelayedAnimation(this); + } + } + + private void doStart() { + // Alpha is a special snowflake that has the canonical value stored + // in mTransformationInfo instead of in RenderNode, so we need to update + // it with the final value here. + if (mRenderProperty == RenderNodeAnimator.ALPHA && mViewListener != null) { + mViewListener.onAlphaAnimationStart(mFinalValue); + } + + moveToRunningState(); + + if (mViewListener != null) { + // Kick off a frame to start the process + mViewListener.invalidateParent(false); + } + } + + private void moveToRunningState() { + mState = STATE_RUNNING; + if (mNativePtr != null) { + nStart(mNativePtr.get()); + } + notifyStartListeners(); + } + + private void notifyStartListeners() { + final ArrayList<AnimatorListener> listeners = cloneListeners(); + final int numListeners = listeners == null ? 0 : listeners.size(); + for (int i = 0; i < numListeners; i++) { + listeners.get(i).onAnimationStart(this); + } + } + + @Override + public void cancel() { + if (mState != STATE_PREPARE && mState != STATE_FINISHED) { + if (mState == STATE_DELAYED) { + getHelper().removeDelayedAnimation(this); + moveToRunningState(); + } + + final ArrayList<AnimatorListener> listeners = cloneListeners(); + final int numListeners = listeners == null ? 0 : listeners.size(); + for (int i = 0; i < numListeners; i++) { + listeners.get(i).onAnimationCancel(this); + } + + end(); + } + } + + @Override + public void end() { + if (mState != STATE_FINISHED) { + if (mState < STATE_RUNNING) { + getHelper().removeDelayedAnimation(this); + doStart(); + } + if (mNativePtr != null) { + nEnd(mNativePtr.get()); + if (mViewListener != null) { + // Kick off a frame to flush the state change + mViewListener.invalidateParent(false); + } + } else { + // It's already dead, jump to onFinish + onFinished(); + } + } + } + + @Override + public void pause() { + throw new UnsupportedOperationException(); + } + + @Override + public void resume() { + throw new UnsupportedOperationException(); + } + + /** @hide */ + public void setViewListener(ViewListener listener) { + mViewListener = listener; + } + + /** Sets the animation target to the owning view of the RecordingCanvas */ + public final void setTarget(RecordingCanvas canvas) { + setTarget(canvas.mNode); + } + + /** Sets the node that is to be the target of this animation */ + protected void setTarget(RenderNode node) { + checkMutable(); + if (mTarget != null) { + throw new IllegalStateException("Target already set!"); + } + nSetListener(mNativePtr.get(), this); + mTarget = node; + mTarget.addAnimator(this); + } + + /** Set the start value for the animation */ + public void setStartValue(float startValue) { + checkMutable(); + nSetStartValue(mNativePtr.get(), startValue); + } + + @Override + public void setStartDelay(long startDelay) { + checkMutable(); + if (startDelay < 0) { + throw new IllegalArgumentException("startDelay must be positive; " + startDelay); + } + mUnscaledStartDelay = startDelay; + mStartDelay = (long) (ValueAnimator.getDurationScale() * startDelay); + } + + @Override + public long getStartDelay() { + return mUnscaledStartDelay; + } + + @Override + public RenderNodeAnimator setDuration(long duration) { + checkMutable(); + if (duration < 0) { + throw new IllegalArgumentException("duration must be positive; " + duration); + } + mUnscaledDuration = duration; + nSetDuration(mNativePtr.get(), (long) (duration * ValueAnimator.getDurationScale())); + return this; + } + + @Override + public long getDuration() { + return mUnscaledDuration; + } + + @Override + public long getTotalDuration() { + return mUnscaledDuration + mUnscaledStartDelay; + } + + @Override + public boolean isRunning() { + return mState == STATE_DELAYED || mState == STATE_RUNNING; + } + + @Override + public boolean isStarted() { + return mState != STATE_PREPARE; + } + + @Override + public void setInterpolator(TimeInterpolator interpolator) { + checkMutable(); + mInterpolator = interpolator; + } + + @Override + public TimeInterpolator getInterpolator() { + return mInterpolator; + } + + protected void onFinished() { + if (mState == STATE_PREPARE) { + // Unlikely but possible, the native side has been destroyed + // before we have started. + releaseNativePtr(); + return; + } + if (mState == STATE_DELAYED) { + getHelper().removeDelayedAnimation(this); + notifyStartListeners(); + } + mState = STATE_FINISHED; + + final ArrayList<AnimatorListener> listeners = cloneListeners(); + final int numListeners = listeners == null ? 0 : listeners.size(); + for (int i = 0; i < numListeners; i++) { + listeners.get(i).onAnimationEnd(this); + } + + // Release the native object, as it has a global reference to us. This + // breaks the cyclic reference chain, and allows this object to be + // GC'd + releaseNativePtr(); + } + + private void releaseNativePtr() { + if (mNativePtr != null) { + mNativePtr.release(); + mNativePtr = null; + } + } + + @SuppressWarnings("unchecked") + private ArrayList<AnimatorListener> cloneListeners() { + ArrayList<AnimatorListener> listeners = getListeners(); + if (listeners != null) { + listeners = (ArrayList<AnimatorListener>) listeners.clone(); + } + return listeners; + } + + public long getNativeAnimator() { + return mNativePtr.get(); + } + + /** + * @return true if the animator was started, false if still delayed + */ + private boolean processDelayed(long frameTimeMs) { + if (mStartTime == 0) { + mStartTime = frameTimeMs; + } else if ((frameTimeMs - mStartTime) >= mStartDelay) { + doStart(); + return true; + } + return false; + } + + private static DelayedAnimationHelper getHelper() { + DelayedAnimationHelper helper = sAnimationHelper.get(); + if (helper == null) { + helper = new DelayedAnimationHelper(); + sAnimationHelper.set(helper); + } + return helper; + } + + private static ThreadLocal<DelayedAnimationHelper> sAnimationHelper = + new ThreadLocal<DelayedAnimationHelper>(); + + private static class DelayedAnimationHelper implements Runnable { + + private ArrayList<RenderNodeAnimator> mDelayedAnims = new ArrayList<RenderNodeAnimator>(); + private final Choreographer mChoreographer; + private boolean mCallbackScheduled; + + DelayedAnimationHelper() { + mChoreographer = Choreographer.getInstance(); + } + + public void addDelayedAnimation(RenderNodeAnimator animator) { + mDelayedAnims.add(animator); + scheduleCallback(); + } + + public void removeDelayedAnimation(RenderNodeAnimator animator) { + mDelayedAnims.remove(animator); + } + + private void scheduleCallback() { + if (!mCallbackScheduled) { + mCallbackScheduled = true; + mChoreographer.postCallback(Choreographer.CALLBACK_ANIMATION, this, null); + } + } + + @Override + public void run() { + long frameTimeMs = mChoreographer.getFrameTime(); + mCallbackScheduled = false; + + int end = 0; + for (int i = 0; i < mDelayedAnims.size(); i++) { + RenderNodeAnimator animator = mDelayedAnims.get(i); + if (!animator.processDelayed(frameTimeMs)) { + if (end != i) { + mDelayedAnims.set(end, animator); + } + end++; + } + } + while (mDelayedAnims.size() > end) { + mDelayedAnims.remove(mDelayedAnims.size() - 1); + } + + if (mDelayedAnims.size() > 0) { + scheduleCallback(); + } + } + } + + // Called by native + private static void callOnFinished(RenderNodeAnimator animator) { + if (animator.mHandler != null) { + animator.mHandler.post(animator::onFinished); + } else { + new Handler(Looper.getMainLooper(), null, true).post(animator::onFinished); + } + } + + @Override + public Animator clone() { + throw new IllegalStateException("Cannot clone this animator"); + } + + @Override + public void setAllowRunningAsynchronously(boolean mayRunAsync) { + checkMutable(); + nSetAllowRunningAsync(mNativePtr.get(), mayRunAsync); + } + + private static native long nCreateAnimator(int property, float finalValue); + private static native long nCreateCanvasPropertyFloatAnimator( + long canvasProperty, float finalValue); + private static native long nCreateCanvasPropertyPaintAnimator( + long canvasProperty, int paintField, float finalValue); + private static native long nCreateRevealAnimator( + int x, int y, float startRadius, float endRadius); + + private static native void nSetStartValue(long nativePtr, float startValue); + private static native void nSetDuration(long nativePtr, long duration); + private static native long nGetDuration(long nativePtr); + private static native void nSetStartDelay(long nativePtr, long startDelay); + private static native void nSetInterpolator(long animPtr, long interpolatorPtr); + private static native void nSetAllowRunningAsync(long animPtr, boolean mayRunAsync); + private static native void nSetListener(long animPtr, RenderNodeAnimator listener); + + private static native void nStart(long animPtr); + private static native void nEnd(long animPtr); +} diff --git a/graphics/java/android/graphics/drawable/AdaptiveIconDrawable.java b/graphics/java/android/graphics/drawable/AdaptiveIconDrawable.java index fab96a1e9fbd..31ad81b9c346 100644 --- a/graphics/java/android/graphics/drawable/AdaptiveIconDrawable.java +++ b/graphics/java/android/graphics/drawable/AdaptiveIconDrawable.java @@ -16,6 +16,7 @@ package android.graphics.drawable; +import android.annotation.DrawableRes; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.TestApi; @@ -223,6 +224,7 @@ public class AdaptiveIconDrawable extends Drawable implements Drawable.Callback final int deviceDensity = Drawable.resolveDensity(r, 0); state.setDensity(deviceDensity); state.mSrcDensityOverride = mSrcDensityOverride; + state.mSourceDrawableId = Resources.getAttributeSetSourceResId(attrs); final ChildDrawable[] array = state.mChildren; for (int i = 0; i < state.mChildren.length; i++) { @@ -385,7 +387,7 @@ public class AdaptiveIconDrawable extends Drawable implements Drawable.Callback @Override public void getOutline(@NonNull Outline outline) { - outline.setConvexPath(mMask); + outline.setPath(mMask); } /** @hide */ @@ -446,6 +448,17 @@ public class AdaptiveIconDrawable extends Drawable implements Drawable.Callback } /** + * If the drawable was inflated from XML, this returns the resource ID for the drawable + * + * @hide + */ + @DrawableRes + public int getSourceDrawableResId() { + final LayerState state = mLayerState; + return state == null ? Resources.ID_NULL : state.mSourceDrawableId; + } + + /** * Inflates child layers using the specified parser. */ private void inflateLayers(@NonNull Resources r, @NonNull XmlPullParser parser, @@ -944,6 +957,8 @@ public class AdaptiveIconDrawable extends Drawable implements Drawable.Callback @Config int mChangingConfigurations; @Config int mChildrenChangingConfigurations; + @DrawableRes int mSourceDrawableId = Resources.ID_NULL; + private boolean mCheckedOpacity; private int mOpacity; @@ -960,6 +975,7 @@ public class AdaptiveIconDrawable extends Drawable implements Drawable.Callback mChangingConfigurations = orig.mChangingConfigurations; mChildrenChangingConfigurations = orig.mChildrenChangingConfigurations; + mSourceDrawableId = orig.mSourceDrawableId; for (int i = 0; i < N_CHILDREN; i++) { final ChildDrawable or = origChildDrawable[i]; diff --git a/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java b/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java index 1acf6c512fbd..9fb72cf08b51 100644 --- a/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java +++ b/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java @@ -42,6 +42,7 @@ import android.graphics.PixelFormat; import android.graphics.RecordingCanvas; import android.graphics.Rect; import android.graphics.RenderNode; +import android.graphics.animation.NativeInterpolatorFactory; import android.os.Build; import android.os.Handler; import android.util.ArrayMap; @@ -54,7 +55,6 @@ import android.util.Property; import android.util.TimeUtils; import android.view.Choreographer; import android.view.NativeVectorDrawableAnimator; -import android.view.RenderNodeAnimatorSetHelper; import android.view.View; import com.android.internal.R; @@ -1532,7 +1532,7 @@ public class AnimatedVectorDrawable extends Drawable implements Animatable2 { long startDelay = extraDelay + animator.getStartDelay(); TimeInterpolator interpolator = animator.getInterpolator(); long nativeInterpolator = - RenderNodeAnimatorSetHelper.createNativeInterpolator(interpolator, duration); + NativeInterpolatorFactory.createNativeInterpolator(interpolator, duration); startDelay *= ValueAnimator.getDurationScale(); duration *= ValueAnimator.getDurationScale(); @@ -1548,7 +1548,7 @@ public class AnimatedVectorDrawable extends Drawable implements Animatable2 { * to the last seen RenderNode target and start right away. */ protected void recordLastSeenTarget(RecordingCanvas canvas) { - final RenderNode node = RenderNodeAnimatorSetHelper.getTarget(canvas); + final RenderNode node = canvas.mNode; mLastSeenTarget = new WeakReference<RenderNode>(node); // Add the animator to the list of animators on every draw if (mInitialized || mPendingAnimationActions.size() > 0) { diff --git a/graphics/java/android/graphics/drawable/DrawableWrapper.java b/graphics/java/android/graphics/drawable/DrawableWrapper.java index e8cb42e75ea2..e197e7123fed 100644 --- a/graphics/java/android/graphics/drawable/DrawableWrapper.java +++ b/graphics/java/android/graphics/drawable/DrawableWrapper.java @@ -364,6 +364,13 @@ public abstract class DrawableWrapper extends Drawable implements Drawable.Callb } @Override + public void jumpToCurrentState() { + if (mDrawable != null) { + mDrawable.jumpToCurrentState(); + } + } + + @Override protected boolean onLevelChange(int level) { return mDrawable != null && mDrawable.setLevel(level); } diff --git a/graphics/java/android/graphics/drawable/GradientDrawable.java b/graphics/java/android/graphics/drawable/GradientDrawable.java index b50ec0d5e3fd..f053f392b97e 100644 --- a/graphics/java/android/graphics/drawable/GradientDrawable.java +++ b/graphics/java/android/graphics/drawable/GradientDrawable.java @@ -97,6 +97,14 @@ import java.lang.annotation.RetentionPolicy; * @attr ref android.R.styleable#GradientDrawablePadding_bottom */ public class GradientDrawable extends Drawable { + + /** + * Flag to determine if we should wrap negative gradient angle measurements + * for API levels that support it + * @hide + */ + public static boolean sWrapNegativeAngleMeasurements = true; + /** * Shape is a rectangle, possibly with rounded corners */ @@ -151,6 +159,9 @@ public class GradientDrawable extends Drawable { /** Radius is a fraction of the bounds size. */ private static final int RADIUS_TYPE_FRACTION_PARENT = 2; + /** Default orientation for GradientDrawable **/ + private static final Orientation DEFAULT_ORIENTATION = Orientation.TOP_BOTTOM; + /** @hide */ @IntDef({RADIUS_TYPE_PIXELS, RADIUS_TYPE_FRACTION, RADIUS_TYPE_FRACTION_PARENT}) @Retention(RetentionPolicy.SOURCE) @@ -207,7 +218,7 @@ public class GradientDrawable extends Drawable { } public GradientDrawable() { - this(new GradientState(Orientation.TOP_BOTTOM, null), null); + this(new GradientState(DEFAULT_ORIENTATION, null), null); } /** @@ -637,7 +648,7 @@ public class GradientDrawable extends Drawable { * @see #setOrientation(Orientation) */ public Orientation getOrientation() { - return mGradientState.getOrientation(); + return mGradientState.mOrientation; } /** @@ -653,7 +664,7 @@ public class GradientDrawable extends Drawable { * @see #getOrientation() */ public void setOrientation(Orientation orientation) { - mGradientState.setOrientation(orientation); + mGradientState.mOrientation = orientation; mGradientIsDirty = true; invalidateSelf(); } @@ -1270,7 +1281,7 @@ public class GradientDrawable extends Drawable { if (st.mGradient == LINEAR_GRADIENT) { final float level = st.mUseLevel ? getLevel() / 10000.0f : 1.0f; - switch (st.getOrientation()) { + switch (st.mOrientation) { case TOP_BOTTOM: x0 = r.left; y0 = r.top; x1 = x0; y1 = level * r.bottom; @@ -1757,7 +1768,49 @@ public class GradientDrawable extends Drawable { } int angle = (int) a.getFloat(R.styleable.GradientDrawableGradient_angle, st.mAngle); - st.mAngle = ((angle % 360) + 360) % 360; // offset negative angle measures + + // GradientDrawable historically has not parsed negative angle measurements and always + // stays on the default orientation for API levels older than Q. + // Only configure the orientation if the angle is greater than zero. + // Otherwise fallback on Orientation.TOP_BOTTOM + // In Android Q and later, actually wrap the negative angle measurement to the correct + // value + if (sWrapNegativeAngleMeasurements) { + st.mAngle = ((angle % 360) + 360) % 360; // offset negative angle measures + } else { + st.mAngle = angle % 360; + } + + if (st.mAngle >= 0) { + switch (st.mAngle) { + case 0: + st.mOrientation = Orientation.LEFT_RIGHT; + break; + case 45: + st.mOrientation = Orientation.BL_TR; + break; + case 90: + st.mOrientation = Orientation.BOTTOM_TOP; + break; + case 135: + st.mOrientation = Orientation.BR_TL; + break; + case 180: + st.mOrientation = Orientation.RIGHT_LEFT; + break; + case 225: + st.mOrientation = Orientation.TR_BL; + break; + case 270: + st.mOrientation = Orientation.TOP_BOTTOM; + break; + case 315: + st.mOrientation = Orientation.TL_BR; + break; + } + } else { + st.mOrientation = DEFAULT_ORIENTATION; + } final TypedValue tv = a.peekValue(R.styleable.GradientDrawableGradient_gradientRadius); if (tv != null) { @@ -1862,7 +1915,7 @@ public class GradientDrawable extends Drawable { case RECTANGLE: if (st.mRadiusArray != null) { buildPathIfDirty(); - outline.setConvexPath(mPath); + outline.setPath(mPath); return; } @@ -1981,7 +2034,7 @@ public class GradientDrawable extends Drawable { int[] mAttrPadding; public GradientState(Orientation orientation, int[] gradientColors) { - setOrientation(orientation); + mOrientation = orientation; setGradientColors(gradientColors); } @@ -2184,93 +2237,11 @@ public class GradientDrawable extends Drawable { mCenterY = y; } - public void setOrientation(Orientation orientation) { - // Update the angle here so that subsequent attempts to obtain the orientation - // from the angle overwrite previously configured values during inflation - mAngle = getAngleFromOrientation(orientation); - mOrientation = orientation; - } - @NonNull public Orientation getOrientation() { - updateGradientStateOrientation(); return mOrientation; } - /** - * Update the orientation of the gradient based on the given angle only if the type is - * {@link #LINEAR_GRADIENT} - */ - private void updateGradientStateOrientation() { - if (mGradient == LINEAR_GRADIENT) { - int angle = mAngle; - if (angle % 45 != 0) { - throw new IllegalArgumentException("Linear gradient requires 'angle' attribute " - + "to be a multiple of 45"); - } - - Orientation orientation; - switch (angle) { - case 0: - orientation = Orientation.LEFT_RIGHT; - break; - case 45: - orientation = Orientation.BL_TR; - break; - case 90: - orientation = Orientation.BOTTOM_TOP; - break; - case 135: - orientation = Orientation.BR_TL; - break; - case 180: - orientation = Orientation.RIGHT_LEFT; - break; - case 225: - orientation = Orientation.TR_BL; - break; - case 270: - orientation = Orientation.TOP_BOTTOM; - break; - case 315: - orientation = Orientation.TL_BR; - break; - default: - // Should not get here as exception is thrown above if angle is not multiple - // of 45 degrees - orientation = Orientation.LEFT_RIGHT; - break; - } - mOrientation = orientation; - } - } - - private int getAngleFromOrientation(@Nullable Orientation orientation) { - if (orientation != null) { - switch (orientation) { - default: - case LEFT_RIGHT: - return 0; - case BL_TR: - return 45; - case BOTTOM_TOP: - return 90; - case BR_TL: - return 135; - case RIGHT_LEFT: - return 180; - case TR_BL: - return 225; - case TOP_BOTTOM: - return 270; - case TL_BR: - return 315; - } - } else { - return 0; - } - } - public void setGradientColors(@Nullable int[] colors) { mGradientColors = colors; mSolidColors = null; diff --git a/graphics/java/android/graphics/drawable/Icon.java b/graphics/java/android/graphics/drawable/Icon.java index 63662099c4c8..cc7182c3fd1c 100644 --- a/graphics/java/android/graphics/drawable/Icon.java +++ b/graphics/java/android/graphics/drawable/Icon.java @@ -92,11 +92,17 @@ public final class Icon implements Parcelable { * @see #getType */ public static final int TYPE_ADAPTIVE_BITMAP = 5; + /** + * An icon that was created using {@link Icon#createWithAdaptiveBitmapContentUri}. + * @see #getType + */ + public static final int TYPE_URI_ADAPTIVE_BITMAP = 6; /** * @hide */ - @IntDef({TYPE_BITMAP, TYPE_RESOURCE, TYPE_DATA, TYPE_URI, TYPE_ADAPTIVE_BITMAP}) + @IntDef({TYPE_BITMAP, TYPE_RESOURCE, TYPE_DATA, TYPE_URI, TYPE_ADAPTIVE_BITMAP, + TYPE_URI_ADAPTIVE_BITMAP}) public @interface IconType { } @@ -113,12 +119,14 @@ public final class Icon implements Parcelable { // based on the value of mType. // TYPE_BITMAP: Bitmap + // TYPE_ADAPTIVE_BITMAP: Bitmap // TYPE_RESOURCE: Resources // TYPE_DATA: DataBytes private Object mObj1; // TYPE_RESOURCE: package name // TYPE_URI: uri string + // TYPE_URI_ADAPTIVE_BITMAP: uri string @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) private String mString1; @@ -141,7 +149,8 @@ public final class Icon implements Parcelable { } /** - * @return The {@link android.graphics.Bitmap} held by this {@link #TYPE_BITMAP} Icon. + * @return The {@link android.graphics.Bitmap} held by this {@link #TYPE_BITMAP} or + * {@link #TYPE_ADAPTIVE_BITMAP} Icon. * @hide */ @UnsupportedAppUsage @@ -243,11 +252,12 @@ public final class Icon implements Parcelable { } /** - * @return The URI (as a String) for this {@link #TYPE_URI} Icon. + * @return The URI (as a String) for this {@link #TYPE_URI} or {@link #TYPE_URI_ADAPTIVE_BITMAP} + * Icon. * @hide */ public String getUriString() { - if (mType != TYPE_URI) { + if (mType != TYPE_URI && mType != TYPE_URI_ADAPTIVE_BITMAP) { throw new IllegalStateException("called getUriString() on " + this); } return mString1; @@ -256,7 +266,7 @@ public final class Icon implements Parcelable { /** * Gets the uri used to create this icon. * <p> - * Only valid for icons of type {@link #TYPE_URI}. + * Only valid for icons of type {@link #TYPE_URI} and {@link #TYPE_URI_ADAPTIVE_BITMAP}. * Note: This uri may not be available in the future, and it is * up to the caller to ensure safety if this uri is re-used and/or persisted. */ @@ -272,6 +282,7 @@ public final class Icon implements Parcelable { case TYPE_DATA: return "DATA"; case TYPE_RESOURCE: return "RESOURCE"; case TYPE_URI: return "URI"; + case TYPE_URI_ADAPTIVE_BITMAP: return "URI_MASKABLE"; default: return "UNKNOWN"; } } @@ -380,28 +391,39 @@ public final class Icon implements Parcelable { BitmapFactory.decodeByteArray(getDataBytes(), getDataOffset(), getDataLength()) ); case TYPE_URI: - final Uri uri = getUri(); - final String scheme = uri.getScheme(); - InputStream is = null; - if (ContentResolver.SCHEME_CONTENT.equals(scheme) - || ContentResolver.SCHEME_FILE.equals(scheme)) { - try { - is = context.getContentResolver().openInputStream(uri); - } catch (Exception e) { - Log.w(TAG, "Unable to load image from URI: " + uri, e); - } - } else { - try { - is = new FileInputStream(new File(mString1)); - } catch (FileNotFoundException e) { - Log.w(TAG, "Unable to load image from path: " + uri, e); - } - } + InputStream is = getUriInputStream(context); if (is != null) { return new BitmapDrawable(context.getResources(), BitmapFactory.decodeStream(is)); } break; + case TYPE_URI_ADAPTIVE_BITMAP: + is = getUriInputStream(context); + if (is != null) { + return new AdaptiveIconDrawable(null, new BitmapDrawable(context.getResources(), + BitmapFactory.decodeStream(is))); + } + break; + } + return null; + } + + private InputStream getUriInputStream(Context context) { + final Uri uri = getUri(); + final String scheme = uri.getScheme(); + if (ContentResolver.SCHEME_CONTENT.equals(scheme) + || ContentResolver.SCHEME_FILE.equals(scheme)) { + try { + return context.getContentResolver().openInputStream(uri); + } catch (Exception e) { + Log.w(TAG, "Unable to load image from URI: " + uri, e); + } + } else { + try { + return new FileInputStream(new File(mString1)); + } catch (FileNotFoundException e) { + Log.w(TAG, "Unable to load image from path: " + uri, e); + } } return null; } @@ -475,6 +497,7 @@ public final class Icon implements Parcelable { dataStream.writeInt(getResId()); break; case TYPE_URI: + case TYPE_URI_ADAPTIVE_BITMAP: dataStream.writeUTF(getUriString()); break; } @@ -513,6 +536,9 @@ public final class Icon implements Parcelable { case TYPE_URI: final String uriOrPath = inputStream.readUTF(); return createWithContentUri(uriOrPath); + case TYPE_URI_ADAPTIVE_BITMAP: + final String uri = inputStream.readUTF(); + return createWithAdaptiveBitmapContentUri(uri); } } return null; @@ -545,6 +571,7 @@ public final class Icon implements Parcelable { return getResId() == otherIcon.getResId() && Objects.equals(getResPackage(), otherIcon.getResPackage()); case TYPE_URI: + case TYPE_URI_ADAPTIVE_BITMAP: return Objects.equals(getUriString(), otherIcon.getUriString()); } return false; @@ -665,12 +692,40 @@ public final class Icon implements Parcelable { if (uri == null) { throw new IllegalArgumentException("Uri must not be null."); } - final Icon rep = new Icon(TYPE_URI); - rep.mString1 = uri.toString(); + return createWithContentUri(uri.toString()); + } + + /** + * Create an Icon pointing to an image file specified by URI. Image file should follow the icon + * design guideline defined by {@link AdaptiveIconDrawable}. + * + * @param uri A uri referring to local content:// or file:// image data. + */ + @NonNull + public static Icon createWithAdaptiveBitmapContentUri(@NonNull String uri) { + if (uri == null) { + throw new IllegalArgumentException("Uri must not be null."); + } + final Icon rep = new Icon(TYPE_URI_ADAPTIVE_BITMAP); + rep.mString1 = uri; return rep; } /** + * Create an Icon pointing to an image file specified by URI. Image file should follow the icon + * design guideline defined by {@link AdaptiveIconDrawable}. + * + * @param uri A uri referring to local content:// or file:// image data. + */ + @NonNull + public static Icon createWithAdaptiveBitmapContentUri(@NonNull Uri uri) { + if (uri == null) { + throw new IllegalArgumentException("Uri must not be null."); + } + return createWithAdaptiveBitmapContentUri(uri.toString()); + } + + /** * Store a color to use whenever this Icon is drawn. * * @param tint a color, as in {@link Drawable#setTint(int)} @@ -758,6 +813,7 @@ public final class Icon implements Parcelable { } break; case TYPE_URI: + case TYPE_URI_ADAPTIVE_BITMAP: sb.append(" uri=").append(getUriString()); break; } @@ -809,6 +865,7 @@ public final class Icon implements Parcelable { mObj1 = a; break; case TYPE_URI: + case TYPE_URI_ADAPTIVE_BITMAP: final String uri = in.readString(); mString1 = uri; break; @@ -840,6 +897,7 @@ public final class Icon implements Parcelable { dest.writeBlob(getDataBytes(), getDataOffset(), getDataLength()); break; case TYPE_URI: + case TYPE_URI_ADAPTIVE_BITMAP: dest.writeString(getUriString()); break; } diff --git a/graphics/java/android/graphics/drawable/RippleForeground.java b/graphics/java/android/graphics/drawable/RippleForeground.java index cce9ba31929f..0f376957c8ff 100644 --- a/graphics/java/android/graphics/drawable/RippleForeground.java +++ b/graphics/java/android/graphics/drawable/RippleForeground.java @@ -25,9 +25,9 @@ import android.graphics.CanvasProperty; import android.graphics.Paint; import android.graphics.RecordingCanvas; import android.graphics.Rect; +import android.graphics.animation.RenderNodeAnimator; import android.util.FloatProperty; import android.util.MathUtils; -import android.view.RenderNodeAnimator; import android.view.animation.AnimationUtils; import android.view.animation.LinearInterpolator; import android.view.animation.PathInterpolator; diff --git a/graphics/java/android/graphics/drawable/shapes/RoundRectShape.java b/graphics/java/android/graphics/drawable/shapes/RoundRectShape.java index 475e0bb70f2b..28ba60577fb1 100644 --- a/graphics/java/android/graphics/drawable/shapes/RoundRectShape.java +++ b/graphics/java/android/graphics/drawable/shapes/RoundRectShape.java @@ -94,7 +94,7 @@ public class RoundRectShape extends RectShape { for (int i = 1; i < 8; i++) { if (mOuterRadii[i] != radius) { // can't call simple constructors, use path - outline.setConvexPath(mPath); + outline.setPath(mPath); return; } } diff --git a/graphics/java/android/graphics/fonts/Font.java b/graphics/java/android/graphics/fonts/Font.java index 552088f7c478..b09082e65ca4 100644 --- a/graphics/java/android/graphics/fonts/Font.java +++ b/graphics/java/android/graphics/fonts/Font.java @@ -19,6 +19,7 @@ package android.graphics.fonts; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; +import android.content.res.AssetFileDescriptor; import android.content.res.AssetManager; import android.content.res.Resources; import android.os.LocaleList; @@ -35,7 +36,9 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStream; import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.nio.channels.FileChannel; import java.util.Arrays; import java.util.Objects; @@ -54,10 +57,6 @@ public final class Font { * A builder class for creating new Font. */ public static final class Builder { - private static final NativeAllocationRegistry sAssetByteBufferRegistry = - NativeAllocationRegistry.createMalloced(ByteBuffer.class.getClassLoader(), - nGetReleaseNativeAssetFunc()); - private static final NativeAllocationRegistry sFontRegistry = NativeAllocationRegistry.createMalloced(Font.class.getClassLoader(), nGetReleaseNativeFont()); @@ -151,7 +150,11 @@ public final class Font { * @param path the file name of the font data in the asset directory */ public Builder(@NonNull AssetManager am, @NonNull String path) { - this(am, path, true /* is asset */, 0 /* cookie */); + try { + mBuffer = createBuffer(am, path, true /* is asset */, 0 /* cookie */); + } catch (IOException e) { + mException = e; + } } /** @@ -165,18 +168,11 @@ public final class Font { */ public Builder(@NonNull AssetManager am, @NonNull String path, boolean isAsset, int cookie) { - final long nativeAsset = nGetNativeAsset(am, path, isAsset, cookie); - if (nativeAsset == 0) { - mException = new FileNotFoundException("Unable to open " + path); - return; - } - final ByteBuffer b = nGetAssetBuffer(nativeAsset); - sAssetByteBufferRegistry.registerNativeAllocation(b, nativeAsset); - if (b == null) { - mException = new FileNotFoundException(path + " not found"); - return; + try { + mBuffer = createBuffer(am, path, isAsset, cookie); + } catch (IOException e) { + mException = e; } - mBuffer = b; } /** @@ -199,19 +195,66 @@ public final class Font { mException = new FileNotFoundException(resId + " must be font file."); return; } - final long nativeAsset = nGetNativeAsset(res.getAssets(), str, false /* is asset */, - value.assetCookie); - if (nativeAsset == 0) { - mException = new FileNotFoundException("Unable to open " + str); - return; + + try { + mBuffer = createBuffer(res.getAssets(), str, false, value.assetCookie); + } catch (IOException e) { + mException = e; } - final ByteBuffer b = nGetAssetBuffer(nativeAsset); - sAssetByteBufferRegistry.registerNativeAllocation(b, nativeAsset); - if (b == null) { - mException = new FileNotFoundException(str + " not found"); - return; + } + + /** + * Creates a buffer containing font data using the assetManager and other + * provided inputs. + * + * @param am the application's asset manager + * @param path the file name of the font data in the asset directory + * @param isAsset true if the undelying data is in asset + * @param cookie set asset cookie + * @return buffer containing the contents of the file + * + * @hide + */ + public static ByteBuffer createBuffer(@NonNull AssetManager am, @NonNull String path, + boolean isAsset, int cookie) throws IOException { + Preconditions.checkNotNull(am, "assetManager can not be null"); + Preconditions.checkNotNull(path, "path can not be null"); + + // Attempt to open as FD, which should work unless the asset is compressed + AssetFileDescriptor assetFD; + try { + if (isAsset) { + assetFD = am.openFd(path); + } else if (cookie > 0) { + assetFD = am.openNonAssetFd(cookie, path); + } else { + assetFD = am.openNonAssetFd(path); + } + + try (FileInputStream fis = assetFD.createInputStream()) { + final FileChannel fc = fis.getChannel(); + long startOffset = assetFD.getStartOffset(); + long declaredLength = assetFD.getDeclaredLength(); + return fc.map(FileChannel.MapMode.READ_ONLY, startOffset, declaredLength); + } + } catch (IOException e) { + // failed to open as FD so now we will attempt to open as an input stream + } + + try (InputStream assetStream = isAsset ? am.open(path, AssetManager.ACCESS_BUFFER) + : am.openNonAsset(cookie, path, AssetManager.ACCESS_BUFFER)) { + + int capacity = assetStream.available(); + ByteBuffer buffer = ByteBuffer.allocateDirect(capacity); + buffer.order(ByteOrder.nativeOrder()); + assetStream.read(buffer.array(), buffer.arrayOffset(), assetStream.available()); + + if (assetStream.read() != -1) { + throw new IOException("Unable to access full contents of " + path); + } + + return buffer; } - mBuffer = b; } /** @@ -396,15 +439,6 @@ public final class Font { } /** - * Native methods for accessing underlying buffer in Asset - */ - private static native long nGetNativeAsset( - @NonNull AssetManager am, @NonNull String path, boolean isAsset, int cookie); - private static native ByteBuffer nGetAssetBuffer(long nativeAsset); - @CriticalNative - private static native long nGetReleaseNativeAssetFunc(); - - /** * Native methods for creating Font */ private static native long nInitBuilder(); @@ -519,12 +553,13 @@ public final class Font { } Font f = (Font) o; return mFontStyle.equals(f.mFontStyle) && f.mTtcIndex == mTtcIndex - && Arrays.equals(f.mAxes, mAxes) && f.mBuffer.equals(mBuffer); + && Arrays.equals(f.mAxes, mAxes) && f.mBuffer.equals(mBuffer) + && Objects.equals(f.mLocaleList, mLocaleList); } @Override public int hashCode() { - return Objects.hash(mFontStyle, mTtcIndex, Arrays.hashCode(mAxes), mBuffer); + return Objects.hash(mFontStyle, mTtcIndex, Arrays.hashCode(mAxes), mBuffer, mLocaleList); } @Override diff --git a/graphics/java/android/graphics/fonts/SystemFonts.java b/graphics/java/android/graphics/fonts/SystemFonts.java index 4a9cf14d04a5..95a8417b6f7f 100644 --- a/graphics/java/android/graphics/fonts/SystemFonts.java +++ b/graphics/java/android/graphics/fonts/SystemFonts.java @@ -105,7 +105,6 @@ public final class SystemFonts { final long fontSize = fileChannel.size(); return fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fontSize); } catch (IOException e) { - Log.e(TAG, "Error mapping font file " + fullPath); return null; } } diff --git a/graphics/java/android/graphics/pdf/PdfDocument.java b/graphics/java/android/graphics/pdf/PdfDocument.java index 1b8336f54637..58421ab5ccd9 100644 --- a/graphics/java/android/graphics/pdf/PdfDocument.java +++ b/graphics/java/android/graphics/pdf/PdfDocument.java @@ -46,8 +46,8 @@ import java.util.List; * // create a new document * PdfDocument document = new PdfDocument(); * - * // crate a page description - * PageInfo pageInfo = new PageInfo.Builder(new Rect(0, 0, 100, 100), 1).create(); + * // create a page description + * PageInfo pageInfo = new PageInfo.Builder(100, 100, 1).create(); * * // start a page * Page page = document.startPage(pageInfo); diff --git a/graphics/java/android/graphics/pdf/TEST_MAPPING b/graphics/java/android/graphics/pdf/TEST_MAPPING new file mode 100644 index 000000000000..d763598f5ba0 --- /dev/null +++ b/graphics/java/android/graphics/pdf/TEST_MAPPING @@ -0,0 +1,7 @@ +{ + "presubmit": [ + { + "name": "CtsPdfTestCases" + } + ] +} |