1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.internal.widget.remotecompose.core;
17 
18 import com.android.internal.widget.remotecompose.core.operations.RootContentBehavior;
19 import com.android.internal.widget.remotecompose.core.operations.Theme;
20 
21 import java.util.ArrayList;
22 import java.util.HashSet;
23 import java.util.Set;
24 
25 /**
26  * Represents a platform independent RemoteCompose document,
27  * containing RemoteCompose operations + state
28  */
29 public class CoreDocument {
30 
31     ArrayList<Operation> mOperations;
32     RemoteComposeState mRemoteComposeState = new RemoteComposeState();
33     TimeVariables mTimeVariables = new TimeVariables();
34     // Semantic version of the document
35     Version mVersion = new Version(0, 1, 0);
36 
37     String mContentDescription; // text description of the document (used for accessibility)
38 
39     long mRequiredCapabilities = 0L; // bitmask indicating needed capabilities of the player(unused)
40     int mWidth = 0; // horizontal dimension of the document in pixels
41     int mHeight = 0; // vertical dimension of the document in pixels
42 
43     int mContentScroll = RootContentBehavior.NONE;
44     int mContentSizing = RootContentBehavior.NONE;
45     int mContentMode = RootContentBehavior.NONE;
46 
47     int mContentAlignment = RootContentBehavior.ALIGNMENT_CENTER;
48 
49     RemoteComposeBuffer mBuffer = new RemoteComposeBuffer(mRemoteComposeState);
50 
getContentDescription()51     public String getContentDescription() {
52         return mContentDescription;
53     }
54 
setContentDescription(String contentDescription)55     public void setContentDescription(String contentDescription) {
56         this.mContentDescription = contentDescription;
57     }
58 
getRequiredCapabilities()59     public long getRequiredCapabilities() {
60         return mRequiredCapabilities;
61     }
62 
setRequiredCapabilities(long requiredCapabilities)63     public void setRequiredCapabilities(long requiredCapabilities) {
64         this.mRequiredCapabilities = requiredCapabilities;
65     }
66 
getWidth()67     public int getWidth() {
68         return mWidth;
69     }
70 
setWidth(int width)71     public void setWidth(int width) {
72         this.mWidth = width;
73         mRemoteComposeState.setWindowWidth(width);
74     }
75 
getHeight()76     public int getHeight() {
77         return mHeight;
78     }
79 
setHeight(int height)80     public void setHeight(int height) {
81         this.mHeight = height;
82         mRemoteComposeState.setWindowHeight(height);
83 
84     }
85 
getBuffer()86     public RemoteComposeBuffer getBuffer() {
87         return mBuffer;
88     }
89 
setBuffer(RemoteComposeBuffer buffer)90     public void setBuffer(RemoteComposeBuffer buffer) {
91         this.mBuffer = buffer;
92     }
93 
getRemoteComposeState()94     public RemoteComposeState getRemoteComposeState() {
95         return mRemoteComposeState;
96     }
97 
setRemoteComposeState(RemoteComposeState remoteComposeState)98     public void setRemoteComposeState(RemoteComposeState remoteComposeState) {
99         this.mRemoteComposeState = remoteComposeState;
100     }
101 
getContentScroll()102     public int getContentScroll() {
103         return mContentScroll;
104     }
105 
getContentSizing()106     public int getContentSizing() {
107         return mContentSizing;
108     }
109 
getContentMode()110     public int getContentMode() {
111         return mContentMode;
112     }
113 
114     /**
115      * Sets the way the player handles the content
116      *
117      * @param scroll    set the horizontal behavior (NONE|SCROLL_HORIZONTAL|SCROLL_VERTICAL)
118      * @param alignment set the alignment of the content (TOP|CENTER|BOTTOM|START|END)
119      * @param sizing    set the type of sizing for the content (NONE|SIZING_LAYOUT|SIZING_SCALE)
120      * @param mode      set the mode of sizing, either LAYOUT modes or SCALE modes
121      *                  the LAYOUT modes are:
122      *                  - LAYOUT_MATCH_PARENT
123      *                  - LAYOUT_WRAP_CONTENT
124      *                  or adding an horizontal mode and a vertical mode:
125      *                  - LAYOUT_HORIZONTAL_MATCH_PARENT
126      *                  - LAYOUT_HORIZONTAL_WRAP_CONTENT
127      *                  - LAYOUT_HORIZONTAL_FIXED
128      *                  - LAYOUT_VERTICAL_MATCH_PARENT
129      *                  - LAYOUT_VERTICAL_WRAP_CONTENT
130      *                  - LAYOUT_VERTICAL_FIXED
131      *                  The LAYOUT_*_FIXED modes will use the intrinsic document size
132      */
setRootContentBehavior(int scroll, int alignment, int sizing, int mode)133     public void setRootContentBehavior(int scroll, int alignment, int sizing, int mode) {
134         this.mContentScroll = scroll;
135         this.mContentAlignment = alignment;
136         this.mContentSizing = sizing;
137         this.mContentMode = mode;
138     }
139 
140     /**
141      * Given dimensions w x h of where to paint the content, returns the corresponding scale factor
142      * according to the contentSizing information
143      *
144      * @param w           horizontal dimension of the rendering area
145      * @param h           vertical dimension of the rendering area
146      * @param scaleOutput will contain the computed scale factor
147      */
computeScale(float w, float h, float[] scaleOutput)148     public void computeScale(float w, float h, float[] scaleOutput) {
149         float contentScaleX = 1f;
150         float contentScaleY = 1f;
151         if (mContentSizing == RootContentBehavior.SIZING_SCALE) {
152             // we need to add canvas transforms ops here
153             switch (mContentMode) {
154                 case RootContentBehavior.SCALE_INSIDE: {
155                     float scaleX = w / mWidth;
156                     float scaleY = h / mHeight;
157                     float scale = Math.min(1f, Math.min(scaleX, scaleY));
158                     contentScaleX = scale;
159                     contentScaleY = scale;
160                 }
161                 break;
162                 case RootContentBehavior.SCALE_FIT: {
163                     float scaleX = w / mWidth;
164                     float scaleY = h / mHeight;
165                     float scale = Math.min(scaleX, scaleY);
166                     contentScaleX = scale;
167                     contentScaleY = scale;
168                 }
169                 break;
170                 case RootContentBehavior.SCALE_FILL_WIDTH: {
171                     float scale = w / mWidth;
172                     contentScaleX = scale;
173                     contentScaleY = scale;
174                 }
175                 break;
176                 case RootContentBehavior.SCALE_FILL_HEIGHT: {
177                     float scale = h / mHeight;
178                     contentScaleX = scale;
179                     contentScaleY = scale;
180                 }
181                 break;
182                 case RootContentBehavior.SCALE_CROP: {
183                     float scaleX = w / mWidth;
184                     float scaleY = h / mHeight;
185                     float scale = Math.max(scaleX, scaleY);
186                     contentScaleX = scale;
187                     contentScaleY = scale;
188                 }
189                 break;
190                 case RootContentBehavior.SCALE_FILL_BOUNDS: {
191                     float scaleX = w / mWidth;
192                     float scaleY = h / mHeight;
193                     contentScaleX = scaleX;
194                     contentScaleY = scaleY;
195                 }
196                 break;
197                 default:
198                     // nothing
199             }
200         }
201         scaleOutput[0] = contentScaleX;
202         scaleOutput[1] = contentScaleY;
203     }
204 
205     /**
206      * Given dimensions w x h of where to paint the content, returns the corresponding translation
207      * according to the contentAlignment information
208      *
209      * @param w               horizontal dimension of the rendering area
210      * @param h               vertical dimension of the rendering area
211      * @param contentScaleX   the horizontal scale we are going to use for the content
212      * @param contentScaleY   the vertical scale we are going to use for the content
213      * @param translateOutput will contain the computed translation
214      */
computeTranslate(float w, float h, float contentScaleX, float contentScaleY, float[] translateOutput)215     private void computeTranslate(float w, float h, float contentScaleX, float contentScaleY,
216                                   float[] translateOutput) {
217         int horizontalContentAlignment = mContentAlignment & 0xF0;
218         int verticalContentAlignment = mContentAlignment & 0xF;
219         float translateX = 0f;
220         float translateY = 0f;
221         float contentWidth = mWidth * contentScaleX;
222         float contentHeight = mHeight * contentScaleY;
223 
224         switch (horizontalContentAlignment) {
225             case RootContentBehavior.ALIGNMENT_START: {
226                 // nothing
227             }
228             break;
229             case RootContentBehavior.ALIGNMENT_HORIZONTAL_CENTER: {
230                 translateX = (w - contentWidth) / 2f;
231             }
232             break;
233             case RootContentBehavior.ALIGNMENT_END: {
234                 translateX = w - contentWidth;
235             }
236             break;
237             default:
238                 // nothing (same as alignment_start)
239         }
240         switch (verticalContentAlignment) {
241             case RootContentBehavior.ALIGNMENT_TOP: {
242                 // nothing
243             }
244             break;
245             case RootContentBehavior.ALIGNMENT_VERTICAL_CENTER: {
246                 translateY = (h - contentHeight) / 2f;
247             }
248             break;
249             case RootContentBehavior.ALIGNMENT_BOTTOM: {
250                 translateY = h - contentHeight;
251             }
252             break;
253             default:
254                 // nothing (same as alignment_top)
255         }
256 
257         translateOutput[0] = translateX;
258         translateOutput[1] = translateY;
259     }
260 
getClickAreas()261     public Set<ClickAreaRepresentation> getClickAreas() {
262         return mClickAreas;
263     }
264 
265     public interface ClickCallbacks {
click(int id, String metadata)266         void click(int id, String metadata);
267     }
268 
269     HashSet<ClickCallbacks> mClickListeners = new HashSet<>();
270     HashSet<ClickAreaRepresentation> mClickAreas = new HashSet<>();
271 
272     static class Version {
273         public final int major;
274         public final int minor;
275         public final int patchLevel;
276 
Version(int major, int minor, int patchLevel)277         Version(int major, int minor, int patchLevel) {
278             this.major = major;
279             this.minor = minor;
280             this.patchLevel = patchLevel;
281         }
282     }
283 
284     public static class ClickAreaRepresentation {
285         int mId;
286         String mContentDescription;
287         float mLeft;
288         float mTop;
289         float mRight;
290         float mBottom;
291         String mMetadata;
292 
ClickAreaRepresentation(int id, String contentDescription, float left, float top, float right, float bottom, String metadata)293         public ClickAreaRepresentation(int id,
294                                        String contentDescription,
295                                        float left,
296                                        float top,
297                                        float right,
298                                        float bottom,
299                                        String metadata) {
300             this.mId = id;
301             this.mContentDescription = contentDescription;
302             this.mLeft = left;
303             this.mTop = top;
304             this.mRight = right;
305             this.mBottom = bottom;
306             this.mMetadata = metadata;
307         }
308 
309         /**
310          * Returns true if x,y coordinate is within bounds
311          * @param x x-coordinate
312          * @param y y-coordinate
313          * @return x,y coordinate is within bounds
314          */
contains(float x, float y)315         public boolean contains(float x, float y) {
316             return x >= mLeft && x < mRight
317                     && y >= mTop && y < mBottom;
318         }
319 
getLeft()320         public float getLeft() {
321             return mLeft;
322         }
323 
getTop()324         public float getTop() {
325             return mTop;
326         }
327 
width()328         public float width() {
329             return Math.max(0, mRight - mLeft);
330         }
331 
height()332         public float height() {
333             return Math.max(0, mBottom - mTop);
334         }
335 
getId()336         public int getId() {
337             return mId;
338         }
339 
getContentDescription()340         public String getContentDescription() {
341             return mContentDescription;
342         }
343 
getMetadata()344         public String getMetadata() {
345             return mMetadata;
346         }
347     }
348 
349     /**
350      * Load operations from the given buffer
351      */
initFromBuffer(RemoteComposeBuffer buffer)352     public void initFromBuffer(RemoteComposeBuffer buffer) {
353         mOperations = new ArrayList<Operation>();
354         buffer.inflateFromBuffer(mOperations);
355         mBuffer = buffer;
356     }
357 
358     /**
359      * Called when an initialization is needed, allowing the document to eg load
360      * resources / cache them.
361      */
initializeContext(RemoteContext context)362     public void initializeContext(RemoteContext context) {
363         mRemoteComposeState.reset();
364         mClickAreas.clear();
365         mRemoteComposeState.setNextId(RemoteComposeState.START_ID);
366         context.mDocument = this;
367         context.mRemoteComposeState = mRemoteComposeState;
368         // mark context to be in DATA mode, which will skip the painting ops.
369         context.mMode = RemoteContext.ContextMode.DATA;
370         mTimeVariables.updateTime(context);
371 
372         for (Operation op : mOperations) {
373             if (op instanceof VariableSupport) {
374                 ((VariableSupport) op).updateVariables(context);
375                 ((VariableSupport) op).registerListening(context);
376             }
377             op.apply(context);
378         }
379         context.mMode = RemoteContext.ContextMode.UNSET;
380 
381     }
382 
383     ///////////////////////////////////////////////////////////////////////////////////////////////
384     // Document infos
385     ///////////////////////////////////////////////////////////////////////////////////////////////
386 
387     /**
388      * Returns true if the document can be displayed given this version of the player
389      *
390      * @param majorVersion the max major version supported by the player
391      * @param minorVersion the max minor version supported by the player
392      * @param capabilities a bitmask of capabilities the player supports (unused for now)
393      */
canBeDisplayed(int majorVersion, int minorVersion, long capabilities)394     public boolean canBeDisplayed(int majorVersion, int minorVersion, long capabilities) {
395         return mVersion.major <= majorVersion && mVersion.minor <= minorVersion;
396     }
397 
398     /**
399      * Set the document version, following semantic versioning.
400      *
401      * @param majorVersion major version number, increased upon changes breaking the compatibility
402      * @param minorVersion minor version number, increased when adding new features
403      * @param patch        patch level, increased upon bugfixes
404      */
setVersion(int majorVersion, int minorVersion, int patch)405     void setVersion(int majorVersion, int minorVersion, int patch) {
406         mVersion = new Version(majorVersion, minorVersion, patch);
407     }
408 
409     ///////////////////////////////////////////////////////////////////////////////////////////////
410     // Click handling
411     ///////////////////////////////////////////////////////////////////////////////////////////////
412 
413     /**
414      * Add a click area to the document, in root coordinates. We are not doing any specific sorting
415      * through the declared areas on click detections, which means that the first one containing
416      * the click coordinates will be the one reported; the order of addition of those click areas
417      * is therefore meaningful.
418      *
419      * @param id                 the id of the area, which will be reported on click
420      * @param contentDescription the content description (used for accessibility)
421      * @param left               the left coordinate of the click area (in pixels)
422      * @param top                the top coordinate of the click area (in pixels)
423      * @param right              the right coordinate of the click area (in pixels)
424      * @param bottom             the bottom coordinate of the click area (in pixels)
425      * @param metadata           arbitrary metadata associated with the are, also reported on click
426      */
addClickArea(int id, String contentDescription, float left, float top, float right, float bottom, String metadata)427     public void addClickArea(int id, String contentDescription,
428                              float left, float top, float right, float bottom, String metadata) {
429         mClickAreas.add(new ClickAreaRepresentation(id,
430                 contentDescription, left, top, right, bottom, metadata));
431     }
432 
433     /**
434      * Add a click listener. This will get called when a click is detected on the document
435      *
436      * @param callback called when a click area has been hit, passing the click are id and metadata.
437      */
addClickListener(ClickCallbacks callback)438     public void addClickListener(ClickCallbacks callback) {
439         mClickListeners.add(callback);
440     }
441 
442     /**
443      * Passing a click event to the document. This will possibly result in calling the click
444      * listeners.
445      */
onClick(float x, float y)446     public void onClick(float x, float y) {
447         for (ClickAreaRepresentation clickArea : mClickAreas) {
448             if (clickArea.contains(x, y)) {
449                 warnClickListeners(clickArea);
450             }
451         }
452     }
453 
454     /**
455      * Programmatically trigger the click response for the given id
456      *
457      * @param id the click area id
458      */
performClick(int id)459     public void performClick(int id) {
460         for (ClickAreaRepresentation clickArea : mClickAreas) {
461             if (clickArea.mId == id) {
462                 warnClickListeners(clickArea);
463             }
464         }
465     }
466 
467     /**
468      * Warn click listeners when a click area is activated
469      */
warnClickListeners(ClickAreaRepresentation clickArea)470     private void warnClickListeners(ClickAreaRepresentation clickArea) {
471         for (ClickCallbacks listener : mClickListeners) {
472             listener.click(clickArea.mId, clickArea.mMetadata);
473         }
474     }
475 
476     @Override
toString()477     public String toString() {
478         StringBuilder builder = new StringBuilder();
479         for (Operation op : mOperations) {
480             builder.append(op.toString());
481             builder.append("\n");
482         }
483         return builder.toString();
484     }
485 
486     //////////////////////////////////////////////////////////////////////////
487     // Painting
488     //////////////////////////////////////////////////////////////////////////
489 
490     private final float[] mScaleOutput = new float[2];
491     private final float[] mTranslateOutput = new float[2];
492     private int mRepaintNext = -1; // delay to next repaint -1 = don't 1 = asap
493 
494     /**
495      * Returns > 0 if it needs to repaint
496      * @return
497      */
needsRepaint()498     public int needsRepaint() {
499         return mRepaintNext;
500     }
501 
502     /**
503      * Paint the document
504      *
505      * @param context the provided PaintContext
506      * @param theme   the theme we want to use for this document.
507      */
paint(RemoteContext context, int theme)508     public void paint(RemoteContext context, int theme) {
509         context.mMode = RemoteContext.ContextMode.PAINT;
510 
511         // current theme starts as UNSPECIFIED, until a Theme setter
512         // operation gets executed and modify it.
513         context.setTheme(Theme.UNSPECIFIED);
514 
515         context.mRemoteComposeState = mRemoteComposeState;
516         if (mContentSizing == RootContentBehavior.SIZING_SCALE) {
517             // we need to add canvas transforms ops here
518             computeScale(context.mWidth, context.mHeight, mScaleOutput);
519             computeTranslate(context.mWidth, context.mHeight,
520                     mScaleOutput[0], mScaleOutput[1], mTranslateOutput);
521             context.mPaintContext.translate(mTranslateOutput[0], mTranslateOutput[1]);
522             context.mPaintContext.scale(mScaleOutput[0], mScaleOutput[1]);
523         }
524         mTimeVariables.updateTime(context);
525         context.loadFloat(RemoteContext.ID_WINDOW_WIDTH, getWidth());
526         context.loadFloat(RemoteContext.ID_WINDOW_HEIGHT, getHeight());
527         mRepaintNext = context.updateOps();
528 
529         for (Operation op : mOperations) {
530             // operations will only be executed if no theme is set (ie UNSPECIFIED)
531             // or the theme is equal as the one passed in argument to paint.
532             boolean apply = true;
533             if (theme != Theme.UNSPECIFIED) {
534                 apply = op instanceof Theme // always apply a theme setter
535                         || context.getTheme() == theme
536                         || context.getTheme() == Theme.UNSPECIFIED;
537             }
538             if (apply) {
539                 op.apply(context);
540             }
541         }
542         context.mMode = RemoteContext.ContextMode.UNSET;
543     }
544 
545 }
546 
547