Invalidate layout on changes to resource versions.

BUG: 280563595
BUG: 285570489
Change-Id: I4f3281861e8b9533a175c7fc3bcc8530cdf9be3d
diff --git a/wear/protolayout/protolayout-renderer/api/restricted_current.txt b/wear/protolayout/protolayout-renderer/api/restricted_current.txt
index e393e1d..47edbef 100644
--- a/wear/protolayout/protolayout-renderer/api/restricted_current.txt
+++ b/wear/protolayout/protolayout-renderer/api/restricted_current.txt
@@ -5,6 +5,7 @@
     ctor public ProtoLayoutViewInstance(androidx.wear.protolayout.renderer.impl.ProtoLayoutViewInstance.Config);
     method public void close() throws java.lang.Exception;
     method @UiThread public void detach(android.view.ViewGroup);
+    method public void invalidateCache();
     method @UiThread public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> renderAndAttach(androidx.wear.protolayout.proto.LayoutElementProto.Layout, androidx.wear.protolayout.proto.ResourceProto.Resources, android.view.ViewGroup);
   }
 
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstance.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstance.java
index 798f1ad..085254c 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstance.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstance.java
@@ -73,6 +73,7 @@
 
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.CancellationException;
 import java.util.concurrent.ExecutionException;
@@ -152,6 +153,13 @@
     private boolean mPrevLayoutAlreadyFailingDepthCheck = false;
 
     /**
+     * This is used to make sure resource version changes invalidate the layout. Otherwise, this
+     * could result in the resource change not getting reflected with diff rendering (if the layout
+     * pointing to that resource hasn't changed)
+     */
+    @Nullable private String mPrevResourcesVersion = null;
+
+    /**
      * This is used as the Future for the currently running inflation session. The first time
      * "attach" is called, it should start the renderer. Subsequent attach calls should only ever
      * re-attach "inflateParent".
@@ -229,6 +237,7 @@
             return Futures.immediateVoidFuture();
         }
     }
+
     /** Result of a {@link #renderOrComputeMutations} call when a failure has happened. */
     static final class FailedRenderResult implements RenderResult {
         @Override
@@ -245,6 +254,7 @@
             return Futures.immediateVoidFuture();
         }
     }
+
     /**
      * Result of a {@link #renderOrComputeMutations} call when the layout has been inflated into a
      * new parent.
@@ -728,8 +738,7 @@
                                     config.getPlatformDataProviders(),
                                     stateStore,
                                     new FixedQuotaManagerImpl(
-                                            config.getRunningAnimationsLimit(),
-                                            "animations"),
+                                            config.getRunningAnimationsLimit(), "animations"),
                                     new FixedQuotaManagerImpl(
                                             DYNAMIC_NODES_MAX_COUNT, "dynamic nodes"))
                             : new ProtoLayoutDynamicDataPipeline(
@@ -892,10 +901,11 @@
         boolean isReattaching = false;
         if (mRenderFuture != null) {
             if (!mRenderFuture.isDone()) {
-                // There is an ongoing rendering operation. We'll skip this request as a missed
-                // frame.
-                Log.w(TAG, "Skipped layout update: previous layout update hasn't finished yet.");
-                return Futures.immediateCancelledFuture();
+                // There is an ongoing rendering operation. Cancel that and render the new layout.
+                Log.w(TAG, "Cancelling the previous layout update that hasn't finished yet.");
+                checkNotNull(mRenderFuture).cancel(/* maybeInterruptIfRunning= */ false);
+
+                mRenderFuture = null;
             } else if (layout == mPrevLayout && mCanReattachWithoutRendering) {
                 isReattaching = true;
             } else {
@@ -903,15 +913,24 @@
             }
         }
 
