diff --git a/build.gradle b/build.gradle index fb0b52bc..40c277f2 100644 --- a/build.gradle +++ b/build.gradle @@ -4,8 +4,8 @@ buildscript { ext { androidPluginVersion = '3.0.1' kotlinVersion = '1.2.21' - supportLibraryVersion = '27.0.2' - takisoftFixVersion = '27.0.2.0' + supportLibraryVersion = '27.1.0' + takisoftFixVersion = '27.1.0.0' } repositories { google() diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterFragment.kt index a2c75d0d..d071c008 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterFragment.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterFragment.kt @@ -16,6 +16,8 @@ import android.support.v4.content.ContextCompat import android.support.v4.content.LocalBroadcastManager import android.support.v7.app.AlertDialog import android.support.v7.app.AppCompatDialog +import android.support.v7.recyclerview.extensions.ListAdapter +import android.support.v7.util.DiffUtil import android.support.v7.widget.DefaultItemAnimator import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.RecyclerView @@ -101,10 +103,28 @@ class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickL else -> throw IllegalStateException("Invalid IpNeighbour.State: $state") }, ip) } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Client + + if (iface != other.iface) return false + if (mac != other.mac) return false + if (ip != other.ip) return false + + return true + } + override fun hashCode() = Objects.hash(iface, mac, ip) + } + private object ClientDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Client, newItem: Client) = + oldItem.iface == newItem.iface && oldItem.mac == newItem.mac + override fun areContentsTheSame(oldItem: Client, newItem: Client) = oldItem == newItem } private class ClientViewHolder(val binding: ListitemClientBinding) : RecyclerView.ViewHolder(binding.root) - private inner class ClientAdapter : RecyclerView.Adapter() { - private val clients = ArrayList() + private inner class ClientAdapter : ListAdapter(ClientDiffCallback) { var p2p: Collection = emptyList() var neighbours = emptyList() @@ -120,10 +140,7 @@ class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickL } client.ip += Pair(neighbour.ip, neighbour.state) } - clients.clear() - clients.addAll(p2p.values) - clients.sortWith(compareBy { it.iface }.thenBy { it.mac }) - notifyDataSetChanged() // recreate everything + submitList(p2p.values.sortedWith(compareBy { it.iface }.thenBy { it.mac })) binding.swipeRefresher.isRefreshing = false } @@ -131,11 +148,9 @@ class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickL ClientViewHolder(ListitemClientBinding.inflate(LayoutInflater.from(parent.context))) override fun onBindViewHolder(holder: ClientViewHolder, position: Int) { - holder.binding.client = clients[position] + holder.binding.client = getItem(position) holder.binding.executePendingBindings() } - - override fun getItemCount() = clients.size } private lateinit var binding: FragmentRepeaterBinding @@ -153,9 +168,7 @@ class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickL binding = DataBindingUtil.inflate(inflater, R.layout.fragment_repeater, container, false) binding.data = data binding.clients.layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) - val animator = DefaultItemAnimator() - animator.supportsChangeAnimations = false // prevent fading-in/out when rebinding - binding.clients.itemAnimator = animator + binding.clients.itemAnimator = DefaultItemAnimator() binding.clients.adapter = adapter binding.swipeRefresher.setColorSchemeResources(R.color.colorAccent) binding.swipeRefresher.setOnRefreshListener { diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/TetheringFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/TetheringFragment.kt index aa43ddbb..a9b21f32 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/TetheringFragment.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/TetheringFragment.kt @@ -7,7 +7,8 @@ import android.os.Bundle import android.os.IBinder import android.support.v4.app.Fragment import android.support.v4.content.ContextCompat -import android.support.v7.util.SortedList +import android.support.v7.recyclerview.extensions.ListAdapter +import android.support.v7.util.DiffUtil import android.support.v7.widget.DefaultItemAnimator import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.RecyclerView @@ -20,6 +21,7 @@ import be.mygod.vpnhotspot.net.ConnectivityManagerHelper import be.mygod.vpnhotspot.net.TetherType import java.net.NetworkInterface import java.net.SocketException +import java.util.* class TetheringFragment : Fragment(), ServiceConnection { companion object { @@ -27,22 +29,6 @@ class TetheringFragment : Fragment(), ServiceConnection { private const val VIEW_TYPE_MANAGE = 1 } - private abstract class BaseSorter : SortedList.Callback() { - override fun onInserted(position: Int, count: Int) { } - override fun areContentsTheSame(oldItem: T?, newItem: T?): Boolean = oldItem == newItem - override fun onMoved(fromPosition: Int, toPosition: Int) { } - override fun onChanged(position: Int, count: Int) { } - override fun onRemoved(position: Int, count: Int) { } - override fun areItemsTheSame(item1: T?, item2: T?): Boolean = item1 == item2 - override fun compare(o1: T?, o2: T?): Int = - if (o1 == null) if (o2 == null) 0 else 1 else if (o2 == null) -1 else compareNonNull(o1, o2) - abstract fun compareNonNull(o1: T, o2: T): Int - } - private open class DefaultSorter> : BaseSorter() { - override fun compareNonNull(o1: T, o2: T): Int = o1.compareTo(o2) - } - private object TetheredInterfaceSorter : DefaultSorter() - inner class Data(val iface: TetheredInterface) : BaseObservable() { val icon: Int get() = TetherType.ofInterface(iface.name).icon val active = binder?.active?.contains(iface.name) == true @@ -80,10 +66,24 @@ class TetheringFragment : Fragment(), ServiceConnection { val addresses = lookup[name]?.formatAddresses() ?: "" override fun compareTo(other: TetheredInterface) = name.compareTo(other.name) - } - inner class TetheringAdapter : RecyclerView.Adapter() { - private val tethered = SortedList(TetheredInterface::class.java, TetheredInterfaceSorter) + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + other as TetheredInterface + if (name != other.name) return false + if (addresses != other.addresses) return false + return true + } + override fun hashCode(): Int = Objects.hash(name, addresses) + object DiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: TetheredInterface, newItem: TetheredInterface) = + oldItem.name == newItem.name + override fun areContentsTheSame(oldItem: TetheredInterface, newItem: TetheredInterface) = oldItem == newItem + } + } + inner class TetheringAdapter : + ListAdapter(TetheredInterface.DiffCallback) { fun update(data: Set) { val lookup = try { NetworkInterface.getNetworkInterfaces().asSequence().associateBy { it.name } @@ -91,14 +91,12 @@ class TetheringFragment : Fragment(), ServiceConnection { e.printStackTrace() emptyMap() } - tethered.clear() - tethered.addAll(data.map { TetheredInterface(it, lookup) }) - notifyDataSetChanged() + submitList(data.map { TetheredInterface(it, lookup) }.sorted()) } - override fun getItemCount() = tethered.size() + 1 + override fun getItemCount() = super.getItemCount() + 1 override fun getItemViewType(position: Int) = - if (position == tethered.size()) VIEW_TYPE_MANAGE else VIEW_TYPE_INTERFACE + if (position == super.getItemCount()) VIEW_TYPE_MANAGE else VIEW_TYPE_INTERFACE override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val inflater = LayoutInflater.from(parent.context) return when (viewType) { @@ -109,7 +107,7 @@ class TetheringFragment : Fragment(), ServiceConnection { } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { when (holder) { - is InterfaceViewHolder -> holder.binding.data = Data(tethered[position]) + is InterfaceViewHolder -> holder.binding.data = Data(getItem(position)) } } } @@ -124,9 +122,7 @@ class TetheringFragment : Fragment(), ServiceConnection { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { binding = DataBindingUtil.inflate(inflater, R.layout.fragment_tethering, container, false) binding.interfaces.layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) - val animator = DefaultItemAnimator() - animator.supportsChangeAnimations = false // prevent fading-in/out when rebinding - binding.interfaces.itemAnimator = animator + binding.interfaces.itemAnimator = DefaultItemAnimator() binding.interfaces.adapter = adapter return binding.root }