Report InternalFeedError when storage fails.

PiperOrigin-RevId: 258380186
Change-Id: I5abb4701f08686b8528f7ecde32165e194914906
diff --git a/src/main/java/com/google/android/libraries/feed/api/host/logging/InternalFeedError.java b/src/main/java/com/google/android/libraries/feed/api/host/logging/InternalFeedError.java
index 975bc81..22888ab 100644
--- a/src/main/java/com/google/android/libraries/feed/api/host/logging/InternalFeedError.java
+++ b/src/main/java/com/google/android/libraries/feed/api/host/logging/InternalFeedError.java
@@ -83,6 +83,12 @@
    */
   int TASK_QUEUE_STARVATION = 12;
 
+  /** Represents a call to ContentStorage that returned with a missing item. */
+  int CONTENT_STORAGE_MISSING_ITEM = 13;
+
+  /** Represents bytes from ContentStorage that cannot be parsed into the proto. */
+  int ITEM_NOT_PARSED = 14;
+
   /** The next value that should be used when adding additional values to the IntDef. */
-  int NEXT_VALUE = 13;
+  int NEXT_VALUE = 15;
 }
diff --git a/src/main/java/com/google/android/libraries/feed/feedstore/FeedStore.java b/src/main/java/com/google/android/libraries/feed/feedstore/FeedStore.java
index 9574229..9cec44f 100644
--- a/src/main/java/com/google/android/libraries/feed/feedstore/FeedStore.java
+++ b/src/main/java/com/google/android/libraries/feed/feedstore/FeedStore.java
@@ -112,7 +112,9 @@
             journalStorage,
             threadUtils,
             this.clock,
-            this.storeHelper);
+            this.storeHelper,
+            basicLoggingApi,
+            mainThreadRunner);
     delegate = persistentStore;
   }
 
diff --git a/src/main/java/com/google/android/libraries/feed/feedstore/internal/BUILD b/src/main/java/com/google/android/libraries/feed/feedstore/internal/BUILD
index c3d4749..e918abc 100644
--- a/src/main/java/com/google/android/libraries/feed/feedstore/internal/BUILD
+++ b/src/main/java/com/google/android/libraries/feed/feedstore/internal/BUILD
@@ -6,10 +6,12 @@
     name = "internal",
     srcs = glob(["*.java"]),
     deps = [
+        "//src/main/java/com/google/android/libraries/feed/api/host/logging",
         "//src/main/java/com/google/android/libraries/feed/api/host/storage",
         "//src/main/java/com/google/android/libraries/feed/api/internal/common",
         "//src/main/java/com/google/android/libraries/feed/api/internal/store",
         "//src/main/java/com/google/android/libraries/feed/common",
+        "//src/main/java/com/google/android/libraries/feed/common/concurrent",
         "//src/main/java/com/google/android/libraries/feed/common/functional",
         "//src/main/java/com/google/android/libraries/feed/common/intern",
         "//src/main/java/com/google/android/libraries/feed/common/logging",
diff --git a/src/main/java/com/google/android/libraries/feed/feedstore/internal/PersistentFeedStore.java b/src/main/java/com/google/android/libraries/feed/feedstore/internal/PersistentFeedStore.java
index fbab4dd..84dfca1 100644
--- a/src/main/java/com/google/android/libraries/feed/feedstore/internal/PersistentFeedStore.java
+++ b/src/main/java/com/google/android/libraries/feed/feedstore/internal/PersistentFeedStore.java
@@ -20,6 +20,8 @@
 import static com.google.android.libraries.feed.feedstore.internal.FeedStoreConstants.UPLOADABLE_ACTION_PREFIX;
 
 import android.util.Base64;
+import com.google.android.libraries.feed.api.host.logging.BasicLoggingApi;
+import com.google.android.libraries.feed.api.host.logging.InternalFeedError;
 import com.google.android.libraries.feed.api.host.storage.CommitResult;
 import com.google.android.libraries.feed.api.host.storage.ContentMutation.Builder;
 import com.google.android.libraries.feed.api.host.storage.ContentStorage;
@@ -38,6 +40,7 @@
 import com.google.android.libraries.feed.api.internal.store.StoreListener;
 import com.google.android.libraries.feed.api.internal.store.UploadableActionMutation;
 import com.google.android.libraries.feed.common.Result;
+import com.google.android.libraries.feed.common.concurrent.MainThreadRunner;
 import com.google.android.libraries.feed.common.functional.Supplier;
 import com.google.android.libraries.feed.common.intern.Interner;
 import com.google.android.libraries.feed.common.intern.InternerWithStats;
@@ -80,6 +83,8 @@
   private final ThreadUtils threadUtils;
   private final Clock clock;
   private final FeedStoreHelper storeHelper;
+  private final BasicLoggingApi basicLoggingApi;
+  private final MainThreadRunner mainThreadRunner;
 
   // We use a common string interner pool because the same content IDs are reused across different
   // protos. The actual proto interners below are backed by thisl common pool.
@@ -97,7 +102,9 @@
       JournalStorageDirect journalStorageDirect,
       ThreadUtils threadUtils,
       Clock clock,
-      FeedStoreHelper storeHelper) {
+      FeedStoreHelper storeHelper,
+      BasicLoggingApi basicLoggingApi,
+      MainThreadRunner mainThreadRunner) {
     this.timingUtils = timingUtils;
     this.extensionRegistry = extensionRegistry;
     this.contentStorageDirect = contentStorageDirect;
@@ -105,6 +112,8 @@
     this.threadUtils = threadUtils;
     this.clock = clock;
     this.storeHelper = storeHelper;
+    this.basicLoggingApi = basicLoggingApi;
+    this.mainThreadRunner = mainThreadRunner;
   }
 
   @Override
