diff options
Diffstat (limited to 'app/src/main/java/com/wireguard/android/bindings')
6 files changed, 537 insertions, 0 deletions
diff --git a/app/src/main/java/com/wireguard/android/bindings/BindingAdapters.java b/app/src/main/java/com/wireguard/android/bindings/BindingAdapters.java new file mode 100644 index 00000000..56540921 --- /dev/null +++ b/app/src/main/java/com/wireguard/android/bindings/BindingAdapters.java @@ -0,0 +1,105 @@ +package com.wireguard.android.bindings; + +import android.databinding.BindingAdapter; +import android.databinding.ObservableList; +import android.databinding.adapters.ListenerUtil; +import android.graphics.Typeface; +import android.text.InputFilter; +import android.widget.LinearLayout; +import android.widget.ListView; +import android.widget.TextView; + +import com.wireguard.android.R; + +/** + * Static methods for use by generated code in the Android data binding library. + */ + +@SuppressWarnings("unused") +public final class BindingAdapters { + @BindingAdapter({"items", "layout"}) + public static <T> void setItems(final LinearLayout view, + final ObservableList<T> oldList, final int oldLayoutId, + final ObservableList<T> newList, final int newLayoutId) { + if (oldList == newList && oldLayoutId == newLayoutId) + return; + ItemChangeListener<T> listener = ListenerUtil.getListener(view, R.id.item_change_listener); + // If the layout changes, any existing listener must be replaced. + if (listener != null && oldList != null && oldLayoutId != newLayoutId) { + listener.setList(null); + listener = null; + } + // Avoid adding a listener when there is no new list or layout. + if (newList == null || newLayoutId == 0) + return; + if (listener == null) { + listener = new ItemChangeListener<>(view, newLayoutId); + ListenerUtil.trackListener(view, listener, R.id.item_change_listener); + } + // Either the list changed, or this is an entirely new listener because the layout changed. + listener.setList(newList); + } + + @BindingAdapter({"items", "layout"}) + public static <T> void setItems(final ListView view, + final ObservableList<T> oldList, final int oldLayoutId, + final ObservableList<T> newList, final int newLayoutId) { + // Remove any existing binding when there is no new list or layout. + if (newList == null || newLayoutId == 0) { + view.setAdapter(null); + return; + } + // The ListAdapter interface is not generic, so this cannot be checked. + @SuppressWarnings("unchecked") + ObservableListAdapter<T> adapter = (ObservableListAdapter<T>) view.getAdapter(); + // If the layout changes, any existing adapter must be replaced. + if (newLayoutId != oldLayoutId) + adapter = null; + // Add a new binding if there was none, or if it must be replaced due to a layout change. + if (adapter == null) { + view.setAdapter(new ObservableListAdapter<>(view.getContext(), newLayoutId, newList)); + } else if (newList != oldList) { + // Changing the list only requires modifying the existing adapter. + adapter.setList(newList); + } + } + + @BindingAdapter({"items", "layout"}) + public static <K extends Comparable<K>, V> void setItems( + final ListView view, + final ObservableSortedMap<K, V> oldMap, final int oldLayoutId, + final ObservableSortedMap<K, V> newMap, final int newLayoutId) { + // Remove any existing binding when there is no new map or layout. + if (newMap == null || newLayoutId == 0) { + view.setAdapter(null); + return; + } + // The ListAdapter interface is not generic, so this cannot be checked. + @SuppressWarnings("unchecked") + ObservableMapAdapter<K, V> adapter = (ObservableMapAdapter<K, V>) view.getAdapter(); + // If the layout changes, any existing adapter must be replaced. + if (newLayoutId != oldLayoutId) + adapter = null; + // Add a new binding if there was none, or if it must be replaced due to a layout change. + if (adapter == null) { + view.setAdapter(new ObservableMapAdapter<>(view.getContext(), newLayoutId, newMap)); + } else if (newMap != oldMap) { + // Changing the list only requires modifying the existing adapter. + adapter.setMap(newMap); + } + } + + @BindingAdapter({"filter"}) + public static void setFilter(final TextView view, final InputFilter filter) { + view.setFilters(new InputFilter[]{filter}); + } + + @BindingAdapter({"android:textStyle"}) + public static void setTextStyle(final TextView view, final Typeface typeface) { + view.setTypeface(typeface); + } + + private BindingAdapters() { + // Prevent instantiation. + } +} diff --git a/app/src/main/java/com/wireguard/android/bindings/ItemChangeListener.java b/app/src/main/java/com/wireguard/android/bindings/ItemChangeListener.java new file mode 100644 index 00000000..39b3b654 --- /dev/null +++ b/app/src/main/java/com/wireguard/android/bindings/ItemChangeListener.java @@ -0,0 +1,128 @@ +package com.wireguard.android.bindings; + +import android.databinding.DataBindingUtil; +import android.databinding.ObservableList; +import android.databinding.ViewDataBinding; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.wireguard.android.BR; + +import java.lang.ref.WeakReference; + +/** + * Helper class for binding an ObservableList to the children of a ViewGroup. + */ + +class ItemChangeListener<T> { + private final OnListChangedCallback<T> callback = new OnListChangedCallback<>(this); + private final ViewGroup container; + private final int layoutId; + private final LayoutInflater layoutInflater; + private ObservableList<T> list; + + ItemChangeListener(final ViewGroup container, final int layoutId) { + this.container = container; + this.layoutId = layoutId; + layoutInflater = LayoutInflater.from(container.getContext()); + } + + private View getView(final int position, final View convertView) { + ViewDataBinding binding = DataBindingUtil.getBinding(convertView); + if (binding == null) + binding = DataBindingUtil.inflate(layoutInflater, layoutId, container, false); + binding.setVariable(BR.item, list.get(position)); + binding.executePendingBindings(); + return binding.getRoot(); + } + + public void setList(final ObservableList<T> newList) { + if (list != null) + list.removeOnListChangedCallback(callback); + list = newList; + if (list != null) { + list.addOnListChangedCallback(callback); + callback.onChanged(list); + } else { + container.removeAllViews(); + } + } + + private static class OnListChangedCallback<T> + extends ObservableList.OnListChangedCallback<ObservableList<T>> { + + private final WeakReference<ItemChangeListener<T>> weakListener; + + private OnListChangedCallback(final ItemChangeListener<T> listener) { + weakListener = new WeakReference<>(listener); + } + + @Override + public void onChanged(final ObservableList<T> sender) { + final ItemChangeListener<T> listener = weakListener.get(); + if (listener != null) { + // TODO: recycle views + listener.container.removeAllViews(); + for (int i = 0; i < sender.size(); ++i) + listener.container.addView(listener.getView(i, null)); + } else { + sender.removeOnListChangedCallback(this); + } + } + + @Override + public void onItemRangeChanged(final ObservableList<T> sender, final int positionStart, + final int itemCount) { + final ItemChangeListener<T> listener = weakListener.get(); + if (listener != null) { + for (int i = positionStart; i < positionStart + itemCount; ++i) { + final View child = listener.container.getChildAt(i); + listener.container.removeViewAt(i); + listener.container.addView(listener.getView(i, child)); + } + } else { + sender.removeOnListChangedCallback(this); + } + } + + @Override + public void onItemRangeInserted(final ObservableList<T> sender, final int positionStart, + final int itemCount) { + final ItemChangeListener<T> listener = weakListener.get(); + if (listener != null) { + for (int i = positionStart; i < positionStart + itemCount; ++i) + listener.container.addView(listener.getView(i, null)); + } else { + sender.removeOnListChangedCallback(this); + } + } + + @Override + public void onItemRangeMoved(final ObservableList<T> sender, final int fromPosition, + final int toPosition, final int itemCount) { + final ItemChangeListener<T> listener = weakListener.get(); + if (listener != null) { + final View[] views = new View[itemCount]; + for (int i = 0; i < itemCount; ++i) + views[i] = listener.container.getChildAt(fromPosition + i); + listener.container.removeViews(fromPosition, itemCount); + for (int i = 0; i < itemCount; ++i) + listener.container.addView(views[i], toPosition + i); + } else { + sender.removeOnListChangedCallback(this); + } + } + + @Override + public void onItemRangeRemoved(final ObservableList<T> sender, final int positionStart, + final int itemCount) { + final ItemChangeListener<T> listener = weakListener.get(); + if (listener != null) { + listener.container.removeViews(positionStart, itemCount); + } else { + sender.removeOnListChangedCallback(this); + } + } + } +} diff --git a/app/src/main/java/com/wireguard/android/bindings/ObservableListAdapter.java b/app/src/main/java/com/wireguard/android/bindings/ObservableListAdapter.java new file mode 100644 index 00000000..5b54ecaf --- /dev/null +++ b/app/src/main/java/com/wireguard/android/bindings/ObservableListAdapter.java @@ -0,0 +1,110 @@ +package com.wireguard.android.bindings; + +import android.content.Context; +import android.databinding.DataBindingUtil; +import android.databinding.ObservableList; +import android.databinding.ViewDataBinding; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ListAdapter; + +import com.wireguard.android.BR; + +import java.lang.ref.WeakReference; + +/** + * A generic ListAdapter backed by an ObservableList. + */ + +class ObservableListAdapter<T> extends BaseAdapter implements ListAdapter { + private final OnListChangedCallback<T> callback = new OnListChangedCallback<>(this); + private final int layoutId; + private final LayoutInflater layoutInflater; + private ObservableList<T> list; + + ObservableListAdapter(final Context context, final int layoutId, final ObservableList<T> list) { + this.layoutId = layoutId; + layoutInflater = LayoutInflater.from(context); + setList(list); + } + + @Override + public int getCount() { + return list != null ? list.size() : 0; + } + + @Override + public T getItem(final int position) { + return list != null ? list.get(position) : null; + } + + @Override + public long getItemId(final int position) { + return position; + } + + @Override + public View getView(final int position, final View convertView, final ViewGroup parent) { + ViewDataBinding binding = DataBindingUtil.getBinding(convertView); + if (binding == null) + binding = DataBindingUtil.inflate(layoutInflater, layoutId, parent, false); + binding.setVariable(BR.item, getItem(position)); + binding.executePendingBindings(); + return binding.getRoot(); + } + + public void setList(final ObservableList<T> newList) { + if (list != null) + list.removeOnListChangedCallback(callback); + list = newList; + if (list != null) { + list.addOnListChangedCallback(callback); + } + notifyDataSetChanged(); + } + + private static class OnListChangedCallback<U> + extends ObservableList.OnListChangedCallback<ObservableList<U>> { + + private final WeakReference<ObservableListAdapter<U>> weakAdapter; + + private OnListChangedCallback(final ObservableListAdapter<U> adapter) { + weakAdapter = new WeakReference<>(adapter); + } + + @Override + public void onChanged(final ObservableList<U> sender) { + final ObservableListAdapter<U> adapter = weakAdapter.get(); + if (adapter != null) + adapter.notifyDataSetChanged(); + else + sender.removeOnListChangedCallback(this); + } + + @Override + public void onItemRangeChanged(final ObservableList<U> sender, final int positionStart, + final int itemCount) { + onChanged(sender); + } + + @Override + public void onItemRangeInserted(final ObservableList<U> sender, final int positionStart, + final int itemCount) { + onChanged(sender); + } + + @Override + public void onItemRangeMoved(final ObservableList<U> sender, final int fromPosition, + final int toPosition, final int itemCount) { + onChanged(sender); + } + + @Override + public void onItemRangeRemoved(final ObservableList<U> sender, final int positionStart, + final int itemCount) { + onChanged(sender); + } + } +} diff --git a/app/src/main/java/com/wireguard/android/bindings/ObservableMapAdapter.java b/app/src/main/java/com/wireguard/android/bindings/ObservableMapAdapter.java new file mode 100644 index 00000000..da0c36a7 --- /dev/null +++ b/app/src/main/java/com/wireguard/android/bindings/ObservableMapAdapter.java @@ -0,0 +1,115 @@ +package com.wireguard.android.bindings; + +import android.content.Context; +import android.databinding.DataBindingUtil; +import android.databinding.ObservableMap; +import android.databinding.ViewDataBinding; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ListAdapter; + +import com.wireguard.android.BR; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Collections; + +/** + * A generic ListAdapter backed by a TreeMap that adds observability. + */ + +public class ObservableMapAdapter<K extends Comparable<K>, V> extends BaseAdapter + implements ListAdapter { + private final OnMapChangedCallback<K, V> callback = new OnMapChangedCallback<>(this); + private ArrayList<K> keys; + private final int layoutId; + private final LayoutInflater layoutInflater; + private ObservableSortedMap<K, V> map; + + ObservableMapAdapter(final Context context, final int layoutId, + final ObservableSortedMap<K, V> map) { + this.layoutId = layoutId; + layoutInflater = LayoutInflater.from(context); + setMap(map); + } + + @Override + public int getCount() { + return map != null ? map.size() : 0; + } + + @Override + public V getItem(final int position) { + if (map == null || position < 0 || position >= map.size()) + return null; + return map.get(getKeys().get(position)); + } + + @Override + public long getItemId(final int position) { + if (map == null || position < 0 || position >= map.size()) + return -1; + return map.get(getKeys().get(position)).hashCode(); + } + + public int getItemPosition(final K key) { + if (map == null) + return -1; + return Collections.binarySearch(getKeys(), key); + } + + private ArrayList<K> getKeys() { + if (keys == null) + keys = new ArrayList<>(map.keySet()); + return keys; + } + + @Override + public View getView(final int position, final View convertView, final ViewGroup parent) { + ViewDataBinding binding = DataBindingUtil.getBinding(convertView); + if (binding == null) + binding = DataBindingUtil.inflate(layoutInflater, layoutId, parent, false); + binding.setVariable(BR.item, getItem(position)); + binding.executePendingBindings(); + return binding.getRoot(); + } + + @Override + public boolean hasStableIds() { + return true; + } + + public void setMap(final ObservableSortedMap<K, V> newMap) { + if (map != null) + map.removeOnMapChangedCallback(callback); + keys = null; + map = newMap; + if (map != null) { + map.addOnMapChangedCallback(callback); + } + notifyDataSetChanged(); + } + + private static class OnMapChangedCallback<K extends Comparable<K>, V> + extends ObservableMap.OnMapChangedCallback<ObservableSortedMap<K, V>, K, V> { + + private final WeakReference<ObservableMapAdapter<K, V>> weakAdapter; + + private OnMapChangedCallback(final ObservableMapAdapter<K, V> adapter) { + weakAdapter = new WeakReference<>(adapter); + } + + @Override + public void onMapChanged(final ObservableSortedMap<K, V> sender, final K key) { + final ObservableMapAdapter<K, V> adapter = weakAdapter.get(); + if (adapter != null) { + adapter.keys = null; + adapter.notifyDataSetChanged(); + } else { + sender.removeOnMapChangedCallback(this); + } + } + } +} diff --git a/app/src/main/java/com/wireguard/android/bindings/ObservableSortedMap.java b/app/src/main/java/com/wireguard/android/bindings/ObservableSortedMap.java new file mode 100644 index 00000000..3317c6a7 --- /dev/null +++ b/app/src/main/java/com/wireguard/android/bindings/ObservableSortedMap.java @@ -0,0 +1,13 @@ +package com.wireguard.android.bindings; + +import android.databinding.ObservableMap; + +import java.util.SortedMap; + +/** + * Interface for maps that are both observable and sorted. + */ + +public interface ObservableSortedMap<K, V> extends ObservableMap<K, V>, SortedMap<K, V> { + // No additional methods. +} diff --git a/app/src/main/java/com/wireguard/android/bindings/ObservableTreeMap.java b/app/src/main/java/com/wireguard/android/bindings/ObservableTreeMap.java new file mode 100644 index 00000000..34fcab61 --- /dev/null +++ b/app/src/main/java/com/wireguard/android/bindings/ObservableTreeMap.java @@ -0,0 +1,66 @@ +package com.wireguard.android.bindings; + +import android.databinding.MapChangeRegistry; +import android.databinding.ObservableMap; +import android.support.annotation.NonNull; + +import java.util.Map; +import java.util.TreeMap; + +/** + * Observable version of a TreeMap. Only notifies for changes made through methods, not iterators or + * views. This behavior is in line with that of ObservableArrayMap. + */ + +public class ObservableTreeMap<K, V> extends TreeMap<K, V> implements ObservableSortedMap<K, V> { + private transient MapChangeRegistry listeners; + + @Override + public void clear() { + super.clear(); + notifyChange(null); + } + + @Override + public void addOnMapChangedCallback( + final OnMapChangedCallback<? extends ObservableMap<K, V>, K, V> listener) { + if (listeners == null) + listeners = new MapChangeRegistry(); + listeners.add(listener); + } + + private void notifyChange(final K key) { + if (listeners != null) + listeners.notifyChange(this, key); + } + + @Override + public V put(final K key, final V value) { + final V oldValue = super.put(key, value); + notifyChange(key); + return oldValue; + } + + @Override + public void putAll(@NonNull final Map<? extends K, ? extends V> map) { + super.putAll(map); + for (final K key : map.keySet()) + notifyChange(key); + } + + @Override + public V remove(final Object key) { + final V oldValue = super.remove(key); + @SuppressWarnings("unchecked") + final K k = (K) key; + notifyChange(k); + return oldValue; + } + + @Override + public void removeOnMapChangedCallback( + final OnMapChangedCallback<? extends ObservableMap<K, V>, K, V> listener) { + if (listeners != null) + listeners.remove(listener); + } +} |