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