blob: 9a207725aee0abfd91894afdb617c7a34a51deee [file] [log] [blame]
// Copyright 2018 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.feedrequestmanager;
import android.content.Context;
import android.os.Build;
import android.util.DisplayMetrics;
import com.google.android.libraries.feed.api.host.config.ApplicationInfo;
import com.google.android.libraries.feed.api.host.config.Configuration;
import com.google.android.libraries.feed.api.host.config.Configuration.ConfigKey;
import com.google.android.libraries.feed.api.host.logging.BasicLoggingApi;
import com.google.android.libraries.feed.api.host.logging.RequestReason;
import com.google.android.libraries.feed.api.host.logging.Task;
import com.google.android.libraries.feed.api.host.network.HttpRequest;
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.scheduler.SchedulerApi;
import com.google.android.libraries.feed.api.host.stream.TooltipInfo.FeatureName;
import com.google.android.libraries.feed.api.host.stream.TooltipSupportedApi;
import com.google.android.libraries.feed.api.internal.actionmanager.ActionReader;
import com.google.android.libraries.feed.api.internal.common.DismissActionWithSemanticProperties;
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.FeedRequestManager;
import com.google.android.libraries.feed.api.internal.store.LocalActionMutation.ActionType;
import com.google.android.libraries.feed.common.Result;
import com.google.android.libraries.feed.common.concurrent.MainThreadRunner;
import com.google.android.libraries.feed.common.concurrent.TaskQueue;
import com.google.android.libraries.feed.common.concurrent.TaskQueue.TaskType;
import com.google.android.libraries.feed.common.functional.Consumer;
import com.google.android.libraries.feed.common.locale.LocaleUtils;
import com.google.android.libraries.feed.common.logging.Logger;
import com.google.android.libraries.feed.common.protoextensions.FeedExtensionRegistry;
import com.google.android.libraries.feed.common.time.TimingUtils;
import com.google.android.libraries.feed.common.time.TimingUtils.ElapsedTimeTracker;
import com.google.android.libraries.feed.feedrequestmanager.internal.Utils;
import com.google.protobuf.ByteString;
import com.google.search.now.feed.client.StreamDataProto.StreamDataOperation;
import com.google.search.now.feed.client.StreamDataProto.StreamToken;
import com.google.search.now.wire.feed.ActionTypeProto;
import com.google.search.now.wire.feed.CapabilityProto.Capability;
import com.google.search.now.wire.feed.ClientInfoProto.ClientInfo;
import com.google.search.now.wire.feed.ClientInfoProto.ClientInfo.PlatformType;
import com.google.search.now.wire.feed.ConsistencyTokenProto.ConsistencyToken;
import com.google.search.now.wire.feed.ContentIdProto.ContentId;
import com.google.search.now.wire.feed.DisplayInfoProto.DisplayInfo;
import com.google.search.now.wire.feed.FeedActionQueryDataProto.Action;
import com.google.search.now.wire.feed.FeedActionQueryDataProto.FeedActionQueryData;
import com.google.search.now.wire.feed.FeedActionQueryDataProto.FeedActionQueryDataItem;
import com.google.search.now.wire.feed.FeedQueryProto.FeedQuery;
import com.google.search.now.wire.feed.FeedRequestProto.FeedRequest;
import com.google.search.now.wire.feed.FeedRequestProto.FeedRequest.Builder;
import com.google.search.now.wire.feed.RequestProto.Request;
import com.google.search.now.wire.feed.RequestProto.Request.RequestVersion;
import com.google.search.now.wire.feed.ResponseProto.Response;
import com.google.search.now.wire.feed.SemanticPropertiesProto.SemanticProperties;
import com.google.search.now.wire.feed.VersionProto.Version;
import com.google.search.now.wire.feed.VersionProto.Version.Architecture;
import com.google.search.now.wire.feed.VersionProto.Version.BuildType;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/** Default implementation of FeedRequestManager. */
public final class FeedRequestManagerImpl implements FeedRequestManager {
private static final String TAG = "FeedRequestManagerImpl";
private final Configuration configuration;
private final NetworkClient networkClient;
private final ProtocolAdapter protocolAdapter;
private final FeedExtensionRegistry extensionRegistry;
private final SchedulerApi scheduler;
private final TaskQueue taskQueue;
private final TimingUtils timingUtils;
private final ThreadUtils threadUtils;
private final ActionReader actionReader;
private final Context context;
private final MainThreadRunner mainThreadRunner;
private final BasicLoggingApi basicLoggingApi;
private final TooltipSupportedApi tooltipSupportedApi;
private final ApplicationInfo applicationInfo;
public FeedRequestManagerImpl(
Configuration configuration,
NetworkClient networkClient,
ProtocolAdapter protocolAdapter,
FeedExtensionRegistry extensionRegistry,
SchedulerApi scheduler,
TaskQueue taskQueue,
TimingUtils timingUtils,
ThreadUtils threadUtils,
ActionReader actionReader,
Context context,
ApplicationInfo applicationInfo,
MainThreadRunner mainThreadRunner,
BasicLoggingApi basicLoggingApi,
TooltipSupportedApi tooltipSupportedApi) {
this.configuration = configuration;
this.networkClient = networkClient;
this.protocolAdapter = protocolAdapter;
this.extensionRegistry = extensionRegistry;
this.scheduler = scheduler;
this.taskQueue = taskQueue;
this.timingUtils = timingUtils;
this.threadUtils = threadUtils;
this.actionReader = actionReader;
this.context = context;
this.applicationInfo = applicationInfo;
this.mainThreadRunner = mainThreadRunner;
this.basicLoggingApi = basicLoggingApi;
this.tooltipSupportedApi = tooltipSupportedApi;
}
@Override
public void loadMore(
StreamToken streamToken,
ConsistencyToken token,
Consumer<Result<List<StreamDataOperation>>> consumer) {
threadUtils.checkNotMainThread();
Logger.i(TAG, "Task: FeedRequestManagerImpl LoadMore");
ElapsedTimeTracker timeTracker = timingUtils.getElapsedTimeTracker(TAG);
RequestBuilder request =
newDefaultRequest(RequestReason.MANUAL_CONTINUATION)
.setPageToken(streamToken.getNextPageToken())
.setConsistencyToken(token);
executeRequest(request, consumer);
timeTracker.stop(
"task", "FeedRequestManagerImpl LoadMore", "token", streamToken.getNextPageToken());
}
@Override
public void triggerRefresh(
@RequestReason int reason, Consumer<Result<List<StreamDataOperation>>> consumer) {
triggerRefresh(reason, ConsistencyToken.getDefaultInstance(), consumer);
}
@Override
public void triggerRefresh(
@RequestReason int reason,
ConsistencyToken token,
Consumer<Result<List<StreamDataOperation>>> consumer) {
Logger.i(TAG, "trigger refresh %s", reason);
RequestBuilder request = newDefaultRequest(reason).setConsistencyToken(token);
if (threadUtils.isMainThread()) {
// This will make a new request, it should invalidate the existing head to delay everything
// until the response is obtained.
taskQueue.execute(
Task.REQUEST_MANAGER_TRIGGER_REFRESH,
TaskType.HEAD_INVALIDATE,
() -> executeRequest(request, consumer));
} else {
executeRequest(request, consumer);
}
}
private RequestBuilder newDefaultRequest(@RequestReason int requestReason) {
return new RequestBuilder(context, applicationInfo, configuration, requestReason);
}
private static FeedQuery.RequestReason getWireRequestReason(@RequestReason int requestReason) {
switch (requestReason) {
case RequestReason.ZERO_STATE:
return FeedQuery.RequestReason.ZERO_STATE_REFRESH;
case RequestReason.HOST_REQUESTED:
return FeedQuery.RequestReason.SCHEDULED_REFRESH;
case RequestReason.OPEN_WITH_CONTENT:
return FeedQuery.RequestReason.WITH_CONTENT;
// TODO: distinguish between automatic and manual continuation for wire
// protocol
case RequestReason.MANUAL_CONTINUATION:
case RequestReason.AUTOMATIC_CONTINUATION:
return FeedQuery.RequestReason.NEXT_PAGE_SCROLL;
case RequestReason.OPEN_WITHOUT_CONTENT:
return FeedQuery.RequestReason.INITIAL_LOAD;
case RequestReason.CLEAR_ALL:
return FeedQuery.RequestReason.CLEAR_ALL;
case RequestReason.UNKNOWN:
return FeedQuery.RequestReason.UNKNOWN_REQUEST_REASON;
default:
Logger.wtf(TAG, "Cannot map request reason with value %d", requestReason);
return FeedQuery.RequestReason.UNKNOWN_REQUEST_REASON;
}
}
private void executeRequest(
RequestBuilder requestBuilder, Consumer<Result<List<StreamDataOperation>>> consumer) {
threadUtils.checkNotMainThread();
Result<List<DismissActionWithSemanticProperties>> dismissActionsResult =
actionReader.getDismissActionsWithSemanticProperties();
if (dismissActionsResult.isSuccessful()) {
requestBuilder.setActions(dismissActionsResult.getValue());
Logger.i(TAG, "Dismiss actions in request: %s", dismissActionsResult.getValue());
} else {
Logger.e(TAG, "Error fetching dismiss actions");
}
if (configuration.getValueOrDefault(ConfigKey.CARD_MENU_TOOLTIP_ELIGIBLE, false)) {
// We need use the main thread to call the {@link TooltipSupportedApi#wouldTriggerHelpUi}.
mainThreadRunner.execute(
"Check Tooltips",
() -> {
tooltipSupportedApi.wouldTriggerHelpUi(
FeatureName.CARD_MENU_TOOLTIP,
(wouldTrigger) -> {
taskQueue.execute(
Task.SEND_REQUEST,
TaskType.IMMEDIATE,
() -> {
requestBuilder.setCardMenuTooltipWouldTrigger(wouldTrigger);
sendRequest(requestBuilder, consumer);
});
});
});
} else {
sendRequest(requestBuilder, consumer);
}
}
private void sendRequest(
RequestBuilder requestBuilder, Consumer<Result<List<StreamDataOperation>>> consumer) {
threadUtils.checkNotMainThread();
String endpoint = configuration.getValueOrDefault(ConfigKey.FEED_SERVER_ENDPOINT, "");
@HttpMethod
String httpMethod =
configuration.getValueOrDefault(ConfigKey.FEED_SERVER_METHOD, HttpMethod.GET);
HttpRequest httpRequest =
RequestHelper.buildHttpRequest(
httpMethod,
requestBuilder.build().toByteArray(),
endpoint,
LocaleUtils.getLanguageTag(context));
Logger.i(TAG, "Making Request: %s", httpRequest.getUri().getPath());
networkClient.send(
httpRequest,
input -> {
basicLoggingApi.onServerRequest(requestBuilder.clientLoggingRequestReason);
Logger.i(
TAG,
"Request: %s completed with response code: %s",
httpRequest.getUri().getPath(),
input.getResponseCode());
if (input.getResponseCode() != 200) {
String errorBody = null;
try {
errorBody = new String(input.getResponseBody(), "UTF-8");
} catch (UnsupportedEncodingException e) {
Logger.e(TAG, "Error handling http error logging", e);
}
Logger.e(TAG, "errorCode: %d", input.getResponseCode());
Logger.e(TAG, "errorResponse: %s", errorBody);
if (!requestBuilder.hasPageToken()) {
scheduler.onRequestError(input.getResponseCode());
}
consumer.accept(Result.failure());
return;
}
handleResponseBytes(input.getResponseBody(), consumer);
});
}
private void handleResponseBytes(
final byte[] responseBytes, final Consumer<Result<List<StreamDataOperation>>> consumer) {
taskQueue.execute(
Task.HANDLE_RESPONSE_BYTES,
TaskType.IMMEDIATE,
() -> {
Response response;
boolean isLengthPrefixed =
configuration.getValueOrDefault(ConfigKey.FEED_SERVER_RESPONSE_LENGTH_PREFIXED, true);
try {
response =
Response.parseFrom(
isLengthPrefixed
? RequestHelper.getLengthPrefixedValue(responseBytes)
: responseBytes,
extensionRegistry.getExtensionRegistry());
} catch (IOException e) {
Logger.e(TAG, e, "Response parse failed");
consumer.accept(Result.failure());
return;
}
Result<List<StreamDataOperation>> result = protocolAdapter.createModel(response);
mainThreadRunner.execute(
"FeedRequestManagerImpl consumer", () -> consumer.accept(result));
});
}
private static final class RequestBuilder {
private ByteString token;
private ConsistencyToken consistencyToken;
private List<DismissActionWithSemanticProperties> dismissActionsWithSemanticProperties;
private final Context context;
private final ApplicationInfo applicationInfo;
private final Configuration configuration;
private final FeedQuery.RequestReason requestReason;
@RequestReason private final int clientLoggingRequestReason;
private boolean cardMenuTooltipWouldTrigger;
RequestBuilder(
Context context,
ApplicationInfo applicationInfo,
Configuration configuration,
@RequestReason int requestReason) {
this.context = context;
this.applicationInfo = applicationInfo;
this.configuration = configuration;
this.clientLoggingRequestReason = requestReason;
this.requestReason = getWireRequestReason(requestReason);
}
/**
* Sets the token used to tell the server which page of results we want in the response.
*
* @param token the token copied from FeedResponse.next_page_token.
*/
RequestBuilder setPageToken(ByteString token) {
this.token = token;
return this;
}
boolean hasPageToken() {
return token != null;
}
/**
* Sets the token used to tell the server which storage version to read/write to.
*
* @param token the token used to maintain read/write consistency.
*/
RequestBuilder setConsistencyToken(ConsistencyToken token) {
this.consistencyToken = token;
return this;
}
RequestBuilder setActions(
List<DismissActionWithSemanticProperties> dismissActionsWithSemanticProperties) {
this.dismissActionsWithSemanticProperties = dismissActionsWithSemanticProperties;
return this;
}
RequestBuilder setCardMenuTooltipWouldTrigger(boolean wouldTrigger) {
cardMenuTooltipWouldTrigger = wouldTrigger;
return this;
}
public Request build() {
Request.Builder requestBuilder =
Request.newBuilder().setRequestVersion(RequestVersion.FEED_QUERY);
FeedQuery.Builder feedQuery = FeedQuery.newBuilder().setReason(requestReason);
if (token != null) {
feedQuery.setPageToken(token);
}
Builder feedRequestBuilder = FeedRequest.newBuilder().setFeedQuery(feedQuery);
if (consistencyToken != null) {
feedRequestBuilder.setConsistencyToken(consistencyToken);
}
if (dismissActionsWithSemanticProperties != null
&& !dismissActionsWithSemanticProperties.isEmpty()) {
feedRequestBuilder.addFeedActionQueryData(buildFeedActionQueryData());
}
feedRequestBuilder.setClientInfo(buildClientInfo());
addCapabilities(feedRequestBuilder);
requestBuilder.setExtension(FeedRequest.feedRequest, feedRequestBuilder.build());
return requestBuilder.build();
}
private void addCapabilities(Builder feedRequestBuilder) {
addCapabilityIfConfigEnabled(
feedRequestBuilder, ConfigKey.FEED_UI_ENABLED, Capability.FEED_UI);
addCapabilityIfConfigEnabled(
feedRequestBuilder, ConfigKey.UNDOABLE_ACTIONS_ENABLED, Capability.UNDOABLE_ACTIONS);
addCapabilityIfConfigEnabled(
feedRequestBuilder, ConfigKey.MANAGE_INTERESTS_ENABLED, Capability.MANAGE_INTERESTS);
addCapabilityIfConfigEnabled(
feedRequestBuilder, ConfigKey.ENABLE_CAROUSELS, Capability.CAROUSELS);
if (cardMenuTooltipWouldTrigger) {
addCapabilityIfConfigEnabled(
feedRequestBuilder, ConfigKey.CARD_MENU_TOOLTIP_ELIGIBLE, Capability.CARD_MENU_TOOLTIP);
}
addCapabilityIfConfigEnabled(
feedRequestBuilder, ConfigKey.SNIPPETS_ENABLED, Capability.ARTICLE_SNIPPETS);
addCapabilityIfConfigEnabled(
feedRequestBuilder,
ConfigKey.USE_SECONDARY_PAGE_REQUEST,
Capability.USE_SECONDARY_PAGE_REQUEST);
feedRequestBuilder.addClientCapability(Capability.BASE_UI);
Logger.i(TAG, "Capability List: %s", feedRequestBuilder.getClientCapabilityList());
}
private void addCapabilityIfConfigEnabled(
Builder feedRequestBuilder, String configKey, Capability capability) {
if (configuration.getValueOrDefault(configKey, false)) {
feedRequestBuilder.addClientCapability(capability);
}
}
private FeedActionQueryData buildFeedActionQueryData() {
Map<Long, Integer> ids = new LinkedHashMap<>(dismissActionsWithSemanticProperties.size());
Map<String, Integer> tables =
new LinkedHashMap<>(dismissActionsWithSemanticProperties.size());
Map<String, Integer> contentDomains =
new LinkedHashMap<>(dismissActionsWithSemanticProperties.size());
Map<SemanticProperties, Integer> semanticProperties =
new LinkedHashMap<>(dismissActionsWithSemanticProperties.size());
ArrayList<FeedActionQueryDataItem> actionDataItems =
new ArrayList<>(dismissActionsWithSemanticProperties.size());
for (DismissActionWithSemanticProperties action : dismissActionsWithSemanticProperties) {
ContentId contentId = action.getContentId();
byte /*@Nullable*/ [] semanticPropertiesBytes = action.getSemanticProperties();
FeedActionQueryDataItem.Builder actionDataItemBuilder =
FeedActionQueryDataItem.newBuilder();
actionDataItemBuilder.setIdIndex(getIndexForItem(ids, contentId.getId()));
actionDataItemBuilder.setTableIndex(getIndexForItem(tables, contentId.getTable()));
actionDataItemBuilder.setContentDomainIndex(
getIndexForItem(contentDomains, contentId.getContentDomain()));
if (semanticPropertiesBytes != null) {
actionDataItemBuilder.setSemanticPropertiesIndex(
getIndexForItem(
semanticProperties,
SemanticProperties.newBuilder()
.setSemanticPropertiesData(ByteString.copyFrom(semanticPropertiesBytes))
.build()));
}
actionDataItems.add(actionDataItemBuilder.build());
}
return FeedActionQueryData.newBuilder()
.setAction(
Action.newBuilder()
.setActionType(ActionTypeProto.ActionType.forNumber(ActionType.DISMISS)))
.addAllUniqueId(ids.keySet())
.addAllUniqueTable(tables.keySet())
.addAllUniqueContentDomain(contentDomains.keySet())
.addAllUniqueSemanticProperties(semanticProperties.keySet())
.addAllFeedActionQueryDataItem(actionDataItems)
.build();
}
private static <T> int getIndexForItem(Map<T, Integer> objectMap, T object) {
if (!objectMap.containsKey(object)) {
int newIndex = objectMap.size();
objectMap.put(object, newIndex);
return newIndex;
} else {
return objectMap.get(object);
}
}
private ClientInfo buildClientInfo() {
ClientInfo.Builder clientInfoBuilder = ClientInfo.newBuilder();
clientInfoBuilder.setPlatformType(PlatformType.ANDROID);
clientInfoBuilder.setPlatformVersion(getPlatformVersion());
clientInfoBuilder.setLocale(LocaleUtils.getLanguageTag(context));
clientInfoBuilder.setAppType(Utils.convertAppType(applicationInfo.getAppType()));
clientInfoBuilder.setAppVersion(getAppVersion());
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
clientInfoBuilder.addDisplayInfo(
DisplayInfo.newBuilder()
.setScreenDensity(metrics.density)
.setScreenWidthInPixels(metrics.widthPixels)
.setScreenHeightInPixels(metrics.heightPixels));
return clientInfoBuilder.build();
}
private static Version getPlatformVersion() {
Version.Builder builder = Version.newBuilder();
Utils.fillVersionsFromString(builder, Build.VERSION.RELEASE);
builder.setArchitecture(getPlatformArchitecture());
builder.setBuildType(getPlatformBuildType());
builder.setApiVersion(Build.VERSION.SDK_INT);
return builder.build();
}
private static Architecture getPlatformArchitecture() {
String androidAbi =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
? Build.SUPPORTED_ABIS[0]
: Build.CPU_ABI;
return Utils.convertArchitectureString(androidAbi);
}
private static BuildType getPlatformBuildType() {
if (Build.TAGS != null) {
if (Build.TAGS.contains("dev-keys") || Build.TAGS.contains("test-keys")) {
return BuildType.DEV;
} else if (Build.TAGS.contains("release-keys")) {
return BuildType.RELEASE;
}
}
return BuildType.UNKNOWN_BUILD_TYPE;
}
private Version getAppVersion() {
Version.Builder builder = Version.newBuilder();
Utils.fillVersionsFromString(builder, applicationInfo.getVersionString());
builder.setArchitecture(Utils.convertArchitecture(applicationInfo.getArchitecture()));
builder.setBuildType(Utils.convertBuildType(applicationInfo.getBuildType()));
return builder.build();
}
}
}