blob: d66c75c2c37248770ec6b36e6ee95b8d2cc798fe [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package io.v.baku.toolkit.bind;
import android.app.Activity;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
import com.google.common.collect.Iterables;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import io.v.baku.toolkit.BakuActivityTrait;
import io.v.rx.syncbase.RxTable;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action1;
import rx.subscriptions.CompositeSubscription;
public abstract class SyncbaseBinding {
/**
* The {@link #deleteValue(Object)} and {@link #defaultValue(Object)} options are minimally
* typesafe, but keeping this builder simple is preferable, and the absence of rigorous static
* type checking here is acceptable.
*/
public static class Builder<T> {
private Activity mActivity;
private RxTable mRxTable;
private String mKey;
private boolean mExplicitDefaultValue;
private T mDeleteValue, mDefaultValue;
private final List<CoordinatorChain<T>> mCoordinators = new ArrayList<>();
private CompositeSubscription mSubscriptionParent;
private Action1<Throwable> mOnError;
public Builder<T> activity(final Activity activity) {
mActivity = activity;
return this;
}
public Builder<T> rxTable(final RxTable rxTable) {
mRxTable = rxTable;
return this;
}
public Builder<T> bakuActivity(final BakuActivityTrait trait) {
return activity(trait.getVAndroidContextTrait().getAndroidContext())
.rxTable(trait.getSyncbaseTable())
.subscriptionParent(trait.getSubscriptions())
.onError(trait::onSyncError);
}
public Builder<T> key(final String key) {
mKey = key;
return this;
}
private String getKey(final Object fallback) {
return mKey == null ? fallback.toString() : mKey;
}
/**
* Note that although this option is generic and attempts to enforce some weak measure of
* type safety in normal use, type pollution may still result in a
* {@link ClassCastException} at build time.
*/
@SuppressWarnings("unchecked")
public <U extends T> Builder<U> deleteValue(final U deleteValue) {
mDeleteValue = deleteValue;
return (Builder<U>) this;
}
private T getDefaultValue(final T fallback) {
return mExplicitDefaultValue ? mDefaultValue : fallback;
}
/**
* Note that although this option is generic and attempts to enforce some weak measure of
* type safety in normal use, type pollution may still result in a
* {@link ClassCastException} at build time.
*/
@SuppressWarnings("unchecked")
public <U extends T> Builder<U> defaultValue(final U defaultValue) {
mDefaultValue = defaultValue;
mExplicitDefaultValue = true;
return (Builder<U>) this;
}
public <U extends T> Builder<U> zeroValue(final U zeroValue) {
return deleteValue(zeroValue).defaultValue(zeroValue);
}
@SafeVarargs
public final Builder<T> coordinators(final CoordinatorChain<T>... coordinators) {
mCoordinators.clear();
return chain(coordinators);
}
@SafeVarargs
public final Builder<T> chain(final CoordinatorChain<T>... coordinators) {
Collections.addAll(mCoordinators, coordinators);
return this;
}
public Builder<T> coordinators(final Iterable<CoordinatorChain<T>> coordinators) {
mCoordinators.clear();
return chain(coordinators);
}
public Builder<T> chain(final Iterable<CoordinatorChain<T>> coordinators) {
Iterables.addAll(mCoordinators, coordinators);
return this;
}
public Builder<T> subscriptionParent(final CompositeSubscription subscriptionParent) {
mSubscriptionParent = subscriptionParent;
return this;
}
private Subscription subscribe(final Subscription subscription) {
if (mSubscriptionParent != null) {
mSubscriptionParent.add(subscription);
}
return subscription;
}
public Builder<T> onError(final Action1<Throwable> onError) {
mOnError = onError;
return this;
}
public Builder<T> bindOneWay(final TextView textView) {
@SuppressWarnings("unchecked")
final Builder<String> t = (Builder<String>) this;
subscribe(TextViewBindingTermini.bindRead(textView,
SyncbaseBindingTermini.bindRead(mRxTable, getKey(textView.getId()),
String.class, t.getDefaultValue(""))
.onBackpressureLatest()
.observeOn(AndroidSchedulers.mainThread()),
mOnError));
return this;
}
/**
* Constructs a two-way binding between a string in Syncbase and the text of a TextView.
* <p>
* Defaults:
* <ul>
* <li>{@code defaultValue}: {@code ""}</li>
* <li>{@code deleteValue}: {@code null}</li>
* <li>{@code coordinators}: {@code DebouncingCoordinator}, and ensures that there is a
* {@code SuppressWriteOnReadCoordinator} somewhere in the chain, injecting it right
* above the {@code TextView} if absent.</li>
* </ul>
* <p>
* The coordination policy must end its downlink on the Android main thread.
* TODO(rosswang): provide a Coordinator that coerces this.
* <p>
* If {@code subscriptionParent} is set, this method adds the generated binding to it.
* <p>
* TODO(rosswang): produce a SyncbaseBinding, and allow mutable bindings.
*/
public Builder<T> bindTwoWay(final TextView textView) {
@SuppressWarnings("unchecked")
final Builder<String> t = (Builder<String>) this;
TwoWayBinding<String> core = SyncbaseBindingTermini.bind(mRxTable,
getKey(textView.getId()), String.class, t.getDefaultValue(""),
(String) mDeleteValue, mOnError);
boolean hasSuppressWriteOnRead = false;
for (final CoordinatorChain<String> c : t.mCoordinators) {
core = c.apply(core);
if (core instanceof SuppressWriteOnReadCoordinator) {
hasSuppressWriteOnRead = true;
}
}
if (mCoordinators.isEmpty()) {
core = new DebouncingCoordinator<>(core);
}
if (!hasSuppressWriteOnRead) {
core = new SuppressWriteOnReadCoordinator<>(core);
}
subscribe(TextViewBindingTermini.bind(textView, core, mOnError));
return this;
}
/**
* Calls {@link #bindTwoWay(TextView)} if the {@link TextView} is an {@link EditText},
* {@link #bindOneWay(TextView)} otherwise.
*/
public Builder<T> bindTo(final TextView textView) {
return textView instanceof EditText ? bindTwoWay(textView) : bindOneWay(textView);
}
/**
* An alias for {@link #bindTwoWay(TextView)}, which is the default for {@link EditText}
* widgets.
*/
public Builder<T> bindTo(final EditText editText) {
return bindTwoWay(editText);
}
public Builder<T> bindTo(final View view) {
if (view instanceof TextView) {
return bindTo((TextView) view);
} else {
throw new IllegalArgumentException("No default binding for view " + view);
}
}
public Builder<T> bindTo(final int viewId) {
return bindTo(mActivity.findViewById(viewId));
}
}
public static <T> Builder<T> builder() {
return new Builder<>();
}
}