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 {