Round-trip opaque SemanticProperties

PiperOrigin-RevId: 264857131
Change-Id: I466ba612960860a4547593b987d78d4499728502
diff --git a/src/main/java/com/google/android/libraries/feed/feedrequestmanager/FeedActionUploadRequestManager.java b/src/main/java/com/google/android/libraries/feed/feedrequestmanager/FeedActionUploadRequestManager.java
index 0a035fe..db83445 100644
--- a/src/main/java/com/google/android/libraries/feed/feedrequestmanager/FeedActionUploadRequestManager.java
+++ b/src/main/java/com/google/android/libraries/feed/feedrequestmanager/FeedActionUploadRequestManager.java
@@ -21,6 +21,7 @@
 import com.google.android.libraries.feed.api.host.network.HttpRequest.HttpMethod;
 import com.google.android.libraries.feed.api.host.network.NetworkClient;
 import com.google.android.libraries.feed.api.host.storage.CommitResult;
+import com.google.android.libraries.feed.api.internal.common.SemanticPropertiesWithId;
 import com.google.android.libraries.feed.api.internal.common.ThreadUtils;
 import com.google.android.libraries.feed.api.internal.protocoladapter.ProtocolAdapter;
 import com.google.android.libraries.feed.api.internal.requestmanager.ActionUploadRequestManager;
@@ -39,7 +40,9 @@
 import com.google.search.now.wire.feed.ResponseProto.Response;
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
@@ -117,15 +120,25 @@
         new UploadableActionsRequestBuilder(protocolAdapter);
     int actionPayloadBytes = 0;
     Set<StreamUploadableAction> actionsToUpload = new HashSet<>();
+    ArrayList<String> contentIds = new ArrayList<>();
     for (StreamUploadableAction action : actions) {
       int actionBytes = action.toByteArray().length;
       if (maxBytesPerRequest > actionPayloadBytes + actionBytes) {
         actionsToUpload.add(action);
+        contentIds.add(action.getFeatureContentId());
         actionPayloadBytes += actionBytes;
       } else {
         break;
       }
     }
+
+    Result<List<SemanticPropertiesWithId>> semanticPropertiesResult =
+        store.getSemanticProperties(contentIds);
+    List<SemanticPropertiesWithId> semanticPropertiesList = new ArrayList<>();
+    if (semanticPropertiesResult.isSuccessful() && !semanticPropertiesResult.getValue().isEmpty()) {
+      semanticPropertiesList = semanticPropertiesResult.getValue();
+    }
+
     Consumer<Result<ConsistencyToken>> tokenConsumer =
         result -> {
           threadUtils.checkNotMainThread();
@@ -141,7 +154,10 @@
             consumer.accept(uploadCount == 0 ? Result.failure() : Result.success(token));
           }
         };
-    requestBuilder.setConsistencyToken(token).setActions(actionsToUpload);
+    requestBuilder
+        .setConsistencyToken(token)
+        .setActions(actionsToUpload)
+        .setSemanticProperties(semanticPropertiesList);
     executeUploadActionRequest(actionsToUpload, requestBuilder, tokenConsumer);
   }
 
diff --git a/src/main/java/com/google/android/libraries/feed/feedrequestmanager/UploadableActionsRequestBuilder.java b/src/main/java/com/google/android/libraries/feed/feedrequestmanager/UploadableActionsRequestBuilder.java
index cb46e29..17adc17 100644
--- a/src/main/java/com/google/android/libraries/feed/feedrequestmanager/UploadableActionsRequestBuilder.java
+++ b/src/main/java/com/google/android/libraries/feed/feedrequestmanager/UploadableActionsRequestBuilder.java
@@ -14,8 +14,10 @@
 
 package com.google.android.libraries.feed.feedrequestmanager;
 
+import com.google.android.libraries.feed.api.internal.common.SemanticPropertiesWithId;
 import com.google.android.libraries.feed.api.internal.protocoladapter.ProtocolAdapter;
 import com.google.android.libraries.feed.common.Result;
+import com.google.protobuf.ByteString;
 import com.google.search.now.feed.client.StreamDataProto.StreamUploadableAction;
 import com.google.search.now.wire.feed.ActionPayloadProto.ActionPayload;
 import com.google.search.now.wire.feed.ActionRequestProto.ActionRequest;
@@ -23,6 +25,9 @@
 import com.google.search.now.wire.feed.ContentIdProto.ContentId;
 import com.google.search.now.wire.feed.FeedActionProto.FeedAction;
 import com.google.search.now.wire.feed.FeedActionRequestProto.FeedActionRequest;
