blob: 5bc71fd40a0765bbbe91be8c79d9da967c849f0d [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.android.libraries.feed.common.Validators.checkState;
import android.view.View;
import com.google.search.now.ui.piet.ElementsProto.BindingContext;
import com.google.search.now.ui.piet.PietProto.PietSharedState;
import com.google.search.now.ui.piet.PietProto.Stylesheet;
import com.google.search.now.ui.piet.PietProto.Template;
import java.util.List;
/**
* Methods to enable creation and binding of Templates.
*
* <p>This class creates and binds adapters for templates allowing for reuse of full template
* layouts as a unit. Release and recycling is handled by the ElementAdapterFactory.
*/
class TemplateBinder {
private final KeyedRecyclerPool<ElementAdapter<? extends View, ?>> templateRecyclerPool;
private final ElementAdapterFactory adapterFactory;
TemplateBinder(
KeyedRecyclerPool<ElementAdapter<? extends View, ?>> templateRecyclerPool,
ElementAdapterFactory adapterFactory) {
this.templateRecyclerPool = templateRecyclerPool;
this.adapterFactory = adapterFactory;
}
/** Higher-performance method that creates and binds an adapter for a template all at once. */
ElementAdapter<? extends View, ?> createAndBindTemplateAdapter(
TemplateAdapterModel model, FrameContext frameContext) {
// Create the template context for use in both create and bind
// This is a modestly expensive operation, and calling createTemplateAdapter followed by
// bindTemplateAdapter would do it twice.
FrameContext templateContext =
frameContext.createTemplateContext(model.getTemplate(), model.getBindingContext());
TemplateKey templateKey = makeTemplateKey(model, frameContext);
ElementAdapter<? extends View, ?> adapter = templateRecyclerPool.get(templateKey);
if (adapter == null) {
// Create new adapter
adapter =
adapterFactory.createAdapterForElement(model.getTemplate().getElement(), templateContext);
adapter.setKey(templateKey);
}
adapter.bindModel(model.getTemplate().getElement(), templateContext);
return adapter;
}
ElementAdapter<? extends View, ?> createTemplateAdapter(
TemplateAdapterModel model, FrameContext frameContext) {
TemplateKey templateKey = makeTemplateKey(model, frameContext);
ElementAdapter<? extends View, ?> adapter = templateRecyclerPool.get(templateKey);
if (adapter == null) {
// Make new FrameContext here
FrameContext templateContext =
frameContext.createTemplateContext(model.getTemplate(), model.getBindingContext());
// Create new adapter
adapter =
adapterFactory.createAdapterForElement(model.getTemplate().getElement(), templateContext);
adapter.setKey(templateKey);
}
return adapter;
}
void bindTemplateAdapter(
ElementAdapter<? extends View, ?> adapter,
TemplateAdapterModel model,
FrameContext frameContext) {
TemplateKey templateKey = makeTemplateKey(model, frameContext);
checkNotNull(adapter.getKey(), "Adapter key was null; not initialized correctly?");
checkState(
adapter.getKey() instanceof TemplateKey,
"bindTemplateAdapter only applicable for template adapters");
checkState(templateKey.equals(adapter.getKey()), "Template keys did not match");
FrameContext templateContext =
frameContext.createTemplateContext(model.getTemplate(), model.getBindingContext());
adapter.bindModel(model.getTemplate().getElement(), templateContext);
}
private static TemplateKey makeTemplateKey(
TemplateAdapterModel model, FrameContext frameContext) {
return new TemplateKey(
model.getTemplate(),
frameContext.getPietSharedStates(),
frameContext.getMediaQueryStylesheets(model.getTemplate()));
}
/**
* Determines whether two templates are compatible for recycling. We're going to call the hash
* code good enough for performance reasons (.equals() is expensive), and hope we don't get a lot
* of collisions.
*/
@SuppressWarnings({"ReferenceEquality", "EqualsUsingHashCode"})
static boolean templateEquals(/*@Nullable*/ Template template1, /*@Nullable*/ Template template2) {
if (template1 == template2) {
return true;
} else if (template1 == null || template2 == null) {
return false;
}
return template1.hashCode() == template2.hashCode();
}
/** Wrap the Template proto object as the recycler key. */
static class TemplateKey extends RecyclerKey {
private final Template template;
/*@Nullable*/ private final List<PietSharedState> pietSharedStates;
// If the Template references Stylesheets that have MediaQueryConditions on them, they need to
// be part of the key.
private final List<Stylesheet> mediaQueryBasedStylesheets;
TemplateKey(
Template template,
/*@Nullable*/ List<PietSharedState> pietSharedStates,
List<Stylesheet> mediaQueryBasedStylesheets) {
this.template = template;
this.pietSharedStates = pietSharedStates;
this.mediaQueryBasedStylesheets = mediaQueryBasedStylesheets;
}
/** Equals checks the hashCode of the component fields to avoid expensive proto equals. */
@SuppressWarnings({"ReferenceEquality", "EqualsUsingHashCode"})
@Override
public boolean equals(/*@Nullable*/ Object o) {
if (this == o) {
return true;
}
if (!(o instanceof TemplateKey)) {
return false;
}
TemplateKey that = (TemplateKey) o;
// Check that Templates are equal
if (!templateEquals(template, that.template)) {
return false;
}
// Check that PietSharedStates are equal or both null
if (that.pietSharedStates == null ^ this.pietSharedStates == null) {
return false;
}
if (this.pietSharedStates != null
&& that.pietSharedStates != null
&& (this.pietSharedStates.size() != that.pietSharedStates.size()
|| this.pietSharedStates.hashCode() != that.pietSharedStates.hashCode())) {
return false;
}
// Check that stylesheets are equal or both empty
if (that.mediaQueryBasedStylesheets.isEmpty() ^ this.mediaQueryBasedStylesheets.isEmpty()) {
return false;
}
return this.mediaQueryBasedStylesheets == that.mediaQueryBasedStylesheets
|| this.mediaQueryBasedStylesheets.hashCode()
== that.mediaQueryBasedStylesheets.hashCode();
}
@Override
public int hashCode() {
int result = template.hashCode();
result = 31 * result + (pietSharedStates != null ? pietSharedStates.hashCode() : 0);
result =
31 * result
+ (mediaQueryBasedStylesheets != null ? mediaQueryBasedStylesheets.hashCode() : 0);
return result;
}
}
static class TemplateAdapterModel {
private final Template template;
private final BindingContext bindingContext;
TemplateAdapterModel(Template template, BindingContext bindingContext) {
this.template = template;
this.bindingContext = bindingContext;
}
TemplateAdapterModel(
String templateId, FrameContext frameContext, BindingContext bindingContext) {
this.template =
checkNotNull(
frameContext.getTemplate(templateId), "Template was not found: %s", templateId);
this.bindingContext = bindingContext;
}
public Template getTemplate() {
return template;
}
BindingContext getBindingContext() {
return bindingContext;
}
@SuppressWarnings({"ReferenceEquality", "EqualsUsingHashCode"})
@Override
public boolean equals(/*@Nullable*/ Object o) {
if (this == o) {
return true;
}
if (!(o instanceof TemplateAdapterModel)) {
return false;
}
TemplateAdapterModel that = (TemplateAdapterModel) o;
return templateEquals(template, that.template)
&& (bindingContext == that.bindingContext
|| (bindingContext.getBindingValuesCount()
== that.bindingContext.getBindingValuesCount()
&& bindingContext.hashCode() == that.bindingContext.hashCode()));
}
@Override
public int hashCode() {
int result = template.hashCode();
result = 31 * result + bindingContext.hashCode();
return result;
}
}
}