Garbage collect PietSharedStates when all live sessions have the appropriate
schema version.

PiperOrigin-RevId: 251509997
Change-Id: I70a39c1bc1ea8a8937d4e6257b5df930aeb27eb7
diff --git a/src/main/java/com/google/android/libraries/feed/api/client/scope/BUILD b/src/main/java/com/google/android/libraries/feed/api/client/scope/BUILD
index 8aa7a36..815b486 100644
--- a/src/main/java/com/google/android/libraries/feed/api/client/scope/BUILD
+++ b/src/main/java/com/google/android/libraries/feed/api/client/scope/BUILD
@@ -56,6 +56,7 @@
         "//src/main/java/com/google/android/libraries/feed/hostimpl/network",
         "//src/main/java/com/google/android/libraries/feed/hostimpl/scheduler",
         "//src/main/java/com/google/android/libraries/feed/hostimpl/storage",
+        "//src/main/java/com/google/android/libraries/feed/sharedstream/piet",
         "@com_google_code_findbugs_jsr305//jar",
         "@maven//:com_android_support_support_annotations",
     ],
diff --git a/src/main/java/com/google/android/libraries/feed/api/client/scope/ProcessScopeBuilder.java b/src/main/java/com/google/android/libraries/feed/api/client/scope/ProcessScopeBuilder.java
index 6a0f387..1a7604f 100644
--- a/src/main/java/com/google/android/libraries/feed/api/client/scope/ProcessScopeBuilder.java
+++ b/src/main/java/com/google/android/libraries/feed/api/client/scope/ProcessScopeBuilder.java
@@ -63,6 +63,7 @@
 import com.google.android.libraries.feed.hostimpl.scheduler.SchedulerApiWrapper;
 import com.google.android.libraries.feed.hostimpl.storage.InMemoryContentStorage;
 import com.google.android.libraries.feed.hostimpl.storage.InMemoryJournalStorage;
+import com.google.android.libraries.feed.sharedstream.piet.PietRequiredContentAdapter;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.concurrent.Executor;
@@ -220,7 +221,9 @@
     FeedAppLifecycleListener lifecycleListener = new FeedAppLifecycleListener(threadUtils);
     lifecycleListener.registerObserver(store);
 
-    ProtocolAdapter protocolAdapter = new FeedProtocolAdapter(Collections.emptyList(), timingUtils);
+    ProtocolAdapter protocolAdapter =
+        new FeedProtocolAdapter(
+            Collections.singletonList(new PietRequiredContentAdapter()), timingUtils);
     ActionReader actionReader =
         new FeedActionReader(store, clock, protocolAdapter, taskQueue, configuration);
     FeedRequestManager feedRequestManager =
diff --git a/src/main/java/com/google/android/libraries/feed/api/internal/common/Model.java b/src/main/java/com/google/android/libraries/feed/api/internal/common/Model.java
index d8c49a8..d85b952 100644
--- a/src/main/java/com/google/android/libraries/feed/api/internal/common/Model.java
+++ b/src/main/java/com/google/android/libraries/feed/api/internal/common/Model.java
@@ -21,7 +21,7 @@
 /** Contains a list of {@link StreamDataOperations}s and a schema version. */
 public final class Model {
   /** The current schema version. */
-  public static final int CURRENT_SCHEMA_VERSION = 1;
+  public static final int CURRENT_SCHEMA_VERSION = 2;
 
   public final List<StreamDataOperation> streamDataOperations;
   public final int schemaVersion;
diff --git a/src/main/java/com/google/android/libraries/feed/common/testing/BUILD b/src/main/java/com/google/android/libraries/feed/common/testing/BUILD
index 691b992..d20fde4 100644
--- a/src/main/java/com/google/android/libraries/feed/common/testing/BUILD
+++ b/src/main/java/com/google/android/libraries/feed/common/testing/BUILD
@@ -33,6 +33,7 @@
         "//src/main/java/com/google/android/libraries/feed/feedsessionmanager",
         "//src/main/java/com/google/android/libraries/feed/feedstore",
         "//src/main/java/com/google/android/libraries/feed/hostimpl/storage",
+        "//src/main/java/com/google/android/libraries/feed/sharedstream/piet",
         "//src/main/java/com/google/android/libraries/feed/testing/host/logging",
         "//src/main/java/com/google/android/libraries/feed/testing/host/scheduler",
         "//src/main/java/com/google/android/libraries/feed/testing/modelprovider",
diff --git a/src/main/java/com/google/android/libraries/feed/common/testing/InfraIntegrationScope.java b/src/main/java/com/google/android/libraries/feed/common/testing/InfraIntegrationScope.java
index 24c4e55..2cc411d 100644
--- a/src/main/java/com/google/android/libraries/feed/common/testing/InfraIntegrationScope.java
+++ b/src/main/java/com/google/android/libraries/feed/common/testing/InfraIntegrationScope.java
@@ -42,6 +42,7 @@
 import com.google.android.libraries.feed.feedstore.FeedStore;
 import com.google.android.libraries.feed.hostimpl.storage.InMemoryContentStorage;
 import com.google.android.libraries.feed.hostimpl.storage.InMemoryJournalStorage;
+import com.google.android.libraries.feed.sharedstream.piet.PietRequiredContentAdapter;
 import com.google.android.libraries.feed.testing.host.logging.FakeBasicLoggingApi;
 import com.google.android.libraries.feed.testing.host.scheduler.FakeSchedulerApi;
 import com.google.android.libraries.feed.testing.requestmanager.FakeActionUploadRequestManager;
@@ -117,7 +118,9 @@
             fakeClock,
             fakeBasicLoggingApi,
             fakeMainThreadRunner);
