diff options
Diffstat (limited to 'graphics')
34 files changed, 4021 insertions, 486 deletions
diff --git a/graphics/java/android/graphics/BLASTBufferQueue.java b/graphics/java/android/graphics/BLASTBufferQueue.java index 1c41d06a3da2..c52f700ef4f6 100644 --- a/graphics/java/android/graphics/BLASTBufferQueue.java +++ b/graphics/java/android/graphics/BLASTBufferQueue.java @@ -16,6 +16,7 @@ package android.graphics; +import android.annotation.NonNull; import android.view.Surface; import android.view.SurfaceControl; @@ -31,9 +32,10 @@ public final class BLASTBufferQueue { private static native long nativeCreate(String name, boolean updateDestinationFrame); private static native void nativeDestroy(long ptr); private static native Surface nativeGetSurface(long ptr, boolean includeSurfaceControlHandle); - private static native void nativeSyncNextTransaction(long ptr, + private static native boolean nativeSyncNextTransaction(long ptr, Consumer<SurfaceControl.Transaction> callback, boolean acquireSingleBuffer); private static native void nativeStopContinuousSyncTransaction(long ptr); + private static native void nativeClearSyncTransaction(long ptr); private static native void nativeUpdate(long ptr, long surfaceControl, long width, long height, int format); private static native void nativeMergeWithNextTransaction(long ptr, long transactionPtr, @@ -47,7 +49,7 @@ public final class BLASTBufferQueue { TransactionHangCallback callback); public interface TransactionHangCallback { - void onTransactionHang(boolean isGpuHang); + void onTransactionHang(String reason); } /** Create a new connection with the surface flinger. */ @@ -92,9 +94,9 @@ public final class BLASTBufferQueue { * acquired. If false, continue to acquire all buffers into the * transaction until stopContinuousSyncTransaction is called. */ - public void syncNextTransaction(boolean acquireSingleBuffer, - Consumer<SurfaceControl.Transaction> callback) { - nativeSyncNextTransaction(mNativeObject, callback, acquireSingleBuffer); + public boolean syncNextTransaction(boolean acquireSingleBuffer, + @NonNull Consumer<SurfaceControl.Transaction> callback) { + return nativeSyncNextTransaction(mNativeObject, callback, acquireSingleBuffer); } /** @@ -104,8 +106,8 @@ public final class BLASTBufferQueue { * @param callback The callback invoked when the buffer has been added to the transaction. The * callback will contain the transaction with the buffer. */ - public void syncNextTransaction(Consumer<SurfaceControl.Transaction> callback) { - syncNextTransaction(true /* acquireSingleBuffer */, callback); + public boolean syncNextTransaction(@NonNull Consumer<SurfaceControl.Transaction> callback) { + return syncNextTransaction(true /* acquireSingleBuffer */, callback); } /** @@ -118,6 +120,14 @@ public final class BLASTBufferQueue { } /** + * Tell BBQ to clear the sync transaction that was previously set. The callback will not be + * invoked when the next frame is acquired. + */ + public void clearSyncTransaction() { + nativeClearSyncTransaction(mNativeObject); + } + + /** * Updates {@link SurfaceControl}, size, and format for a particular BLASTBufferQueue * @param sc The new SurfaceControl that this BLASTBufferQueue will update * @param width The new width for the buffer. diff --git a/graphics/java/android/graphics/BaseCanvas.java b/graphics/java/android/graphics/BaseCanvas.java index 425a37891afb..a7acaf924c15 100644 --- a/graphics/java/android/graphics/BaseCanvas.java +++ b/graphics/java/android/graphics/BaseCanvas.java @@ -96,7 +96,7 @@ public abstract class BaseCanvas { // These are also implemented in RecordingCanvas so that we can // selectively apply on them // Everything below here is copy/pasted from Canvas.java - // The JNI registration is handled by android_view_Canvas.cpp + // The JNI registration is handled by android_graphics_Canvas.cpp // --------------------------------------------------------------------------- public void drawArc(float left, float top, float right, float bottom, float startAngle, @@ -330,11 +330,7 @@ public abstract class BaseCanvas { public void drawPath(@NonNull Path path, @NonNull Paint paint) { throwIfHasHwFeaturesInSwMode(paint); - if (path.isSimplePath && path.rects != null) { - nDrawRegion(mNativeCanvasWrapper, path.rects.mNativeRegion, paint.getNativeInstance()); - } else { - nDrawPath(mNativeCanvasWrapper, path.readOnlyNI(), paint.getNativeInstance()); - } + nDrawPath(mNativeCanvasWrapper, path.readOnlyNI(), paint.getNativeInstance()); } public void drawPoint(float x, float y, @NonNull Paint paint) { @@ -672,10 +668,34 @@ public abstract class BaseCanvas { } /** + * Draws a mesh object to the screen. + * + * <p>Note: antialiasing is not supported, therefore {@link Paint#ANTI_ALIAS_FLAG} is + * ignored.</p> + * + * @param mesh {@link Mesh} object that will be drawn to the screen + * @param blendMode {@link BlendMode} used to blend mesh primitives as the destination color + * with the Paint color/shader as the source color. This defaults to + * {@link BlendMode#MODULATE} if null. + * @param paint {@link Paint} used to provide a color/shader/blend mode. + */ + public void drawMesh(@NonNull Mesh mesh, @Nullable BlendMode blendMode, @NonNull Paint paint) { + if (!isHardwareAccelerated() && onHwFeatureInSwMode()) { + throw new RuntimeException("software rendering doesn't support meshes"); + } + if (blendMode == null) { + blendMode = BlendMode.MODULATE; + } + nDrawMesh(this.mNativeCanvasWrapper, mesh.getNativeWrapperInstance(), + blendMode.getXfermode().porterDuffMode, paint.getNativeInstance()); + } + + /** * @hide */ - public void punchHole(float left, float top, float right, float bottom, float rx, float ry) { - nPunchHole(mNativeCanvasWrapper, left, top, right, bottom, rx, ry); + public void punchHole(float left, float top, float right, float bottom, float rx, float ry, + float alpha) { + nPunchHole(mNativeCanvasWrapper, left, top, right, bottom, rx, ry, alpha); } /** @@ -804,6 +824,9 @@ public abstract class BaseCanvas { int vertOffset, float[] texs, int texOffset, int[] colors, int colorOffset, short[] indices, int indexOffset, int indexCount, long nativePaint); + private static native void nDrawMesh( + long nativeCanvas, long nativeMesh, int mode, long nativePaint); + private static native void nDrawGlyphs(long nativeCanvas, int[] glyphIds, float[] positions, int glyphIdStart, int positionStart, int glyphCount, long nativeFont, long nativePaint); @@ -827,5 +850,5 @@ public abstract class BaseCanvas { float hOffset, float vOffset, int flags, long nativePaint); private static native void nPunchHole(long renderer, float left, float top, float right, - float bottom, float rx, float ry); + float bottom, float rx, float ry, float alpha); } diff --git a/graphics/java/android/graphics/BaseRecordingCanvas.java b/graphics/java/android/graphics/BaseRecordingCanvas.java index a998ba870f74..2ec4524e1241 100644 --- a/graphics/java/android/graphics/BaseRecordingCanvas.java +++ b/graphics/java/android/graphics/BaseRecordingCanvas.java @@ -286,11 +286,7 @@ public class BaseRecordingCanvas extends Canvas { @Override public final void drawPath(@NonNull Path path, @NonNull Paint paint) { - if (path.isSimplePath && path.rects != null) { - nDrawRegion(mNativeCanvasWrapper, path.rects.mNativeRegion, paint.getNativeInstance()); - } else { - nDrawPath(mNativeCanvasWrapper, path.readOnlyNI(), paint.getNativeInstance()); - } + nDrawPath(mNativeCanvasWrapper, path.readOnlyNI(), paint.getNativeInstance()); } @Override @@ -610,12 +606,22 @@ public class BaseRecordingCanvas extends Canvas { indices, indexOffset, indexCount, paint.getNativeInstance()); } + @Override + public final void drawMesh(@NonNull Mesh mesh, BlendMode blendMode, @NonNull Paint paint) { + if (blendMode == null) { + blendMode = BlendMode.MODULATE; + } + nDrawMesh(mNativeCanvasWrapper, mesh.getNativeWrapperInstance(), + blendMode.getXfermode().porterDuffMode, paint.getNativeInstance()); + } + /** * @hide */ @Override - public void punchHole(float left, float top, float right, float bottom, float rx, float ry) { - nPunchHole(mNativeCanvasWrapper, left, top, right, bottom, rx, ry); + public void punchHole(float left, float top, float right, float bottom, float rx, float ry, + float alpha) { + nPunchHole(mNativeCanvasWrapper, left, top, right, bottom, rx, ry, alpha); } @FastNative @@ -711,6 +717,10 @@ public class BaseRecordingCanvas extends Canvas { long nativePaint); @FastNative + private static native void nDrawMesh( + long canvasHandle, long nativeMesh, int mode, long nativePaint); + + @FastNative private static native void nDrawVertices(long nativeCanvas, int mode, int n, float[] verts, int vertOffset, float[] texs, int texOffset, int[] colors, int colorOffset, short[] indices, int indexOffset, int indexCount, long nativePaint); @@ -746,5 +756,5 @@ public class BaseRecordingCanvas extends Canvas { @FastNative private static native void nPunchHole(long renderer, float left, float top, float right, - float bottom, float rx, float ry); + float bottom, float rx, float ry, float alpha); } diff --git a/graphics/java/android/graphics/Bitmap.java b/graphics/java/android/graphics/Bitmap.java index 857af11e4ca3..b9d3756ac6d2 100644 --- a/graphics/java/android/graphics/Bitmap.java +++ b/graphics/java/android/graphics/Bitmap.java @@ -26,7 +26,9 @@ import android.compat.annotation.UnsupportedAppUsage; import android.hardware.HardwareBuffer; import android.os.Build; import android.os.Parcel; +import android.os.ParcelFileDescriptor; import android.os.Parcelable; +import android.os.SharedMemory; import android.os.StrictMode; import android.os.Trace; import android.util.DisplayMetrics; @@ -38,6 +40,7 @@ import dalvik.annotation.optimization.CriticalNative; import libcore.util.NativeAllocationRegistry; +import java.io.IOException; import java.io.OutputStream; import java.lang.ref.WeakReference; import java.nio.Buffer; @@ -90,6 +93,7 @@ public final class Bitmap implements Parcelable { private boolean mRecycled; private ColorSpace mColorSpace; + private Gainmap mGainmap; /*package*/ int mDensity = getDefaultDensity(); @@ -277,7 +281,7 @@ public final class Bitmap implements Parcelable { * @see #setHeight(int) * @see #setConfig(Config) */ - public void reconfigure(int width, int height, Config config) { + public void reconfigure(int width, int height, @NonNull Config config) { checkRecycled("Can't call reconfigure() on a recycled bitmap"); if (width <= 0 || height <= 0) { throw new IllegalArgumentException("width and height must be > 0"); @@ -336,7 +340,7 @@ public final class Bitmap implements Parcelable { * @see #setWidth(int) * @see #setHeight(int) */ - public void setConfig(Config config) { + public void setConfig(@NonNull Config config) { reconfigure(getWidth(), getHeight(), config); } @@ -397,8 +401,9 @@ public final class Bitmap implements Parcelable { /** * This is called by methods that want to throw an exception if the bitmap * has already been recycled. + * @hide */ - private void checkRecycled(String errorMessage) { + void checkRecycled(String errorMessage) { if (mRecycled) { throw new IllegalStateException(errorMessage); } @@ -590,7 +595,7 @@ public final class Bitmap implements Parcelable { * in the buffer.</p> * @throws IllegalStateException if the bitmap's config is {@link Config#HARDWARE} */ - public void copyPixelsToBuffer(Buffer dst) { + public void copyPixelsToBuffer(@NonNull Buffer dst) { checkHardware("unable to copyPixelsToBuffer, " + "pixel access is not supported on Config#HARDWARE bitmaps"); int elements = dst.remaining(); @@ -632,7 +637,7 @@ public final class Bitmap implements Parcelable { * first rewind the buffer.</p> * @throws IllegalStateException if the bitmap's config is {@link Config#HARDWARE} */ - public void copyPixelsFromBuffer(Buffer src) { + public void copyPixelsFromBuffer(@NonNull Buffer src) { checkRecycled("copyPixelsFromBuffer called on recycled bitmap"); checkHardware("unable to copyPixelsFromBuffer, Config#HARDWARE bitmaps are immutable"); @@ -686,7 +691,7 @@ public final class Bitmap implements Parcelable { * @return the new bitmap, or null if the copy could not be made. * @throws IllegalArgumentException if config is {@link Config#HARDWARE} and isMutable is true */ - public Bitmap copy(Config config, boolean isMutable) { + public Bitmap copy(@NonNull Config config, boolean isMutable) { checkRecycled("Can't copy a recycled bitmap"); if (config == Config.HARDWARE && isMutable) { throw new IllegalArgumentException("Hardware bitmaps are always immutable"); @@ -738,6 +743,26 @@ public final class Bitmap implements Parcelable { } /** + * Returns the shared memory handle to the pixel storage if the bitmap is already using + * shared memory and null if it is not. The SharedMemory object is then useful to then pass + * through HIDL APIs (e.g. WearOS's DisplayOffload service). + * + * @hide + */ + public SharedMemory getSharedMemory() { + checkRecycled("Cannot access shared memory of a recycled bitmap"); + if (nativeIsBackedByAshmem(mNativePtr)) { + try { + int fd = nativeGetAshmemFD(mNativePtr); + return SharedMemory.fromFileDescriptor(ParcelFileDescriptor.fromFd(fd)); + } catch (IOException e) { + Log.e(TAG, "Unable to create dup'd file descriptor for shared bitmap memory"); + } + } + return null; + } + + /** * Create a hardware bitmap backed by a {@link HardwareBuffer}. * * <p>The passed HardwareBuffer's usage flags must contain @@ -791,6 +816,7 @@ public final class Bitmap implements Parcelable { * @return The new scaled bitmap or the source bitmap if no scaling is required. * @throws IllegalArgumentException if width is <= 0, or height is <= 0 */ + @NonNull public static Bitmap createScaledBitmap(@NonNull Bitmap src, int dstWidth, int dstHeight, boolean filter) { Matrix m = new Matrix(); @@ -810,6 +836,7 @@ public final class Bitmap implements Parcelable { * be the same object as source, or a copy may have been made. It is * initialized with the same density and color space as the original bitmap. */ + @NonNull public static Bitmap createBitmap(@NonNull Bitmap src) { return createBitmap(src, 0, 0, src.getWidth(), src.getHeight()); } @@ -830,6 +857,7 @@ public final class Bitmap implements Parcelable { * outside of the dimensions of the source bitmap, or width is <= 0, * or height is <= 0 */ + @NonNull public static Bitmap createBitmap(@NonNull Bitmap source, int x, int y, int width, int height) { return createBitmap(source, x, y, width, height, null, false); } @@ -865,6 +893,7 @@ public final class Bitmap implements Parcelable { * outside of the dimensions of the source bitmap, or width is <= 0, * or height is <= 0, or if the source bitmap has already been recycled */ + @NonNull public static Bitmap createBitmap(@NonNull Bitmap source, int x, int y, int width, int height, @Nullable Matrix m, boolean filter) { @@ -968,12 +997,63 @@ public final class Bitmap implements Parcelable { canvas.concat(m); canvas.drawBitmap(source, srcR, dstR, paint); canvas.setBitmap(null); + + // If the source has a gainmap, apply the same set of transformations to the gainmap + // and set it on the output + if (source.hasGainmap()) { + Bitmap newMapContents = transformGainmap(source, m, neww, newh, paint, srcR, dstR, + deviceR); + if (newMapContents != null) { + bitmap.setGainmap(new Gainmap(source.getGainmap(), newMapContents)); + } + } + if (isHardware) { return bitmap.copy(Config.HARDWARE, false); } return bitmap; } + private static Bitmap transformGainmap(Bitmap source, Matrix m, int neww, int newh, Paint paint, + Rect srcR, RectF dstR, RectF deviceR) { + Canvas canvas; + Bitmap sourceGainmap = source.getGainmap().getGainmapContents(); + // Gainmaps can be scaled relative to the base image (eg, 1/4th res) + // Preserve that relative scaling between the base & gainmap in the output + float scaleX = (sourceGainmap.getWidth() / (float) source.getWidth()); + float scaleY = (sourceGainmap.getHeight() / (float) source.getHeight()); + int mapw = Math.round(neww * scaleX); + int maph = Math.round(newh * scaleY); + + if (mapw == 0 || maph == 0) { + // The gainmap has been scaled away entirely, drop it + return null; + } + + // Scale the computed `srcR` used for rendering the source bitmap to the destination + // to be in gainmap dimensions + Rect gSrcR = new Rect((int) (srcR.left * scaleX), + (int) (srcR.top * scaleY), (int) (srcR.right * scaleX), + (int) (srcR.bottom * scaleY)); + + // Note: createBitmap isn't used as that requires a non-null colorspace, however + // gainmaps don't have a colorspace. So use `nativeCreate` directly to bypass + // that colorspace enforcement requirement (#getColorSpace() allows a null return) + Bitmap newMapContents = nativeCreate(null, 0, mapw, mapw, maph, + sourceGainmap.getConfig().nativeInt, true, 0); + newMapContents.eraseColor(0); + canvas = new Canvas(newMapContents); + // Scale the translate & matrix to be in gainmap-relative dimensions + canvas.scale(scaleX, scaleY); + canvas.translate(-deviceR.left, -deviceR.top); + canvas.concat(m); + canvas.drawBitmap(sourceGainmap, gSrcR, dstR, paint); + canvas.setBitmap(null); + // Create a new gainmap using a copy of the metadata information from the source but + // with the transformed bitmap created above + return newMapContents; + } + /** * Returns a mutable bitmap with the specified width and height. Its * initial density is as per {@link #getDensity}. The newly created @@ -985,6 +1065,7 @@ public final class Bitmap implements Parcelable { * @throws IllegalArgumentException if the width or height are <= 0, or if * Config is Config.HARDWARE, because hardware bitmaps are always immutable */ + @NonNull public static Bitmap createBitmap(int width, int height, @NonNull Config config) { return createBitmap(width, height, config, true); } @@ -1003,6 +1084,7 @@ public final class Bitmap implements Parcelable { * @throws IllegalArgumentException if the width or height are <= 0, or if * Config is Config.HARDWARE, because hardware bitmaps are always immutable */ + @NonNull public static Bitmap createBitmap(@Nullable DisplayMetrics display, int width, int height, @NonNull Config config) { return createBitmap(display, width, height, config, true); @@ -1023,6 +1105,7 @@ public final class Bitmap implements Parcelable { * @throws IllegalArgumentException if the width or height are <= 0, or if * Config is Config.HARDWARE, because hardware bitmaps are always immutable */ + @NonNull public static Bitmap createBitmap(int width, int height, @NonNull Config config, boolean hasAlpha) { return createBitmap(null, width, height, config, hasAlpha); @@ -1050,6 +1133,7 @@ public final class Bitmap implements Parcelable { * {@link ColorSpace.Rgb.TransferParameters ICC parametric curve}, or if * the color space is null */ + @NonNull public static Bitmap createBitmap(int width, int height, @NonNull Config config, boolean hasAlpha, @NonNull ColorSpace colorSpace) { return createBitmap(null, width, height, config, hasAlpha, colorSpace); @@ -1073,6 +1157,7 @@ public final class Bitmap implements Parcelable { * @throws IllegalArgumentException if the width or height are <= 0, or if * Config is Config.HARDWARE, because hardware bitmaps are always immutable */ + @NonNull public static Bitmap createBitmap(@Nullable DisplayMetrics display, int width, int height, @NonNull Config config, boolean hasAlpha) { return createBitmap(display, width, height, config, hasAlpha, @@ -1105,6 +1190,7 @@ public final class Bitmap implements Parcelable { * {@link ColorSpace.Rgb.TransferParameters ICC parametric curve}, or if * the color space is null */ + @NonNull public static Bitmap createBitmap(@Nullable DisplayMetrics display, int width, int height, @NonNull Config config, boolean hasAlpha, @NonNull ColorSpace colorSpace) { if (width <= 0 || height <= 0) { @@ -1152,6 +1238,7 @@ public final class Bitmap implements Parcelable { * @throws IllegalArgumentException if the width or height are <= 0, or if * the color array's length is less than the number of pixels. */ + @NonNull public static Bitmap createBitmap(@NonNull @ColorInt int[] colors, int offset, int stride, int width, int height, @NonNull Config config) { return createBitmap(null, colors, offset, stride, width, height, config); @@ -1179,6 +1266,7 @@ public final class Bitmap implements Parcelable { * @throws IllegalArgumentException if the width or height are <= 0, or if * the color array's length is less than the number of pixels. */ + @NonNull public static Bitmap createBitmap(@NonNull DisplayMetrics display, @NonNull @ColorInt int[] colors, int offset, int stride, int width, int height, @NonNull Config config) { @@ -1221,6 +1309,7 @@ public final class Bitmap implements Parcelable { * @throws IllegalArgumentException if the width or height are <= 0, or if * the color array's length is less than the number of pixels. */ + @NonNull public static Bitmap createBitmap(@NonNull @ColorInt int[] colors, int width, int height, Config config) { return createBitmap(null, colors, 0, width, width, height, config); @@ -1245,6 +1334,7 @@ public final class Bitmap implements Parcelable { * @throws IllegalArgumentException if the width or height are <= 0, or if * the color array's length is less than the number of pixels. */ + @NonNull public static Bitmap createBitmap(@Nullable DisplayMetrics display, @NonNull @ColorInt int colors[], int width, int height, @NonNull Config config) { return createBitmap(display, colors, 0, width, width, height, config); @@ -1262,7 +1352,8 @@ public final class Bitmap implements Parcelable { * @return An immutable bitmap with a HARDWARE config whose contents are created * from the recorded drawing commands in the Picture source. */ - public static @NonNull Bitmap createBitmap(@NonNull Picture source) { + @NonNull + public static Bitmap createBitmap(@NonNull Picture source) { return createBitmap(source, source.getWidth(), source.getHeight(), Config.HARDWARE); } @@ -1283,7 +1374,8 @@ public final class Bitmap implements Parcelable { * * @return An immutable bitmap with a configuration specified by the config parameter */ - public static @NonNull Bitmap createBitmap(@NonNull Picture source, int width, int height, + @NonNull + public static Bitmap createBitmap(@NonNull Picture source, int width, int height, @NonNull Config config) { if (width <= 0 || height <= 0) { throw new IllegalArgumentException("width & height must be > 0"); @@ -1330,6 +1422,7 @@ public final class Bitmap implements Parcelable { * Returns an optional array of private data, used by the UI system for * some bitmaps. Not intended to be called by applications. */ + @Nullable public byte[] getNinePatchChunk() { return mNinePatchChunk; } @@ -1431,7 +1524,8 @@ public final class Bitmap implements Parcelable { * @return true if successfully compressed to the specified stream. */ @WorkerThread - public boolean compress(CompressFormat format, int quality, OutputStream stream) { + public boolean compress(@NonNull CompressFormat format, int quality, + @NonNull OutputStream stream) { checkRecycled("Can't compress a recycled bitmap"); // do explicit check before calling the native method if (stream == null) { @@ -1548,7 +1642,7 @@ public final class Bitmap implements Parcelable { * Convenience for calling {@link #getScaledWidth(int)} with the target * density of the given {@link Canvas}. */ - public int getScaledWidth(Canvas canvas) { + public int getScaledWidth(@NonNull Canvas canvas) { return scaleFromDensity(getWidth(), mDensity, canvas.mDensity); } @@ -1556,7 +1650,7 @@ public final class Bitmap implements Parcelable { * Convenience for calling {@link #getScaledHeight(int)} with the target * density of the given {@link Canvas}. */ - public int getScaledHeight(Canvas canvas) { + public int getScaledHeight(@NonNull Canvas canvas) { return scaleFromDensity(getHeight(), mDensity, canvas.mDensity); } @@ -1564,7 +1658,7 @@ public final class Bitmap implements Parcelable { * Convenience for calling {@link #getScaledWidth(int)} with the target * density of the given {@link DisplayMetrics}. */ - public int getScaledWidth(DisplayMetrics metrics) { + public int getScaledWidth(@NonNull DisplayMetrics metrics) { return scaleFromDensity(getWidth(), mDensity, metrics.densityDpi); } @@ -1572,7 +1666,7 @@ public final class Bitmap implements Parcelable { * Convenience for calling {@link #getScaledHeight(int)} with the target * density of the given {@link DisplayMetrics}. */ - public int getScaledHeight(DisplayMetrics metrics) { + public int getScaledHeight(@NonNull DisplayMetrics metrics) { return scaleFromDensity(getHeight(), mDensity, metrics.densityDpi); } @@ -1682,6 +1776,7 @@ public final class Bitmap implements Parcelable { * If the bitmap's internal config is in one of the public formats, return * that config, otherwise return null. */ + @NonNull public final Config getConfig() { if (mRecycled) { Log.w(TAG, "Called getConfig() on a recycle()'d bitmap! This is undefined behavior!"); @@ -1791,10 +1886,15 @@ public final class Bitmap implements Parcelable { * {@code ColorSpace}.</p> * * @throws IllegalArgumentException If the specified color space is {@code null}, not - * {@link ColorSpace.Model#RGB RGB}, has a transfer function that is not an - * {@link ColorSpace.Rgb.TransferParameters ICC parametric curve}, or whose - * components min/max values reduce the numerical range compared to the - * previously assigned color space. + * {@link ColorSpace.Model#RGB RGB}, or whose components min/max values reduce + * the numerical range compared to the previously assigned color space. + * Prior to {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, + * <code>IllegalArgumentException</code> will also be thrown + * if the specified color space has a transfer function that is not an + * {@link ColorSpace.Rgb.TransferParameters ICC parametric curve}. Starting from + * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, the color spaces with non + * ICC parametric curve transfer function are allowed. + * E.g., {@link ColorSpace.Named#BT2020_HLG BT2020_HLG}. * * @throws IllegalArgumentException If the {@code Config} (returned by {@link #getConfig()}) * is {@link Config#ALPHA_8}. @@ -1850,6 +1950,34 @@ public final class Bitmap implements Parcelable { } /** + * Returns whether or not this Bitmap contains a Gainmap. + */ + public boolean hasGainmap() { + checkRecycled("Bitmap is recycled"); + return nativeHasGainmap(mNativePtr); + } + + /** + * Returns the gainmap or null if the bitmap doesn't contain a gainmap + */ + public @Nullable Gainmap getGainmap() { + checkRecycled("Bitmap is recycled"); + if (mGainmap == null) { + mGainmap = nativeExtractGainmap(mNativePtr); + } + return mGainmap; + } + + /** + * Sets a gainmap on this bitmap, or removes the gainmap if null + */ + public void setGainmap(@Nullable Gainmap gainmap) { + checkRecycled("Bitmap is recycled"); + mGainmap = null; + nativeSetGainmap(mNativePtr, gainmap == null ? 0 : gainmap.mNativePtr); + } + + /** * Fills the bitmap's pixels with the specified {@link Color}. * * @throws IllegalStateException if the bitmap is not mutable. @@ -1926,7 +2054,7 @@ public final class Bitmap implements Parcelable { checkPixelAccess(x, y); final ColorSpace cs = getColorSpace(); - if (cs.equals(ColorSpace.get(ColorSpace.Named.SRGB))) { + if (cs == null || cs.equals(ColorSpace.get(ColorSpace.Named.SRGB))) { return Color.valueOf(nativeGetPixel(mNativePtr, x, y)); } // The returned value is in kRGBA_F16_SkColorType, which is packed as @@ -1967,7 +2095,7 @@ public final class Bitmap implements Parcelable { * to receive the specified number of pixels. * @throws IllegalStateException if the bitmap's config is {@link Config#HARDWARE} */ - public void getPixels(@ColorInt int[] pixels, int offset, int stride, + public void getPixels(@NonNull @ColorInt int[] pixels, int offset, int stride, int x, int y, int width, int height) { checkRecycled("Can't call getPixels() on a recycled bitmap"); checkHardware("unable to getPixels(), " @@ -2084,7 +2212,7 @@ public final class Bitmap implements Parcelable { * @throws ArrayIndexOutOfBoundsException if the pixels array is too small * to receive the specified number of pixels. */ - public void setPixels(@ColorInt int[] pixels, int offset, int stride, + public void setPixels(@NonNull @ColorInt int[] pixels, int offset, int stride, int x, int y, int width, int height) { checkRecycled("Can't call setPixels() on a recycled bitmap"); if (!isMutable()) { @@ -2098,25 +2226,28 @@ public final class Bitmap implements Parcelable { x, y, width, height); } - public static final @android.annotation.NonNull Parcelable.Creator<Bitmap> CREATOR + public static final @NonNull Parcelable.Creator<Bitmap> CREATOR = new Parcelable.Creator<Bitmap>() { - /** - * Rebuilds a bitmap previously stored with writeToParcel(). - * - * @param p Parcel object to read the bitmap from - * @return a new bitmap created from the data in the parcel - */ - public Bitmap createFromParcel(Parcel p) { - Bitmap bm = nativeCreateFromParcel(p); - if (bm == null) { - throw new RuntimeException("Failed to unparcel Bitmap"); - } - return bm; - } - public Bitmap[] newArray(int size) { - return new Bitmap[size]; - } - }; + /** + * Rebuilds a bitmap previously stored with writeToParcel(). + * + * @param p Parcel object to read the bitmap from + * @return a new bitmap created from the data in the parcel + */ + public Bitmap createFromParcel(Parcel p) { + Bitmap bm = nativeCreateFromParcel(p); + if (bm == null) { + throw new RuntimeException("Failed to unparcel Bitmap"); + } + if (p.readBoolean()) { + bm.setGainmap(p.readTypedObject(Gainmap.CREATOR)); + } + return bm; + } + public Bitmap[] newArray(int size) { + return new Bitmap[size]; + } + }; /** * No special parcel contents. @@ -2134,12 +2265,18 @@ public final class Bitmap implements Parcelable { * by the final pixel format * @param p Parcel object to write the bitmap data into */ - public void writeToParcel(Parcel p, int flags) { + public void writeToParcel(@NonNull Parcel p, int flags) { checkRecycled("Can't parcel a recycled bitmap"); noteHardwareBitmapSlowCall(); if (!nativeWriteToParcel(mNativePtr, mDensity, p)) { throw new RuntimeException("native writeToParcel failed"); } + if (hasGainmap()) { + p.writeBoolean(true); + p.writeTypedObject(mGainmap, flags); + } else { + p.writeBoolean(false); + } } /** @@ -2150,6 +2287,7 @@ public final class Bitmap implements Parcelable { * @return new bitmap containing the alpha channel of the original bitmap. */ @CheckResult + @NonNull public Bitmap extractAlpha() { return extractAlpha(null, null); } @@ -2180,7 +2318,8 @@ public final class Bitmap implements Parcelable { * paint that is passed to the draw call. */ @CheckResult - public Bitmap extractAlpha(Paint paint, int[] offsetXY) { + @NonNull + public Bitmap extractAlpha(@Nullable Paint paint, int[] offsetXY) { checkRecycled("Can't extractAlpha on a recycled bitmap"); long nativePaint = paint != null ? paint.getNativeInstance() : 0; noteHardwareBitmapSlowCall(); @@ -2197,12 +2336,12 @@ public final class Bitmap implements Parcelable { * and pixel data as this bitmap. If any of those differ, return false. * If other is null, return false. */ - public boolean sameAs(Bitmap other) { + @WorkerThread + public boolean sameAs(@Nullable Bitmap other) { + StrictMode.noteSlowCall("sameAs compares pixel data, not expected to be fast"); checkRecycled("Can't call sameAs on a recycled bitmap!"); - noteHardwareBitmapSlowCall(); if (this == other) return true; if (other == null) return false; - other.noteHardwareBitmapSlowCall(); if (other.isRecycled()) { throw new IllegalArgumentException("Can't compare to a recycled bitmap!"); } @@ -2247,7 +2386,8 @@ public final class Bitmap implements Parcelable { * @throws IllegalStateException if the bitmap's config is not {@link Config#HARDWARE} * or if the bitmap has been recycled. */ - public @NonNull HardwareBuffer getHardwareBuffer() { + @NonNull + public HardwareBuffer getHardwareBuffer() { checkRecycled("Can't getHardwareBuffer from a recycled bitmap"); HardwareBuffer hardwareBuffer = mHardwareBuffer == null ? null : mHardwareBuffer.get(); if (hardwareBuffer == null || hardwareBuffer.isClosed()) { @@ -2267,6 +2407,7 @@ public final class Bitmap implements Parcelable { boolean isMutable); private static native Bitmap nativeCopyAshmem(long nativeSrcBitmap); private static native Bitmap nativeCopyAshmemConfig(long nativeSrcBitmap, int nativeConfig); + private static native int nativeGetAshmemFD(long nativeBitmap); private static native long nativeGetNativeFinalizer(); private static native void nativeRecycle(long nativeBitmap); @UnsupportedAppUsage @@ -2329,6 +2470,9 @@ public final class Bitmap implements Parcelable { private static native void nativeSetImmutable(long nativePtr); + private static native Gainmap nativeExtractGainmap(long nativePtr); + private static native void nativeSetGainmap(long bitmapPtr, long gainmapPtr); + // ---------------- @CriticalNative ------------------- @CriticalNative @@ -2336,4 +2480,7 @@ public final class Bitmap implements Parcelable { @CriticalNative private static native boolean nativeIsBackedByAshmem(long nativePtr); + + @CriticalNative + private static native boolean nativeHasGainmap(long nativePtr); } diff --git a/graphics/java/android/graphics/BitmapFactory.java b/graphics/java/android/graphics/BitmapFactory.java index ef1e7bfc6651..1da8e189d768 100644 --- a/graphics/java/android/graphics/BitmapFactory.java +++ b/graphics/java/android/graphics/BitmapFactory.java @@ -161,11 +161,17 @@ public class BitmapFactory { * be thrown by the decode methods when setting a non-RGB color space * such as {@link ColorSpace.Named#CIE_LAB Lab}.</p> * - * <p class="note">The specified color space's transfer function must be + * <p class="note"> + * Prior to {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, + * the specified color space's transfer function must be * an {@link ColorSpace.Rgb.TransferParameters ICC parametric curve}. An * <code>IllegalArgumentException</code> will be thrown by the decode methods * if calling {@link ColorSpace.Rgb#getTransferParameters()} on the - * specified color space returns null.</p> + * specified color space returns null. + * + * Starting from {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, + * non ICC parametric curve transfer function is allowed. + * E.g., {@link ColorSpace.Named#BT2020_HLG BT2020_HLG}.</p> * * <p>After decode, the bitmap's color space is stored in * {@link #outColorSpace}.</p> @@ -458,7 +464,11 @@ public class BitmapFactory { throw new IllegalArgumentException("The destination color space must use the " + "RGB color model"); } - if (((ColorSpace.Rgb) opts.inPreferredColorSpace).getTransferParameters() == null) { + if (!opts.inPreferredColorSpace.equals(ColorSpace.get(ColorSpace.Named.BT2020_HLG)) + && !opts.inPreferredColorSpace.equals( + ColorSpace.get(ColorSpace.Named.BT2020_PQ)) + && ((ColorSpace.Rgb) opts.inPreferredColorSpace) + .getTransferParameters() == null) { throw new IllegalArgumentException("The destination color space must use an " + "ICC parametric transfer function"); } @@ -472,7 +482,9 @@ public class BitmapFactory { if (opts == null || opts.inBitmap == null) { return 0; } - + // Clear out the gainmap since we don't attempt to reuse it and don't want to + // accidentally keep it on the re-used bitmap + opts.inBitmap.setGainmap(null); return opts.inBitmap.getNativeInstance(); } diff --git a/graphics/java/android/graphics/BitmapShader.java b/graphics/java/android/graphics/BitmapShader.java index 43cb5ee8b5c0..5c065775eea2 100644 --- a/graphics/java/android/graphics/BitmapShader.java +++ b/graphics/java/android/graphics/BitmapShader.java @@ -17,6 +17,7 @@ package android.graphics; import android.annotation.IntDef; +import android.annotation.IntRange; import android.annotation.NonNull; import java.lang.annotation.Retention; @@ -102,6 +103,8 @@ public class BitmapShader extends Shader { private boolean mRequestDirectSampling; + private int mMaxAniso = 0; + /** * Call this to create a new shader that will draw with a bitmap. * @@ -117,6 +120,7 @@ public class BitmapShader extends Shader { if (bitmap == null) { throw new IllegalArgumentException("Bitmap must be non-null"); } + bitmap.checkRecycled("Cannot create BitmapShader for recycled bitmap"); mBitmap = bitmap; mTileX = tileX; mTileY = tileY; @@ -135,15 +139,47 @@ public class BitmapShader extends Shader { } /** - * Set the filter mode to be used when sampling from this shader + * Set the filter mode to be used when sampling from this shader. If this is configured + * then the anisotropic filtering value specified in any previous call to + * {@link #setMaxAnisotropy(int)} is ignored. */ public void setFilterMode(@FilterMode int mode) { if (mode != mFilterMode) { mFilterMode = mode; + mMaxAniso = 0; + discardNativeInstance(); + } + } + + /** + * Enables and configures the max anisotropy sampling value. If this value is configured, + * {@link #setFilterMode(int)} is ignored. + * + * Anisotropic filtering can enhance visual quality by removing aliasing effects of images + * that are at oblique viewing angles. This value is typically consumed as a power of 2 and + * anisotropic values of the next power of 2 typically provide twice the quality improvement + * as the previous value. For example, a sampling value of 4 would provide twice the improvement + * of a sampling value of 2. It is important to note that higher sampling values reach + * diminishing returns as the improvements between 8 and 16 can be slight. + * + * @param maxAnisotropy The Anisotropy value to use for filtering. Must be greater than 0. + */ + public void setMaxAnisotropy(@IntRange(from = 1) int maxAnisotropy) { + if (mMaxAniso != maxAnisotropy && maxAnisotropy > 0) { + mMaxAniso = maxAnisotropy; + mFilterMode = FILTER_MODE_DEFAULT; discardNativeInstance(); } } + /** + * Returns the current max anisotropic filtering value configured by + * {@link #setFilterMode(int)}. If {@link #setFilterMode(int)} is invoked this returns zero. + */ + public int getMaxAnisotropy() { + return mMaxAniso; + } + /** @hide */ /* package */ synchronized long getNativeInstanceWithDirectSampling() { mRequestDirectSampling = true; @@ -153,6 +189,8 @@ public class BitmapShader extends Shader { /** @hide */ @Override protected long createNativeInstance(long nativeMatrix, boolean filterFromPaint) { + mBitmap.checkRecycled("BitmapShader's bitmap has been recycled"); + boolean enableLinearFilter = mFilterMode == FILTER_MODE_LINEAR; if (mFilterMode == FILTER_MODE_DEFAULT) { mFilterFromPaint = filterFromPaint; @@ -162,8 +200,13 @@ public class BitmapShader extends Shader { mIsDirectSampled = mRequestDirectSampling; mRequestDirectSampling = false; - return nativeCreate(nativeMatrix, mBitmap.getNativeInstance(), mTileX, mTileY, - enableLinearFilter, mIsDirectSampled); + if (mMaxAniso > 0) { + return nativeCreateWithMaxAniso(nativeMatrix, mBitmap.getNativeInstance(), mTileX, + mTileY, mMaxAniso, mIsDirectSampled); + } else { + return nativeCreate(nativeMatrix, mBitmap.getNativeInstance(), mTileX, mTileY, + enableLinearFilter, mIsDirectSampled); + } } /** @hide */ @@ -175,5 +218,8 @@ public class BitmapShader extends Shader { private static native long nativeCreate(long nativeMatrix, long bitmapHandle, int shaderTileModeX, int shaderTileModeY, boolean filter, boolean isDirectSampled); + + private static native long nativeCreateWithMaxAniso(long nativeMatrix, long bitmapHandle, + int shaderTileModeX, int shaderTileModeY, int maxAniso, boolean isDirectSampled); } diff --git a/graphics/java/android/graphics/Canvas.java b/graphics/java/android/graphics/Canvas.java index 42c892a240b6..e7814cbd67e7 100644 --- a/graphics/java/android/graphics/Canvas.java +++ b/graphics/java/android/graphics/Canvas.java @@ -241,7 +241,7 @@ public class Canvas extends BaseCanvas { /** * Return true if the device that the current layer draws into is opaque - * (i.e. does not support per-pixel alpha). + * (that is, it does not support per-pixel alpha). * * @return true if the device that the current layer draws into is opaque */ diff --git a/graphics/java/android/graphics/ColorSpace.java b/graphics/java/android/graphics/ColorSpace.java index 6e60e9e2df6a..99bebb8b9812 100644 --- a/graphics/java/android/graphics/ColorSpace.java +++ b/graphics/java/android/graphics/ColorSpace.java @@ -24,7 +24,7 @@ import android.annotation.Size; import android.annotation.SuppressAutoDoc; import android.annotation.SuppressLint; import android.hardware.DataSpace; -import android.hardware.DataSpace.NamedDataSpace; +import android.hardware.DataSpace.ColorDataSpace; import android.util.SparseIntArray; import libcore.util.NativeAllocationRegistry; @@ -199,6 +199,8 @@ 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 }; + private static final float[] BT2020_PRIMARIES = + { 0.708f, 0.292f, 0.170f, 0.797f, 0.131f, 0.046f }; /** * A gray color space does not have meaningful primaries, so we use this arbitrary set. */ @@ -209,6 +211,16 @@ public abstract class ColorSpace { private static final Rgb.TransferParameters SRGB_TRANSFER_PARAMETERS = new Rgb.TransferParameters(1 / 1.055, 0.055 / 1.055, 1 / 12.92, 0.04045, 2.4); + // HLG transfer with an SDR whitepoint of 203 nits + private static final Rgb.TransferParameters BT2020_HLG_TRANSFER_PARAMETERS = + new Rgb.TransferParameters(2.0, 2.0, 1 / 0.17883277, 0.28466892, 0.55991073, + -0.685490157, Rgb.TransferParameters.TYPE_HLGish); + + // PQ transfer with an SDR whitepoint of 203 nits + private static final Rgb.TransferParameters BT2020_PQ_TRANSFER_PARAMETERS = + new Rgb.TransferParameters(-1.555223, 1.860454, 32 / 2523.0, 2413 / 128.0, + -2392 / 128.0, 8192 / 1305.0, Rgb.TransferParameters.TYPE_PQish); + // See static initialization block next to #get(Named) private static final ColorSpace[] sNamedColorSpaces = new ColorSpace[Named.values().length]; private static final SparseIntArray sDataToColorSpaces = new SparseIntArray(); @@ -703,7 +715,29 @@ public abstract class ColorSpace { * <tr><td>Range</td><td colspan="4">\(L: [0.0, 100.0], a: [-128, 128], b: [-128, 128]\)</td></tr> * </table> */ - CIE_LAB + CIE_LAB, + /** + * <p>{@link ColorSpace.Rgb RGB} color space BT.2100 standardized as + * Hybrid Log Gamma encoding.</p> + * <table summary="Color space definition"> + * <tr><th>Property</th><th colspan="4">Value</th></tr> + * <tr><td>Name</td><td colspan="4">Hybrid Log Gamma encoding</td></tr> + * <tr><td>CIE standard illuminant</td><td colspan="4">D65</td></tr> + * <tr><td>Range</td><td colspan="4">\([0..1]\)</td></tr> + * </table> + */ + BT2020_HLG, + /** + * <p>{@link ColorSpace.Rgb RGB} color space BT.2100 standardized as + * Perceptual Quantizer encoding.</p> + * <table summary="Color space definition"> + * <tr><th>Property</th><th colspan="4">Value</th></tr> + * <tr><td>Name</td><td colspan="4">Perceptual Quantizer encoding</td></tr> + * <tr><td>CIE standard illuminant</td><td colspan="4">D65</td></tr> + * <tr><td>Range</td><td colspan="4">\([0..1]\)</td></tr> + * </table> + */ + BT2020_PQ // Update the initialization block next to #get(Named) when adding new values } @@ -1406,7 +1440,7 @@ public abstract class ColorSpace { */ @SuppressLint("MethodNameUnits") @Nullable - public static ColorSpace getFromDataSpace(@NamedDataSpace int dataSpace) { + public static ColorSpace getFromDataSpace(@ColorDataSpace int dataSpace) { int index = sDataToColorSpaces.get(dataSpace, -1); if (index != -1) { return ColorSpace.get(index); @@ -1425,7 +1459,7 @@ public abstract class ColorSpace { * @return the dataspace value. */ @SuppressLint("MethodNameUnits") - public @NamedDataSpace int getDataSpace() { + public @ColorDataSpace int getDataSpace() { int index = sDataToColorSpaces.indexOfValue(getId()); if (index != -1) { return sDataToColorSpaces.keyAt(index); @@ -1534,7 +1568,7 @@ public abstract class ColorSpace { sDataToColorSpaces.put(DataSpace.DATASPACE_BT709, Named.BT709.ordinal()); sNamedColorSpaces[Named.BT2020.ordinal()] = new ColorSpace.Rgb( "Rec. ITU-R BT.2020-1", - new float[] { 0.708f, 0.292f, 0.170f, 0.797f, 0.131f, 0.046f }, + BT2020_PRIMARIES, ILLUMINANT_D65, null, new Rgb.TransferParameters(1 / 1.0993, 0.0993 / 1.0993, 1 / 4.5, 0.08145, 1 / 0.45), @@ -1616,6 +1650,84 @@ public abstract class ColorSpace { "Generic L*a*b*", Named.CIE_LAB.ordinal() ); + sNamedColorSpaces[Named.BT2020_HLG.ordinal()] = new ColorSpace.Rgb( + "Hybrid Log Gamma encoding", + BT2020_PRIMARIES, + ILLUMINANT_D65, + null, + x -> transferHLGOETF(BT2020_HLG_TRANSFER_PARAMETERS, x), + x -> transferHLGEOTF(BT2020_HLG_TRANSFER_PARAMETERS, x), + 0.0f, 1.0f, + BT2020_HLG_TRANSFER_PARAMETERS, + Named.BT2020_HLG.ordinal() + ); + sDataToColorSpaces.put(DataSpace.DATASPACE_BT2020_HLG, Named.BT2020_HLG.ordinal()); + sNamedColorSpaces[Named.BT2020_PQ.ordinal()] = new ColorSpace.Rgb( + "Perceptual Quantizer encoding", + BT2020_PRIMARIES, + ILLUMINANT_D65, + null, + x -> transferST2048OETF(BT2020_PQ_TRANSFER_PARAMETERS, x), + x -> transferST2048EOTF(BT2020_PQ_TRANSFER_PARAMETERS, x), + 0.0f, 1.0f, + BT2020_PQ_TRANSFER_PARAMETERS, + Named.BT2020_PQ.ordinal() + ); + sDataToColorSpaces.put(DataSpace.DATASPACE_BT2020_PQ, Named.BT2020_PQ.ordinal()); + } + + private static double transferHLGOETF(Rgb.TransferParameters params, double x) { + double sign = x < 0 ? -1.0 : 1.0; + x *= sign; + + // Unpack the transfer params matching skia's packing & invert R, G, and a + final double R = 1.0 / params.a; + final double G = 1.0 / params.b; + final double a = 1.0 / params.c; + final double b = params.d; + final double c = params.e; + final double K = params.f + 1.0; + + x /= K; + return sign * (x <= 1 ? R * Math.pow(x, G) : a * Math.log(x - b) + c); + } + + private static double transferHLGEOTF(Rgb.TransferParameters params, double x) { + double sign = x < 0 ? -1.0 : 1.0; + x *= sign; + + // Unpack the transfer params matching skia's packing + final double R = params.a; + final double G = params.b; + final double a = params.c; + final double b = params.d; + final double c = params.e; + final double K = params.f + 1.0; + + return K * sign * (x * R <= 1 ? Math.pow(x * R, G) : Math.exp((x - c) * a) + b); + } + + private static double transferST2048OETF(Rgb.TransferParameters params, double x) { + double sign = x < 0 ? -1.0 : 1.0; + x *= sign; + + double a = -params.a; + double b = params.d; + double c = 1.0 / params.f; + double d = params.b; + double e = -params.e; + double f = 1.0 / params.c; + + double tmp = Math.max(a + b * Math.pow(x, c), 0); + return sign * Math.pow(tmp / (d + e * Math.pow(x, c)), f); + } + + private static double transferST2048EOTF(Rgb.TransferParameters pq, double x) { + double sign = x < 0 ? -1.0 : 1.0; + x *= sign; + + double tmp = Math.max(pq.a + pq.b * Math.pow(x, pq.c), 0); + return sign * Math.pow(tmp / (pq.d + pq.e * Math.pow(x, pq.c)), pq.f); } // Reciprocal piecewise gamma response @@ -2182,6 +2294,10 @@ public abstract class ColorSpace { * </ul> */ public static class TransferParameters { + + private static final double TYPE_PQish = -2.0; + private static final double TYPE_HLGish = -3.0; + /** Variable \(a\) in the equation of the EOTF described above. */ public final double a; /** Variable \(b\) in the equation of the EOTF described above. */ @@ -2197,6 +2313,10 @@ public abstract class ColorSpace { /** Variable \(g\) in the equation of the EOTF described above. */ public final double g; + private static boolean isSpecialG(double g) { + return g == TYPE_PQish || g == TYPE_HLGish; + } + /** * <p>Defines the parameters for the ICC parametric curve type 3, as * defined in ICC.1:2004-10, section 10.15.</p> @@ -2238,44 +2358,45 @@ public abstract class ColorSpace { */ public TransferParameters(double a, double b, double c, double d, double e, double f, double g) { - - if (Double.isNaN(a) || Double.isNaN(b) || Double.isNaN(c) || - Double.isNaN(d) || Double.isNaN(e) || Double.isNaN(f) || - Double.isNaN(g)) { + if (Double.isNaN(a) || Double.isNaN(b) || Double.isNaN(c) + || Double.isNaN(d) || Double.isNaN(e) || Double.isNaN(f) + || Double.isNaN(g)) { throw new IllegalArgumentException("Parameters cannot be NaN"); } + if (!isSpecialG(g)) { + // Next representable float after 1.0 + // We use doubles here but the representation inside our native code + // is often floats + if (!(d >= 0.0 && d <= 1.0f + Math.ulp(1.0f))) { + throw new IllegalArgumentException( + "Parameter d must be in the range [0..1], " + "was " + d); + } - // Next representable float after 1.0 - // We use doubles here but the representation inside our native code is often floats - if (!(d >= 0.0 && d <= 1.0f + Math.ulp(1.0f))) { - throw new IllegalArgumentException("Parameter d must be in the range [0..1], " + - "was " + d); - } - - if (d == 0.0 && (a == 0.0 || g == 0.0)) { - throw new IllegalArgumentException( - "Parameter a or g is zero, the transfer function is constant"); - } + if (d == 0.0 && (a == 0.0 || g == 0.0)) { + throw new IllegalArgumentException( + "Parameter a or g is zero, the transfer function is constant"); + } - if (d >= 1.0 && c == 0.0) { - throw new IllegalArgumentException( - "Parameter c is zero, the transfer function is constant"); - } + if (d >= 1.0 && c == 0.0) { + throw new IllegalArgumentException( + "Parameter c is zero, the transfer function is constant"); + } - if ((a == 0.0 || g == 0.0) && c == 0.0) { - throw new IllegalArgumentException("Parameter a or g is zero," + - " and c is zero, the transfer function is constant"); - } + if ((a == 0.0 || g == 0.0) && c == 0.0) { + throw new IllegalArgumentException("Parameter a or g is zero," + + " and c is zero, the transfer function is constant"); + } - if (c < 0.0) { - throw new IllegalArgumentException("The transfer function must be increasing"); - } + if (c < 0.0) { + throw new IllegalArgumentException( + "The transfer function must be increasing"); + } - if (a < 0.0 || g < 0.0) { - throw new IllegalArgumentException("The transfer function must be " + - "positive or increasing"); + if (a < 0.0 || g < 0.0) { + throw new IllegalArgumentException( + "The transfer function must be positive or increasing"); + } } - this.a = a; this.b = b; this.c = c; @@ -2322,6 +2443,17 @@ public abstract class ColorSpace { result = 31 * result + (int) (temp ^ (temp >>> 32)); return result; } + + /** + * @hide + */ + private boolean isHLGish() { + return g == TYPE_HLGish; + } + + private boolean isPQish() { + return g == TYPE_PQish; + } } @NonNull private final float[] mWhitePoint; @@ -2357,6 +2489,34 @@ public abstract class ColorSpace { private static native long nativeCreate(float a, float b, float c, float d, float e, float f, float g, float[] xyz); + private static DoubleUnaryOperator generateOETF(TransferParameters function) { + if (function.isHLGish()) { + return x -> transferHLGOETF(function, x); + } else if (function.isPQish()) { + return x -> transferST2048OETF(function, x); + } else { + return function.e == 0.0 && function.f == 0.0 + ? x -> rcpResponse(x, function.a, function.b, + function.c, function.d, function.g) + : x -> rcpResponse(x, function.a, function.b, function.c, + function.d, function.e, function.f, function.g); + } + } + + private static DoubleUnaryOperator generateEOTF(TransferParameters function) { + if (function.isHLGish()) { + return x -> transferHLGEOTF(function, x); + } else if (function.isPQish()) { + return x -> transferST2048OETF(function, x); + } else { + return function.e == 0.0 && function.f == 0.0 + ? x -> response(x, function.a, function.b, + function.c, function.d, function.g) + : x -> response(x, function.a, function.b, function.c, + function.d, function.e, function.f, function.g); + } + } + /** * <p>Creates a new RGB color space using a 3x3 column-major transform matrix. * The transform matrix must convert from the RGB space to the profile connection @@ -2553,16 +2713,8 @@ public abstract class ColorSpace { @NonNull TransferParameters function, @IntRange(from = MIN_ID, to = MAX_ID) int id) { 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) : - x -> rcpResponse(x, function.a, function.b, function.c, - function.d, function.e, function.f, function.g), - function.e == 0.0 && function.f == 0.0 ? - x -> response(x, function.a, function.b, - function.c, function.d, function.g) : - x -> response(x, function.a, function.b, function.c, - function.d, function.e, function.f, function.g), + generateOETF(function), + generateEOTF(function), 0.0f, 1.0f, function, id); } @@ -3063,7 +3215,12 @@ public abstract class ColorSpace { */ @Nullable public TransferParameters getTransferParameters() { - return mTransferParameters; + if (mTransferParameters != null + && !mTransferParameters.equals(BT2020_PQ_TRANSFER_PARAMETERS) + && !mTransferParameters.equals(BT2020_HLG_TRANSFER_PARAMETERS)) { + return mTransferParameters; + } + return null; } @Override diff --git a/graphics/java/android/graphics/FontListParser.java b/graphics/java/android/graphics/FontListParser.java index 4bb16c6b8186..674246acafef 100644 --- a/graphics/java/android/graphics/FontListParser.java +++ b/graphics/java/android/graphics/FontListParser.java @@ -16,6 +16,8 @@ package android.graphics; +import static android.text.FontConfig.NamedFamilyList; + import android.annotation.NonNull; import android.annotation.Nullable; import android.compat.annotation.UnsupportedAppUsage; @@ -36,6 +38,7 @@ import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; @@ -46,6 +49,7 @@ import java.util.regex.Pattern; * @hide */ public class FontListParser { + private static final String TAG = "FontListParser"; // XML constants for FontFamily. private static final String ATTR_NAME = "name"; @@ -148,27 +152,60 @@ public class FontListParser { boolean allowNonExistingFile) throws XmlPullParserException, IOException { List<FontConfig.FontFamily> families = new ArrayList<>(); + List<FontConfig.NamedFamilyList> resultNamedFamilies = new ArrayList<>(); List<FontConfig.Alias> aliases = new ArrayList<>(customization.getAdditionalAliases()); - Map<String, FontConfig.FontFamily> oemNamedFamilies = + Map<String, NamedFamilyList> oemNamedFamilies = customization.getAdditionalNamedFamilies(); + boolean firstFamily = true; parser.require(XmlPullParser.START_TAG, null, "familyset"); while (keepReading(parser)) { if (parser.getEventType() != XmlPullParser.START_TAG) continue; String tag = parser.getName(); if (tag.equals("family")) { - FontConfig.FontFamily family = readFamily(parser, fontDir, updatableFontMap, - allowNonExistingFile); - if (family == null) { + final String name = parser.getAttributeValue(null, "name"); + if (name == null) { + FontConfig.FontFamily family = readFamily(parser, fontDir, updatableFontMap, + allowNonExistingFile); + if (family == null) { + continue; + } + families.add(family); + + } else { + FontConfig.NamedFamilyList namedFamilyList = readNamedFamily( + parser, fontDir, updatableFontMap, allowNonExistingFile); + if (namedFamilyList == null) { + continue; + } + if (!oemNamedFamilies.containsKey(name)) { + // The OEM customization overrides system named family. Skip if OEM + // customization XML defines the same named family. + resultNamedFamilies.add(namedFamilyList); + } + if (firstFamily) { + // The first font family is used as a fallback family as well. + families.addAll(namedFamilyList.getFamilies()); + } + } + firstFamily = false; + } else if (tag.equals("family-list")) { + FontConfig.NamedFamilyList namedFamilyList = readNamedFamilyList( + parser, fontDir, updatableFontMap, allowNonExistingFile); + if (namedFamilyList == null) { continue; } - String name = family.getName(); - if (name == null || !oemNamedFamilies.containsKey(name)) { + if (!oemNamedFamilies.containsKey(namedFamilyList.getName())) { // The OEM customization overrides system named family. Skip if OEM // customization XML defines the same named family. - families.add(family); + resultNamedFamilies.add(namedFamilyList); } + if (firstFamily) { + // The first font family is used as a fallback family as well. + families.addAll(namedFamilyList.getFamilies()); + } + firstFamily = false; } else if (tag.equals("alias")) { aliases.add(readAlias(parser)); } else { @@ -176,12 +213,12 @@ public class FontListParser { } } - families.addAll(oemNamedFamilies.values()); + resultNamedFamilies.addAll(oemNamedFamilies.values()); // Filters aliases that point to non-existing families. Set<String> namedFamilies = new ArraySet<>(); - for (int i = 0; i < families.size(); ++i) { - String name = families.get(i).getName(); + for (int i = 0; i < resultNamedFamilies.size(); ++i) { + String name = resultNamedFamilies.get(i).getName(); if (name != null) { namedFamilies.add(name); } @@ -194,7 +231,8 @@ public class FontListParser { } } - return new FontConfig(families, filtered, lastModifiedDate, configVersion); + return new FontConfig(families, filtered, resultNamedFamilies, lastModifiedDate, + configVersion); } private static boolean keepReading(XmlPullParser parser) @@ -215,7 +253,6 @@ public class FontListParser { public static @Nullable FontConfig.FontFamily readFamily(XmlPullParser parser, String fontDir, @Nullable Map<String, File> updatableFontMap, boolean allowNonExistingFile) throws XmlPullParserException, IOException { - final String name = parser.getAttributeValue(null, "name"); final String lang = parser.getAttributeValue("", "lang"); final String variant = parser.getAttributeValue(null, "variant"); final String ignore = parser.getAttributeValue(null, "ignore"); @@ -246,7 +283,68 @@ public class FontListParser { if (skip || fonts.isEmpty()) { return null; } - return new FontConfig.FontFamily(fonts, name, LocaleList.forLanguageTags(lang), intVariant); + return new FontConfig.FontFamily(fonts, LocaleList.forLanguageTags(lang), intVariant); + } + + private static void throwIfAttributeExists(String attrName, XmlPullParser parser) { + if (parser.getAttributeValue(null, attrName) != null) { + throw new IllegalArgumentException(attrName + " cannot be used in FontFamily inside " + + " family or family-list with name attribute."); + } + } + + /** + * Read a font family with name attribute as a single element family-list element. + */ + public static @Nullable FontConfig.NamedFamilyList readNamedFamily( + @NonNull XmlPullParser parser, @NonNull String fontDir, + @Nullable Map<String, File> updatableFontMap, boolean allowNonExistingFile) + throws XmlPullParserException, IOException { + final String name = parser.getAttributeValue(null, "name"); + throwIfAttributeExists("lang", parser); + throwIfAttributeExists("variant", parser); + throwIfAttributeExists("ignore", parser); + + final FontConfig.FontFamily family = readFamily(parser, fontDir, updatableFontMap, + allowNonExistingFile); + if (family == null) { + return null; + } + return new NamedFamilyList(Collections.singletonList(family), name); + } + + /** + * Read a family-list element + */ + public static @Nullable FontConfig.NamedFamilyList readNamedFamilyList( + @NonNull XmlPullParser parser, @NonNull String fontDir, + @Nullable Map<String, File> updatableFontMap, boolean allowNonExistingFile) + throws XmlPullParserException, IOException { + final String name = parser.getAttributeValue(null, "name"); + final List<FontConfig.FontFamily> familyList = new ArrayList<>(); + while (keepReading(parser)) { + if (parser.getEventType() != XmlPullParser.START_TAG) continue; + final String tag = parser.getName(); + if (tag.equals("family")) { + throwIfAttributeExists("name", parser); + throwIfAttributeExists("lang", parser); + throwIfAttributeExists("variant", parser); + throwIfAttributeExists("ignore", parser); + + final FontConfig.FontFamily family = readFamily(parser, fontDir, updatableFontMap, + allowNonExistingFile); + if (family != null) { + familyList.add(family); + } + } else { + skip(parser); + } + } + + if (familyList.isEmpty()) { + return null; + } + return new FontConfig.NamedFamilyList(familyList, name); } /** Matches leading and trailing XML whitespace. */ diff --git a/graphics/java/android/graphics/Gainmap.java b/graphics/java/android/graphics/Gainmap.java new file mode 100644 index 000000000000..f639521ff250 --- /dev/null +++ b/graphics/java/android/graphics/Gainmap.java @@ -0,0 +1,364 @@ +/* + * Copyright (C) 2023 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.FloatRange; +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; + +import libcore.util.NativeAllocationRegistry; + +/** + * Gainmap represents a mechanism for augmenting an SDR image to produce an HDR one with variable + * display adjustment capability. It is a combination of a set of metadata describing how to apply + * the gainmap, as well as either a 1 (such as {@link android.graphics.Bitmap.Config#ALPHA_8} or 3 + * (such as {@link android.graphics.Bitmap.Config#ARGB_8888} with the alpha channel ignored) + * channel Bitmap that represents the gainmap data itself. + * <p> + * When rendering to an {@link android.content.pm.ActivityInfo#COLOR_MODE_HDR} activity, the + * hardware accelerated {@link Canvas} will automatically apply the gainmap when sufficient + * HDR headroom is available. + * + * <h3>Gainmap Structure</h3> + * + * The logical whole of a gainmap'd image consists of a base Bitmap that represents the original + * image as would be displayed without gainmap support in addition to a gainmap with a second + * enhancement image. In the case of a JPEG, the base image would be the typical 8-bit SDR image + * that the format is commonly associated with. The gainmap image is embedded alongside the base + * image, often at a lower resolution (such as 1/4th), along with some metadata to describe + * how to apply the gainmap. The gainmap image itself is then a greyscale image representing + * the transformation to apply onto the base image to reconstruct an HDR rendition of it. + * <p> + * As such these "gainmap images" consist of 3 parts - a base {@link Bitmap} with a + * {@link Bitmap#getGainmap()} that returns an instance of this class which in turn contains + * the enhancement layer represented as another Bitmap, accessible via {@link #getGainmapContents()} + * + * <h3>Applying a gainmap manually</h3> + * + * When doing custom rendering such as to an OpenGL ES or Vulkan context, the gainmap is not + * automatically applied. In such situations, the following steps are appropriate to render the + * gainmap in combination with the base image. + * <p> + * Suppose our display has HDR to SDR ratio of H, and we wish to display an image with gainmap on + * this display. Let B be the pixel value from the base image in a color space that has the + * primaries of the base image and a linear transfer function. Let G be the pixel value from the + * gainmap. Let D be the output pixel in the same color space as B. The value of D is computed + * as follows: + * <p> + * First, let W be a weight parameter determining how much the gainmap will be applied. + * <pre class="prettyprint"> + * W = clamp((log(H) - log(minDisplayRatioForHdrTransition)) / + * (log(displayRatioForFullHdr) - log(minDisplayRatioForHdrTransition), 0, 1)</pre> + * + * Next, let L be the gainmap value in log space. We compute this from the value G that was + * sampled from the texture as follows: + * <pre class="prettyprint"> + * L = mix(log(ratioMin), log(ratioMax), pow(G, gamma))</pre> + * Finally, apply the gainmap to compute D, the displayed pixel. If the base image is SDR then + * compute: + * <pre class="prettyprint"> + * D = (B + epsilonSdr) * exp(L * W) - epsilonHdr</pre> + * <p> + * In the above math, log() is a natural logarithm and exp() is natural exponentiation. The base + * for these functions cancels out and does not affect the result, so other bases may be used + * if preferred. + */ +public final class Gainmap implements Parcelable { + + // Use a Holder to allow static initialization of Gainmap in the boot image. + private static class NoImagePreloadHolder { + public static final NativeAllocationRegistry sRegistry = + NativeAllocationRegistry.createMalloced( + Gainmap.class.getClassLoader(), nGetFinalizer()); + } + + final long mNativePtr; + private Bitmap mGainmapContents; + + // called from JNI + private Gainmap(Bitmap gainmapContents, long nativeGainmap) { + if (nativeGainmap == 0) { + throw new RuntimeException("internal error: native gainmap is 0"); + } + + mNativePtr = nativeGainmap; + setGainmapContents(gainmapContents); + + NoImagePreloadHolder.sRegistry.registerNativeAllocation(this, nativeGainmap); + } + + /** + * Creates a gainmap from a given Bitmap. The caller is responsible for setting the various + * fields to the desired values. The defaults are as follows: + * <ul> + * <li>Ratio min is 1f, 1f, 1f</li> + * <li>Ratio max is 2f, 2f, 2f</li> + * <li>Gamma is 1f, 1f, 1f</li> + * <li>Epsilon SDR is 0f, 0f, 0f</li> + * <li>Epsilon HDR is 0f, 0f, 0f</li> + * <li>Display ratio SDR is 1f</li> + * <li>Display ratio HDR is 2f</li> + * </ul> + * It is strongly recommended that at least the ratio max and display ratio HDR are adjusted + * to better suit the given gainmap contents. + */ + public Gainmap(@NonNull Bitmap gainmapContents) { + this(gainmapContents, nCreateEmpty()); + } + + /** + * Creates a new gainmap using the provided gainmap as the metadata source and the provided + * bitmap as the replacement for the gainmapContents + * TODO: Make public, it's useful + * @hide + */ + public Gainmap(@NonNull Gainmap gainmap, @NonNull Bitmap gainmapContents) { + this(gainmapContents, nCreateCopy(gainmap.mNativePtr)); + } + + /** + * @return Returns the image data of the gainmap represented as a Bitmap. This is represented + * as a Bitmap for broad API compatibility, however certain aspects of the Bitmap are ignored + * such as {@link Bitmap#getColorSpace()} or {@link Bitmap#getGainmap()} as they are not + * relevant to the gainmap's enhancement layer. + */ + @NonNull + public Bitmap getGainmapContents() { + return mGainmapContents; + } + + /** + * Sets the image data of the gainmap. This is the 1 or 3 channel enhancement layer to apply + * to the base image. This is represented as a Bitmap for broad API compatibility, however + * certain aspects of the Bitmap are ignored such as {@link Bitmap#getColorSpace()} or + * {@link Bitmap#getGainmap()} as they are not relevant to the gainmap's enhancement layer. + * + * @param bitmap The non-null bitmap to set as the gainmap's contents + */ + public void setGainmapContents(@NonNull Bitmap bitmap) { + // TODO: Validate here or leave native-side? + if (bitmap.isRecycled()) throw new IllegalArgumentException("Bitmap is recycled"); + nSetBitmap(mNativePtr, bitmap); + mGainmapContents = bitmap; + } + + /** + * Sets the gainmap ratio min. For single-plane gainmaps, r, g, and b should be the same. + */ + public void setRatioMin(float r, float g, float b) { + nSetRatioMin(mNativePtr, r, g, b); + } + + /** + * Gets the gainmap ratio max. For single-plane gainmaps, all 3 components should be the + * same. The components are in r, g, b order. + */ + @NonNull + public float[] getRatioMin() { + float[] ret = new float[3]; + nGetRatioMin(mNativePtr, ret); + return ret; + } + + /** + * Sets the gainmap ratio max. For single-plane gainmaps, r, g, and b should be the same. + */ + public void setRatioMax(float r, float g, float b) { + nSetRatioMax(mNativePtr, r, g, b); + } + + /** + * Gets the gainmap ratio max. For single-plane gainmaps, all 3 components should be the + * same. The components are in r, g, b order. + */ + @NonNull + public float[] getRatioMax() { + float[] ret = new float[3]; + nGetRatioMax(mNativePtr, ret); + return ret; + } + + /** + * Sets the gainmap gamma. For single-plane gainmaps, r, g, and b should be the same. + */ + public void setGamma(float r, float g, float b) { + nSetGamma(mNativePtr, r, g, b); + } + + /** + * Gets the gainmap gamma. For single-plane gainmaps, all 3 components should be the + * same. The components are in r, g, b order. + */ + @NonNull + public float[] getGamma() { + float[] ret = new float[3]; + nGetGamma(mNativePtr, ret); + return ret; + } + + /** + * Sets the sdr epsilon which is used to avoid numerical instability. + * For single-plane gainmaps, r, g, and b should be the same. + */ + public void setEpsilonSdr(float r, float g, float b) { + nSetEpsilonSdr(mNativePtr, r, g, b); + } + + /** + * Gets the sdr epsilon. For single-plane gainmaps, all 3 components should be the + * same. The components are in r, g, b order. + */ + @NonNull + public float[] getEpsilonSdr() { + float[] ret = new float[3]; + nGetEpsilonSdr(mNativePtr, ret); + return ret; + } + + /** + * Sets the hdr epsilon which is used to avoid numerical instability. + * For single-plane gainmaps, r, g, and b should be the same. + */ + public void setEpsilonHdr(float r, float g, float b) { + nSetEpsilonHdr(mNativePtr, r, g, b); + } + + /** + * Gets the hdr epsilon. For single-plane gainmaps, all 3 components should be the + * same. The components are in r, g, b order. + */ + @NonNull + public float[] getEpsilonHdr() { + float[] ret = new float[3]; + nGetEpsilonHdr(mNativePtr, ret); + return ret; + } + + /** + * Sets the hdr/sdr ratio at which point the gainmap is fully applied. + * @param max The hdr/sdr ratio at which the gainmap is fully applied. Must be >= 1.0f + */ + public void setDisplayRatioForFullHdr(@FloatRange(from = 1.0f) float max) { + if (!Float.isFinite(max) || max < 1f) { + throw new IllegalArgumentException( + "setDisplayRatioForFullHdr must be >= 1.0f, got = " + max); + } + nSetDisplayRatioHdr(mNativePtr, max); + } + + /** + * Gets the hdr/sdr ratio at which point the gainmap is fully applied. + */ + @NonNull + public float getDisplayRatioForFullHdr() { + return nGetDisplayRatioHdr(mNativePtr); + } + + /** + * Sets the hdr/sdr ratio below which only the SDR image is displayed. + * @param min The minimum hdr/sdr ratio at which to begin applying the gainmap. Must be >= 1.0f + */ + public void setMinDisplayRatioForHdrTransition(@FloatRange(from = 1.0f) float min) { + if (!Float.isFinite(min) || min < 1f) { + throw new IllegalArgumentException( + "setMinDisplayRatioForHdrTransition must be >= 1.0f, got = " + min); + } + nSetDisplayRatioSdr(mNativePtr, min); + } + + /** + * Gets the hdr/sdr ratio below which only the SDR image is displayed. + */ + @NonNull + public float getMinDisplayRatioForHdrTransition() { + return nGetDisplayRatioSdr(mNativePtr); + } + + /** + * No special parcel contents. + */ + @Override + public int describeContents() { + return 0; + } + + /** + * Write the gainmap to the parcel. + * + * @param dest Parcel object to write the gainmap data into + * @param flags Additional flags about how the object should be written. + */ + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + if (mNativePtr == 0) { + throw new IllegalStateException("Cannot be written to a parcel"); + } + dest.writeTypedObject(mGainmapContents, flags); + // write gainmapinfo into parcel + nWriteGainmapToParcel(mNativePtr, dest); + } + + public static final @NonNull Parcelable.Creator<Gainmap> CREATOR = + new Parcelable.Creator<Gainmap>() { + /** + * Rebuilds a gainmap previously stored with writeToParcel(). + * + * @param in Parcel object to read the gainmap from + * @return a new gainmap created from the data in the parcel + */ + public Gainmap createFromParcel(Parcel in) { + Gainmap gm = new Gainmap(in.readTypedObject(Bitmap.CREATOR)); + // read gainmapinfo from parcel + nReadGainmapFromParcel(gm.mNativePtr, in); + return gm; + } + + public Gainmap[] newArray(int size) { + return new Gainmap[size]; + } + }; + + private static native long nGetFinalizer(); + private static native long nCreateEmpty(); + private static native long nCreateCopy(long source); + + private static native void nSetBitmap(long ptr, Bitmap bitmap); + + private static native void nSetRatioMin(long ptr, float r, float g, float b); + private static native void nGetRatioMin(long ptr, float[] components); + + private static native void nSetRatioMax(long ptr, float r, float g, float b); + private static native void nGetRatioMax(long ptr, float[] components); + + private static native void nSetGamma(long ptr, float r, float g, float b); + private static native void nGetGamma(long ptr, float[] components); + + private static native void nSetEpsilonSdr(long ptr, float r, float g, float b); + private static native void nGetEpsilonSdr(long ptr, float[] components); + + private static native void nSetEpsilonHdr(long ptr, float r, float g, float b); + private static native void nGetEpsilonHdr(long ptr, float[] components); + + private static native void nSetDisplayRatioHdr(long ptr, float max); + private static native float nGetDisplayRatioHdr(long ptr); + + private static native void nSetDisplayRatioSdr(long ptr, float min); + private static native float nGetDisplayRatioSdr(long ptr); + private static native void nWriteGainmapToParcel(long ptr, Parcel dest); + private static native void nReadGainmapFromParcel(long ptr, Parcel src); +} diff --git a/graphics/java/android/graphics/GraphicBuffer.java b/graphics/java/android/graphics/GraphicBuffer.java index f9113a21405c..6705b25ab0ec 100644 --- a/graphics/java/android/graphics/GraphicBuffer.java +++ b/graphics/java/android/graphics/GraphicBuffer.java @@ -57,7 +57,7 @@ public class GraphicBuffer implements Parcelable { private final int mUsage; // Note: do not rename, this field is used by native code @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - private final long mNativeObject; + private long mNativeObject; // These two fields are only used by lock/unlockCanvas() private Canvas mCanvas; @@ -219,6 +219,7 @@ public class GraphicBuffer implements Parcelable { if (!mDestroyed) { mDestroyed = true; nDestroyGraphicBuffer(mNativeObject); + mNativeObject = 0; } } @@ -239,7 +240,7 @@ public class GraphicBuffer implements Parcelable { @Override protected void finalize() throws Throwable { try { - if (!mDestroyed) nDestroyGraphicBuffer(mNativeObject); + destroy(); } finally { super.finalize(); } diff --git a/graphics/java/android/graphics/HardwareBufferRenderer.java b/graphics/java/android/graphics/HardwareBufferRenderer.java new file mode 100644 index 000000000000..e04f13c9b922 --- /dev/null +++ b/graphics/java/android/graphics/HardwareBufferRenderer.java @@ -0,0 +1,401 @@ +/* + * Copyright (C) 2022 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.FloatRange; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.graphics.ColorSpace.Named; +import android.hardware.HardwareBuffer; +import android.hardware.SyncFence; +import android.view.SurfaceControl; + +import libcore.util.NativeAllocationRegistry; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.concurrent.Executor; +import java.util.function.Consumer; + +/** + * <p>Creates an instance of a hardware-accelerated renderer. This is used to render a scene built + * from {@link RenderNode}s to an output {@link HardwareBuffer}. There can be as many + * HardwareBufferRenderer instances as desired.</p> + * + * <h3>Resources & lifecycle</h3> + * + * <p>All HardwareBufferRenderer and {@link HardwareRenderer} instances share a common render + * thread. Therefore HardwareBufferRenderer will share common resources and GPU utilization with + * hardware accelerated rendering initiated by the UI thread of an application. + * The render thread contains the GPU context & resources necessary to do GPU-accelerated + * rendering. As such, the first HardwareBufferRenderer created comes with the cost of also creating + * the associated GPU contexts, however each incremental HardwareBufferRenderer thereafter is fairly + * cheap. The expected usage is to have a HardwareBufferRenderer instance for every active {@link + * HardwareBuffer}.</p> + * + * This is useful in situations where a scene built with {@link RenderNode}s can be consumed + * directly by the system compositor through + * {@link SurfaceControl.Transaction#setBuffer(SurfaceControl, HardwareBuffer)}. + * + * HardwareBufferRenderer will never clear contents before each draw invocation so previous contents + * in the {@link HardwareBuffer} target will be preserved across renders. + */ +public class HardwareBufferRenderer implements AutoCloseable { + + private static final ColorSpace DEFAULT_COLORSPACE = ColorSpace.get(Named.SRGB); + + private static class HardwareBufferRendererHolder { + public static final NativeAllocationRegistry REGISTRY = + NativeAllocationRegistry.createMalloced( + HardwareBufferRenderer.class.getClassLoader(), nGetFinalizer()); + } + + private final HardwareBuffer mHardwareBuffer; + private final RenderRequest mRenderRequest; + private final RenderNode mRootNode; + private final Runnable mCleaner; + + private long mProxy; + + /** + * Creates a new instance of {@link HardwareBufferRenderer} with the provided {@link + * HardwareBuffer} as the output of the rendered scene. + */ + public HardwareBufferRenderer(@NonNull HardwareBuffer buffer) { + RenderNode rootNode = RenderNode.adopt(nCreateRootRenderNode()); + rootNode.setClipToBounds(false); + mProxy = nCreateHardwareBufferRenderer(buffer, rootNode.mNativeRenderNode); + mCleaner = HardwareBufferRendererHolder.REGISTRY.registerNativeAllocation(this, mProxy); + mRenderRequest = new RenderRequest(); + mRootNode = rootNode; + mHardwareBuffer = buffer; + } + + /** + * Sets the content root to render. It is not necessary to call this whenever the content + * recording changes. Any mutations to the RenderNode content, or any of the RenderNodes + * contained within the content node, will be applied whenever a new {@link RenderRequest} is + * issued via {@link #obtainRenderRequest()} and {@link RenderRequest#draw(Executor, + * Consumer)}. + * + * @param content The content to set as the root RenderNode. If null the content root is removed + * and the renderer will draw nothing. + */ + public void setContentRoot(@Nullable RenderNode content) { + RecordingCanvas canvas = mRootNode.beginRecording(); + if (content != null) { + canvas.drawRenderNode(content); + } + mRootNode.endRecording(); + } + + /** + * Returns a {@link RenderRequest} that can be used to render into the provided {@link + * HardwareBuffer}. This is used to synchronize the RenderNode content provided by {@link + * #setContentRoot(RenderNode)}. + * + * @return An instance of {@link RenderRequest}. The instance may be reused for every frame, so + * the caller should not hold onto it for longer than a single render request. + */ + @NonNull + public RenderRequest obtainRenderRequest() { + mRenderRequest.reset(); + return mRenderRequest; + } + + /** + * Returns if the {@link HardwareBufferRenderer} has already been closed. That is + * {@link HardwareBufferRenderer#close()} has been invoked. + * @return True if the {@link HardwareBufferRenderer} has been closed, false otherwise. + */ + public boolean isClosed() { + return mProxy == 0L; + } + + /** + * Releases the resources associated with this {@link HardwareBufferRenderer} instance. **Note** + * this does not call {@link HardwareBuffer#close()} on the provided {@link HardwareBuffer} + * instance + */ + @Override + public void close() { + // Note we explicitly call this only here to clean-up potential animator state + // This is not done as part of the NativeAllocationRegistry as it would invoke animator + // callbacks on the wrong thread + nDestroyRootRenderNode(mRootNode.mNativeRenderNode); + if (mProxy != 0L) { + mCleaner.run(); + mProxy = 0L; + } + } + + /** + * Sets the center of the light source. The light source point controls the directionality and + * shape of shadows rendered by RenderNode Z & elevation. + * + * <p>The light source should be setup both as part of initial configuration, and whenever + * the window moves to ensure the light source stays anchored in display space instead of in + * window space. + * + * <p>This must be set at least once along with {@link #setLightSourceAlpha(float, float)} + * before shadows will work. + * + * @param lightX The X position of the light source. If unsure, a reasonable default + * is 'displayWidth / 2f - windowLeft'. + * @param lightY The Y position of the light source. If unsure, a reasonable default + * is '0 - windowTop' + * @param lightZ The Z position of the light source. Must be >= 0. If unsure, a reasonable + * default is 600dp. + * @param lightRadius The radius of the light source. Smaller radius will have sharper edges, + * larger radius will have softer shadows. If unsure, a reasonable default is 800 dp. + */ + public void setLightSourceGeometry( + float lightX, + float lightY, + @FloatRange(from = 0f) float lightZ, + @FloatRange(from = 0f) float lightRadius + ) { + validateFinite(lightX, "lightX"); + validateFinite(lightY, "lightY"); + validatePositive(lightZ, "lightZ"); + validatePositive(lightRadius, "lightRadius"); + nSetLightGeometry(mProxy, lightX, lightY, lightZ, lightRadius); + } + + /** + * Configures the ambient & spot shadow alphas. This is the alpha used when the shadow has max + * alpha, and ramps down from the values provided to zero. + * + * <p>These values are typically provided by the current theme, see + * {@link android.R.attr#spotShadowAlpha} and {@link android.R.attr#ambientShadowAlpha}. + * + * <p>This must be set at least once along with + * {@link #setLightSourceGeometry(float, float, float, float)} before shadows will work. + * + * @param ambientShadowAlpha The alpha for the ambient shadow. If unsure, a reasonable default + * is 0.039f. + * @param spotShadowAlpha The alpha for the spot shadow. If unsure, a reasonable default is + * 0.19f. + */ + public void setLightSourceAlpha(@FloatRange(from = 0.0f, to = 1.0f) float ambientShadowAlpha, + @FloatRange(from = 0.0f, to = 1.0f) float spotShadowAlpha) { + validateAlpha(ambientShadowAlpha, "ambientShadowAlpha"); + validateAlpha(spotShadowAlpha, "spotShadowAlpha"); + nSetLightAlpha(mProxy, ambientShadowAlpha, spotShadowAlpha); + } + + /** + * Class that contains data regarding the result of the render request. + * Consumers are to wait on the provided {@link SyncFence} before consuming the HardwareBuffer + * provided to {@link HardwareBufferRenderer} as well as verify that the status returned by + * {@link RenderResult#getStatus()} returns {@link RenderResult#SUCCESS}. + */ + public static final class RenderResult { + + /** + * Render request was completed successfully + */ + public static final int SUCCESS = 0; + + /** + * Render request failed with an unknown error + */ + public static final int ERROR_UNKNOWN = 1; + + /** @hide **/ + @IntDef(value = {SUCCESS, ERROR_UNKNOWN}) + @Retention(RetentionPolicy.SOURCE) + public @interface RenderResultStatus{} + + private final SyncFence mFence; + private final int mResultStatus; + + private RenderResult(@NonNull SyncFence fence, @RenderResultStatus int resultStatus) { + mFence = fence; + mResultStatus = resultStatus; + } + + @NonNull + public SyncFence getFence() { + return mFence; + } + + @RenderResultStatus + public int getStatus() { + return mResultStatus; + } + } + + /** + * Sets the parameters that can be used to control a render request for a {@link + * HardwareBufferRenderer}. This is not thread-safe and must not be held on to for longer than a + * single request. + */ + public final class RenderRequest { + + private ColorSpace mColorSpace = DEFAULT_COLORSPACE; + private int mTransform = SurfaceControl.BUFFER_TRANSFORM_IDENTITY; + + private RenderRequest() { } + + /** + * Syncs the RenderNode tree to the render thread and requests content to be drawn. This + * {@link RenderRequest} instance should no longer be used after calling this method. The + * system internally may reuse instances of {@link RenderRequest} to reduce allocation + * churn. + * + * @param executor Executor used to deliver callbacks + * @param renderCallback Callback invoked when rendering is complete. This includes a + * {@link RenderResult} that provides a {@link SyncFence} that should be waited upon for + * completion before consuming the rendered output in the provided {@link HardwareBuffer} + * instance. + * + * @throws IllegalStateException if attempt to draw is made when + * {@link HardwareBufferRenderer#isClosed()} returns true + */ + public void draw( + @NonNull Executor executor, + @NonNull Consumer<RenderResult> renderCallback + ) { + Consumer<RenderResult> wrapped = consumable -> executor.execute( + () -> renderCallback.accept(consumable)); + if (!isClosed()) { + int renderWidth; + int renderHeight; + if (mTransform == SurfaceControl.BUFFER_TRANSFORM_ROTATE_90 + || mTransform == SurfaceControl.BUFFER_TRANSFORM_ROTATE_270) { + renderWidth = mHardwareBuffer.getHeight(); + renderHeight = mHardwareBuffer.getWidth(); + } else { + renderWidth = mHardwareBuffer.getWidth(); + renderHeight = mHardwareBuffer.getHeight(); + } + + nRender( + mProxy, + mTransform, + renderWidth, + renderHeight, + mColorSpace.getNativeInstance(), + wrapped); + } else { + throw new IllegalStateException("Attempt to draw with a HardwareBufferRenderer " + + "instance that has already been closed"); + } + } + + private void reset() { + mColorSpace = DEFAULT_COLORSPACE; + mTransform = SurfaceControl.BUFFER_TRANSFORM_IDENTITY; + } + + /** + * Configures the color space which the content should be rendered in. This affects + * how the framework will interpret the color at each pixel. The color space provided here + * must be non-null, RGB based and leverage an ICC parametric curve. The min/max values + * of the components should not reduce the numerical range compared to the previously + * assigned color space. If left unspecified, the default color space of SRGB will be used. + * + * @param colorSpace The color space the content should be rendered in. If null is provided + * the default of SRGB will be used. + */ + @NonNull + public RenderRequest setColorSpace(@Nullable ColorSpace colorSpace) { + if (colorSpace == null) { + mColorSpace = DEFAULT_COLORSPACE; + } else { + mColorSpace = colorSpace; + } + return this; + } + + /** + * Specifies a transform to be applied before content is rendered. This is useful + * for pre-rotating content for the current display orientation to increase performance + * of displaying the associated buffer. This transformation will also adjust the light + * source position for the specified rotation. + * @see SurfaceControl.Transaction#setBufferTransform(SurfaceControl, int) + */ + @NonNull + public RenderRequest setBufferTransform( + @SurfaceControl.BufferTransform int bufferTransform) { + boolean validTransform = bufferTransform == SurfaceControl.BUFFER_TRANSFORM_IDENTITY + || bufferTransform == SurfaceControl.BUFFER_TRANSFORM_ROTATE_90 + || bufferTransform == SurfaceControl.BUFFER_TRANSFORM_ROTATE_180 + || bufferTransform == SurfaceControl.BUFFER_TRANSFORM_ROTATE_270; + if (validTransform) { + mTransform = bufferTransform; + } else { + throw new IllegalArgumentException("Invalid transform provided, must be one of" + + "the SurfaceControl.BufferTransform values"); + } + return this; + } + } + + /** + * @hide + */ + /* package */ + static native int nRender(long renderer, int transform, int width, int height, long colorSpace, + Consumer<RenderResult> callback); + + private static native long nCreateRootRenderNode(); + + private static native void nDestroyRootRenderNode(long rootRenderNode); + + private static native long nCreateHardwareBufferRenderer(HardwareBuffer buffer, + long rootRenderNode); + + private static native void nSetLightGeometry(long bufferRenderer, float lightX, float lightY, + float lightZ, float radius); + + private static native void nSetLightAlpha(long nativeProxy, float ambientShadowAlpha, + float spotShadowAlpha); + + private static native long nGetFinalizer(); + + // Called by native + private static void invokeRenderCallback( + @NonNull Consumer<RenderResult> callback, + int fd, + int status + ) { + callback.accept(new RenderResult(SyncFence.adopt(fd), status)); + } + + private static void validateAlpha(float alpha, String argumentName) { + if (!(alpha >= 0.0f && alpha <= 1.0f)) { + throw new IllegalArgumentException(argumentName + " must be a valid alpha, " + + alpha + " is not in the range of 0.0f to 1.0f"); + } + } + + private static void validateFinite(float f, String argumentName) { + if (!Float.isFinite(f)) { + throw new IllegalArgumentException(argumentName + " must be finite, given=" + f); + } + } + + private static void validatePositive(float f, String argumentName) { + if (!(Float.isFinite(f) && f >= 0.0f)) { + throw new IllegalArgumentException(argumentName + + " must be a finite positive, given=" + f); + } + } +} diff --git a/graphics/java/android/graphics/HardwareRenderer.java b/graphics/java/android/graphics/HardwareRenderer.java index 7cc22d753f69..9cde1878d9d8 100644 --- a/graphics/java/android/graphics/HardwareRenderer.java +++ b/graphics/java/android/graphics/HardwareRenderer.java @@ -25,6 +25,7 @@ import android.app.ActivityManager; import android.content.Context; import android.content.pm.ActivityInfo; import android.content.res.Configuration; +import android.hardware.OverlayProperties; import android.hardware.display.DisplayManager; import android.os.IBinder; import android.os.ParcelFileDescriptor; @@ -47,9 +48,7 @@ import java.io.File; import java.io.FileDescriptor; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.util.Optional; import java.util.concurrent.Executor; -import java.util.stream.Stream; import sun.misc.Cleaner; @@ -145,6 +144,32 @@ public class HardwareRenderer { public @interface DumpFlags { } + + /** + * Trims all Skia caches. + * @hide + */ + public static final int CACHE_TRIM_ALL = 0; + /** + * Trims Skia font caches. + * @hide + */ + public static final int CACHE_TRIM_FONT = 1; + /** + * Trims Skia resource caches. + * @hide + */ + public static final int CACHE_TRIM_RESOURCES = 2; + + /** @hide */ + @IntDef(prefix = {"CACHE_TRIM_"}, value = { + CACHE_TRIM_ALL, + CACHE_TRIM_FONT, + CACHE_TRIM_RESOURCES + }) + @Retention(RetentionPolicy.SOURCE) + public @interface CacheTrimLevel {} + /** * Name of the file that holds the shaders cache. */ @@ -159,6 +184,7 @@ public class HardwareRenderer { private boolean mOpaque = true; private boolean mForceDark = false; private @ActivityInfo.ColorMode int mColorMode = ActivityInfo.COLOR_MODE_DEFAULT; + private float mDesiredSdrHdrRatio = 1f; /** * Creates a new instance of a HardwareRenderer. The HardwareRenderer will default @@ -320,7 +346,8 @@ public class HardwareRenderer { * @param surfaceControl The surface control to pass to render thread in hwui. * If null, any previous references held in render thread will be discarded. */ - public void setSurfaceControl(@Nullable SurfaceControl surfaceControl) { + public void setSurfaceControl(@Nullable SurfaceControl surfaceControl, + @Nullable BLASTBufferQueue blastBufferQueue) { nSetSurfaceControl(mNativeProxy, surfaceControl != null ? surfaceControl.mNativeObject : 0); } @@ -644,11 +671,12 @@ public class HardwareRenderer { * @param colorMode The @{@link ActivityInfo.ColorMode} to request * @hide */ - public void setColorMode(@ActivityInfo.ColorMode int colorMode) { + public float setColorMode(@ActivityInfo.ColorMode int colorMode) { if (mColorMode != colorMode) { mColorMode = colorMode; - nSetColorMode(mNativeProxy, colorMode); + mDesiredSdrHdrRatio = nSetColorMode(mNativeProxy, colorMode); } + return mDesiredSdrHdrRatio; } /** @@ -664,6 +692,12 @@ public class HardwareRenderer { nSetColorMode(mNativeProxy, colorMode); } + /** @hide */ + public void setTargetHdrSdrRatio(float ratio) { + if (ratio < 1.f || !Float.isFinite(ratio)) ratio = 1.f; + nSetTargetSdrHdrRatio(mNativeProxy, ratio); + } + /** * Blocks until all previously queued work has completed. * @@ -984,6 +1018,24 @@ public class HardwareRenderer { } /** + * Notifies the hardware renderer about pending choreographer callbacks. + * + * @hide + */ + public void notifyCallbackPending() { + nNotifyCallbackPending(mNativeProxy); + } + + /** + * Notifies the hardware renderer about upcoming expensive frames. + * + * @hide + */ + public void notifyExpensiveFrame() { + nNotifyExpensiveFrame(mNativeProxy); + } + + /** * b/68769804, b/66945974: For low FPS experiments. * * @hide @@ -1046,14 +1098,39 @@ public class HardwareRenderer { } /** @hide */ - public static int copySurfaceInto(Surface surface, Rect srcRect, Bitmap bitmap) { - if (srcRect == null) { - // Empty rect means entire surface - return nCopySurfaceInto(surface, 0, 0, 0, 0, bitmap.getNativeInstance()); - } else { - return nCopySurfaceInto(surface, srcRect.left, srcRect.top, - srcRect.right, srcRect.bottom, bitmap.getNativeInstance()); + public abstract static class CopyRequest { + protected Bitmap mDestinationBitmap; + final Rect mSrcRect; + + protected CopyRequest(Rect srcRect, Bitmap destinationBitmap) { + mDestinationBitmap = destinationBitmap; + if (srcRect != null) { + mSrcRect = srcRect; + } else { + mSrcRect = new Rect(); + } } + + /** + * Retrieve the bitmap in which to store the result of the copy request + */ + public long getDestinationBitmap(int srcWidth, int srcHeight) { + if (mDestinationBitmap == null) { + mDestinationBitmap = + Bitmap.createBitmap(srcWidth, srcHeight, Bitmap.Config.ARGB_8888); + } + return mDestinationBitmap.getNativeInstance(); + } + + /** Called when the copy is completed */ + public abstract void onCopyFinished(int result); + } + + /** @hide */ + public static void copySurfaceInto(Surface surface, CopyRequest copyRequest) { + final Rect srcRect = copyRequest.mSrcRect; + nCopySurfaceInto(surface, srcRect.left, srcRect.top, srcRect.right, srcRect.bottom, + copyRequest); } /** @@ -1080,6 +1157,20 @@ public class HardwareRenderer { nTrimMemory(level); } + /** + * Invoke this when all font caches should be flushed. This can cause jank on next render + * commands so use it only after expensive font allocation operations which would + * allocate large amount of temporary memory. + * + * @param level Hint about which caches to trim. See {@link #CACHE_TRIM_ALL}, + * {@link #CACHE_TRIM_FONT}, {@link #CACHE_TRIM_RESOURCES} + * + * @hide + */ + public static void trimCaches(@CacheTrimLevel int level) { + nTrimCaches(level); + } + /** @hide */ public static void overrideProperty(@NonNull String name, @NonNull String value) { if (name == null || value == null) { @@ -1117,6 +1208,16 @@ public class HardwareRenderer { } /** + * Sets whether or not the current process is a system or persistent process. Used to influence + * the chosen memory usage policy. + * + * @hide + **/ + public static void setIsSystemOrPersistent() { + nSetIsSystemOrPersistent(true); + } + + /** * Returns true if HardwareRender will produce output. * * This value is global to the process and affects all uses of HardwareRenderer, @@ -1179,30 +1280,6 @@ public class HardwareRenderer { private static class ProcessInitializer { static ProcessInitializer sInstance = new ProcessInitializer(); - // Magic values from android/data_space.h - private static final int INTERNAL_DATASPACE_SRGB = 142671872; - private static final int INTERNAL_DATASPACE_DISPLAY_P3 = 143261696; - private static final int INTERNAL_DATASPACE_SCRGB = 411107328; - - private enum Dataspace { - DISPLAY_P3(ColorSpace.Named.DISPLAY_P3, INTERNAL_DATASPACE_DISPLAY_P3), - SCRGB(ColorSpace.Named.EXTENDED_SRGB, INTERNAL_DATASPACE_SCRGB), - SRGB(ColorSpace.Named.SRGB, INTERNAL_DATASPACE_SRGB); - - private final ColorSpace.Named mColorSpace; - private final int mNativeDataspace; - Dataspace(ColorSpace.Named colorSpace, int nativeDataspace) { - this.mColorSpace = colorSpace; - this.mNativeDataspace = nativeDataspace; - } - - static Optional<Dataspace> find(ColorSpace colorSpace) { - return Stream.of(Dataspace.values()) - .filter(d -> ColorSpace.get(d.mColorSpace).equals(colorSpace)) - .findFirst(); - } - } - private boolean mInitialized = false; private boolean mDisplayInitialized = false; @@ -1271,6 +1348,7 @@ public class HardwareRenderer { initDisplayInfo(); nSetIsHighEndGfx(ActivityManager.isHighEndGfx()); + nSetIsLowRam(ActivityManager.isLowRamDeviceStatic()); // Defensively clear out the context in case we were passed a context that can leak // if we live longer than it, e.g. an activity context. mContext = null; @@ -1289,26 +1367,61 @@ public class HardwareRenderer { return; } - Display display = dm.getDisplay(Display.DEFAULT_DISPLAY); - if (display == null) { + final Display defaultDisplay = dm.getDisplay(Display.DEFAULT_DISPLAY); + if (defaultDisplay == null) { Log.d(LOG_TAG, "Failed to find default display for display-based configuration"); return; } - Dataspace wideColorDataspace = - Optional.ofNullable(display.getPreferredWideGamutColorSpace()) - .flatMap(Dataspace::find) - // Default to SRGB if the display doesn't support wide color - .orElse(Dataspace.SRGB); - - // Grab the physical screen dimensions from the active display mode - // Strictly speaking the screen resolution may not always be constant - it is for - // sizing the font cache for the underlying rendering thread. Since it's a - // heuristic we don't need to be always 100% correct. - Mode activeMode = display.getMode(); - nInitDisplayInfo(activeMode.getPhysicalWidth(), activeMode.getPhysicalHeight(), - display.getRefreshRate(), wideColorDataspace.mNativeDataspace, - display.getAppVsyncOffsetNanos(), display.getPresentationDeadlineNanos()); + final Display[] allDisplays = dm.getDisplays(); + if (allDisplays.length == 0) { + Log.d(LOG_TAG, "Failed to query displays"); + return; + } + + final Mode activeMode = defaultDisplay.getMode(); + final ColorSpace defaultWideColorSpace = + defaultDisplay.getPreferredWideGamutColorSpace(); + int wideColorDataspace = defaultWideColorSpace != null + ? defaultWideColorSpace.getDataSpace() : 0; + // largest width & height are used to size the default HWUI cache sizes. So find the + // largest display resolution we could encounter & use that as the guidance. The actual + // memory policy in play will interpret these values differently. + int largestWidth = activeMode.getPhysicalWidth(); + int largestHeight = activeMode.getPhysicalHeight(); + final OverlayProperties overlayProperties = defaultDisplay.getOverlaySupport(); + boolean supportFp16ForHdr = overlayProperties != null + ? overlayProperties.supportFp16ForHdr() : false; + boolean supportMixedColorSpaces = overlayProperties != null + ? overlayProperties.supportMixedColorSpaces() : false; + + for (int i = 0; i < allDisplays.length; i++) { + final Display display = allDisplays[i]; + // Take the first wide gamut dataspace as the source of truth + // Possibly should do per-HardwareRenderer wide gamut dataspace so we can use the + // target display's ideal instead + if (wideColorDataspace == 0) { + ColorSpace cs = display.getPreferredWideGamutColorSpace(); + if (cs != null) { + wideColorDataspace = cs.getDataSpace(); + } + } + Mode[] modes = display.getSupportedModes(); + for (int j = 0; j < modes.length; j++) { + Mode mode = modes[j]; + int width = mode.getPhysicalWidth(); + int height = mode.getPhysicalHeight(); + if ((width * height) > (largestWidth * largestHeight)) { + largestWidth = width; + largestHeight = height; + } + } + } + + nInitDisplayInfo(largestWidth, largestHeight, defaultDisplay.getRefreshRate(), + wideColorDataspace, defaultDisplay.getAppVsyncOffsetNanos(), + defaultDisplay.getPresentationDeadlineNanos(), + supportFp16ForHdr, supportMixedColorSpaces); mDisplayInitialized = true; } @@ -1387,12 +1500,18 @@ public class HardwareRenderer { private static native void nSetOpaque(long nativeProxy, boolean opaque); - private static native void nSetColorMode(long nativeProxy, int colorMode); + private static native float nSetColorMode(long nativeProxy, int colorMode); + + private static native void nSetTargetSdrHdrRatio(long nativeProxy, float ratio); private static native void nSetSdrWhitePoint(long nativeProxy, float whitePoint); private static native void nSetIsHighEndGfx(boolean isHighEndGfx); + private static native void nSetIsLowRam(boolean isLowRam); + + private static native void nSetIsSystemOrPersistent(boolean isSystemOrPersistent); + private static native int nSyncAndDrawFrame(long nativeProxy, long[] frameInfo, int size); private static native void nDestroy(long nativeProxy, long rootRenderNode); @@ -1418,6 +1537,8 @@ public class HardwareRenderer { private static native void nTrimMemory(int level); + private static native void nTrimCaches(int level); + private static native void nOverrideProperty(String name, String value); private static native void nFence(long nativeProxy); @@ -1464,8 +1585,8 @@ public class HardwareRenderer { 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); + private static native void nCopySurfaceInto(Surface surface, + int srcLeft, int srcTop, int srcRight, int srcBottom, CopyRequest request); private static native Bitmap nCreateHardwareBitmap(long renderNode, int width, int height); @@ -1484,11 +1605,16 @@ public class HardwareRenderer { private static native void nSetDisplayDensityDpi(int densityDpi); private static native void nInitDisplayInfo(int width, int height, float refreshRate, - int wideColorDataspace, long appVsyncOffsetNanos, long presentationDeadlineNanos); + int wideColorDataspace, long appVsyncOffsetNanos, long presentationDeadlineNanos, + boolean supportsFp16ForHdr, boolean nInitDisplayInfo); private static native void nSetDrawingEnabled(boolean drawingEnabled); private static native boolean nIsDrawingEnabled(); private static native void nSetRtAnimationsEnabled(boolean rtAnimationsEnabled); + + private static native void nNotifyCallbackPending(long nativeProxy); + + private static native void nNotifyExpensiveFrame(long nativeProxy); } diff --git a/graphics/java/android/graphics/ImageDecoder.java b/graphics/java/android/graphics/ImageDecoder.java index 239621eeed1e..b2da233fc21e 100644 --- a/graphics/java/android/graphics/ImageDecoder.java +++ b/graphics/java/android/graphics/ImageDecoder.java @@ -38,6 +38,9 @@ import android.graphics.drawable.AnimatedImageDrawable; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.NinePatchDrawable; +import android.media.MediaCodecInfo; +import android.media.MediaCodecList; +import android.media.MediaFormat; import android.net.Uri; import android.os.Build; import android.os.Trace; @@ -624,11 +627,19 @@ public final class ImageDecoder implements AutoCloseable { */ public static class ImageInfo { private final Size mSize; - private ImageDecoder mDecoder; + private final boolean mIsAnimated; + private final String mMimeType; + private final ColorSpace mColorSpace; - private ImageInfo(@NonNull ImageDecoder decoder) { - mSize = new Size(decoder.mWidth, decoder.mHeight); - mDecoder = decoder; + private ImageInfo( + @NonNull Size size, + boolean isAnimated, + @NonNull String mimeType, + @Nullable ColorSpace colorSpace) { + mSize = size; + mIsAnimated = isAnimated; + mMimeType = mimeType; + mColorSpace = colorSpace; } /** @@ -644,7 +655,7 @@ public final class ImageDecoder implements AutoCloseable { */ @NonNull public String getMimeType() { - return mDecoder.getMimeType(); + return mMimeType; } /** @@ -654,7 +665,7 @@ public final class ImageDecoder implements AutoCloseable { * return an {@link AnimatedImageDrawable}.</p> */ public boolean isAnimated() { - return mDecoder.mAnimated; + return mIsAnimated; } /** @@ -666,7 +677,7 @@ public final class ImageDecoder implements AutoCloseable { */ @Nullable public ColorSpace getColorSpace() { - return mDecoder.getColorSpace(); + return mColorSpace; } }; @@ -912,8 +923,6 @@ public final class ImageDecoder implements AutoCloseable { case "image/jpeg": case "image/webp": case "image/gif": - case "image/heif": - case "image/heic": case "image/bmp": case "image/x-ico": case "image/vnd.wap.wbmp": @@ -928,6 +937,11 @@ public final class ImageDecoder implements AutoCloseable { case "image/x-pentax-pef": case "image/x-samsung-srw": return true; + case "image/heif": + case "image/heic": + return isHevcDecoderSupported(); + case "image/avif": + return isP010SupportedForAV1(); default: return false; } @@ -1682,11 +1696,16 @@ public final class ImageDecoder implements AutoCloseable { * {@link #decodeBitmap decodeBitmap} when setting a non-RGB color space * such as {@link ColorSpace.Named#CIE_LAB Lab}.</p> * - * <p class="note">The specified color space's transfer function must be + * <p class="note">Prior to {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, + * the specified color space's transfer function must be * an {@link ColorSpace.Rgb.TransferParameters ICC parametric curve}. An * <code>IllegalArgumentException</code> will be thrown by the decode methods * if calling {@link ColorSpace.Rgb#getTransferParameters()} on the - * specified color space returns null.</p> + * specified color space returns null. + * Starting from {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, + * the color spaces with non ICC parametric curve transfer function are allowed. + * E.g., {@link ColorSpace.Named#BT2020_HLG BT2020_HLG}. + * </p> * * <p>Like all setters on ImageDecoder, this must be called inside * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p> @@ -1787,12 +1806,39 @@ public final class ImageDecoder implements AutoCloseable { private void callHeaderDecoded(@Nullable OnHeaderDecodedListener listener, @NonNull Source src) { if (listener != null) { - ImageInfo info = new ImageInfo(this); - try { - listener.onHeaderDecoded(this, info, src); - } finally { - info.mDecoder = null; - } + ImageInfo info = + new ImageInfo( + new Size(mWidth, mHeight), mAnimated, getMimeType(), getColorSpace()); + listener.onHeaderDecoded(this, info, src); + } + } + + /** + * Return {@link ImageInfo} from a {@code Source}. + * + * <p>Returns the same {@link ImageInfo} object that a usual decoding process would return as + * part of {@link OnHeaderDecodedListener}. + * + * @param src representing the encoded image. + * @return ImageInfo describing the image. + * @throws IOException if {@code src} is not found, is an unsupported format, or cannot be + * decoded for any reason. + * @hide + */ + @WorkerThread + @NonNull + public static ImageInfo decodeHeader(@NonNull Source src) throws IOException { + Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ImageDecoder#decodeHeader"); + try (ImageDecoder decoder = src.createImageDecoder(true /*preferAnimation*/)) { + // We don't want to leak decoder so resolve all properties immediately. + return new ImageInfo( + new Size(decoder.mWidth, decoder.mHeight), + decoder.mAnimated, + decoder.getMimeType(), + decoder.getColorSpace()); + } finally { + // Close the ImageDecoder#decodeHeader trace. + Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); } } @@ -2058,6 +2104,92 @@ public final class ImageDecoder implements AutoCloseable { return decodeBitmapImpl(src, null); } + private static boolean sIsHevcDecoderSupported = false; + private static boolean sIsHevcDecoderSupportedInitialized = false; + private static final Object sIsHevcDecoderSupportedLock = new Object(); + + /* + * Check if HEVC decoder is supported by the device. + */ + @SuppressWarnings("AndroidFrameworkCompatChange") + private static boolean isHevcDecoderSupported() { + synchronized (sIsHevcDecoderSupportedLock) { + if (sIsHevcDecoderSupportedInitialized) { + return sIsHevcDecoderSupported; + } + MediaFormat format = new MediaFormat(); + format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_HEVC); + MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS); + sIsHevcDecoderSupported = mcl.findDecoderForFormat(format) != null; + sIsHevcDecoderSupportedInitialized = true; + return sIsHevcDecoderSupported; + } + } + + private static boolean sIsP010SupportedForAV1 = false; + private static boolean sIsP010SupportedForHEVC = false; + private static boolean sIsP010SupportedFlagsInitialized = false; + private static final Object sIsP010SupportedLock = new Object(); + + /** + * Checks if the device supports decoding 10-bit AV1. + */ + @SuppressWarnings("AndroidFrameworkCompatChange") // This is not an app-visible API. + private static boolean isP010SupportedForAV1() { + synchronized (sIsP010SupportedLock) { + if (sIsP010SupportedFlagsInitialized) { + return sIsP010SupportedForAV1; + } + checkP010SupportforAV1HEVC(); + return sIsP010SupportedForAV1; + } + } + + /** + * Checks if the device supports decoding 10-bit HEVC. + * This method is called by JNI. + */ + @SuppressWarnings("unused") + private static boolean isP010SupportedForHEVC() { + synchronized (sIsP010SupportedLock) { + if (sIsP010SupportedFlagsInitialized) { + return sIsP010SupportedForHEVC; + } + checkP010SupportforAV1HEVC(); + return sIsP010SupportedForHEVC; + } + } + + /** + * Checks if the device supports decoding 10-bit for the given mime type. + */ + private static void checkP010SupportforAV1HEVC() { + MediaCodecList codecList = new MediaCodecList(MediaCodecList.REGULAR_CODECS); + for (MediaCodecInfo mediaCodecInfo : codecList.getCodecInfos()) { + if (mediaCodecInfo.isEncoder()) { + continue; + } + for (String mediaType : mediaCodecInfo.getSupportedTypes()) { + if (mediaType.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_AV1) + || mediaType.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_HEVC)) { + MediaCodecInfo.CodecCapabilities codecCapabilities = + mediaCodecInfo.getCapabilitiesForType(mediaType); + for (int i = 0; i < codecCapabilities.colorFormats.length; ++i) { + if (codecCapabilities.colorFormats[i] + == MediaCodecInfo.CodecCapabilities.COLOR_FormatYUVP010) { + if (mediaType.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_AV1)) { + sIsP010SupportedForAV1 = true; + } else { + sIsP010SupportedForHEVC = true; + } + } + } + } + } + } + sIsP010SupportedFlagsInitialized = true; + } + /** * Private method called by JNI. */ diff --git a/graphics/java/android/graphics/ImageFormat.java b/graphics/java/android/graphics/ImageFormat.java index b341a4e27e67..cb3b64c3e6cd 100644 --- a/graphics/java/android/graphics/ImageFormat.java +++ b/graphics/java/android/graphics/ImageFormat.java @@ -26,10 +26,21 @@ public class ImageFormat { @Retention(RetentionPolicy.SOURCE) @IntDef(value = { UNKNOWN, + /* + * Since some APIs accept either ImageFormat or PixelFormat (and the two + * enums do not overlap since they're both partial versions of the + * internal format enum), add PixelFormat values here so linting + * tools won't complain when method arguments annotated with + * ImageFormat are provided with PixelFormat values. + */ + PixelFormat.RGBA_8888, + PixelFormat.RGBX_8888, + PixelFormat.RGB_888, RGB_565, YV12, Y8, Y16, + YCBCR_P010, NV16, NV21, YUY2, @@ -49,7 +60,8 @@ public class ImageFormat { RAW_DEPTH, RAW_DEPTH10, PRIVATE, - HEIC + HEIC, + JPEG_R }) public @interface Format { } @@ -247,6 +259,15 @@ public class ImageFormat { public static final int DEPTH_JPEG = 0x69656963; /** + * Compressed JPEG format that includes an embedded recovery map. + * + * <p>JPEG compressed main image along with embedded recovery map following the + * <a href="https://developer.android.com/guide/topics/media/hdr-image-format">Ultra HDR + * Image format specification</a>.</p> + */ + public static final int JPEG_R = 0x1005; + + /** * <p>Multi-plane Android YUV 420 format</p> * * <p>This format is a generic YCbCr format, capable of describing any 4:2:0 @@ -875,6 +896,7 @@ public class ImageFormat { case Y8: case DEPTH_JPEG: case HEIC: + case JPEG_R: return true; } diff --git a/graphics/java/android/graphics/Mesh.java b/graphics/java/android/graphics/Mesh.java new file mode 100644 index 000000000000..66fabec91924 --- /dev/null +++ b/graphics/java/android/graphics/Mesh.java @@ -0,0 +1,385 @@ +/* + * Copyright (C) 2022 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.ColorInt; +import android.annotation.ColorLong; +import android.annotation.IntDef; +import android.annotation.NonNull; + +import libcore.util.NativeAllocationRegistry; + +import java.nio.Buffer; +import java.nio.ShortBuffer; + +/** + * Class representing a mesh object. + * + * This class represents a Mesh object that can optionally be indexed. + * A {@link MeshSpecification} is required along with various attributes for + * detailing the mesh object, including a mode, vertex buffer, optional index buffer, and bounds + * for the mesh. Once generated, a mesh object can be drawn through + * {@link Canvas#drawMesh(Mesh, BlendMode, Paint)} + */ +public class Mesh { + private long mNativeMeshWrapper; + private boolean mIsIndexed; + + /** + * Determines how the mesh is represented and will be drawn. + */ + @IntDef({TRIANGLES, TRIANGLE_STRIP}) + private @interface Mode {} + + /** + * The mesh will be drawn with triangles without utilizing shared vertices. + */ + public static final int TRIANGLES = 0; + + /** + * The mesh will be drawn with triangles utilizing shared vertices. + */ + public static final int TRIANGLE_STRIP = 1; + + private static class MeshHolder { + public static final NativeAllocationRegistry MESH_SPECIFICATION_REGISTRY = + NativeAllocationRegistry.createMalloced( + MeshSpecification.class.getClassLoader(), nativeGetFinalizer()); + } + + /** + * Constructor for a non-indexed Mesh. + * + * @param meshSpec {@link MeshSpecification} used when generating the mesh. + * @param mode Determines what mode to draw the mesh in. Must be one of + * {@link Mesh#TRIANGLES} or {@link Mesh#TRIANGLE_STRIP} + * @param vertexBuffer vertex buffer representing through {@link Buffer}. This provides the data + * for all attributes provided within the meshSpec for every vertex. That + * is, a vertex buffer should be (attributes size * number of vertices) in + * length to be valid. Note that currently implementation will have a CPU + * backed buffer generated. + * @param vertexCount the number of vertices represented in the vertexBuffer and mesh. + * @param bounds bounds of the mesh object. + */ + public Mesh(@NonNull MeshSpecification meshSpec, @Mode int mode, + @NonNull Buffer vertexBuffer, int vertexCount, @NonNull RectF bounds) { + if (mode != TRIANGLES && mode != TRIANGLE_STRIP) { + throw new IllegalArgumentException("Invalid value passed in for mode parameter"); + } + long nativeMesh = nativeMake(meshSpec.mNativeMeshSpec, mode, vertexBuffer, + vertexBuffer.isDirect(), vertexCount, vertexBuffer.position(), bounds.left, + bounds.top, bounds.right, bounds.bottom); + if (nativeMesh == 0) { + throw new IllegalArgumentException("Mesh construction failed."); + } + + meshSetup(nativeMesh, false); + } + + /** + * Constructor for an indexed Mesh. + * + * @param meshSpec {@link MeshSpecification} used when generating the mesh. + * @param mode Determines what mode to draw the mesh in. Must be one of + * {@link Mesh#TRIANGLES} or {@link Mesh#TRIANGLE_STRIP} + * @param vertexBuffer vertex buffer representing through {@link Buffer}. This provides the data + * for all attributes provided within the meshSpec for every vertex. That + * is, a vertex buffer should be (attributes size * number of vertices) in + * length to be valid. Note that currently implementation will have a CPU + * backed buffer generated. + * @param vertexCount the number of vertices represented in the vertexBuffer and mesh. + * @param indexBuffer index buffer representing through {@link ShortBuffer}. Indices are + * required to be 16 bits, so ShortBuffer is necessary. Note that + * currently implementation will have a CPU + * backed buffer generated. + * @param bounds bounds of the mesh object. + */ + public Mesh(@NonNull MeshSpecification meshSpec, @Mode int mode, + @NonNull Buffer vertexBuffer, int vertexCount, @NonNull ShortBuffer indexBuffer, + @NonNull RectF bounds) { + if (mode != TRIANGLES && mode != TRIANGLE_STRIP) { + throw new IllegalArgumentException("Invalid value passed in for mode parameter"); + } + long nativeMesh = nativeMakeIndexed(meshSpec.mNativeMeshSpec, mode, vertexBuffer, + vertexBuffer.isDirect(), vertexCount, vertexBuffer.position(), indexBuffer, + indexBuffer.isDirect(), indexBuffer.capacity(), indexBuffer.position(), bounds.left, + bounds.top, bounds.right, bounds.bottom); + if (nativeMesh == 0) { + throw new IllegalArgumentException("Mesh construction failed."); + } + + meshSetup(nativeMesh, true); + } + + /** + * Sets the uniform color value corresponding to the shader assigned to the mesh. If the shader + * does not have a uniform with that name or if the uniform is declared with a type other than + * vec3 or vec4 and corresponding layout(color) annotation then an IllegalArgumentExcepton is + * thrown. + * + * @param uniformName name matching the color uniform declared in the shader program. + * @param color the provided sRGB color will be converted into the shader program's output + * colorspace and be available as a vec4 uniform in the program. + */ + public void setColorUniform(@NonNull String uniformName, @ColorInt int color) { + setUniform(uniformName, Color.valueOf(color).getComponents(), true); + } + + /** + * Sets the uniform color value corresponding to the shader assigned to the mesh. If the shader + * does not have a uniform with that name or if the uniform is declared with a type other than + * vec3 or vec4 and corresponding layout(color) annotation then an IllegalArgumentExcepton is + * thrown. + * + * @param uniformName name matching the color uniform declared in the shader program. + * @param color the provided sRGB color will be converted into the shader program's output + * colorspace and be available as a vec4 uniform in the program. + */ + public void setColorUniform(@NonNull String uniformName, @ColorLong long color) { + Color exSRGB = Color.valueOf(color).convert(ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB)); + setUniform(uniformName, exSRGB.getComponents(), true); + } + + /** + * Sets the uniform color value corresponding to the shader assigned to the mesh. If the shader + * does not have a uniform with that name or if the uniform is declared with a type other than + * vec3 or vec4 and corresponding layout(color) annotation then an IllegalArgumentExcepton is + * thrown. + * + * @param uniformName name matching the color uniform declared in the shader program. + * @param color the provided sRGB color will be converted into the shader program's output + * colorspace and will be made available as a vec4 uniform in the program. + */ + public void setColorUniform(@NonNull String uniformName, @NonNull Color color) { + if (color == null) { + throw new NullPointerException("The color parameter must not be null"); + } + + Color exSRGB = color.convert(ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB)); + setUniform(uniformName, exSRGB.getComponents(), true); + } + + /** + * Sets the uniform value corresponding to the shader assigned to the mesh. If the shader does + * not have a uniform with that name or if the uniform is declared with a type other than a + * float or float[1] then an IllegalArgumentException is thrown. + * + * @param uniformName name matching the float uniform declared in the shader program. + * @param value float value corresponding to the float uniform with the given name. + */ + public void setFloatUniform(@NonNull String uniformName, float value) { + setFloatUniform(uniformName, value, 0.0f, 0.0f, 0.0f, 1); + } + + /** + * Sets the uniform value corresponding to the shader assigned to the mesh. If the shader does + * not have a uniform with that name or if the uniform is declared with a type other than a + * vec2 or float[2] then an IllegalArgumentException is thrown. + * + * @param uniformName name matching the float uniform declared in the shader program. + * @param value1 first float value corresponding to the float uniform with the given name. + * @param value2 second float value corresponding to the float uniform with the given name. + */ + public void setFloatUniform(@NonNull String uniformName, float value1, float value2) { + setFloatUniform(uniformName, value1, value2, 0.0f, 0.0f, 2); + } + + /** + * Sets the uniform value corresponding to the shader assigned to the mesh. If the shader does + * not have a uniform with that name or if the uniform is declared with a type other than a + * vec3 or float[3] then an IllegalArgumentException is thrown. + * + * @param uniformName name matching the float uniform declared in the shader program. + * @param value1 first float value corresponding to the float uniform with the given name. + * @param value2 second float value corresponding to the float uniform with the given name. + * @param value3 third float value corresponding to the float unifiform with the given + * name. + */ + public void setFloatUniform( + @NonNull String uniformName, float value1, float value2, float value3) { + setFloatUniform(uniformName, value1, value2, value3, 0.0f, 3); + } + + /** + * Sets the uniform value corresponding to the shader assigned to the mesh. If the shader does + * not have a uniform with that name or if the uniform is declared with a type other than a + * vec4 or float[4] then an IllegalArgumentException is thrown. + * + * @param uniformName name matching the float uniform declared in the shader program. + * @param value1 first float value corresponding to the float uniform with the given name. + * @param value2 second float value corresponding to the float uniform with the given name. + * @param value3 third float value corresponding to the float uniform with the given name. + * @param value4 fourth float value corresponding to the float uniform with the given name. + */ + public void setFloatUniform( + @NonNull String uniformName, float value1, float value2, float value3, float value4) { + setFloatUniform(uniformName, value1, value2, value3, value4, 4); + } + + /** + * Sets the uniform value corresponding to the shader assigned to the mesh. If the shader does + * not have a uniform with that name or if the uniform is declared with a type other than a + * float (for N=1), vecN, or float[N], where N is the length of the values param, then an + * IllegalArgumentException is thrown. + * + * @param uniformName name matching the float uniform declared in the shader program. + * @param values float value corresponding to the vec4 float uniform with the given name. + */ + public void setFloatUniform(@NonNull String uniformName, @NonNull float[] values) { + setUniform(uniformName, values, false); + } + + private void setFloatUniform( + String uniformName, float value1, float value2, float value3, float value4, int count) { + if (uniformName == null) { + throw new NullPointerException("The uniformName parameter must not be null"); + } + nativeUpdateUniforms( + mNativeMeshWrapper, uniformName, value1, value2, value3, value4, count); + } + + private void setUniform(String uniformName, float[] values, boolean isColor) { + if (uniformName == null) { + throw new NullPointerException("The uniformName parameter must not be null"); + } + if (values == null) { + throw new NullPointerException("The uniform values parameter must not be null"); + } + + nativeUpdateUniforms(mNativeMeshWrapper, uniformName, values, isColor); + } + + /** + * Sets the uniform value corresponding to the shader assigned to the mesh. If the shader does + * not have a uniform with that name or if the uniform is declared with a type other than int + * or int[1] then an IllegalArgumentException is thrown. + * + * @param uniformName name matching the int uniform delcared in the shader program. + * @param value value corresponding to the int uniform with the given name. + */ + public void setIntUniform(@NonNull String uniformName, int value) { + setIntUniform(uniformName, value, 0, 0, 0, 1); + } + + /** + * Sets the uniform value corresponding to the shader assigned to the mesh. If the shader does + * not have a uniform with that name or if the uniform is declared with a type other than ivec2 + * or int[2] then an IllegalArgumentException is thrown. + * + * @param uniformName name matching the int uniform delcared in the shader program. + * @param value1 first value corresponding to the int uniform with the given name. + * @param value2 second value corresponding to the int uniform with the given name. + */ + public void setIntUniform(@NonNull String uniformName, int value1, int value2) { + setIntUniform(uniformName, value1, value2, 0, 0, 2); + } + + /** + * Sets the uniform value corresponding to the shader assigned to the mesh. If the shader does + * not have a uniform with that name or if the uniform is declared with a type other than ivec3 + * or int[3] then an IllegalArgumentException is thrown. + * + * @param uniformName name matching the int uniform delcared in the shader program. + * @param value1 first value corresponding to the int uniform with the given name. + * @param value2 second value corresponding to the int uniform with the given name. + * @param value3 third value corresponding to the int uniform with the given name. + */ + public void setIntUniform(@NonNull String uniformName, int value1, int value2, int value3) { + setIntUniform(uniformName, value1, value2, value3, 0, 3); + } + + /** + * Sets the uniform value corresponding to the shader assigned to the mesh. If the shader does + * not have a uniform with that name or if the uniform is declared with a type other than ivec4 + * or int[4] then an IllegalArgumentException is thrown. + * + * @param uniformName name matching the int uniform delcared in the shader program. + * @param value1 first value corresponding to the int uniform with the given name. + * @param value2 second value corresponding to the int uniform with the given name. + * @param value3 third value corresponding to the int uniform with the given name. + * @param value4 fourth value corresponding to the int uniform with the given name. + */ + public void setIntUniform( + @NonNull String uniformName, int value1, int value2, int value3, int value4) { + setIntUniform(uniformName, value1, value2, value3, value4, 4); + } + + /** + * Sets the uniform value corresponding to the shader assigned to the mesh. If the shader does + * not have a uniform with that name or if the uniform is declared with a type other than an + * int (for N=1), ivecN, or int[N], where N is the length of the values param, then an + * IllegalArgumentException is thrown. + * + * @param uniformName name matching the int uniform delcared in the shader program. + * @param values int values corresponding to the vec4 int uniform with the given name. + */ + public void setIntUniform(@NonNull String uniformName, @NonNull int[] values) { + if (uniformName == null) { + throw new NullPointerException("The uniformName parameter must not be null"); + } + if (values == null) { + throw new NullPointerException("The uniform values parameter must not be null"); + } + nativeUpdateUniforms(mNativeMeshWrapper, uniformName, values); + } + + /** + * @hide so only calls from module can utilize it + */ + long getNativeWrapperInstance() { + return mNativeMeshWrapper; + } + + private void setIntUniform( + String uniformName, int value1, int value2, int value3, int value4, int count) { + if (uniformName == null) { + throw new NullPointerException("The uniformName parameter must not be null"); + } + + nativeUpdateUniforms( + mNativeMeshWrapper, uniformName, value1, value2, value3, value4, count); + } + + private void meshSetup(long nativeMeshWrapper, boolean isIndexed) { + mNativeMeshWrapper = nativeMeshWrapper; + this.mIsIndexed = isIndexed; + MeshHolder.MESH_SPECIFICATION_REGISTRY.registerNativeAllocation(this, mNativeMeshWrapper); + } + + private static native long nativeGetFinalizer(); + + private static native long nativeMake(long meshSpec, int mode, Buffer vertexBuffer, + boolean isDirect, int vertexCount, int vertexOffset, float left, float top, float right, + float bottom); + + private static native long nativeMakeIndexed(long meshSpec, int mode, Buffer vertexBuffer, + boolean isVertexDirect, int vertexCount, int vertexOffset, ShortBuffer indexBuffer, + boolean isIndexDirect, int indexCount, int indexOffset, float left, float top, + float right, float bottom); + + private static native void nativeUpdateUniforms(long builder, String uniformName, float value1, + float value2, float value3, float value4, int count); + + private static native void nativeUpdateUniforms( + long builder, String uniformName, float[] values, boolean isColor); + + private static native void nativeUpdateUniforms(long builder, String uniformName, int value1, + int value2, int value3, int value4, int count); + + private static native void nativeUpdateUniforms(long builder, String uniformName, int[] values); + +} diff --git a/graphics/java/android/graphics/MeshSpecification.java b/graphics/java/android/graphics/MeshSpecification.java new file mode 100644 index 000000000000..b1aae7f37c31 --- /dev/null +++ b/graphics/java/android/graphics/MeshSpecification.java @@ -0,0 +1,406 @@ +/* + * Copyright (C) 2022 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.IntDef; +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.Size; +import android.annotation.SuppressLint; + +import libcore.util.NativeAllocationRegistry; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Class responsible for holding specifications for {@link Mesh} creations. This class generates a + * {@link MeshSpecification} via the + * {@link MeshSpecification#make(Attribute[], int, Varying[], String, String)} method, + * where multiple parameters to set up the mesh are supplied, including attributes, vertex stride, + * {@link Varying}, and vertex/fragment shaders. There are also additional methods to provide an + * optional {@link ColorSpace} as well as an alpha type. + * + * For example a vertex shader that leverages a {@link Varying} may look like the following: + * + * <pre> + * Varyings main(const Attributes attributes) { + * Varyings varyings; + * varyings.position = attributes.position; + * return varyings; + * } + * </pre> + * + * The corresponding fragment shader that may consume the varying look like the following: + * + * <pre> + * float2 main(const Varyings varyings, out float4 color) { + * color = vec4(1.0, 0.0, 0.0, 1.0); + * return varyings.position; + * } + * </pre> + * + * The color returned from this fragment shader is blended with the other parameters that are + * configured on the Paint object (ex. {@link Paint#setBlendMode(BlendMode)} used to draw the mesh. + * + * The position returned in the fragment shader can be consumed by any following fragment shaders in + * the shader chain. + * + * See https://developer.android.com/develop/ui/views/graphics/agsl for more information + * regarding Android Graphics Shader Language. + * + * Note that there are several limitations on various mesh specifications: + * 1. The max amount of attributes allowed is 8. + * 2. The offset alignment length is 4 bytes. + * 2. The max stride length is 1024. + * 3. The max amount of varyings is 6. + * + * These should be kept in mind when generating a mesh specification, as exceeding them will + * lead to errors. + */ +public class MeshSpecification { + long mNativeMeshSpec; + + /** + * Constants for {@link #make(Attribute[], int, Varying[], String, String)} + * to determine alpha type. Describes how to interpret the alpha component of a pixel. + * + * @hide + */ + @IntDef( + prefix = {"ALPHA_TYPE_"}, + value = {ALPHA_TYPE_UNKNOWN, ALPHA_TYPE_OPAQUE, ALPHA_TYPE_PREMULTIPLIED, + ALPHA_TYPE_UNPREMULTIPLIED} + ) + @Retention(RetentionPolicy.SOURCE) + private @interface AlphaType {} + + /** + * uninitialized. + */ + public static final int ALPHA_TYPE_UNKNOWN = 0; + + /** + * Pixel is opaque. + */ + public static final int ALPHA_TYPE_OPAQUE = 1; + + /** + * Pixel components are premultiplied by alpha. + */ + public static final int ALPHA_TYPE_PREMULTIPLIED = 2; + + /** + * Pixel components are independent of alpha. + */ + public static final int ALPHA_TYPE_UNPREMULTIPLIED = 3; + + /** + * Constants for {@link Attribute} and {@link Varying} for determining the data type. + * + * @hide + */ + @IntDef( + prefix = {"TYPE_"}, + value = {TYPE_FLOAT, TYPE_FLOAT2, TYPE_FLOAT3, TYPE_FLOAT4, TYPE_UBYTE4} + ) + @Retention(RetentionPolicy.SOURCE) + private @interface Type {} + + /** + * Represents one float. Its equivalent shader type is float. + */ + public static final int TYPE_FLOAT = 0; + + /** + * Represents two floats. Its equivalent shader type is float2. + */ + public static final int TYPE_FLOAT2 = 1; + + /** + * Represents three floats. Its equivalent shader type is float3. + */ + public static final int TYPE_FLOAT3 = 2; + + /** + * Represents four floats. Its equivalent shader type is float4. + */ + public static final int TYPE_FLOAT4 = 3; + + /** + * Represents four bytes. Its equivalent shader type is half4. + */ + public static final int TYPE_UBYTE4 = 4; + + /** + * Data class to represent a single attribute in a shader. An attribute is a variable that + * accompanies a vertex, this can be a color or texture coordinates. + * + * See https://developer.android.com/develop/ui/views/graphics/agsl for more information + * regarding Android Graphics Shader Language. + * + * Note that offset is the offset in number of bytes. For example, if we had two attributes + * + * <pre> + * Float3 att1 + * Float att2 + * </pre> + * + * att1 would have an offset of 0, while att2 would have an offset of 12 bytes. + * + * This is consumed as part of + * {@link MeshSpecification#make(Attribute[], int, Varying[], String, String, ColorSpace, int)} + * to create a {@link MeshSpecification} instance. + */ + public static class Attribute { + @Type + private final int mType; + private final int mOffset; + private final String mName; + + public Attribute(@Type int type, int offset, @NonNull String name) { + mType = type; + mOffset = offset; + mName = name; + } + + /** + * Return the corresponding data type for this {@link Attribute}. + */ + @Type + public int getType() { + return mType; + } + + /** + * Return the offset of the attribute in bytes + */ + public int getOffset() { + return mOffset; + } + + /** + * Return the name of this {@link Attribute} + */ + @NonNull + public String getName() { + return mName; + } + + @Override + public String toString() { + return "Attribute{" + + "mType=" + mType + + ", mOffset=" + mOffset + + ", mName='" + mName + '\'' + + '}'; + } + } + + /** + * Data class to represent a single varying variable. A Varying variable can be altered by the + * vertex shader defined on the mesh but not by the fragment shader defined by AGSL. + * + * See https://developer.android.com/develop/ui/views/graphics/agsl for more information + * regarding Android Graphics Shader Language. + * + * This is consumed as part of + * {@link MeshSpecification#make(Attribute[], int, Varying[], String, String, ColorSpace, int)} + * to create a {@link MeshSpecification} instance. + */ + public static class Varying { + @Type + private final int mType; + private final String mName; + + public Varying(@Type int type, @NonNull String name) { + mType = type; + mName = name; + } + + /** + * Return the corresponding data type for this {@link Varying}. + */ + @Type + public int getType() { + return mType; + } + + /** + * Return the name of this {@link Varying} + */ + @NonNull + public String getName() { + return mName; + } + + @Override + public String toString() { + return "Varying{" + + "mType=" + mType + + ", mName='" + mName + '\'' + + '}'; + } + } + + private static class MeshSpecificationHolder { + public static final NativeAllocationRegistry MESH_SPECIFICATION_REGISTRY = + NativeAllocationRegistry.createMalloced( + MeshSpecification.class.getClassLoader(), nativeGetFinalizer()); + } + + /** + * Creates a {@link MeshSpecification} object for use within {@link Mesh}. This uses a default + * color space of {@link ColorSpace.Named#SRGB} and alphaType of + * {@link #ALPHA_TYPE_PREMULTIPLIED}. + * + * @param attributes list of attributes represented by {@link Attribute}. Can hold a max of + * 8. + * @param vertexStride length of vertex stride in bytes. This should be the size of a single + * vertex' attributes. Max of 1024 is accepted. + * @param varyings List of varyings represented by {@link Varying}. Can hold a max of 6. + * Note that `position` is provided by default, does not need to be + * provided in the list, and does not count towards + * the 6 varyings allowed. + * @param vertexShader vertex shader to be supplied to the mesh. Ensure that the position + * varying is set within the shader to get proper results. + * See {@link MeshSpecification} for an example vertex shader + * implementation + * @param fragmentShader fragment shader to be supplied to the mesh. + * See {@link MeshSpecification} for an example fragment shader + * implementation + * @return {@link MeshSpecification} object for use when creating {@link Mesh} + */ + @NonNull + public static MeshSpecification make( + @SuppressLint("ArrayReturn") @NonNull @Size(max = 8) Attribute[] attributes, + @IntRange(from = 1, to = 1024) int vertexStride, + @SuppressLint("ArrayReturn") @NonNull @Size(max = 6) Varying[] varyings, + @NonNull String vertexShader, + @NonNull String fragmentShader) { + long nativeMeshSpec = nativeMake(attributes, + vertexStride, varyings, vertexShader, + fragmentShader); + if (nativeMeshSpec == 0) { + throw new IllegalArgumentException("MeshSpecification construction failed"); + } + return new MeshSpecification(nativeMeshSpec); + } + + /** + * Creates a {@link MeshSpecification} object. This uses a default alphaType of + * {@link #ALPHA_TYPE_PREMULTIPLIED}. + * + * @param attributes list of attributes represented by {@link Attribute}. Can hold a max of + * 8. + * @param vertexStride length of vertex stride in bytes. This should be the size of a single + * vertex' attributes. Max of 1024 is accepted. + * @param varyings List of varyings represented by {@link Varying}. Can hold a max of 6. + * Note that `position` is provided by default, does not need to be + * provided in the list, and does not count towards + * the 6 varyings allowed. + * @param vertexShader vertex shader to be supplied to the mesh. Ensure that the position + * varying is set within the shader to get proper results. + * See {@link MeshSpecification} for an example vertex shader + * implementation + * @param fragmentShader fragment shader to be supplied to the mesh. + * See {@link MeshSpecification} for an example fragment shader + * implementation + * @param colorSpace {@link ColorSpace} to tell what color space to work in. + * @return {@link MeshSpecification} object for use when creating {@link Mesh} + */ + @NonNull + public static MeshSpecification make( + @SuppressLint("ArrayReturn") @NonNull @Size(max = 8) Attribute[] attributes, + @IntRange(from = 1, to = 1024) int vertexStride, + @SuppressLint("ArrayReturn") @NonNull @Size(max = 6) Varying[] varyings, + @NonNull String vertexShader, + @NonNull String fragmentShader, + @NonNull ColorSpace colorSpace + ) { + long nativeMeshSpec = nativeMakeWithCS(attributes, + vertexStride, varyings, vertexShader, + fragmentShader, colorSpace.getNativeInstance()); + if (nativeMeshSpec == 0) { + throw new IllegalArgumentException("MeshSpecification construction failed"); + } + return new MeshSpecification(nativeMeshSpec); + } + + /** + * Creates a {@link MeshSpecification} object. + * + * @param attributes list of attributes represented by {@link Attribute}. Can hold a max of + * 8. + * @param vertexStride length of vertex stride in bytes. This should be the size of a single + * vertex' attributes. Max of 1024 is accepted. + * @param varyings List of varyings represented by {@link Varying}. Can hold a max of 6. + * Note that `position` is provided by default, does not need to be + * provided in the list, and does not count towards + * the 6 varyings allowed. + * @param vertexShader vertex shader to be supplied to the mesh. Ensure that the position + * varying is set within the shader to get proper results. + * See {@link MeshSpecification} for an example vertex shader + * implementation + * @param fragmentShader fragment shader to be supplied to the mesh. + * See {@link MeshSpecification} for an example fragment shader + * implementation + * @param colorSpace {@link ColorSpace} to tell what color space to work in. + * @param alphaType Describes how to interpret the alpha component for a pixel. Must be + * one of + * {@link MeshSpecification#ALPHA_TYPE_UNKNOWN}, + * {@link MeshSpecification#ALPHA_TYPE_OPAQUE}, + * {@link MeshSpecification#ALPHA_TYPE_PREMULTIPLIED}, or + * {@link MeshSpecification#ALPHA_TYPE_UNPREMULTIPLIED} + * @return {@link MeshSpecification} object for use when creating {@link Mesh} + */ + @NonNull + public static MeshSpecification make( + @SuppressLint("ArrayReturn") @NonNull @Size(max = 8) Attribute[] attributes, + @IntRange(from = 1, to = 1024) int vertexStride, + @SuppressLint("ArrayReturn") @NonNull @Size(max = 6) Varying[] varyings, + @NonNull String vertexShader, + @NonNull String fragmentShader, + @NonNull ColorSpace colorSpace, + @AlphaType int alphaType) { + long nativeMeshSpec = + nativeMakeWithAlpha(attributes, vertexStride, varyings, vertexShader, + fragmentShader, colorSpace.getNativeInstance(), alphaType); + if (nativeMeshSpec == 0) { + throw new IllegalArgumentException("MeshSpecification construction failed"); + } + return new MeshSpecification(nativeMeshSpec); + } + + private MeshSpecification(long meshSpec) { + mNativeMeshSpec = meshSpec; + MeshSpecificationHolder.MESH_SPECIFICATION_REGISTRY.registerNativeAllocation( + this, meshSpec); + } + + private static native long nativeGetFinalizer(); + + private static native long nativeMake(Attribute[] attributes, int vertexStride, + Varying[] varyings, String vertexShader, String fragmentShader); + + private static native long nativeMakeWithCS(Attribute[] attributes, int vertexStride, + Varying[] varyings, String vertexShader, String fragmentShader, long colorSpace); + + private static native long nativeMakeWithAlpha(Attribute[] attributes, int vertexStride, + Varying[] varyings, String vertexShader, String fragmentShader, long colorSpace, + int alphaType); +} diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java index f438a03b1434..d35dcab11f49 100644 --- a/graphics/java/android/graphics/Paint.java +++ b/graphics/java/android/graphics/Paint.java @@ -3143,6 +3143,128 @@ public class Paint { return result; } + + /** + * Measure the advance of each character within a run of text and also return the cursor + * position within the run. + * + * @see #getRunAdvance(char[], int, int, int, int, boolean, int) for more details. + * + * @param text the text to measure. Cannot be null. + * @param start the start index of the range to measure, inclusive + * @param end the end index of the range to measure, exclusive + * @param contextStart the start index of the shaping context, inclusive + * @param contextEnd the end index of the shaping context, exclusive + * @param isRtl whether the run is in RTL direction + * @param offset index of caret position + * @param advances the array that receives the computed character advances + * @param advancesIndex the start index from which the advances array is filled + * @return width measurement between start and offset + * @throws IndexOutOfBoundsException if a) contextStart or contextEnd is out of array's range + * or contextStart is larger than contextEnd, + * b) start or end is not within the range [contextStart, contextEnd), or start is larger than + * end, + * c) offset is not within the range [start, end), + * d) advances.length - advanceIndex is smaller than the length of the run, which equals to + * end - start. + * + */ + public float getRunCharacterAdvance(@NonNull char[] text, int start, int end, int contextStart, + int contextEnd, boolean isRtl, int offset, + @Nullable float[] advances, int advancesIndex) { + if (text == null) { + throw new IllegalArgumentException("text cannot be null"); + } + if (contextStart < 0 || contextEnd > text.length) { + throw new IndexOutOfBoundsException("Invalid Context Range: " + contextStart + ", " + + contextEnd + " must be in 0, " + text.length); + } + + if (start < contextStart || contextEnd < end) { + throw new IndexOutOfBoundsException("Invalid start/end range: " + start + ", " + end + + " must be in " + contextStart + ", " + contextEnd); + } + + if (offset < start || end < offset) { + throw new IndexOutOfBoundsException("Invalid offset position: " + offset + + " must be in " + start + ", " + end); + } + + if (advances != null && advances.length < advancesIndex - start + end) { + throw new IndexOutOfBoundsException("Given array doesn't have enough space to receive " + + "the result, advances.length: " + advances.length + " advanceIndex: " + + advancesIndex + " needed space: " + (offset - start)); + } + + if (end == start) { + return 0.0f; + } + + return nGetRunCharacterAdvance(mNativePaint, text, start, end, contextStart, contextEnd, + isRtl, offset, advances, advancesIndex); + } + + /** + * @see #getRunCharacterAdvance(char[], int, int, int, int, boolean, int, float[], int) + * + * @param text the text to measure. Cannot be null. + * @param start the index of the start of the range to measure + * @param end the index + 1 of the end of the range to measure + * @param contextStart the index of the start of the shaping context + * @param contextEnd the index + 1 of the end of the shaping context + * @param isRtl whether the run is in RTL direction + * @param offset index of caret position + * @param advances the array that receives the computed character advances + * @param advancesIndex the start index from which the advances array is filled + * @return width measurement between start and offset + * @throws IndexOutOfBoundsException if a) contextStart or contextEnd is out of array's range + * or contextStart is larger than contextEnd, + * b) start or end is not within the range [contextStart, contextEnd), or end is larger than + * start, + * c) offset is not within the range [start, end), + * d) advances.length - advanceIndex is smaller than the run length, which equals to + * end - start. + */ + public float getRunCharacterAdvance(@NonNull CharSequence text, int start, int end, + int contextStart, int contextEnd, boolean isRtl, int offset, + @Nullable float[] advances, int advancesIndex) { + if (text == null) { + throw new IllegalArgumentException("text cannot be null"); + } + if (contextStart < 0 || contextEnd > text.length()) { + throw new IndexOutOfBoundsException("Invalid Context Range: " + contextStart + ", " + + contextEnd + " must be in 0, " + text.length()); + } + + if (start < contextStart || contextEnd < end) { + throw new IndexOutOfBoundsException("Invalid start/end range: " + start + ", " + end + + " must be in " + contextStart + ", " + contextEnd); + } + + if (offset < start || end < offset) { + throw new IndexOutOfBoundsException("Invalid offset position: " + offset + + " must be in " + start + ", " + end); + } + + if (advances != null && advances.length < advancesIndex - start + end) { + throw new IndexOutOfBoundsException("Given array doesn't have enough space to receive " + + "the result, advances.length: " + advances.length + " advanceIndex: " + + advancesIndex + " needed space: " + (offset - start)); + } + + if (end == start) { + return 0.0f; + } + + char[] buf = TemporaryBuffer.obtain(contextEnd - contextStart); + TextUtils.getChars(text, contextStart, contextEnd, buf, 0); + final float result = getRunCharacterAdvance(buf, start - contextStart, end - contextStart, + 0, contextEnd - contextStart, isRtl, offset - contextStart, + advances, advancesIndex); + TemporaryBuffer.recycle(buf); + return result; + } + /** * Get the character offset within the string whose position is closest to the specified * horizontal position. @@ -3254,6 +3376,9 @@ public class Paint { private static native boolean nHasGlyph(long paintPtr, int bidiFlags, String string); private static native float nGetRunAdvance(long paintPtr, char[] text, int start, int end, int contextStart, int contextEnd, boolean isRtl, int offset); + private static native float nGetRunCharacterAdvance(long paintPtr, char[] text, int start, + int end, int contextStart, int contextEnd, boolean isRtl, int offset, float[] advances, + int advancesIndex); private static native int nGetOffsetForAdvance(long paintPtr, char[] text, int start, int end, int contextStart, int contextEnd, boolean isRtl, float advance); private static native void nGetFontMetricsIntForText(long paintPtr, char[] text, diff --git a/graphics/java/android/graphics/Path.java b/graphics/java/android/graphics/Path.java index e5ef10d1d555..81b8542c20f7 100644 --- a/graphics/java/android/graphics/Path.java +++ b/graphics/java/android/graphics/Path.java @@ -20,8 +20,6 @@ import android.annotation.FloatRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.Size; -import android.compat.annotation.UnsupportedAppUsage; -import android.os.Build; import dalvik.annotation.optimization.CriticalNative; import dalvik.annotation.optimization.FastNative; @@ -47,18 +45,6 @@ public class Path { public final long mNativePath; /** - * @hide - */ - @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - public boolean isSimplePath = true; - /** - * @hide - */ - @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - public Region rects; - private Direction mLastDirection = null; - - /** * Create an empty path */ public Path() { @@ -72,15 +58,7 @@ public class Path { * @param src The path to copy from when initializing the new path */ public Path(@Nullable Path src) { - long valNative = 0; - if (src != null) { - valNative = src.mNativePath; - isSimplePath = src.isSimplePath; - if (src.rects != null) { - rects = new Region(src.rects); - } - } - mNativePath = nInit(valNative); + mNativePath = nInit(src != null ? src.mNativePath : 0); sRegistry.registerNativeAllocation(this, mNativePath); } @@ -89,9 +67,6 @@ public class Path { * This does NOT change the fill-type setting. */ public void reset() { - isSimplePath = true; - mLastDirection = null; - if (rects != null) rects.setEmpty(); // We promised not to change this, so preserve it around the native // call, which does now reset fill type. final FillType fillType = getFillType(); @@ -104,9 +79,6 @@ public class Path { * keeps the internal data structure for faster reuse. */ public void rewind() { - isSimplePath = true; - mLastDirection = null; - if (rects != null) rects.setEmpty(); nRewind(mNativePath); } @@ -116,19 +88,17 @@ public class Path { if (this == src) { return; } - isSimplePath = src.isSimplePath; nSet(mNativePath, src.mNativePath); - if (!isSimplePath) { - return; - } + } - if (rects != null && src.rects != null) { - rects.set(src.rects); - } else if (rects != null && src.rects == null) { - rects.setEmpty(); - } else if (src.rects != null) { - rects = new Region(src.rects); - } + /** + * Returns an iterator over the segments of this path. + * + * @return the Iterator object + */ + @NonNull + public PathIterator getPathIterator() { + return new PathIterator(this); } /** @@ -192,12 +162,7 @@ public class Path { * @see #op(Path, android.graphics.Path.Op) */ public boolean op(@NonNull Path path1, @NonNull Path path2, @NonNull Op op) { - if (nOp(path1.mNativePath, path2.mNativePath, op.ordinal(), this.mNativePath)) { - isSimplePath = false; - rects = null; - return true; - } - return false; + return nOp(path1.mNativePath, path2.mNativePath, op.ordinal(), this.mNativePath); } /** @@ -378,7 +343,6 @@ public class Path { * @param y The y-coordinate of the end of a line */ public void lineTo(float x, float y) { - isSimplePath = false; nLineTo(mNativePath, x, y); } @@ -393,7 +357,6 @@ public class Path { * this contour, to specify a line */ public void rLineTo(float dx, float dy) { - isSimplePath = false; nRLineTo(mNativePath, dx, dy); } @@ -408,7 +371,6 @@ public class Path { * @param y2 The y-coordinate of the end point on a quadratic curve */ public void quadTo(float x1, float y1, float x2, float y2) { - isSimplePath = false; nQuadTo(mNativePath, x1, y1, x2, y2); } @@ -427,11 +389,53 @@ public class Path { * this contour, for the end point of a quadratic curve */ public void rQuadTo(float dx1, float dy1, float dx2, float dy2) { - isSimplePath = false; nRQuadTo(mNativePath, dx1, dy1, dx2, dy2); } /** + * Add a quadratic bezier from the last point, approaching control point + * (x1,y1), and ending at (x2,y2), weighted by <code>weight</code>. If no + * moveTo() call has been made for this contour, the first point is + * automatically set to (0,0). + * + * A weight of 1 is equivalent to calling {@link #quadTo(float, float, float, float)}. + * A weight of 0 is equivalent to calling {@link #lineTo(float, float)} to + * <code>(x1, y1)</code> followed by {@link #lineTo(float, float)} to <code>(x2, y2)</code>. + * + * @param x1 The x-coordinate of the control point on a conic curve + * @param y1 The y-coordinate of the control point on a conic curve + * @param x2 The x-coordinate of the end point on a conic curve + * @param y2 The y-coordinate of the end point on a conic curve + * @param weight The weight of the conic applied to the curve. A value of 1 is equivalent + * to a quadratic with the given control and anchor points and a value of 0 is + * equivalent to a line to the first and another line to the second point. + */ + public void conicTo(float x1, float y1, float x2, float y2, float weight) { + nConicTo(mNativePath, x1, y1, x2, y2, weight); + } + + /** + * Same as conicTo, but the coordinates are considered relative to the last + * point on this contour. If there is no previous point, then a moveTo(0,0) + * is inserted automatically. + * + * @param dx1 The amount to add to the x-coordinate of the last point on + * this contour, for the control point of a conic curve + * @param dy1 The amount to add to the y-coordinate of the last point on + * this contour, for the control point of a conic curve + * @param dx2 The amount to add to the x-coordinate of the last point on + * this contour, for the end point of a conic curve + * @param dy2 The amount to add to the y-coordinate of the last point on + * this contour, for the end point of a conic curve + * @param weight The weight of the conic applied to the curve. A value of 1 is equivalent + * to a quadratic with the given control and anchor points and a value of 0 is + * equivalent to a line to the first and another line to the second point. + */ + public void rConicTo(float dx1, float dy1, float dx2, float dy2, float weight) { + nRConicTo(mNativePath, dx1, dy1, dx2, dy2, weight); + } + + /** * Add a cubic bezier from the last point, approaching control points * (x1,y1) and (x2,y2), and ending at (x3,y3). If no moveTo() call has been * made for this contour, the first point is automatically set to (0,0). @@ -445,7 +449,6 @@ public class Path { */ public void cubicTo(float x1, float y1, float x2, float y2, float x3, float y3) { - isSimplePath = false; nCubicTo(mNativePath, x1, y1, x2, y2, x3, y3); } @@ -456,7 +459,6 @@ public class Path { */ public void rCubicTo(float x1, float y1, float x2, float y2, float x3, float y3) { - isSimplePath = false; nRCubicTo(mNativePath, x1, y1, x2, y2, x3, y3); } @@ -507,7 +509,6 @@ public class Path { */ public void arcTo(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean forceMoveTo) { - isSimplePath = false; nArcTo(mNativePath, left, top, right, bottom, startAngle, sweepAngle, forceMoveTo); } @@ -516,7 +517,6 @@ public class Path { * first point of the contour, a line segment is automatically added. */ public void close() { - isSimplePath = false; nClose(mNativePath); } @@ -536,18 +536,6 @@ public class Path { final int nativeInt; } - private void detectSimplePath(float left, float top, float right, float bottom, Direction dir) { - if (mLastDirection == null) { - mLastDirection = dir; - } - if (mLastDirection != dir) { - isSimplePath = false; - } else { - if (rects == null) rects = new Region(); - rects.op((int) left, (int) top, (int) right, (int) bottom, Region.Op.UNION); - } - } - /** * Add a closed rectangle contour to the path * @@ -568,7 +556,6 @@ public class Path { * @param dir The direction to wind the rectangle's contour */ public void addRect(float left, float top, float right, float bottom, @NonNull Direction dir) { - detectSimplePath(left, top, right, bottom, dir); nAddRect(mNativePath, left, top, right, bottom, dir.nativeInt); } @@ -588,7 +575,6 @@ public class Path { * @param dir The direction to wind the oval's contour */ public void addOval(float left, float top, float right, float bottom, @NonNull Direction dir) { - isSimplePath = false; nAddOval(mNativePath, left, top, right, bottom, dir.nativeInt); } @@ -601,7 +587,6 @@ public class Path { * @param dir The direction to wind the circle's contour */ public void addCircle(float x, float y, float radius, @NonNull Direction dir) { - isSimplePath = false; nAddCircle(mNativePath, x, y, radius, dir.nativeInt); } @@ -624,7 +609,6 @@ public class Path { */ public void addArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle) { - isSimplePath = false; nAddArc(mNativePath, left, top, right, bottom, startAngle, sweepAngle); } @@ -649,7 +633,6 @@ public class Path { */ public void addRoundRect(float left, float top, float right, float bottom, float rx, float ry, @NonNull Direction dir) { - isSimplePath = false; nAddRoundRect(mNativePath, left, top, right, bottom, rx, ry, dir.nativeInt); } @@ -682,7 +665,6 @@ public class Path { if (radii.length < 8) { throw new ArrayIndexOutOfBoundsException("radii[] needs 8 values"); } - isSimplePath = false; nAddRoundRect(mNativePath, left, top, right, bottom, radii, dir.nativeInt); } @@ -693,7 +675,6 @@ public class Path { * @param dx The amount to translate the path in X as it is added */ public void addPath(@NonNull Path src, float dx, float dy) { - isSimplePath = false; nAddPath(mNativePath, src.mNativePath, dx, dy); } @@ -703,7 +684,6 @@ public class Path { * @param src The path that is appended to the current path */ public void addPath(@NonNull Path src) { - isSimplePath = false; nAddPath(mNativePath, src.mNativePath); } @@ -713,7 +693,6 @@ public class Path { * @param src The path to add as a new contour */ public void addPath(@NonNull Path src, @NonNull Matrix matrix) { - if (!src.isSimplePath) isSimplePath = false; nAddPath(mNativePath, src.mNativePath, matrix.ni()); } @@ -741,15 +720,6 @@ public class Path { * @param dy The amount in the Y direction to offset the entire path */ public void offset(float dx, float dy) { - if (isSimplePath && rects == null) { - // nothing to offset - return; - } - if (isSimplePath && dx == Math.rint(dx) && dy == Math.rint(dy)) { - rects.translate((int) dx, (int) dy); - } else { - isSimplePath = false; - } nOffset(mNativePath, dx, dy); } @@ -760,7 +730,6 @@ public class Path { * @param dy The new Y coordinate for the last point */ public void setLastPoint(float dx, float dy) { - isSimplePath = false; nSetLastPoint(mNativePath, dx, dy); } @@ -773,12 +742,7 @@ public class Path { * then the the original path is modified */ public void transform(@NonNull Matrix matrix, @Nullable Path dst) { - long dstNative = 0; - if (dst != null) { - dst.isSimplePath = false; - dstNative = dst.mNativePath; - } - nTransform(mNativePath, matrix.ni(), dstNative); + nTransform(mNativePath, matrix.ni(), dst != null ? dst.mNativePath : 0); } /** @@ -787,7 +751,6 @@ public class Path { * @param matrix The matrix to apply to the path */ public void transform(@NonNull Matrix matrix) { - isSimplePath = false; nTransform(mNativePath, matrix.ni()); } @@ -797,7 +760,6 @@ public class Path { } final long mutateNI() { - isSimplePath = false; return mNativePath; } @@ -827,6 +789,46 @@ public class Path { return nApproximate(mNativePath, acceptableError); } + /** + * Returns the generation ID of this path. The generation ID changes + * whenever the path is modified. This can be used as an efficient way to + * check if a path has changed. + * + * @return The current generation ID for this path + */ + public int getGenerationId() { + return nGetGenerationID(mNativePath); + } + + /** + * Two paths can be interpolated, by calling {@link #interpolate(Path, float, Path)}, if they + * have exactly the same structure. That is, both paths must have the same + * operations, in the same order. If any of the operations are + * of type {@link PathIterator#VERB_CONIC}, then the weights of those conics must also match. + * + * @param otherPath The other <code>Path</code> being interpolated to from this one. + * @return true if interpolation is possible, false otherwise + */ + public boolean isInterpolatable(@NonNull Path otherPath) { + return nIsInterpolatable(mNativePath, otherPath.mNativePath); + } + + /** + * This method will linearly interpolate from this path to <code>otherPath</code> given + * the interpolation parameter <code>t</code>, returning the result in + * <code>interpolatedPath</code>. Interpolation will only succeed if the structures of the + * two paths match exactly, as discussed in {@link #isInterpolatable(Path)}. + * + * @param otherPath The other <code>Path</code> being interpolated to. + * @param t The interpolation parameter. A value of 0 results in a <code>Path</code> + * equivalent to this path, a value of 1 results in one equivalent to + * <code>otherPath</code>. + * @param interpolatedPath The interpolated results. + */ + public boolean interpolate(@NonNull Path otherPath, float t, @NonNull Path interpolatedPath) { + return nInterpolate(mNativePath, otherPath.mNativePath, t, interpolatedPath.mNativePath); + } + // ------------------ Regular JNI ------------------------ private static native long nInit(); @@ -841,6 +843,10 @@ public class Path { private static native void nRLineTo(long nPath, float dx, float dy); private static native void nQuadTo(long nPath, float x1, float y1, float x2, float y2); private static native void nRQuadTo(long nPath, float dx1, float dy1, float dx2, float dy2); + private static native void nConicTo(long nPath, float x1, float y1, float x2, float y2, + float weight); + private static native void nRConicTo(long nPath, float dx1, float dy1, float dx2, float dy2, + float weight); private static native void nCubicTo(long nPath, float x1, float y1, float x2, float y2, float x3, float y3); private static native void nRCubicTo(long nPath, float x1, float y1, float x2, float y2, @@ -868,6 +874,8 @@ public class Path { private static native void nTransform(long nPath, long matrix); private static native boolean nOp(long path1, long path2, int op, long result); private static native float[] nApproximate(long nPath, float error); + private static native boolean nInterpolate(long startPath, long endPath, float t, + long interpolatedPath); // ------------------ Fast JNI ------------------------ @@ -877,6 +885,10 @@ public class Path { // ------------------ Critical JNI ------------------------ @CriticalNative + private static native int nGetGenerationID(long nativePath); + @CriticalNative + private static native boolean nIsInterpolatable(long startPath, long endPath); + @CriticalNative private static native void nReset(long nPath); @CriticalNative private static native void nRewind(long nPath); diff --git a/graphics/java/android/graphics/PathIterator.java b/graphics/java/android/graphics/PathIterator.java new file mode 100644 index 000000000000..48b29f4e81f4 --- /dev/null +++ b/graphics/java/android/graphics/PathIterator.java @@ -0,0 +1,297 @@ +/* + * Copyright (C) 2022 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 static java.lang.annotation.RetentionPolicy.SOURCE; + +import android.annotation.IntDef; +import android.annotation.NonNull; + +import dalvik.annotation.optimization.CriticalNative; +import dalvik.system.VMRuntime; + +import libcore.util.NativeAllocationRegistry; + +import java.lang.annotation.Retention; +import java.util.ConcurrentModificationException; +import java.util.Iterator; + +/** + * <code>PathIterator</code> can be used to query a given {@link Path} object, to discover its + * operations and point values. + */ +public class PathIterator implements Iterator<PathIterator.Segment> { + + private final float[] mPointsArray; + private final long mPointsAddress; + private int mCachedVerb = -1; + private boolean mDone = false; + private final long mNativeIterator; + private final Path mPath; + private final int mPathGenerationId; + private static final int POINT_ARRAY_SIZE = 8; + + private static final NativeAllocationRegistry sRegistry = + NativeAllocationRegistry.createMalloced( + PathIterator.class.getClassLoader(), nGetFinalizer()); + + /** + * The <code>Verb</code> indicates the operation for a given segment of a path. These + * operations correspond exactly to the primitive operations on {@link Path}, such as + * {@link Path#moveTo(float, float)} and {@link Path#lineTo(float, float)}, except for + * {@link #VERB_DONE}, which means that there are no more operations in this path. + */ + @Retention(SOURCE) + @IntDef({VERB_MOVE, VERB_LINE, VERB_QUAD, VERB_CONIC, VERB_CUBIC, VERB_CLOSE, VERB_DONE}) + @interface Verb {} + // these must match the values in SkPath.h + public static final int VERB_MOVE = 0; + public static final int VERB_LINE = 1; + public static final int VERB_QUAD = 2; + public static final int VERB_CONIC = 3; + public static final int VERB_CUBIC = 4; + public static final int VERB_CLOSE = 5; + public static final int VERB_DONE = 6; + + /** + * Returns a {@link PathIterator} object for this path, which can be used to query the + * data (operations and points) in the path. Iterators can only be used on Path objects + * that have not been modified since the iterator was created. Calling + * {@link #next(float[], int)}, {@link #next()}, or {@link #hasNext()} on an + * iterator for a modified path will result in a {@link ConcurrentModificationException}. + * + * @param path The {@link Path} for which this iterator can be queried. + */ + PathIterator(@NonNull Path path) { + mPath = path; + mNativeIterator = nCreate(mPath.mNativePath); + mPathGenerationId = mPath.getGenerationId(); + final VMRuntime runtime = VMRuntime.getRuntime(); + mPointsArray = (float[]) runtime.newNonMovableArray(float.class, POINT_ARRAY_SIZE); + mPointsAddress = runtime.addressOf(mPointsArray); + sRegistry.registerNativeAllocation(this, mNativeIterator); + } + + /** + * Returns the next verb in this iterator's {@link Path}, and fills entries in the + * <code>points</code> array with the point data (if any) for that operation. + * Each two floats represent the data for a single point of that operation. + * The number of pairs of floats supplied in the resulting array depends on the verb: + * <ul> + * <li>{@link #VERB_MOVE}: 1 pair (indices 0 to 1)</li> + * <li>{@link #VERB_LINE}: 2 pairs (indices 0 to 3)</li> + * <li>{@link #VERB_QUAD}: 3 pairs (indices 0 to 5)</li> + * <li>{@link #VERB_CONIC}: 3.5 pairs (indices 0 to 6), the seventh entry has the conic + * weight</li> + * <li>{@link #VERB_CUBIC}: 4 pairs (indices 0 to 7)</li> + * <li>{@link #VERB_CLOSE}: 0 pairs</li> + * <li>{@link #VERB_DONE}: 0 pairs</li> + * </ul> + * @param points The point data for this operation, must have at least + * 8 items available to hold up to 4 pairs of point values + * @param offset An offset into the <code>points</code> array where entries should be placed. + * @return the operation for the next element in the iteration + * @throws ArrayIndexOutOfBoundsException if the points array is too small + * @throws ConcurrentModificationException if the underlying path was modified + * since this iterator was created. + */ + @NonNull + public @Verb int next(@NonNull float[] points, int offset) { + if (points.length < offset + POINT_ARRAY_SIZE) { + throw new ArrayIndexOutOfBoundsException("points array must be able to " + + "hold at least 8 entries"); + } + @Verb int returnVerb = getReturnVerb(mCachedVerb); + mCachedVerb = -1; + System.arraycopy(mPointsArray, 0, points, offset, POINT_ARRAY_SIZE); + return returnVerb; + } + + /** + * Returns true if the there are more elements in this iterator to be returned. + * A return value of <code>false</code> means there are no more elements, and an + * ensuing call to {@link #next()} or {@link #next(float[], int)} )} will return + * {@link #VERB_DONE}. + * + * @return true if there are more elements to be iterated through, false otherwise + * @throws ConcurrentModificationException if the underlying path was modified + * since this iterator was created. + */ + @Override + public boolean hasNext() { + if (mCachedVerb == -1) { + mCachedVerb = nextInternal(); + } + return mCachedVerb != VERB_DONE; + } + + /** + * Returns the next verb in the iteration, or {@link #VERB_DONE} if there are no more + * elements. + * + * @return the next verb in the iteration, or {@link #VERB_DONE} if there are no more + * elements + * @throws ConcurrentModificationException if the underlying path was modified + * since this iterator was created. + */ + @NonNull + public @Verb int peek() { + if (mPathGenerationId != mPath.getGenerationId()) { + throw new ConcurrentModificationException( + "Iterator cannot be used on modified Path"); + } + if (mDone) { + return VERB_DONE; + } + return nPeek(mNativeIterator); + } + + /** + * This is where the work is done for {@link #next()}. Using this internal method + * is helfpul for managing the cached segment used by {@link #hasNext()}. + * + * @return the segment to be returned by {@link #next()} + * @throws ConcurrentModificationException if the underlying path was modified + * since this iterator was created. + */ + @NonNull + private @Verb int nextInternal() { + if (mDone) { + return VERB_DONE; + } + if (mPathGenerationId != mPath.getGenerationId()) { + throw new ConcurrentModificationException( + "Iterator cannot be used on modified Path"); + } + @Verb int verb = nNext(mNativeIterator, mPointsAddress); + if (verb == VERB_DONE) { + mDone = true; + } + return verb; + } + + /** + * Returns the next {@link Segment} element in this iterator. + * + * There are two versions of <code>next()</code>. This version is slightly more + * expensive at runtime, since it allocates a new {@link Segment} object with + * every call. The other version, {@link #next(float[], int)} requires no such allocation, but + * requires a little more manual effort to use. + * + * @return the next segment in this iterator + * @throws ConcurrentModificationException if the underlying path was modified + * since this iterator was created. + */ + @NonNull + @Override + public Segment next() { + @Verb int returnVerb = getReturnVerb(mCachedVerb); + mCachedVerb = -1; + float conicWeight = 0f; + if (returnVerb == VERB_CONIC) { + conicWeight = mPointsArray[6]; + } + float[] returnPoints = new float[8]; + System.arraycopy(mPointsArray, 0, returnPoints, 0, POINT_ARRAY_SIZE); + return new Segment(returnVerb, returnPoints, conicWeight); + } + + private @Verb int getReturnVerb(int cachedVerb) { + switch (cachedVerb) { + case VERB_MOVE: return VERB_MOVE; + case VERB_LINE: return VERB_LINE; + case VERB_QUAD: return VERB_QUAD; + case VERB_CONIC: return VERB_CONIC; + case VERB_CUBIC: return VERB_CUBIC; + case VERB_CLOSE: return VERB_CLOSE; + case VERB_DONE: return VERB_DONE; + } + return nextInternal(); + } + + /** + * This class holds the data for a given segment in a path, as returned by + * {@link #next()}. + */ + public static class Segment { + private final @Verb int mVerb; + private final float[] mPoints; + private final float mConicWeight; + + /** + * The operation for this segment. + * + * @return the verb which indicates the operation happening in this segment + */ + @NonNull + public @Verb int getVerb() { + return mVerb; + } + + /** + * The point data for this segment. + * + * Each two floats represent the data for a single point of that operation. + * The number of pairs of floats supplied in the resulting array depends on the verb: + * <ul> + * <li>{@link #VERB_MOVE}: 1 pair (indices 0 to 1)</li> + * <li>{@link #VERB_LINE}: 2 pairs (indices 0 to 3)</li> + * <li>{@link #VERB_QUAD}: 3 pairs (indices 0 to 5)</li> + * <li>{@link #VERB_CONIC}: 4 pairs (indices 0 to 7), the last pair contains the + * conic weight twice</li> + * <li>{@link #VERB_CUBIC}: 4 pairs (indices 0 to 7)</li> + * <li>{@link #VERB_CLOSE}: 0 pairs</li> + * <li>{@link #VERB_DONE}: 0 pairs</li> + * </ul> + * @return the point data for this segment + */ + @NonNull + public float[] getPoints() { + return mPoints; + } + + /** + * The weight for the conic operation in this segment. If the verb in this segment + * is not equal to {@link #VERB_CONIC}, the weight value is undefined. + * + * @see Path#conicTo(float, float, float, float, float) + * @return the weight for the conic operation in this segment, if any + */ + public float getConicWeight() { + return mConicWeight; + } + + Segment(@NonNull @Verb int verb, @NonNull float[] points, float conicWeight) { + mVerb = verb; + mPoints = points; + mConicWeight = conicWeight; + } + } + + // ------------------ Regular JNI ------------------------ + + private static native long nCreate(long nativePath); + private static native long nGetFinalizer(); + + // ------------------ Critical JNI ------------------------ + + @CriticalNative + private static native int nNext(long nativeIterator, long pointsAddress); + + @CriticalNative + private static native int nPeek(long nativeIterator); +} diff --git a/graphics/java/android/graphics/RenderNode.java b/graphics/java/android/graphics/RenderNode.java index dadbd8d2d1aa..2e91c240d71b 100644 --- a/graphics/java/android/graphics/RenderNode.java +++ b/graphics/java/android/graphics/RenderNode.java @@ -1561,6 +1561,16 @@ public final class RenderNode { return nGetUniqueId(mNativeRenderNode); } + /** + * Captures whether this RenderNote represents a TextureView + * TODO(b/281695725): Clean this up once TextureView use setFrameRate API + * + * @hide + */ + public void setIsTextureView() { + nSetIsTextureView(mNativeRenderNode); + } + /////////////////////////////////////////////////////////////////////////// // Animations /////////////////////////////////////////////////////////////////////////// @@ -1891,4 +1901,7 @@ public final class RenderNode { @CriticalNative private static native long nGetUniqueId(long renderNode); + + @CriticalNative + private static native void nSetIsTextureView(long renderNode); } diff --git a/graphics/java/android/graphics/RuntimeShader.java b/graphics/java/android/graphics/RuntimeShader.java index 9c36fc36474c..3e6457919031 100644 --- a/graphics/java/android/graphics/RuntimeShader.java +++ b/graphics/java/android/graphics/RuntimeShader.java @@ -19,6 +19,7 @@ package android.graphics; import android.annotation.ColorInt; import android.annotation.ColorLong; import android.annotation.NonNull; +import android.util.ArrayMap; import android.view.Window; import libcore.util.NativeAllocationRegistry; @@ -256,6 +257,12 @@ public class RuntimeShader extends Shader { private long mNativeInstanceRuntimeShaderBuilder; /** + * For tracking GC usage. Keep a java-side reference for reachable objects to + * enable better heap tracking & tooling support + */ + private ArrayMap<String, Shader> mShaderUniforms = new ArrayMap<>(); + + /** * Creates a new RuntimeShader. * * @param shader The text of AGSL shader program to run. @@ -490,6 +497,7 @@ public class RuntimeShader extends Shader { if (shader == null) { throw new NullPointerException("The shader parameter must not be null"); } + mShaderUniforms.put(shaderName, shader); nativeUpdateShader( mNativeInstanceRuntimeShaderBuilder, shaderName, shader.getNativeInstance()); discardNativeInstance(); @@ -511,6 +519,7 @@ public class RuntimeShader extends Shader { throw new NullPointerException("The shader parameter must not be null"); } + mShaderUniforms.put(shaderName, shader); nativeUpdateShader(mNativeInstanceRuntimeShaderBuilder, shaderName, shader.getNativeInstanceWithDirectSampling()); discardNativeInstance(); diff --git a/graphics/java/android/graphics/Typeface.java b/graphics/java/android/graphics/Typeface.java index a2f5301e353f..9fb627fcc501 100644 --- a/graphics/java/android/graphics/Typeface.java +++ b/graphics/java/android/graphics/Typeface.java @@ -50,6 +50,7 @@ import android.util.Base64; import android.util.Log; import android.util.LongSparseArray; import android.util.LruCache; +import android.util.Pair; import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; @@ -57,6 +58,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.Preconditions; import dalvik.annotation.optimization.CriticalNative; +import dalvik.annotation.optimization.FastNative; import libcore.util.NativeAllocationRegistry; @@ -193,6 +195,8 @@ public class Typeface { @UnsupportedAppUsage public final long native_instance; + private final String mSystemFontFamilyName; + private final Runnable mCleaner; /** @hide */ @@ -270,6 +274,14 @@ public class Typeface { } /** + * Returns the system font family name if the typeface was created from a system font family, + * otherwise returns null. + */ + public final @Nullable String getSystemFontFamilyName() { + return mSystemFontFamilyName; + } + + /** * Returns true if the system has the font family with the name [familyName]. For example * querying with "sans-serif" would check if the "sans-serif" family is defined in the system * and return true if does. @@ -868,7 +880,7 @@ public class Typeface { final int italic = (mStyle == null || mStyle.getSlant() == FontStyle.FONT_SLANT_UPRIGHT) ? 0 : 1; return new Typeface(nativeCreateFromArray( - ptrArray, fallbackTypeface.native_instance, weight, italic)); + ptrArray, fallbackTypeface.native_instance, weight, italic), null); } } @@ -933,7 +945,8 @@ public class Typeface { } } - typeface = new Typeface(nativeCreateFromTypeface(ni, style)); + typeface = new Typeface(nativeCreateFromTypeface(ni, style), + family.getSystemFontFamilyName()); styles.put(style, typeface); } return typeface; @@ -1001,7 +1014,8 @@ public class Typeface { } typeface = new Typeface( - nativeCreateFromTypefaceWithExactStyle(base.native_instance, weight, italic)); + nativeCreateFromTypefaceWithExactStyle(base.native_instance, weight, italic), + base.getSystemFontFamilyName()); innerCache.put(key, typeface); } return typeface; @@ -1011,7 +1025,10 @@ public class Typeface { public static Typeface createFromTypefaceWithVariation(@Nullable Typeface family, @NonNull List<FontVariationAxis> axes) { final Typeface base = family == null ? Typeface.DEFAULT : family; - return new Typeface(nativeCreateFromTypefaceWithVariation(base.native_instance, axes)); + Typeface typeface = new Typeface( + nativeCreateFromTypefaceWithVariation(base.native_instance, axes), + base.getSystemFontFamilyName()); + return typeface; } /** @@ -1106,7 +1123,7 @@ public class Typeface { } return new Typeface(nativeCreateFromArray( ptrArray, 0, RESOLVE_BY_FONT_TABLE, - RESOLVE_BY_FONT_TABLE)); + RESOLVE_BY_FONT_TABLE), null); } /** @@ -1114,13 +1131,14 @@ public class Typeface { * * @param families array of font families */ - private static Typeface createFromFamilies(@Nullable FontFamily[] families) { + private static Typeface createFromFamilies(@NonNull String familyName, + @Nullable FontFamily[] families) { final long[] ptrArray = new long[families.length]; for (int i = 0; i < families.length; ++i) { ptrArray[i] = families[i].getNativePtr(); } return new Typeface(nativeCreateFromArray(ptrArray, 0, - RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE)); + RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE), familyName); } /** @@ -1160,12 +1178,17 @@ public class Typeface { ptrArray[i] = families[i].mNativePtr; } return new Typeface(nativeCreateFromArray( - ptrArray, fallbackTypeface.native_instance, weight, italic)); + ptrArray, fallbackTypeface.native_instance, weight, italic), null); } // don't allow clients to call this directly @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) private Typeface(long ni) { + this(ni, null); + } + + // don't allow clients to call this directly + private Typeface(long ni, @Nullable String systemFontFamilyName) { if (ni == 0) { throw new RuntimeException("native typeface cannot be made"); } @@ -1174,6 +1197,19 @@ public class Typeface { mCleaner = sRegistry.registerNativeAllocation(this, native_instance); mStyle = nativeGetStyle(ni); mWeight = nativeGetWeight(ni); + mSystemFontFamilyName = systemFontFamilyName; + } + + /** + * Releases the underlying native object. + * + * <p>For testing only. Do not use the instance after this method is called. + * It is safe to call this method twice or more on the same instance. + * @hide + */ + @TestApi + public void releaseNativeObjectForTest() { + mCleaner.run(); } private static Typeface getSystemDefaultTypeface(@NonNull String familyName) { @@ -1187,7 +1223,8 @@ public class Typeface { List<FontConfig.Alias> aliases, Map<String, Typeface> outSystemFontMap) { for (Map.Entry<String, FontFamily[]> entry : fallbacks.entrySet()) { - outSystemFontMap.put(entry.getKey(), createFromFamilies(entry.getValue())); + outSystemFontMap.put(entry.getKey(), + createFromFamilies(entry.getKey(), entry.getValue())); } for (int i = 0; i < aliases.size(); ++i) { @@ -1202,8 +1239,8 @@ public class Typeface { continue; } final int weight = alias.getWeight(); - final Typeface newFace = weight == 400 ? base : - new Typeface(nativeCreateWeightAlias(base.native_instance, weight)); + final Typeface newFace = weight == 400 ? base : new Typeface( + nativeCreateWeightAlias(base.native_instance, weight), alias.getName()); outSystemFontMap.put(alias.getName(), newFace); } } @@ -1231,14 +1268,16 @@ public class Typeface { nativePtrs[i++] = entry.getValue().native_instance; writeString(namesBytes, entry.getKey()); } - int typefacesBytesCount = nativeWriteTypefaces(null, nativePtrs); // int (typefacesBytesCount), typefaces, namesBytes + final int typefaceBytesCountSize = Integer.BYTES; + int typefacesBytesCount = nativeWriteTypefaces(null, typefaceBytesCountSize, nativePtrs); SharedMemory sharedMemory = SharedMemory.create( - "fontMap", Integer.BYTES + typefacesBytesCount + namesBytes.size()); + "fontMap", typefaceBytesCountSize + typefacesBytesCount + namesBytes.size()); ByteBuffer writableBuffer = sharedMemory.mapReadWrite().order(ByteOrder.BIG_ENDIAN); try { writableBuffer.putInt(typefacesBytesCount); - int writtenBytesCount = nativeWriteTypefaces(writableBuffer.slice(), nativePtrs); + int writtenBytesCount = + nativeWriteTypefaces(writableBuffer, writableBuffer.position(), nativePtrs); if (writtenBytesCount != typefacesBytesCount) { throw new IOException(String.format("Unexpected bytes written: %d, expected: %d", writtenBytesCount, typefacesBytesCount)); @@ -1256,6 +1295,13 @@ public class Typeface { /** * Deserialize the font mapping from the serialized byte buffer. * + * <p>Warning: the given {@code buffer} must outlive generated Typeface + * objects in {@code out}. In production code, this is guaranteed by + * storing the buffer in {@link #sSystemFontMapBuffer}. + * If you call this method in a test, please make sure to destroy the + * generated Typeface objects by calling + * {@link #releaseNativeObjectForTest()}. + * * @hide */ @TestApi @@ -1263,7 +1309,9 @@ public class Typeface { @NonNull ByteBuffer buffer, @NonNull Map<String, Typeface> out) throws IOException { int typefacesBytesCount = buffer.getInt(); - long[] nativePtrs = nativeReadTypefaces(buffer.slice()); + // Note: Do not call buffer.slice(), as nativeReadTypefaces() expects + // that buffer.address() is page-aligned. + long[] nativePtrs = nativeReadTypefaces(buffer, buffer.position()); if (nativePtrs == null) { throw new IOException("Could not read typefaces"); } @@ -1271,7 +1319,7 @@ public class Typeface { buffer.position(buffer.position() + typefacesBytesCount); for (long nativePtr : nativePtrs) { String name = readString(buffer); - out.put(name, new Typeface(nativePtr)); + out.put(name, new Typeface(nativePtr, name)); } return nativePtrs; } @@ -1384,6 +1432,41 @@ public class Typeface { } } + /** + * Change default typefaces for testing purpose. + * + * Note: The existing TextView or Paint instance still holds the old Typeface. + * + * @param defaults array of [default, default_bold, default_italic, default_bolditalic]. + * @param genericFamilies array of [sans-serif, serif, monospace] + * @return return the old defaults and genericFamilies + * @hide + */ + @TestApi + @NonNull + public static Pair<List<Typeface>, List<Typeface>> changeDefaultFontForTest( + @NonNull List<Typeface> defaults, + @NonNull List<Typeface> genericFamilies + ) { + synchronized (SYSTEM_FONT_MAP_LOCK) { + List<Typeface> oldDefaults = Arrays.asList(sDefaults); + sDefaults = defaults.toArray(new Typeface[4]); + setDefault(defaults.get(0)); + + ArrayList<Typeface> oldGenerics = new ArrayList<>(); + oldGenerics.add(sSystemFontMap.get("sans-serif")); + sSystemFontMap.put("sans-serif", genericFamilies.get(0)); + + oldGenerics.add(sSystemFontMap.get("serif")); + sSystemFontMap.put("serif", genericFamilies.get(1)); + + oldGenerics.add(sSystemFontMap.get("monospace")); + sSystemFontMap.put("monospace", genericFamilies.get(2)); + + return new Pair<>(oldDefaults, oldGenerics); + } + } + static { // Preload Roboto-Regular.ttf in Zygote for improving app launch performance. preloadFontFile("/system/fonts/Roboto-Regular.ttf"); @@ -1395,6 +1478,9 @@ public class Typeface { FontConfig config = SystemFonts.getSystemPreinstalledFontConfig(); for (int i = 0; i < config.getFontFamilies().size(); ++i) { FontConfig.FontFamily family = config.getFontFamilies().get(i); + if (!family.getLocaleList().isEmpty()) { + nativeRegisterLocaleList(family.getLocaleList().toLanguageTags()); + } boolean loadFamily = false; for (int j = 0; j < family.getLocaleList().size(); ++j) { String fontScript = ULocale.addLikelySubtags( @@ -1425,7 +1511,7 @@ public class Typeface { public static void destroySystemFontMap() { synchronized (SYSTEM_FONT_MAP_LOCK) { for (Typeface typeface : sSystemFontMap.values()) { - typeface.mCleaner.run(); + typeface.releaseNativeObjectForTest(); } sSystemFontMap.clear(); if (sSystemFontMapBuffer != null) { @@ -1433,7 +1519,23 @@ public class Typeface { } sSystemFontMapBuffer = null; sSystemFontMapSharedMemory = null; + synchronized (sStyledCacheLock) { + destroyTypefaceCacheLocked(sStyledTypefaceCache); + } + synchronized (sWeightCacheLock) { + destroyTypefaceCacheLocked(sWeightTypefaceCache); + } + } + } + + private static void destroyTypefaceCacheLocked(LongSparseArray<SparseArray<Typeface>> cache) { + for (int i = 0; i < cache.size(); i++) { + SparseArray<Typeface> array = cache.valueAt(i); + for (int j = 0; j < array.size(); j++) { + array.valueAt(j).releaseNativeObjectForTest(); + } } + cache.clear(); } /** @hide */ @@ -1486,16 +1588,6 @@ public class Typeface { return Arrays.binarySearch(mSupportedAxes, axis) >= 0; } - /** @hide */ - public List<FontFamily> getFallback() { - ArrayList<FontFamily> families = new ArrayList<>(); - int familySize = nativeGetFamilySize(native_instance); - for (int i = 0; i < familySize; ++i) { - families.add(new FontFamily(nativeGetFamily(native_instance, i))); - } - return families; - } - private static native long nativeCreateFromTypeface(long native_instance, int style); private static native long nativeCreateFromTypefaceWithExactStyle( long native_instance, int weight, boolean italic); @@ -1521,19 +1613,13 @@ public class Typeface { @CriticalNative private static native long nativeGetReleaseFunc(); - @CriticalNative - private static native int nativeGetFamilySize(long naitvePtr); - - @CriticalNative - private static native long nativeGetFamily(long nativePtr, int index); - - private static native void nativeRegisterGenericFamily(String str, long nativePtr); private static native int nativeWriteTypefaces( - @Nullable ByteBuffer buffer, @NonNull long[] nativePtrs); + @Nullable ByteBuffer buffer, int position, @NonNull long[] nativePtrs); - private static native @Nullable long[] nativeReadTypefaces(@NonNull ByteBuffer buffer); + private static native + @Nullable long[] nativeReadTypefaces(@NonNull ByteBuffer buffer, int position); private static native void nativeForceSetStaticFinalField(String fieldName, Typeface typeface); @@ -1541,4 +1627,7 @@ public class Typeface { private static native void nativeAddFontCollections(long nativePtr); private static native void nativeWarmUpCache(String fileName); + + @FastNative + private static native void nativeRegisterLocaleList(String locales); } diff --git a/graphics/java/android/graphics/YuvImage.java b/graphics/java/android/graphics/YuvImage.java index af3f27661c84..6b5238b20cdc 100644 --- a/graphics/java/android/graphics/YuvImage.java +++ b/graphics/java/android/graphics/YuvImage.java @@ -16,6 +16,8 @@ package android.graphics; +import android.annotation.NonNull; +import android.annotation.Nullable; import java.io.OutputStream; /** @@ -63,7 +65,70 @@ public class YuvImage { private int mHeight; /** - * Construct an YuvImage. + * The color space of the image, defaults to SRGB + */ + @NonNull private ColorSpace mColorSpace; + + /** + * Array listing all supported ImageFormat that are supported by this class + */ + private final static String[] sSupportedFormats = + {"NV21", "YUY2", "YCBCR_P010", "YUV_420_888"}; + + private static String printSupportedFormats() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < sSupportedFormats.length; ++i) { + sb.append(sSupportedFormats[i]); + if (i != sSupportedFormats.length - 1) { + sb.append(", "); + } + } + return sb.toString(); + } + + /** + * Array listing all supported HDR ColorSpaces that are supported by JPEG/R encoding + */ + private final static ColorSpace.Named[] sSupportedJpegRHdrColorSpaces = { + ColorSpace.Named.BT2020_HLG, + ColorSpace.Named.BT2020_PQ + }; + + /** + * Array listing all supported SDR ColorSpaces that are supported by JPEG/R encoding + */ + private final static ColorSpace.Named[] sSupportedJpegRSdrColorSpaces = { + ColorSpace.Named.SRGB, + ColorSpace.Named.DISPLAY_P3 + }; + + private static String printSupportedJpegRColorSpaces(boolean isHdr) { + ColorSpace.Named[] colorSpaces = isHdr ? sSupportedJpegRHdrColorSpaces : + sSupportedJpegRSdrColorSpaces; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < colorSpaces.length; ++i) { + sb.append(ColorSpace.get(colorSpaces[i]).getName()); + if (i != colorSpaces.length - 1) { + sb.append(", "); + } + } + return sb.toString(); + } + + private static boolean isSupportedJpegRColorSpace(boolean isHdr, int colorSpace) { + ColorSpace.Named[] colorSpaces = isHdr ? sSupportedJpegRHdrColorSpaces : + sSupportedJpegRSdrColorSpaces; + for (ColorSpace.Named cs : colorSpaces) { + if (cs.ordinal() == colorSpace) { + return true; + } + } + return false; + } + + + /** + * Construct an YuvImage. Use SRGB for as default {@link ColorSpace}. * * @param yuv The YUV data. In the case of more than one image plane, all the planes must be * concatenated into a single byte array. @@ -77,11 +142,33 @@ public class YuvImage { * null. */ public YuvImage(byte[] yuv, int format, int width, int height, int[] strides) { + this(yuv, format, width, height, strides, ColorSpace.get(ColorSpace.Named.SRGB)); + } + + /** + * Construct an YuvImage. + * + * @param yuv The YUV data. In the case of more than one image plane, all the planes + * must be concatenated into a single byte array. + * @param format The YUV data format as defined in {@link ImageFormat}. + * @param width The width of the YuvImage. + * @param height The height of the YuvImage. + * @param strides (Optional) Row bytes of each image plane. If yuv contains padding, the + * stride of each image must be provided. If strides is null, the method + * assumes no padding and derives the row bytes by format and width itself. + * @param colorSpace The YUV image color space as defined in {@link ColorSpace}. + * If the parameter is null, SRGB will be set as the default value. + * @throws IllegalArgumentException if format is not support; width or height <= 0; or yuv is + * null. + */ + public YuvImage(@NonNull byte[] yuv, int format, int width, int height, + @Nullable int[] strides, @NonNull ColorSpace colorSpace) { if (format != ImageFormat.NV21 && - format != ImageFormat.YUY2) { + format != ImageFormat.YUY2 && + format != ImageFormat.YCBCR_P010 && + format != ImageFormat.YUV_420_888) { throw new IllegalArgumentException( - "only support ImageFormat.NV21 " + - "and ImageFormat.YUY2 for now"); + "only supports the following ImageFormat:" + printSupportedFormats()); } if (width <= 0 || height <= 0) { @@ -93,6 +180,10 @@ public class YuvImage { throw new IllegalArgumentException("yuv cannot be null"); } + if (colorSpace == null) { + throw new IllegalArgumentException("ColorSpace cannot be null"); + } + if (strides == null) { mStrides = calculateStrides(width, format); } else { @@ -103,12 +194,13 @@ public class YuvImage { mFormat = format; mWidth = width; mHeight = height; + mColorSpace = colorSpace; } /** * Compress a rectangle region in the YuvImage to a jpeg. - * Only ImageFormat.NV21 and ImageFormat.YUY2 - * are supported for now. + * For image format, only ImageFormat.NV21 and ImageFormat.YUY2 are supported. + * For color space, only SRGB is supported. * * @param rectangle The rectangle region to be compressed. The medthod checks if rectangle is * inside the image. Also, the method modifies rectangle if the chroma pixels @@ -117,10 +209,18 @@ public class YuvImage { * small size, 100 meaning compress for max quality. * @param stream OutputStream to write the compressed data. * @return True if the compression is successful. - * @throws IllegalArgumentException if rectangle is invalid; quality is not within [0, - * 100]; or stream is null. + * @throws IllegalArgumentException if rectangle is invalid; color space or image format + * is not supported; quality is not within [0, 100]; or stream is null. */ public boolean compressToJpeg(Rect rectangle, int quality, OutputStream stream) { + if (mFormat != ImageFormat.NV21 && mFormat != ImageFormat.YUY2) { + throw new IllegalArgumentException( + "Only ImageFormat.NV21 and ImageFormat.YUY2 are supported."); + } + if (mColorSpace.getId() != ColorSpace.Named.SRGB.ordinal()) { + throw new IllegalArgumentException("Only SRGB color space is supported."); + } + Rect wholeImage = new Rect(0, 0, mWidth, mHeight); if (!wholeImage.contains(rectangle)) { throw new IllegalArgumentException( @@ -143,6 +243,70 @@ public class YuvImage { new byte[WORKING_COMPRESS_STORAGE]); } + /** + * Compress the HDR image into JPEG/R format. + * + * Sample usage: + * hdr_image.compressToJpegR(sdr_image, 90, stream); + * + * For the SDR image, only YUV_420_888 image format is supported, and the following + * color spaces are supported: + * ColorSpace.Named.SRGB, + * ColorSpace.Named.DISPLAY_P3 + * + * For the HDR image, only YCBCR_P010 image format is supported, and the following + * color spaces are supported: + * ColorSpace.Named.BT2020_HLG, + * ColorSpace.Named.BT2020_PQ + * + * @param sdr The SDR image, only ImageFormat.YUV_420_888 is supported. + * @param quality Hint to the compressor, 0-100. 0 meaning compress for + * small size, 100 meaning compress for max quality. + * @param stream OutputStream to write the compressed data. + * @return True if the compression is successful. + * @throws IllegalArgumentException if input images are invalid; quality is not within [0, + * 100]; or stream is null. + */ + public boolean compressToJpegR(@NonNull YuvImage sdr, int quality, + @NonNull OutputStream stream) { + if (sdr == null) { + throw new IllegalArgumentException("SDR input cannot be null"); + } + + if (mData.length == 0 || sdr.getYuvData().length == 0) { + throw new IllegalArgumentException("Input images cannot be empty"); + } + + if (mFormat != ImageFormat.YCBCR_P010 || sdr.getYuvFormat() != ImageFormat.YUV_420_888) { + throw new IllegalArgumentException( + "only support ImageFormat.YCBCR_P010 and ImageFormat.YUV_420_888"); + } + + if (sdr.getWidth() != mWidth || sdr.getHeight() != mHeight) { + throw new IllegalArgumentException("HDR and SDR resolution mismatch"); + } + + if (quality < 0 || quality > 100) { + throw new IllegalArgumentException("quality must be 0..100"); + } + + if (stream == null) { + throw new IllegalArgumentException("stream cannot be null"); + } + + if (!isSupportedJpegRColorSpace(true, mColorSpace.getId()) || + !isSupportedJpegRColorSpace(false, sdr.getColorSpace().getId())) { + throw new IllegalArgumentException("Not supported color space. " + + "SDR only supports: " + printSupportedJpegRColorSpaces(false) + + "HDR only supports: " + printSupportedJpegRColorSpaces(true)); + } + + return nativeCompressToJpegR(mData, mColorSpace.getDataSpace(), + sdr.getYuvData(), sdr.getColorSpace().getDataSpace(), + mWidth, mHeight, quality, stream, + new byte[WORKING_COMPRESS_STORAGE]); + } + /** * @return the YUV data. @@ -179,6 +343,12 @@ public class YuvImage { return mHeight; } + + /** + * @return the color space of the image. + */ + public @NonNull ColorSpace getColorSpace() { return mColorSpace; } + int[] calculateOffsets(int left, int top) { int[] offsets = null; if (mFormat == ImageFormat.NV21) { @@ -198,17 +368,23 @@ public class YuvImage { private int[] calculateStrides(int width, int format) { int[] strides = null; - if (format == ImageFormat.NV21) { + switch (format) { + case ImageFormat.NV21: strides = new int[] {width, width}; return strides; - } - - if (format == ImageFormat.YUY2) { + case ImageFormat.YCBCR_P010: + strides = new int[] {width * 2, width * 2}; + return strides; + case ImageFormat.YUV_420_888: + strides = new int[] {width, (width + 1) / 2, (width + 1) / 2}; + return strides; + case ImageFormat.YUY2: strides = new int[] {width * 2}; return strides; + default: + throw new IllegalArgumentException( + "only supports the following ImageFormat:" + printSupportedFormats()); } - - return strides; } private void adjustRectangle(Rect rect) { @@ -237,4 +413,8 @@ public class YuvImage { private static native boolean nativeCompressToJpeg(byte[] oriYuv, int format, int width, int height, int[] offsets, int[] strides, int quality, OutputStream stream, byte[] tempStorage); + + private static native boolean nativeCompressToJpegR(byte[] hdr, int hdrColorSpaceId, + byte[] sdr, int sdrColorSpaceId, int width, int height, int quality, + OutputStream stream, byte[] tempStorage); } diff --git a/graphics/java/android/graphics/drawable/Icon.java b/graphics/java/android/graphics/drawable/Icon.java index a76d74edc0f4..708feeb9e421 100644 --- a/graphics/java/android/graphics/drawable/Icon.java +++ b/graphics/java/android/graphics/drawable/Icon.java @@ -35,6 +35,7 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.BlendMode; import android.graphics.PorterDuff; +import android.graphics.RecordingCanvas; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; @@ -70,6 +71,7 @@ import java.util.Objects; public final class Icon implements Parcelable { private static final String TAG = "Icon"; + private static final boolean DEBUG = false; /** * An icon that was created using {@link Icon#createWithBitmap(Bitmap)}. @@ -361,15 +363,52 @@ public final class Icon implements Parcelable { } /** + * Resizes image if size too large for Canvas to draw + * @param bitmap Bitmap to be resized if size > {@link RecordingCanvas.MAX_BITMAP_SIZE} + * @return resized bitmap + */ + private Bitmap fixMaxBitmapSize(Bitmap bitmap) { + if (bitmap != null && bitmap.getByteCount() > RecordingCanvas.MAX_BITMAP_SIZE) { + int bytesPerPixel = bitmap.getRowBytes() / bitmap.getWidth(); + int maxNumPixels = RecordingCanvas.MAX_BITMAP_SIZE / bytesPerPixel; + float aspRatio = (float) bitmap.getWidth() / (float) bitmap.getHeight(); + int newHeight = (int) Math.sqrt(maxNumPixels / aspRatio); + int newWidth = (int) (newHeight * aspRatio); + + if (DEBUG) { + Log.d(TAG, + "Image size too large: " + bitmap.getByteCount() + ". Resizing bitmap to: " + + newWidth + " " + newHeight); + } + + return scaleDownIfNecessary(bitmap, newWidth, newHeight); + } + return bitmap; + } + + /** + * Resizes BitmapDrawable if size too large for Canvas to draw + * @param drawable Drawable to be resized if size > {@link RecordingCanvas.MAX_BITMAP_SIZE} + * @return resized Drawable + */ + private Drawable fixMaxBitmapSize(Resources res, Drawable drawable) { + if (drawable instanceof BitmapDrawable) { + Bitmap scaledBmp = fixMaxBitmapSize(((BitmapDrawable) drawable).getBitmap()); + return new BitmapDrawable(res, scaledBmp); + } + return drawable; + } + + /** * Do the heavy lifting of loading the drawable, but stop short of applying any tint. */ private Drawable loadDrawableInner(Context context) { switch (mType) { case TYPE_BITMAP: - return new BitmapDrawable(context.getResources(), getBitmap()); + return new BitmapDrawable(context.getResources(), fixMaxBitmapSize(getBitmap())); case TYPE_ADAPTIVE_BITMAP: return new AdaptiveIconDrawable(null, - new BitmapDrawable(context.getResources(), getBitmap())); + new BitmapDrawable(context.getResources(), fixMaxBitmapSize(getBitmap()))); case TYPE_RESOURCE: if (getResources() == null) { // figure out where to load resources from @@ -400,7 +439,8 @@ public final class Icon implements Parcelable { } } try { - return getResources().getDrawable(getResId(), context.getTheme()); + return fixMaxBitmapSize(getResources(), + getResources().getDrawable(getResId(), context.getTheme())); } catch (RuntimeException e) { Log.e(TAG, String.format("Unable to load resource 0x%08x from pkg=%s", getResId(), @@ -409,21 +449,21 @@ public final class Icon implements Parcelable { } break; case TYPE_DATA: - return new BitmapDrawable(context.getResources(), - BitmapFactory.decodeByteArray(getDataBytes(), getDataOffset(), getDataLength()) - ); + return new BitmapDrawable(context.getResources(), fixMaxBitmapSize( + BitmapFactory.decodeByteArray(getDataBytes(), getDataOffset(), + getDataLength()))); case TYPE_URI: InputStream is = getUriInputStream(context); if (is != null) { return new BitmapDrawable(context.getResources(), - BitmapFactory.decodeStream(is)); + fixMaxBitmapSize(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))); + fixMaxBitmapSize(BitmapFactory.decodeStream(is)))); } break; } diff --git a/graphics/java/android/graphics/drawable/RippleDrawable.java b/graphics/java/android/graphics/drawable/RippleDrawable.java index 6b1cf8b1ed2a..641a2ae7b2c3 100644 --- a/graphics/java/android/graphics/drawable/RippleDrawable.java +++ b/graphics/java/android/graphics/drawable/RippleDrawable.java @@ -16,6 +16,8 @@ package android.graphics.drawable; +import android.annotation.TestApi; + import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.LOCAL_VARIABLE; import static java.lang.annotation.ElementType.METHOD; @@ -360,7 +362,9 @@ public class RippleDrawable extends LayerDrawable { } } - private void setBackgroundActive(boolean hovered, boolean focused, boolean pressed, + /** @hide */ + @TestApi + public void setBackgroundActive(boolean hovered, boolean focused, boolean pressed, boolean windowFocused) { if (mState.mRippleStyle == STYLE_SOLID) { if (mBackground == null && (hovered || focused)) { @@ -843,6 +847,12 @@ public class RippleDrawable extends LayerDrawable { invalidateSelf(false); } + /** @hide */ + @TestApi + public float getTargetBackgroundOpacity() { + return mTargetBackgroundOpacity; + } + private void enterPatternedBackgroundAnimation(boolean focused, boolean hovered, boolean windowFocused) { mBackgroundOpacity = 0; @@ -1003,7 +1013,8 @@ public class RippleDrawable extends LayerDrawable { } p.setShader(shader); p.setColorFilter(null); - p.setColor(color); + // Alpha is handled by the shader (and color is a no-op because there's a shader) + p.setColor(0xFF000000); return properties; } diff --git a/graphics/java/android/graphics/fonts/Font.java b/graphics/java/android/graphics/fonts/Font.java index abd0be9c2872..28cc05162cbb 100644 --- a/graphics/java/android/graphics/fonts/Font.java +++ b/graphics/java/android/graphics/fonts/Font.java @@ -497,8 +497,6 @@ public final class Font { private static native long nBuild( long builderPtr, @NonNull ByteBuffer buffer, @NonNull String filePath, @NonNull String localeList, int weight, boolean italic, int ttcIndex); - @CriticalNative - private static native long nGetReleaseNativeFont(); @FastNative private static native long nClone(long fontPtr, long builderPtr, int weight, diff --git a/graphics/java/android/graphics/fonts/FontCustomizationParser.java b/graphics/java/android/graphics/fonts/FontCustomizationParser.java index df47f73eb04a..b458dd9021d0 100644 --- a/graphics/java/android/graphics/fonts/FontCustomizationParser.java +++ b/graphics/java/android/graphics/fonts/FontCustomizationParser.java @@ -17,7 +17,7 @@ package android.graphics.fonts; import static android.text.FontConfig.Alias; -import static android.text.FontConfig.FontFamily; +import static android.text.FontConfig.NamedFamilyList; import android.annotation.NonNull; import android.annotation.Nullable; @@ -42,11 +42,14 @@ import java.util.Map; * @hide */ public class FontCustomizationParser { + private static final String TAG = "FontCustomizationParser"; + /** * Represents a customization XML */ public static class Result { - private final Map<String, FontFamily> mAdditionalNamedFamilies; + private final Map<String, NamedFamilyList> mAdditionalNamedFamilies; + private final List<Alias> mAdditionalAliases; public Result() { @@ -54,13 +57,13 @@ public class FontCustomizationParser { mAdditionalAliases = Collections.emptyList(); } - public Result(Map<String, FontFamily> additionalNamedFamilies, + public Result(Map<String, NamedFamilyList> additionalNamedFamilies, List<Alias> additionalAliases) { mAdditionalNamedFamilies = additionalNamedFamilies; mAdditionalAliases = additionalAliases; } - public Map<String, FontFamily> getAdditionalNamedFamilies() { + public Map<String, NamedFamilyList> getAdditionalNamedFamilies() { return mAdditionalNamedFamilies; } @@ -85,20 +88,24 @@ public class FontCustomizationParser { return readFamilies(parser, fontDir, updatableFontMap); } - private static Map<String, FontFamily> validateAndTransformToMap(List<FontFamily> families) { - HashMap<String, FontFamily> namedFamily = new HashMap<>(); + private static Result validateAndTransformToResult( + List<NamedFamilyList> families, List<Alias> aliases) { + HashMap<String, NamedFamilyList> namedFamily = new HashMap<>(); for (int i = 0; i < families.size(); ++i) { - final FontFamily family = families.get(i); + final NamedFamilyList family = families.get(i); final String name = family.getName(); - if (name == null) { - throw new IllegalArgumentException("new-named-family requires name attribute"); - } - if (namedFamily.put(name, family) != null) { + if (name != null) { + if (namedFamily.put(name, family) != null) { + throw new IllegalArgumentException( + "new-named-family requires unique name attribute"); + } + } else { throw new IllegalArgumentException( - "new-named-family requires unique name attribute"); + "new-named-family requires name attribute or new-default-fallback-family" + + "requires fallackTarget attribute"); } } - return namedFamily; + return new Result(namedFamily, aliases); } private static Result readFamilies( @@ -106,7 +113,7 @@ public class FontCustomizationParser { @NonNull String fontDir, @Nullable Map<String, File> updatableFontMap ) throws XmlPullParserException, IOException { - List<FontFamily> families = new ArrayList<>(); + List<NamedFamilyList> families = new ArrayList<>(); List<Alias> aliases = new ArrayList<>(); parser.require(XmlPullParser.START_TAG, null, "fonts-modification"); while (parser.next() != XmlPullParser.END_TAG) { @@ -114,19 +121,42 @@ public class FontCustomizationParser { String tag = parser.getName(); if (tag.equals("family")) { readFamily(parser, fontDir, families, updatableFontMap); + } else if (tag.equals("family-list")) { + readFamilyList(parser, fontDir, families, updatableFontMap); } else if (tag.equals("alias")) { aliases.add(FontListParser.readAlias(parser)); } else { FontListParser.skip(parser); } } - return new Result(validateAndTransformToMap(families), aliases); + return validateAndTransformToResult(families, aliases); } private static void readFamily( @NonNull XmlPullParser parser, @NonNull String fontDir, - @NonNull List<FontFamily> out, + @NonNull List<NamedFamilyList> out, + @Nullable Map<String, File> updatableFontMap) + throws XmlPullParserException, IOException { + final String customizationType = parser.getAttributeValue(null, "customizationType"); + if (customizationType == null) { + throw new IllegalArgumentException("customizationType must be specified"); + } + if (customizationType.equals("new-named-family")) { + NamedFamilyList fontFamily = FontListParser.readNamedFamily( + parser, fontDir, updatableFontMap, false); + if (fontFamily != null) { + out.add(fontFamily); + } + } else { + throw new IllegalArgumentException("Unknown customizationType=" + customizationType); + } + } + + private static void readFamilyList( + @NonNull XmlPullParser parser, + @NonNull String fontDir, + @NonNull List<NamedFamilyList> out, @Nullable Map<String, File> updatableFontMap) throws XmlPullParserException, IOException { final String customizationType = parser.getAttributeValue(null, "customizationType"); @@ -134,7 +164,7 @@ public class FontCustomizationParser { throw new IllegalArgumentException("customizationType must be specified"); } if (customizationType.equals("new-named-family")) { - FontFamily fontFamily = FontListParser.readFamily( + NamedFamilyList fontFamily = FontListParser.readNamedFamilyList( parser, fontDir, updatableFontMap, false); if (fontFamily != null) { out.add(fontFamily); diff --git a/graphics/java/android/graphics/fonts/FontFamily.java b/graphics/java/android/graphics/fonts/FontFamily.java index a771a6e35345..bf79b1bedd8e 100644 --- a/graphics/java/android/graphics/fonts/FontFamily.java +++ b/graphics/java/android/graphics/fonts/FontFamily.java @@ -114,17 +114,19 @@ public final class FontFamily { * @return a font family */ public @NonNull FontFamily build() { - return build("", FontConfig.FontFamily.VARIANT_DEFAULT, true /* isCustomFallback */); + return build("", FontConfig.FontFamily.VARIANT_DEFAULT, true /* isCustomFallback */, + false /* isDefaultFallback */); } /** @hide */ public @NonNull FontFamily build(@NonNull String langTags, int variant, - boolean isCustomFallback) { + boolean isCustomFallback, boolean isDefaultFallback) { final long builderPtr = nInitBuilder(); for (int i = 0; i < mFonts.size(); ++i) { nAddFont(builderPtr, mFonts.get(i).getNativePtr()); } - final long ptr = nBuild(builderPtr, langTags, variant, isCustomFallback); + final long ptr = nBuild(builderPtr, langTags, variant, isCustomFallback, + isDefaultFallback); final FontFamily family = new FontFamily(ptr); sFamilyRegistory.registerNativeAllocation(family, ptr); return family; @@ -138,7 +140,7 @@ public final class FontFamily { @CriticalNative private static native void nAddFont(long builderPtr, long fontPtr); private static native long nBuild(long builderPtr, String langTags, int variant, - boolean isCustomFallback); + boolean isCustomFallback, boolean isDefaultFallback); @CriticalNative private static native long nGetReleaseNativeFamily(); } diff --git a/graphics/java/android/graphics/fonts/FontStyle.java b/graphics/java/android/graphics/fonts/FontStyle.java index 09799fdf5a13..48969aa71059 100644 --- a/graphics/java/android/graphics/fonts/FontStyle.java +++ b/graphics/java/android/graphics/fonts/FontStyle.java @@ -48,6 +48,10 @@ public final class FontStyle { private static final String TAG = "FontStyle"; /** + * A default value when font weight is unspecified + */ + public static final int FONT_WEIGHT_UNSPECIFIED = -1; + /** * A minimum weight value for the font */ public static final int FONT_WEIGHT_MIN = 1; diff --git a/graphics/java/android/graphics/fonts/SystemFonts.java b/graphics/java/android/graphics/fonts/SystemFonts.java index 1c1723cbd195..acc4da6e1527 100644 --- a/graphics/java/android/graphics/fonts/SystemFonts.java +++ b/graphics/java/android/graphics/fonts/SystemFonts.java @@ -23,6 +23,7 @@ import android.graphics.Typeface; import android.text.FontConfig; import android.util.ArrayMap; import android.util.Log; +import android.util.SparseIntArray; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; @@ -92,9 +93,8 @@ public final class SystemFonts { } private static void pushFamilyToFallback(@NonNull FontConfig.FontFamily xmlFamily, - @NonNull ArrayMap<String, ArrayList<FontFamily>> fallbackMap, + @NonNull ArrayMap<String, NativeFamilyListSet> fallbackMap, @NonNull Map<String, ByteBuffer> cache) { - final String languageTags = xmlFamily.getLocaleList().toLanguageTags(); final int variant = xmlFamily.getVariant(); @@ -117,27 +117,31 @@ public final class SystemFonts { } } - final FontFamily defaultFamily = defaultFonts.isEmpty() ? null : createFontFamily( - xmlFamily.getName(), defaultFonts, languageTags, variant, cache); + final FontFamily defaultFamily = defaultFonts.isEmpty() ? null : createFontFamily( + defaultFonts, languageTags, variant, false, cache); // Insert family into fallback map. for (int i = 0; i < fallbackMap.size(); i++) { - String name = fallbackMap.keyAt(i); + final String name = fallbackMap.keyAt(i); + final NativeFamilyListSet familyListSet = fallbackMap.valueAt(i); + int identityHash = System.identityHashCode(xmlFamily); + if (familyListSet.seenXmlFamilies.get(identityHash, -1) != -1) { + continue; + } else { + familyListSet.seenXmlFamilies.append(identityHash, 1); + } final ArrayList<FontConfig.Font> fallback = specificFallbackFonts.get(name); if (fallback == null) { - String familyName = xmlFamily.getName(); - if (defaultFamily != null - // do not add myself to the fallback chain. - && (familyName == null || !familyName.equals(name))) { - fallbackMap.valueAt(i).add(defaultFamily); + if (defaultFamily != null) { + familyListSet.familyList.add(defaultFamily); } } else { - final FontFamily family = createFontFamily( - xmlFamily.getName(), fallback, languageTags, variant, cache); + final FontFamily family = createFontFamily(fallback, languageTags, variant, false, + cache); if (family != null) { - fallbackMap.valueAt(i).add(family); + familyListSet.familyList.add(family); } else if (defaultFamily != null) { - fallbackMap.valueAt(i).add(defaultFamily); + familyListSet.familyList.add(defaultFamily); } else { // There is no valid for for default fallback. Ignore. } @@ -145,10 +149,11 @@ public final class SystemFonts { } } - private static @Nullable FontFamily createFontFamily(@NonNull String familyName, + private static @Nullable FontFamily createFontFamily( @NonNull List<FontConfig.Font> fonts, @NonNull String languageTags, @FontConfig.FontFamily.Variant int variant, + boolean isDefaultFallback, @NonNull Map<String, ByteBuffer> cache) { if (fonts.size() == 0) { return null; @@ -188,23 +193,30 @@ public final class SystemFonts { b.addFont(font); } } - return b == null ? null : b.build(languageTags, variant, false /* isCustomFallback */); + return b == null ? null : b.build(languageTags, variant, false /* isCustomFallback */, + isDefaultFallback); } - private static void appendNamedFamily(@NonNull FontConfig.FontFamily xmlFamily, + private static void appendNamedFamilyList(@NonNull FontConfig.NamedFamilyList namedFamilyList, @NonNull ArrayMap<String, ByteBuffer> bufferCache, - @NonNull ArrayMap<String, ArrayList<FontFamily>> fallbackListMap) { - final String familyName = xmlFamily.getName(); - final FontFamily family = createFontFamily( - familyName, xmlFamily.getFontList(), - xmlFamily.getLocaleList().toLanguageTags(), xmlFamily.getVariant(), - bufferCache); - if (family == null) { - return; + @NonNull ArrayMap<String, NativeFamilyListSet> fallbackListMap) { + final String familyName = namedFamilyList.getName(); + final NativeFamilyListSet familyListSet = new NativeFamilyListSet(); + final List<FontConfig.FontFamily> xmlFamilies = namedFamilyList.getFamilies(); + for (int i = 0; i < xmlFamilies.size(); ++i) { + FontConfig.FontFamily xmlFamily = xmlFamilies.get(i); + final FontFamily family = createFontFamily( + xmlFamily.getFontList(), + xmlFamily.getLocaleList().toLanguageTags(), xmlFamily.getVariant(), + true, // named family is always default + bufferCache); + if (family == null) { + return; + } + familyListSet.familyList.add(family); + familyListSet.seenXmlFamilies.append(System.identityHashCode(xmlFamily), 1); } - final ArrayList<FontFamily> fallback = new ArrayList<>(); - fallback.add(family); - fallbackListMap.put(familyName, fallback); + fallbackListMap.put(familyName, familyListSet); } /** @@ -258,10 +270,12 @@ public final class SystemFonts { updatableFontMap, lastModifiedDate, configVersion); } catch (IOException e) { Log.e(TAG, "Failed to open/read system font configurations.", e); - return new FontConfig(Collections.emptyList(), Collections.emptyList(), 0, 0); + return new FontConfig(Collections.emptyList(), Collections.emptyList(), + Collections.emptyList(), 0, 0); } catch (XmlPullParserException e) { Log.e(TAG, "Failed to parse the system font configuration.", e); - return new FontConfig(Collections.emptyList(), Collections.emptyList(), 0, 0); + return new FontConfig(Collections.emptyList(), Collections.emptyList(), + Collections.emptyList(), 0, 0); } } @@ -274,37 +288,36 @@ public final class SystemFonts { return buildSystemFallback(fontConfig, new ArrayMap<>()); } + private static final class NativeFamilyListSet { + public List<FontFamily> familyList = new ArrayList<>(); + public SparseIntArray seenXmlFamilies = new SparseIntArray(); + } + /** @hide */ @VisibleForTesting public static Map<String, FontFamily[]> buildSystemFallback(FontConfig fontConfig, ArrayMap<String, ByteBuffer> outBufferCache) { - final Map<String, FontFamily[]> fallbackMap = new ArrayMap<>(); - final List<FontConfig.FontFamily> xmlFamilies = fontConfig.getFontFamilies(); - final ArrayMap<String, ArrayList<FontFamily>> fallbackListMap = new ArrayMap<>(); - // First traverse families which have a 'name' attribute to create fallback map. - for (final FontConfig.FontFamily xmlFamily : xmlFamilies) { - final String familyName = xmlFamily.getName(); - if (familyName == null) { - continue; - } - appendNamedFamily(xmlFamily, outBufferCache, fallbackListMap); + final ArrayMap<String, NativeFamilyListSet> fallbackListMap = new ArrayMap<>(); + + final List<FontConfig.NamedFamilyList> namedFamilies = fontConfig.getNamedFamilyLists(); + for (int i = 0; i < namedFamilies.size(); ++i) { + FontConfig.NamedFamilyList namedFamilyList = namedFamilies.get(i); + appendNamedFamilyList(namedFamilyList, outBufferCache, fallbackListMap); } - // Then, add fallback fonts to the each fallback map. + // Then, add fallback fonts to the fallback map. + final List<FontConfig.FontFamily> xmlFamilies = fontConfig.getFontFamilies(); for (int i = 0; i < xmlFamilies.size(); i++) { final FontConfig.FontFamily xmlFamily = xmlFamilies.get(i); - // The first family (usually the sans-serif family) is always placed immediately - // after the primary family in the fallback. - if (i == 0 || xmlFamily.getName() == null) { - pushFamilyToFallback(xmlFamily, fallbackListMap, outBufferCache); - } + pushFamilyToFallback(xmlFamily, fallbackListMap, outBufferCache); } // Build the font map and fallback map. + final Map<String, FontFamily[]> fallbackMap = new ArrayMap<>(); for (int i = 0; i < fallbackListMap.size(); i++) { final String fallbackName = fallbackListMap.keyAt(i); - final List<FontFamily> familyList = fallbackListMap.valueAt(i); + final List<FontFamily> familyList = fallbackListMap.valueAt(i).familyList; fallbackMap.put(fallbackName, familyList.toArray(new FontFamily[0])); } diff --git a/graphics/java/android/graphics/text/GraphemeBreak.java b/graphics/java/android/graphics/text/GraphemeBreak.java new file mode 100644 index 000000000000..f82b2fd659cc --- /dev/null +++ b/graphics/java/android/graphics/text/GraphemeBreak.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2023 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.text; + +/** @hide */ +public class GraphemeBreak { + private GraphemeBreak() { } + + /** + * Util method that checks if the offsets in given range are grapheme break. + * + * @param advances the advances of characters in the given text. It contains the font + * information used by the algorithm to determine the grapheme break. It's useful + * when some character is missing in the font. For example, if the smile emoji + * "0xD83D 0xDE0A" is not found in the font and is displayed as 2 characters. + * We can't treat it as a single grapheme cluster. + * @param text the text to be processed. + * @param start the start offset of the queried range, inclusive. + * @param end the end offset of the queried range, exclusive. + * @param isGraphemeBreak the array to receive the result. The i-th element of the + * array will be set to true if the offset (start + i) is a grapheme + * break; otherwise, it will be set to false. + */ + public static void isGraphemeBreak(float[] advances, char[] text, int start, int end, + boolean[] isGraphemeBreak) { + if (start > end) { + throw new IllegalArgumentException("start is greater than end: start = " + start + + " end = " + end); + } + if (advances.length < end) { + throw new IllegalArgumentException("the length of advances is less than end" + + "advances.length = " + advances.length + + " end = " + end); + } + if (isGraphemeBreak.length < end - start) { + throw new IndexOutOfBoundsException("isGraphemeBreak doesn't have enough space to " + + "receive the result, isGraphemeBreak.length: " + isGraphemeBreak.length + + " needed space: " + (end - start)); + } + nIsGraphemeBreak(advances, text, start, end, isGraphemeBreak); + } + + private static native void nIsGraphemeBreak(float[] advances, char[] text, int start, int end, + boolean[] isGraphemeBreak); +} diff --git a/graphics/java/android/graphics/text/LineBreakConfig.java b/graphics/java/android/graphics/text/LineBreakConfig.java index d083e444e996..d0327159b04d 100644 --- a/graphics/java/android/graphics/text/LineBreakConfig.java +++ b/graphics/java/android/graphics/text/LineBreakConfig.java @@ -24,29 +24,32 @@ import java.lang.annotation.RetentionPolicy; import java.util.Objects; /** - * Indicates the strategies can be used when calculating the text wrapping. + * Specifies the line-break strategies for text wrapping. * - * See <a href="https://www.w3.org/TR/css-text-3/#line-break-property">the line-break property</a> + * <p>See the + * <a href="https://www.w3.org/TR/css-text-3/#line-break-property" class="external"> + * line-break property</a> for more information. */ public final class LineBreakConfig { /** - * No line break style specified. + * No line-break rules are used for line breaking. */ public static final int LINE_BREAK_STYLE_NONE = 0; /** - * Use the least restrictive rule for line-breaking. This is usually used for short lines. + * The least restrictive line-break rules are used for line breaking. This + * setting is typically used for short lines. */ public static final int LINE_BREAK_STYLE_LOOSE = 1; /** - * Indicate breaking text with the most comment set of line-breaking rules. + * The most common line-break rules are used for line breaking. */ public static final int LINE_BREAK_STYLE_NORMAL = 2; /** - * Indicates breaking text with the most strictest line-breaking rules. + * The most strict line-break rules are used for line breaking. */ public static final int LINE_BREAK_STYLE_STRICT = 3; @@ -59,15 +62,17 @@ public final class LineBreakConfig { public @interface LineBreakStyle {} /** - * No line break word style specified. + * No line-break word style is used for line breaking. */ public static final int LINE_BREAK_WORD_STYLE_NONE = 0; /** - * Indicates the line breaking is based on the phrased. This makes text wrapping only on - * meaningful words. The support of the text wrapping word style varies depending on the - * locales. If the locale does not support the phrase based text wrapping, - * there will be no effect. + * Line breaking is based on phrases, which results in text wrapping only on + * meaningful words. + * + * <p>Support for this line-break word style depends on locale. If the + * current locale does not support phrase-based text wrapping, this setting + * has no effect. */ public static final int LINE_BREAK_WORD_STYLE_PHRASE = 1; @@ -79,7 +84,7 @@ public final class LineBreakConfig { public @interface LineBreakWordStyle {} /** - * A builder for creating {@link LineBreakConfig}. + * A builder for creating a {@code LineBreakConfig} instance. */ public static final class Builder { // The line break style for the LineBreakConfig. @@ -89,17 +94,22 @@ public final class LineBreakConfig { private @LineBreakWordStyle int mLineBreakWordStyle = LineBreakConfig.LINE_BREAK_WORD_STYLE_NONE; + // Whether or not enabling phrase breaking automatically. + // TODO(b/226012260): Remove this and add LINE_BREAK_WORD_STYLE_PHRASE_AUTO after + // the experiment. + private boolean mAutoPhraseBreaking = false; + /** - * Builder constructor with line break parameters. + * Builder constructor. */ public Builder() { } /** - * Set the line break style. + * Sets the line-break style. * - * @param lineBreakStyle the new line break style. - * @return this Builder + * @param lineBreakStyle The new line-break style. + * @return This {@code Builder}. */ public @NonNull Builder setLineBreakStyle(@LineBreakStyle int lineBreakStyle) { mLineBreakStyle = lineBreakStyle; @@ -107,10 +117,10 @@ public final class LineBreakConfig { } /** - * Set the line break word style. + * Sets the line-break word style. * - * @param lineBreakWordStyle the new line break word style. - * @return this Builder + * @param lineBreakWordStyle The new line-break word style. + * @return This {@code Builder}. */ public @NonNull Builder setLineBreakWordStyle(@LineBreakWordStyle int lineBreakWordStyle) { mLineBreakWordStyle = lineBreakWordStyle; @@ -118,28 +128,56 @@ public final class LineBreakConfig { } /** - * Build the {@link LineBreakConfig} + * Enables or disables the automation of {@link LINE_BREAK_WORD_STYLE_PHRASE}. + * + * @hide + */ + public @NonNull Builder setAutoPhraseBreaking(boolean autoPhraseBreaking) { + mAutoPhraseBreaking = autoPhraseBreaking; + return this; + } + + /** + * Builds a {@link LineBreakConfig} instance. * - * @return the LineBreakConfig instance. + * @return The {@code LineBreakConfig} instance. */ public @NonNull LineBreakConfig build() { - return new LineBreakConfig(mLineBreakStyle, mLineBreakWordStyle); + return new LineBreakConfig(mLineBreakStyle, mLineBreakWordStyle, mAutoPhraseBreaking); } } /** + * Creates a {@code LineBreakConfig} instance with the provided line break + * parameters. + * + * @param lineBreakStyle The line-break style for text wrapping. + * @param lineBreakWordStyle The line-break word style for text wrapping. + * @return The {@code LineBreakConfig} instance. + * @hide + */ + public static @NonNull LineBreakConfig getLineBreakConfig(@LineBreakStyle int lineBreakStyle, + @LineBreakWordStyle int lineBreakWordStyle) { + LineBreakConfig.Builder builder = new LineBreakConfig.Builder(); + return builder.setLineBreakStyle(lineBreakStyle) + .setLineBreakWordStyle(lineBreakWordStyle) + .build(); + } + + /** * Create the LineBreakConfig instance. * * @param lineBreakStyle the line break style for text wrapping. * @param lineBreakWordStyle the line break word style for text wrapping. - * @return the {@link LineBreakConfig} instance. + * @return the {@link LineBreakConfig} instance. * * @hide */ public static @NonNull LineBreakConfig getLineBreakConfig(@LineBreakStyle int lineBreakStyle, - @LineBreakWordStyle int lineBreakWordStyle) { + @LineBreakWordStyle int lineBreakWordStyle, boolean autoPhraseBreaking) { LineBreakConfig.Builder builder = new LineBreakConfig.Builder(); return builder.setLineBreakStyle(lineBreakStyle) .setLineBreakWordStyle(lineBreakWordStyle) + .setAutoPhraseBreaking(autoPhraseBreaking) .build(); } @@ -150,35 +188,50 @@ public final class LineBreakConfig { private final @LineBreakStyle int mLineBreakStyle; private final @LineBreakWordStyle int mLineBreakWordStyle; + private final boolean mAutoPhraseBreaking; /** - * Constructor with the line break parameters. - * Use the {@link LineBreakConfig.Builder} to create the LineBreakConfig instance. + * Constructor with line-break parameters. + * + * <p>Use {@link LineBreakConfig.Builder} to create the + * {@code LineBreakConfig} instance. */ private LineBreakConfig(@LineBreakStyle int lineBreakStyle, - @LineBreakWordStyle int lineBreakWordStyle) { + @LineBreakWordStyle int lineBreakWordStyle, boolean autoPhraseBreaking) { mLineBreakStyle = lineBreakStyle; mLineBreakWordStyle = lineBreakWordStyle; + mAutoPhraseBreaking = autoPhraseBreaking; } /** - * Get the line break style. + * Gets the current line-break style. * - * @return The current line break style to be used for the text wrapping. + * @return The line-break style to be used for text wrapping. */ public @LineBreakStyle int getLineBreakStyle() { return mLineBreakStyle; } /** - * Get the line break word style. + * Gets the current line-break word style. * - * @return The current line break word style to be used for the text wrapping. + * @return The line-break word style to be used for text wrapping. */ public @LineBreakWordStyle int getLineBreakWordStyle() { return mLineBreakWordStyle; } + /** + * Used to identify if the automation of {@link LINE_BREAK_WORD_STYLE_PHRASE} is enabled. + * + * @return The result that records whether or not the automation of + * {@link LINE_BREAK_WORD_STYLE_PHRASE} is enabled. + * @hide + */ + public boolean getAutoPhraseBreaking() { + return mAutoPhraseBreaking; + } + @Override public boolean equals(Object o) { if (o == null) return false; @@ -186,7 +239,8 @@ public final class LineBreakConfig { if (!(o instanceof LineBreakConfig)) return false; LineBreakConfig that = (LineBreakConfig) o; return (mLineBreakStyle == that.mLineBreakStyle) - && (mLineBreakWordStyle == that.mLineBreakWordStyle); + && (mLineBreakWordStyle == that.mLineBreakWordStyle) + && (mAutoPhraseBreaking == that.mAutoPhraseBreaking); } @Override diff --git a/graphics/java/android/view/PixelCopy.java b/graphics/java/android/view/PixelCopy.java index 2797a4daa925..0e198d5c56ec 100644 --- a/graphics/java/android/view/PixelCopy.java +++ b/graphics/java/android/view/PixelCopy.java @@ -19,13 +19,17 @@ package android.view; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.graphics.Bitmap; +import android.graphics.HardwareRenderer; import android.graphics.Rect; import android.os.Handler; import android.view.ViewTreeObserver.OnDrawListener; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.concurrent.Executor; +import java.util.function.Consumer; /** * Provides a mechanisms to issue pixel copy requests to allow for copy @@ -183,12 +187,10 @@ public final class PixelCopy { if (srcRect != null && srcRect.isEmpty()) { throw new IllegalArgumentException("sourceRect is empty"); } - // TODO: Make this actually async and fast and cool and stuff - int result = ThreadedRenderer.copySurfaceInto(source, srcRect, dest); - listenerThread.post(new Runnable() { + HardwareRenderer.copySurfaceInto(source, new HardwareRenderer.CopyRequest(srcRect, dest) { @Override - public void run() { - listener.onPixelCopyFinished(result); + public void onCopyFinished(int result) { + listenerThread.post(() -> listener.onPixelCopyFinished(result)); } }); } @@ -255,6 +257,26 @@ public final class PixelCopy { @NonNull Bitmap dest, @NonNull OnPixelCopyFinishedListener listener, @NonNull Handler listenerThread) { validateBitmapDest(dest); + final Rect insets = new Rect(); + final Surface surface = sourceForWindow(source, insets); + request(surface, adjustSourceRectForInsets(insets, srcRect), dest, listener, + listenerThread); + } + + private static void validateBitmapDest(Bitmap bitmap) { + // TODO: Pre-check max texture dimens if we can + if (bitmap == null) { + throw new IllegalArgumentException("Bitmap cannot be null"); + } + if (bitmap.isRecycled()) { + throw new IllegalArgumentException("Bitmap is recycled"); + } + if (!bitmap.isMutable()) { + throw new IllegalArgumentException("Bitmap is immutable"); + } + } + + private static Surface sourceForWindow(Window source, Rect outInsets) { if (source == null) { throw new IllegalArgumentException("source is null"); } @@ -267,31 +289,268 @@ public final class PixelCopy { if (root != null) { surface = root.mSurface; final Rect surfaceInsets = root.mWindowAttributes.surfaceInsets; - if (srcRect == null) { - srcRect = new Rect(surfaceInsets.left, surfaceInsets.top, - root.mWidth + surfaceInsets.left, root.mHeight + surfaceInsets.top); - } else { - srcRect.offset(surfaceInsets.left, surfaceInsets.top); - } + outInsets.set(surfaceInsets.left, surfaceInsets.top, + root.mWidth + surfaceInsets.left, root.mHeight + surfaceInsets.top); } if (surface == null || !surface.isValid()) { throw new IllegalArgumentException( "Window doesn't have a backing surface!"); } - request(surface, srcRect, dest, listener, listenerThread); + return surface; } - private static void validateBitmapDest(Bitmap bitmap) { - // TODO: Pre-check max texture dimens if we can - if (bitmap == null) { - throw new IllegalArgumentException("Bitmap cannot be null"); + private static Rect adjustSourceRectForInsets(Rect insets, Rect srcRect) { + if (srcRect == null) { + return insets; } - if (bitmap.isRecycled()) { - throw new IllegalArgumentException("Bitmap is recycled"); + if (insets != null) { + srcRect.offset(insets.left, insets.top); } - if (!bitmap.isMutable()) { - throw new IllegalArgumentException("Bitmap is immutable"); + return srcRect; + } + + /** + * Contains the result of a PixelCopy request + */ + public static final class Result { + private int mStatus; + private Bitmap mBitmap; + + private Result(@CopyResultStatus int status, Bitmap bitmap) { + mStatus = status; + mBitmap = bitmap; + } + + /** + * Returns the {@link CopyResultStatus} of the copy request. + */ + public @CopyResultStatus int getStatus() { + return mStatus; } + + private void validateStatus() { + if (mStatus != SUCCESS) { + throw new IllegalStateException("Copy request didn't succeed, status = " + mStatus); + } + } + + /** + * If the PixelCopy {@link Request} was given a destination bitmap with + * {@link Request.Builder#setDestinationBitmap(Bitmap)} then the returned bitmap will be + * the same as the one given. If no destination bitmap was provided, then this + * will contain the automatically allocated Bitmap to hold the result. + * + * @return the Bitmap the copy request was stored in. + * @throws IllegalStateException if {@link #getStatus()} is not SUCCESS + */ + public @NonNull Bitmap getBitmap() { + validateStatus(); + return mBitmap; + } + } + + /** + * Represents a PixelCopy request. + * + * To create a copy request, use either of the PixelCopy.Request.ofWindow or + * PixelCopy.Request.ofSurface factories to create a {@link Request.Builder} for the + * given source content. After setting any optional parameters, such as + * {@link Builder#setSourceRect(Rect)}, build the request with {@link Builder#build()} and + * then execute it with {@link PixelCopy#request(Request, Executor, Consumer)} + */ + public static final class Request { + private final Surface mSource; + private final Rect mSourceInsets; + private Rect mSrcRect; + private Bitmap mDest; + + private Request(Surface source, Rect sourceInsets) { + this.mSource = source; + this.mSourceInsets = sourceInsets; + } + + /** + * A builder to create the complete PixelCopy request, which is then executed by calling + * {@link #request(Request, Executor, Consumer)} with the built request returned from + * {@link #build()} + */ + public static final class Builder { + private Request mRequest; + + private Builder(Request request) { + mRequest = request; + } + + /** + * Creates a PixelCopy Builder for the given {@link Window} + * @param source The Window to copy from + * @return A {@link Builder} builder to set the optional params & build the request + */ + @SuppressLint("BuilderSetStyle") + public static @NonNull Builder ofWindow(@NonNull Window source) { + final Rect insets = new Rect(); + final Surface surface = sourceForWindow(source, insets); + return new Builder(new Request(surface, insets)); + } + + /** + * Creates a PixelCopy Builder for the {@link Window} that the given {@link View} is + * attached to. + * + * Note that this copy request is not cropped to the area the View occupies by default. + * If that behavior is desired, use {@link View#getLocationInWindow(int[])} combined + * with {@link Builder#setSourceRect(Rect)} to set a crop area to restrict the copy + * operation. + * + * @param source A View that {@link View#isAttachedToWindow() is attached} to a window + * that will be used to retrieve the window to copy from. + * @return A {@link Builder} builder to set the optional params & build the request + */ + @SuppressLint("BuilderSetStyle") + public static @NonNull Builder ofWindow(@NonNull View source) { + if (source == null || !source.isAttachedToWindow()) { + throw new IllegalArgumentException( + "View must not be null & must be attached to window"); + } + final Rect insets = new Rect(); + Surface surface = null; + final ViewRootImpl root = source.getViewRootImpl(); + if (root != null) { + surface = root.mSurface; + insets.set(root.mWindowAttributes.surfaceInsets); + } + if (surface == null || !surface.isValid()) { + throw new IllegalArgumentException( + "Window doesn't have a backing surface!"); + } + return new Builder(new Request(surface, insets)); + } + + /** + * Creates a PixelCopy Builder for the given {@link Surface} + * + * @param source The Surface to copy from. Must be {@link Surface#isValid() valid}. + * @return A {@link Builder} builder to set the optional params & build the request + */ + @SuppressLint("BuilderSetStyle") + public static @NonNull Builder ofSurface(@NonNull Surface source) { + if (source == null || !source.isValid()) { + throw new IllegalArgumentException("Source must not be null & must be valid"); + } + return new Builder(new Request(source, null)); + } + + /** + * Creates a PixelCopy Builder for the {@link Surface} belonging to the + * given {@link SurfaceView} + * + * @param source The SurfaceView to copy from. The backing surface must be + * {@link Surface#isValid() valid} + * @return A {@link Builder} builder to set the optional params & build the request + */ + @SuppressLint("BuilderSetStyle") + public static @NonNull Builder ofSurface(@NonNull SurfaceView source) { + return ofSurface(source.getHolder().getSurface()); + } + + private void requireNotBuilt() { + if (mRequest == null) { + throw new IllegalStateException("build() already called on this builder"); + } + } + + /** + * Sets the region of the source to copy from. By default, the entire source is copied + * to the output. If only a subset of the source is necessary to be copied, specifying + * a srcRect will improve performance by reducing + * the amount of data being copied. + * + * @param srcRect The area of the source to read from. Null or empty will be treated to + * mean the entire source + * @return this + */ + public @NonNull Builder setSourceRect(@Nullable Rect srcRect) { + requireNotBuilt(); + mRequest.mSrcRect = srcRect; + return this; + } + + /** + * Specifies the output bitmap in which to store the result. By default, a Bitmap of + * format {@link android.graphics.Bitmap.Config#ARGB_8888} with a width & height + * matching that of the {@link #setSourceRect(Rect) source area} will be created to + * place the result. + * + * @param destination The bitmap to store the result, or null to have a bitmap + * automatically created of the appropriate size. If not null, must + * not be {@link Bitmap#isRecycled() recycled} and must be + * {@link Bitmap#isMutable() mutable}. + * @return this + */ + public @NonNull Builder setDestinationBitmap(@Nullable Bitmap destination) { + requireNotBuilt(); + if (destination != null) { + validateBitmapDest(destination); + } + mRequest.mDest = destination; + return this; + } + + /** + * @return The built {@link PixelCopy.Request} + */ + public @NonNull Request build() { + requireNotBuilt(); + Request ret = mRequest; + mRequest = null; + return ret; + } + } + + /** + * @return The destination bitmap as set by {@link Builder#setDestinationBitmap(Bitmap)} + */ + public @Nullable Bitmap getDestinationBitmap() { + return mDest; + } + + /** + * @return The source rect to copy from as set by {@link Builder#setSourceRect(Rect)} + */ + public @Nullable Rect getSourceRect() { + return mSrcRect; + } + + /** + * @hide + */ + public void request(@NonNull Executor callbackExecutor, + @NonNull Consumer<Result> listener) { + if (!mSource.isValid()) { + callbackExecutor.execute(() -> listener.accept( + new Result(ERROR_SOURCE_INVALID, null))); + return; + } + HardwareRenderer.copySurfaceInto(mSource, new HardwareRenderer.CopyRequest( + adjustSourceRectForInsets(mSourceInsets, mSrcRect), mDest) { + @Override + public void onCopyFinished(int result) { + callbackExecutor.execute(() -> listener.accept( + new Result(result, mDestinationBitmap))); + } + }); + } + } + + /** + * Executes the pixel copy request + * @param request The request to execute + * @param callbackExecutor The executor to run the callback on + * @param listener The callback for when the copy request is completed + */ + public static void request(@NonNull Request request, @NonNull Executor callbackExecutor, + @NonNull Consumer<Result> listener) { + request.request(callbackExecutor, listener); } private PixelCopy() {} |