-        @Nullable ViewGroup prevInflateParent = getOnlyChildViewGroup(mAttachParent);
-        @Nullable
-        RenderedMetadata prevRenderedMetadata =
-                prevInflateParent != null
-                        ? ProtoLayoutInflater.getRenderedMetadata(prevInflateParent)
-                        : null;
+        @Nullable ViewGroup prevInflateParent = getOnlyChildViewGroup(parent);
 
         if (mRenderFuture == null) {
+            if (prevInflateParent != null
+                    && !Objects.equals(resources.getVersion(), mPrevResourcesVersion)) {
+                // If the resource version has changed, clear the diff metadata to force a full
+                // reinflation.
+                ProtoLayoutInflater.clearRenderedMetadata(checkNotNull(prevInflateParent));
+            }
+
+            @Nullable
+            RenderedMetadata prevRenderedMetadata =
+                    prevInflateParent != null
+                            ? ProtoLayoutInflater.getRenderedMetadata(prevInflateParent)
+                            : null;
+
             mPrevLayout = layout;
+            mPrevResourcesVersion = resources.getVersion();
 
             int gravity = UNSPECIFIED_GRAVITY;
             LayoutParams layoutParams = new LayoutParams(MATCH_PARENT, MATCH_PARENT);
@@ -937,24 +956,32 @@
 
             mRenderFuture =
                     mBgExecutorService.submit(
-                            () -> renderOrComputeMutations(
-                                layout, resources, prevRenderedMetadata, parentViewProp));
+                            () ->
+                                    renderOrComputeMutations(
+                                            layout,
+                                            resources,
+                                            prevRenderedMetadata,
+                                            parentViewProp));
             mCanReattachWithoutRendering = false;
         }
         SettableFuture<Void> result = SettableFuture.create();
         if (!checkNotNull(mRenderFuture).isDone()) {
+            ListenableFuture<RenderResult> rendererFuture = mRenderFuture;
             mRenderFuture.addListener(
                     () -> {
-                        // Ensure that this inflater is attached to the same parent as when this
-                        // listener was created. If not, something has re-attached us in the time it
-                        // took for the inflater to execute.
+                        if (rendererFuture.isCancelled()) {
+                            result.cancel(/* mayInterruptIfRunning= */ false);
+                        }
+                        // Ensure that this inflater is attached to the same attachParent as when
+                        // this listener was created. If not, something has re-attached us in the
+                        // time it took for the inflater to execute.
                         if (mAttachParent == parent) {
                             try {
                                 result.setFuture(
                                         postInflate(
                                                 parent,
                                                 prevInflateParent,
-                                                checkNotNull(mRenderFuture).get(),
+                                                checkNotNull(rendererFuture).get(),
                                                 /* isReattaching= */ false,
                                                 layout,
                                                 resources));
@@ -991,6 +1018,15 @@
         return result;
     }
 
+    /**
+     * Notifies that the future calls to {@link #renderAndAttach(Layout, ResourceProto.Resources,
+     * ViewGroup)} will have a different versioning for layouts and resources. So any cached
+     * rendered result should be cleared.
+     */
+    public void invalidateCache() {
+        mPrevResourcesVersion = null;
+    }
+
     @Nullable
     private static ViewGroup getOnlyChildViewGroup(@NonNull ViewGroup parent) {
         if (parent.getChildCount() == 1) {
diff --git a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstanceTest.java b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstanceTest.java
index 2f8401c..1571b81 100644
--- a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstanceTest.java
+++ b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstanceTest.java
@@ -224,7 +224,7 @@
     }
 
     @Test
-    public void adaptiveUpdateRatesEnabled_ongoingRendering_skipsNewLayout() throws Exception {
+    public void adaptiveUpdateRatesEnabled_ongoingRendering_skipsPreviousLayout() {
         FrameLayout container = new FrameLayout(mApplicationContext);
         setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
         ListenableFuture<Void> result1 =
@@ -237,11 +237,11 @@
                         layout(column(text(TEXT1), text(TEXT3))), RESOURCES, container);
         shadowOf(Looper.getMainLooper()).idle();
 
-        assertNoException(result1);
-        assertThat(result2.isCancelled()).isTrue();
-        // Assert that only the modified text is reinflated.
-        assertThat(findViewsWithText(container, TEXT2)).hasSize(1);
-        assertThat(findViewsWithText(container, TEXT3)).isEmpty();
+        assertThat(result1.isCancelled()).isTrue();
+        assertThat(result2.isDone()).isTrue();
+        // Assert that the most recent layout is reinflated.
+        assertThat(findViewsWithText(container, TEXT2)).isEmpty();
+        assertThat(findViewsWithText(container, TEXT3)).hasSize(1);
     }
 
     @Test
@@ -390,6 +390,52 @@
         assertThat(mRootContainer.getChildCount()).isEqualTo(0);
     }
 
+    @Test
+    public void resourceVersionChange_sameLayout_causesFullInflation() throws Exception {
+        Layout layout1 = layout(text(TEXT1));
+        Resources resources1 = Resources.newBuilder().setVersion("1").build();
+        Layout layout2 = layout(text(TEXT1));
+        Resources resources2 = Resources.newBuilder().setVersion("2").build();
+        setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
+        ListenableFuture<Void> result =
+                mInstanceUnderTest.renderAndAttach(layout1, resources1, mRootContainer);
+        shadowOf(Looper.getMainLooper()).idle();
+        assertNoException(result);
+        assertThat(findViewsWithText(mRootContainer, TEXT1)).hasSize(1);
+        View view1 = findViewsWithText(mRootContainer, TEXT1).get(0);
+
+        result = mInstanceUnderTest.renderAndAttach(layout2, resources2, mRootContainer);
+        shadowOf(Looper.getMainLooper()).idle();
+
+        assertNoException(result);
+        assertThat(findViewsWithText(mRootContainer, TEXT1)).hasSize(1);
+        View view2 = findViewsWithText(mRootContainer, TEXT1).get(0);
+        assertThat(view1).isNotSameInstanceAs(view2);
+    }
+
+    @Test
+    public void invalidateCache_sameResourceVersion_fullInflation() throws Exception {
+        Layout layout1 = layout(text(TEXT1));
+        Resources resources1 = Resources.newBuilder().setVersion("1").build();
+        Layout layout2 = layout(text(TEXT1));
+        Resources resources2 = Resources.newBuilder().setVersion("1").build();
+        setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
+        ListenableFuture<Void> result =
+                mInstanceUnderTest.renderAndAttach(layout1, resources1, mRootContainer);
+        shadowOf(Looper.getMainLooper()).idle();
+        assertNoException(result);
+        assertThat(findViewsWithText(mRootContainer, TEXT1)).hasSize(1);
+        View view1 = findViewsWithText(mRootContainer, TEXT1).get(0);
+
+        mInstanceUnderTest.invalidateCache();
+        result = mInstanceUnderTest.renderAndAttach(layout2, resources2, mRootContainer);
+        shadowOf(Looper.getMainLooper()).idle();
+
+        assertNoException(result);
+        assertThat(findViewsWithText(mRootContainer, TEXT1)).hasSize(1);
+        View view2 = findViewsWithText(mRootContainer, TEXT1).get(0);
+        assertThat(view1).isNotSameInstanceAs(view2);
+    }
 
     @Test
     public void adaptiveUpdateRatesEnabled_rootElementdiff_keepsElementCentered() throws Exception {