-    feedProtocolAdapter = new FeedProtocolAdapter(Collections.emptyList(), timingUtils);
+    feedProtocolAdapter =
+        new FeedProtocolAdapter(
+            Collections.singletonList(new PietRequiredContentAdapter()), timingUtils);
     fakeFeedRequestManager =
         new FakeFeedRequestManager(
             fakeThreadUtils, fakeMainThreadRunner, feedProtocolAdapter, taskQueue);
diff --git a/src/main/java/com/google/android/libraries/feed/feedsessionmanager/internal/SessionCache.java b/src/main/java/com/google/android/libraries/feed/feedsessionmanager/internal/SessionCache.java
index 7f6002f..14c2e4c 100644
--- a/src/main/java/com/google/android/libraries/feed/feedsessionmanager/internal/SessionCache.java
+++ b/src/main/java/com/google/android/libraries/feed/feedsessionmanager/internal/SessionCache.java
@@ -54,6 +54,7 @@
  */
 public final class SessionCache implements Dumpable {
   private static final String TAG = "SessionCache";
+  @VisibleForTesting static final int MIN_SCHEMA_VERSION_FOR_PIET_SHARED_STATE_REQUIRED_CONTENT = 2;
   @VisibleForTesting static final String STREAM_SESSION_CONTENT_ID = "FSM::Sessions::0";
 
   /**
@@ -485,7 +486,21 @@
         store.triggerContentGc(
             reservedContentIds,
             getAccessibleContentSupplier(sessionContentTrackers),
-            /* keepSharedStates= */ true));
+            shouldKeepSharedStates()));
+  }
+
+  private boolean shouldKeepSharedStates() {
+    synchronized (lock) {
+      for (String sessionId : sessionsMetadata.keySet()) {
+        SessionMetadata metadata = sessionsMetadata.get(sessionId);
+        if (metadata.getSchemaVersion()
+            < MIN_SCHEMA_VERSION_FOR_PIET_SHARED_STATE_REQUIRED_CONTENT) {
+          return true;
+        }
+      }
+    }
+
+    return false;
   }
 
   private Supplier<Set<String>> getAccessibleContentSupplier(
diff --git a/src/main/java/com/google/android/libraries/feed/feedstore/internal/ContentGc.java b/src/main/java/com/google/android/libraries/feed/feedstore/internal/ContentGc.java
index cc4aea1..75ab2c4 100644
--- a/src/main/java/com/google/android/libraries/feed/feedstore/internal/ContentGc.java
+++ b/src/main/java/com/google/android/libraries/feed/feedstore/internal/ContentGc.java
@@ -89,6 +89,7 @@
     ElapsedTimeTracker tracker = timingUtils.getElapsedTimeTracker(TAG);
     ContentMutation.Builder mutationBuilder = new ContentMutation.Builder();
     for (String key : unAccessible) {
+      Logger.i(TAG, "Removing %s", key);
       mutationBuilder.delete(key);
     }
     CommitResult result = contentStorageDirect.commit(mutationBuilder.build());
diff --git a/src/main/java/com/google/android/libraries/feed/sharedstream/piet/BUILD b/src/main/java/com/google/android/libraries/feed/sharedstream/piet/BUILD
index 94793fa..f9fbb81 100644
--- a/src/main/java/com/google/android/libraries/feed/sharedstream/piet/BUILD
+++ b/src/main/java/com/google/android/libraries/feed/sharedstream/piet/BUILD
@@ -8,6 +8,7 @@
     deps = [
         "//src/main/java/com/google/android/libraries/feed/api/host/imageloader",
         "//src/main/java/com/google/android/libraries/feed/api/host/logging",
+        "//src/main/java/com/google/android/libraries/feed/api/internal/protocoladapter",
         "//src/main/java/com/google/android/libraries/feed/common/functional",
         "//src/main/java/com/google/android/libraries/feed/common/logging",
         "//src/main/java/com/google/android/libraries/feed/common/time",
@@ -15,7 +16,9 @@
         "//src/main/java/com/google/android/libraries/feed/sharedstream/offlinemonitor",
         "//src/main/proto/search/now/ui/piet:piet_errors_java_proto_lite",
         "//src/main/proto/search/now/ui/piet:piet_java_proto_lite",
+        "//src/main/proto/search/now/ui/stream:stream_java_proto_lite",
         "//src/main/proto/search/now/ui/stream:stream_offline_extension_java_proto_lite",
+        "//src/main/proto/search/now/wire/feed:feed_java_proto_lite",
         "@com_google_code_findbugs_jsr305//jar",
         "@javax_inject_javax_inject//jar",
     ],
diff --git a/src/main/java/com/google/android/libraries/feed/sharedstream/piet/PietRequiredContentAdapter.java b/src/main/java/com/google/android/libraries/feed/sharedstream/piet/PietRequiredContentAdapter.java
new file mode 100644
index 0000000..397ba8e
--- /dev/null
+++ b/src/main/java/com/google/android/libraries/feed/sharedstream/piet/PietRequiredContentAdapter.java
@@ -0,0 +1,40 @@
+// Copyright 2019 The Feed Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.android.libraries.feed.sharedstream.piet;
+
+import com.google.android.libraries.feed.api.internal.protocoladapter.RequiredContentAdapter;
+import com.google.search.now.ui.stream.StreamStructureProto.Content;
+import com.google.search.now.ui.stream.StreamStructureProto.PietContent;
+import com.google.search.now.wire.feed.ContentIdProto.ContentId;
+import com.google.search.now.wire.feed.DataOperationProto.DataOperation;
+import com.google.search.now.wire.feed.DataOperationProto.DataOperation.Operation;
+import java.util.Collections;
+import java.util.List;
+
+/** Implementation of {@link RequiredContentAdapter} that identifies dependent PietSharedStates. */
+public final class PietRequiredContentAdapter implements RequiredContentAdapter {
+  @Override
+  public List<ContentId> determineRequiredContentIds(DataOperation dataOperation) {
+    if (dataOperation.getOperation() != Operation.UPDATE_OR_APPEND) {
+      return Collections.emptyList();
+    }
+
+    return dataOperation
+        .getFeature()
+        .getExtension(Content.contentExtension)
+        .getExtension(PietContent.pietContentExtension)
+        .getPietSharedStatesList();
+  }
+}
diff --git a/src/test/java/com/google/android/libraries/feed/feedsessionmanager/internal/SessionCacheTest.java b/src/test/java/com/google/android/libraries/feed/feedsessionmanager/internal/SessionCacheTest.java
index bc90038..14b28f9 100644
--- a/src/test/java/com/google/android/libraries/feed/feedsessionmanager/internal/SessionCacheTest.java
+++ b/src/test/java/com/google/android/libraries/feed/feedsessionmanager/internal/SessionCacheTest.java
@@ -34,6 +34,7 @@
 import com.google.search.now.feed.client.StreamDataProto.StreamPayload;
 import com.google.search.now.feed.client.StreamDataProto.StreamSession;
 import com.google.search.now.feed.client.StreamDataProto.StreamSessions;
+import com.google.search.now.feed.client.StreamDataProto.StreamSharedState;
 import com.google.search.now.feed.client.StreamDataProto.StreamStructure;
 import com.google.search.now.feed.client.StreamDataProto.StreamStructure.Operation;
 import com.google.search.now.feed.client.StreamDataProto.UiContext;
@@ -100,6 +101,55 @@
   }
 
   @Test