+import com.google.search.now.wire.feed.SemanticPropertiesProto.SemanticProperties;
+import java.util.HashMap;
+import java.util.List;
 import java.util.Set;
 
 // A class that creates an ActionsRequest for uploading actions
@@ -30,6 +35,7 @@
   private Set<StreamUploadableAction> uploadableActions;
   private ConsistencyToken token;
   private final ProtocolAdapter protocolAdapter;
+  private final HashMap<String, byte[]> semanticPropertiesMap = new HashMap<>();
 
   UploadableActionsRequestBuilder(ProtocolAdapter protocolAdapter) {
     this.protocolAdapter = protocolAdapter;
@@ -49,6 +55,14 @@
     return this;
   }
 
+  UploadableActionsRequestBuilder setSemanticProperties(
+      List<SemanticPropertiesWithId> semanticPropertiesList) {
+    for (SemanticPropertiesWithId semanticProperties : semanticPropertiesList) {
+      semanticPropertiesMap.put(semanticProperties.contentId, semanticProperties.semanticData);
+    }
+    return this;
+  }
+
   public ActionRequest build() {
     ActionRequest.Builder requestBuilder =
         ActionRequest.newBuilder()
@@ -65,6 +79,12 @@
                     FeedAction.ClientData.newBuilder()
                         .setTimestampSeconds(action.getTimestampSeconds())
                         .build());
+        if (semanticPropertiesMap.containsKey(contentId)) {
+          feedAction.setSemanticProperties(
+              SemanticProperties.newBuilder()
+                  .setSemanticPropertiesData(
+                      ByteString.copyFrom(semanticPropertiesMap.get(contentId))));
+        }
         Result<ContentId> contentIdResult = protocolAdapter.getWireContentId(contentId);
         if (contentIdResult.isSuccessful()) {
           feedAction.setContentId(contentIdResult.getValue());
diff --git a/src/main/java/com/google/android/libraries/feed/testing/store/BUILD b/src/main/java/com/google/android/libraries/feed/testing/store/BUILD
index 2db5f8e..ab08ebe 100644
--- a/src/main/java/com/google/android/libraries/feed/testing/store/BUILD
+++ b/src/main/java/com/google/android/libraries/feed/testing/store/BUILD
@@ -20,5 +20,6 @@
         "//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",
+        "//src/main/proto/search/now/wire/feed:feed_java_proto_lite",
     ],
 )
diff --git a/src/main/java/com/google/android/libraries/feed/testing/store/FakeStore.java b/src/main/java/com/google/android/libraries/feed/testing/store/FakeStore.java
index 4029786..8abe6fe 100644
--- a/src/main/java/com/google/android/libraries/feed/testing/store/FakeStore.java
+++ b/src/main/java/com/google/android/libraries/feed/testing/store/FakeStore.java
@@ -18,6 +18,7 @@
 import com.google.android.libraries.feed.api.host.storage.CommitResult;
 import com.google.android.libraries.feed.api.internal.common.PayloadWithId;
 import com.google.android.libraries.feed.api.internal.store.ContentMutation;
+import com.google.android.libraries.feed.api.internal.store.SemanticPropertiesMutation;
 import com.google.android.libraries.feed.api.internal.store.SessionMutation;
 import com.google.android.libraries.feed.api.internal.store.UploadableActionMutation;
 import com.google.android.libraries.feed.common.Result;
@@ -35,6 +36,7 @@
 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;
+import com.google.search.now.wire.feed.SemanticPropertiesProto.SemanticProperties;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -236,6 +238,16 @@
     return this;
   }
 
