blob: 9cb0c62995a7977989f23d08649835816fdda9af [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.feedsessionmanager.internal;
import android.support.annotation.VisibleForTesting;
import com.google.android.libraries.feed.api.common.MutationContext;
import com.google.android.libraries.feed.api.internal.common.ThreadUtils;
import com.google.android.libraries.feed.api.internal.modelprovider.ModelChild;
import com.google.android.libraries.feed.api.internal.modelprovider.ModelChild.Type;
import com.google.android.libraries.feed.api.internal.modelprovider.ModelProvider;
import com.google.android.libraries.feed.api.internal.store.Store;
import com.google.android.libraries.feed.common.Validators;
import com.google.android.libraries.feed.common.concurrent.TaskQueue;
import com.google.android.libraries.feed.common.logging.Logger;
import com.google.android.libraries.feed.common.time.TimingUtils;
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;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/** SessionImpl which implements the TimeoutScheduler specific behaviors need within the session. */
public final class TimeoutSessionImpl extends SessionImpl {
private static final String TAG = "TimeoutSessionImpl";
TimeoutSessionImpl(
Store store,
boolean limitPagingUpdates,
TaskQueue taskQueue,
TimingUtils timingUtils,
ThreadUtils threadUtils) {
super(store, limitPagingUpdates, taskQueue, timingUtils, threadUtils);
Logger.i(TAG, "Using TimeoutSessionImpl");
}
@Override
public void populateModelProvider(
List<StreamStructure> head,
boolean cachedBindings,
boolean legacyHeadContent,
UiContext uiContext) {
Logger.i(TAG, "TimeoutSession.populateModelProvider, shouldAppend %s", legacyHeadContent);
super.populateModelProvider(head, cachedBindings, legacyHeadContent, uiContext);
}
@Override
public void updateSession(
boolean clearHead,
List<StreamStructure> streamStructures,
int schemaVersion,
/*@Nullable*/ MutationContext mutationContext) {
String localSessionId = Validators.checkNotNull(sessionId);
Logger.i(
TAG,
"updateSession; clearHead(%b), shouldAppend(%b), sessionId(%s)",
clearHead,
legacyHeadContent,
localSessionId);
if (clearHead) {
if (!legacyHeadContent) {
if (shouldInvalidateModelProvider(mutationContext, localSessionId)) {
if (modelProvider != null) {
if (mutationContext == null) {
modelProvider.invalidate(UiContext.getDefaultInstance());
} else {
modelProvider.invalidate(mutationContext.getUiContext());
}
Logger.i(
TAG,
"Invalidating Model Provider for session %s due to a clear head",
localSessionId);
}
} else {
// Without having legacy HEAD content, don't update the session,
Logger.i(TAG, "Not configured to append: %s", localSessionId);
}
return;
}
if (viewDepthProvider != null) {
// Append the new items to the existing copy of HEAD, removing the existing items which
// have not yet been seen by the user.
List<ModelChild> rootChildren = captureRootContent();
if (!rootChildren.isEmpty()) {
// Calculate the children to remove and append StreamStructure remove operations
String lowestChild = Validators.checkNotNull(viewDepthProvider).getChildViewDepth();
List<StreamStructure> removeOperations = removeItems(lowestChild, rootChildren);
Logger.i(TAG, "Removing %d items", removeOperations.size());
if (!removeOperations.isEmpty()) {
removeOperations.addAll(streamStructures);
streamStructures = removeOperations;
}
}
}
// Only do this once
legacyHeadContent = false;
}
updateCount++;
updateSessionInternal(streamStructures, mutationContext);
}
@Override
public boolean invalidateOnResetHead() {
return false;
}
/**
* Remove items found below the specified {@code lowestChild}. If {@code lowestChild} is itself a
* token, it will also be removed to avoid multiple "More" buttons. If {@code lowestChild} is
* {@literal null}, only tokens will be removed to prevent potentially removing items that the
* user can currently see.
*/
private List<StreamStructure> removeItems(
/*@Nullable*/ String lowestChild, List<ModelChild> rootChildren) {
boolean remove = false;
List<StreamStructure> removeOperations = new ArrayList<>();
for (ModelChild child : rootChildren) {
if (remove || child.getType() == Type.TOKEN) {
removeOperations.add(createRemoveFeature(child.getContentId(), child.getParentId()));
} else if (child.getContentId().equals(lowestChild)) {
remove = true;
}
}
return removeOperations;
}
@VisibleForTesting
List<ModelChild> captureRootContent() {
ModelProvider modelProvider = getModelProvider();
if (modelProvider == null) {
Logger.w(TAG, "ModelProvider was not found");
return Collections.emptyList();
}
return modelProvider.getAllRootChildren();
}
@VisibleForTesting
StreamStructure createRemoveFeature(String contentId, /*@Nullable*/ String parentId) {
StreamStructure.Builder builder =
StreamStructure.newBuilder().setOperation(Operation.REMOVE).setContentId(contentId);
if (parentId != null) {
builder.setParentContentId(parentId);
}
return builder.build();
}
}