diff options
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); + } + +} + |