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