1 /* 2 * Copyright 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 17 package androidx.wear.protolayout.renderer.impl; 18 19 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; 20 import static android.widget.FrameLayout.LayoutParams.UNSPECIFIED_GRAVITY; 21 22 import static androidx.core.util.Preconditions.checkNotNull; 23 import static androidx.wear.protolayout.renderer.common.ProviderStatsLogger.IGNORED_FAILURE_ANIMATION_QUOTA_EXCEEDED; 24 import static androidx.wear.protolayout.renderer.common.ProviderStatsLogger.IGNORED_FAILURE_APPLY_MUTATION_EXCEPTION; 25 import static androidx.wear.protolayout.renderer.common.ProviderStatsLogger.INFLATION_FAILURE_REASON_EXPRESSION_NODE_COUNT_EXCEEDED; 26 import static androidx.wear.protolayout.renderer.common.ProviderStatsLogger.INFLATION_FAILURE_REASON_LAYOUT_DEPTH_EXCEEDED; 27 28 import static com.google.common.util.concurrent.Futures.immediateCancelledFuture; 29 import static com.google.common.util.concurrent.Futures.immediateFuture; 30 31 import android.content.Context; 32 import android.content.res.Resources; 33 import android.util.Log; 34 import android.util.Printer; 35 import android.view.Gravity; 36 import android.view.View; 37 import android.view.ViewGroup; 38 import android.view.ViewGroup.LayoutParams; 39 import android.widget.FrameLayout; 40 41 import androidx.annotation.RestrictTo; 42 import androidx.annotation.RestrictTo.Scope; 43 import androidx.annotation.UiThread; 44 import androidx.annotation.VisibleForTesting; 45 import androidx.annotation.WorkerThread; 46 import androidx.collection.ArrayMap; 47 import androidx.wear.protolayout.expression.PlatformDataKey; 48 import androidx.wear.protolayout.expression.pipeline.DynamicTypeAnimator; 49 import androidx.wear.protolayout.expression.pipeline.FixedQuotaManagerImpl; 50 import androidx.wear.protolayout.expression.pipeline.PlatformDataProvider; 51 import androidx.wear.protolayout.expression.pipeline.QuotaManager; 52 import androidx.wear.protolayout.expression.pipeline.StateStore; 53 import androidx.wear.protolayout.proto.LayoutElementProto.ArcLayoutElement; 54 import androidx.wear.protolayout.proto.LayoutElementProto.ArcLayoutElement.InnerCase; 55 import androidx.wear.protolayout.proto.LayoutElementProto.Layout; 56 import androidx.wear.protolayout.proto.LayoutElementProto.LayoutElement; 57 import androidx.wear.protolayout.proto.ResourceProto; 58 import androidx.wear.protolayout.proto.StateProto.State; 59 import androidx.wear.protolayout.renderer.ProtoLayoutExtensionViewProvider; 60 import androidx.wear.protolayout.renderer.ProtoLayoutTheme; 61 import androidx.wear.protolayout.renderer.ProtoLayoutVisibilityState; 62 import androidx.wear.protolayout.renderer.common.LoggingUtils; 63 import androidx.wear.protolayout.renderer.common.NoOpProviderStatsLogger; 64 import androidx.wear.protolayout.renderer.common.ProtoLayoutDiffer; 65 import androidx.wear.protolayout.renderer.common.ProviderStatsLogger; 66 import androidx.wear.protolayout.renderer.common.ProviderStatsLogger.InflaterStatsLogger; 67 import androidx.wear.protolayout.renderer.common.RenderingArtifact; 68 import androidx.wear.protolayout.renderer.dynamicdata.ProtoLayoutDynamicDataPipeline; 69 import androidx.wear.protolayout.renderer.inflater.ProtoLayoutInflater; 70 import androidx.wear.protolayout.renderer.inflater.ProtoLayoutInflater.InflateResult; 71 import androidx.wear.protolayout.renderer.inflater.ProtoLayoutInflater.ViewGroupMutation; 72 import androidx.wear.protolayout.renderer.inflater.ProtoLayoutInflater.ViewMutationException; 73 import androidx.wear.protolayout.renderer.inflater.ProtoLayoutThemeImpl; 74 import androidx.wear.protolayout.renderer.inflater.RenderedMetadata; 75 import androidx.wear.protolayout.renderer.inflater.RenderedMetadata.PendingFrameLayoutParams; 76 import androidx.wear.protolayout.renderer.inflater.RenderedMetadata.ViewProperties; 77 import androidx.wear.protolayout.renderer.inflater.ResourceResolvers; 78 import androidx.wear.protolayout.renderer.inflater.StandardResourceResolvers; 79 80 import com.google.common.collect.ImmutableList; 81 import com.google.common.collect.ImmutableSet; 82 import com.google.common.util.concurrent.Futures; 83 import com.google.common.util.concurrent.ListenableFuture; 84 import com.google.common.util.concurrent.ListeningExecutorService; 85 import com.google.common.util.concurrent.SettableFuture; 86 87 import org.jspecify.annotations.NonNull; 88 import org.jspecify.annotations.Nullable; 89 90 import java.util.List; 91 import java.util.Map; 92 import java.util.Objects; 93 import java.util.Set; 94 import java.util.concurrent.CancellationException; 95 import java.util.concurrent.ExecutionException; 96 97 /** 98 * A single attached instance of a ProtoLayout. This class will ensure that a ProtoLayout is 99 * inflated on a background thread, the first time it is attached to the carousel. As much of the 100 * inflation as possible will be done in the background, with only the final attachment of the 101 * generated layout to a parent container done on the UI thread. 102 */ 103 @RestrictTo(Scope.LIBRARY_GROUP_PREFIX) 104 public class ProtoLayoutViewInstance implements AutoCloseable { 105 106 /** 107 * Returns list of all ProtoAnimations contained in this ProtoLayout. Used by ui-tooling library 108 * for inspection amd modification of animations. 109 */ getAnimations()110 public @NonNull List<DynamicTypeAnimator> getAnimations() { 111 if (mDataPipeline == null) return List.of(); 112 return mDataPipeline.getAnimations(); 113 } 114 115 /** 116 * Listener for clicks on Clickable objects that have an Action to (re)load the contents of a 117 * layout. 118 */ 119 public interface LoadActionListener { 120 121 /** 122 * Called when a Clickable that has a LoadAction is clicked. 123 * 124 * @param nextState The state that the next layout should be in. 125 */ onClick(@onNull State nextState)126 void onClick(@NonNull State nextState); 127 } 128 129 private static final int DEFAULT_MAX_CONCURRENT_RUNNING_ANIMATIONS = 4; 130 static final int MAX_LAYOUT_ELEMENT_DEPTH = 30; 131 private static final @NonNull String TAG = "ProtoLayoutViewInstance"; 132 133 private final @NonNull Context mUiContext; 134 private final @NonNull Resources mRendererResources; 135 private final @NonNull ResourceResolversProvider mResourceResolversProvider; 136 private final @NonNull ProtoLayoutTheme mProtoLayoutTheme; 137 private final @Nullable ProtoLayoutDynamicDataPipeline mDataPipeline; 138 private final @NonNull LoadActionListener mLoadActionListener; 139 private final @NonNull ListeningExecutorService mUiExecutorService; 140 private final @NonNull ListeningExecutorService mBgExecutorService; 141 private final @NonNull String mClickableIdExtra; 142 private final @NonNull ProviderStatsLogger mProviderStatsLogger; 143 private final @Nullable LoggingUtils mLoggingUtils; 144 145 private final @Nullable ProtoLayoutExtensionViewProvider mExtensionViewProvider; 146 147 private final boolean mAnimationEnabled; 148 149 private final boolean mAdaptiveUpdateRatesEnabled; 150 private boolean mWasFullyVisibleBefore; 151 private final boolean mAllowLayoutChangingBindsWithoutDefault; 152 153 /** This keeps track of the current inflated parent for the layout. */ 154 private @Nullable ViewGroup mInflateParent = null; 155 156 /** 157 * This is simply a reference to the current parent for this layout instance (i.e. the last 158 * thing passed into "attach"). This is used because it is technically possible to attach the 159 * layout to a parent container, detach it again, then re-attach it before the render pass is 160 * complete. In this case, the listener attached to renderFuture will fire multiple time and try 161 * and attach the layout multiple times, leading to a crash. 162 * 163 * <p>This field is used inside of renderFuture's listener to ensure that the layout is still 164 * attached to the same object that it was when the listener was added, and hence the layout 165 * should be attached. 166 * 167 * <p>This field should only ever be accessed from the UI thread. 168 */ 169 private @Nullable ViewGroup mAttachParent = null; 170 171 /** 172 * This field is used to avoid unnecessary rendering when dealing with non-interactive layouts. 173 * For interactive layouts, the diffing should already handle this. 174 */ 175 private @Nullable Layout mPrevLayout = null; 176 177 /** 178 * This field is used to avoid unnecessarily checking layout depth if the layout was previously 179 * failing the check. 180 */ 181 private boolean mPrevLayoutAlreadyFailingDepthCheck = false; 182 183 /** 184 * This is used to make sure resource version changes invalidate the layout. Otherwise, this 185 * could result in the resource change not getting reflected with diff rendering (if the layout 186 * pointing to that resource hasn't changed) 187 */ 188 private @Nullable String mPrevResourcesVersion = null; 189 190 /** 191 * This is used as the Future for the currently running inflation session. The first time 192 * "attach" is called, it should start the renderer. Subsequent attach calls should only ever 193 * re-attach "inflateParent". 194 * 195 * <p>If this is null, then nothing has yet called "attach", and hence "render" should be called 196 * on a background thread. If this is non-null but not done, then the inflation is in progress. 197 * If this is non-null and done, then the inflation is complete, and inflateParent can be safely 198 * accessed from the UI thread. 199 * 200 * <p>This field should only ever be accessed from the UI thread. 201 */ 202 @VisibleForTesting @Nullable ListenableFuture<RenderResult> mRenderFuture = null; 203 204 private boolean mCanReattachWithoutRendering = false; 205 206 private static final int DYNAMIC_NODES_MAX_COUNT = 200; 207 208 /** 209 * This is used to provide a {@link ResourceResolvers} object to the {@link 210 * ProtoLayoutViewInstance} allowing it to query {@link ResourceProto.Resources} when needed. 211 */ 212 @RestrictTo(Scope.LIBRARY_GROUP) 213 public interface ResourceResolversProvider { 214 215 /** Provide a {@link ResourceResolvers} instance */ getResourceResolvers( @onNull Context context, ResourceProto.@NonNull Resources resources, @NonNull ListeningExecutorService listeningExecutorService, boolean animationEnabled)216 @Nullable ResourceResolvers getResourceResolvers( 217 @NonNull Context context, 218 ResourceProto.@NonNull Resources resources, 219 @NonNull ListeningExecutorService listeningExecutorService, 220 boolean animationEnabled); 221 } 222 223 /** Data about a parent that a layout has been inflated into. */ 224 static final class InflateParentData { 225 final @Nullable InflateResult mInflateResult; 226 InflateParentData(@ullable InflateResult inflateResult)227 InflateParentData(@Nullable InflateResult inflateResult) { 228 this.mInflateResult = inflateResult; 229 } 230 } 231 232 /** Base class for result of a {@link #renderOrComputeMutations} call. */ 233 interface RenderResult { 234 /** If this result can be reused when attaching to a parent. */ canReattachWithoutRendering()235 boolean canReattachWithoutRendering(); 236 237 /** 238 * Run any final inflation steps that need to be run on the Ui thread. 239 * 240 * @param attachParent the parent view to which newly inflated layouts should attach. 241 * @param prevInflateParent the parent view for the previously inflated layout. 242 * @param isReattaching if True, this layout is being reattached and will skip content 243 * transition animations. 244 */ 245 @UiThread postInflate( @onNull ViewGroup attachParent, @Nullable ViewGroup prevInflateParent, boolean isReattaching, InflaterStatsLogger inflaterStatsLogger)246 @NonNull ListenableFuture<RenderingArtifact> postInflate( 247 @NonNull ViewGroup attachParent, 248 @Nullable ViewGroup prevInflateParent, 249 boolean isReattaching, 250 InflaterStatsLogger inflaterStatsLogger); 251 } 252 253 /** Result of a {@link #renderOrComputeMutations} call when no changes are required. */ 254 static final class UnchangedRenderResult implements RenderResult { 255 @Override canReattachWithoutRendering()256 public boolean canReattachWithoutRendering() { 257 return false; 258 } 259 260 @Override postInflate( @onNull ViewGroup attachParent, @Nullable ViewGroup prevInflateParent, boolean isReattaching, InflaterStatsLogger inflaterStatsLogger)261 public @NonNull ListenableFuture<RenderingArtifact> postInflate( 262 @NonNull ViewGroup attachParent, 263 @Nullable ViewGroup prevInflateParent, 264 boolean isReattaching, 265 InflaterStatsLogger inflaterStatsLogger) { 266 return immediateFuture(RenderingArtifact.create(inflaterStatsLogger)); 267 } 268 } 269 270 /** Result of a {@link #renderOrComputeMutations} call when a failure has happened. */ 271 static final class FailedRenderResult implements RenderResult { 272 @Override canReattachWithoutRendering()273 public boolean canReattachWithoutRendering() { 274 return false; 275 } 276 277 @Override postInflate( @onNull ViewGroup attachParent, @Nullable ViewGroup prevInflateParent, boolean isReattaching, InflaterStatsLogger inflaterStatsLogger)278 public @NonNull ListenableFuture<RenderingArtifact> postInflate( 279 @NonNull ViewGroup attachParent, 280 @Nullable ViewGroup prevInflateParent, 281 boolean isReattaching, 282 InflaterStatsLogger inflaterStatsLogger) { 283 return immediateFuture(RenderingArtifact.failed()); 284 } 285 } 286 287 /** 288 * Result of a {@link #renderOrComputeMutations} call when the layout has been inflated into a 289 * new parent. 290 */ 291 static final class InflatedIntoNewParentRenderResult implements RenderResult { 292 final @NonNull InflateParentData mNewInflateParentData; 293 InflatedIntoNewParentRenderResult(@onNull InflateParentData newInflateParentData)294 InflatedIntoNewParentRenderResult(@NonNull InflateParentData newInflateParentData) { 295 this.mNewInflateParentData = newInflateParentData; 296 } 297 298 @Override canReattachWithoutRendering()299 public boolean canReattachWithoutRendering() { 300 return true; 301 } 302 303 @Override 304 @UiThread postInflate( @onNull ViewGroup attachParent, @Nullable ViewGroup prevInflateParent, boolean isReattaching, InflaterStatsLogger inflaterStatsLogger)305 public @NonNull ListenableFuture<RenderingArtifact> postInflate( 306 @NonNull ViewGroup attachParent, 307 @Nullable ViewGroup prevInflateParent, 308 boolean isReattaching, 309 InflaterStatsLogger inflaterStatsLogger) { 310 InflateResult inflateResult = 311 checkNotNull( 312 mNewInflateParentData.mInflateResult, 313 TAG 314 + " - inflated result was null, but inflating into new" 315 + " attachParent requested."); 316 attachParent.removeAllViews(); 317 attachParent.addView( 318 inflateResult.inflateParent, new LayoutParams(MATCH_PARENT, MATCH_PARENT)); 319 inflateResult.updateDynamicDataPipeline(isReattaching); 320 return immediateFuture(RenderingArtifact.create(inflaterStatsLogger)); 321 } 322 } 323 324 /** 325 * Result of a {@link #renderOrComputeMutations} call when the diffs have been computed and 326 * needs to be applied to the previous parent. 327 */ 328 static final class ApplyToPrevParentRenderResult implements RenderResult { 329 final @NonNull ProtoLayoutInflater mInflater; 330 final @NonNull ViewGroupMutation mMutation; 331 ApplyToPrevParentRenderResult( @onNull ProtoLayoutInflater inflater, @NonNull ViewGroupMutation mutation)332 ApplyToPrevParentRenderResult( 333 @NonNull ProtoLayoutInflater inflater, @NonNull ViewGroupMutation mutation) { 334 this.mInflater = inflater; 335 this.mMutation = mutation; 336 } 337 338 @Override canReattachWithoutRendering()339 public boolean canReattachWithoutRendering() { 340 return false; 341 } 342 343 @Override 344 @UiThread postInflate( @onNull ViewGroup attachParent, @Nullable ViewGroup prevInflateParent, boolean isReattaching, InflaterStatsLogger inflaterStatsLogger)345 public @NonNull ListenableFuture<RenderingArtifact> postInflate( 346 @NonNull ViewGroup attachParent, 347 @Nullable ViewGroup prevInflateParent, 348 boolean isReattaching, 349 InflaterStatsLogger inflaterStatsLogger) { 350 return mInflater.applyMutation(checkNotNull(prevInflateParent), mMutation); 351 } 352 } 353 354 /** Config class for {@link ProtoLayoutViewInstance}. */ 355 @RestrictTo(Scope.LIBRARY_GROUP_PREFIX) 356 public static final class Config { 357 private final @NonNull Context mUiContext; 358 private final @NonNull Resources mRendererResources; 359 private final @NonNull ResourceResolversProvider mResourceResolversProvider; 360 private final @NonNull ProtoLayoutTheme mProtoLayoutTheme; 361 362 private final @NonNull Map<PlatformDataProvider, Set<PlatformDataKey<?>>> 363 mPlatformDataProviders; 364 365 private final @Nullable StateStore mStateStore; 366 private final @NonNull LoadActionListener mLoadActionListener; 367 private final @NonNull ListeningExecutorService mUiExecutorService; 368 private final @NonNull ListeningExecutorService mBgExecutorService; 369 private final @Nullable ProtoLayoutExtensionViewProvider mExtensionViewProvider; 370 private final @NonNull String mClickableIdExtra; 371 372 private final @Nullable LoggingUtils mLoggingUtils; 373 private final @NonNull ProviderStatsLogger mProviderStatsLogger; 374 private final boolean mAnimationEnabled; 375 private final int mRunningAnimationsLimit; 376 377 private final boolean mUpdatesEnabled; 378 private final boolean mAdaptiveUpdateRatesEnabled; 379 private final boolean mIsViewFullyVisible; 380 private final boolean mAllowLayoutChangingBindsWithoutDefault; 381 Config( @onNull Context uiContext, @NonNull Resources rendererResources, @NonNull ResourceResolversProvider resourceResolversProvider, @NonNull ProtoLayoutTheme protoLayoutTheme, @NonNull Map<PlatformDataProvider, Set<PlatformDataKey<?>>> platformDataProviders, @Nullable StateStore stateStore, @NonNull LoadActionListener loadActionListener, @NonNull ListeningExecutorService uiExecutorService, @NonNull ListeningExecutorService bgExecutorService, @Nullable ProtoLayoutExtensionViewProvider extensionViewProvider, @NonNull String clickableIdExtra, @Nullable LoggingUtils loggingUtils, @NonNull ProviderStatsLogger providerStatsLogger, boolean animationEnabled, int runningAnimationsLimit, boolean updatesEnabled, boolean adaptiveUpdateRatesEnabled, boolean isViewFullyVisible, boolean allowLayoutChangingBindsWithoutDefault)382 Config( 383 @NonNull Context uiContext, 384 @NonNull Resources rendererResources, 385 @NonNull ResourceResolversProvider resourceResolversProvider, 386 @NonNull ProtoLayoutTheme protoLayoutTheme, 387 @NonNull Map<PlatformDataProvider, Set<PlatformDataKey<?>>> platformDataProviders, 388 @Nullable StateStore stateStore, 389 @NonNull LoadActionListener loadActionListener, 390 @NonNull ListeningExecutorService uiExecutorService, 391 @NonNull ListeningExecutorService bgExecutorService, 392 @Nullable ProtoLayoutExtensionViewProvider extensionViewProvider, 393 @NonNull String clickableIdExtra, 394 @Nullable LoggingUtils loggingUtils, 395 @NonNull ProviderStatsLogger providerStatsLogger, 396 boolean animationEnabled, 397 int runningAnimationsLimit, 398 boolean updatesEnabled, 399 boolean adaptiveUpdateRatesEnabled, 400 boolean isViewFullyVisible, 401 boolean allowLayoutChangingBindsWithoutDefault) { 402 this.mUiContext = uiContext; 403 this.mRendererResources = rendererResources; 404 this.mResourceResolversProvider = resourceResolversProvider; 405 this.mProtoLayoutTheme = protoLayoutTheme; 406 this.mPlatformDataProviders = platformDataProviders; 407 this.mStateStore = stateStore; 408 this.mLoadActionListener = loadActionListener; 409 this.mUiExecutorService = uiExecutorService; 410 this.mBgExecutorService = bgExecutorService; 411 this.mExtensionViewProvider = extensionViewProvider; 412 this.mClickableIdExtra = clickableIdExtra; 413 this.mLoggingUtils = loggingUtils; 414 this.mProviderStatsLogger = providerStatsLogger; 415 this.mAnimationEnabled = animationEnabled; 416 this.mRunningAnimationsLimit = runningAnimationsLimit; 417 this.mUpdatesEnabled = updatesEnabled; 418 this.mAdaptiveUpdateRatesEnabled = adaptiveUpdateRatesEnabled; 419 this.mIsViewFullyVisible = isViewFullyVisible; 420 this.mAllowLayoutChangingBindsWithoutDefault = allowLayoutChangingBindsWithoutDefault; 421 } 422 423 /** Returns UI Context used for interacting with the UI. */ getUiContext()424 public @NonNull Context getUiContext() { 425 return mUiContext; 426 } 427 428 /** Returns the Android Resources object for the renderer package. */ 429 @RestrictTo(Scope.LIBRARY) getRendererResources()430 public @NonNull Resources getRendererResources() { 431 return mRendererResources; 432 } 433 434 /** Returns provider for resolving resources. */ 435 @RestrictTo(Scope.LIBRARY) getResourceResolversProvider()436 public @NonNull ResourceResolversProvider getResourceResolversProvider() { 437 return mResourceResolversProvider; 438 } 439 440 /** Returns theme used for this instance. */ 441 @RestrictTo(Scope.LIBRARY) getProtoLayoutTheme()442 public @NonNull ProtoLayoutTheme getProtoLayoutTheme() { 443 return mProtoLayoutTheme; 444 } 445 446 /** Returns the registered platform data providers. */ 447 public @NonNull Map<PlatformDataProvider, Set<PlatformDataKey<?>>> getPlatformDataProviders()448 getPlatformDataProviders() { 449 return mPlatformDataProviders; 450 } 451 452 /** Returns state store. */ getStateStore()453 public @Nullable StateStore getStateStore() { 454 return mStateStore; 455 } 456 457 /** Returns listener for load actions. */ getLoadActionListener()458 public @NonNull LoadActionListener getLoadActionListener() { 459 return mLoadActionListener; 460 } 461 462 /** Returns ExecutorService for UI tasks. */ getUiExecutorService()463 public @NonNull ListeningExecutorService getUiExecutorService() { 464 return mUiExecutorService; 465 } 466 467 /** Returns ExecutorService for background tasks. */ getBgExecutorService()468 public @NonNull ListeningExecutorService getBgExecutorService() { 469 return mBgExecutorService; 470 } 471 472 /** Returns provider for renderer extension. */ 473 @RestrictTo(Scope.LIBRARY) getExtensionViewProvider()474 public @Nullable ProtoLayoutExtensionViewProvider getExtensionViewProvider() { 475 return mExtensionViewProvider; 476 } 477 478 /** Returns extra used for storing clickable id. */ getClickableIdExtra()479 public @NonNull String getClickableIdExtra() { 480 return mClickableIdExtra; 481 } 482 483 /** Returns the debug logger. */ getLoggingUtils()484 public @Nullable LoggingUtils getLoggingUtils() { 485 return mLoggingUtils; 486 } 487 488 /** Returns the provider stats logger used for telemetry. */ 489 @RestrictTo(Scope.LIBRARY_GROUP) getProviderStatsLogger()490 public @NonNull ProviderStatsLogger getProviderStatsLogger() { 491 return mProviderStatsLogger; 492 } 493 494 /** Returns whether animations are enabled. */ 495 @RestrictTo(Scope.LIBRARY) getAnimationEnabled()496 public boolean getAnimationEnabled() { 497 return mAnimationEnabled; 498 } 499 500 /** Returns how many animations can be concurrently run. */ 501 @RestrictTo(Scope.LIBRARY) getRunningAnimationsLimit()502 public int getRunningAnimationsLimit() { 503 return mRunningAnimationsLimit; 504 } 505 506 /** Returns whether updates are enabled. */ 507 @RestrictTo(Scope.LIBRARY) getUpdatesEnabled()508 public boolean getUpdatesEnabled() { 509 return mUpdatesEnabled; 510 } 511 512 /** Returns whether adaptive updates are enabled. */ 513 @RestrictTo(Scope.LIBRARY) getAdaptiveUpdateRatesEnabled()514 public boolean getAdaptiveUpdateRatesEnabled() { 515 return mAdaptiveUpdateRatesEnabled; 516 } 517 518 /** Returns whether view is fully visible. */ 519 @RestrictTo(Scope.LIBRARY) getIsViewFullyVisible()520 public boolean getIsViewFullyVisible() { 521 return mIsViewFullyVisible; 522 } 523 524 /** 525 * Sets whether a "layout changing" data bind can be applied without the "value_for_layout" 526 * field being filled in, or being set to zero / empty. Defaults to false. 527 * 528 * <p>This is to support legacy apps which use layout-changing data bind before the full 529 * support was built. 530 */ 531 @RestrictTo(Scope.LIBRARY) getAllowLayoutChangingBindsWithoutDefault()532 public boolean getAllowLayoutChangingBindsWithoutDefault() { 533 return mAllowLayoutChangingBindsWithoutDefault; 534 } 535 536 /** Builder for {@link Config}. */ 537 @RestrictTo(Scope.LIBRARY_GROUP_PREFIX) 538 public static final class Builder { 539 private final @NonNull Context mUiContext; 540 private @Nullable Resources mRendererResources; 541 private @Nullable ResourceResolversProvider mResourceResolversProvider; 542 private @Nullable ProtoLayoutTheme mProtoLayoutTheme; 543 544 private final @NonNull Map<PlatformDataProvider, Set<PlatformDataKey<?>>> 545 mPlatformDataProviders = new ArrayMap<>(); 546 547 private @Nullable StateStore mStateStore; 548 private @Nullable LoadActionListener mLoadActionListener; 549 private final @NonNull ListeningExecutorService mUiExecutorService; 550 private final @NonNull ListeningExecutorService mBgExecutorService; 551 private @Nullable ProtoLayoutExtensionViewProvider mExtensionViewProvider; 552 private final @NonNull String mClickableIdExtra; 553 private @Nullable LoggingUtils mLoggingUtils; 554 private @Nullable ProviderStatsLogger mProviderStatsLogger; 555 private boolean mAnimationEnabled = true; 556 private int mRunningAnimationsLimit = DEFAULT_MAX_CONCURRENT_RUNNING_ANIMATIONS; 557 558 private boolean mUpdatesEnabled = true; 559 private boolean mAdaptiveUpdateRatesEnabled = true; 560 private boolean mIsViewFullyVisible = true; 561 private boolean mAllowLayoutChangingBindsWithoutDefault = false; 562 563 /** 564 * Builder for the {@link Config} class. 565 * 566 * @param uiContext Context suitable for interacting with UI. 567 * @param uiExecutorService Executor for UI related tasks. 568 * @param bgExecutorService Executor for background tasks. 569 * @param clickableIdExtra String extra for storing clickable id. 570 */ Builder( @onNull Context uiContext, @NonNull ListeningExecutorService uiExecutorService, @NonNull ListeningExecutorService bgExecutorService, @NonNull String clickableIdExtra)571 public Builder( 572 @NonNull Context uiContext, 573 @NonNull ListeningExecutorService uiExecutorService, 574 @NonNull ListeningExecutorService bgExecutorService, 575 @NonNull String clickableIdExtra) { 576 this.mUiContext = uiContext; 577 this.mUiExecutorService = uiExecutorService; 578 this.mBgExecutorService = bgExecutorService; 579 this.mClickableIdExtra = clickableIdExtra; 580 } 581 582 /** Sets provider for resolving resources. */ 583 @RestrictTo(Scope.LIBRARY) setResourceResolverProvider( @onNull ResourceResolversProvider resourceResolversProvider)584 public @NonNull Builder setResourceResolverProvider( 585 @NonNull ResourceResolversProvider resourceResolversProvider) { 586 this.mResourceResolversProvider = resourceResolversProvider; 587 return this; 588 } 589 590 /** 591 * Sets the Android Resources object for the renderer package. This can usually be 592 * retrieved with {@link 593 * android.content.pm.PackageManager#getResourcesForApplication(String)}. If not 594 * specified, this is retrieved from the Ui Context. 595 */ 596 @RestrictTo(Scope.LIBRARY) setRendererResources(@onNull Resources rendererResources)597 public @NonNull Builder setRendererResources(@NonNull Resources rendererResources) { 598 this.mRendererResources = rendererResources; 599 return this; 600 } 601 602 /** 603 * Sets theme for this ProtoLayout instance. If not set, default theme would be used. 604 */ 605 @RestrictTo(Scope.LIBRARY) setProtoLayoutTheme( @onNull ProtoLayoutTheme protoLayoutTheme)606 public @NonNull Builder setProtoLayoutTheme( 607 @NonNull ProtoLayoutTheme protoLayoutTheme) { 608 this.mProtoLayoutTheme = protoLayoutTheme; 609 return this; 610 } 611 612 /** Adds a {@link PlatformDataProvider} for accessing {@code supportedKeys}. */ addPlatformDataProvider( @onNull PlatformDataProvider platformDataProvider, PlatformDataKey<?> @NonNull ... supportedKeys)613 public @NonNull Builder addPlatformDataProvider( 614 @NonNull PlatformDataProvider platformDataProvider, 615 PlatformDataKey<?> @NonNull ... supportedKeys) { 616 this.mPlatformDataProviders.put( 617 platformDataProvider, ImmutableSet.copyOf(supportedKeys)); 618 return this; 619 } 620 621 /** Sets the storage for state updates. */ setStateStore(@onNull StateStore stateStore)622 public @NonNull Builder setStateStore(@NonNull StateStore stateStore) { 623 this.mStateStore = stateStore; 624 return this; 625 } 626 627 /** 628 * Sets the listener for clicks that will cause contents to be reloaded. Defaults to 629 * no-op. 630 */ setLoadActionListener( @onNull LoadActionListener loadActionListener)631 public @NonNull Builder setLoadActionListener( 632 @NonNull LoadActionListener loadActionListener) { 633 this.mLoadActionListener = loadActionListener; 634 return this; 635 } 636 637 /** Sets provider for the renderer extension. */ 638 @RestrictTo(Scope.LIBRARY) setExtensionViewProvider( @onNull ProtoLayoutExtensionViewProvider extensionViewProvider)639 public @NonNull Builder setExtensionViewProvider( 640 @NonNull ProtoLayoutExtensionViewProvider extensionViewProvider) { 641 this.mExtensionViewProvider = extensionViewProvider; 642 return this; 643 } 644 645 /** Sets the debug logger. */ 646 @RestrictTo(Scope.LIBRARY) setLoggingUtils(@onNull LoggingUtils loggingUitls)647 public @NonNull Builder setLoggingUtils(@NonNull LoggingUtils loggingUitls) { 648 this.mLoggingUtils = loggingUitls; 649 return this; 650 } 651 652 /** Sets the provider stats logger used for telemetry. */ 653 @RestrictTo(Scope.LIBRARY_GROUP) setProviderStatsLogger( @onNull ProviderStatsLogger providerStatsLogger)654 public @NonNull Builder setProviderStatsLogger( 655 @NonNull ProviderStatsLogger providerStatsLogger) { 656 this.mProviderStatsLogger = providerStatsLogger; 657 return this; 658 } 659 660 /** 661 * Sets whether animation are enabled. If disabled, none of the animation will be 662 * played. 663 */ 664 @RestrictTo(Scope.LIBRARY) setAnimationEnabled(boolean animationEnabled)665 public @NonNull Builder setAnimationEnabled(boolean animationEnabled) { 666 this.mAnimationEnabled = animationEnabled; 667 return this; 668 } 669 670 /** Sets the limit to how much concurrently running animations are allowed. */ 671 @RestrictTo(Scope.LIBRARY) setRunningAnimationsLimit(int runningAnimationsLimit)672 public @NonNull Builder setRunningAnimationsLimit(int runningAnimationsLimit) { 673 this.mRunningAnimationsLimit = runningAnimationsLimit; 674 return this; 675 } 676 677 /** Sets whether sending updates is enabled. */ 678 @RestrictTo(Scope.LIBRARY) setUpdatesEnabled(boolean updatesEnabled)679 public @NonNull Builder setUpdatesEnabled(boolean updatesEnabled) { 680 this.mUpdatesEnabled = updatesEnabled; 681 return this; 682 } 683 684 /** Sets whether adaptive updates rates is enabled. */ 685 @RestrictTo(Scope.LIBRARY) setAdaptiveUpdateRatesEnabled( boolean adaptiveUpdateRatesEnabled)686 public @NonNull Builder setAdaptiveUpdateRatesEnabled( 687 boolean adaptiveUpdateRatesEnabled) { 688 this.mAdaptiveUpdateRatesEnabled = adaptiveUpdateRatesEnabled; 689 return this; 690 } 691 692 /** Sets whether the view is fully visible. */ 693 @RestrictTo(Scope.LIBRARY) setIsViewFullyVisible(boolean isViewFullyVisible)694 public @NonNull Builder setIsViewFullyVisible(boolean isViewFullyVisible) { 695 this.mIsViewFullyVisible = isViewFullyVisible; 696 return this; 697 } 698 699 /** 700 * Sets whether a "layout changing" data bind can be applied without the 701 * "value_for_layout" field being filled in, or being set to zero / empty. Defaults to 702 * false. 703 * 704 * <p>This is to support legacy apps which use layout-changing data bind before the full 705 * support was built. 706 */ 707 @RestrictTo(Scope.LIBRARY) setAllowLayoutChangingBindsWithoutDefault( boolean allowLayoutChangingBindsWithoutDefault)708 public @NonNull Builder setAllowLayoutChangingBindsWithoutDefault( 709 boolean allowLayoutChangingBindsWithoutDefault) { 710 this.mAllowLayoutChangingBindsWithoutDefault = 711 allowLayoutChangingBindsWithoutDefault; 712 return this; 713 } 714 715 /** Builds {@link Config} object. */ build()716 public @NonNull Config build() { 717 LoadActionListener loadActionListener = mLoadActionListener; 718 if (loadActionListener == null) { 719 loadActionListener = p -> {}; 720 } 721 if (mProtoLayoutTheme == null) { 722 mProtoLayoutTheme = ProtoLayoutThemeImpl.defaultTheme(mUiContext); 723 } 724 if (mResourceResolversProvider == null) { 725 mResourceResolversProvider = 726 (context, resources, listeningExecutorService, animationEnabled) -> 727 StandardResourceResolvers.forLocalApp( 728 resources, 729 mUiContext, 730 listeningExecutorService, 731 mAnimationEnabled) 732 .build(); 733 } 734 if (mRendererResources == null) { 735 this.mRendererResources = mUiContext.getResources(); 736 } 737 738 if (mProviderStatsLogger == null) { 739 mProviderStatsLogger = 740 new NoOpProviderStatsLogger( 741 "ProviderStatsLogger not provided to " + TAG); 742 } 743 return new Config( 744 mUiContext, 745 mRendererResources, 746 mResourceResolversProvider, 747 mProtoLayoutTheme, 748 mPlatformDataProviders, 749 mStateStore, 750 loadActionListener, 751 mUiExecutorService, 752 mBgExecutorService, 753 mExtensionViewProvider, 754 mClickableIdExtra, 755 mLoggingUtils, 756 mProviderStatsLogger, 757 mAnimationEnabled, 758 mRunningAnimationsLimit, 759 mUpdatesEnabled, 760 mAdaptiveUpdateRatesEnabled, 761 mIsViewFullyVisible, 762 mAllowLayoutChangingBindsWithoutDefault); 763 } 764 } 765 } 766 ProtoLayoutViewInstance(@onNull Config config)767 public ProtoLayoutViewInstance(@NonNull Config config) { 768 this.mUiContext = config.getUiContext(); 769 this.mRendererResources = config.getRendererResources(); 770 this.mResourceResolversProvider = config.getResourceResolversProvider(); 771 this.mProtoLayoutTheme = config.getProtoLayoutTheme(); 772 this.mLoadActionListener = config.getLoadActionListener(); 773 this.mUiExecutorService = config.getUiExecutorService(); 774 this.mBgExecutorService = config.getBgExecutorService(); 775 this.mExtensionViewProvider = config.getExtensionViewProvider(); 776 this.mAnimationEnabled = config.getAnimationEnabled(); 777 this.mClickableIdExtra = config.getClickableIdExtra(); 778 this.mLoggingUtils = config.getLoggingUtils(); 779 this.mAdaptiveUpdateRatesEnabled = config.getAdaptiveUpdateRatesEnabled(); 780 this.mWasFullyVisibleBefore = false; 781 this.mAllowLayoutChangingBindsWithoutDefault = 782 config.getAllowLayoutChangingBindsWithoutDefault(); 783 this.mProviderStatsLogger = config.getProviderStatsLogger(); 784 785 StateStore stateStore = config.getStateStore(); 786 if (stateStore == null) { 787 mDataPipeline = null; 788 return; 789 } 790 791 if (config.getAnimationEnabled()) { 792 QuotaManager nodeQuotaManager = 793 new FixedQuotaManagerImpl(DYNAMIC_NODES_MAX_COUNT, "dynamic nodes") { 794 @Override 795 public boolean tryAcquireQuota(int quota) { 796 boolean success = super.tryAcquireQuota(quota); 797 if (!success) { 798 mProviderStatsLogger.logInflationFailed( 799 INFLATION_FAILURE_REASON_EXPRESSION_NODE_COUNT_EXCEEDED); 800 } 801 return success; 802 } 803 }; 804 mDataPipeline = 805 new ProtoLayoutDynamicDataPipeline( 806 config.getPlatformDataProviders(), 807 stateStore, 808 new FixedQuotaManagerImpl( 809 config.getRunningAnimationsLimit(), "animations") { 810 @Override 811 public boolean tryAcquireQuota(int quota) { 812 boolean success = super.tryAcquireQuota(quota); 813 if (!success) { 814 mProviderStatsLogger.logIgnoredFailure( 815 IGNORED_FAILURE_ANIMATION_QUOTA_EXCEEDED); 816 } 817 return success; 818 } 819 }, 820 nodeQuotaManager); 821 } else { 822 mDataPipeline = 823 new ProtoLayoutDynamicDataPipeline( 824 config.getPlatformDataProviders(), stateStore); 825 } 826 827 mDataPipeline.setFullyVisible(config.getIsViewFullyVisible()); 828 } 829 830 @WorkerThread renderOrComputeMutations( @onNull Layout layout, ResourceProto.@NonNull Resources resources, @Nullable RenderedMetadata prevRenderedMetadata, @NonNull ViewProperties parentViewProp, @NonNull InflaterStatsLogger inflaterStatsLogger)831 private @NonNull RenderResult renderOrComputeMutations( 832 @NonNull Layout layout, 833 ResourceProto.@NonNull Resources resources, 834 @Nullable RenderedMetadata prevRenderedMetadata, 835 @NonNull ViewProperties parentViewProp, 836 @NonNull InflaterStatsLogger inflaterStatsLogger) { 837 ResourceResolvers resolvers = 838 mResourceResolversProvider.getResourceResolvers( 839 mUiContext, resources, mUiExecutorService, mAnimationEnabled); 840 841 if (resolvers == null) { 842 Log.w(TAG, "Resource resolvers cannot be retrieved."); 843 return new FailedRenderResult(); 844 } 845 846 boolean sameFingerprint = 847 prevRenderedMetadata != null 848 && ProtoLayoutDiffer.areSameFingerprints( 849 prevRenderedMetadata.getTreeFingerprint(), layout.getFingerprint()); 850 851 if (sameFingerprint) { 852 if (mPrevLayoutAlreadyFailingDepthCheck) { 853 handleLayoutDepthCheckFailure(inflaterStatsLogger); 854 } 855 } else { 856 checkLayoutDepth(layout.getRoot(), MAX_LAYOUT_ELEMENT_DEPTH, inflaterStatsLogger); 857 } 858 859 mPrevLayoutAlreadyFailingDepthCheck = false; 860 861 ProtoLayoutInflater.Config.Builder inflaterConfigBuilder = 862 new ProtoLayoutInflater.Config.Builder(mUiContext, layout, resolvers) 863 .setLoadActionExecutor(mUiExecutorService) 864 .setLoadActionListener(mLoadActionListener::onClick) 865 .setRendererResources(mRendererResources) 866 .setProtoLayoutTheme(mProtoLayoutTheme) 867 .setAnimationEnabled(mAnimationEnabled) 868 .setClickableIdExtra(mClickableIdExtra) 869 .setAllowLayoutChangingBindsWithoutDefault( 870 mAllowLayoutChangingBindsWithoutDefault) 871 .setInflaterStatsLogger(inflaterStatsLogger) 872 .setApplyFontVariantBodyAsDefault(true); 873 if (mDataPipeline != null) { 874 inflaterConfigBuilder.setDynamicDataPipeline(mDataPipeline); 875 } 876 877 if (mExtensionViewProvider != null) { 878 inflaterConfigBuilder.setExtensionViewProvider(mExtensionViewProvider); 879 } 880 881 if (mLoggingUtils != null) { 882 inflaterConfigBuilder.setLoggingUtils(mLoggingUtils); 883 } 884 885 ProtoLayoutInflater inflater = new ProtoLayoutInflater(inflaterConfigBuilder.build()); 886 887 // mark the view and skip doing diff update (to avoid doubling the work each time). 888 ViewGroupMutation mutation = null; 889 if (mAdaptiveUpdateRatesEnabled && prevRenderedMetadata != null) { 890 // Compute the mutation here, but if there is a change, apply it in the UI thread. 891 try { 892 mutation = inflater.computeMutation(prevRenderedMetadata, layout, parentViewProp); 893 } catch (UnsupportedOperationException ex) { 894 Log.w(TAG, "Error computing mutation.", ex); 895 } 896 } 897 if (mutation == null) { 898 // Couldn't compute mutation. Inflate from scratch. 899 InflateParentData inflateParentData = inflateIntoNewParent(mUiContext, inflater); 900 if (inflateParentData.mInflateResult == null) { 901 return new FailedRenderResult(); 902 } 903 return new InflatedIntoNewParentRenderResult(inflateParentData); 904 } else if (mutation.isNoOp()) { 905 // New layout is the same. Nothing to do. 906 return new UnchangedRenderResult(); 907 } else { 908 // We have a diff. Ask for it to be applied to the previously inflated parent, but in 909 // the UI thread. 910 checkNotNull(prevRenderedMetadata); 911 return new ApplyToPrevParentRenderResult(inflater, mutation); 912 } 913 } 914 915 // dereference of possibly-null reference childLp incompatible argument for parameter arg0 of 916 // setLayoutParams. 917 @SuppressWarnings({"nullness:dereference.of.nullable", "nullness:argument"}) 918 @WorkerThread inflateIntoNewParent( @onNull Context uiContext, @NonNull ProtoLayoutInflater inflater)919 private @NonNull InflateParentData inflateIntoNewParent( 920 @NonNull Context uiContext, @NonNull ProtoLayoutInflater inflater) { 921 FrameLayout inflateParent; 922 int gravity; 923 inflateParent = new FrameLayout(uiContext); 924 gravity = Gravity.CENTER; 925 926 // Inflate the current timeline entry (passed above) into "inflateParent". This should, at 927 // most, add a single element into that container. 928 InflateResult result = inflater.inflate(inflateParent); 929 930 // The inflater will only ever add one child to the container. Set correct gravity on it to 931 // ensure that the inflated layout is centered within the inflation parent above. 932 if (inflateParent.getChildCount() > 0) { 933 View firstChild = inflateParent.getChildAt(0); 934 FrameLayout.LayoutParams childLp = 935 (FrameLayout.LayoutParams) firstChild.getLayoutParams(); 936 childLp.gravity = gravity; 937 firstChild.setLayoutParams(childLp); 938 } 939 return new InflateParentData(result); 940 } 941 942 @UiThread 943 @RestrictTo(Scope.LIBRARY_GROUP) renderLayoutAndAttach( @onNull Layout layout, ResourceProto.@NonNull Resources resources, @NonNull ViewGroup attachParent)944 public @NonNull ListenableFuture<RenderingArtifact> renderLayoutAndAttach( 945 @NonNull Layout layout, 946 ResourceProto.@NonNull Resources resources, 947 @NonNull ViewGroup attachParent) { 948 return renderAndAttach( 949 layout, resources, attachParent, mProviderStatsLogger.createInflaterStatsLogger()); 950 } 951 952 /** Dumps the state of this tile view instance. */ 953 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 954 @UiThread dump(@onNull Printer printer)955 public void dump(@NonNull Printer printer) { 956 printer.println( 957 "attachedParent: " + Integer.toHexString(System.identityHashCode(mAttachParent))); 958 } 959 960 /** 961 * Render the layout for this layout and attach this layout instance to a {@code attachParent} 962 * container. Note that this method may clear all of {@code attachParent}'s children before 963 * attaching the layout, but only if it's not possible to update them in place. 964 * 965 * <p>If the layout has not yet been inflated, it will not be attached to the {@code 966 * attachParent} container immediately; it will instead inflate the layout in the background, 967 * then attach it at some point in the future once it has been inflated. 968 * 969 * <p>Note that it is safe to call {@link ProtoLayoutViewInstance#detach}, and subsequently, 970 * attach again while the layout is inflating; it will only attach to the last requested {@code 971 * attachParent} (or if detach was the last call, it will not be attached to anything). 972 * 973 * <p>Note also that this method must be called from the UI thread; 974 */ 975 @UiThread 976 @SuppressWarnings({ 977 "ReferenceEquality", 978 "ExecutorTaskName", 979 }) // layout == prevLayout is intentional (and enough in this case) renderAndAttach( @onNull Layout layout, ResourceProto.@NonNull Resources resources, @NonNull ViewGroup attachParent)980 public @NonNull ListenableFuture<Void> renderAndAttach( 981 @NonNull Layout layout, 982 ResourceProto.@NonNull Resources resources, 983 @NonNull ViewGroup attachParent) { 984 SettableFuture<Void> result = SettableFuture.create(); 985 ListenableFuture<RenderingArtifact> future = 986 renderLayoutAndAttach(layout, resources, attachParent); 987 if (future.isDone()) { 988 if (future.isCancelled()) { 989 return immediateCancelledFuture(); 990 } 991 return immediateFuture(null); 992 } else { 993 future.addListener( 994 () -> { 995 if (future.isCancelled()) { 996 result.cancel(/* mayInterruptIfRunning= */ false); 997 } else { 998 try { 999 RenderingArtifact ignored = future.get(); 1000 result.set(null); 1001 } catch (ExecutionException 1002 | InterruptedException 1003 | CancellationException e) { 1004 Log.e(TAG, "Failed to render layout", e); 1005 result.setException(e); 1006 } 1007 } 1008 }, 1009 mUiExecutorService); 1010 } 1011 return result; 1012 } 1013 1014 @UiThread 1015 @SuppressWarnings({ 1016 "ReferenceEquality", 1017 "ExecutorTaskName", 1018 }) // layout == prevLayout is intentional (and enough in this case) renderAndAttach( @onNull Layout layout, ResourceProto.@NonNull Resources resources, @NonNull ViewGroup attachParent, @NonNull InflaterStatsLogger inflaterStatsLogger)1019 private @NonNull ListenableFuture<RenderingArtifact> renderAndAttach( 1020 @NonNull Layout layout, 1021 ResourceProto.@NonNull Resources resources, 1022 @NonNull ViewGroup attachParent, 1023 @NonNull InflaterStatsLogger inflaterStatsLogger) { 1024 if (mLoggingUtils != null && mLoggingUtils.canLogD(TAG)) { 1025 mLoggingUtils.logD(TAG, "Layout received in #renderAndAttach:\n %s", layout.toString()); 1026 mLoggingUtils.logD( 1027 TAG, "Resources received in #renderAndAttach:\n %s", resources.toString()); 1028 } 1029 1030 if (mAttachParent == null) { 1031 mAttachParent = attachParent; 1032 mAttachParent.removeAllViews(); 1033 // Preload it with the previous layout if we have one. 1034 if (mInflateParent != null) { 1035 mAttachParent.addView(mInflateParent); 1036 } 1037 } else if (mAttachParent != attachParent) { 1038 throw new IllegalStateException("ProtoLayoutViewInstance is already attached!"); 1039 } 1040 1041 if (layout == mPrevLayout && mInflateParent != null) { 1042 // Nothing to do. 1043 return Futures.immediateFuture(RenderingArtifact.skipped()); 1044 } 1045 1046 boolean isReattaching = false; 1047 if (mRenderFuture != null) { 1048 if (!mRenderFuture.isDone()) { 1049 // There is an ongoing rendering operation. Cancel that and render the new layout. 1050 Log.w(TAG, "Cancelling the previous layout update that hasn't finished yet."); 1051 checkNotNull(mRenderFuture).cancel(/* maybeInterruptIfRunning= */ false); 1052 1053 mRenderFuture = null; 1054 } else if (layout == mPrevLayout && mCanReattachWithoutRendering) { 1055 isReattaching = true; 1056 } else { 1057 mRenderFuture = null; 1058 } 1059 } 1060 1061 ViewGroup prevInflateParent = getOnlyChildViewGroup(attachParent); 1062 1063 if (mRenderFuture == null) { 1064 if (prevInflateParent != null 1065 && !Objects.equals(resources.getVersion(), mPrevResourcesVersion)) { 1066 // If the resource version has changed, clear the diff metadata to force a full 1067 // reinflation. 1068 ProtoLayoutInflater.clearRenderedMetadata(checkNotNull(prevInflateParent)); 1069 } 1070 1071 RenderedMetadata prevRenderedMetadata = 1072 prevInflateParent != null 1073 ? ProtoLayoutInflater.getRenderedMetadata(prevInflateParent) 1074 : null; 1075 1076 mPrevLayout = layout; 1077 mPrevResourcesVersion = resources.getVersion(); 1078 1079 int gravity = UNSPECIFIED_GRAVITY; 1080 LayoutParams layoutParams = new LayoutParams(MATCH_PARENT, MATCH_PARENT); 1081 1082 if (prevInflateParent != null 1083 && prevInflateParent.getChildCount() > 0 1084 // This is to ensure we are centering the correct parent and that it wasn't 1085 // changed after previous inflation. 1086 && prevRenderedMetadata != null) { 1087 View firstChild = prevInflateParent.getChildAt(0); 1088 if (firstChild != null) { 1089 FrameLayout.LayoutParams childLp = 1090 (FrameLayout.LayoutParams) firstChild.getLayoutParams(); 1091 if (childLp != null) { 1092 gravity = childLp.gravity; 1093 } 1094 } 1095 } 1096 1097 ViewProperties parentViewProp = 1098 ViewProperties.fromViewGroup( 1099 attachParent, 1100 layoutParams, 1101 // We need this specific ones as otherwise gravity gets lost for 1102 // attachParent node. 1103 new PendingFrameLayoutParams(gravity)); 1104 1105 mRenderFuture = 1106 mBgExecutorService.submit( 1107 () -> 1108 renderOrComputeMutations( 1109 layout, 1110 resources, 1111 prevRenderedMetadata, 1112 parentViewProp, 1113 inflaterStatsLogger)); 1114 mCanReattachWithoutRendering = false; 1115 } 1116 SettableFuture<RenderingArtifact> result = SettableFuture.create(); 1117 if (!checkNotNull(mRenderFuture).isDone()) { 1118 ListenableFuture<RenderResult> rendererFuture = mRenderFuture; 1119 mRenderFuture.addListener( 1120 () -> { 1121 if (rendererFuture.isCancelled()) { 1122 result.cancel(/* mayInterruptIfRunning= */ false); 1123 } 1124 // Ensure that this inflater is attached to the same attachParent as when 1125 // this listener was created. If not, something has re-attached us in the 1126 // time it took for the inflater to execute. 1127 if (mAttachParent == attachParent) { 1128 try { 1129 result.setFuture( 1130 postInflate( 1131 attachParent, 1132 prevInflateParent, 1133 checkNotNull(rendererFuture).get(), 1134 /* isReattaching= */ false, 1135 layout, 1136 resources, 1137 inflaterStatsLogger)); 1138 } catch (ExecutionException 1139 | InterruptedException 1140 | CancellationException e) { 1141 Log.e(TAG, "Failed to render layout", e); 1142 result.setException(e); 1143 } 1144 } else { 1145 Log.w( 1146 TAG, 1147 "Layout is rendered, but inflater is no longer attached to the" 1148 + " same attachParent. Cancelling inflation."); 1149 result.cancel(/* mayInterruptIfRunning= */ false); 1150 } 1151 }, 1152 mUiExecutorService); 1153 } else { 1154 try { 1155 result.setFuture( 1156 postInflate( 1157 attachParent, 1158 prevInflateParent, 1159 mRenderFuture.get(), 1160 isReattaching, 1161 layout, 1162 resources, 1163 inflaterStatsLogger)); 1164 } catch (ExecutionException | InterruptedException | CancellationException e) { 1165 Log.e(TAG, "Failed to render layout", e); 1166 result.setException(e); 1167 } 1168 } 1169 return result; 1170 } 1171 1172 /** 1173 * Notifies that the future calls to {@link #renderAndAttach(Layout, ResourceProto.Resources, 1174 * ViewGroup)} will have a different versioning for layouts and resources. So any cached 1175 * rendered result should be cleared. 1176 */ invalidateCache()1177 public void invalidateCache() { 1178 mPrevResourcesVersion = null; 1179 // Cancel any ongoing rendering which might have a reference to older app resources. 1180 if (mRenderFuture != null && !mRenderFuture.isDone()) { 1181 mRenderFuture.cancel(/* mayInterruptIfRunning= */ false); 1182 mRenderFuture = null; 1183 Log.w(TAG, "Cancelled ongoing rendering due to cache invalidation."); 1184 } 1185 } 1186 getOnlyChildViewGroup(@onNull ViewGroup parent)1187 private static @Nullable ViewGroup getOnlyChildViewGroup(@NonNull ViewGroup parent) { 1188 if (parent.getChildCount() == 1) { 1189 View child = parent.getChildAt(0); 1190 if (child instanceof ViewGroup) { 1191 return (ViewGroup) child; 1192 } 1193 } 1194 return null; 1195 } 1196 1197 @UiThread 1198 @SuppressWarnings("ExecutorTaskName") postInflate( @onNull ViewGroup attachParent, @Nullable ViewGroup prevInflateParent, @NonNull RenderResult renderResult, boolean isReattaching, @NonNull Layout layout, ResourceProto.@NonNull Resources resources, InflaterStatsLogger inflaterStatsLogger)1199 private @NonNull ListenableFuture<RenderingArtifact> postInflate( 1200 @NonNull ViewGroup attachParent, 1201 @Nullable ViewGroup prevInflateParent, 1202 @NonNull RenderResult renderResult, 1203 boolean isReattaching, 1204 @NonNull Layout layout, 1205 ResourceProto.@NonNull Resources resources, 1206 InflaterStatsLogger inflaterStatsLogger) { 1207 mCanReattachWithoutRendering = renderResult.canReattachWithoutRendering(); 1208 1209 if (renderResult instanceof InflatedIntoNewParentRenderResult) { 1210 InflateParentData newInflateParentData = 1211 ((InflatedIntoNewParentRenderResult) renderResult).mNewInflateParentData; 1212 mInflateParent = 1213 checkNotNull( 1214 newInflateParentData.mInflateResult, 1215 TAG 1216 + " - inflated result was null, but inflating was" 1217 + " requested.") 1218 .inflateParent; 1219 } 1220 1221 ListenableFuture<RenderingArtifact> postInflateFuture = 1222 renderResult.postInflate( 1223 attachParent, prevInflateParent, isReattaching, inflaterStatsLogger); 1224 SettableFuture<RenderingArtifact> result = SettableFuture.create(); 1225 if (!postInflateFuture.isDone()) { 1226 postInflateFuture.addListener( 1227 () -> { 1228 try { 1229 result.set(postInflateFuture.get()); 1230 } catch (ExecutionException 1231 | InterruptedException 1232 | CancellationException e) { 1233 result.setFuture( 1234 handlePostInflateFailure( 1235 e, 1236 layout, 1237 resources, 1238 prevInflateParent, 1239 attachParent, 1240 inflaterStatsLogger)); 1241 } 1242 }, 1243 mUiExecutorService); 1244 } else { 1245 try { 1246 return immediateFuture(postInflateFuture.get()); 1247 } catch (ExecutionException 1248 | InterruptedException 1249 | CancellationException 1250 | ViewMutationException e) { 1251 return handlePostInflateFailure( 1252 e, layout, resources, prevInflateParent, attachParent, inflaterStatsLogger); 1253 } 1254 } 1255 return result; 1256 } 1257 1258 @UiThread 1259 @SuppressWarnings("ReferenceEquality") // layout == prevLayout is intentional handlePostInflateFailure( @onNull Throwable error, @NonNull Layout layout, ResourceProto.@NonNull Resources resources, @Nullable ViewGroup prevInflateParent, @NonNull ViewGroup parent, InflaterStatsLogger inflaterStatsLogger)1260 private @NonNull ListenableFuture<RenderingArtifact> handlePostInflateFailure( 1261 @NonNull Throwable error, 1262 @NonNull Layout layout, 1263 ResourceProto.@NonNull Resources resources, 1264 @Nullable ViewGroup prevInflateParent, 1265 @NonNull ViewGroup parent, 1266 InflaterStatsLogger inflaterStatsLogger) { 1267 // If a RuntimeError is thrown, it'll be wrapped in an UncheckedExecutionException 1268 Throwable e = error.getCause(); 1269 if (e instanceof ViewMutationException) { 1270 inflaterStatsLogger.logIgnoredFailure(IGNORED_FAILURE_APPLY_MUTATION_EXCEPTION); 1271 Log.w(TAG, "applyMutation failed." + e.getMessage()); 1272 if (mPrevLayout == layout && parent == mAttachParent) { 1273 Log.w(TAG, "Retrying full inflation."); 1274 // Clear rendering metadata and prevLayout to force a full reinflation. 1275 ProtoLayoutInflater.clearRenderedMetadata(checkNotNull(prevInflateParent)); 1276 mPrevLayout = null; 1277 return renderAndAttach(layout, resources, parent, inflaterStatsLogger); 1278 } 1279 } else { 1280 Log.e(TAG, "postInflate failed.", error); 1281 } 1282 return Futures.immediateFailedFuture(error); 1283 } 1284 1285 /** 1286 * Detach this layout from a parent container. Note that it is safe to call this method while 1287 * the layout is inflating; see the notes on {@link ProtoLayoutViewInstance#renderAndAttach} for 1288 * more information. 1289 */ 1290 @UiThread detach(@onNull ViewGroup parent)1291 public void detach(@NonNull ViewGroup parent) { 1292 if (mAttachParent != null && mAttachParent != parent) { 1293 throw new IllegalStateException("Layout is not attached to parent " + parent); 1294 } 1295 detachInternal(); 1296 } 1297 1298 @UiThread detachInternal()1299 private void detachInternal() { 1300 if (mRenderFuture != null && !mRenderFuture.isDone()) { 1301 mRenderFuture.cancel(/* mayInterruptIfRunning= */ false); 1302 mRenderFuture = null; 1303 } 1304 setLayoutVisibility(ProtoLayoutVisibilityState.VISIBILITY_STATE_INVISIBLE); 1305 1306 ViewGroup inflateParent = mInflateParent; 1307 if (inflateParent != null) { 1308 ViewGroup parent = (ViewGroup) inflateParent.getParent(); 1309 if (mAttachParent != null && mAttachParent != parent) { 1310 Log.w(TAG, "inflateParent was attached to the wrong parent."); 1311 } 1312 if (parent != null) { 1313 parent.removeView(inflateParent); 1314 } 1315 } 1316 mAttachParent = null; 1317 } 1318 1319 /** 1320 * Sets whether updates are enabled for this layout. When disabled, updates through the data 1321 * pipeline (e.g. health updates) will be suppressed. 1322 */ 1323 @RestrictTo(Scope.LIBRARY) 1324 @UiThread 1325 @SuppressWarnings("RestrictTo") setUpdatesEnabled(boolean updatesEnabled)1326 public void setUpdatesEnabled(boolean updatesEnabled) { 1327 if (mDataPipeline != null) { 1328 mDataPipeline.setUpdatesEnabled(updatesEnabled); 1329 } 1330 } 1331 1332 /** Sets the visibility state for this layout. */ 1333 @RestrictTo(Scope.LIBRARY) 1334 @UiThread setLayoutVisibility(@rotoLayoutVisibilityState int visibility)1335 public void setLayoutVisibility(@ProtoLayoutVisibilityState int visibility) { 1336 1337 if (mAnimationEnabled && mDataPipeline != null) { 1338 // Need to check here the previous layout visibility was not FULLY_VISIBLE, so that when 1339 // the user swipes a little away from the layout, which will emit PARTIALLY_VISIBLE, but 1340 // then go back to the current layout without entering another one, we do not want to 1341 // restart the animation when FULLY_VISIBILITY is emitted in this situation. 1342 if (visibility == ProtoLayoutVisibilityState.VISIBILITY_STATE_FULLY_VISIBLE 1343 && !mWasFullyVisibleBefore) { 1344 mDataPipeline.setFullyVisible(true); 1345 mWasFullyVisibleBefore = true; 1346 } else if (visibility == ProtoLayoutVisibilityState.VISIBILITY_STATE_INVISIBLE) { 1347 mDataPipeline.setFullyVisible(false); 1348 mWasFullyVisibleBefore = false; 1349 } 1350 } 1351 } 1352 1353 /** Sets whether a new layout is pending. This is used to update the data pipeline. */ 1354 @RestrictTo(Scope.LIBRARY) 1355 @UiThread setLayoutUpdatePending(boolean isLayoutUpdatePending)1356 public void setLayoutUpdatePending(boolean isLayoutUpdatePending) { 1357 if (mDataPipeline != null) { 1358 mDataPipeline.setLayoutUpdatePending(isLayoutUpdatePending); 1359 } 1360 } 1361 1362 /** Returns true if the layout element depth doesn't exceed the given {@code allowedDepth}. */ checkLayoutDepth( LayoutElement layoutElement, int allowedDepth, InflaterStatsLogger inflaterStatsLogger)1363 private void checkLayoutDepth( 1364 LayoutElement layoutElement, 1365 int allowedDepth, 1366 InflaterStatsLogger inflaterStatsLogger) { 1367 if (allowedDepth <= 0) { 1368 handleLayoutDepthCheckFailure(inflaterStatsLogger); 1369 } 1370 List<LayoutElement> children = ImmutableList.of(); 1371 switch (layoutElement.getInnerCase()) { 1372 case COLUMN: 1373 children = layoutElement.getColumn().getContentsList(); 1374 break; 1375 case ROW: 1376 children = layoutElement.getRow().getContentsList(); 1377 break; 1378 case BOX: 1379 children = layoutElement.getBox().getContentsList(); 1380 break; 1381 case ARC: 1382 List<ArcLayoutElement> arcElements = layoutElement.getArc().getContentsList(); 1383 if (!arcElements.isEmpty() && allowedDepth == 1) { 1384 handleLayoutDepthCheckFailure(inflaterStatsLogger); 1385 } 1386 for (ArcLayoutElement element : arcElements) { 1387 if (element.getInnerCase() == InnerCase.ADAPTER) { 1388 checkLayoutDepth( 1389 element.getAdapter().getContent(), 1390 allowedDepth - 1, 1391 inflaterStatsLogger); 1392 } 1393 } 1394 break; 1395 case SPANNABLE: 1396 if (layoutElement.getSpannable().getSpansCount() > 0 && allowedDepth == 1) { 1397 handleLayoutDepthCheckFailure(inflaterStatsLogger); 1398 } 1399 break; 1400 default: 1401 // Other LayoutElements have depth of one. 1402 } 1403 for (LayoutElement child : children) { 1404 checkLayoutDepth(child, allowedDepth - 1, inflaterStatsLogger); 1405 } 1406 } 1407 handleLayoutDepthCheckFailure(InflaterStatsLogger inflaterStatsLogger)1408 private void handleLayoutDepthCheckFailure(InflaterStatsLogger inflaterStatsLogger) { 1409 inflaterStatsLogger.logInflationFailed(INFLATION_FAILURE_REASON_LAYOUT_DEPTH_EXCEEDED); 1410 mPrevLayoutAlreadyFailingDepthCheck = true; 1411 throw new IllegalStateException( 1412 "Layout depth exceeds maximum allowed depth: " + MAX_LAYOUT_ELEMENT_DEPTH); 1413 } 1414 1415 @Override close()1416 public void close() throws Exception { 1417 detachInternal(); 1418 mRenderFuture = null; 1419 mPrevLayout = null; 1420 if (mDataPipeline != null) { 1421 mDataPipeline.close(); 1422 } 1423 } 1424 } 1425