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