summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/com/android/internal/widget/remotecompose/core/CompanionOperation.java30
-rw-r--r--core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java494
-rw-r--r--core/java/com/android/internal/widget/remotecompose/core/Operation.java40
-rw-r--r--core/java/com/android/internal/widget/remotecompose/core/Operations.java65
-rw-r--r--core/java/com/android/internal/widget/remotecompose/core/PaintContext.java40
-rw-r--r--core/java/com/android/internal/widget/remotecompose/core/PaintOperation.java38
-rw-r--r--core/java/com/android/internal/widget/remotecompose/core/Platform.java24
-rw-r--r--core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java317
-rw-r--r--core/java/com/android/internal/widget/remotecompose/core/RemoteComposeOperation.java20
-rw-r--r--core/java/com/android/internal/widget/remotecompose/core/RemoteComposeState.java100
-rw-r--r--core/java/com/android/internal/widget/remotecompose/core/RemoteContext.java154
-rw-r--r--core/java/com/android/internal/widget/remotecompose/core/WireBuffer.java254
-rw-r--r--core/java/com/android/internal/widget/remotecompose/core/operations/BitmapData.java104
-rw-r--r--core/java/com/android/internal/widget/remotecompose/core/operations/ClickArea.java132
-rw-r--r--core/java/com/android/internal/widget/remotecompose/core/operations/DrawBitmapInt.java132
-rw-r--r--core/java/com/android/internal/widget/remotecompose/core/operations/Header.java127
-rw-r--r--core/java/com/android/internal/widget/remotecompose/core/operations/RootContentBehavior.java234
-rw-r--r--core/java/com/android/internal/widget/remotecompose/core/operations/RootContentDescription.java89
-rw-r--r--core/java/com/android/internal/widget/remotecompose/core/operations/TextData.java87
-rw-r--r--core/java/com/android/internal/widget/remotecompose/core/operations/Theme.java97
-rw-r--r--core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntMap.java115
-rw-r--r--core/java/com/android/internal/widget/remotecompose/player/RemoteComposeDocument.java93
-rw-r--r--core/java/com/android/internal/widget/remotecompose/player/RemoteComposePlayer.java177
-rw-r--r--core/java/com/android/internal/widget/remotecompose/player/platform/AndroidPaintContext.java93
-rw-r--r--core/java/com/android/internal/widget/remotecompose/player/platform/AndroidRemoteContext.java87
-rw-r--r--core/java/com/android/internal/widget/remotecompose/player/platform/ClickAreaView.java66
-rw-r--r--core/java/com/android/internal/widget/remotecompose/player/platform/RemoteComposeCanvas.java230
27 files changed, 3439 insertions, 0 deletions
diff --git a/core/java/com/android/internal/widget/remotecompose/core/CompanionOperation.java b/core/java/com/android/internal/widget/remotecompose/core/CompanionOperation.java
new file mode 100644
index 000000000000..ce8ca0d781e4
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/CompanionOperation.java
@@ -0,0 +1,30 @@
+/*
+ * 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 com.android.internal.widget.remotecompose.core;
+
+import java.util.List;
+
+/**
+ * Interface for the companion operations
+ */
+public interface CompanionOperation {
+ void read(WireBuffer buffer, List<Operation> operations);
+
+ // Debugging / Documentation utility functions
+ String name();
+ int id();
+}
+
diff --git a/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java b/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java
new file mode 100644
index 000000000000..0e4c7430afb5
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java
@@ -0,0 +1,494 @@
+/*
+ * 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 com.android.internal.widget.remotecompose.core;
+
+import com.android.internal.widget.remotecompose.core.operations.RootContentBehavior;
+import com.android.internal.widget.remotecompose.core.operations.Theme;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Represents a platform independent RemoteCompose document,
+ * containing RemoteCompose operations + state
+ */
+public class CoreDocument {
+
+ ArrayList<Operation> mOperations;
+ RemoteComposeState mRemoteComposeState = new RemoteComposeState();
+
+ // Semantic version of the document
+ Version mVersion = new Version(0, 1, 0);
+
+ String mContentDescription; // text description of the document (used for accessibility)
+
+ long mRequiredCapabilities = 0L; // bitmask indicating needed capabilities of the player(unused)
+ int mWidth = 0; // horizontal dimension of the document in pixels
+ int mHeight = 0; // vertical dimension of the document in pixels
+
+ int mContentScroll = RootContentBehavior.NONE;
+ int mContentSizing = RootContentBehavior.NONE;
+ int mContentMode = RootContentBehavior.NONE;
+
+ int mContentAlignment = RootContentBehavior.ALIGNMENT_CENTER;
+
+ RemoteComposeBuffer mBuffer = new RemoteComposeBuffer(mRemoteComposeState);
+
+ public String getContentDescription() {
+ return mContentDescription;
+ }
+
+ public void setContentDescription(String contentDescription) {
+ this.mContentDescription = contentDescription;
+ }
+
+ public long getRequiredCapabilities() {
+ return mRequiredCapabilities;
+ }
+
+ public void setRequiredCapabilities(long requiredCapabilities) {
+ this.mRequiredCapabilities = requiredCapabilities;
+ }
+
+ public int getWidth() {
+ return mWidth;
+ }
+
+ public void setWidth(int width) {
+ this.mWidth = width;
+ }
+
+ public int getHeight() {
+ return mHeight;
+ }
+
+ public void setHeight(int height) {
+ this.mHeight = height;
+ }
+
+ public RemoteComposeBuffer getBuffer() {
+ return mBuffer;
+ }
+
+ public void setBuffer(RemoteComposeBuffer buffer) {
+ this.mBuffer = buffer;
+ }
+
+ public RemoteComposeState getRemoteComposeState() {
+ return mRemoteComposeState;
+ }
+
+ public void setRemoteComposeState(RemoteComposeState remoteComposeState) {
+ this.mRemoteComposeState = remoteComposeState;
+ }
+
+ public int getContentScroll() {
+ return mContentScroll;
+ }
+
+ public int getContentSizing() {
+ return mContentSizing;
+ }
+
+ public int getContentMode() {
+ return mContentMode;
+ }
+
+ /**
+ * Sets the way the player handles the content
+ *
+ * @param scroll set the horizontal behavior (NONE|SCROLL_HORIZONTAL|SCROLL_VERTICAL)
+ * @param alignment set the alignment of the content (TOP|CENTER|BOTTOM|START|END)
+ * @param sizing set the type of sizing for the content (NONE|SIZING_LAYOUT|SIZING_SCALE)
+ * @param mode set the mode of sizing, either LAYOUT modes or SCALE modes
+ * the LAYOUT modes are:
+ * - LAYOUT_MATCH_PARENT
+ * - LAYOUT_WRAP_CONTENT
+ * or adding an horizontal mode and a vertical mode:
+ * - LAYOUT_HORIZONTAL_MATCH_PARENT
+ * - LAYOUT_HORIZONTAL_WRAP_CONTENT
+ * - LAYOUT_HORIZONTAL_FIXED
+ * - LAYOUT_VERTICAL_MATCH_PARENT
+ * - LAYOUT_VERTICAL_WRAP_CONTENT
+ * - LAYOUT_VERTICAL_FIXED
+ * The LAYOUT_*_FIXED modes will use the intrinsic document size
+ */
+ public void setRootContentBehavior(int scroll, int alignment, int sizing, int mode) {
+ this.mContentScroll = scroll;
+ this.mContentAlignment = alignment;
+ this.mContentSizing = sizing;
+ this.mContentMode = mode;
+ }
+
+ /**
+ * Given dimensions w x h of where to paint the content, returns the corresponding scale factor
+ * according to the contentSizing information
+ *
+ * @param w horizontal dimension of the rendering area
+ * @param h vertical dimension of the rendering area
+ * @param scaleOutput will contain the computed scale factor
+ */
+ public void computeScale(float w, float h, float[] scaleOutput) {
+ float contentScaleX = 1f;
+ float contentScaleY = 1f;
+ if (mContentSizing == RootContentBehavior.SIZING_SCALE) {
+ // we need to add canvas transforms ops here
+ switch (mContentMode) {
+ case RootContentBehavior.SCALE_INSIDE: {
+ float scaleX = w / mWidth;
+ float scaleY = h / mHeight;
+ float scale = Math.min(1f, Math.min(scaleX, scaleY));
+ contentScaleX = scale;
+ contentScaleY = scale;
+ } break;
+ case RootContentBehavior.SCALE_FIT: {
+ float scaleX = w / mWidth;
+ float scaleY = h / mHeight;
+ float scale = Math.min(scaleX, scaleY);
+ contentScaleX = scale;
+ contentScaleY = scale;
+ } break;
+ case RootContentBehavior.SCALE_FILL_WIDTH: {
+ float scale = w / mWidth;
+ contentScaleX = scale;
+ contentScaleY = scale;
+ } break;
+ case RootContentBehavior.SCALE_FILL_HEIGHT: {
+ float scale = h / mHeight;
+ contentScaleX = scale;
+ contentScaleY = scale;
+ } break;
+ case RootContentBehavior.SCALE_CROP: {
+ float scaleX = w / mWidth;
+ float scaleY = h / mHeight;
+ float scale = Math.max(scaleX, scaleY);
+ contentScaleX = scale;
+ contentScaleY = scale;
+ } break;
+ case RootContentBehavior.SCALE_FILL_BOUNDS: {
+ float scaleX = w / mWidth;
+ float scaleY = h / mHeight;
+ contentScaleX = scaleX;
+ contentScaleY = scaleY;
+ } break;
+ default:
+ // nothing
+ }
+ }
+ scaleOutput[0] = contentScaleX;
+ scaleOutput[1] = contentScaleY;
+ }
+
+ /**
+ * Given dimensions w x h of where to paint the content, returns the corresponding translation
+ * according to the contentAlignment information
+ *
+ * @param w horizontal dimension of the rendering area
+ * @param h vertical dimension of the rendering area
+ * @param contentScaleX the horizontal scale we are going to use for the content
+ * @param contentScaleY the vertical scale we are going to use for the content
+ * @param translateOutput will contain the computed translation
+ */
+ private void computeTranslate(float w, float h, float contentScaleX, float contentScaleY,
+ float[] translateOutput) {
+ int horizontalContentAlignment = mContentAlignment & 0xF0;
+ int verticalContentAlignment = mContentAlignment & 0xF;
+ float translateX = 0f;
+ float translateY = 0f;
+ float contentWidth = mWidth * contentScaleX;
+ float contentHeight = mHeight * contentScaleY;
+
+ switch (horizontalContentAlignment) {
+ case RootContentBehavior.ALIGNMENT_START: {
+ // nothing
+ } break;
+ case RootContentBehavior.ALIGNMENT_HORIZONTAL_CENTER: {
+ translateX = (w - contentWidth) / 2f;
+ } break;
+ case RootContentBehavior.ALIGNMENT_END: {
+ translateX = w - contentWidth;
+ } break;
+ default:
+ // nothing (same as alignment_start)
+ }
+ switch (verticalContentAlignment) {
+ case RootContentBehavior.ALIGNMENT_TOP: {
+ // nothing
+ } break;
+ case RootContentBehavior.ALIGNMENT_VERTICAL_CENTER: {
+ translateY = (h - contentHeight) / 2f;
+ } break;
+ case RootContentBehavior.ALIGNMENT_BOTTOM: {
+ translateY = h - contentHeight;
+ } break;
+ default:
+ // nothing (same as alignment_top)
+ }
+
+ translateOutput[0] = translateX;
+ translateOutput[1] = translateY;
+ }
+
+ public Set<ClickAreaRepresentation> getClickAreas() {
+ return mClickAreas;
+ }
+
+ public interface ClickCallbacks {
+ void click(int id, String metadata);
+ }
+
+ HashSet<ClickCallbacks> mClickListeners = new HashSet<>();
+ HashSet<ClickAreaRepresentation> mClickAreas = new HashSet<>();
+
+ static class Version {
+ public final int major;
+ public final int minor;
+ public final int patchLevel;
+
+ Version(int major, int minor, int patchLevel) {
+ this.major = major;
+ this.minor = minor;
+ this.patchLevel = patchLevel;
+ }
+ }
+
+ public static class ClickAreaRepresentation {
+ int mId;
+ String mContentDescription;
+ float mLeft;
+ float mTop;
+ float mRight;
+ float mBottom;
+ String mMetadata;
+
+ public ClickAreaRepresentation(int id,
+ String contentDescription,
+ float left,
+ float top,
+ float right,
+ float bottom,
+ String metadata) {
+ this.mId = id;
+ this.mContentDescription = contentDescription;
+ this.mLeft = left;
+ this.mTop = top;
+ this.mRight = right;
+ this.mBottom = bottom;
+ this.mMetadata = metadata;
+ }
+
+ public boolean contains(float x, float y) {
+ return x >= mLeft && x < mRight
+ && y >= mTop && y < mBottom;
+ }
+
+ public float getLeft() {
+ return mLeft;
+ }
+
+ public float getTop() {
+ return mTop;
+ }
+
+ public float width() {
+ return Math.max(0, mRight - mLeft);
+ }
+
+ public float height() {
+ return Math.max(0, mBottom - mTop);
+ }
+
+ public int getId() {
+ return mId;
+ }
+
+ public String getContentDescription() {
+ return mContentDescription;
+ }
+
+ public String getMetadata() {
+ return mMetadata;
+ }
+ }
+
+ /**
+ * Load operations from the given buffer
+ */
+ public void initFromBuffer(RemoteComposeBuffer buffer) {
+ mOperations = new ArrayList<Operation>();
+ buffer.inflateFromBuffer(mOperations);
+ }
+
+ /**
+ * Called when an initialization is needed, allowing the document to eg load
+ * resources / cache them.
+ */
+ public void initializeContext(RemoteContext context) {
+ mRemoteComposeState.reset();
+ mClickAreas.clear();
+
+ context.mDocument = this;
+ context.mRemoteComposeState = mRemoteComposeState;
+
+ // mark context to be in DATA mode, which will skip the painting ops.
+ context.mMode = RemoteContext.ContextMode.DATA;
+ for (Operation op: mOperations) {
+ op.apply(context);
+ }
+ context.mMode = RemoteContext.ContextMode.UNSET;
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // Document infos
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Returns true if the document can be displayed given this version of the player
+ *
+ * @param majorVersion the max major version supported by the player
+ * @param minorVersion the max minor version supported by the player
+ * @param capabilities a bitmask of capabilities the player supports (unused for now)
+ */
+ public boolean canBeDisplayed(int majorVersion, int minorVersion, long capabilities) {
+ return mVersion.major <= majorVersion && mVersion.minor <= minorVersion;
+ }
+
+ /**
+ * Set the document version, following semantic versioning.
+ *
+ * @param majorVersion major version number, increased upon changes breaking the compatibility
+ * @param minorVersion minor version number, increased when adding new features
+ * @param patch patch level, increased upon bugfixes
+ */
+ void setVersion(int majorVersion, int minorVersion, int patch) {
+ mVersion = new Version(majorVersion, minorVersion, patch);
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // Click handling
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Add a click area to the document, in root coordinates. We are not doing any specific sorting
+ * through the declared areas on click detections, which means that the first one containing
+ * the click coordinates will be the one reported; the order of addition of those click areas
+ * is therefore meaningful.
+ *
+ * @param id the id of the area, which will be reported on click
+ * @param contentDescription the content description (used for accessibility)
+ * @param left the left coordinate of the click area (in pixels)
+ * @param top the top coordinate of the click area (in pixels)
+ * @param right the right coordinate of the click area (in pixels)
+ * @param bottom the bottom coordinate of the click area (in pixels)
+ * @param metadata arbitrary metadata associated with the are, also reported on click
+ */
+ public void addClickArea(int id, String contentDescription,
+ float left, float top, float right, float bottom, String metadata) {
+ mClickAreas.add(new ClickAreaRepresentation(id,
+ contentDescription, left, top, right, bottom, metadata));
+ }
+
+ /**
+ * Add a click listener. This will get called when a click is detected on the document
+ *
+ * @param callback called when a click area has been hit, passing the click are id and metadata.
+ */
+ public void addClickListener(ClickCallbacks callback) {
+ mClickListeners.add(callback);
+ }
+
+ /**
+ * Passing a click event to the document. This will possibly result in calling the click
+ * listeners.
+ */
+ public void onClick(float x, float y) {
+ for (ClickAreaRepresentation clickArea: mClickAreas) {
+ if (clickArea.contains(x, y)) {
+ warnClickListeners(clickArea);
+ }
+ }
+ }
+
+ /**
+ * Programmatically trigger the click response for the given id
+ *
+ * @param id the click area id
+ */
+ public void performClick(int id) {
+ for (ClickAreaRepresentation clickArea: mClickAreas) {
+ if (clickArea.mId == id) {
+ warnClickListeners(clickArea);
+ }
+ }
+ }
+
+ /**
+ * Warn click listeners when a click area is activated
+ */
+ private void warnClickListeners(ClickAreaRepresentation clickArea) {
+ for (ClickCallbacks listener: mClickListeners) {
+ listener.click(clickArea.mId, clickArea.mMetadata);
+ }
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // Painting
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ private final float[] mScaleOutput = new float[2];
+ private final float[] mTranslateOutput = new float[2];
+
+ /**
+ * Paint the document
+ *
+ * @param context the provided PaintContext
+ * @param theme the theme we want to use for this document.
+ */
+ public void paint(RemoteContext context, int theme) {
+ context.mMode = RemoteContext.ContextMode.PAINT;
+
+ // current theme starts as UNSPECIFIED, until a Theme setter
+ // operation gets executed and modify it.
+ context.setTheme(Theme.UNSPECIFIED);
+
+ context.mRemoteComposeState = mRemoteComposeState;
+ if (mContentSizing == RootContentBehavior.SIZING_SCALE) {
+ // we need to add canvas transforms ops here
+ computeScale(context.mWidth, context.mHeight, mScaleOutput);
+ computeTranslate(context.mWidth, context.mHeight,
+ mScaleOutput[0], mScaleOutput[1], mTranslateOutput);
+ context.mPaintContext.translate(mTranslateOutput[0], mTranslateOutput[1]);
+ context.mPaintContext.scale(mScaleOutput[0], mScaleOutput[1]);
+ }
+ for (Operation op : mOperations) {
+ // operations will only be executed if no theme is set (ie UNSPECIFIED)
+ // or the theme is equal as the one passed in argument to paint.
+ boolean apply = true;
+ if (theme != Theme.UNSPECIFIED) {
+ apply = op instanceof Theme // always apply a theme setter
+ || context.getTheme() == theme
+ || context.getTheme() == Theme.UNSPECIFIED;
+ }
+ if (apply) {
+ op.apply(context);
+ }
+ }
+ context.mMode = RemoteContext.ContextMode.UNSET;
+ }
+
+}
+
diff --git a/core/java/com/android/internal/widget/remotecompose/core/Operation.java b/core/java/com/android/internal/widget/remotecompose/core/Operation.java
new file mode 100644
index 000000000000..7cb9a4272704
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/Operation.java
@@ -0,0 +1,40 @@
+/*
+ * 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 com.android.internal.widget.remotecompose.core;
+
+/**
+ * Base interface for RemoteCompose operations
+ */
+public interface Operation {
+
+ /**
+ * add the operation to the buffer
+ */
+ void write(WireBuffer buffer);
+
+ /**
+ * paint an operation
+ *
+ * @param context the paint context used to paint the operation
+ */
+ void apply(RemoteContext context);
+
+ /**
+ * Debug utility to display an operation + indentation
+ */
+ String deepToString(String indent);
+}
+
diff --git a/core/java/com/android/internal/widget/remotecompose/core/Operations.java b/core/java/com/android/internal/widget/remotecompose/core/Operations.java
new file mode 100644
index 000000000000..b8bb1f0f3519
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/Operations.java
@@ -0,0 +1,65 @@
+/*
+ * 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 com.android.internal.widget.remotecompose.core;
+
+import com.android.internal.widget.remotecompose.core.operations.BitmapData;
+import com.android.internal.widget.remotecompose.core.operations.ClickArea;
+import com.android.internal.widget.remotecompose.core.operations.DrawBitmapInt;
+import com.android.internal.widget.remotecompose.core.operations.Header;
+import com.android.internal.widget.remotecompose.core.operations.RootContentBehavior;
+import com.android.internal.widget.remotecompose.core.operations.RootContentDescription;
+import com.android.internal.widget.remotecompose.core.operations.TextData;
+import com.android.internal.widget.remotecompose.core.operations.Theme;
+import com.android.internal.widget.remotecompose.core.operations.utilities.IntMap;
+
+/**
+ * List of operations supported in a RemoteCompose document
+ */
+public class Operations {
+
+ ////////////////////////////////////////
+ // Protocol
+ ////////////////////////////////////////
+ public static final int HEADER = 0;
+ public static final int LOAD_BITMAP = 4;
+ public static final int THEME = 63;
+ public static final int CLICK_AREA = 64;
+ public static final int ROOT_CONTENT_BEHAVIOR = 65;
+ public static final int ROOT_CONTENT_DESCRIPTION = 103;
+
+ ////////////////////////////////////////
+ // Draw commands
+ ////////////////////////////////////////
+ public static final int DRAW_BITMAP = 44;
+ public static final int DRAW_BITMAP_INT = 66;
+ public static final int DATA_BITMAP = 101;
+ public static final int DATA_TEXT = 102;
+
+
+ public static IntMap<CompanionOperation> map = new IntMap<>();
+
+ static {
+ map.put(HEADER, Header.COMPANION);
+ map.put(DRAW_BITMAP_INT, DrawBitmapInt.COMPANION);
+ map.put(DATA_BITMAP, BitmapData.COMPANION);
+ map.put(DATA_TEXT, TextData.COMPANION);
+ map.put(THEME, Theme.COMPANION);
+ map.put(CLICK_AREA, ClickArea.COMPANION);
+ map.put(ROOT_CONTENT_BEHAVIOR, RootContentBehavior.COMPANION);
+ map.put(ROOT_CONTENT_DESCRIPTION, RootContentDescription.COMPANION);
+ }
+
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/PaintContext.java b/core/java/com/android/internal/widget/remotecompose/core/PaintContext.java
new file mode 100644
index 000000000000..6999cdeadfd7
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/PaintContext.java
@@ -0,0 +1,40 @@
+/*
+ * 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 com.android.internal.widget.remotecompose.core;
+
+/**
+ * Specify an abstract paint context used by RemoteCompose commands to draw
+ */
+public abstract class PaintContext {
+ protected RemoteContext mContext;
+
+ public PaintContext(RemoteContext context) {
+ this.mContext = context;
+ }
+
+ public void setContext(RemoteContext context) {
+ this.mContext = context;
+ }
+
+ public abstract void drawBitmap(int imageId,
+ int srcLeft, int srcTop, int srcRight, int srcBottom,
+ int dstLeft, int dstTop, int dstRight, int dstBottom,
+ int cdId);
+
+ public abstract void scale(float scaleX, float scaleY);
+ public abstract void translate(float translateX, float translateY);
+}
+
diff --git a/core/java/com/android/internal/widget/remotecompose/core/PaintOperation.java b/core/java/com/android/internal/widget/remotecompose/core/PaintOperation.java
new file mode 100644
index 000000000000..2f3fe5739b58
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/PaintOperation.java
@@ -0,0 +1,38 @@
+/*
+ * 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 com.android.internal.widget.remotecompose.core;
+
+/**
+ * PaintOperation interface, used for operations aimed at painting
+ * (while any operation _can_ paint, this make it a little more explicit)
+ */
+public abstract class PaintOperation implements Operation {
+
+ @Override
+ public void apply(RemoteContext context) {
+ if (context.getMode() == RemoteContext.ContextMode.PAINT
+ && context.getPaintContext() != null) {
+ paint((PaintContext) context.getPaintContext());
+ }
+ }
+
+ @Override
+ public String deepToString(String indent) {
+ return indent + toString();
+ }
+
+ public abstract void paint(PaintContext context);
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/Platform.java b/core/java/com/android/internal/widget/remotecompose/core/Platform.java
new file mode 100644
index 000000000000..abda0c0d9a0c
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/Platform.java
@@ -0,0 +1,24 @@
+/*
+ * 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 com.android.internal.widget.remotecompose.core;
+
+/**
+ * Services that are needed to be provided by the platform during encoding.
+ */
+public interface Platform {
+ byte[] imageToByteArray(Object image);
+}
+
diff --git a/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java
new file mode 100644
index 000000000000..3ab6c4744fb9
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java
@@ -0,0 +1,317 @@
+/*
+ * 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 com.android.internal.widget.remotecompose.core;
+
+import com.android.internal.widget.remotecompose.core.operations.BitmapData;
+import com.android.internal.widget.remotecompose.core.operations.ClickArea;
+import com.android.internal.widget.remotecompose.core.operations.DrawBitmapInt;
+import com.android.internal.widget.remotecompose.core.operations.Header;
+import com.android.internal.widget.remotecompose.core.operations.RootContentBehavior;
+import com.android.internal.widget.remotecompose.core.operations.RootContentDescription;
+import com.android.internal.widget.remotecompose.core.operations.TextData;
+import com.android.internal.widget.remotecompose.core.operations.Theme;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * Provides an abstract buffer to encode/decode RemoteCompose operations
+ */
+public class RemoteComposeBuffer {
+ WireBuffer mBuffer = new WireBuffer();
+ Platform mPlatform = null;
+ RemoteComposeState mRemoteComposeState;
+
+ /**
+ * Provides an abstract buffer to encode/decode RemoteCompose operations
+ *
+ * @param remoteComposeState the state used while encoding on the buffer
+ */
+ public RemoteComposeBuffer(RemoteComposeState remoteComposeState) {
+ this.mRemoteComposeState = remoteComposeState;
+ }
+
+ public void reset() {
+ mBuffer.reset();
+ mRemoteComposeState.reset();
+ }
+
+ public Platform getPlatform() {
+ return mPlatform;
+ }
+
+ public void setPlatform(Platform platform) {
+ this.mPlatform = platform;
+ }
+
+ public WireBuffer getBuffer() {
+ return mBuffer;
+ }
+
+ public void setBuffer(WireBuffer buffer) {
+ this.mBuffer = buffer;
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // Supported operations on the buffer
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Insert a header
+ *
+ * @param width the width of the document in pixels
+ * @param height the height of the document in pixels
+ * @param contentDescription content description of the document
+ * @param capabilities bitmask indicating needed capabilities (unused for now)
+ */
+ public void header(int width, int height, String contentDescription, long capabilities) {
+ Header.COMPANION.apply(mBuffer, width, height, capabilities);
+ int contentDescriptionId = 0;
+ if (contentDescription != null) {
+ contentDescriptionId = addText(contentDescription);
+ RootContentDescription.COMPANION.apply(mBuffer, contentDescriptionId);
+ }
+ }
+
+ /**
+ * Insert a header
+ *
+ * @param width the width of the document in pixels
+ * @param height the height of the document in pixels
+ * @param contentDescription content description of the document
+ */
+ public void header(int width, int height, String contentDescription) {
+ header(width, height, contentDescription, 0);
+ }
+
+ /**
+ * Insert a bitmap
+ *
+ * @param image an opaque image that we'll add to the buffer
+ * @param imageWidth the width of the image
+ * @param imageHeight the height of the image
+ * @param srcLeft left coordinate of the source area
+ * @param srcTop top coordinate of the source area
+ * @param srcRight right coordinate of the source area
+ * @param srcBottom bottom coordinate of the source area
+ * @param dstLeft left coordinate of the destination area
+ * @param dstTop top coordinate of the destination area
+ * @param dstRight right coordinate of the destination area
+ * @param dstBottom bottom coordinate of the destination area
+ */
+ public void drawBitmap(Object image,
+ int imageWidth, int imageHeight,
+ int srcLeft, int srcTop, int srcRight, int srcBottom,
+ int dstLeft, int dstTop, int dstRight, int dstBottom,
+ String contentDescription) {
+ int imageId = mRemoteComposeState.dataGetId(image);
+ if (imageId == -1) {
+ imageId = mRemoteComposeState.cache(image);
+ byte[] data = mPlatform.imageToByteArray(image);
+ BitmapData.COMPANION.apply(mBuffer, imageId, imageWidth, imageHeight, data);
+ }
+ int contentDescriptionId = 0;
+ if (contentDescription != null) {
+ contentDescriptionId = addText(contentDescription);
+ }
+ DrawBitmapInt.COMPANION.apply(
+ mBuffer, imageId, srcLeft, srcTop, srcRight, srcBottom,
+ dstLeft, dstTop, dstRight, dstBottom, contentDescriptionId
+ );
+ }
+
+ /**
+ * Adds a text string data to the stream and returns its id
+ * Will be used to insert string with bitmaps etc.
+ *
+ * @param text the string to inject in the buffer
+ */
+ int addText(String text) {
+ int id = mRemoteComposeState.dataGetId(text);
+ if (id == -1) {
+ id = mRemoteComposeState.cache(text);
+ TextData.COMPANION.apply(mBuffer, id, text);
+ }
+ return id;
+ }
+
+ /**
+ * Add a click area to the document
+ *
+ * @param id the id of the click area, reported in the click listener callback
+ * @param contentDescription the content description of that click area (accessibility)
+ * @param left left coordinate of the area bounds
+ * @param top top coordinate of the area bounds
+ * @param right right coordinate of the area bounds
+ * @param bottom bottom coordinate of the area bounds
+ * @param metadata associated metadata, user-provided
+ */
+ public void addClickArea(
+ int id,
+ String contentDescription,
+ float left,
+ float top,
+ float right,
+ float bottom,
+ String metadata
+ ) {
+ int contentDescriptionId = 0;
+ if (contentDescription != null) {
+ contentDescriptionId = addText(contentDescription);
+ }
+ int metadataId = 0;
+ if (metadata != null) {
+ metadataId = addText(metadata);
+ }
+ ClickArea.COMPANION.apply(mBuffer, id, contentDescriptionId,
+ left, top, right, bottom, metadataId);
+ }
+
+ /**
+ * Sets the way the player handles the content
+ *
+ * @param scroll set the horizontal behavior (NONE|SCROLL_HORIZONTAL|SCROLL_VERTICAL)
+ * @param alignment set the alignment of the content (TOP|CENTER|BOTTOM|START|END)
+ * @param sizing set the type of sizing for the content (NONE|SIZING_LAYOUT|SIZING_SCALE)
+ * @param mode set the mode of sizing, either LAYOUT modes or SCALE modes
+ * the LAYOUT modes are:
+ * - LAYOUT_MATCH_PARENT
+ * - LAYOUT_WRAP_CONTENT
+ * or adding an horizontal mode and a vertical mode:
+ * - LAYOUT_HORIZONTAL_MATCH_PARENT
+ * - LAYOUT_HORIZONTAL_WRAP_CONTENT
+ * - LAYOUT_HORIZONTAL_FIXED
+ * - LAYOUT_VERTICAL_MATCH_PARENT
+ * - LAYOUT_VERTICAL_WRAP_CONTENT
+ * - LAYOUT_VERTICAL_FIXED
+ * The LAYOUT_*_FIXED modes will use the intrinsic document size
+ */
+ public void setRootContentBehavior(int scroll, int alignment, int sizing, int mode) {
+ RootContentBehavior.COMPANION.apply(mBuffer, scroll, alignment, sizing, mode);
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ public void inflateFromBuffer(ArrayList<Operation> operations) {
+ mBuffer.setIndex(0);
+ while (mBuffer.available()) {
+ int opId = mBuffer.readByte();
+ CompanionOperation operation = Operations.map.get(opId);
+ if (operation == null) {
+ throw new RuntimeException("Unknown operation encountered");
+ }
+ operation.read(mBuffer, operations);
+ }
+ }
+
+ RemoteComposeBuffer copy() {
+ ArrayList<Operation> operations = new ArrayList<>();
+ inflateFromBuffer(operations);
+ RemoteComposeBuffer buffer = new RemoteComposeBuffer(mRemoteComposeState);
+ return copyFromOperations(operations, buffer);
+ }
+
+ public void setTheme(int theme) {
+ Theme.COMPANION.apply(mBuffer, theme);
+ }
+
+
+ static String version() {
+ return "v1.0";
+ }
+
+ public static RemoteComposeBuffer fromFile(String path,
+ RemoteComposeState remoteComposeState)
+ throws IOException {
+ RemoteComposeBuffer buffer = new RemoteComposeBuffer(remoteComposeState);
+ read(new File(path), buffer);
+ return buffer;
+ }
+
+ public RemoteComposeBuffer fromFile(File file,
+ RemoteComposeState remoteComposeState) throws IOException {
+ RemoteComposeBuffer buffer = new RemoteComposeBuffer(remoteComposeState);
+ read(file, buffer);
+ return buffer;
+ }
+
+ public static RemoteComposeBuffer fromInputStream(InputStream inputStream,
+ RemoteComposeState remoteComposeState) {
+ RemoteComposeBuffer buffer = new RemoteComposeBuffer(remoteComposeState);
+ read(inputStream, buffer);
+ return buffer;
+ }
+
+ RemoteComposeBuffer copyFromOperations(ArrayList<Operation> operations,
+ RemoteComposeBuffer buffer) {
+
+ for (Operation operation : operations) {
+ operation.write(buffer.mBuffer);
+ }
+ return buffer;
+ }
+
+ public void write(RemoteComposeBuffer buffer, File file) {
+ try {
+ FileOutputStream fd = new FileOutputStream(file);
+ fd.write(buffer.mBuffer.getBuffer(), 0, buffer.mBuffer.getSize());
+ fd.flush();
+ fd.close();
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ }
+ }
+
+ static void read(File file, RemoteComposeBuffer buffer) throws IOException {
+ FileInputStream fd = new FileInputStream(file);
+ read(fd, buffer);
+ }
+
+ public static void read(InputStream fd, RemoteComposeBuffer buffer) {
+ try {
+ byte[] bytes = readAllBytes(fd);
+ buffer.reset();
+ System.arraycopy(bytes, 0, buffer.mBuffer.mBuffer, 0, bytes.length);
+ buffer.mBuffer.mSize = bytes.length;
+ } catch (Exception e) {
+ e.printStackTrace();
+ // todo decide how to handel this stuff
+ }
+ }
+
+ private static byte[] readAllBytes(InputStream is) throws IOException {
+ byte[] buff = new byte[32 * 1024]; // moderate size buff to start
+ int red = 0;
+ while (true) {
+ int ret = is.read(buff, red, buff.length - red);
+ if (ret == -1) {
+ is.close();
+ return Arrays.copyOf(buff, red);
+ }
+ red += ret;
+ if (red == buff.length) {
+ buff = Arrays.copyOf(buff, buff.length * 2);
+ }
+ }
+ }
+
+}
+
diff --git a/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeOperation.java b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeOperation.java
new file mode 100644
index 000000000000..c7ec33593286
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeOperation.java
@@ -0,0 +1,20 @@
+/*
+ * 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 com.android.internal.widget.remotecompose.core;
+
+public interface RemoteComposeOperation extends Operation {
+
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeState.java b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeState.java
new file mode 100644
index 000000000000..17e8c839a080
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeState.java
@@ -0,0 +1,100 @@
+/*
+ * 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 com.android.internal.widget.remotecompose.core;
+
+import com.android.internal.widget.remotecompose.core.operations.utilities.IntMap;
+
+import java.util.HashMap;
+
+/**
+ * Represents runtime state for a RemoteCompose document
+ */
+public class RemoteComposeState {
+
+ private final IntMap<Object> mIntDataMap = new IntMap<>();
+ private final IntMap<Boolean> mIntWrittenMap = new IntMap<>();
+ private final HashMap<Object, Integer> mDataIntMap = new HashMap();
+
+ private static int sNextId = 42;
+
+ public Object getFromId(int id) {
+ return mIntDataMap.get(id);
+ }
+
+ public boolean containsId(int id) {
+ return mIntDataMap.get(id) != null;
+ }
+
+ /**
+ * Return the id of an item from the cache.
+ */
+ public int dataGetId(Object image) {
+ Integer res = mDataIntMap.get(image);
+ if (res == null) {
+ return -1;
+ }
+ return res;
+ }
+
+ /**
+ * Add an image to the cache. Generates an id for the image and adds it to the cache based on
+ * that id.
+ */
+ public int cache(Object image) {
+ int id = nextId();
+ mDataIntMap.put(image, id);
+ mIntDataMap.put(id, image);
+ return id;
+ }
+
+ /**
+ * Insert an item in the cache
+ */
+ public void cache(int id, Object item) {
+ mDataIntMap.put(item, id);
+ mIntDataMap.put(id, item);
+ }
+
+ /**
+ * Method to determine if a cached value has been written to the documents WireBuffer based on
+ * its id.
+ */
+ public boolean wasNotWritten(int id) {
+ return !mIntWrittenMap.get(id);
+ }
+
+ /**
+ * Method to mark that a value, represented by its id, has been written to the WireBuffer
+ */
+ public void markWritten(int id) {
+ mIntWrittenMap.put(id, true);
+ }
+
+ /**
+ * Clear the record of the values that have been written to the WireBuffer.
+ */
+ void reset() {
+ mIntWrittenMap.clear();
+ }
+
+ public static int nextId() {
+ return sNextId++;
+ }
+ public static void setNextId(int id) {
+ sNextId = id;
+ }
+
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/RemoteContext.java b/core/java/com/android/internal/widget/remotecompose/core/RemoteContext.java
new file mode 100644
index 000000000000..1b7c6fd0f218
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/RemoteContext.java
@@ -0,0 +1,154 @@
+/*
+ * 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 com.android.internal.widget.remotecompose.core;
+
+import com.android.internal.widget.remotecompose.core.operations.Theme;
+
+/**
+ * Specify an abstract context used to playback RemoteCompose documents
+ *
+ * This allows us to intercept the different operations in a document and react to them.
+ *
+ * We also contain a PaintContext, so that any operation can draw as needed.
+ */
+public abstract class RemoteContext {
+ protected CoreDocument mDocument;
+ public RemoteComposeState mRemoteComposeState;
+
+ protected PaintContext mPaintContext = null;
+ ContextMode mMode = ContextMode.UNSET;
+
+ boolean mDebug = false;
+ private int mTheme = Theme.UNSPECIFIED;
+
+ public float mWidth = 0f;
+ public float mHeight = 0f;
+
+ /**
+ * The context can be used in a few different mode, allowing operations to skip being executed:
+ * - UNSET : all operations will get executed
+ * - DATA : only operations dealing with DATA (eg loading a bitmap) should execute
+ * - PAINT : only operations painting should execute
+ */
+ public enum ContextMode {
+ UNSET, DATA, PAINT
+ }
+
+ public int getTheme() {
+ return mTheme;
+ }
+
+ public void setTheme(int theme) {
+ this.mTheme = theme;
+ }
+
+ public ContextMode getMode() {
+ return mMode;
+ }
+
+ public void setMode(ContextMode mode) {
+ this.mMode = mode;
+ }
+
+ public PaintContext getPaintContext() {
+ return mPaintContext;
+ }
+
+ public void setPaintContext(PaintContext paintContext) {
+ this.mPaintContext = paintContext;
+ }
+
+ public CoreDocument getDocument() {
+ return mDocument;
+ }
+
+ public boolean isDebug() {
+ return mDebug;
+ }
+
+ public void setDebug(boolean debug) {
+ this.mDebug = debug;
+ }
+
+ public void setDocument(CoreDocument document) {
+ this.mDocument = document;
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // Operations
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ public void header(int majorVersion, int minorVersion, int patchVersion,
+ int width, int height, long capabilities
+ ) {
+ mDocument.setVersion(majorVersion, minorVersion, patchVersion);
+ mDocument.setWidth(width);
+ mDocument.setHeight(height);
+ mDocument.setRequiredCapabilities(capabilities);
+ }
+
+ /**
+ * Sets the way the player handles the content
+ *
+ * @param scroll set the horizontal behavior (NONE|SCROLL_HORIZONTAL|SCROLL_VERTICAL)
+ * @param alignment set the alignment of the content (TOP|CENTER|BOTTOM|START|END)
+ * @param sizing set the type of sizing for the content (NONE|SIZING_LAYOUT|SIZING_SCALE)
+ * @param mode set the mode of sizing, either LAYOUT modes or SCALE modes
+ * the LAYOUT modes are:
+ * - LAYOUT_MATCH_PARENT
+ * - LAYOUT_WRAP_CONTENT
+ * or adding an horizontal mode and a vertical mode:
+ * - LAYOUT_HORIZONTAL_MATCH_PARENT
+ * - LAYOUT_HORIZONTAL_WRAP_CONTENT
+ * - LAYOUT_HORIZONTAL_FIXED
+ * - LAYOUT_VERTICAL_MATCH_PARENT
+ * - LAYOUT_VERTICAL_WRAP_CONTENT
+ * - LAYOUT_VERTICAL_FIXED
+ * The LAYOUT_*_FIXED modes will use the intrinsic document size
+ */
+ public void setRootContentBehavior(int scroll, int alignment, int sizing, int mode) {
+ mDocument.setRootContentBehavior(scroll, alignment, sizing, mode);
+ }
+
+ /**
+ * Set a content description for the document
+ * @param contentDescriptionId the text id pointing at the description
+ */
+ public void setDocumentContentDescription(int contentDescriptionId) {
+ String contentDescription = (String) mRemoteComposeState.getFromId(contentDescriptionId);
+ mDocument.setContentDescription(contentDescription);
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // Data handling
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ public abstract void loadBitmap(int imageId, int width, int height, byte[] bitmap);
+ public abstract void loadText(int id, String text);
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // Click handling
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ public abstract void addClickArea(
+ int id,
+ int contentDescription,
+ float left,
+ float top,
+ float right,
+ float bottom,
+ int metadataId
+ );
+}
+
diff --git a/core/java/com/android/internal/widget/remotecompose/core/WireBuffer.java b/core/java/com/android/internal/widget/remotecompose/core/WireBuffer.java
new file mode 100644
index 000000000000..3e701c17291b
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/WireBuffer.java
@@ -0,0 +1,254 @@
+/*
+ * 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 com.android.internal.widget.remotecompose.core;
+
+import java.util.Arrays;
+
+/**
+ * The base communication buffer capable of encoding and decoding various types
+ */
+public class WireBuffer {
+ private static final int BUFFER_SIZE = 1024 * 1024 * 1;
+ int mMaxSize;
+ byte[] mBuffer;
+ int mIndex = 0;
+ int mStartingIndex = 0;
+ int mSize = 0;
+
+ public WireBuffer(int size) {
+ mMaxSize = size;
+ mBuffer = new byte[mMaxSize];
+ }
+
+ public WireBuffer() {
+ this(BUFFER_SIZE);
+ }
+
+ private void resize(int need) {
+ if (mSize + need >= mMaxSize) {
+ mMaxSize = Math.max(mMaxSize * 2, mSize + need);
+ mBuffer = Arrays.copyOf(mBuffer, mMaxSize);
+ }
+ }
+
+ public byte[] getBuffer() {
+ return mBuffer;
+ }
+
+ public int getMax_size() {
+ return mMaxSize;
+ }
+
+ public int getIndex() {
+ return mIndex;
+ }
+
+ public int getSize() {
+ return mSize;
+ }
+
+ public void setIndex(int index) {
+ this.mIndex = index;
+ }
+
+ public void start(int type) {
+ mStartingIndex = mIndex;
+ writeByte(type);
+ }
+
+ public void startWithSize(int type) {
+ mStartingIndex = mIndex;
+ writeByte(type);
+ mIndex += 4; // skip ahead for the future size
+ }
+
+ public void endWithSize() {
+ int size = mIndex - mStartingIndex;
+ int currentIndex = mIndex;
+ mIndex = mStartingIndex + 1; // (type)
+ writeInt(size);
+ mIndex = currentIndex;
+ }
+
+ public void reset() {
+ mIndex = 0;
+ mStartingIndex = 0;
+ mSize = 0;
+ }
+
+ public int size() {
+ return mSize;
+ }
+
+ public boolean available() {
+ return mSize - mIndex > 0;
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Read values
+ ///////////////////////////////////////////////////////////////////////////
+
+ public int readOperationType() {
+ return readByte();
+ }
+
+ public boolean readBoolean() {
+ byte value = mBuffer[mIndex];
+ mIndex++;
+ return (value == 1);
+ }
+
+ public int readByte() {
+ byte value = mBuffer[mIndex];
+ mIndex++;
+ return value;
+ }
+
+ public int readShort() {
+ int v1 = (mBuffer[mIndex++] & 0xFF) << 8;
+ int v2 = (mBuffer[mIndex++] & 0xFF) << 0;
+ return v1 + v2;
+ }
+
+ public int readInt() {
+ int v1 = (mBuffer[mIndex++] & 0xFF) << 24;
+ int v2 = (mBuffer[mIndex++] & 0xFF) << 16;
+ int v3 = (mBuffer[mIndex++] & 0xFF) << 8;
+ int v4 = (mBuffer[mIndex++] & 0xFF) << 0;
+ return v1 + v2 + v3 + v4;
+ }
+
+ public long readLong() {
+ long v1 = (mBuffer[mIndex++] & 0xFFL) << 56;
+ long v2 = (mBuffer[mIndex++] & 0xFFL) << 48;
+ long v3 = (mBuffer[mIndex++] & 0xFFL) << 40;
+ long v4 = (mBuffer[mIndex++] & 0xFFL) << 32;
+ long v5 = (mBuffer[mIndex++] & 0xFFL) << 24;
+ long v6 = (mBuffer[mIndex++] & 0xFFL) << 16;
+ long v7 = (mBuffer[mIndex++] & 0xFFL) << 8;
+ long v8 = (mBuffer[mIndex++] & 0xFFL) << 0;
+ return v1 + v2 + v3 + v4 + v5 + v6 + v7 + v8;
+ }
+
+ public float readFloat() {
+ return java.lang.Float.intBitsToFloat(readInt());
+ }
+
+ public double readDouble() {
+ return java.lang.Double.longBitsToDouble(readLong());
+ }
+
+ public byte[] readBuffer() {
+ int count = readInt();
+ byte[] b = Arrays.copyOfRange(mBuffer, mIndex, mIndex + count);
+ mIndex += count;
+ return b;
+ }
+
+ public byte[] readBuffer(int maxSize) {
+ int count = readInt();
+ if (count < 0 || count > maxSize) {
+ throw new RuntimeException("attempt read a buff of invalid size 0 <= "
+ + count + " > " + maxSize);
+ }
+ byte[] b = Arrays.copyOfRange(mBuffer, mIndex, mIndex + count);
+ mIndex += count;
+ return b;
+ }
+
+ public String readUTF8() {
+ byte[] stringBuffer = readBuffer();
+ return new String(stringBuffer);
+ }
+
+ public String readUTF8(int maxSize) {
+ byte[] stringBuffer = readBuffer(maxSize);
+ return new String(stringBuffer);
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Write values
+ ///////////////////////////////////////////////////////////////////////////
+
+ public void writeBoolean(boolean value) {
+ resize(1);
+ mBuffer[mIndex++] = (byte) ((value) ? 1 : 0);
+ mSize++;
+ }
+
+ public void writeByte(int value) {
+ resize(1);
+ mBuffer[mIndex++] = (byte) value;
+ mSize++;
+ }
+
+ public void writeShort(int value) {
+ int need = 2;
+ resize(need);
+ mBuffer[mIndex++] = (byte) (value >>> 8 & 0xFF);
+ mBuffer[mIndex++] = (byte) (value & 0xFF);
+ mSize += need;
+ }
+
+ public void writeInt(int value) {
+ int need = 4;
+ resize(need);
+ mBuffer[mIndex++] = (byte) (value >>> 24 & 0xFF);
+ mBuffer[mIndex++] = (byte) (value >>> 16 & 0xFF);
+ mBuffer[mIndex++] = (byte) (value >>> 8 & 0xFF);
+ mBuffer[mIndex++] = (byte) (value & 0xFF);
+ mSize += need;
+ }
+
+ public void writeLong(long value) {
+ int need = 8;
+ resize(need);
+ mBuffer[mIndex++] = (byte) (value >>> 56 & 0xFF);
+ mBuffer[mIndex++] = (byte) (value >>> 48 & 0xFF);
+ mBuffer[mIndex++] = (byte) (value >>> 40 & 0xFF);
+ mBuffer[mIndex++] = (byte) (value >>> 32 & 0xFF);
+ mBuffer[mIndex++] = (byte) (value >>> 24 & 0xFF);
+ mBuffer[mIndex++] = (byte) (value >>> 16 & 0xFF);
+ mBuffer[mIndex++] = (byte) (value >>> 8 & 0xFF);
+ mBuffer[mIndex++] = (byte) (value & 0xFF);
+ mSize += need;
+ }
+
+ public void writeFloat(float value) {
+ writeInt(Float.floatToRawIntBits(value));
+ }
+
+ public void writeDouble(double value) {
+ writeLong(Double.doubleToRawLongBits(value));
+ }
+
+ public void writeBuffer(byte[] b) {
+ resize(b.length + 4);
+ writeInt(b.length);
+ for (int i = 0; i < b.length; i++) {
+ mBuffer[mIndex++] = b[i];
+
+ }
+ mSize += b.length;
+ }
+
+ public void writeUTF8(String content) {
+ byte[] buffer = content.getBytes();
+ writeBuffer(buffer);
+ }
+
+}
+
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/BitmapData.java b/core/java/com/android/internal/widget/remotecompose/core/operations/BitmapData.java
new file mode 100644
index 000000000000..4bfdc59aad3a
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/BitmapData.java
@@ -0,0 +1,104 @@
+/*
+ * 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 com.android.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+/**
+ * Operation to deal with bitmap data
+ * On getting an Image during a draw call the bitmap is compressed and saved
+ * in playback the image is decompressed
+ */
+public class BitmapData implements Operation {
+ int mImageId;
+ int mImageWidth;
+ int mImageHeight;
+ byte[] mBitmap;
+ public static final int MAX_IMAGE_DIMENSION = 6000;
+
+ public static final Companion COMPANION = new Companion();
+
+ public BitmapData(int imageId, int width, int height, byte[] bitmap) {
+ this.mImageId = imageId;
+ this.mImageWidth = width;
+ this.mImageHeight = height;
+ this.mBitmap = bitmap;
+ }
+
+ @Override
+ public void write(WireBuffer buffer) {
+ COMPANION.apply(buffer, mImageId, mImageWidth, mImageHeight, mBitmap);
+ }
+
+ @Override
+ public String toString() {
+ return "BITMAP DATA $imageId";
+ }
+
+ public static class Companion implements CompanionOperation {
+ private Companion() {}
+
+ @Override
+ public String name() {
+ return "BitmapData";
+ }
+
+ @Override
+ public int id() {
+ return Operations.DATA_BITMAP;
+ }
+
+ public void apply(WireBuffer buffer, int imageId, int width, int height, byte[] bitmap) {
+ buffer.start(Operations.DATA_BITMAP);
+ buffer.writeInt(imageId);
+ buffer.writeInt(width);
+ buffer.writeInt(height);
+ buffer.writeBuffer(bitmap);
+ }
+
+ @Override
+ public void read(WireBuffer buffer, List<Operation> operations) {
+ int imageId = buffer.readInt();
+ int width = buffer.readInt();
+ int height = buffer.readInt();
+ if (width < 1
+ || height < 1
+ || height > MAX_IMAGE_DIMENSION
+ || width > MAX_IMAGE_DIMENSION) {
+ throw new RuntimeException("Dimension of image is invalid " + width + "x" + height);
+ }
+ byte[] bitmap = buffer.readBuffer();
+ operations.add(new BitmapData(imageId, width, height, bitmap));
+ }
+ }
+
+ @Override
+ public void apply(RemoteContext context) {
+ context.loadBitmap(mImageId, mImageWidth, mImageHeight, mBitmap);
+ }
+
+ @Override
+ public String deepToString(String indent) {
+ return indent + toString();
+ }
+
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/ClickArea.java b/core/java/com/android/internal/widget/remotecompose/core/operations/ClickArea.java
new file mode 100644
index 000000000000..a3cd1973f647
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/ClickArea.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.RemoteComposeOperation;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+/**
+ * Add a click area to the document
+ */
+public class ClickArea implements RemoteComposeOperation {
+ int mId;
+ int mContentDescription;
+ float mLeft;
+ float mTop;
+ float mRight;
+ float mBottom;
+ int mMetadata;
+
+ public static final Companion COMPANION = new Companion();
+
+ /**
+ * Add a click area to the document
+ *
+ * @param id the id of the click area, which will be reported in the listener
+ * callback on the player
+ * @param contentDescription the content description (used for accessibility, as a textID)
+ * @param left left coordinate of the area bounds
+ * @param top top coordinate of the area bounds
+ * @param right right coordinate of the area bounds
+ * @param bottom bottom coordinate of the area bounds
+ * @param metadata associated metadata, user-provided (as a textID, pointing to a string)
+ */
+ public ClickArea(int id, int contentDescription,
+ float left, float top,
+ float right, float bottom,
+ int metadata) {
+ this.mId = id;
+ this.mContentDescription = contentDescription;
+ this.mLeft = left;
+ this.mTop = top;
+ this.mRight = right;
+ this.mBottom = bottom;
+ this.mMetadata = metadata;
+ }
+
+ @Override
+ public void write(WireBuffer buffer) {
+ COMPANION.apply(buffer, mId, mContentDescription, mLeft, mTop, mRight, mBottom, mMetadata);
+ }
+
+ @Override
+ public String toString() {
+ return "CLICK_AREA <" + mId + " <" + mContentDescription + "> "
+ + "<" + mMetadata + ">+" + mLeft + " "
+ + mTop + " " + mRight + " " + mBottom + "+"
+ + " (" + (mRight - mLeft) + " x " + (mBottom - mTop) + " }";
+ }
+
+ @Override
+ public void apply(RemoteContext context) {
+ if (context.getMode() != RemoteContext.ContextMode.DATA) {
+ return;
+ }
+ context.addClickArea(mId, mContentDescription, mLeft, mTop, mRight, mBottom, mMetadata);
+ }
+
+ @Override
+ public String deepToString(String indent) {
+ return indent + toString();
+ }
+
+ public static class Companion implements CompanionOperation {
+ private Companion() {}
+
+ @Override
+ public String name() {
+ return "ClickArea";
+ }
+
+ @Override
+ public int id() {
+ return Operations.CLICK_AREA;
+ }
+
+ public void apply(WireBuffer buffer, int id, int contentDescription,
+ float left, float top, float right, float bottom,
+ int metadata) {
+ buffer.start(Operations.CLICK_AREA);
+ buffer.writeInt(id);
+ buffer.writeInt(contentDescription);
+ buffer.writeFloat(left);
+ buffer.writeFloat(top);
+ buffer.writeFloat(right);
+ buffer.writeFloat(bottom);
+ buffer.writeInt(metadata);
+ }
+
+ @Override
+ public void read(WireBuffer buffer, List<Operation> operations) {
+ int id = buffer.readInt();
+ int contentDescription = buffer.readInt();
+ float left = buffer.readFloat();
+ float top = buffer.readFloat();
+ float right = buffer.readFloat();
+ float bottom = buffer.readFloat();
+ int metadata = buffer.readInt();
+ ClickArea clickArea = new ClickArea(id, contentDescription,
+ left, top, right, bottom, metadata);
+ operations.add(clickArea);
+ }
+ }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBitmapInt.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBitmapInt.java
new file mode 100644
index 000000000000..3fbdf94427d1
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBitmapInt.java
@@ -0,0 +1,132 @@
+/*
+ * 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 com.android.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+/**
+ * Operation to draw a given cached bitmap
+ */
+public class DrawBitmapInt extends PaintOperation {
+ int mImageId;
+ int mSrcLeft;
+ int mSrcTop;
+ int mSrcRight;
+ int mSrcBottom;
+ int mDstLeft;
+ int mDstTop;
+ int mDstRight;
+ int mDstBottom;
+ int mContentDescId = 0;
+ public static final Companion COMPANION = new Companion();
+
+ public DrawBitmapInt(int imageId,
+ int srcLeft,
+ int srcTop,
+ int srcRight,
+ int srcBottom,
+ int dstLeft,
+ int dstTop,
+ int dstRight,
+ int dstBottom,
+ int cdId) {
+ this.mImageId = imageId;
+ this.mSrcLeft = srcLeft;
+ this.mSrcTop = srcTop;
+ this.mSrcRight = srcRight;
+ this.mSrcBottom = srcBottom;
+ this.mDstLeft = dstLeft;
+ this.mDstTop = dstTop;
+ this.mDstRight = dstRight;
+ this.mDstBottom = dstBottom;
+ this.mContentDescId = cdId;
+ }
+
+ @Override
+ public void write(WireBuffer buffer) {
+ COMPANION.apply(buffer, mImageId, mSrcLeft, mSrcTop, mSrcRight, mSrcBottom,
+ mDstLeft, mDstTop, mDstRight, mDstBottom, mContentDescId);
+ }
+
+ @Override
+ public String toString() {
+ return "DRAW_BITMAP_INT " + mImageId + " on " + mSrcLeft + " " + mSrcTop
+ + " " + mSrcRight + " " + mSrcBottom + " "
+ + "- " + mDstLeft + " " + mDstTop + " " + mDstRight + " " + mDstBottom + ";";
+ }
+
+ public static class Companion implements CompanionOperation {
+ private Companion() {}
+
+ @Override
+ public String name() {
+ return "DrawBitmapInt";
+ }
+
+ @Override
+ public int id() {
+ return Operations.DRAW_BITMAP;
+ }
+
+ public void apply(WireBuffer buffer, int imageId,
+ int srcLeft, int srcTop, int srcRight, int srcBottom,
+ int dstLeft, int dstTop, int dstRight, int dstBottom,
+ int cdId) {
+ buffer.start(Operations.DRAW_BITMAP_INT);
+ buffer.writeInt(imageId);
+ buffer.writeInt(srcLeft);
+ buffer.writeInt(srcTop);
+ buffer.writeInt(srcRight);
+ buffer.writeInt(srcBottom);
+ buffer.writeInt(dstLeft);
+ buffer.writeInt(dstTop);
+ buffer.writeInt(dstRight);
+ buffer.writeInt(dstBottom);
+ buffer.writeInt(cdId);
+ }
+
+ @Override
+ public void read(WireBuffer buffer, List<Operation> operations) {
+ int imageId = buffer.readInt();
+ int sLeft = buffer.readInt();
+ int srcTop = buffer.readInt();
+ int srcRight = buffer.readInt();
+ int srcBottom = buffer.readInt();
+ int dstLeft = buffer.readInt();
+ int dstTop = buffer.readInt();
+ int dstRight = buffer.readInt();
+ int dstBottom = buffer.readInt();
+ int cdId = buffer.readInt();
+ DrawBitmapInt op = new DrawBitmapInt(imageId, sLeft, srcTop, srcRight, srcBottom,
+ dstLeft, dstTop, dstRight, dstBottom, cdId);
+
+ operations.add(op);
+ }
+ }
+
+ @Override
+ public void paint(PaintContext context) {
+ context.drawBitmap(mImageId, mSrcLeft, mSrcTop, mSrcRight, mSrcBottom,
+ mDstLeft, mDstTop, mDstRight, mDstBottom, mContentDescId);
+ }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/Header.java b/core/java/com/android/internal/widget/remotecompose/core/operations/Header.java
new file mode 100644
index 000000000000..eca43c5e3281
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/Header.java
@@ -0,0 +1,127 @@
+/*
+ * 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 com.android.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.RemoteComposeOperation;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+/**
+ * Describe some basic information for a RemoteCompose document
+ *
+ * It encodes the version of the document (following semantic versioning) as well
+ * as the dimensions of the document in pixels.
+ */
+public class Header implements RemoteComposeOperation {
+ public static final int MAJOR_VERSION = 0;
+ public static final int MINOR_VERSION = 1;
+ public static final int PATCH_VERSION = 0;
+
+ int mMajorVersion;
+ int mMinorVersion;
+ int mPatchVersion;
+
+ int mWidth;
+ int mHeight;
+ long mCapabilities;
+
+ public static final Companion COMPANION = new Companion();
+
+ /**
+ * It encodes the version of the document (following semantic versioning) as well
+ * as the dimensions of the document in pixels.
+ *
+ * @param majorVersion the major version of the RemoteCompose document API
+ * @param minorVersion the minor version of the RemoteCompose document API
+ * @param patchVersion the patch version of the RemoteCompose document API
+ * @param width the width of the RemoteCompose document
+ * @param height the height of the RemoteCompose document
+ * @param capabilities bitmask field storing needed capabilities (unused for now)
+ */
+ public Header(int majorVersion, int minorVersion, int patchVersion,
+ int width, int height, long capabilities) {
+ this.mMajorVersion = majorVersion;
+ this.mMinorVersion = minorVersion;
+ this.mPatchVersion = patchVersion;
+ this.mWidth = width;
+ this.mHeight = height;
+ this.mCapabilities = capabilities;
+ }
+
+ @Override
+ public void write(WireBuffer buffer) {
+ COMPANION.apply(buffer, mWidth, mHeight, mCapabilities);
+ }
+
+ @Override
+ public String toString() {
+ return "HEADER v" + mMajorVersion + "."
+ + mMinorVersion + "." + mPatchVersion + ", "
+ + mWidth + " x " + mHeight + " [" + mCapabilities + "]";
+ }
+
+ @Override
+ public void apply(RemoteContext context) {
+ context.header(mMajorVersion, mMinorVersion, mPatchVersion, mWidth, mHeight, mCapabilities);
+ }
+
+ @Override
+ public String deepToString(String indent) {
+ return toString();
+ }
+
+ public static class Companion implements CompanionOperation {
+ private Companion() {}
+
+ @Override
+ public String name() {
+ return "Header";
+ }
+
+ @Override
+ public int id() {
+ return Operations.HEADER;
+ }
+
+ public void apply(WireBuffer buffer, int width, int height, long capabilities) {
+ buffer.start(Operations.HEADER);
+ buffer.writeInt(MAJOR_VERSION); // major version number of the protocol
+ buffer.writeInt(MINOR_VERSION); // minor version number of the protocol
+ buffer.writeInt(PATCH_VERSION); // patch version number of the protocol
+ buffer.writeInt(width);
+ buffer.writeInt(height);
+ buffer.writeLong(capabilities);
+ }
+
+ @Override
+ public void read(WireBuffer buffer, List<Operation> operations) {
+ int majorVersion = buffer.readInt();
+ int minorVersion = buffer.readInt();
+ int patchVersion = buffer.readInt();
+ int width = buffer.readInt();
+ int height = buffer.readInt();
+ long capabilities = buffer.readLong();
+ Header header = new Header(majorVersion, minorVersion, patchVersion,
+ width, height, capabilities);
+ operations.add(header);
+ }
+ }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/RootContentBehavior.java b/core/java/com/android/internal/widget/remotecompose/core/operations/RootContentBehavior.java
new file mode 100644
index 000000000000..ad4caea7aef8
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/RootContentBehavior.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations;
+
+import android.util.Log;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.RemoteComposeOperation;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+/**
+ * Describe some basic information for a RemoteCompose document
+ *
+ * It encodes the version of the document (following semantic versioning) as well
+ * as the dimensions of the document in pixels.
+ */
+public class RootContentBehavior implements RemoteComposeOperation {
+
+ int mScroll = NONE;
+ int mSizing = NONE;
+
+ int mAlignment = ALIGNMENT_CENTER;
+
+ int mMode = NONE;
+
+ protected static final String TAG = "RootContentBehavior";
+
+ public static final int NONE = 0;
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // Scrolling
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ public static final int SCROLL_HORIZONTAL = 1;
+ public static final int SCROLL_VERTICAL = 2;
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // Sizing
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ public static final int SIZING_LAYOUT = 1;
+ public static final int SIZING_SCALE = 2;
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // Sizing
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ public static final int ALIGNMENT_TOP = 1;
+ public static final int ALIGNMENT_VERTICAL_CENTER = 2;
+ public static final int ALIGNMENT_BOTTOM = 4;
+ public static final int ALIGNMENT_START = 16;
+ public static final int ALIGNMENT_HORIZONTAL_CENTER = 32;
+ public static final int ALIGNMENT_END = 64;
+ public static final int ALIGNMENT_CENTER = ALIGNMENT_HORIZONTAL_CENTER
+ + ALIGNMENT_VERTICAL_CENTER;
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // Layout mode
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ public static final int LAYOUT_HORIZONTAL_MATCH_PARENT = 1;
+ public static final int LAYOUT_HORIZONTAL_WRAP_CONTENT = 2;
+ public static final int LAYOUT_HORIZONTAL_FIXED = 4;
+ public static final int LAYOUT_VERTICAL_MATCH_PARENT = 8;
+ public static final int LAYOUT_VERTICAL_WRAP_CONTENT = 16;
+ public static final int LAYOUT_VERTICAL_FIXED = 32;
+ public static final int LAYOUT_MATCH_PARENT =
+ LAYOUT_HORIZONTAL_MATCH_PARENT + LAYOUT_VERTICAL_MATCH_PARENT;
+ public static final int LAYOUT_WRAP_CONTENT =
+ LAYOUT_HORIZONTAL_WRAP_CONTENT + LAYOUT_VERTICAL_WRAP_CONTENT;
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // Sizing mode
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ public static final int SCALE_INSIDE = 1;
+ public static final int SCALE_FILL_WIDTH = 2;
+ public static final int SCALE_FILL_HEIGHT = 3;
+ public static final int SCALE_FIT = 4;
+ public static final int SCALE_CROP = 5;
+ public static final int SCALE_FILL_BOUNDS = 6;
+
+
+ public static final Companion COMPANION = new Companion();
+
+ /**
+ * Sets the way the player handles the content
+ *
+ * @param scroll set the horizontal behavior (NONE|SCROLL_HORIZONTAL|SCROLL_VERTICAL)
+ * @param alignment set the alignment of the content (TOP|CENTER|BOTTOM|START|END)
+ * @param sizing set the type of sizing for the content (NONE|SIZING_LAYOUT|SIZING_SCALE)
+ * @param mode set the mode of sizing, either LAYOUT modes or SCALE modes
+ * the LAYOUT modes are:
+ * - LAYOUT_MATCH_PARENT
+ * - LAYOUT_WRAP_CONTENT
+ * or adding an horizontal mode and a vertical mode:
+ * - LAYOUT_HORIZONTAL_MATCH_PARENT
+ * - LAYOUT_HORIZONTAL_WRAP_CONTENT
+ * - LAYOUT_HORIZONTAL_FIXED
+ * - LAYOUT_VERTICAL_MATCH_PARENT
+ * - LAYOUT_VERTICAL_WRAP_CONTENT
+ * - LAYOUT_VERTICAL_FIXED
+ * The LAYOUT_*_FIXED modes will use the intrinsic document size
+ */
+ public RootContentBehavior(int scroll, int alignment, int sizing, int mode) {
+ switch (scroll) {
+ case NONE:
+ case SCROLL_HORIZONTAL:
+ case SCROLL_VERTICAL:
+ mScroll = scroll;
+ break;
+ default: {
+ Log.e(TAG, "incorrect scroll value " + scroll);
+ }
+ }
+ if (alignment == ALIGNMENT_CENTER) {
+ mAlignment = alignment;
+ } else {
+ int horizontalContentAlignment = alignment & 0xF0;
+ int verticalContentAlignment = alignment & 0xF;
+ boolean validHorizontalAlignment = horizontalContentAlignment == ALIGNMENT_START
+ || horizontalContentAlignment == ALIGNMENT_HORIZONTAL_CENTER
+ || horizontalContentAlignment == ALIGNMENT_END;
+ boolean validVerticalAlignment = verticalContentAlignment == ALIGNMENT_TOP
+ || verticalContentAlignment == ALIGNMENT_VERTICAL_CENTER
+ || verticalContentAlignment == ALIGNMENT_BOTTOM;
+ if (validHorizontalAlignment && validVerticalAlignment) {
+ mAlignment = alignment;
+ } else {
+ Log.e(TAG, "incorrect alignment "
+ + " h: " + horizontalContentAlignment
+ + " v: " + verticalContentAlignment);
+ }
+ }
+ switch (sizing) {
+ case SIZING_LAYOUT: {
+ Log.e(TAG, "sizing_layout is not yet supported");
+ } break;
+ case SIZING_SCALE: {
+ mSizing = sizing;
+ } break;
+ default: {
+ Log.e(TAG, "incorrect sizing value " + sizing);
+ }
+ }
+ if (mSizing == SIZING_LAYOUT) {
+ if (mode != NONE) {
+ Log.e(TAG, "mode for sizing layout is not yet supported");
+ }
+ } else if (mSizing == SIZING_SCALE) {
+ switch (mode) {
+ case SCALE_INSIDE:
+ case SCALE_FIT:
+ case SCALE_FILL_WIDTH:
+ case SCALE_FILL_HEIGHT:
+ case SCALE_CROP:
+ case SCALE_FILL_BOUNDS:
+ mMode = mode;
+ break;
+ default: {
+ Log.e(TAG, "incorrect mode for scale sizing, mode: " + mode);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void write(WireBuffer buffer) {
+ COMPANION.apply(buffer, mScroll, mAlignment, mSizing, mMode);
+ }
+
+ @Override
+ public String toString() {
+ return "ROOT_CONTENT_BEHAVIOR scroll: " + mScroll
+ + " sizing: " + mSizing + " mode: " + mMode;
+ }
+
+ @Override
+ public void apply(RemoteContext context) {
+ context.setRootContentBehavior(mScroll, mAlignment, mSizing, mMode);
+ }
+
+ @Override
+ public String deepToString(String indent) {
+ return toString();
+ }
+
+ public static class Companion implements CompanionOperation {
+ private Companion() {}
+
+ @Override
+ public String name() {
+ return "RootContentBehavior";
+ }
+
+ @Override
+ public int id() {
+ return Operations.ROOT_CONTENT_BEHAVIOR;
+ }
+
+ public void apply(WireBuffer buffer, int scroll, int alignment, int sizing, int mode) {
+ buffer.start(Operations.ROOT_CONTENT_BEHAVIOR);
+ buffer.writeInt(scroll);
+ buffer.writeInt(alignment);
+ buffer.writeInt(sizing);
+ buffer.writeInt(mode);
+ }
+
+ @Override
+ public void read(WireBuffer buffer, List<Operation> operations) {
+ int scroll = buffer.readInt();
+ int alignment = buffer.readInt();
+ int sizing = buffer.readInt();
+ int mode = buffer.readInt();
+ RootContentBehavior rootContentBehavior =
+ new RootContentBehavior(scroll, alignment, sizing, mode);
+ operations.add(rootContentBehavior);
+ }
+ }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/RootContentDescription.java b/core/java/com/android/internal/widget/remotecompose/core/operations/RootContentDescription.java
new file mode 100644
index 000000000000..64c7f3ef2d44
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/RootContentDescription.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.RemoteComposeOperation;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+/**
+ * Describe a content description for the document
+ */
+public class RootContentDescription implements RemoteComposeOperation {
+ int mContentDescription;
+
+ public static final Companion COMPANION = new Companion();
+
+ /**
+ * Encodes a content description for the document
+ *
+ * @param contentDescription content description for the document
+ */
+ public RootContentDescription(int contentDescription) {
+ this.mContentDescription = contentDescription;
+ }
+
+ @Override
+ public void write(WireBuffer buffer) {
+ COMPANION.apply(buffer, mContentDescription);
+ }
+
+ @Override
+ public String toString() {
+ return "ROOT_CONTENT_DESCRIPTION " + mContentDescription;
+ }
+
+ @Override
+ public void apply(RemoteContext context) {
+ context.setDocumentContentDescription(mContentDescription);
+ }
+
+ @Override
+ public String deepToString(String indent) {
+ return toString();
+ }
+
+ public static class Companion implements CompanionOperation {
+ private Companion() {}
+
+ @Override
+ public String name() {
+ return "RootContentDescription";
+ }
+
+ @Override
+ public int id() {
+ return Operations.ROOT_CONTENT_DESCRIPTION;
+ }
+
+ public void apply(WireBuffer buffer, int contentDescription) {
+ buffer.start(Operations.ROOT_CONTENT_DESCRIPTION);
+ buffer.writeInt(contentDescription);
+ }
+
+ @Override
+ public void read(WireBuffer buffer, List<Operation> operations) {
+ int contentDescription = buffer.readInt();
+ RootContentDescription header = new RootContentDescription(contentDescription);
+ operations.add(header);
+ }
+ }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/TextData.java b/core/java/com/android/internal/widget/remotecompose/core/operations/TextData.java
new file mode 100644
index 000000000000..5b622ae96d0b
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/TextData.java
@@ -0,0 +1,87 @@
+/*
+ * 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 com.android.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+/**
+ * Operation to deal with Text data
+ */
+public class TextData implements Operation {
+ public int mTextId;
+ public String mText;
+ public static final Companion COMPANION = new Companion();
+ public static final int MAX_STRING_SIZE = 4000;
+
+ public TextData(int textId, String text) {
+ this.mTextId = textId;
+ this.mText = text;
+ }
+
+ @Override
+ public void write(WireBuffer buffer) {
+ COMPANION.apply(buffer, mTextId, mText);
+ }
+
+ @Override
+ public String toString() {
+ return "TEXT DATA " + mTextId + "\"" + mText + "\"";
+ }
+
+ public static class Companion implements CompanionOperation {
+ private Companion() {}
+
+ @Override
+ public String name() {
+ return "TextData";
+ }
+
+ @Override
+ public int id() {
+ return Operations.DATA_TEXT;
+ }
+
+ public void apply(WireBuffer buffer, int textId, String text) {
+ buffer.start(Operations.DATA_TEXT);
+ buffer.writeInt(textId);
+ buffer.writeUTF8(text);
+ }
+
+ @Override
+ public void read(WireBuffer buffer, List<Operation> operations) {
+ int textId = buffer.readInt();
+
+ String text = buffer.readUTF8(MAX_STRING_SIZE);
+ operations.add(new TextData(textId, text));
+ }
+ }
+
+ @Override
+ public void apply(RemoteContext context) {
+ context.loadText(mTextId, mText);
+ }
+
+ @Override
+ public String deepToString(String indent) {
+ return indent + toString();
+ }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/Theme.java b/core/java/com/android/internal/widget/remotecompose/core/operations/Theme.java
new file mode 100644
index 000000000000..cbe9c12e666c
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/Theme.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.RemoteComposeOperation;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+/**
+ * Set a current theme, applied to the following operations in the document.
+ * This can be used to "tag" the subsequent operations to a given theme. On playback,
+ * we can then filter operations depending on the chosen theme.
+ *
+ */
+public class Theme implements RemoteComposeOperation {
+ int mTheme;
+ public static final int UNSPECIFIED = -1;
+ public static final int DARK = -2;
+ public static final int LIGHT = -3;
+
+ public static final Companion COMPANION = new Companion();
+
+ /**
+ * we can then filter operations depending on the chosen theme.
+ *
+ * @param theme the theme we are interested in:
+ * - Theme.UNSPECIFIED
+ * - Theme.DARK
+ * - Theme.LIGHT
+ */
+ public Theme(int theme) {
+ this.mTheme = theme;
+ }
+
+ @Override
+ public void write(WireBuffer buffer) {
+ COMPANION.apply(buffer, mTheme);
+ }
+
+ @Override
+ public String toString() {
+ return "SET_THEME " + mTheme;
+ }
+
+ @Override
+ public void apply(RemoteContext context) {
+ context.setTheme(mTheme);
+ }
+
+ @Override
+ public String deepToString(String indent) {
+ return indent + toString();
+ }
+
+ public static class Companion implements CompanionOperation {
+ private Companion() {}
+
+ @Override
+ public String name() {
+ return "SetTheme";
+ }
+
+ @Override
+ public int id() {
+ return Operations.THEME;
+ }
+
+ public void apply(WireBuffer buffer, int theme) {
+ buffer.start(Operations.THEME);
+ buffer.writeInt(theme);
+ }
+
+ @Override
+ public void read(WireBuffer buffer, List<Operation> operations) {
+ int theme = buffer.readInt();
+ operations.add(new Theme(theme));
+ }
+ }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntMap.java b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntMap.java
new file mode 100644
index 000000000000..8051ef1ab37c
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntMap.java
@@ -0,0 +1,115 @@
+/*
+ * 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 com.android.internal.widget.remotecompose.core.operations.utilities;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+public class IntMap<T> {
+
+ private static final int DEFAULT_CAPACITY = 16;
+ private static final float LOAD_FACTOR = 0.75f;
+ private static final int NOT_PRESENT = Integer.MIN_VALUE;
+ private int[] mKeys;
+ private ArrayList<T> mValues;
+ int mSize;
+
+ public IntMap() {
+ mKeys = new int[DEFAULT_CAPACITY];
+ Arrays.fill(mKeys, NOT_PRESENT);
+ mValues = new ArrayList<T>(DEFAULT_CAPACITY);
+ for (int i = 0; i < DEFAULT_CAPACITY; i++) {
+ mValues.add(null);
+ }
+ }
+
+ public void clear() {
+ Arrays.fill(mKeys, NOT_PRESENT);
+ mValues.clear();
+ mSize = 0;
+ }
+
+ public T put(int key, T value) {
+ if (key == NOT_PRESENT) throw new IllegalArgumentException("Key cannot be NOT_PRESENT");
+ if (mSize > mKeys.length * LOAD_FACTOR) {
+ resize();
+ }
+ return insert(key, value);
+ }
+
+
+ public T get(int key) {
+ int index = findKey(key);
+ if (index == -1) {
+ return null;
+ } else
+ return mValues.get(index);
+ }
+
+ public int size() {
+ return mSize;
+ }
+
+ private T insert(int key, T value) {
+ int index = hash(key) % mKeys.length;
+ while (mKeys[index] != NOT_PRESENT && mKeys[index] != key) {
+ index = (index + 1) % mKeys.length;
+ }
+ T oldValue = null;
+ if (mKeys[index] == NOT_PRESENT) {
+ mSize++;
+ } else {
+ oldValue = mValues.get(index);
+ }
+ mKeys[index] = key;
+ mValues.set(index, value);
+ return oldValue;
+ }
+
+ private int findKey(int key) {
+ int index = hash(key) % mKeys.length;
+ while (mKeys[index] != NOT_PRESENT) {
+ if (mKeys[index] == key) {
+ return index;
+ }
+ index = (index + 1) % mKeys.length;
+ }
+ return -1;
+ }
+
+ private int hash(int key) {
+ return key;
+ }
+
+ private void resize() {
+ int[] oldKeys = mKeys;
+ ArrayList<T> oldValues = mValues;
+ mKeys = new int[(oldKeys.length * 2)];
+ for (int i = 0; i < mKeys.length; i++) {
+ mKeys[i] = NOT_PRESENT;
+ }
+ mValues = new ArrayList<T>(oldKeys.length * 2);
+ for (int i = 0; i < oldKeys.length * 2; i++) {
+ mValues.add(null);
+ }
+ mSize = 0;
+ for (int i = 0; i < oldKeys.length; i++) {
+ if (oldKeys[i] != NOT_PRESENT) {
+ put(oldKeys[i], oldValues.get(i));
+ }
+ }
+ }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/player/RemoteComposeDocument.java b/core/java/com/android/internal/widget/remotecompose/player/RemoteComposeDocument.java
new file mode 100644
index 000000000000..bcda27a40f64
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/player/RemoteComposeDocument.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.player;
+
+import com.android.internal.widget.remotecompose.core.CoreDocument;
+import com.android.internal.widget.remotecompose.core.RemoteComposeBuffer;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+
+import java.io.InputStream;
+
+/**
+ * Public API to create a new RemoteComposeDocument coming from an input stream
+ */
+public class RemoteComposeDocument {
+
+ CoreDocument mDocument = new CoreDocument();
+
+ public RemoteComposeDocument(InputStream inputStream) {
+ RemoteComposeBuffer buffer =
+ RemoteComposeBuffer.fromInputStream(inputStream, mDocument.getRemoteComposeState());
+ mDocument.initFromBuffer(buffer);
+ }
+
+ public CoreDocument getDocument() {
+ return mDocument;
+ }
+
+ public void setDocument(CoreDocument document) {
+ this.mDocument = document;
+ }
+
+ /**
+ * Called when an initialization is needed, allowing the document to eg load
+ * resources / cache them.
+ */
+ public void initializeContext(RemoteContext context) {
+ mDocument.initializeContext(context);
+ }
+
+ /**
+ * Returns the width of the document in pixels
+ */
+ public int getWidth() {
+ return mDocument.getWidth();
+ }
+
+ /**
+ * Returns the height of the document in pixels
+ */
+ public int getHeight() {
+ return mDocument.getHeight();
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // Painting
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Paint the document
+ *
+ * @param context the provided PaintContext
+ * @param theme the theme we want to use for this document.
+ */
+ public void paint(RemoteContext context, int theme) {
+ mDocument.paint(context, theme);
+ }
+
+ /**
+ * Returns true if the document can be displayed given this version of the player
+ *
+ * @param majorVersion the max major version supported by the player
+ * @param minorVersion the max minor version supported by the player
+ * @param capabilities a bitmask of capabilities the player supports (unused for now)
+ */
+ public boolean canBeDisplayed(int majorVersion, int minorVersion, long capabilities) {
+ return mDocument.canBeDisplayed(majorVersion, minorVersion, capabilities);
+ }
+
+}
+
diff --git a/core/java/com/android/internal/widget/remotecompose/player/RemoteComposePlayer.java b/core/java/com/android/internal/widget/remotecompose/player/RemoteComposePlayer.java
new file mode 100644
index 000000000000..cc1f3ddcc093
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/player/RemoteComposePlayer.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.player;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.HorizontalScrollView;
+import android.widget.ScrollView;
+
+import com.android.internal.widget.remotecompose.core.operations.RootContentBehavior;
+import com.android.internal.widget.remotecompose.player.platform.RemoteComposeCanvas;
+
+/**
+ * A view to to display and play RemoteCompose documents
+ */
+public class RemoteComposePlayer extends FrameLayout {
+ private RemoteComposeCanvas mInner;
+
+ private static final int MAX_SUPPORTED_MAJOR_VERSION = 0;
+ private static final int MAX_SUPPORTED_MINOR_VERSION = 1;
+
+ public RemoteComposePlayer(Context context) {
+ super(context);
+ init(context, null, 0);
+ }
+
+ public RemoteComposePlayer(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context, attrs, 0);
+ }
+
+ public RemoteComposePlayer(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(context, attrs, defStyleAttr);
+ }
+
+ /**
+ * Turn on debug information
+ * @param debugFlags 1 to set debug on
+ */
+ public void setDebug(int debugFlags) {
+ if (debugFlags == 1) {
+ mInner.setDebug(true);
+ } else {
+ mInner.setDebug(false);
+ }
+ }
+
+ public void setDocument(RemoteComposeDocument value) {
+ if (value != null) {
+ if (value.canBeDisplayed(
+ MAX_SUPPORTED_MAJOR_VERSION,
+ MAX_SUPPORTED_MINOR_VERSION, 0L
+ )
+ ) {
+ mInner.setDocument(value);
+ int contentBehavior = value.getDocument().getContentScroll();
+ applyContentBehavior(contentBehavior);
+ } else {
+ Log.e("RemoteComposePlayer", "Unsupported document ");
+ }
+ } else {
+ mInner.setDocument(null);
+ }
+ }
+
+ /**
+ * Apply the content behavior (NONE|SCROLL_HORIZONTAL|SCROLL_VERTICAL) to the player,
+ * adding or removing scrollviews as needed.
+ *
+ * @param contentBehavior document content behavior (NONE|SCROLL_HORIZONTAL|SCROLL_VERTICAL)
+ */
+ private void applyContentBehavior(int contentBehavior) {
+ switch (contentBehavior) {
+ case RootContentBehavior.SCROLL_HORIZONTAL: {
+ if (!(mInner.getParent() instanceof HorizontalScrollView)) {
+ ((ViewGroup) mInner.getParent()).removeView(mInner);
+ removeAllViews();
+ LayoutParams layoutParamsInner = new LayoutParams(
+ LayoutParams.WRAP_CONTENT,
+ LayoutParams.MATCH_PARENT);
+ HorizontalScrollView horizontalScrollView =
+ new HorizontalScrollView(getContext());
+ horizontalScrollView.setFillViewport(true);
+ horizontalScrollView.addView(mInner, layoutParamsInner);
+ LayoutParams layoutParams = new LayoutParams(
+ LayoutParams.MATCH_PARENT,
+ LayoutParams.MATCH_PARENT);
+ addView(horizontalScrollView, layoutParams);
+ }
+ } break;
+ case RootContentBehavior.SCROLL_VERTICAL: {
+ if (!(mInner.getParent() instanceof ScrollView)) {
+ ((ViewGroup) mInner.getParent()).removeView(mInner);
+ removeAllViews();
+ LayoutParams layoutParamsInner = new LayoutParams(
+ LayoutParams.MATCH_PARENT,
+ LayoutParams.WRAP_CONTENT);
+ ScrollView scrollView = new ScrollView(getContext());
+ scrollView.setFillViewport(true);
+ scrollView.addView(mInner, layoutParamsInner);
+ LayoutParams layoutParams = new LayoutParams(
+ LayoutParams.MATCH_PARENT,
+ LayoutParams.MATCH_PARENT);
+ addView(scrollView, layoutParams);
+ }
+ } break;
+ default:
+ if (mInner.getParent() != this) {
+ ((ViewGroup) mInner.getParent()).removeView(mInner);
+ removeAllViews();
+ LayoutParams layoutParams = new LayoutParams(
+ LayoutParams.MATCH_PARENT,
+ LayoutParams.MATCH_PARENT);
+ addView(mInner, layoutParams);
+ }
+ }
+ }
+
+ private void init(Context context, AttributeSet attrs, int defStyleAttr) {
+ LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT,
+ LayoutParams.MATCH_PARENT);
+ mInner = new RemoteComposeCanvas(context, attrs, defStyleAttr);
+ addView(mInner, layoutParams);
+ }
+
+ public interface ClickCallbacks {
+ void click(int id, String metadata);
+ }
+
+ /**
+ * Add a callback for handling click events on the document
+ *
+ * @param callback the callback lambda that will be used when a click is detected
+ * <p>
+ * The parameter of the callback are:
+ * id : the id of the clicked area
+ * metadata: a client provided unstructured string associated with that area
+ */
+ public void addClickListener(ClickCallbacks callback) {
+ mInner.addClickListener((id, metadata) -> callback.click(id, metadata));
+ }
+
+ /**
+ * Set the playback theme for the document. This allows to filter operations in order
+ * to have the document adapt to the given theme. This method is intended to be used
+ * to support night/light themes (system or app level), not custom themes.
+ *
+ * @param theme the theme used for playing the document. Possible values for theme are:
+ * - Theme.UNSPECIFIED -- all instructions in the document will be executed
+ * - Theme.DARK -- only executed NON Light theme instructions
+ * - Theme.LIGHT -- only executed NON Dark theme instructions
+ */
+ public void setTheme(int theme) {
+ if (mInner.getTheme() != theme) {
+ mInner.setTheme(theme);
+ mInner.invalidate();
+ }
+ }
+}
+
diff --git a/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidPaintContext.java b/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidPaintContext.java
new file mode 100644
index 000000000000..3799cf6baac9
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidPaintContext.java
@@ -0,0 +1,93 @@
+/*
+ * 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 com.android.internal.widget.remotecompose.player.platform;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+
+/**
+ * An implementation of PaintContext for the Android Canvas.
+ * This is used to play the RemoteCompose operations on Android.
+ */
+public class AndroidPaintContext extends PaintContext {
+ Paint mPaint = new Paint();
+ Canvas mCanvas;
+
+ public AndroidPaintContext(RemoteContext context, Canvas canvas) {
+ super(context);
+ this.mCanvas = canvas;
+ }
+
+ public Canvas getCanvas() {
+ return mCanvas;
+ }
+
+ public void setCanvas(Canvas canvas) {
+ this.mCanvas = canvas;
+ }
+
+ /**
+ * Draw an image onto the canvas
+ *
+ * @param imageId the id of the image
+ * @param srcLeft left coordinate of the source area
+ * @param srcTop top coordinate of the source area
+ * @param srcRight right coordinate of the source area
+ * @param srcBottom bottom coordinate of the source area
+ * @param dstLeft left coordinate of the destination area
+ * @param dstTop top coordinate of the destination area
+ * @param dstRight right coordinate of the destination area
+ * @param dstBottom bottom coordinate of the destination area
+ */
+
+ @Override
+ public void drawBitmap(int imageId,
+ int srcLeft,
+ int srcTop,
+ int srcRight,
+ int srcBottom,
+ int dstLeft,
+ int dstTop,
+ int dstRight,
+ int dstBottom,
+ int cdId) {
+ AndroidRemoteContext androidContext = (AndroidRemoteContext) mContext;
+ if (androidContext.mRemoteComposeState.containsId(imageId)) {
+ Bitmap bitmap = (Bitmap) androidContext.mRemoteComposeState.getFromId(imageId);
+ mCanvas.drawBitmap(
+ bitmap,
+ new Rect(srcLeft, srcTop, srcRight, srcBottom),
+ new Rect(dstLeft, dstTop, dstRight, dstBottom), mPaint
+ );
+ }
+ }
+
+ @Override
+ public void scale(float scaleX, float scaleY) {
+ mCanvas.scale(scaleX, scaleY);
+ }
+
+ @Override
+ public void translate(float translateX, float translateY) {
+ mCanvas.translate(translateX, translateY);
+ }
+}
+
diff --git a/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidRemoteContext.java b/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidRemoteContext.java
new file mode 100644
index 000000000000..ce15855fecfc
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidRemoteContext.java
@@ -0,0 +1,87 @@
+/*
+ * 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 com.android.internal.widget.remotecompose.player.platform;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+
+/**
+ * An implementation of Context for Android.
+ *
+ * This is used to play the RemoteCompose operations on Android.
+ */
+class AndroidRemoteContext extends RemoteContext {
+
+ public void useCanvas(Canvas canvas) {
+ if (mPaintContext == null) {
+ mPaintContext = new AndroidPaintContext(this, canvas);
+ } else {
+ // need to make sure to update the canvas for the current one
+ ((AndroidPaintContext) mPaintContext).setCanvas(canvas);
+ }
+ mWidth = canvas.getWidth();
+ mHeight = canvas.getHeight();
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // Data handling
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Decode a byte array into an image and cache it using the given imageId
+ *
+ * @oaram imageId the id of the image
+ * @param width with of image to be loaded
+ * @param height height of image to be loaded
+ * @param bitmap a byte array containing the image information
+ */
+ @Override
+ public void loadBitmap(int imageId, int width, int height, byte[] bitmap) {
+ if (!mRemoteComposeState.containsId(imageId)) {
+ Bitmap image = BitmapFactory.decodeByteArray(bitmap, 0, bitmap.length);
+ mRemoteComposeState.cache(imageId, image);
+ }
+ }
+
+ @Override
+ public void loadText(int id, String text) {
+ if (!mRemoteComposeState.containsId(id)) {
+ mRemoteComposeState.cache(id, text);
+ }
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // Click handling
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+
+
+ @Override
+ public void addClickArea(int id,
+ int contentDescriptionId,
+ float left,
+ float top,
+ float right,
+ float bottom,
+ int metadataId) {
+ String contentDescription = (String) mRemoteComposeState.getFromId(contentDescriptionId);
+ String metadata = (String) mRemoteComposeState.getFromId(metadataId);
+ mDocument.addClickArea(id, contentDescription, left, top, right, bottom, metadata);
+ }
+}
+
diff --git a/core/java/com/android/internal/widget/remotecompose/player/platform/ClickAreaView.java b/core/java/com/android/internal/widget/remotecompose/player/platform/ClickAreaView.java
new file mode 100644
index 000000000000..672dae32d8e0
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/player/platform/ClickAreaView.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.player.platform;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.view.View;
+
+
+/**
+ * Implementation for the click handling
+ */
+class ClickAreaView extends View {
+ private int mId;
+ private String mMetadata;
+ Paint mPaint = new Paint();
+
+ private boolean mDebug;
+
+ ClickAreaView(Context context, boolean debug, int id,
+ String contentDescription, String metadata) {
+ super(context);
+ this.mId = id;
+ this.mMetadata = metadata;
+ this.mDebug = debug;
+ setContentDescription(contentDescription);
+ }
+
+
+ public void setDebug(boolean value) {
+ if (mDebug != value) {
+ mDebug = value;
+ invalidate();
+ }
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ if (mDebug) {
+ mPaint.setARGB(200, 200, 0, 0);
+ mPaint.setStrokeWidth(3f);
+ canvas.drawLine(0, 0, getWidth(), 0, mPaint);
+ canvas.drawLine(getWidth(), 0, getWidth(), getHeight(), mPaint);
+ canvas.drawLine(getWidth(), getHeight(), 0, getHeight(), mPaint);
+ canvas.drawLine(0, getHeight(), 0, 0, mPaint);
+
+ mPaint.setTextSize(20f);
+ canvas.drawText("id: " + mId + " : " + mMetadata, 4, 22, mPaint);
+ }
+ }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/player/platform/RemoteComposeCanvas.java b/core/java/com/android/internal/widget/remotecompose/player/platform/RemoteComposeCanvas.java
new file mode 100644
index 000000000000..a3bb73e22c59
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/player/platform/RemoteComposeCanvas.java
@@ -0,0 +1,230 @@
+/*
+ * 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 com.android.internal.widget.remotecompose.player.platform;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Point;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import com.android.internal.widget.remotecompose.core.CoreDocument;
+import com.android.internal.widget.remotecompose.core.operations.RootContentBehavior;
+import com.android.internal.widget.remotecompose.core.operations.Theme;
+import com.android.internal.widget.remotecompose.player.RemoteComposeDocument;
+
+import java.util.Set;
+
+/**
+ * Internal view handling the actual painting / interactions
+ */
+public class RemoteComposeCanvas extends FrameLayout implements View.OnAttachStateChangeListener {
+
+ static final boolean USE_VIEW_AREA_CLICK = true; // Use views to represent click areas
+ RemoteComposeDocument mDocument = null;
+ int mTheme = Theme.LIGHT;
+ boolean mInActionDown = false;
+ boolean mDebug = false;
+ Point mActionDownPoint = new Point(0, 0);
+
+ public RemoteComposeCanvas(Context context) {
+ super(context);
+ if (USE_VIEW_AREA_CLICK) {
+ addOnAttachStateChangeListener(this);
+ }
+ }
+
+ public RemoteComposeCanvas(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ if (USE_VIEW_AREA_CLICK) {
+ addOnAttachStateChangeListener(this);
+ }
+ }
+
+ public RemoteComposeCanvas(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ setBackgroundColor(Color.WHITE);
+ if (USE_VIEW_AREA_CLICK) {
+ addOnAttachStateChangeListener(this);
+ }
+ }
+
+ public void setDebug(boolean value) {
+ if (mDebug != value) {
+ mDebug = value;
+ if (USE_VIEW_AREA_CLICK) {
+ for (int i = 0; i < getChildCount(); i++) {
+ View child = getChildAt(i);
+ if (child instanceof ClickAreaView) {
+ ((ClickAreaView) child).setDebug(mDebug);
+ }
+ }
+ }
+ invalidate();
+ }
+ }
+
+ public void setDocument(RemoteComposeDocument value) {
+ mDocument = value;
+ mDocument.initializeContext(mARContext);
+ setContentDescription(mDocument.getDocument().getContentDescription());
+ requestLayout();
+ }
+
+ AndroidRemoteContext mARContext = new AndroidRemoteContext();
+
+ @Override
+ public void onViewAttachedToWindow(View view) {
+ if (mDocument == null) {
+ return;
+ }
+ Set<CoreDocument.ClickAreaRepresentation> clickAreas = mDocument
+ .getDocument().getClickAreas();
+ removeAllViews();
+ for (CoreDocument.ClickAreaRepresentation area : clickAreas) {
+ ClickAreaView viewArea = new ClickAreaView(getContext(), mDebug,
+ area.getId(), area.getContentDescription(),
+ area.getMetadata());
+ int w = (int) area.width();
+ int h = (int) area.height();
+ FrameLayout.LayoutParams param = new FrameLayout.LayoutParams(w, h);
+ param.width = w;
+ param.height = h;
+ param.leftMargin = (int) area.getLeft();
+ param.topMargin = (int) area.getTop();
+ viewArea.setOnClickListener(view1
+ -> mDocument.getDocument().performClick(area.getId()));
+ addView(viewArea, param);
+ }
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View view) {
+ removeAllViews();
+ }
+
+
+ public interface ClickCallbacks {
+ void click(int id, String metadata);
+ }
+
+ public void addClickListener(ClickCallbacks callback) {
+ if (mDocument == null) {
+ return;
+ }
+ mDocument.getDocument().addClickListener((id, metadata) -> callback.click(id, metadata));
+ }
+
+ public int getTheme() {
+ return mTheme;
+ }
+
+ public void setTheme(int theme) {
+ this.mTheme = theme;
+ }
+
+ public boolean onTouchEvent(MotionEvent event) {
+ if (USE_VIEW_AREA_CLICK) {
+ return super.onTouchEvent(event);
+ }
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN: {
+ mActionDownPoint.x = (int) event.getX();
+ mActionDownPoint.y = (int) event.getY();
+ mInActionDown = true;
+ return true;
+ }
+ case MotionEvent.ACTION_CANCEL: {
+ mInActionDown = false;
+ return true;
+ }
+ case MotionEvent.ACTION_UP: {
+ mInActionDown = false;
+ performClick();
+ return true;
+ }
+ case MotionEvent.ACTION_MOVE: {
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean performClick() {
+ if (USE_VIEW_AREA_CLICK) {
+ return super.performClick();
+ }
+ mDocument.getDocument().onClick((float) mActionDownPoint.x, (float) mActionDownPoint.y);
+ super.performClick();
+ return true;
+ }
+
+ public int measureDimension(int measureSpec, int intrinsicSize) {
+ int result = intrinsicSize;
+ int mode = MeasureSpec.getMode(measureSpec);
+ int size = MeasureSpec.getSize(measureSpec);
+ switch (mode) {
+ case MeasureSpec.EXACTLY:
+ result = size;
+ break;
+ case MeasureSpec.AT_MOST:
+ result = Integer.min(size, intrinsicSize);
+ break;
+ case MeasureSpec.UNSPECIFIED:
+ result = intrinsicSize;
+ }
+ return result;
+ }
+
+ private static final float[] sScaleOutput = new float[2];
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ if (mDocument == null) {
+ return;
+ }
+ int w = measureDimension(widthMeasureSpec, mDocument.getWidth());
+ int h = measureDimension(heightMeasureSpec, mDocument.getHeight());
+
+ if (!USE_VIEW_AREA_CLICK) {
+ if (mDocument.getDocument().getContentSizing() == RootContentBehavior.SIZING_SCALE) {
+ mDocument.getDocument().computeScale(w, h, sScaleOutput);
+ w = (int) (mDocument.getWidth() * sScaleOutput[0]);
+ h = (int) (mDocument.getHeight() * sScaleOutput[1]);
+ }
+ }
+ setMeasuredDimension(w, h);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ if (mDocument == null) {
+ return;
+ }
+ mARContext.setDebug(mDebug);
+ mARContext.useCanvas(canvas);
+ mARContext.mWidth = getWidth();
+ mARContext.mHeight = getHeight();
+ mDocument.paint(mARContext, mTheme);
+ }
+
+}
+