@@ -119,8 +128,15 @@
       return Result.failure();
     }
 
-    // TODO: This code should be asserting that all requested contentIds have been
-    // provided, otherwise an unbound child could be exposed to the UI.
+    if (contentResult.getValue().entrySet().size() != contentIds.size()) {
+      Logger.e(TAG, "ContentStorage is missing content");
+      mainThreadRunner.execute(
+          "CONTENT_STORAGE_MISSING_ITEM",
+          () -> {
+            basicLoggingApi.onInternalError(InternalFeedError.CONTENT_STORAGE_MISSING_ITEM);
+          });
+    }
+
     for (Map.Entry<String, byte[]> entry : contentResult.getValue().entrySet()) {
       try {
         StreamPayload streamPayload =
@@ -130,9 +146,14 @@
         payloads.add(new PayloadWithId(entry.getKey(), streamPayload));
       } catch (InvalidProtocolBufferException e) {
         Logger.e(TAG, "Couldn't parse content proto for id %s", entry.getKey());
+        mainThreadRunner.execute(
+            "ITEM_NOT_PARSED",
+            () -> {
+              basicLoggingApi.onInternalError(InternalFeedError.ITEM_NOT_PARSED);
+            });
       }
     }
-    tracker.stop("", "getPayloads", "items", contentIds.size());
+    tracker.stop("", "getPayloads", "items", payloads.size());
     return Result.success(payloads);
   }
 
diff --git a/src/test/java/com/google/android/libraries/feed/feedstore/internal/BUILD b/src/test/java/com/google/android/libraries/feed/feedstore/internal/BUILD
index 7bf57ba..b428e85 100644
--- a/src/test/java/com/google/android/libraries/feed/feedstore/internal/BUILD
+++ b/src/test/java/com/google/android/libraries/feed/feedstore/internal/BUILD
@@ -73,6 +73,7 @@
     aapt_version = "aapt2",
     manifest_values = DEFAULT_ANDROID_LOCAL_TEST_MANIFEST,
     deps = [
+        "//src/main/java/com/google/android/libraries/feed/api/host/logging",
         "//src/main/java/com/google/android/libraries/feed/api/host/storage",
         "//src/main/java/com/google/android/libraries/feed/api/internal/common",
         "//src/main/java/com/google/android/libraries/feed/api/internal/store",
@@ -83,9 +84,11 @@
         "//src/main/java/com/google/android/libraries/feed/feedstore/internal",
         "//src/main/java/com/google/android/libraries/feed/feedstore/testing",
         "//src/main/java/com/google/android/libraries/feed/hostimpl/storage/testing",
+        "//src/main/java/com/google/android/libraries/feed/testing/host/logging",
         "//src/main/proto/com/google/android/libraries/feed/api/internal/proto:client_feed_java_proto_lite",
         "//third_party:robolectric",
         "@com_google_protobuf_javalite//:protobuf_java_lite",
+        "@maven//:com_google_guava_guava",
         "@maven//:com_google_truth_truth",
         "@maven//:org_mockito_mockito_core",
         "@robolectric//bazel:android-all",
diff --git a/src/test/java/com/google/android/libraries/feed/feedstore/internal/PersistentFeedStoreTest.java b/src/test/java/com/google/android/libraries/feed/feedstore/internal/PersistentFeedStoreTest.java
index 9895bc8..d0b9072 100644
--- a/src/test/java/com/google/android/libraries/feed/feedstore/internal/PersistentFeedStoreTest.java
+++ b/src/test/java/com/google/android/libraries/feed/feedstore/internal/PersistentFeedStoreTest.java
@@ -22,6 +22,7 @@
 import static org.mockito.Mockito.spy;
 import static org.mockito.MockitoAnnotations.initMocks;
 
+import com.google.android.libraries.feed.api.host.logging.InternalFeedError;
 import com.google.android.libraries.feed.api.host.storage.CommitResult;
 import com.google.android.libraries.feed.api.host.storage.ContentMutation;
 import com.google.android.libraries.feed.api.host.storage.ContentStorageDirect;
@@ -41,8 +42,12 @@
 import com.google.android.libraries.feed.feedstore.testing.DelegatingJournalStorage;
 import com.google.android.libraries.feed.hostimpl.storage.testing.InMemoryContentStorage;
 import com.google.android.libraries.feed.hostimpl.storage.testing.InMemoryJournalStorage;
+import com.google.android.libraries.feed.testing.host.logging.FakeBasicLoggingApi;
+import com.google.common.collect.ImmutableList;
 import com.google.protobuf.ByteString;
+import com.google.search.now.feed.client.StreamDataProto.StreamFeature;
 import com.google.search.now.feed.client.StreamDataProto.StreamLocalAction;
+import com.google.search.now.feed.client.StreamDataProto.StreamPayload;
 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.StreamUploadableAction;
@@ -67,12 +72,12 @@
   private final FeedExtensionRegistry extensionRegistry = new FeedExtensionRegistry(ArrayList::new);
   private final ContentStorageDirect contentStorage = new InMemoryContentStorage();
   private final JournalStorageDirect journalStorage = new InMemoryJournalStorage();
-  private FakeMainThreadRunner mainThreadRunner;
+  private final FakeBasicLoggingApi basicLoggingApi = new FakeBasicLoggingApi();
+  private final FakeMainThreadRunner mainThreadRunner = FakeMainThreadRunner.runTasksImmediately();
 
   @Before
   public void setUp() throws Exception {
     initMocks(this);
-    mainThreadRunner = FakeMainThreadRunner.runTasksImmediately();
   }
 
   @Override
@@ -84,7 +89,9 @@
         journalStorage,
         threadUtils,
         fakeClock,
-        new FeedStoreHelper());
+        new FeedStoreHelper(),
+        basicLoggingApi,
+        mainThreadRunner);
   }
 
   @Test