+  /** Adds the {@code actions} to the store. */
+  public FakeStore addSemanticProperties(String contentId, SemanticProperties properties) {
+    boolean policy = fakeThreadUtils.enforceMainThread(false);
+    SemanticPropertiesMutation mutation = editSemanticProperties();
+    mutation.add(contentId, properties.getSemanticPropertiesData());
+    mutation.commit();
+    fakeThreadUtils.enforceMainThread(policy);
+    return this;
+  }
+
   /** Gets all content associated with the {@code contentId}. */
   public List<Object> getContentById(String contentId) {
     boolean policy = fakeThreadUtils.enforceMainThread(false);
diff --git a/src/main/proto/search/now/wire/feed/feed_action.proto b/src/main/proto/search/now/wire/feed/feed_action.proto
index 406dbee..51258dd 100644
--- a/src/main/proto/search/now/wire/feed/feed_action.proto
+++ b/src/main/proto/search/now/wire/feed/feed_action.proto
@@ -20,6 +20,7 @@
 
 import "src/main/proto/search/now/wire/feed/action_payload.proto";
 import "src/main/proto/search/now/wire/feed/content_id.proto";
+import "src/main/proto/search/now/wire/feed/semantic_properties.proto";
 
 option java_package = "com.google.search.now.wire.feed";
 option java_outer_classname = "FeedActionProto";
@@ -31,7 +32,10 @@
   optional ActionPayload action_payload = 4;
   // Client-generated data that pertains to the action.
   optional ClientData client_data = 5;
-  // Next Id: 6
+  // Fields beyond ID needed to determine uniqueness for the card or collection.
+  // Opaque to the client, round-tripped from the server.
+  optional SemanticProperties semantic_properties = 6;
+  // Next Id: 7
   reserved 2, 3;
 
   // The data the client provides to the server.
diff --git a/src/test/java/com/google/android/libraries/feed/feedrequestmanager/BUILD b/src/test/java/com/google/android/libraries/feed/feedrequestmanager/BUILD
index e4a7aa4..3c22693 100644
--- a/src/test/java/com/google/android/libraries/feed/feedrequestmanager/BUILD
+++ b/src/test/java/com/google/android/libraries/feed/feedrequestmanager/BUILD
@@ -97,6 +97,7 @@
     srcs = ["UploadableActionsRequestBuilderTest.java"],
     manifest_values = DEFAULT_ANDROID_LOCAL_TEST_MANIFEST,
     deps = [
+        "//src/main/java/com/google/android/libraries/feed/api/internal/common",
         "//src/main/java/com/google/android/libraries/feed/feedrequestmanager",
         "//src/main/java/com/google/android/libraries/feed/testing/protocoladapter",
         "//src/main/proto/com/google/android/libraries/feed/api/internal/proto:client_feed_java_proto_lite",
diff --git a/src/test/java/com/google/android/libraries/feed/feedrequestmanager/FeedActionUploadRequestManagerTest.java b/src/test/java/com/google/android/libraries/feed/feedrequestmanager/FeedActionUploadRequestManagerTest.java
index 744062e..63cb1e0 100644
--- a/src/test/java/com/google/android/libraries/feed/feedrequestmanager/FeedActionUploadRequestManagerTest.java
+++ b/src/test/java/com/google/android/libraries/feed/feedrequestmanager/FeedActionUploadRequestManagerTest.java
@@ -40,9 +40,11 @@
 import com.google.search.now.feed.client.StreamDataProto.StreamUploadableAction;
 import com.google.search.now.wire.feed.ActionRequestProto.ActionRequest;
 import com.google.search.now.wire.feed.ConsistencyTokenProto.ConsistencyToken;
+import com.google.search.now.wire.feed.FeedActionRequestProto.FeedActionRequest;
 import com.google.search.now.wire.feed.FeedActionResponseProto.FeedActionResponse;
 import com.google.search.now.wire.feed.FeedRequestProto.FeedRequest;
 import com.google.search.now.wire.feed.ResponseProto.Response;
+import com.google.search.now.wire.feed.SemanticPropertiesProto.SemanticProperties;
 import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
@@ -68,6 +70,11 @@
       ConsistencyToken.newBuilder().setToken(ByteString.copyFrom(new byte[] {0x2, 0xa})).build();
   private static final String CONTENT_ID = "contentId";
   private static final String CONTENT_ID_2 = "contentId2";
+  private static final byte[] SEMANTIC_PROPERTIES_BYTES = new byte[] {0x1, 0xa};
+  private static final SemanticProperties SEMANTIC_PROPERTIES =
+      SemanticProperties.newBuilder()
+          .setSemanticPropertiesData(ByteString.copyFrom(SEMANTIC_PROPERTIES_BYTES))
+          .build();
   private static final Response RESPONSE_1 =
       Response.newBuilder()
           .setExtension(
@@ -98,6 +105,7 @@
     initMocks(this);
     registry = ExtensionRegistryLite.newInstance();
     registry.add(FeedRequest.feedRequest);
+    registry.add(FeedActionRequest.feedActionRequest);
     fakeThreadUtils = FakeThreadUtils.withThreadChecks();
     fakeNetworkClient = new FakeNetworkClient(fakeThreadUtils);
     fakeTaskQueue = new FakeTaskQueue(fakeClock, fakeThreadUtils);
@@ -343,6 +351,28 @@
     assertThat(consumer.isCalled()).isTrue();
   }
 
+  @Test
+  public void testTriggerUploadActions_withSemanticProperties() throws Exception {
+    fakeStore.addSemanticProperties(CONTENT_ID, SEMANTIC_PROPERTIES);
+    Set<StreamUploadableAction> actionSet =
+        setOf(StreamUploadableAction.newBuilder().setFeatureContentId(CONTENT_ID).build());
+    fakeNetworkClient.addResponse(
+        createHttpResponse(/* responseCode= */ 200, Response.getDefaultInstance()));
+    requestManager.triggerUploadActions(actionSet, ConsistencyToken.getDefaultInstance(), consumer);
+
+    HttpRequest httpRequest = fakeNetworkClient.getLatestRequest();
+
+    ActionRequest request = getActionRequestFromHttpRequestBody(httpRequest);
+    assertThat(
+            request
+                .getExtension(FeedActionRequest.feedActionRequest)
+                .getFeedActionList()
+                .get(0)
+                .getSemanticProperties()
+                .getSemanticPropertiesData())
+        .isEqualTo(SEMANTIC_PROPERTIES.getSemanticPropertiesData());
+  }
+
   private static void assertHttpRequestFormattedCorrectly(HttpRequest httpRequest) {
     assertThat(httpRequest.getBody()).hasLength(0);
     assertThat(httpRequest.getMethod()).isEqualTo(HttpMethod.GET);
diff --git a/src/test/java/com/google/android/libraries/feed/feedrequestmanager/UploadableActionsRequestBuilderTest.java b/src/test/java/com/google/android/libraries/feed/feedrequestmanager/UploadableActionsRequestBuilderTest.java
index d7cac77..387d78f 100644
--- a/src/test/java/com/google/android/libraries/feed/feedrequestmanager/UploadableActionsRequestBuilderTest.java
+++ b/src/test/java/com/google/android/libraries/feed/feedrequestmanager/UploadableActionsRequestBuilderTest.java
@@ -17,6 +17,7 @@
 import static com.google.common.truth.Truth.assertThat;
 import static org.mockito.MockitoAnnotations.initMocks;
 
+import com.google.android.libraries.feed.api.internal.common.SemanticPropertiesWithId;
 import com.google.android.libraries.feed.testing.protocoladapter.FakeProtocolAdapter;
 import com.google.protobuf.ByteString;
 import com.google.search.now.feed.client.StreamDataProto.StreamUploadableAction;
@@ -27,6 +28,8 @@
 import com.google.search.now.wire.feed.ContentIdProto.ContentId;
 import com.google.search.now.wire.feed.FeedActionProto.FeedAction;
 import com.google.search.now.wire.feed.FeedActionRequestProto.FeedActionRequest;
+import com.google.search.now.wire.feed.SemanticPropertiesProto.SemanticProperties;
+import java.util.Arrays;
 import java.util.HashSet;
 import org.junit.Before;
 import org.junit.Test;
@@ -38,6 +41,13 @@
 public class UploadableActionsRequestBuilderTest {
   private static final String CONTENT_ID = "contentId";
   private static final int TIME = 100;
+  private static final byte[] SEMANTIC_PROPERTIES_BYTES = new byte[] {0x1, 0xf};
+  private static final SemanticProperties SEMANTIC_PROPERTIES =
+      SemanticProperties.newBuilder()
+          .setSemanticPropertiesData(ByteString.copyFrom(SEMANTIC_PROPERTIES_BYTES))
+          .build();
+  private static final SemanticPropertiesWithId SEMANTIC_PROPERTIES_WITH_ID =
+      new SemanticPropertiesWithId(CONTENT_ID, SEMANTIC_PROPERTIES_BYTES);
   private final ActionPayload payload =
       ActionPayload.newBuilder()
           .setExtension(
@@ -104,6 +114,7 @@
     FeedAction feedAction =
         FeedAction.newBuilder()
             .setContentId(ContentId.getDefaultInstance())
+            .setSemanticProperties(SEMANTIC_PROPERTIES)
             .setActionPayload(payload)
             .setClientData(FeedAction.ClientData.newBuilder().setTimestampSeconds(TIME).build())
             .build();
@@ -113,7 +124,12 @@
         FeedActionRequest.feedActionRequest, feedActionRequestBuilder.build());
 
     ActionRequest expectedResult = requestBuilder.build();
-    ActionRequest result = builder.setActions(actionSet).setConsistencyToken(token).build();
+    ActionRequest result =
+        builder
+            .setActions(actionSet)
+            .setConsistencyToken(token)
+            .setSemanticProperties(Arrays.asList(SEMANTIC_PROPERTIES_WITH_ID))
+            .build();
     assertThat(result).isEqualTo(expectedResult);
   }
 }