• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 android.annotation.NonNull;
19 import android.annotation.Nullable;
20 
21 import com.android.internal.annotations.VisibleForTesting;
22 import com.android.internal.widget.remotecompose.core.operations.BitmapData;
23 import com.android.internal.widget.remotecompose.core.operations.ComponentValue;
24 import com.android.internal.widget.remotecompose.core.operations.DataListFloat;
25 import com.android.internal.widget.remotecompose.core.operations.DrawContent;
26 import com.android.internal.widget.remotecompose.core.operations.FloatConstant;
27 import com.android.internal.widget.remotecompose.core.operations.FloatExpression;
28 import com.android.internal.widget.remotecompose.core.operations.Header;
29 import com.android.internal.widget.remotecompose.core.operations.IntegerExpression;
30 import com.android.internal.widget.remotecompose.core.operations.NamedVariable;
31 import com.android.internal.widget.remotecompose.core.operations.RootContentBehavior;
32 import com.android.internal.widget.remotecompose.core.operations.ShaderData;
33 import com.android.internal.widget.remotecompose.core.operations.TextData;
34 import com.android.internal.widget.remotecompose.core.operations.Theme;
35 import com.android.internal.widget.remotecompose.core.operations.layout.CanvasOperations;
36 import com.android.internal.widget.remotecompose.core.operations.layout.Component;
37 import com.android.internal.widget.remotecompose.core.operations.layout.Container;
38 import com.android.internal.widget.remotecompose.core.operations.layout.ContainerEnd;
39 import com.android.internal.widget.remotecompose.core.operations.layout.LayoutComponent;
40 import com.android.internal.widget.remotecompose.core.operations.layout.LoopOperation;
41 import com.android.internal.widget.remotecompose.core.operations.layout.RootLayoutComponent;
42 import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.ComponentModifiers;
43 import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.ModifierOperation;
44 import com.android.internal.widget.remotecompose.core.operations.utilities.IntMap;
45 import com.android.internal.widget.remotecompose.core.operations.utilities.StringSerializer;
46 import com.android.internal.widget.remotecompose.core.serialize.MapSerializer;
47 import com.android.internal.widget.remotecompose.core.serialize.Serializable;
48 import com.android.internal.widget.remotecompose.core.types.IntegerConstant;
49 import com.android.internal.widget.remotecompose.core.types.LongConstant;
50 
51 import java.util.ArrayList;
52 import java.util.HashMap;
53 import java.util.HashSet;
54 import java.util.List;
55 import java.util.Objects;
56 import java.util.Set;
57 
58 /**
59  * Represents a platform independent RemoteCompose document, containing RemoteCompose operations +
60  * state
61  */
62 public class CoreDocument implements Serializable {
63 
64     private static final boolean DEBUG = false;
65 
66     // Semantic version
67     public static final int MAJOR_VERSION = 1;
68     public static final int MINOR_VERSION = 0;
69     public static final int PATCH_VERSION = 0;
70 
71     // Internal version level
72     public static final int DOCUMENT_API_LEVEL = 6;
73 
74     // We also keep a more fine-grained BUILD number, exposed as
75     // ID_API_LEVEL = DOCUMENT_API_LEVEL + BUILD
76     static final float BUILD = 0.0f;
77 
78     private static final boolean UPDATE_VARIABLES_BEFORE_LAYOUT = false;
79 
80     @NonNull ArrayList<Operation> mOperations = new ArrayList<>();
81 
82     @Nullable RootLayoutComponent mRootLayoutComponent = null;
83 
84     @NonNull RemoteComposeState mRemoteComposeState = new RemoteComposeState();
85     @VisibleForTesting @NonNull public TimeVariables mTimeVariables = new TimeVariables();
86 
87     // Semantic version of the document
88     @NonNull Version mVersion = new Version(MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION);
89 
90     @Nullable
91     String mContentDescription; // text description of the document (used for accessibility)
92 
93     long mRequiredCapabilities = 0L; // bitmask indicating needed capabilities of the player(unused)
94     int mWidth = 0; // horizontal dimension of the document in pixels
95     int mHeight = 0; // vertical dimension of the document in pixels
96 
97     int mContentScroll = RootContentBehavior.NONE;
98     int mContentSizing = RootContentBehavior.NONE;
99     int mContentMode = RootContentBehavior.NONE;
100 
101     int mContentAlignment = RootContentBehavior.ALIGNMENT_CENTER;
102 
103     @NonNull RemoteComposeBuffer mBuffer = new RemoteComposeBuffer(mRemoteComposeState);
104 
105     private final HashMap<Long, IntegerExpression> mIntegerExpressions = new HashMap<>();
106 
107     private final HashMap<Integer, FloatExpression> mFloatExpressions = new HashMap<>();
108 
109     private HashSet<Component> mAppliedTouchOperations = new HashSet<>();
110 
111     private int mLastId = 1; // last component id when inflating the file
112 
113     private IntMap<Object> mDocProperties;
114 
115     boolean mFirstPaint = true;
116     private boolean mIsUpdateDoc = false;
117 
118     /** Returns a version number that is monotonically increasing. */
getDocumentApiLevel()119     public static int getDocumentApiLevel() {
120         return DOCUMENT_API_LEVEL;
121     }
122 
123     @Nullable
getContentDescription()124     public String getContentDescription() {
125         return mContentDescription;
126     }
127 
setContentDescription(@ullable String contentDescription)128     public void setContentDescription(@Nullable String contentDescription) {
129         this.mContentDescription = contentDescription;
130     }
131 
getRequiredCapabilities()132     public long getRequiredCapabilities() {
133         return mRequiredCapabilities;
134     }
135 
setRequiredCapabilities(long requiredCapabilities)136     public void setRequiredCapabilities(long requiredCapabilities) {
137         this.mRequiredCapabilities = requiredCapabilities;
138     }
139 
getWidth()140     public int getWidth() {
141         return mWidth;
142     }
143 
144     /**
145      * Set the viewport width of the document
146      *
147      * @param width document width
148      */
setWidth(int width)149     public void setWidth(int width) {
150         this.mWidth = width;
151         mRemoteComposeState.setWindowWidth(width);
152     }
153 
getHeight()154     public int getHeight() {
155         return mHeight;
156     }
157 
158     /**
159      * Set the viewport height of the document
160      *
161      * @param height document height
162      */
setHeight(int height)163     public void setHeight(int height) {
164         this.mHeight = height;
165         mRemoteComposeState.setWindowHeight(height);
166     }
167 
168     @NonNull
getBuffer()169     public RemoteComposeBuffer getBuffer() {
170         return mBuffer;
171     }
172 
setBuffer(@onNull RemoteComposeBuffer buffer)173     public void setBuffer(@NonNull RemoteComposeBuffer buffer) {
174         this.mBuffer = buffer;
175     }
176 
177     @NonNull
getRemoteComposeState()178     public RemoteComposeState getRemoteComposeState() {
179         return mRemoteComposeState;
180     }
181 
setRemoteComposeState(@onNull RemoteComposeState remoteComposeState)182     public void setRemoteComposeState(@NonNull RemoteComposeState remoteComposeState) {
183         this.mRemoteComposeState = remoteComposeState;
184     }
185 
getContentScroll()186     public int getContentScroll() {
187         return mContentScroll;
188     }
189 
getContentSizing()190     public int getContentSizing() {
191         return mContentSizing;
192     }
193 
getContentMode()194     public int getContentMode() {
195         return mContentMode;
196     }
197 
198     /**
199      * Sets the way the player handles the content
200      *
201      * @param scroll set the horizontal behavior (NONE|SCROLL_HORIZONTAL|SCROLL_VERTICAL)
202      * @param alignment set the alignment of the content (TOP|CENTER|BOTTOM|START|END)
203      * @param sizing set the type of sizing for the content (NONE|SIZING_LAYOUT|SIZING_SCALE)
204      * @param mode set the mode of sizing, either LAYOUT modes or SCALE modes the LAYOUT modes are:
205      *     - LAYOUT_MATCH_PARENT - LAYOUT_WRAP_CONTENT or adding an horizontal mode and a vertical
206      *     mode: - LAYOUT_HORIZONTAL_MATCH_PARENT - LAYOUT_HORIZONTAL_WRAP_CONTENT -
207      *     LAYOUT_HORIZONTAL_FIXED - LAYOUT_VERTICAL_MATCH_PARENT - LAYOUT_VERTICAL_WRAP_CONTENT -
208      *     LAYOUT_VERTICAL_FIXED The LAYOUT_*_FIXED modes will use the intrinsic document size
209      */
setRootContentBehavior(int scroll, int alignment, int sizing, int mode)210     public void setRootContentBehavior(int scroll, int alignment, int sizing, int mode) {
211         this.mContentScroll = scroll;
212         this.mContentAlignment = alignment;
213         this.mContentSizing = sizing;
214         this.mContentMode = mode;
215     }
216 
217     /**
218      * Given dimensions w x h of where to paint the content, returns the corresponding scale factor
219      * according to the contentSizing information
220      *
221      * @param w horizontal dimension of the rendering area
222      * @param h vertical dimension of the rendering area
223      * @param scaleOutput will contain the computed scale factor
224      */
computeScale(float w, float h, @NonNull float[] scaleOutput)225     public void computeScale(float w, float h, @NonNull float[] scaleOutput) {
226         float contentScaleX = 1f;
227         float contentScaleY = 1f;
228         if (mContentSizing == RootContentBehavior.SIZING_SCALE) {
229             // we need to add canvas transforms ops here
230             float scaleX = 1f;
231             float scaleY = 1f;
232             float scale = 1f;
233             switch (mContentMode) {
234                 case RootContentBehavior.SCALE_INSIDE:
235                     scaleX = w / mWidth;
236                     scaleY = h / mHeight;
237                     scale = Math.min(1f, Math.min(scaleX, scaleY));
238                     contentScaleX = scale;
239                     contentScaleY = scale;
240                     break;
241                 case RootContentBehavior.SCALE_FIT:
242                     scaleX = w / mWidth;
243                     scaleY = h / mHeight;
244                     scale = Math.min(scaleX, scaleY);
245                     contentScaleX = scale;
246                     contentScaleY = scale;
247                     break;
248                 case RootContentBehavior.SCALE_FILL_WIDTH:
249                     scale = w / mWidth;
250                     contentScaleX = scale;
251                     contentScaleY = scale;
252                     break;
253                 case RootContentBehavior.SCALE_FILL_HEIGHT:
254                     scale = h / mHeight;
255                     contentScaleX = scale;
256                     contentScaleY = scale;
257                     break;
258                 case RootContentBehavior.SCALE_CROP:
259                     scaleX = w / mWidth;
260                     scaleY = h / mHeight;
261                     scale = Math.max(scaleX, scaleY);
262                     contentScaleX = scale;
263                     contentScaleY = scale;
264                     break;
265                 case RootContentBehavior.SCALE_FILL_BOUNDS:
266                     scaleX = w / mWidth;
267                     scaleY = h / mHeight;
268                     contentScaleX = scaleX;
269                     contentScaleY = scaleY;
270                     break;
271                 default:
272                     // nothing
273             }
274         }
275         scaleOutput[0] = contentScaleX;
276         scaleOutput[1] = contentScaleY;
277     }
278 
279     /**
280      * Given dimensions w x h of where to paint the content, returns the corresponding translation
281      * according to the contentAlignment information
282      *
283      * @param w horizontal dimension of the rendering area
284      * @param h vertical dimension of the rendering area
285      * @param contentScaleX the horizontal scale we are going to use for the content
286      * @param contentScaleY the vertical scale we are going to use for the content
287      * @param translateOutput will contain the computed translation
288      */
computeTranslate( float w, float h, float contentScaleX, float contentScaleY, @NonNull float[] translateOutput)289     private void computeTranslate(
290             float w,
291             float h,
292             float contentScaleX,
293             float contentScaleY,
294             @NonNull float[] translateOutput) {
295         int horizontalContentAlignment = mContentAlignment & 0xF0;
296         int verticalContentAlignment = mContentAlignment & 0xF;
297         float translateX = 0f;
298         float translateY = 0f;
299         float contentWidth = mWidth * contentScaleX;
300         float contentHeight = mHeight * contentScaleY;
301 
302         switch (horizontalContentAlignment) {
303             case RootContentBehavior.ALIGNMENT_START:
304                 // nothing
305                 break;
306             case RootContentBehavior.ALIGNMENT_HORIZONTAL_CENTER:
307                 translateX = (w - contentWidth) / 2f;
308                 break;
309             case RootContentBehavior.ALIGNMENT_END:
310                 translateX = w - contentWidth;
311                 break;
312             default:
313                 // nothing (same as alignment_start)
314         }
315         switch (verticalContentAlignment) {
316             case RootContentBehavior.ALIGNMENT_TOP:
317                 // nothing
318                 break;
319             case RootContentBehavior.ALIGNMENT_VERTICAL_CENTER:
320                 translateY = (h - contentHeight) / 2f;
321                 break;
322             case RootContentBehavior.ALIGNMENT_BOTTOM:
323                 translateY = h - contentHeight;
324                 break;
325             default:
326                 // nothing (same as alignment_top)
327         }
328 
329         translateOutput[0] = translateX;
330         translateOutput[1] = translateY;
331     }
332 
333     /**
334      * Returns the list of click areas
335      *
336      * @return list of click areas in document coordinates
337      */
338     @NonNull
getClickAreas()339     public Set<ClickAreaRepresentation> getClickAreas() {
340         return mClickAreas;
341     }
342 
343     /**
344      * Returns the root layout component
345      *
346      * @return returns the root component if it exists, null otherwise
347      */
348     @Nullable
getRootLayoutComponent()349     public RootLayoutComponent getRootLayoutComponent() {
350         return mRootLayoutComponent;
351     }
352 
353     /** Invalidate the document for layout measures. This will trigger a layout remeasure pass. */
invalidateMeasure()354     public void invalidateMeasure() {
355         if (mRootLayoutComponent != null) {
356             mRootLayoutComponent.invalidateMeasure();
357         }
358     }
359 
360     /**
361      * Returns the component with the given id
362      *
363      * @param id component id
364      * @return the component if it exists, null otherwise
365      */
366     @Nullable
getComponent(int id)367     public Component getComponent(int id) {
368         if (mRootLayoutComponent != null) {
369             return mRootLayoutComponent.getComponent(id);
370         }
371         return null;
372     }
373 
374     /**
375      * Returns a string representation of the component hierarchy of the document
376      *
377      * @return a standardized string representation of the component hierarchy
378      */
379     @NonNull
displayHierarchy()380     public String displayHierarchy() {
381         StringSerializer serializer = new StringSerializer();
382         for (Operation op : mOperations) {
383             if (op instanceof RootLayoutComponent) {
384                 ((RootLayoutComponent) op).displayHierarchy((Component) op, 0, serializer);
385             } else if (op instanceof SerializableToString) {
386                 ((SerializableToString) op).serializeToString(0, serializer);
387             }
388         }
389         return serializer.toString();
390     }
391 
392     /**
393      * Execute an integer expression with the given id and put its value on the targetId
394      *
395      * @param expressionId the id of the integer expression
396      * @param targetId the id of the value to update with the expression
397      * @param context the current context
398      */
evaluateIntExpression( long expressionId, int targetId, @NonNull RemoteContext context)399     public void evaluateIntExpression(
400             long expressionId, int targetId, @NonNull RemoteContext context) {
401         IntegerExpression expression = mIntegerExpressions.get(expressionId);
402         if (expression != null) {
403             int v = expression.evaluate(context);
404             context.overrideInteger(targetId, v);
405         }
406     }
407 
408     /**
409      * Execute an integer expression with the given id and put its value on the targetId
410      *
411      * @param expressionId the id of the integer expression
412      * @param targetId the id of the value to update with the expression
413      * @param context the current context
414      */
evaluateFloatExpression( int expressionId, int targetId, @NonNull RemoteContext context)415     public void evaluateFloatExpression(
416             int expressionId, int targetId, @NonNull RemoteContext context) {
417         FloatExpression expression = mFloatExpressions.get(expressionId);
418         if (expression != null) {
419             float v = expression.evaluate(context);
420             context.overrideFloat(targetId, v);
421         }
422     }
423 
424     @Override
serialize(MapSerializer serializer)425     public void serialize(MapSerializer serializer) {
426         serializer
427                 .addType("CoreDocument")
428                 .add("width", mWidth)
429                 .add("height", mHeight)
430                 .add("operations", mOperations);
431     }
432 
433     /**
434      * Set the properties of the document
435      *
436      * @param properties the properties to set
437      */
setProperties(IntMap<Object> properties)438     public void setProperties(IntMap<Object> properties) {
439         mDocProperties = properties;
440     }
441 
442     /**
443      * @param key the key
444      * @return the value associated with the key
445      */
getProperty(short key)446     public Object getProperty(short key) {
447         if (mDocProperties == null) {
448             return null;
449         }
450         return mDocProperties.get(key);
451     }
452 
453     /**
454      * Apply a collection of operations to the document
455      *
456      * @param delta the delta to apply
457      */
applyUpdate(CoreDocument delta)458     public void applyUpdate(CoreDocument delta) {
459         HashMap<Integer, TextData> txtData = new HashMap<Integer, TextData>();
460         HashMap<Integer, BitmapData> imgData = new HashMap<Integer, BitmapData>();
461         HashMap<Integer, FloatConstant> fltData = new HashMap<Integer, FloatConstant>();
462         HashMap<Integer, IntegerConstant> intData = new HashMap<Integer, IntegerConstant>();
463         HashMap<Integer, LongConstant> longData = new HashMap<Integer, LongConstant>();
464         HashMap<Integer, DataListFloat> floatListData = new HashMap<Integer, DataListFloat>();
465         recursiveTraverse(
466                 mOperations,
467                 (op) -> {
468                     if (op instanceof TextData) {
469                         TextData d = (TextData) op;
470                         txtData.put(d.mTextId, d);
471                     } else if (op instanceof BitmapData) {
472                         BitmapData d = (BitmapData) op;
473                         imgData.put(d.mImageId, d);
474                     } else if (op instanceof FloatConstant) {
475                         FloatConstant d = (FloatConstant) op;
476                         fltData.put(d.mId, d);
477                     } else if (op instanceof IntegerConstant) {
478                         IntegerConstant d = (IntegerConstant) op;
479                         intData.put(d.mId, d);
480                     } else if (op instanceof LongConstant) {
481                         LongConstant d = (LongConstant) op;
482                         longData.put(d.mId, d);
483                     } else if (op instanceof DataListFloat) {
484                         DataListFloat d = (DataListFloat) op;
485                         floatListData.put(d.mId, d);
486                     }
487                 });
488 
489         recursiveTraverse(
490                 delta.mOperations,
491                 (op) -> {
492                     if (op instanceof TextData) {
493                         TextData t = (TextData) op;
494                         TextData txtInDoc = txtData.get(t.mTextId);
495                         if (txtInDoc != null) {
496                             txtInDoc.update(t);
497                             txtInDoc.markDirty();
498                         }
499                     } else if (op instanceof BitmapData) {
500                         BitmapData b = (BitmapData) op;
501                         BitmapData imgInDoc = imgData.get(b.mImageId);
502                         if (imgInDoc != null) {
503                             imgInDoc.update(b);
504                             imgInDoc.markDirty();
505                         }
506                     } else if (op instanceof FloatConstant) {
507                         FloatConstant f = (FloatConstant) op;
508                         FloatConstant fltInDoc = fltData.get(f.mId);
509                         if (fltInDoc != null) {
510                             fltInDoc.update(f);
511                             fltInDoc.markDirty();
512                         }
513                     } else if (op instanceof IntegerConstant) {
514                         IntegerConstant ic = (IntegerConstant) op;
515                         IntegerConstant intInDoc = intData.get(ic.mId);
516                         if (intInDoc != null) {
517                             intInDoc.update(ic);
518                             intInDoc.markDirty();
519                         }
520                     } else if (op instanceof LongConstant) {
521                         LongConstant lc = (LongConstant) op;
522                         LongConstant longInDoc = longData.get(lc.mId);
523                         if (longInDoc != null) {
524                             longInDoc.update(lc);
525                             longInDoc.markDirty();
526                         }
527                     } else if (op instanceof DataListFloat) {
528                         DataListFloat lc = (DataListFloat) op;
529                         DataListFloat longInDoc = floatListData.get(lc.mId);
530                         if (longInDoc != null) {
531                             longInDoc.update(lc);
532                             longInDoc.markDirty();
533                         }
534                     }
535                 });
536     }
537 
538     private interface Visitor {
visit(Operation op)539         void visit(Operation op);
540     }
541 
recursiveTraverse(ArrayList<Operation> mOperations, Visitor visitor)542     private void recursiveTraverse(ArrayList<Operation> mOperations, Visitor visitor) {
543         for (Operation op : mOperations) {
544             if (op instanceof Container) {
545                 recursiveTraverse(((Container) op).getList(), visitor);
546             }
547             visitor.visit(op);
548         }
549     }
550 
551     // ============== Haptic support ==================
552     public interface HapticEngine {
553         /**
554          * Implements a haptic effect
555          *
556          * @param type the type of effect
557          */
haptic(int type)558         void haptic(int type);
559     }
560 
561     HapticEngine mHapticEngine;
562 
setHapticEngine(HapticEngine engine)563     public void setHapticEngine(HapticEngine engine) {
564         mHapticEngine = engine;
565     }
566 
567     /**
568      * Execute an haptic command
569      *
570      * @param type the type of haptic pre-defined effect
571      */
haptic(int type)572     public void haptic(int type) {
573         if (mHapticEngine != null) {
574             mHapticEngine.haptic(type);
575         }
576     }
577 
578     // ============== Haptic support ==================
579 
580     /**
581      * To signal that the given component will apply the touch operation
582      *
583      * @param component the component applying the touch
584      */
appliedTouchOperation(Component component)585     public void appliedTouchOperation(Component component) {
586         mAppliedTouchOperations.add(component);
587     }
588 
589     /** Callback interface for host actions */
590     public interface ActionCallback {
591         /**
592          * Callback for actions
593          *
594          * @param name the action name
595          * @param value the payload of the action
596          */
onAction(@onNull String name, Object value)597         void onAction(@NonNull String name, Object value);
598     }
599 
600     @NonNull HashSet<ActionCallback> mActionListeners = new HashSet<ActionCallback>();
601 
602     /**
603      * Warn action listeners for the given named action
604      *
605      * @param name the action name
606      * @param value a parameter to the action
607      */
runNamedAction(@onNull String name, Object value)608     public void runNamedAction(@NonNull String name, Object value) {
609         // TODO: we might add an interface to group all valid parameter types
610         for (ActionCallback callback : mActionListeners) {
611             callback.onAction(name, value);
612         }
613     }
614 
615     /**
616      * Add a callback for handling the named host actions
617      *
618      * @param callback
619      */
addActionCallback(@onNull ActionCallback callback)620     public void addActionCallback(@NonNull ActionCallback callback) {
621         mActionListeners.add(callback);
622     }
623 
624     /** Clear existing callbacks for named host actions */
clearActionCallbacks()625     public void clearActionCallbacks() {
626         mActionListeners.clear();
627     }
628 
629     /** Id Actions */
630     public interface IdActionCallback {
631         /**
632          * Callback on Id Actions
633          *
634          * @param id the actio id triggered
635          * @param metadata optional metadata
636          */
onAction(int id, @Nullable String metadata)637         void onAction(int id, @Nullable String metadata);
638     }
639 
640     @NonNull HashSet<IdActionCallback> mIdActionListeners = new HashSet<>();
641     @NonNull HashSet<TouchListener> mTouchListeners = new HashSet<>();
642     @NonNull HashSet<ClickAreaRepresentation> mClickAreas = new HashSet<>();
643 
644     static class Version {
645         public final int major;
646         public final int minor;
647         public final int patchLevel;
648 
Version(int major, int minor, int patchLevel)649         Version(int major, int minor, int patchLevel) {
650             this.major = major;
651             this.minor = minor;
652             this.patchLevel = patchLevel;
653         }
654 
655         /**
656          * Returns true if the document has been encoded for at least the given version MAJOR.MINOR
657          *
658          * @param major major version number
659          * @param minor minor version number
660          * @param patch patch version number
661          * @return true if the document was written at least with the given version
662          */
supportsVersion(int major, int minor, int patch)663         public boolean supportsVersion(int major, int minor, int patch) {
664             if (major > this.major) {
665                 return false;
666             }
667             if (major < this.major) {
668                 return true;
669             }
670             // major is the same
671             if (minor > this.minor) {
672                 return false;
673             }
674             if (minor < this.minor) {
675                 return true;
676             }
677             // minor is the same
678             return patch <= this.patchLevel;
679         }
680     }
681 
682     public static class ClickAreaRepresentation {
683         int mId;
684         @Nullable final String mContentDescription;
685         float mLeft;
686         float mTop;
687         float mRight;
688         float mBottom;
689         @Nullable final String mMetadata;
690 
691         @Override
equals(Object o)692         public boolean equals(Object o) {
693             if (this == o) return true;
694             if (!(o instanceof ClickAreaRepresentation)) return false;
695             ClickAreaRepresentation that = (ClickAreaRepresentation) o;
696             return mId == that.mId
697                     && Objects.equals(mContentDescription, that.mContentDescription)
698                     && Objects.equals(mMetadata, that.mMetadata);
699         }
700 
701         @Override
hashCode()702         public int hashCode() {
703             return Objects.hash(mId, mContentDescription, mMetadata);
704         }
705 
ClickAreaRepresentation( int id, @Nullable String contentDescription, float left, float top, float right, float bottom, @Nullable String metadata)706         public ClickAreaRepresentation(
707                 int id,
708                 @Nullable String contentDescription,
709                 float left,
710                 float top,
711                 float right,
712                 float bottom,
713                 @Nullable String metadata) {
714             this.mId = id;
715             this.mContentDescription = contentDescription;
716             this.mLeft = left;
717             this.mTop = top;
718             this.mRight = right;
719             this.mBottom = bottom;
720             this.mMetadata = metadata;
721         }
722 
723         /**
724          * Returns true if x,y coordinate is within bounds
725          *
726          * @param x x-coordinate
727          * @param y y-coordinate
728          * @return x, y coordinate is within bounds
729          */
contains(float x, float y)730         public boolean contains(float x, float y) {
731             return x >= mLeft && x < mRight && y >= mTop && y < mBottom;
732         }
733 
getLeft()734         public float getLeft() {
735             return mLeft;
736         }
737 
getTop()738         public float getTop() {
739             return mTop;
740         }
741 
742         /**
743          * Returns the width of the click area
744          *
745          * @return the width of the click area
746          */
width()747         public float width() {
748             return Math.max(0, mRight - mLeft);
749         }
750 
751         /**
752          * Returns the height of the click area
753          *
754          * @return the height of the click area
755          */
height()756         public float height() {
757             return Math.max(0, mBottom - mTop);
758         }
759 
getId()760         public int getId() {
761             return mId;
762         }
763 
getContentDescription()764         public @Nullable String getContentDescription() {
765             return mContentDescription;
766         }
767 
768         @Nullable
getMetadata()769         public String getMetadata() {
770             return mMetadata;
771         }
772     }
773 
774     /** Load operations from the given buffer */
initFromBuffer(@onNull RemoteComposeBuffer buffer)775     public void initFromBuffer(@NonNull RemoteComposeBuffer buffer) {
776         mOperations = new ArrayList<Operation>();
777         buffer.inflateFromBuffer(mOperations);
778         for (Operation op : mOperations) {
779             if (op instanceof Header) {
780                 // Make sure we parse the version at init time...
781                 Header header = (Header) op;
782                 header.setVersion(this);
783             }
784             if (op instanceof IntegerExpression) {
785                 IntegerExpression expression = (IntegerExpression) op;
786                 mIntegerExpressions.put((long) expression.mId, expression);
787             }
788             if (op instanceof FloatExpression) {
789                 FloatExpression expression = (FloatExpression) op;
790                 mFloatExpressions.put(expression.mId, expression);
791             }
792         }
793         mOperations = inflateComponents(mOperations);
794         mBuffer = buffer;
795         for (Operation op : mOperations) {
796             if (op instanceof RootLayoutComponent) {
797                 mRootLayoutComponent = (RootLayoutComponent) op;
798                 break;
799             }
800         }
801         if (mRootLayoutComponent != null) {
802             mRootLayoutComponent.assignIds(mLastId);
803         }
804     }
805 
806     /**
807      * Inflate a component tree
808      *
809      * @param operations flat list of operations
810      * @return nested list of operations / components
811      */
812     @NonNull
inflateComponents(@onNull ArrayList<Operation> operations)813     private ArrayList<Operation> inflateComponents(@NonNull ArrayList<Operation> operations) {
814         ArrayList<Operation> finalOperationsList = new ArrayList<>();
815         ArrayList<Operation> ops = finalOperationsList;
816 
817         ArrayList<Container> containers = new ArrayList<>();
818         LayoutComponent lastLayoutComponent = null;
819 
820         mLastId = -1;
821         for (Operation o : operations) {
822             if (o instanceof Container) {
823                 Container container = (Container) o;
824                 if (container instanceof Component) {
825                     Component component = (Component) container;
826                     // Make sure to set the parent when a component is first found, so that
827                     // the inflate when closing the component is in a state where the hierarchy
828                     // is already existing.
829                     if (!containers.isEmpty()) {
830                         Container parentContainer = containers.get(containers.size() - 1);
831                         if (parentContainer instanceof Component) {
832                             component.setParent((Component) parentContainer);
833                         }
834                     }
835                     if (component.getComponentId() < mLastId) {
836                         mLastId = component.getComponentId();
837                     }
838                     if (component instanceof LayoutComponent) {
839                         lastLayoutComponent = (LayoutComponent) component;
840                     }
841                 }
842                 containers.add(container);
843                 ops = container.getList();
844             } else if (o instanceof ContainerEnd) {
845                 // check if we have a parent container
846                 Container container = null;
847                 // pop the container
848                 if (!containers.isEmpty()) {
849                     container = containers.remove(containers.size() - 1);
850                 }
851                 Container parentContainer = null;
852                 if (!containers.isEmpty()) {
853                     parentContainer = containers.get(containers.size() - 1);
854                 }
855                 if (parentContainer != null) {
856                     ops = parentContainer.getList();
857                 } else {
858                     ops = finalOperationsList;
859                 }
860                 if (container != null) {
861                     if (container instanceof Component) {
862                         Component component = (Component) container;
863                         component.inflate();
864                     }
865                     ops.add((Operation) container);
866                 }
867                 if (container instanceof CanvasOperations) {
868                     ((CanvasOperations) container).setComponent(lastLayoutComponent);
869                 }
870             } else {
871                 if (o instanceof DrawContent) {
872                     ((DrawContent) o).setComponent(lastLayoutComponent);
873                 }
874                 ops.add(o);
875             }
876         }
877         return ops;
878     }
879 
880     @NonNull private HashMap<Integer, Component> mComponentMap = new HashMap<Integer, Component>();
881 
882     /**
883      * Register all the operations recursively
884      *
885      * @param context
886      * @param list
887      */
registerVariables( @onNull RemoteContext context, @NonNull ArrayList<Operation> list)888     private void registerVariables(
889             @NonNull RemoteContext context, @NonNull ArrayList<Operation> list) {
890         for (Operation op : list) {
891             if (op instanceof VariableSupport) {
892                 ((VariableSupport) op).registerListening(context);
893             }
894             if (op instanceof Component) {
895                 mComponentMap.put(((Component) op).getComponentId(), (Component) op);
896                 ((Component) op).registerVariables(context);
897             }
898             if (op instanceof Container) {
899                 registerVariables(context, ((Container) op).getList());
900             }
901             if (op instanceof ComponentValue) {
902                 ComponentValue v = (ComponentValue) op;
903                 Component component = mComponentMap.get(v.getComponentId());
904                 if (component != null) {
905                     component.addComponentValue(v);
906                 } else {
907                     System.out.println("=> Component not found for id " + v.getComponentId());
908                 }
909             }
910             if (op instanceof ComponentModifiers) {
911                 for (ModifierOperation modifier : ((ComponentModifiers) op).getList()) {
912                     if (modifier instanceof VariableSupport) {
913                         ((VariableSupport) modifier).registerListening(context);
914                     }
915                 }
916             }
917         }
918     }
919 
920     /**
921      * Apply the operations recursively, for the original initialization pass with mode == DATA
922      *
923      * @param context
924      * @param list
925      */
applyOperations( @onNull RemoteContext context, @NonNull ArrayList<Operation> list)926     private void applyOperations(
927             @NonNull RemoteContext context, @NonNull ArrayList<Operation> list) {
928         for (Operation op : list) {
929             if (op instanceof VariableSupport) {
930                 ((VariableSupport) op).updateVariables(context);
931             }
932             if (op instanceof Component) { // for componentvalues...
933                 ((Component) op).updateVariables(context);
934             }
935             op.markNotDirty();
936             op.apply(context);
937             context.incrementOpCount();
938             if (op instanceof Container) {
939                 applyOperations(context, ((Container) op).getList());
940             }
941         }
942     }
943 
944     /**
945      * Called when an initialization is needed, allowing the document to eg load resources / cache
946      * them.
947      */
initializeContext(@onNull RemoteContext context)948     public void initializeContext(@NonNull RemoteContext context) {
949         mRemoteComposeState.reset();
950         mRemoteComposeState.setContext(context);
951         mClickAreas.clear();
952         mRemoteComposeState.setNextId(RemoteComposeState.START_ID);
953         context.mDocument = this;
954         context.mRemoteComposeState = mRemoteComposeState;
955         // mark context to be in DATA mode, which will skip the painting ops.
956         context.mMode = RemoteContext.ContextMode.DATA;
957         mTimeVariables.updateTime(context);
958 
959         registerVariables(context, mOperations);
960         applyOperations(context, mOperations);
961         context.mMode = RemoteContext.ContextMode.UNSET;
962 
963         if (UPDATE_VARIABLES_BEFORE_LAYOUT) {
964             mFirstPaint = true;
965         }
966     }
967 
968     ///////////////////////////////////////////////////////////////////////////////////////////////
969     // Document infos
970     ///////////////////////////////////////////////////////////////////////////////////////////////
971 
972     /**
973      * Returns true if the document can be displayed given this version of the player
974      *
975      * @param playerMajorVersion the max major version supported by the player
976      * @param playerMinorVersion the max minor version supported by the player
977      * @param capabilities a bitmask of capabilities the player supports (unused for now)
978      */
canBeDisplayed( int playerMajorVersion, int playerMinorVersion, long capabilities)979     public boolean canBeDisplayed(
980             int playerMajorVersion, int playerMinorVersion, long capabilities) {
981         if (mVersion.major < playerMajorVersion) {
982             return true;
983         }
984         if (mVersion.major > playerMajorVersion) {
985             return false;
986         }
987         // same major version
988         return mVersion.minor <= playerMinorVersion;
989     }
990 
991     /**
992      * Set the document version, following semantic versioning.
993      *
994      * @param majorVersion major version number, increased upon changes breaking the compatibility
995      * @param minorVersion minor version number, increased when adding new features
996      * @param patch patch level, increased upon bugfixes
997      */
setVersion(int majorVersion, int minorVersion, int patch)998     public void setVersion(int majorVersion, int minorVersion, int patch) {
999         mVersion = new Version(majorVersion, minorVersion, patch);
1000     }
1001 
1002     ///////////////////////////////////////////////////////////////////////////////////////////////
1003     // Click handling
1004     ///////////////////////////////////////////////////////////////////////////////////////////////
1005 
1006     /**
1007      * Add a click area to the document, in root coordinates. We are not doing any specific sorting
1008      * through the declared areas on click detections, which means that the first one containing the
1009      * click coordinates will be the one reported; the order of addition of those click areas is
1010      * therefore meaningful.
1011      *
1012      * @param id the id of the area, which will be reported on click
1013      * @param contentDescription the content description (used for accessibility)
1014      * @param left the left coordinate of the click area (in pixels)
1015      * @param top the top coordinate of the click area (in pixels)
1016      * @param right the right coordinate of the click area (in pixels)
1017      * @param bottom the bottom coordinate of the click area (in pixels)
1018      * @param metadata arbitrary metadata associated with the are, also reported on click
1019      */
addClickArea( int id, @Nullable String contentDescription, float left, float top, float right, float bottom, @Nullable String metadata)1020     public void addClickArea(
1021             int id,
1022             @Nullable String contentDescription,
1023             float left,
1024             float top,
1025             float right,
1026             float bottom,
1027             @Nullable String metadata) {
1028 
1029         ClickAreaRepresentation car =
1030                 new ClickAreaRepresentation(
1031                         id, contentDescription, left, top, right, bottom, metadata);
1032 
1033         boolean old = mClickAreas.remove(car);
1034         mClickAreas.add(car);
1035     }
1036 
1037     /**
1038      * Called by commands to listen to touch events
1039      *
1040      * @param listener
1041      */
addTouchListener(TouchListener listener)1042     public void addTouchListener(TouchListener listener) {
1043         mTouchListeners.add(listener);
1044     }
1045 
1046     /**
1047      * Add an id action listener. This will get called when e.g. a click is detected on the document
1048      *
1049      * @param callback called when an action is executed, passing the id and metadata.
1050      */
addIdActionListener(@onNull IdActionCallback callback)1051     public void addIdActionListener(@NonNull IdActionCallback callback) {
1052         mIdActionListeners.add(callback);
1053     }
1054 
1055     /**
1056      * Returns the list of set click listeners
1057      *
1058      * @return set of click listeners
1059      */
1060     @NonNull
getIdActionListeners()1061     public HashSet<IdActionCallback> getIdActionListeners() {
1062         return mIdActionListeners;
1063     }
1064 
1065     /**
1066      * Passing a click event to the document. This will possibly result in calling the click
1067      * listeners.
1068      */
onClick(@onNull RemoteContext context, float x, float y)1069     public void onClick(@NonNull RemoteContext context, float x, float y) {
1070         for (ClickAreaRepresentation clickArea : mClickAreas) {
1071             if (clickArea.contains(x, y)) {
1072                 warnClickListeners(clickArea);
1073             }
1074         }
1075         if (mRootLayoutComponent != null) {
1076             mRootLayoutComponent.onClick(context, this, x, y);
1077         }
1078     }
1079 
1080     /**
1081      * Programmatically trigger the click response for the given id
1082      *
1083      * @param id the click area id
1084      */
performClick(@onNull RemoteContext context, int id, @NonNull String metadata)1085     public void performClick(@NonNull RemoteContext context, int id, @NonNull String metadata) {
1086         for (ClickAreaRepresentation clickArea : mClickAreas) {
1087             if (clickArea.mId == id) {
1088                 warnClickListeners(clickArea);
1089                 return;
1090             }
1091         }
1092 
1093         for (IdActionCallback listener : mIdActionListeners) {
1094             listener.onAction(id, metadata);
1095         }
1096 
1097         Component component = getComponent(id);
1098         if (component != null) {
1099             component.onClick(context, this, -1, -1);
1100         }
1101     }
1102 
1103     /** Warn click listeners when a click area is activated */
warnClickListeners(@onNull ClickAreaRepresentation clickArea)1104     private void warnClickListeners(@NonNull ClickAreaRepresentation clickArea) {
1105         for (IdActionCallback listener : mIdActionListeners) {
1106             listener.onAction(clickArea.mId, clickArea.mMetadata);
1107         }
1108     }
1109 
1110     /**
1111      * Returns true if the document has touch listeners
1112      *
1113      * @return true if the document needs to react to touch events
1114      */
hasTouchListener()1115     public boolean hasTouchListener() {
1116         boolean hasComponentsTouchListeners =
1117                 mRootLayoutComponent != null && mRootLayoutComponent.hasTouchListeners();
1118         return hasComponentsTouchListeners || !mTouchListeners.isEmpty();
1119     }
1120 
1121     // TODO support velocity estimate support, support regions
1122     /**
1123      * Support touch drag events on commands supporting touch
1124      *
1125      * @param x position of touch
1126      * @param y position of touch
1127      */
touchDrag(RemoteContext context, float x, float y)1128     public boolean touchDrag(RemoteContext context, float x, float y) {
1129         context.loadFloat(RemoteContext.ID_TOUCH_POS_X, x);
1130         context.loadFloat(RemoteContext.ID_TOUCH_POS_Y, y);
1131         for (TouchListener clickArea : mTouchListeners) {
1132             clickArea.touchDrag(context, x, y);
1133         }
1134         if (mRootLayoutComponent != null) {
1135             for (Component component : mAppliedTouchOperations) {
1136                 component.onTouchDrag(context, this, x, y, true);
1137             }
1138             if (!mAppliedTouchOperations.isEmpty()) {
1139                 return true;
1140             }
1141         }
1142         if (!mTouchListeners.isEmpty()) {
1143             return true;
1144         }
1145         return false;
1146     }
1147 
1148     /**
1149      * Support touch down events on commands supporting touch
1150      *
1151      * @param x position of touch
1152      * @param y position of touch
1153      */
touchDown(RemoteContext context, float x, float y)1154     public void touchDown(RemoteContext context, float x, float y) {
1155         context.loadFloat(RemoteContext.ID_TOUCH_POS_X, x);
1156         context.loadFloat(RemoteContext.ID_TOUCH_POS_Y, y);
1157         for (TouchListener clickArea : mTouchListeners) {
1158             clickArea.touchDown(context, x, y);
1159         }
1160         if (mRootLayoutComponent != null) {
1161             mRootLayoutComponent.onTouchDown(context, this, x, y);
1162         }
1163         mRepaintNext = 1;
1164     }
1165 
1166     /**
1167      * Support touch up events on commands supporting touch
1168      *
1169      * @param x position of touch
1170      * @param y position of touch
1171      */
touchUp(RemoteContext context, float x, float y, float dx, float dy)1172     public void touchUp(RemoteContext context, float x, float y, float dx, float dy) {
1173         context.loadFloat(RemoteContext.ID_TOUCH_POS_X, x);
1174         context.loadFloat(RemoteContext.ID_TOUCH_POS_Y, y);
1175         for (TouchListener clickArea : mTouchListeners) {
1176             clickArea.touchUp(context, x, y, dx, dy);
1177         }
1178         if (mRootLayoutComponent != null) {
1179             for (Component component : mAppliedTouchOperations) {
1180                 component.onTouchUp(context, this, x, y, dx, dy, true);
1181             }
1182             mAppliedTouchOperations.clear();
1183         }
1184         mRepaintNext = 1;
1185     }
1186 
1187     /**
1188      * Support touch cancel events on commands supporting touch
1189      *
1190      * @param x position of touch
1191      * @param y position of touch
1192      */
touchCancel(RemoteContext context, float x, float y, float dx, float dy)1193     public void touchCancel(RemoteContext context, float x, float y, float dx, float dy) {
1194         if (mRootLayoutComponent != null) {
1195             for (Component component : mAppliedTouchOperations) {
1196                 component.onTouchCancel(context, this, x, y, true);
1197             }
1198             mAppliedTouchOperations.clear();
1199         }
1200         mRepaintNext = 1;
1201     }
1202 
1203     @NonNull
1204     @Override
toString()1205     public String toString() {
1206         StringBuilder builder = new StringBuilder();
1207         for (Operation op : mOperations) {
1208             builder.append(op.toString());
1209             builder.append("\n");
1210         }
1211         return builder.toString();
1212     }
1213 
1214     /**
1215      * Gets the names of all named colors.
1216      *
1217      * @return array of named colors or null
1218      */
1219     @Nullable
getNamedColors()1220     public String[] getNamedColors() {
1221         return getNamedVariables(NamedVariable.COLOR_TYPE);
1222     }
1223 
1224     /**
1225      * Gets the names of all named Variables.
1226      *
1227      * @return array of named variables or null
1228      */
getNamedVariables(int type)1229     public String[] getNamedVariables(int type) {
1230         ArrayList<String> ret = new ArrayList<>();
1231         getNamedVars(type, mOperations, ret);
1232         return ret.toArray(new String[0]);
1233     }
1234 
getNamedVars(int type, ArrayList<Operation> ops, ArrayList<String> list)1235     private void getNamedVars(int type, ArrayList<Operation> ops, ArrayList<String> list) {
1236         for (Operation op : ops) {
1237             if (op instanceof NamedVariable) {
1238                 NamedVariable n = (NamedVariable) op;
1239                 if (n.mVarType == type) {
1240                     list.add(n.mVarName);
1241                 }
1242             }
1243             if (op instanceof Container) {
1244                 getNamedVars(type, ((Container) op).getList(), list);
1245             }
1246         }
1247     }
1248 
1249     //////////////////////////////////////////////////////////////////////////
1250     // Painting
1251     //////////////////////////////////////////////////////////////////////////
1252 
1253     private final float[] mScaleOutput = new float[2];
1254     private final float[] mTranslateOutput = new float[2];
1255     private int mRepaintNext = -1; // delay to next repaint -1 = don't 1 = asap
1256     private int mLastOpCount;
1257 
1258     /**
1259      * This is the number of ops used to calculate the last frame.
1260      *
1261      * @return number of ops
1262      */
getOpsPerFrame()1263     public int getOpsPerFrame() {
1264         return mLastOpCount;
1265     }
1266 
1267     /**
1268      * Returns > 0 if it needs to repaint
1269      *
1270      * @return
1271      */
needsRepaint()1272     public int needsRepaint() {
1273         return mRepaintNext;
1274     }
1275 
1276     /**
1277      * Traverse the list of operations to update the variables. TODO: this should walk the
1278      * dependency tree instead
1279      *
1280      * @param context
1281      * @param operations
1282      */
updateVariables( @onNull RemoteContext context, int theme, List<Operation> operations)1283     private void updateVariables(
1284             @NonNull RemoteContext context, int theme, List<Operation> operations) {
1285         for (int i = 0; i < operations.size(); i++) {
1286             Operation op = operations.get(i);
1287             if (op.isDirty() && op instanceof VariableSupport) {
1288                 ((VariableSupport) op).updateVariables(context);
1289                 op.apply(context);
1290                 op.markNotDirty();
1291             }
1292             if (op instanceof Container) {
1293                 updateVariables(context, theme, ((Container) op).getList());
1294             }
1295         }
1296     }
1297 
1298     /**
1299      * Paint the document
1300      *
1301      * @param context the provided PaintContext
1302      * @param theme the theme we want to use for this document.
1303      */
paint(@onNull RemoteContext context, int theme)1304     public void paint(@NonNull RemoteContext context, int theme) {
1305         context.clearLastOpCount();
1306         context.getPaintContext().clearNeedsRepaint();
1307         context.loadFloat(RemoteContext.ID_DENSITY, context.getDensity());
1308         context.mMode = RemoteContext.ContextMode.UNSET;
1309         // current theme starts as UNSPECIFIED, until a Theme setter
1310         // operation gets executed and modify it.
1311         context.setTheme(Theme.UNSPECIFIED);
1312 
1313         context.mRemoteComposeState = mRemoteComposeState;
1314         context.mRemoteComposeState.setContext(context);
1315 
1316         if (UPDATE_VARIABLES_BEFORE_LAYOUT) {
1317             // Update any dirty variables
1318             if (mFirstPaint) {
1319                 mFirstPaint = false;
1320             } else {
1321                 updateVariables(context, theme, mOperations);
1322             }
1323         }
1324 
1325         // If we have a content sizing set, we are going to take the original document
1326         // dimension into account and apply scale+translate according to the RootContentBehavior
1327         // rules.
1328         if (mContentSizing == RootContentBehavior.SIZING_SCALE) {
1329             // we need to add canvas transforms ops here
1330             computeScale(context.mWidth, context.mHeight, mScaleOutput);
1331             float sw = mScaleOutput[0];
1332             float sh = mScaleOutput[1];
1333             computeTranslate(context.mWidth, context.mHeight, sw, sh, mTranslateOutput);
1334             context.mPaintContext.translate(mTranslateOutput[0], mTranslateOutput[1]);
1335             context.mPaintContext.scale(sw, sh);
1336         } else {
1337             // If not, we set the document width and height to be the current context width and
1338             // height.
1339             setWidth((int) context.mWidth);
1340             setHeight((int) context.mHeight);
1341         }
1342         mTimeVariables.updateTime(context);
1343         mRepaintNext = context.updateOps();
1344         if (mRootLayoutComponent != null) {
1345             if (context.mWidth != mRootLayoutComponent.getWidth()
1346                     || context.mHeight != mRootLayoutComponent.getHeight()) {
1347                 mRootLayoutComponent.invalidateMeasure();
1348             }
1349             if (mRootLayoutComponent.needsMeasure()) {
1350                 mRootLayoutComponent.layout(context);
1351             }
1352             if (mRootLayoutComponent.needsBoundsAnimation()) {
1353                 mRepaintNext = 1;
1354                 mRootLayoutComponent.clearNeedsBoundsAnimation();
1355                 mRootLayoutComponent.animatingBounds(context);
1356             }
1357             if (DEBUG) {
1358                 String hierarchy = mRootLayoutComponent.displayHierarchy();
1359                 System.out.println(hierarchy);
1360             }
1361             if (mRootLayoutComponent.doesNeedsRepaint()) {
1362                 mRepaintNext = 1;
1363             }
1364         }
1365         context.mMode = RemoteContext.ContextMode.PAINT;
1366         for (int i = 0; i < mOperations.size(); i++) {
1367             Operation op = mOperations.get(i);
1368             // operations will only be executed if no theme is set (ie UNSPECIFIED)
1369             // or the theme is equal as the one passed in argument to paint.
1370             boolean apply = true;
1371             if (theme != Theme.UNSPECIFIED) {
1372                 int currentTheme = context.getTheme();
1373                 apply =
1374                         currentTheme == theme
1375                                 || currentTheme == Theme.UNSPECIFIED
1376                                 || op instanceof Theme; // always apply a theme setter
1377             }
1378             if (apply) {
1379                 boolean opIsDirty = op.isDirty();
1380                 if (opIsDirty || op instanceof PaintOperation) {
1381                     if (opIsDirty && op instanceof VariableSupport) {
1382                         op.markNotDirty();
1383                         ((VariableSupport) op).updateVariables(context);
1384                     }
1385                     context.incrementOpCount();
1386                     op.apply(context);
1387                 }
1388             }
1389         }
1390         if (context.getPaintContext().doesNeedsRepaint()
1391                 || (mRootLayoutComponent != null && mRootLayoutComponent.doesNeedsRepaint())) {
1392             mRepaintNext = 1;
1393         }
1394         context.mMode = RemoteContext.ContextMode.UNSET;
1395         if (DEBUG && mRootLayoutComponent != null) {
1396             System.out.println(mRootLayoutComponent.displayHierarchy());
1397         }
1398         mLastOpCount = context.getLastOpCount();
1399     }
1400 
1401     /**
1402      * Get an estimated number of operations executed in a paint
1403      *
1404      * @return number of operations
1405      */
getNumberOfOps()1406     public int getNumberOfOps() {
1407         int count = mOperations.size();
1408 
1409         for (Operation mOperation : mOperations) {
1410             if (mOperation instanceof Component) {
1411                 count += getChildOps((Component) mOperation);
1412             }
1413         }
1414         return count;
1415     }
1416 
getChildOps(@onNull Component base)1417     private int getChildOps(@NonNull Component base) {
1418         int count = base.mList.size();
1419         for (Operation mOperation : base.mList) {
1420 
1421             if (mOperation instanceof Component) {
1422                 int mult = 1;
1423                 if (mOperation instanceof LoopOperation) {
1424                     mult = ((LoopOperation) mOperation).estimateIterations();
1425                 }
1426                 count += mult * getChildOps((Component) mOperation);
1427             }
1428         }
1429         return count;
1430     }
1431 
1432     /**
1433      * Returns a list of useful statistics for the runtime document
1434      *
1435      * @return
1436      */
1437     @NonNull
getStats()1438     public String[] getStats() {
1439         ArrayList<String> ret = new ArrayList<>();
1440         WireBuffer buffer = new WireBuffer();
1441         int count = mOperations.size();
1442         HashMap<String, int[]> map = new HashMap<>();
1443         for (Operation mOperation : mOperations) {
1444             Class<? extends Operation> c = mOperation.getClass();
1445             int[] values;
1446             if (map.containsKey(c.getSimpleName())) {
1447                 values = map.get(c.getSimpleName());
1448             } else {
1449                 values = new int[2];
1450                 map.put(c.getSimpleName(), values);
1451             }
1452 
1453             values[0] += 1;
1454             values[1] += sizeOfComponent(mOperation, buffer);
1455             if (mOperation instanceof Container) {
1456                 Container com = (Container) mOperation;
1457                 count += addChildren(com, map, buffer);
1458             } else if (mOperation instanceof LoopOperation) {
1459                 LoopOperation com = (LoopOperation) mOperation;
1460                 count += addChildren(com, map, buffer);
1461             }
1462         }
1463 
1464         ret.add(0, "number of operations : " + count);
1465 
1466         for (String s : map.keySet()) {
1467             int[] v = map.get(s);
1468             ret.add(s + " : " + v[0] + ":" + v[1]);
1469         }
1470         return ret.toArray(new String[0]);
1471     }
1472 
sizeOfComponent(@onNull Operation com, @NonNull WireBuffer tmp)1473     private int sizeOfComponent(@NonNull Operation com, @NonNull WireBuffer tmp) {
1474         tmp.reset(100);
1475         com.write(tmp);
1476         int size = tmp.getSize();
1477         tmp.reset(100);
1478         return size;
1479     }
1480 
addChildren( @onNull Container base, @NonNull HashMap<String, int[]> map, @NonNull WireBuffer tmp)1481     private int addChildren(
1482             @NonNull Container base, @NonNull HashMap<String, int[]> map, @NonNull WireBuffer tmp) {
1483         int count = base.getList().size();
1484         for (Operation mOperation : base.getList()) {
1485             Class<? extends Operation> c = mOperation.getClass();
1486             int[] values;
1487             if (map.containsKey(c.getSimpleName())) {
1488                 values = map.get(c.getSimpleName());
1489             } else {
1490                 values = new int[2];
1491                 map.put(c.getSimpleName(), values);
1492             }
1493             values[0] += 1;
1494             values[1] += sizeOfComponent(mOperation, tmp);
1495             if (mOperation instanceof Container) {
1496                 count += addChildren((Container) mOperation, map, tmp);
1497             }
1498         }
1499         return count;
1500     }
1501 
1502     /**
1503      * Returns a string representation of the operations, traversing the list of operations &
1504      * containers
1505      *
1506      * @return
1507      */
1508     @NonNull
toNestedString()1509     public String toNestedString() {
1510         StringBuilder ret = new StringBuilder();
1511         for (Operation mOperation : mOperations) {
1512             ret.append(mOperation.toString());
1513             ret.append("\n");
1514             if (mOperation instanceof Container) {
1515                 toNestedString((Container) mOperation, ret, "  ");
1516             }
1517         }
1518         return ret.toString();
1519     }
1520 
toNestedString( @onNull Container base, @NonNull StringBuilder ret, String indent)1521     private void toNestedString(
1522             @NonNull Container base, @NonNull StringBuilder ret, String indent) {
1523         for (Operation mOperation : base.getList()) {
1524             for (String line : mOperation.toString().split("\n")) {
1525                 ret.append(indent);
1526                 ret.append(line);
1527                 ret.append("\n");
1528             }
1529             if (mOperation instanceof Container) {
1530                 toNestedString((Container) mOperation, ret, indent + "  ");
1531             }
1532         }
1533     }
1534 
1535     @NonNull
getOperations()1536     public List<Operation> getOperations() {
1537         return mOperations;
1538     }
1539 
1540     /** defines if a shader can be run */
1541     public interface ShaderControl {
1542         /**
1543          * validate if a shader can run in the document
1544          *
1545          * @param shader the source of the shader
1546          * @return true if the shader is allowed to run
1547          */
isShaderValid(String shader)1548         boolean isShaderValid(String shader);
1549     }
1550 
1551     /**
1552      * validate the shaders
1553      *
1554      * @param context the remote context
1555      * @param ctl the call back to allow evaluation of shaders
1556      */
checkShaders(RemoteContext context, ShaderControl ctl)1557     public void checkShaders(RemoteContext context, ShaderControl ctl) {
1558         checkShaders(context, ctl, mOperations);
1559     }
1560 
1561     /**
1562      * Recursive private version that checks the shaders
1563      *
1564      * @param context the remote context
1565      * @param ctl the call back to allow evaluation of shaders
1566      * @param operations the operations to check
1567      */
checkShaders( RemoteContext context, ShaderControl ctl, List<Operation> operations)1568     private void checkShaders(
1569             RemoteContext context, ShaderControl ctl, List<Operation> operations) {
1570         for (Operation op : operations) {
1571             if (op instanceof TextData) {
1572                 op.apply(context);
1573             }
1574             if (op instanceof Container) {
1575                 checkShaders(context, ctl, ((Container) op).getList());
1576             }
1577             if (op instanceof ShaderData) {
1578                 ShaderData sd = (ShaderData) op;
1579                 int id = sd.getShaderTextId();
1580                 String str = context.getText(id);
1581                 sd.enable(ctl.isShaderValid(str));
1582             }
1583         }
1584     }
1585 
1586     /**
1587      * Set if this is an update doc
1588      *
1589      * @param isUpdateDoc
1590      */
setUpdateDoc(boolean isUpdateDoc)1591     public void setUpdateDoc(boolean isUpdateDoc) {
1592         mIsUpdateDoc = isUpdateDoc;
1593     }
1594 
1595     /**
1596      * @return if this is an update doc
1597      */
isUpdateDoc()1598     public boolean isUpdateDoc() {
1599         return mIsUpdateDoc;
1600     }
1601 }
1602