+  public void testInitialization_contentGcShouldKeepSharedStates() {
+    fakeStore.setContent(
+        "foo",
+        StreamPayload.newBuilder()
+            .setStreamSharedState(StreamSharedState.newBuilder().setContentId("foo").build())
+            .build());
+    mockStreamSessions(
+        StreamSessions.newBuilder()
+            .addStreamSession(
+                StreamSession.newBuilder()
+                    .setSessionId(Store.HEAD_SESSION_ID)
+                    .setSessionMetadata(
+                        SessionMetadata.newBuilder()
+                            .setSchemaVersion(
+                                SessionCache
+                                        .MIN_SCHEMA_VERSION_FOR_PIET_SHARED_STATE_REQUIRED_CONTENT
+                                    - 1)))
+            .build());
+
+    assertThat(fakeStore.getSharedStates().getValue()).hasSize(1);
+    sessionCache.initialize();
+    assertThat(fakeStore.getSharedStates().getValue()).hasSize(1);
+  }
+
+  @Test
+  public void testInitialization_contentGcShouldDiscardSharedStates() {
+    fakeStore.setContent(
+        "foo",
+        StreamPayload.newBuilder()
+            .setStreamSharedState(StreamSharedState.newBuilder().setContentId("foo").build())
+            .build());
+    mockStreamSessions(
+        StreamSessions.newBuilder()
+            .addStreamSession(
+                StreamSession.newBuilder()
+                    .setSessionId(Store.HEAD_SESSION_ID)
+                    .setSessionMetadata(
+                        SessionMetadata.newBuilder()
+                            .setSchemaVersion(
+                                SessionCache
+                                    .MIN_SCHEMA_VERSION_FOR_PIET_SHARED_STATE_REQUIRED_CONTENT)))
+            .build());
+
+    assertThat(fakeStore.getSharedStates().getValue()).hasSize(1);
+    sessionCache.initialize();
+    assertThat(fakeStore.getSharedStates().getValue()).isEmpty();
+  }
+
+  @Test
   public void testPutGet() {
     sessionCache.initialize();
     Session session = populateSession(1, 2);
diff --git a/src/test/java/com/google/android/libraries/feed/infraintegration/GcTest.java b/src/test/java/com/google/android/libraries/feed/infraintegration/GcTest.java
index 708c1ae..3c02e51 100644
--- a/src/test/java/com/google/android/libraries/feed/infraintegration/GcTest.java
+++ b/src/test/java/com/google/android/libraries/feed/infraintegration/GcTest.java
@@ -130,7 +130,8 @@
     fakeClock.advance(LIFETIME_MS + 1L);
     InfraIntegrationScope secondScope = scope.clone();
     assertPayloads(REQUEST_1, secondScope, /* shouldExist= */ false);
-    assertSharedStates(new ContentId[] {PIET_SHARED_STATE_1}, secondScope, /* shouldExist= */ true);
+    assertSharedStates(
+        new ContentId[] {PIET_SHARED_STATE_1}, secondScope, /* shouldExist= */ false);
     assertPayloads(REQUEST_2, secondScope, /* shouldExist= */ true);
     assertSharedStates(new ContentId[] {PIET_SHARED_STATE_2}, secondScope, /* shouldExist= */ true);
   }
