From 8450ae9e86828a64e5f4e55738ed392de3450209 Mon Sep 17 00:00:00 2001 From: Samuel Holland Date: Tue, 7 Nov 2017 23:34:20 -0600 Subject: Bindings: Move to a separate package These are really app-independent reusable classes. Moving them to a separate package helps avoid polluting the main app package, and makes them easier to upgrade. --- .../com/wireguard/android/BindingAdapters.java | 103 ----------------- .../com/wireguard/android/ConfigListFragment.java | 1 + .../com/wireguard/android/ItemChangeListener.java | 126 -------------------- .../wireguard/android/ObservableListAdapter.java | 108 ----------------- .../wireguard/android/ObservableMapAdapter.java | 112 ------------------ .../com/wireguard/android/ObservableSortedMap.java | 13 --- .../com/wireguard/android/ObservableTreeMap.java | 66 ----------- .../java/com/wireguard/android/VpnService.java | 2 + .../android/bindings/BindingAdapters.java | 105 +++++++++++++++++ .../android/bindings/ItemChangeListener.java | 128 +++++++++++++++++++++ .../android/bindings/ObservableListAdapter.java | 110 ++++++++++++++++++ .../android/bindings/ObservableMapAdapter.java | 115 ++++++++++++++++++ .../android/bindings/ObservableSortedMap.java | 13 +++ .../android/bindings/ObservableTreeMap.java | 66 +++++++++++ 14 files changed, 540 insertions(+), 528 deletions(-) delete mode 100644 app/src/main/java/com/wireguard/android/BindingAdapters.java delete mode 100644 app/src/main/java/com/wireguard/android/ItemChangeListener.java delete mode 100644 app/src/main/java/com/wireguard/android/ObservableListAdapter.java delete mode 100644 app/src/main/java/com/wireguard/android/ObservableMapAdapter.java delete mode 100644 app/src/main/java/com/wireguard/android/ObservableSortedMap.java delete mode 100644 app/src/main/java/com/wireguard/android/ObservableTreeMap.java create mode 100644 app/src/main/java/com/wireguard/android/bindings/BindingAdapters.java create mode 100644 app/src/main/java/com/wireguard/android/bindings/ItemChangeListener.java create mode 100644 app/src/main/java/com/wireguard/android/bindings/ObservableListAdapter.java create mode 100644 app/src/main/java/com/wireguard/android/bindings/ObservableMapAdapter.java create mode 100644 app/src/main/java/com/wireguard/android/bindings/ObservableSortedMap.java create mode 100644 app/src/main/java/com/wireguard/android/bindings/ObservableTreeMap.java (limited to 'app/src/main/java/com') diff --git a/app/src/main/java/com/wireguard/android/BindingAdapters.java b/app/src/main/java/com/wireguard/android/BindingAdapters.java deleted file mode 100644 index d5c07ed6..00000000 --- a/app/src/main/java/com/wireguard/android/BindingAdapters.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.wireguard.android; - -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; - -/** - * Static methods for use by generated code in the Android data binding library. - */ - -@SuppressWarnings("unused") -public final class BindingAdapters { - @BindingAdapter({"items", "layout"}) - public static void setItems(final LinearLayout view, - final ObservableList oldList, final int oldLayoutId, - final ObservableList newList, final int newLayoutId) { - if (oldList == newList && oldLayoutId == newLayoutId) - return; - ItemChangeListener 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 void setItems(final ListView view, - final ObservableList oldList, final int oldLayoutId, - final ObservableList 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 adapter = (ObservableListAdapter) 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 , V> void setItems( - final ListView view, - final ObservableSortedMap oldMap, final int oldLayoutId, - final ObservableSortedMap 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 adapter = (ObservableMapAdapter) 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/ConfigListFragment.java b/app/src/main/java/com/wireguard/android/ConfigListFragment.java index 8586a741..c89d2339 100644 --- a/app/src/main/java/com/wireguard/android/ConfigListFragment.java +++ b/app/src/main/java/com/wireguard/android/ConfigListFragment.java @@ -13,6 +13,7 @@ import android.widget.AbsListView; import android.widget.AdapterView; import android.widget.ListView; +import com.wireguard.android.bindings.ObservableMapAdapter; import com.wireguard.android.databinding.ConfigListFragmentBinding; import com.wireguard.config.Config; diff --git a/app/src/main/java/com/wireguard/android/ItemChangeListener.java b/app/src/main/java/com/wireguard/android/ItemChangeListener.java deleted file mode 100644 index e3f34019..00000000 --- a/app/src/main/java/com/wireguard/android/ItemChangeListener.java +++ /dev/null @@ -1,126 +0,0 @@ -package com.wireguard.android; - -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 java.lang.ref.WeakReference; - -/** - * Helper class for binding an ObservableList to the children of a ViewGroup. - */ - -class ItemChangeListener { - private final OnListChangedCallback callback = new OnListChangedCallback<>(this); - private final ViewGroup container; - private final int layoutId; - private final LayoutInflater layoutInflater; - private ObservableList 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 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 - extends ObservableList.OnListChangedCallback> { - - private final WeakReference> weakListener; - - private OnListChangedCallback(final ItemChangeListener listener) { - weakListener = new WeakReference<>(listener); - } - - @Override - public void onChanged(final ObservableList sender) { - final ItemChangeListener 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 sender, final int positionStart, - final int itemCount) { - final ItemChangeListener 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 sender, final int positionStart, - final int itemCount) { - final ItemChangeListener 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 sender, final int fromPosition, - final int toPosition, final int itemCount) { - final ItemChangeListener 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 sender, final int positionStart, - final int itemCount) { - final ItemChangeListener 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/ObservableListAdapter.java b/app/src/main/java/com/wireguard/android/ObservableListAdapter.java deleted file mode 100644 index 8e240b71..00000000 --- a/app/src/main/java/com/wireguard/android/ObservableListAdapter.java +++ /dev/null @@ -1,108 +0,0 @@ -package com.wireguard.android; - -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 java.lang.ref.WeakReference; - -/** - * A generic ListAdapter backed by an ObservableList. - */ - -class ObservableListAdapter extends BaseAdapter implements ListAdapter { - private final OnListChangedCallback callback = new OnListChangedCallback<>(this); - private final int layoutId; - private final LayoutInflater layoutInflater; - private ObservableList list; - - ObservableListAdapter(final Context context, final int layoutId, final ObservableList 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 newList) { - if (list != null) - list.removeOnListChangedCallback(callback); - list = newList; - if (list != null) { - list.addOnListChangedCallback(callback); - } - notifyDataSetChanged(); - } - - private static class OnListChangedCallback - extends ObservableList.OnListChangedCallback> { - - private final WeakReference> weakAdapter; - - private OnListChangedCallback(final ObservableListAdapter adapter) { - weakAdapter = new WeakReference<>(adapter); - } - - @Override - public void onChanged(final ObservableList sender) { - final ObservableListAdapter adapter = weakAdapter.get(); - if (adapter != null) - adapter.notifyDataSetChanged(); - else - sender.removeOnListChangedCallback(this); - } - - @Override - public void onItemRangeChanged(final ObservableList sender, final int positionStart, - final int itemCount) { - onChanged(sender); - } - - @Override - public void onItemRangeInserted(final ObservableList sender, final int positionStart, - final int itemCount) { - onChanged(sender); - } - - @Override - public void onItemRangeMoved(final ObservableList sender, final int fromPosition, - final int toPosition, final int itemCount) { - onChanged(sender); - } - - @Override - public void onItemRangeRemoved(final ObservableList sender, final int positionStart, - final int itemCount) { - onChanged(sender); - } - } -} diff --git a/app/src/main/java/com/wireguard/android/ObservableMapAdapter.java b/app/src/main/java/com/wireguard/android/ObservableMapAdapter.java deleted file mode 100644 index 308c818d..00000000 --- a/app/src/main/java/com/wireguard/android/ObservableMapAdapter.java +++ /dev/null @@ -1,112 +0,0 @@ -package com.wireguard.android; - -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 java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.Collections; - -/** - * A generic ListAdapter backed by a TreeMap that adds observability. - */ - -class ObservableMapAdapter, V> extends BaseAdapter implements ListAdapter { - private final OnMapChangedCallback callback = new OnMapChangedCallback<>(this); - private ArrayList keys; - private final int layoutId; - private final LayoutInflater layoutInflater; - private ObservableSortedMap map; - - ObservableMapAdapter(final Context context, final int layoutId, - final ObservableSortedMap 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 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 newMap) { - if (map != null) - map.removeOnMapChangedCallback(callback); - keys = null; - map = newMap; - if (map != null) { - map.addOnMapChangedCallback(callback); - } - notifyDataSetChanged(); - } - - private static class OnMapChangedCallback, V> - extends ObservableMap.OnMapChangedCallback, K, V> { - - private final WeakReference> weakAdapter; - - private OnMapChangedCallback(final ObservableMapAdapter adapter) { - weakAdapter = new WeakReference<>(adapter); - } - - @Override - public void onMapChanged(final ObservableSortedMap sender, final K key) { - final ObservableMapAdapter 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/ObservableSortedMap.java b/app/src/main/java/com/wireguard/android/ObservableSortedMap.java deleted file mode 100644 index 5c96c68c..00000000 --- a/app/src/main/java/com/wireguard/android/ObservableSortedMap.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.wireguard.android; - -import android.databinding.ObservableMap; - -import java.util.SortedMap; - -/** - * Interface for maps that are both observable and sorted. - */ - -public interface ObservableSortedMap extends ObservableMap, SortedMap { - // No additional methods. -} diff --git a/app/src/main/java/com/wireguard/android/ObservableTreeMap.java b/app/src/main/java/com/wireguard/android/ObservableTreeMap.java deleted file mode 100644 index db72d857..00000000 --- a/app/src/main/java/com/wireguard/android/ObservableTreeMap.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.wireguard.android; - -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 extends TreeMap implements ObservableSortedMap { - private transient MapChangeRegistry listeners; - - @Override - public void clear() { - super.clear(); - notifyChange(null); - } - - @Override - public void addOnMapChangedCallback( - final OnMapChangedCallback, 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 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, K, V> listener) { - if (listeners != null) - listeners.remove(listener); - } -} diff --git a/app/src/main/java/com/wireguard/android/VpnService.java b/app/src/main/java/com/wireguard/android/VpnService.java index 0fd67136..265c6e57 100644 --- a/app/src/main/java/com/wireguard/android/VpnService.java +++ b/app/src/main/java/com/wireguard/android/VpnService.java @@ -12,6 +12,8 @@ import android.preference.PreferenceManager; import android.service.quicksettings.TileService; import android.util.Log; +import com.wireguard.android.bindings.ObservableSortedMap; +import com.wireguard.android.bindings.ObservableTreeMap; import com.wireguard.config.Config; import com.wireguard.config.Peer; 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 void setItems(final LinearLayout view, + final ObservableList oldList, final int oldLayoutId, + final ObservableList newList, final int newLayoutId) { + if (oldList == newList && oldLayoutId == newLayoutId) + return; + ItemChangeListener 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 void setItems(final ListView view, + final ObservableList oldList, final int oldLayoutId, + final ObservableList 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 adapter = (ObservableListAdapter) 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 , V> void setItems( + final ListView view, + final ObservableSortedMap oldMap, final int oldLayoutId, + final ObservableSortedMap 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 adapter = (ObservableMapAdapter) 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 { + private final OnListChangedCallback callback = new OnListChangedCallback<>(this); + private final ViewGroup container; + private final int layoutId; + private final LayoutInflater layoutInflater; + private ObservableList 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 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 + extends ObservableList.OnListChangedCallback> { + + private final WeakReference> weakListener; + + private OnListChangedCallback(final ItemChangeListener listener) { + weakListener = new WeakReference<>(listener); + } + + @Override + public void onChanged(final ObservableList sender) { + final ItemChangeListener 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 sender, final int positionStart, + final int itemCount) { + final ItemChangeListener 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 sender, final int positionStart, + final int itemCount) { + final ItemChangeListener 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 sender, final int fromPosition, + final int toPosition, final int itemCount) { + final ItemChangeListener 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 sender, final int positionStart, + final int itemCount) { + final ItemChangeListener 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 extends BaseAdapter implements ListAdapter { + private final OnListChangedCallback callback = new OnListChangedCallback<>(this); + private final int layoutId; + private final LayoutInflater layoutInflater; + private ObservableList list; + + ObservableListAdapter(final Context context, final int layoutId, final ObservableList 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 newList) { + if (list != null) + list.removeOnListChangedCallback(callback); + list = newList; + if (list != null) { + list.addOnListChangedCallback(callback); + } + notifyDataSetChanged(); + } + + private static class OnListChangedCallback + extends ObservableList.OnListChangedCallback> { + + private final WeakReference> weakAdapter; + + private OnListChangedCallback(final ObservableListAdapter adapter) { + weakAdapter = new WeakReference<>(adapter); + } + + @Override + public void onChanged(final ObservableList sender) { + final ObservableListAdapter adapter = weakAdapter.get(); + if (adapter != null) + adapter.notifyDataSetChanged(); + else + sender.removeOnListChangedCallback(this); + } + + @Override + public void onItemRangeChanged(final ObservableList sender, final int positionStart, + final int itemCount) { + onChanged(sender); + } + + @Override + public void onItemRangeInserted(final ObservableList sender, final int positionStart, + final int itemCount) { + onChanged(sender); + } + + @Override + public void onItemRangeMoved(final ObservableList sender, final int fromPosition, + final int toPosition, final int itemCount) { + onChanged(sender); + } + + @Override + public void onItemRangeRemoved(final ObservableList 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, V> extends BaseAdapter + implements ListAdapter { + private final OnMapChangedCallback callback = new OnMapChangedCallback<>(this); + private ArrayList keys; + private final int layoutId; + private final LayoutInflater layoutInflater; + private ObservableSortedMap map; + + ObservableMapAdapter(final Context context, final int layoutId, + final ObservableSortedMap 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 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 newMap) { + if (map != null) + map.removeOnMapChangedCallback(callback); + keys = null; + map = newMap; + if (map != null) { + map.addOnMapChangedCallback(callback); + } + notifyDataSetChanged(); + } + + private static class OnMapChangedCallback, V> + extends ObservableMap.OnMapChangedCallback, K, V> { + + private final WeakReference> weakAdapter; + + private OnMapChangedCallback(final ObservableMapAdapter adapter) { + weakAdapter = new WeakReference<>(adapter); + } + + @Override + public void onMapChanged(final ObservableSortedMap sender, final K key) { + final ObservableMapAdapter 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 extends ObservableMap, SortedMap { + // 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 extends TreeMap implements ObservableSortedMap { + private transient MapChangeRegistry listeners; + + @Override + public void clear() { + super.clear(); + notifyChange(null); + } + + @Override + public void addOnMapChangedCallback( + final OnMapChangedCallback, 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 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, K, V> listener) { + if (listeners != null) + listeners.remove(listener); + } +} -- cgit v1.2.3