blob: 65966c4516c538a21b9cd4de043f57b2f343ae18 [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.piet;
import static com.google.android.libraries.feed.common.Validators.checkNotNull;
import static com.google.search.now.ui.piet.ErrorsProto.ErrorCode.ERR_MISSING_OR_UNHANDLED_CONTENT;
import static com.google.search.now.ui.piet.ErrorsProto.ErrorCode.ERR_POOR_FRAME_RATE;
import android.content.Context;
import android.support.annotation.VisibleForTesting;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.widget.LinearLayout;
import com.google.android.libraries.feed.api.host.config.DebugBehavior;
import com.google.android.libraries.feed.common.logging.Logger;
import com.google.android.libraries.feed.piet.DebugLogger.MessageType;
import com.google.android.libraries.feed.piet.TemplateBinder.TemplateAdapterModel;
import com.google.android.libraries.feed.piet.host.ActionHandler;
import com.google.android.libraries.feed.piet.host.EventLogger;
import com.google.search.now.ui.piet.ActionsProto.VisibilityAction;
import com.google.search.now.ui.piet.ElementsProto.BindingContext;
import com.google.search.now.ui.piet.ElementsProto.Content;
import com.google.search.now.ui.piet.ElementsProto.Element;
import com.google.search.now.ui.piet.ElementsProto.TemplateInvocation;
import com.google.search.now.ui.piet.PietAndroidSupport.ShardingControl;
import com.google.search.now.ui.piet.PietProto.Frame;
import com.google.search.now.ui.piet.PietProto.PietSharedState;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* An adapter which manages {@link Frame} instances. Frames will contain one or more slices. This
* class has additional public methods to support host access to the primary view of the frame
* before the model is bound to the frame. A frame is basically a vertical LinearLayout of slice
* Views which are created by {@link ElementAdapter}. This Adapter is not created through a Factory
* and is managed by the host.
*/
public class FrameAdapterImpl implements FrameAdapter {
private static final String TAG = "FrameAdapter";
private static final String GENERIC_EXCEPTION = "Top Level Exception was caught - see logcat";
private final Set<ElementAdapter<?, ?>> childAdapters;
private final Context context;
private final AdapterParameters parameters;
private final ActionHandler actionHandler;
private final EventLogger eventLogger;
private final DebugBehavior debugBehavior;
private final Set<VisibilityAction> activeActions = new HashSet<>();
/*@Nullable*/ private LinearLayout view = null;
/*@Nullable*/ private FrameContext frameContext = null;
FrameAdapterImpl(
Context context,
AdapterParameters parameters,
ActionHandler actionHandler,
EventLogger eventLogger,
DebugBehavior debugBehavior) {
this.context = context;
this.parameters = parameters;
this.actionHandler = actionHandler;
this.eventLogger = eventLogger;
this.debugBehavior = debugBehavior;
childAdapters = new HashSet<>();
}
// TODO: Need to implement support for sharding
@Override
public void bindModel(
Frame frame,
int frameWidthPx,
/*@Nullable*/ ShardingControl shardingControl,
List<PietSharedState> pietSharedStates) {
long startTime = System.nanoTime();
initialBind(parameters.parentViewSupplier.get());
FrameContext localFrameContext =
createFrameContext(frame, frameWidthPx, pietSharedStates, checkNotNull(view));
frameContext = localFrameContext;
activeActions.clear();
activeActions.addAll(frame.getActions().getOnHideActionsList());
LinearLayout frameView = checkNotNull(view);
try {
for (Content content : frame.getContentsList()) {
// For Slice we will create the lower level slice instead to remove the extra
// level.
List<ElementAdapter<?, ?>> adapters =
getBoundAdaptersForContent(content, localFrameContext);
for (ElementAdapter<?, ?> adapter : adapters) {
childAdapters.add(adapter);
setLayoutParamsOnChild(adapter);
frameView.addView(adapter.getView());
}
}
StyleProvider style = localFrameContext.makeStyleFor(frame.getStyleReferences());
frameView.setBackground(style.createBackground());
} catch (RuntimeException e) {
// TODO: Remove this once error reporting is fully implemented.
Logger.e(TAG, e, "Catch top level exception");
String message = e.getMessage() != null ? e.getMessage() : GENERIC_EXCEPTION;
if (e instanceof PietFatalException) {
localFrameContext.reportMessage(
MessageType.ERROR, ((PietFatalException) e).getErrorCode(), message);
} else {
localFrameContext.reportMessage(MessageType.ERROR, message);
}
}
startTime = System.nanoTime() - startTime;
// TODO: We should be targeting < 15ms and warn at 10ms?
// Until we get a chance to do the performance tuning, leave this at 30ms to prevent
// warnings on large GridRows based frames.
if (startTime / 1000000 > 30) {
Logger.w(
TAG,
localFrameContext.reportMessage(
MessageType.WARNING,
ERR_POOR_FRAME_RATE,
String.format("Slow Bind (%s) time: %s ps", frame.getTag(), startTime / 1000)));
}
// If there were errors add an error slice to the frame
if (localFrameContext.getDebugBehavior().getShowDebugViews()) {
View errorView = localFrameContext.getDebugLogger().getReportView(MessageType.ERROR, context);
if (errorView != null) {
frameView.addView(errorView);
}
View warningView =
localFrameContext.getDebugLogger().getReportView(MessageType.WARNING, context);
if (warningView != null) {
frameView.addView(warningView);
}
}
eventLogger.logEvents(localFrameContext.getDebugLogger().getErrorCodes());
}
@Override
public void unbindModel() {
triggerHideActions();
LinearLayout view = checkNotNull(this.view);
for (ElementAdapter<?, ?> child : childAdapters) {
parameters.elementAdapterFactory.releaseAdapter(child);
}
childAdapters.clear();
view.removeAllViews();
frameContext = null;
}
private void setLayoutParamsOnChild(ElementAdapter<?, ?> childAdapter) {
int width = childAdapter.getComputedWidthPx();
width = width == StyleProvider.DIMENSION_NOT_SET ? LayoutParams.MATCH_PARENT : width;
int height = childAdapter.getComputedHeightPx();
height = height == StyleProvider.DIMENSION_NOT_SET ? LayoutParams.WRAP_CONTENT : height;
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(width, height);
childAdapter.getElementStyle().applyMargins(context, params);
params.gravity = childAdapter.getGravity(Gravity.TOP | Gravity.START);
childAdapter.setLayoutParams(params);
}
@Override
public LinearLayout getFrameContainer() {
initialBind(parameters.parentViewSupplier.get());
return checkNotNull(view);
}
@VisibleForTesting
FrameContext createFrameContext(
Frame frame, int frameWidthPx, List<PietSharedState> pietSharedStates, View frameView) {
MediaQueryHelper mediaQueryHelper =
new MediaQueryHelper(frameWidthPx, parameters.hostProviders.getAssetProvider(), context);
PietStylesHelper pietStylesHelper =
checkNotNull(parameters.pietStylesHelperFactory.get(pietSharedStates, mediaQueryHelper));
return FrameContext.createFrameContext(
frame,
pietSharedStates,
pietStylesHelper,
debugBehavior,
new DebugLogger(),
actionHandler,
parameters.hostProviders,
frameView);
}
@Override
public void triggerHideActions() {
if (frameContext == null || view == null) {
return;
}
FrameContext localFrameContext = frameContext;
ViewUtils.triggerHideActions(
view,
localFrameContext.getFrame().getActions(),
localFrameContext.getActionHandler(),
localFrameContext.getFrame(),
activeActions);
for (ElementAdapter<?, ?> adapter : childAdapters) {
adapter.triggerHideActions(localFrameContext);
}
}
@Override
public void triggerViewActions(View viewport) {
if (frameContext == null || view == null) {
return;
}
FrameContext localFrameContext = frameContext;
ViewUtils.maybeTriggerViewActions(
view,
viewport,
localFrameContext.getFrame().getActions(),
localFrameContext.getActionHandler(),
localFrameContext.getFrame(),
activeActions);
for (ElementAdapter<?, ?> adapter : childAdapters) {
adapter.triggerViewActions(viewport, localFrameContext);
}
}
@VisibleForTesting
List<ElementAdapter<?, ?>> getBoundAdaptersForContent(
Content content, FrameContext frameContext) {
switch (content.getContentTypeCase()) {
case ELEMENT:
Element element = content.getElement();
ElementAdapter<?, ?> inlineSliceAdapter =
parameters.elementAdapterFactory.createAdapterForElement(element, frameContext);
inlineSliceAdapter.bindModel(element, frameContext);
return Collections.singletonList(inlineSliceAdapter);
case TEMPLATE_INVOCATION:
TemplateInvocation templateInvocation = content.getTemplateInvocation();
ArrayList<ElementAdapter<?, ?>> returnList = new ArrayList<>();
for (BindingContext bindingContext : templateInvocation.getBindingContextsList()) {
TemplateAdapterModel model =
new TemplateAdapterModel(
templateInvocation.getTemplateId(), frameContext, bindingContext);
ElementAdapter<? extends View, ?> templateAdapter =
parameters.templateBinder.createAndBindTemplateAdapter(model, frameContext);
returnList.add(templateAdapter);
}
returnList.trimToSize();
return Collections.unmodifiableList(returnList);
default:
Logger.wtf(
TAG,
frameContext.reportMessage(
MessageType.ERROR,
ERR_MISSING_OR_UNHANDLED_CONTENT,
String.format(
"Unsupported Content type for frame: %s", content.getContentTypeCase())));
return Collections.emptyList();
}
}
@VisibleForTesting
AdapterParameters getParameters() {
return this.parameters;
}
@VisibleForTesting
/*@Nullable*/
LinearLayout getView() {
return this.view;
}
private void initialBind(/*@Nullable*/ ViewGroup parent) {
if (view != null) {
return;
}
this.view = createView(parent);
}
private LinearLayout createView(/*@Nullable*/ ViewGroup parent) {
LinearLayout linearLayout = new LinearLayout(context);
linearLayout.setOrientation(LinearLayout.VERTICAL);
ViewGroup.LayoutParams layoutParams;
if (parent != null && parent.getLayoutParams() != null) {
layoutParams = new LinearLayout.LayoutParams(parent.getLayoutParams());
layoutParams.width = LayoutParams.MATCH_PARENT;
layoutParams.height = LayoutParams.WRAP_CONTENT;
} else {
layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
}
linearLayout.setLayoutParams(layoutParams);
return linearLayout;
}
}