databinding: rewrite in kotlin
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
This commit is contained in:
parent
8669c01eaa
commit
48a9fd46a6
@ -1,150 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.databinding;
|
||||
|
||||
import android.text.InputFilter;
|
||||
import android.view.LayoutInflater;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.wireguard.android.BR;
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler;
|
||||
import com.wireguard.android.util.ObservableKeyedList;
|
||||
import com.wireguard.android.widget.ToggleSwitch;
|
||||
import com.wireguard.android.widget.ToggleSwitch.OnBeforeCheckedChangeListener;
|
||||
import com.wireguard.config.Attribute;
|
||||
import com.wireguard.config.InetNetwork;
|
||||
import com.wireguard.util.Keyed;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.databinding.BindingAdapter;
|
||||
import androidx.databinding.DataBindingUtil;
|
||||
import androidx.databinding.ObservableList;
|
||||
import androidx.databinding.ViewDataBinding;
|
||||
import androidx.databinding.adapters.ListenerUtil;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import java9.util.Optional;
|
||||
|
||||
/**
|
||||
* Static methods for use by generated code in the Android data binding library.
|
||||
*/
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@NonNullForAll
|
||||
public final class BindingAdapters {
|
||||
private BindingAdapters() {
|
||||
// Prevent instantiation.
|
||||
}
|
||||
|
||||
@BindingAdapter("checked")
|
||||
public static void setChecked(final ToggleSwitch view, final boolean checked) {
|
||||
view.setCheckedInternal(checked);
|
||||
}
|
||||
|
||||
@BindingAdapter("filter")
|
||||
public static void setFilter(final TextView view, final InputFilter filter) {
|
||||
view.setFilters(new InputFilter[]{filter});
|
||||
}
|
||||
|
||||
@BindingAdapter({"items", "layout"})
|
||||
public static <E>
|
||||
void setItems(final LinearLayout view,
|
||||
@Nullable final ObservableList<E> oldList, final int oldLayoutId,
|
||||
@Nullable final ObservableList<E> newList, final int newLayoutId) {
|
||||
if (oldList == newList && oldLayoutId == newLayoutId)
|
||||
return;
|
||||
ItemChangeListener<E> 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;
|
||||
// Stop tracking the old listener.
|
||||
ListenerUtil.trackListener(view, null, R.id.item_change_listener);
|
||||
}
|
||||
// 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 <E>
|
||||
void setItems(final LinearLayout view,
|
||||
@Nullable final Iterable<E> oldList, final int oldLayoutId,
|
||||
@Nullable final Iterable<E> newList, final int newLayoutId) {
|
||||
if (oldList == newList && oldLayoutId == newLayoutId)
|
||||
return;
|
||||
view.removeAllViews();
|
||||
if (newList == null)
|
||||
return;
|
||||
final LayoutInflater layoutInflater = LayoutInflater.from(view.getContext());
|
||||
for (final E item : newList) {
|
||||
final ViewDataBinding binding =
|
||||
DataBindingUtil.inflate(layoutInflater, newLayoutId, view, false);
|
||||
binding.setVariable(BR.collection, newList);
|
||||
binding.setVariable(BR.item, item);
|
||||
binding.executePendingBindings();
|
||||
view.addView(binding.getRoot());
|
||||
}
|
||||
}
|
||||
|
||||
@BindingAdapter(requireAll = false, value = {"items", "layout", "configurationHandler"})
|
||||
public static <K, E extends Keyed<? extends K>>
|
||||
void setItems(final RecyclerView view,
|
||||
@Nullable final ObservableKeyedList<K, E> oldList, final int oldLayoutId,
|
||||
final RowConfigurationHandler oldRowConfigurationHandler,
|
||||
@Nullable final ObservableKeyedList<K, E> newList, final int newLayoutId,
|
||||
final RowConfigurationHandler newRowConfigurationHandler) {
|
||||
if (view.getLayoutManager() == null)
|
||||
view.setLayoutManager(new LinearLayoutManager(view.getContext(), RecyclerView.VERTICAL, false));
|
||||
|
||||
if (oldList == newList && oldLayoutId == newLayoutId)
|
||||
return;
|
||||
// The ListAdapter interface is not generic, so this cannot be checked.
|
||||
@SuppressWarnings("unchecked") ObservableKeyedRecyclerViewAdapter<K, E> adapter =
|
||||
(ObservableKeyedRecyclerViewAdapter<K, E>) view.getAdapter();
|
||||
// If the layout changes, any existing adapter must be replaced.
|
||||
if (adapter != null && oldList != null && oldLayoutId != newLayoutId) {
|
||||
adapter.setList(null);
|
||||
adapter = null;
|
||||
}
|
||||
// Avoid setting an adapter when there is no new list or layout.
|
||||
if (newList == null || newLayoutId == 0)
|
||||
return;
|
||||
if (adapter == null) {
|
||||
adapter = new ObservableKeyedRecyclerViewAdapter<>(view.getContext(), newLayoutId, newList);
|
||||
view.setAdapter(adapter);
|
||||
}
|
||||
|
||||
adapter.setRowConfigurationHandler(newRowConfigurationHandler);
|
||||
// Either the list changed, or this is an entirely new listener because the layout changed.
|
||||
adapter.setList(newList);
|
||||
}
|
||||
|
||||
@BindingAdapter("onBeforeCheckedChanged")
|
||||
public static void setOnBeforeCheckedChanged(final ToggleSwitch view,
|
||||
final OnBeforeCheckedChangeListener listener) {
|
||||
view.setOnBeforeCheckedChangeListener(listener);
|
||||
}
|
||||
|
||||
@BindingAdapter("android:text")
|
||||
public static void setText(final TextView view, final Optional<?> text) {
|
||||
view.setText(text.map(Object::toString).orElse(""));
|
||||
}
|
||||
|
||||
@BindingAdapter("android:text")
|
||||
public static void setText(final TextView view, @Nullable final Iterable<InetNetwork> networks) {
|
||||
view.setText(networks != null ? Attribute.join(networks) : "");
|
||||
}
|
||||
}
|
@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package com.wireguard.android.databinding
|
||||
|
||||
import android.text.InputFilter
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.databinding.BindingAdapter
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.ObservableList
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.databinding.adapters.ListenerUtil
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.wireguard.android.BR
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler
|
||||
import com.wireguard.android.widget.ToggleSwitch
|
||||
import com.wireguard.android.widget.ToggleSwitch.OnBeforeCheckedChangeListener
|
||||
import com.wireguard.config.Attribute
|
||||
import com.wireguard.config.InetNetwork
|
||||
import java9.util.Optional
|
||||
|
||||
/**
|
||||
* Static methods for use by generated code in the Android data binding library.
|
||||
*/
|
||||
object BindingAdapters {
|
||||
@JvmStatic
|
||||
@BindingAdapter("checked")
|
||||
fun setChecked(view: ToggleSwitch, checked: Boolean) {
|
||||
view.setCheckedInternal(checked)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@BindingAdapter("filter")
|
||||
fun setFilter(view: TextView, filter: InputFilter) {
|
||||
view.filters = arrayOf(filter)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@BindingAdapter("items", "layout")
|
||||
fun <E> setItems(view: LinearLayout,
|
||||
oldList: ObservableList<E>?, oldLayoutId: Int,
|
||||
newList: ObservableList<E>?, newLayoutId: Int) {
|
||||
if (oldList === newList && oldLayoutId == newLayoutId)
|
||||
return
|
||||
var listener: ItemChangeListener<E>? = 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
|
||||
// Stop tracking the old listener.
|
||||
ListenerUtil.trackListener<Any?>(view, null, R.id.item_change_listener)
|
||||
}
|
||||
// Avoid adding a listener when there is no new list or layout.
|
||||
if (newList == null || newLayoutId == 0)
|
||||
return
|
||||
if (listener == null) {
|
||||
listener = 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)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@BindingAdapter("items", "layout")
|
||||
fun <E> setItems(view: LinearLayout,
|
||||
oldList: Iterable<E>?, oldLayoutId: Int,
|
||||
newList: Iterable<E>?, newLayoutId: Int) {
|
||||
if (oldList === newList && oldLayoutId == newLayoutId)
|
||||
return
|
||||
view.removeAllViews()
|
||||
if (newList == null)
|
||||
return
|
||||
val layoutInflater = LayoutInflater.from(view.context)
|
||||
for (item in newList) {
|
||||
val binding = DataBindingUtil.inflate<ViewDataBinding>(layoutInflater, newLayoutId, view, false)
|
||||
binding.setVariable(BR.collection, newList)
|
||||
binding.setVariable(BR.item, item)
|
||||
binding.executePendingBindings()
|
||||
view.addView(binding.root)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@BindingAdapter(requireAll = false, value = ["items", "layout", "configurationHandler"])
|
||||
fun <K, E : Keyed<out K>> setItems(view: RecyclerView,
|
||||
oldList: ObservableKeyedArrayList<K, E>?, oldLayoutId: Int,
|
||||
oldRowConfigurationHandler: RowConfigurationHandler<*, *>?,
|
||||
newList: ObservableKeyedArrayList<K, E>?, newLayoutId: Int,
|
||||
newRowConfigurationHandler: RowConfigurationHandler<*, *>?) {
|
||||
if (view.layoutManager == null)
|
||||
view.layoutManager = LinearLayoutManager(view.context, RecyclerView.VERTICAL, false)
|
||||
if (oldList === newList && oldLayoutId == newLayoutId)
|
||||
return
|
||||
// The ListAdapter interface is not generic, so this cannot be checked.
|
||||
var adapter = view.adapter as ObservableKeyedRecyclerViewAdapter<K, E>?
|
||||
// If the layout changes, any existing adapter must be replaced.
|
||||
if (adapter != null && oldList != null && oldLayoutId != newLayoutId) {
|
||||
adapter.setList(null)
|
||||
adapter = null
|
||||
}
|
||||
// Avoid setting an adapter when there is no new list or layout.
|
||||
if (newList == null || newLayoutId == 0)
|
||||
return
|
||||
if (adapter == null) {
|
||||
adapter = ObservableKeyedRecyclerViewAdapter(view.context, newLayoutId, newList)
|
||||
view.adapter = adapter
|
||||
}
|
||||
adapter.setRowConfigurationHandler(newRowConfigurationHandler)
|
||||
// Either the list changed, or this is an entirely new listener because the layout changed.
|
||||
adapter.setList(newList)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@BindingAdapter("onBeforeCheckedChanged")
|
||||
fun setOnBeforeCheckedChanged(view: ToggleSwitch,
|
||||
listener: OnBeforeCheckedChangeListener?) {
|
||||
view.setOnBeforeCheckedChangeListener(listener)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@BindingAdapter("android:text")
|
||||
fun setText(view: TextView, text: Optional<*>) {
|
||||
view.text = text.map { obj: Any -> obj.toString() }.orElse("")
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@BindingAdapter("android:text")
|
||||
fun setText(view: TextView, networks: Iterable<InetNetwork?>?) {
|
||||
view.text = if (networks != null) Attribute.join(networks) else ""
|
||||
}
|
||||
}
|
@ -1,143 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.databinding;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.wireguard.android.BR;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Objects;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.databinding.DataBindingUtil;
|
||||
import androidx.databinding.ObservableList;
|
||||
import androidx.databinding.ViewDataBinding;
|
||||
|
||||
/**
|
||||
* Helper class for binding an ObservableList to the children of a ViewGroup.
|
||||
*/
|
||||
|
||||
@NonNullForAll
|
||||
class ItemChangeListener<T> {
|
||||
private final OnListChangedCallback<T> callback = new OnListChangedCallback<>(this);
|
||||
private final ViewGroup container;
|
||||
private final int layoutId;
|
||||
private final LayoutInflater layoutInflater;
|
||||
@Nullable 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, @Nullable final View convertView) {
|
||||
ViewDataBinding binding = convertView != null ? DataBindingUtil.getBinding(convertView) : null;
|
||||
if (binding == null) {
|
||||
binding = DataBindingUtil.inflate(layoutInflater, layoutId, container, false);
|
||||
}
|
||||
|
||||
Objects.requireNonNull(list, "Trying to get a view while list is still null");
|
||||
|
||||
binding.setVariable(BR.collection, list);
|
||||
binding.setVariable(BR.item, list.get(position));
|
||||
binding.executePendingBindings();
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
void setList(@Nullable 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 final 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package com.wireguard.android.databinding
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.ObservableList
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import com.wireguard.android.BR
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
/**
|
||||
* Helper class for binding an ObservableList to the children of a ViewGroup.
|
||||
*/
|
||||
internal class ItemChangeListener<T>(private val container: ViewGroup, private val layoutId: Int) {
|
||||
private val callback = OnListChangedCallback(this)
|
||||
private val layoutInflater: LayoutInflater = LayoutInflater.from(container.context)
|
||||
private var list: ObservableList<T>? = null
|
||||
|
||||
private fun getView(position: Int, convertView: View?): View {
|
||||
var binding = if (convertView != null) DataBindingUtil.getBinding<ViewDataBinding>(convertView) else null
|
||||
if (binding == null) {
|
||||
binding = DataBindingUtil.inflate(layoutInflater, layoutId, container, false)
|
||||
}
|
||||
require(list != null) { "Trying to get a view while list is still null" }
|
||||
binding!!.setVariable(BR.collection, list)
|
||||
binding.setVariable(BR.item, list!![position])
|
||||
binding.executePendingBindings()
|
||||
return binding.root
|
||||
}
|
||||
|
||||
fun setList(newList: ObservableList<T>?) {
|
||||
list?.removeOnListChangedCallback(callback)
|
||||
list = newList
|
||||
if (list != null) {
|
||||
list!!.addOnListChangedCallback(callback)
|
||||
callback.onChanged(list!!)
|
||||
} else {
|
||||
container.removeAllViews()
|
||||
}
|
||||
}
|
||||
|
||||
private class OnListChangedCallback<T> constructor(listener: ItemChangeListener<T>) : ObservableList.OnListChangedCallback<ObservableList<T>>() {
|
||||
private val weakListener: WeakReference<ItemChangeListener<T>> = WeakReference(listener)
|
||||
|
||||
override fun onChanged(sender: ObservableList<T>) {
|
||||
val listener = weakListener.get()
|
||||
if (listener != null) {
|
||||
// TODO: recycle views
|
||||
listener.container.removeAllViews()
|
||||
for (i in sender.indices)
|
||||
listener.container.addView(listener.getView(i, null))
|
||||
} else {
|
||||
sender.removeOnListChangedCallback(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemRangeChanged(sender: ObservableList<T>, positionStart: Int,
|
||||
itemCount: Int) {
|
||||
val listener = weakListener.get()
|
||||
if (listener != null) {
|
||||
for (i in positionStart until positionStart + itemCount) {
|
||||
val child = listener.container.getChildAt(i)
|
||||
listener.container.removeViewAt(i)
|
||||
listener.container.addView(listener.getView(i, child))
|
||||
}
|
||||
} else {
|
||||
sender.removeOnListChangedCallback(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemRangeInserted(sender: ObservableList<T>, positionStart: Int,
|
||||
itemCount: Int) {
|
||||
val listener = weakListener.get()
|
||||
if (listener != null) {
|
||||
for (i in positionStart until positionStart + itemCount)
|
||||
listener.container.addView(listener.getView(i, null))
|
||||
} else {
|
||||
sender.removeOnListChangedCallback(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemRangeMoved(sender: ObservableList<T>, fromPosition: Int,
|
||||
toPosition: Int, itemCount: Int) {
|
||||
val listener = weakListener.get()
|
||||
if (listener != null) {
|
||||
val views = arrayOfNulls<View>(itemCount)
|
||||
for (i in 0 until itemCount) views[i] = listener.container.getChildAt(fromPosition + i)
|
||||
listener.container.removeViews(fromPosition, itemCount)
|
||||
for (i in 0 until itemCount) listener.container.addView(views[i], toPosition + i)
|
||||
} else {
|
||||
sender.removeOnListChangedCallback(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemRangeRemoved(sender: ObservableList<T>, positionStart: Int,
|
||||
itemCount: Int) {
|
||||
val listener = weakListener.get()
|
||||
if (listener != null) {
|
||||
listener.container.removeViews(positionStart, itemCount)
|
||||
} else {
|
||||
sender.removeOnListChangedCallback(this)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
12
ui/src/main/java/com/wireguard/android/databinding/Keyed.kt
Normal file
12
ui/src/main/java/com/wireguard/android/databinding/Keyed.kt
Normal file
@ -0,0 +1,12 @@
|
||||
/*
|
||||
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package com.wireguard.android.databinding
|
||||
|
||||
/**
|
||||
* Interface for objects that have a identifying key of the given type.
|
||||
*/
|
||||
interface Keyed<K> {
|
||||
val key: K
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package com.wireguard.android.databinding
|
||||
|
||||
import androidx.databinding.ObservableArrayList
|
||||
|
||||
/**
|
||||
* ArrayList that allows looking up elements by some key property. As the key property must always
|
||||
* be retrievable, this list cannot hold `null` elements. Because this class places no
|
||||
* restrictions on the order or duplication of keys, lookup by key, as well as all list modification
|
||||
* operations, require O(n) time.
|
||||
*/
|
||||
open class ObservableKeyedArrayList<K, E : Keyed<out K>> : ObservableArrayList<E>() {
|
||||
fun containsKey(key: K) = indexOfKey(key) >= 0
|
||||
|
||||
operator fun get(key: K): E? {
|
||||
val index = indexOfKey(key)
|
||||
return if (index >= 0) get(index) else null
|
||||
}
|
||||
|
||||
open fun indexOfKey(key: K): Int {
|
||||
val iterator = listIterator()
|
||||
while (iterator.hasNext()) {
|
||||
val index = iterator.nextIndex()
|
||||
if (iterator.next()!!.key == key)
|
||||
return index
|
||||
}
|
||||
return -1
|
||||
}
|
||||
}
|
@ -1,162 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.databinding;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.wireguard.android.BR;
|
||||
import com.wireguard.android.util.ObservableKeyedList;
|
||||
import com.wireguard.util.Keyed;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.databinding.DataBindingUtil;
|
||||
import androidx.databinding.ObservableList;
|
||||
import androidx.databinding.ViewDataBinding;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.RecyclerView.Adapter;
|
||||
|
||||
/**
|
||||
* A generic {@code RecyclerView.Adapter} backed by a {@code ObservableKeyedList}.
|
||||
*/
|
||||
|
||||
@NonNullForAll
|
||||
public class ObservableKeyedRecyclerViewAdapter<K, E extends Keyed<? extends K>> extends Adapter<ObservableKeyedRecyclerViewAdapter.ViewHolder> {
|
||||
|
||||
private final OnListChangedCallback<E> callback = new OnListChangedCallback<>(this);
|
||||
private final int layoutId;
|
||||
private final LayoutInflater layoutInflater;
|
||||
@Nullable private ObservableKeyedList<K, E> list;
|
||||
@Nullable private RowConfigurationHandler rowConfigurationHandler;
|
||||
|
||||
ObservableKeyedRecyclerViewAdapter(final Context context, final int layoutId,
|
||||
final ObservableKeyedList<K, E> list) {
|
||||
this.layoutId = layoutId;
|
||||
layoutInflater = LayoutInflater.from(context);
|
||||
setList(list);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private E getItem(final int position) {
|
||||
if (list == null || position < 0 || position >= list.size())
|
||||
return null;
|
||||
return list.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return list != null ? list.size() : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(final int position) {
|
||||
final K key = getKey(position);
|
||||
return key != null ? key.hashCode() : -1;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private K getKey(final int position) {
|
||||
final E item = getItem(position);
|
||||
return item != null ? item.getKey() : null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public void onBindViewHolder(final ViewHolder holder, final int position) {
|
||||
holder.binding.setVariable(BR.collection, list);
|
||||
holder.binding.setVariable(BR.key, getKey(position));
|
||||
holder.binding.setVariable(BR.item, getItem(position));
|
||||
holder.binding.executePendingBindings();
|
||||
|
||||
if (rowConfigurationHandler != null) {
|
||||
final E item = getItem(position);
|
||||
if (item != null) {
|
||||
rowConfigurationHandler.onConfigureRow(holder.binding, item, position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
|
||||
return new ViewHolder(DataBindingUtil.inflate(layoutInflater, layoutId, parent, false));
|
||||
}
|
||||
|
||||
void setList(@Nullable final ObservableKeyedList<K, E> newList) {
|
||||
if (list != null)
|
||||
list.removeOnListChangedCallback(callback);
|
||||
list = newList;
|
||||
if (list != null) {
|
||||
list.addOnListChangedCallback(callback);
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
void setRowConfigurationHandler(final RowConfigurationHandler rowConfigurationHandler) {
|
||||
this.rowConfigurationHandler = rowConfigurationHandler;
|
||||
}
|
||||
|
||||
public interface RowConfigurationHandler<B extends ViewDataBinding, T> {
|
||||
void onConfigureRow(B binding, T item, int position);
|
||||
}
|
||||
|
||||
private static final class OnListChangedCallback<E extends Keyed<?>>
|
||||
extends ObservableList.OnListChangedCallback<ObservableList<E>> {
|
||||
|
||||
private final WeakReference<ObservableKeyedRecyclerViewAdapter<?, E>> weakAdapter;
|
||||
|
||||
private OnListChangedCallback(final ObservableKeyedRecyclerViewAdapter<?, E> adapter) {
|
||||
weakAdapter = new WeakReference<>(adapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChanged(final ObservableList<E> sender) {
|
||||
final ObservableKeyedRecyclerViewAdapter adapter = weakAdapter.get();
|
||||
if (adapter != null)
|
||||
adapter.notifyDataSetChanged();
|
||||
else
|
||||
sender.removeOnListChangedCallback(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeChanged(final ObservableList<E> sender, final int positionStart,
|
||||
final int itemCount) {
|
||||
onChanged(sender);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeInserted(final ObservableList<E> sender, final int positionStart,
|
||||
final int itemCount) {
|
||||
onChanged(sender);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeMoved(final ObservableList<E> sender, final int fromPosition,
|
||||
final int toPosition, final int itemCount) {
|
||||
onChanged(sender);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeRemoved(final ObservableList<E> sender, final int positionStart,
|
||||
final int itemCount) {
|
||||
onChanged(sender);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
final ViewDataBinding binding;
|
||||
|
||||
public ViewHolder(final ViewDataBinding binding) {
|
||||
super(binding.getRoot());
|
||||
|
||||
this.binding = binding;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package com.wireguard.android.databinding
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.ObservableList
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.wireguard.android.BR
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
/**
|
||||
* A generic `RecyclerView.Adapter` backed by a `ObservableKeyedArrayList`.
|
||||
*/
|
||||
class ObservableKeyedRecyclerViewAdapter<K, E : Keyed<out K>> internal constructor(context: Context, private val layoutId: Int,
|
||||
list: ObservableKeyedArrayList<K, E>?) : RecyclerView.Adapter<ObservableKeyedRecyclerViewAdapter.ViewHolder>() {
|
||||
private val callback = OnListChangedCallback(this)
|
||||
private val layoutInflater: LayoutInflater = LayoutInflater.from(context)
|
||||
private var list: ObservableKeyedArrayList<K, E>? = null
|
||||
private var rowConfigurationHandler: RowConfigurationHandler<ViewDataBinding, Any>? = null
|
||||
|
||||
private fun getItem(position: Int): E? = if (list == null || position < 0 || position >= list!!.size) null else list?.get(position)
|
||||
|
||||
override fun getItemCount() = list?.size ?: 0
|
||||
|
||||
override fun getItemId(position: Int) = (getKey(position)?.hashCode() ?: -1).toLong()
|
||||
|
||||
private fun getKey(position: Int): K? = getItem(position)?.key
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.binding.setVariable(BR.collection, list)
|
||||
holder.binding.setVariable(BR.key, getKey(position))
|
||||
holder.binding.setVariable(BR.item, getItem(position))
|
||||
holder.binding.executePendingBindings()
|
||||
if (rowConfigurationHandler != null) {
|
||||
val item = getItem(position)
|
||||
if (item != null) {
|
||||
rowConfigurationHandler?.onConfigureRow(holder.binding, item, position)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(DataBindingUtil.inflate(layoutInflater, layoutId, parent, false))
|
||||
|
||||
fun setList(newList: ObservableKeyedArrayList<K, E>?) {
|
||||
list?.removeOnListChangedCallback(callback)
|
||||
list = newList
|
||||
list?.addOnListChangedCallback(callback)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun setRowConfigurationHandler(rowConfigurationHandler: RowConfigurationHandler<*, *>?) {
|
||||
this.rowConfigurationHandler = rowConfigurationHandler as? RowConfigurationHandler<ViewDataBinding, Any>
|
||||
}
|
||||
|
||||
interface RowConfigurationHandler<B : ViewDataBinding, T> {
|
||||
fun onConfigureRow(binding: B, item: T, position: Int)
|
||||
}
|
||||
|
||||
private class OnListChangedCallback<E : Keyed<*>> constructor(adapter: ObservableKeyedRecyclerViewAdapter<*, E>) : ObservableList.OnListChangedCallback<ObservableList<E>>() {
|
||||
private val weakAdapter: WeakReference<ObservableKeyedRecyclerViewAdapter<*, E>> = WeakReference(adapter)
|
||||
|
||||
override fun onChanged(sender: ObservableList<E>) {
|
||||
val adapter = weakAdapter.get()
|
||||
if (adapter != null)
|
||||
adapter.notifyDataSetChanged()
|
||||
else
|
||||
sender.removeOnListChangedCallback(this)
|
||||
}
|
||||
|
||||
override fun onItemRangeChanged(sender: ObservableList<E>, positionStart: Int,
|
||||
itemCount: Int) {
|
||||
onChanged(sender)
|
||||
}
|
||||
|
||||
override fun onItemRangeInserted(sender: ObservableList<E>, positionStart: Int,
|
||||
itemCount: Int) {
|
||||
onChanged(sender)
|
||||
}
|
||||
|
||||
override fun onItemRangeMoved(sender: ObservableList<E>, fromPosition: Int,
|
||||
toPosition: Int, itemCount: Int) {
|
||||
onChanged(sender)
|
||||
}
|
||||
|
||||
override fun onItemRangeRemoved(sender: ObservableList<E>, positionStart: Int,
|
||||
itemCount: Int) {
|
||||
onChanged(sender)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ViewHolder(val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
init {
|
||||
setList(list)
|
||||
}
|
||||
}
|
@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package com.wireguard.android.databinding
|
||||
|
||||
import java.util.AbstractList
|
||||
import java.util.Collections
|
||||
import java.util.Comparator
|
||||
import java.util.NoSuchElementException
|
||||
import java.util.Spliterator
|
||||
|
||||
/**
|
||||
* KeyedArrayList that enforces uniqueness and sorted order across the set of keys. This class uses
|
||||
* binary search to improve lookup and replacement times to O(log(n)). However, due to the
|
||||
* array-based nature of this class, insertion and removal of elements with anything but the largest
|
||||
* key still require O(n) time.
|
||||
*/
|
||||
class ObservableSortedKeyedArrayList<K, E : Keyed<out K>>(private val comparator: Comparator<in K>?) : ObservableKeyedArrayList<K, E>() {
|
||||
@Transient
|
||||
private val keyList = KeyList(this)
|
||||
|
||||
override fun add(element: E): Boolean {
|
||||
val insertionPoint = getInsertionPoint(element)
|
||||
if (insertionPoint < 0) {
|
||||
// Skipping insertion is non-destructive if the new and existing objects are the same.
|
||||
if (element === get(-insertionPoint - 1)) return false
|
||||
throw IllegalArgumentException("Element with same key already exists in list")
|
||||
}
|
||||
super.add(insertionPoint, element)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun add(index: Int, element: E) {
|
||||
val insertionPoint = getInsertionPoint(element)
|
||||
require(insertionPoint >= 0) { "Element with same key already exists in list" }
|
||||
if (insertionPoint != index) throw IndexOutOfBoundsException("Wrong index given for element")
|
||||
super.add(index, element)
|
||||
}
|
||||
|
||||
override fun addAll(elements: Collection<E>): Boolean {
|
||||
var didChange = false
|
||||
for (e in elements) {
|
||||
if (add(e))
|
||||
didChange = true
|
||||
}
|
||||
return didChange
|
||||
}
|
||||
|
||||
override fun addAll(index: Int, elements: Collection<E>): Boolean {
|
||||
var i = index
|
||||
for (e in elements)
|
||||
add(i++, e)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun getInsertionPoint(e: E): Int {
|
||||
return if (comparator != null) {
|
||||
-Collections.binarySearch(keyList, e.key, comparator) - 1
|
||||
} else {
|
||||
val list = keyList as List<Comparable<K>>
|
||||
-Collections.binarySearch(list, e.key) - 1
|
||||
}
|
||||
}
|
||||
|
||||
override fun indexOfKey(key: K): Int {
|
||||
val index: Int
|
||||
index = if (comparator != null) {
|
||||
Collections.binarySearch(keyList, key, comparator)
|
||||
} else {
|
||||
val list = keyList as List<Comparable<K>>
|
||||
Collections.binarySearch(list, key)
|
||||
}
|
||||
return if (index >= 0) index else -1
|
||||
}
|
||||
|
||||
override fun set(index: Int, element: E): E {
|
||||
val order: Int
|
||||
order = if (comparator != null) {
|
||||
comparator.compare(element.key, get(index).key)
|
||||
} else {
|
||||
val key = element.key as Comparable<K>
|
||||
key.compareTo(get(index).key)
|
||||
}
|
||||
if (order != 0) {
|
||||
// Allow replacement if the new key would be inserted adjacent to the replaced element.
|
||||
val insertionPoint = getInsertionPoint(element)
|
||||
if (insertionPoint < index || insertionPoint > index + 1)
|
||||
throw IndexOutOfBoundsException("Wrong index given for element")
|
||||
}
|
||||
return super.set(index, element)
|
||||
}
|
||||
|
||||
fun values(): Collection<E> {
|
||||
return this
|
||||
}
|
||||
|
||||
private class KeyList<K, E : Keyed<out K>>(private val list: ObservableSortedKeyedArrayList<K, E>) : AbstractList<K>(), Set<K> {
|
||||
override fun get(index: Int): K = list[index].key
|
||||
|
||||
override val size
|
||||
get() = list.size
|
||||
|
||||
override fun spliterator(): Spliterator<K> = super<AbstractList>.spliterator()
|
||||
}
|
||||
}
|
@ -17,11 +17,10 @@ import com.wireguard.android.R
|
||||
import com.wireguard.android.databinding.AppListDialogFragmentBinding
|
||||
import com.wireguard.android.model.ApplicationData
|
||||
import com.wireguard.android.util.ErrorMessages
|
||||
import com.wireguard.android.util.ObservableKeyedArrayList
|
||||
import com.wireguard.android.util.ObservableKeyedList
|
||||
import com.wireguard.android.databinding.ObservableKeyedArrayList
|
||||
|
||||
class AppListDialogFragment : DialogFragment() {
|
||||
private val appData: ObservableKeyedList<String, ApplicationData> = ObservableKeyedArrayList()
|
||||
private val appData: ObservableKeyedArrayList<String, ApplicationData> = ObservableKeyedArrayList()
|
||||
private var currentlyExcludedApps = emptyList<String>()
|
||||
|
||||
private fun loadData() {
|
||||
|
@ -267,11 +267,13 @@ class TunnelListFragment : BaseFragment() {
|
||||
binding ?: return
|
||||
binding!!.fragment = this
|
||||
Application.getTunnelManager().tunnels.thenAccept { tunnels -> binding!!.tunnels = tunnels }
|
||||
binding!!.rowConfigurationHandler = RowConfigurationHandler { binding: TunnelListItemBinding, tunnel: ObservableTunnel, position ->
|
||||
binding.fragment = this
|
||||
val parent = this
|
||||
binding!!.rowConfigurationHandler = object : RowConfigurationHandler<TunnelListItemBinding, ObservableTunnel> {
|
||||
override fun onConfigureRow(binding: TunnelListItemBinding, item: ObservableTunnel, position: Int) {
|
||||
binding.fragment = parent
|
||||
binding.root.setOnClickListener {
|
||||
if (actionMode == null) {
|
||||
selectedTunnel = tunnel
|
||||
selectedTunnel = item
|
||||
} else {
|
||||
actionModeListener.toggleItemChecked(position)
|
||||
}
|
||||
@ -283,7 +285,8 @@ class TunnelListFragment : BaseFragment() {
|
||||
if (actionMode != null)
|
||||
(binding.root as MultiselectableRelativeLayout).setMultiSelected(actionModeListener.checkedItems.contains(position))
|
||||
else
|
||||
(binding.root as MultiselectableRelativeLayout).setSingleSelected(selectedTunnel == tunnel)
|
||||
(binding.root as MultiselectableRelativeLayout).setSingleSelected(selectedTunnel == item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,12 +8,10 @@ import android.graphics.drawable.Drawable
|
||||
import androidx.databinding.BaseObservable
|
||||
import androidx.databinding.Bindable
|
||||
import com.wireguard.android.BR
|
||||
import com.wireguard.util.Keyed
|
||||
import com.wireguard.android.databinding.Keyed
|
||||
|
||||
class ApplicationData(val icon: Drawable, val name: String, val packageName: String, isExcludedFromTunnel: Boolean) : BaseObservable(), Keyed<String> {
|
||||
override fun getKey(): String {
|
||||
return name
|
||||
}
|
||||
override val key = name
|
||||
|
||||
@get:Bindable
|
||||
var isExcludedFromTunnel = isExcludedFromTunnel
|
||||
|
@ -11,7 +11,7 @@ import com.wireguard.android.backend.Statistics
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.wireguard.android.util.ExceptionLoggers
|
||||
import com.wireguard.config.Config
|
||||
import com.wireguard.util.Keyed
|
||||
import com.wireguard.android.databinding.Keyed
|
||||
import java9.util.concurrent.CompletableFuture
|
||||
import java9.util.concurrent.CompletionStage
|
||||
|
||||
@ -24,8 +24,7 @@ class ObservableTunnel internal constructor(
|
||||
config: Config?,
|
||||
state: Tunnel.State
|
||||
) : BaseObservable(), Keyed<String>, Tunnel {
|
||||
override fun getKey() = name
|
||||
|
||||
override val key = name
|
||||
|
||||
@Bindable
|
||||
override fun getName() = name
|
||||
|
@ -22,8 +22,7 @@ import com.wireguard.android.backend.Statistics
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.wireguard.android.configStore.ConfigStore
|
||||
import com.wireguard.android.util.ExceptionLoggers
|
||||
import com.wireguard.android.util.ObservableSortedKeyedArrayList
|
||||
import com.wireguard.android.util.ObservableSortedKeyedList
|
||||
import com.wireguard.android.databinding.ObservableSortedKeyedArrayList
|
||||
import com.wireguard.config.Config
|
||||
import java9.util.Comparators
|
||||
import java9.util.concurrent.CompletableFuture
|
||||
@ -34,10 +33,10 @@ import java.util.ArrayList
|
||||
* Maintains and mediates changes to the set of available WireGuard tunnels,
|
||||
*/
|
||||
class TunnelManager(private val configStore: ConfigStore) : BaseObservable() {
|
||||
val tunnels = CompletableFuture<ObservableSortedKeyedList<String, ObservableTunnel>>()
|
||||
val tunnels = CompletableFuture<ObservableSortedKeyedArrayList<String, ObservableTunnel>>()
|
||||
private val context: Context = get()
|
||||
private val delayedLoadRestoreTunnels = ArrayList<CompletableFuture<Void>>()
|
||||
private val tunnelMap: ObservableSortedKeyedList<String, ObservableTunnel> = ObservableSortedKeyedArrayList(COMPARATOR)
|
||||
private val tunnelMap: ObservableSortedKeyedArrayList<String, ObservableTunnel> = ObservableSortedKeyedArrayList(COMPARATOR)
|
||||
private var haveLoaded = false
|
||||
|
||||
private fun addToList(name: String, config: Config?, state: Tunnel.State): ObservableTunnel? {
|
||||
|
@ -1,111 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.util;
|
||||
|
||||
import com.wireguard.util.Keyed;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.ListIterator;
|
||||
import java.util.Objects;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.databinding.ObservableArrayList;
|
||||
|
||||
/**
|
||||
* ArrayList that allows looking up elements by some key property. As the key property must always
|
||||
* be retrievable, this list cannot hold {@code null} elements. Because this class places no
|
||||
* restrictions on the order or duplication of keys, lookup by key, as well as all list modification
|
||||
* operations, require O(n) time.
|
||||
*/
|
||||
|
||||
@NonNullForAll
|
||||
public class ObservableKeyedArrayList<K, E extends Keyed<? extends K>>
|
||||
extends ObservableArrayList<E> implements ObservableKeyedList<K, E> {
|
||||
@Override
|
||||
public boolean add(@Nullable final E e) {
|
||||
if (e == null)
|
||||
throw new NullPointerException("Trying to add a null element");
|
||||
return super.add(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(final int index, @Nullable final E e) {
|
||||
if (e == null)
|
||||
throw new NullPointerException("Trying to add a null element");
|
||||
super.add(index, e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addAll(final Collection<? extends E> c) {
|
||||
if (c.contains(null))
|
||||
throw new NullPointerException("Trying to add a collection with null element(s)");
|
||||
return super.addAll(c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addAll(final int index, final Collection<? extends E> c) {
|
||||
if (c.contains(null))
|
||||
throw new NullPointerException("Trying to add a collection with null element(s)");
|
||||
return super.addAll(index, c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsAllKeys(final Collection<K> keys) {
|
||||
for (final K key : keys)
|
||||
if (!containsKey(key))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsKey(final K key) {
|
||||
return indexOfKey(key) >= 0;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public E get(final K key) {
|
||||
final int index = indexOfKey(key);
|
||||
return index >= 0 ? get(index) : null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public E getLast(final K key) {
|
||||
final int index = lastIndexOfKey(key);
|
||||
return index >= 0 ? get(index) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int indexOfKey(final K key) {
|
||||
final ListIterator<E> iterator = listIterator();
|
||||
while (iterator.hasNext()) {
|
||||
final int index = iterator.nextIndex();
|
||||
if (Objects.equals(iterator.next().getKey(), key))
|
||||
return index;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int lastIndexOfKey(final K key) {
|
||||
final ListIterator<E> iterator = listIterator(size());
|
||||
while (iterator.hasPrevious()) {
|
||||
final int index = iterator.previousIndex();
|
||||
if (Objects.equals(iterator.previous().getKey(), key))
|
||||
return index;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public E set(final int index, @Nullable final E e) {
|
||||
if (e == null)
|
||||
throw new NullPointerException("Trying to set a null key");
|
||||
return super.set(index, e);
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.util;
|
||||
|
||||
import com.wireguard.util.Keyed;
|
||||
import com.wireguard.util.KeyedList;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import androidx.databinding.ObservableList;
|
||||
|
||||
/**
|
||||
* A list that is both keyed and observable.
|
||||
*/
|
||||
|
||||
@NonNullForAll
|
||||
public interface ObservableKeyedList<K, E extends Keyed<? extends K>>
|
||||
extends KeyedList<K, E>, ObservableList<E> {
|
||||
}
|
@ -1,200 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.util;
|
||||
|
||||
import com.wireguard.util.Keyed;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
import com.wireguard.util.SortedKeyedList;
|
||||
|
||||
import java.util.AbstractList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Set;
|
||||
import java.util.Spliterator;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* KeyedArrayList that enforces uniqueness and sorted order across the set of keys. This class uses
|
||||
* binary search to improve lookup and replacement times to O(log(n)). However, due to the
|
||||
* array-based nature of this class, insertion and removal of elements with anything but the largest
|
||||
* key still require O(n) time.
|
||||
*/
|
||||
|
||||
@NonNullForAll
|
||||
public class ObservableSortedKeyedArrayList<K, E extends Keyed<? extends K>>
|
||||
extends ObservableKeyedArrayList<K, E> implements ObservableSortedKeyedList<K, E> {
|
||||
@Nullable private final Comparator<? super K> comparator;
|
||||
private final transient KeyList<K, E> keyList = new KeyList<>(this);
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public ObservableSortedKeyedArrayList() {
|
||||
comparator = null;
|
||||
}
|
||||
|
||||
public ObservableSortedKeyedArrayList(final Comparator<? super K> comparator) {
|
||||
this.comparator = comparator;
|
||||
}
|
||||
|
||||
public ObservableSortedKeyedArrayList(final Collection<? extends E> c) {
|
||||
this();
|
||||
addAll(c);
|
||||
}
|
||||
|
||||
public ObservableSortedKeyedArrayList(final SortedKeyedList<K, E> other) {
|
||||
this(other.comparator());
|
||||
addAll(other);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean add(final E e) {
|
||||
final int insertionPoint = getInsertionPoint(e);
|
||||
if (insertionPoint < 0) {
|
||||
// Skipping insertion is non-destructive if the new and existing objects are the same.
|
||||
if (e == get(-insertionPoint - 1))
|
||||
return false;
|
||||
throw new IllegalArgumentException("Element with same key already exists in list");
|
||||
}
|
||||
super.add(insertionPoint, e);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(final int index, final E e) {
|
||||
final int insertionPoint = getInsertionPoint(e);
|
||||
if (insertionPoint < 0)
|
||||
throw new IllegalArgumentException("Element with same key already exists in list");
|
||||
if (insertionPoint != index)
|
||||
throw new IndexOutOfBoundsException("Wrong index given for element");
|
||||
super.add(index, e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addAll(final Collection<? extends E> c) {
|
||||
boolean didChange = false;
|
||||
for (final E e : c)
|
||||
if (add(e))
|
||||
didChange = true;
|
||||
return didChange;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addAll(int index, final Collection<? extends E> c) {
|
||||
for (final E e : c)
|
||||
add(index++, e);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Comparator<? super K> comparator() {
|
||||
return comparator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public K firstKey() {
|
||||
if (isEmpty())
|
||||
// The parameter in the exception is only to shut
|
||||
// lint up, we never care for the exception message.
|
||||
throw new NoSuchElementException("Empty set");
|
||||
return get(0).getKey();
|
||||
}
|
||||
|
||||
private int getInsertionPoint(final E e) {
|
||||
if (comparator != null) {
|
||||
return -Collections.binarySearch(keyList, e.getKey(), comparator) - 1;
|
||||
} else {
|
||||
@SuppressWarnings("unchecked") final List<Comparable<? super K>> list =
|
||||
(List<Comparable<? super K>>) keyList;
|
||||
return -Collections.binarySearch(list, e.getKey()) - 1;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int indexOfKey(final K key) {
|
||||
final int index;
|
||||
if (comparator != null) {
|
||||
index = Collections.binarySearch(keyList, key, comparator);
|
||||
} else {
|
||||
@SuppressWarnings("unchecked") final List<Comparable<? super K>> list =
|
||||
(List<Comparable<? super K>>) keyList;
|
||||
index = Collections.binarySearch(list, key);
|
||||
}
|
||||
return index >= 0 ? index : -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<K> keySet() {
|
||||
return keyList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int lastIndexOfKey(final K key) {
|
||||
// There can never be more than one element with the same key in the list.
|
||||
return indexOfKey(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public K lastKey() {
|
||||
if (isEmpty())
|
||||
// The parameter in the exception is only to shut
|
||||
// lint up, we never care for the exception message.
|
||||
throw new NoSuchElementException("Empty set");
|
||||
return get(size() - 1).getKey();
|
||||
}
|
||||
|
||||
@Override
|
||||
public E set(final int index, final E e) {
|
||||
final int order;
|
||||
if (comparator != null) {
|
||||
order = comparator.compare(e.getKey(), get(index).getKey());
|
||||
} else {
|
||||
@SuppressWarnings("unchecked") final Comparable<? super K> key =
|
||||
(Comparable<? super K>) e.getKey();
|
||||
order = key.compareTo(get(index).getKey());
|
||||
}
|
||||
if (order != 0) {
|
||||
// Allow replacement if the new key would be inserted adjacent to the replaced element.
|
||||
final int insertionPoint = getInsertionPoint(e);
|
||||
if (insertionPoint < index || insertionPoint > index + 1)
|
||||
throw new IndexOutOfBoundsException("Wrong index given for element");
|
||||
}
|
||||
return super.set(index, e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<E> values() {
|
||||
return this;
|
||||
}
|
||||
|
||||
private static final class KeyList<K, E extends Keyed<? extends K>>
|
||||
extends AbstractList<K> implements Set<K> {
|
||||
private final ObservableSortedKeyedArrayList<K, E> list;
|
||||
|
||||
private KeyList(final ObservableSortedKeyedArrayList<K, E> list) {
|
||||
this.list = list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public K get(final int index) {
|
||||
return list.get(index).getKey();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return list.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("EmptyMethod")
|
||||
public Spliterator<K> spliterator() {
|
||||
return super.spliterator();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.util;
|
||||
|
||||
import com.wireguard.util.Keyed;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
import com.wireguard.util.SortedKeyedList;
|
||||
|
||||
/**
|
||||
* A list that is both sorted/keyed and observable.
|
||||
*/
|
||||
|
||||
@NonNullForAll
|
||||
public interface ObservableSortedKeyedList<K, E extends Keyed<? extends K>>
|
||||
extends ObservableKeyedList<K, E>, SortedKeyedList<K, E> {
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.util;
|
||||
|
||||
/**
|
||||
* Interface for objects that have a identifying key of the given type.
|
||||
*/
|
||||
|
||||
@NonNullForAll
|
||||
public interface Keyed<K> {
|
||||
K getKey();
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.util;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* A list containing elements that can be looked up by key. A {@code KeyedList} cannot contain
|
||||
* {@code null} elements.
|
||||
*/
|
||||
|
||||
@NonNullForAll
|
||||
public interface KeyedList<K, E extends Keyed<? extends K>> extends List<E> {
|
||||
boolean containsAllKeys(Collection<K> keys);
|
||||
|
||||
boolean containsKey(K key);
|
||||
|
||||
@Nullable
|
||||
E get(K key);
|
||||
|
||||
@Nullable
|
||||
E getLast(K key);
|
||||
|
||||
int indexOfKey(K key);
|
||||
|
||||
int lastIndexOfKey(K key);
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.util;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.Set;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* A keyed list where all elements are sorted by the comparator returned by {@code comparator()}
|
||||
* applied to their keys.
|
||||
*/
|
||||
|
||||
@NonNullForAll
|
||||
public interface SortedKeyedList<K, E extends Keyed<? extends K>> extends KeyedList<K, E> {
|
||||
Comparator<? super K> comparator();
|
||||
|
||||
@Nullable
|
||||
K firstKey();
|
||||
|
||||
Set<K> keySet();
|
||||
|
||||
@Nullable
|
||||
K lastKey();
|
||||
|
||||
Collection<E> values();
|
||||
}
|
@ -15,7 +15,7 @@
|
||||
|
||||
<variable
|
||||
name="appData"
|
||||
type="com.wireguard.android.util.ObservableKeyedList<String, ApplicationData>" />
|
||||
type="com.wireguard.android.databinding.ObservableKeyedArrayList<String, ApplicationData>" />
|
||||
</data>
|
||||
|
||||
<FrameLayout
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
<variable
|
||||
name="collection"
|
||||
type="com.wireguard.android.util.ObservableKeyedList<String, com.wireguard.android.model.ApplicationData>" />
|
||||
type="com.wireguard.android.databinding.ObservableKeyedArrayList<String, com.wireguard.android.model.ApplicationData>" />
|
||||
|
||||
<variable
|
||||
name="key"
|
||||
|
@ -17,7 +17,7 @@
|
||||
|
||||
<variable
|
||||
name="tunnels"
|
||||
type="com.wireguard.android.util.ObservableKeyedList<String, ObservableTunnel>" />
|
||||
type="com.wireguard.android.databinding.ObservableKeyedArrayList<String, ObservableTunnel>" />
|
||||
</data>
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
|
@ -11,7 +11,7 @@
|
||||
|
||||
<variable
|
||||
name="collection"
|
||||
type="com.wireguard.android.util.ObservableKeyedList<String, ObservableTunnel>" />
|
||||
type="com.wireguard.android.databinding.ObservableKeyedArrayList<String, ObservableTunnel>" />
|
||||
|
||||
<variable
|
||||
name="key"
|
||||
|
Loading…
Reference in New Issue
Block a user