diff --git a/src/test/java/com/google/android/libraries/feed/sharedstream/piet/BUILD b/src/test/java/com/google/android/libraries/feed/sharedstream/piet/BUILD
index 028491b..f62c4c9 100644
--- a/src/test/java/com/google/android/libraries/feed/sharedstream/piet/BUILD
+++ b/src/test/java/com/google/android/libraries/feed/sharedstream/piet/BUILD
@@ -80,3 +80,21 @@
         "@robolectric//bazel:android-all",
     ],
 )
+
+android_local_test(
+    name = "PietRequiredContentAdapterTest",
+    size = "small",
+    timeout = "moderate",
+    srcs = ["PietRequiredContentAdapterTest.java"],
+    aapt_version = "aapt2",
+    manifest_values = DEFAULT_ANDROID_LOCAL_TEST_MANIFEST,
+    deps = [
+        "//src/main/java/com/google/android/libraries/feed/sharedstream/piet",
+        "//src/main/proto/search/now/ui/stream:stream_java_proto_lite",
+        "//src/main/proto/search/now/wire/feed:feed_java_proto_lite",
+        "//third_party:robolectric",
+        "@com_google_protobuf_javalite//:protobuf_java_lite",
+        "@maven//:com_google_truth_truth",
+        "@robolectric//bazel:android-all",
+    ],
+)
diff --git a/src/test/java/com/google/android/libraries/feed/sharedstream/piet/PietRequiredContentAdapterTest.java b/src/test/java/com/google/android/libraries/feed/sharedstream/piet/PietRequiredContentAdapterTest.java
new file mode 100644
index 0000000..bfbf987
--- /dev/null
+++ b/src/test/java/com/google/android/libraries/feed/sharedstream/piet/PietRequiredContentAdapterTest.java
@@ -0,0 +1,90 @@
+// Copyright 2019 The Feed Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.android.libraries.feed.sharedstream.piet;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.search.now.ui.stream.StreamStructureProto.Content;
+import com.google.search.now.ui.stream.StreamStructureProto.PietContent;
+import com.google.search.now.wire.feed.ContentIdProto.ContentId;
+import com.google.search.now.wire.feed.DataOperationProto.DataOperation;
+import com.google.search.now.wire.feed.DataOperationProto.DataOperation.Operation;
+import com.google.search.now.wire.feed.FeatureProto.Feature;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+/** Tests for {@link PietRequiredContentAdapter}. */
+@RunWith(RobolectricTestRunner.class)
+public class PietRequiredContentAdapterTest {
+  private static final ContentId CONTENT_ID_1 = ContentId.newBuilder().setId(1).build();
+  private static final ContentId CONTENT_ID_2 = ContentId.newBuilder().setId(2).build();
+  private static final Feature FEATURE =
+      Feature.newBuilder()
+          .setExtension(
+              Content.contentExtension,
+              Content.newBuilder()
+                  .setExtension(
+                      PietContent.pietContentExtension,
+                      PietContent.newBuilder()
+                          .addPietSharedStates(CONTENT_ID_1)
+                          .addPietSharedStates(CONTENT_ID_2)
+                          .build())
+                  .build())
+          .build();
+
+  private final PietRequiredContentAdapter adapter = new PietRequiredContentAdapter();
+
+  @Test
+  public void testDetermineRequiredContentIds() {
+    assertThat(
+            adapter.determineRequiredContentIds(
+                DataOperation.newBuilder()
+                    .setOperation(Operation.UPDATE_OR_APPEND)
+                    .setFeature(FEATURE)
+                    .build()))
+        .containsExactly(CONTENT_ID_1, CONTENT_ID_2);
+  }
+
+  @Test
+  public void testDetermineRequiredContentIds_removeDoesNotRequireContent() {
+    assertThat(
+            adapter.determineRequiredContentIds(
+                DataOperation.newBuilder()
+                    .setOperation(Operation.REMOVE)
+                    .setFeature(FEATURE)
+                    .build()))
+        .isEmpty();
+  }
+
+  @Test
+  public void testDetermineRequiredContentIds_clearAllDoesNotRequireContent() {
+    assertThat(
+            adapter.determineRequiredContentIds(
+                DataOperation.newBuilder()
+                    .setOperation(Operation.CLEAR_ALL)
+                    .setFeature(FEATURE)
+                    .build()))
+        .isEmpty();
+  }
+
+  @Test
+  public void testDetermineRequiredContentIds_defaultInstanceDoesNotRequireContent() {
+    assertThat(
+            adapter.determineRequiredContentIds(
+                DataOperation.newBuilder().setOperation(Operation.UPDATE_OR_APPEND).build()))
+        .isEmpty();
+  }
+}