@@ -98,7 +105,9 @@
             journalStorage,
             threadUtils,
             fakeClock,
-            new FeedStoreHelper());
+            new FeedStoreHelper(),
+            basicLoggingApi,
+            mainThreadRunner);
     doAnswer(ans -> Result.failure()).when(contentStorageSpy).getAllKeys();
 
     boolean clearSuccess = store.clearNonActionContent();
@@ -116,7 +125,9 @@
             journalStorage,
             threadUtils,
             fakeClock,
-            new FeedStoreHelper());
+            new FeedStoreHelper(),
+            basicLoggingApi,
+            mainThreadRunner);
 
     CommitResult commitResult = store.editContent().add(CONTENT_ID, STREAM_PAYLOAD).commit();
     assertThat(commitResult).isEqualTo(CommitResult.SUCCESS);
@@ -146,7 +157,9 @@
             journalStorageSpy,
             threadUtils,
             fakeClock,
-            new FeedStoreHelper());
+            new FeedStoreHelper(),
+            basicLoggingApi,
+            mainThreadRunner);
 
     boolean commitResult =
         store
@@ -179,7 +192,9 @@
             journalStorageSpy,
             threadUtils,
             fakeClock,
-            new FeedStoreHelper());
+            new FeedStoreHelper(),
+            basicLoggingApi,
+            mainThreadRunner);
     doAnswer(ans -> Result.failure()).when(journalStorageSpy).getAllJournals();
 
     boolean clearSuccess = store.clearNonActionContent();
@@ -414,6 +429,41 @@
   }
 
   @Test
+  public void getPayloads_noContent() {
+    Result<List<PayloadWithId>> result =
+        getStore(mainThreadRunner).getPayloads(ImmutableList.of("foo"));
+    assertThat(result.isSuccessful()).isTrue();
+    assertThat(result.getValue()).isEmpty();
+    assertThat(basicLoggingApi.lastInternalError)
+        .isEqualTo(InternalFeedError.CONTENT_STORAGE_MISSING_ITEM);
+  }
+
+  @Test
+  public void getPayloads_withContent() {
+    StreamPayload streamPayload =
+        StreamPayload.newBuilder().setStreamFeature(StreamFeature.getDefaultInstance()).build();
+    contentStorage.commit(
+        new ContentMutation.Builder().upsert("foo", streamPayload.toByteArray()).build());
+
+    Result<List<PayloadWithId>> result =
+        getStore(mainThreadRunner).getPayloads(ImmutableList.of("foo"));
+    assertThat(result.isSuccessful()).isTrue();
+    assertThat(result.getValue()).hasSize(1);
+    assertThat(result.getValue().get(0).payload).isEqualTo(streamPayload);
+  }
+
+  @Test
+  public void getPayloads_cannotParse() {
+    contentStorage.commit(new ContentMutation.Builder().upsert("foo", new byte[] {5}).build());
+
+    Result<List<PayloadWithId>> result =
+        getStore(mainThreadRunner).getPayloads(ImmutableList.of("foo"));
+    assertThat(result.isSuccessful()).isTrue();
+    assertThat(result.getValue()).isEmpty();
+    assertThat(basicLoggingApi.lastInternalError).isEqualTo(InternalFeedError.ITEM_NOT_PARSED);
+  }
+
+  @Test
   public void getSharedStates_noContent() {
     Result<List<StreamSharedState>> result = getStore(mainThreadRunner).getSharedStates();
     assertThat(result.isSuccessful()).isTrue();