summaryrefslogtreecommitdiff
path: root/graphics
diff options
context:
space:
mode:
Diffstat (limited to 'graphics')
-rw-r--r--graphics/java/android/graphics/BLASTBufferQueue.java24
-rw-r--r--graphics/java/android/graphics/BaseCanvas.java41
-rw-r--r--graphics/java/android/graphics/BaseRecordingCanvas.java26
-rw-r--r--graphics/java/android/graphics/Bitmap.java235
-rw-r--r--graphics/java/android/graphics/BitmapFactory.java20
-rw-r--r--graphics/java/android/graphics/BitmapShader.java52
-rw-r--r--graphics/java/android/graphics/Canvas.java2
-rw-r--r--graphics/java/android/graphics/ColorSpace.java249
-rw-r--r--graphics/java/android/graphics/FontListParser.java124
-rw-r--r--graphics/java/android/graphics/Gainmap.java364
-rw-r--r--graphics/java/android/graphics/GraphicBuffer.java5
-rw-r--r--graphics/java/android/graphics/HardwareBufferRenderer.java401
-rw-r--r--graphics/java/android/graphics/HardwareRenderer.java238
-rw-r--r--graphics/java/android/graphics/ImageDecoder.java166
-rw-r--r--graphics/java/android/graphics/ImageFormat.java24
-rw-r--r--graphics/java/android/graphics/Mesh.java385
-rw-r--r--graphics/java/android/graphics/MeshSpecification.java406
-rw-r--r--graphics/java/android/graphics/Paint.java125
-rw-r--r--graphics/java/android/graphics/Path.java198
-rw-r--r--graphics/java/android/graphics/PathIterator.java297
-rw-r--r--graphics/java/android/graphics/RenderNode.java13
-rw-r--r--graphics/java/android/graphics/RuntimeShader.java9
-rw-r--r--graphics/java/android/graphics/Typeface.java161
-rw-r--r--graphics/java/android/graphics/YuvImage.java208
-rw-r--r--graphics/java/android/graphics/drawable/Icon.java56
-rw-r--r--graphics/java/android/graphics/drawable/RippleDrawable.java15
-rw-r--r--graphics/java/android/graphics/fonts/Font.java2
-rw-r--r--graphics/java/android/graphics/fonts/FontCustomizationParser.java64
-rw-r--r--graphics/java/android/graphics/fonts/FontFamily.java10
-rw-r--r--graphics/java/android/graphics/fonts/FontStyle.java4
-rw-r--r--graphics/java/android/graphics/fonts/SystemFonts.java107
-rw-r--r--graphics/java/android/graphics/text/GraphemeBreak.java59
-rw-r--r--graphics/java/android/graphics/text/LineBreakConfig.java118
-rw-r--r--graphics/java/android/view/PixelCopy.java299
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() {}