Add client count badge

This commit is contained in:
Mygod
2018-05-09 17:38:49 -07:00
parent d7c5dd18a5
commit dad9bc19e3
13 changed files with 227 additions and 104 deletions

View File

@@ -24,6 +24,7 @@ allprojects {
repositories { repositories {
google() google()
jcenter() jcenter()
maven { url 'https://jitpack.io' }
} }
} }

View File

@@ -34,6 +34,7 @@ dependencies {
implementation "com.android.support:design:$supportLibraryVersion" implementation "com.android.support:design:$supportLibraryVersion"
implementation "com.android.support:preference-v14:$supportLibraryVersion" implementation "com.android.support:preference-v14:$supportLibraryVersion"
implementation 'com.android.support.constraint:constraint-layout:1.1.0' implementation 'com.android.support.constraint:constraint-layout:1.1.0'
implementation 'com.github.luongvo:BadgeView:1.1.5'
implementation 'com.linkedin.dexmaker:dexmaker-mockito:2.16.0' implementation 'com.linkedin.dexmaker:dexmaker-mockito:2.16.0'
implementation "com.takisoft.fix:preference-v7:$takisoftFixVersion" implementation "com.takisoft.fix:preference-v7:$takisoftFixVersion"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"

View File

@@ -69,6 +69,7 @@
<action android:name="android.service.quicksettings.action.QS_TILE" /> <action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter> </intent-filter>
</service> </service>
<service android:name=".client.ClientMonitorService"/>
<provider <provider
android:name="android.support.v4.content.FileProvider" android:name="android.support.v4.content.FileProvider"

View File

@@ -9,10 +9,11 @@ abstract class IpNeighbourMonitoringService : Service(), IpNeighbourMonitor.Call
protected abstract val activeIfaces: List<String> protected abstract val activeIfaces: List<String>
override fun onIpNeighbourAvailable(neighbours: Map<String, IpNeighbour>) { override fun onIpNeighbourAvailable(neighbours: List<IpNeighbour>) {
this.neighbours = neighbours.values.toList() this.neighbours = neighbours
updateNotification()
} }
override fun postIpNeighbourAvailable() { protected fun updateNotification() {
val sizeLookup = neighbours.groupBy { it.dev }.mapValues { (_, neighbours) -> val sizeLookup = neighbours.groupBy { it.dev }.mapValues { (_, neighbours) ->
neighbours neighbours
.filter { it.state != IpNeighbour.State.FAILED } .filter { it.state != IpNeighbour.State.FAILED }

View File

@@ -1,21 +1,39 @@
package be.mygod.vpnhotspot package be.mygod.vpnhotspot
import android.content.ComponentName
import android.content.ServiceConnection
import android.databinding.DataBindingUtil import android.databinding.DataBindingUtil
import android.os.Bundle import android.os.Bundle
import android.os.IBinder
import android.support.design.internal.BottomNavigationMenuView
import android.support.design.widget.BottomNavigationView import android.support.design.widget.BottomNavigationView
import android.support.v4.app.Fragment import android.support.v4.app.Fragment
import android.support.v4.content.ContextCompat
import android.support.v7.app.AppCompatActivity import android.support.v7.app.AppCompatActivity
import android.view.Gravity
import android.view.MenuItem import android.view.MenuItem
import be.mygod.vpnhotspot.client.ClientMonitorService
import be.mygod.vpnhotspot.databinding.ActivityMainBinding import be.mygod.vpnhotspot.databinding.ActivityMainBinding
import be.mygod.vpnhotspot.util.ServiceForegroundConnector
import q.rorbin.badgeview.QBadgeView
class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemSelectedListener { class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemSelectedListener, ServiceConnection {
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
private lateinit var badge: QBadgeView
private var clients: ClientMonitorService.Binder? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main) binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.navigation.setOnNavigationItemSelectedListener(this) binding.navigation.setOnNavigationItemSelectedListener(this)
if (savedInstanceState == null) displayFragment(RepeaterFragment()) if (savedInstanceState == null) displayFragment(RepeaterFragment())
badge = QBadgeView(this)
badge.bindTarget((binding.navigation.getChildAt(0) as BottomNavigationMenuView).getChildAt(0))
badge.badgeBackgroundColor = ContextCompat.getColor(this, R.color.colorAccent)
badge.badgeTextColor = ContextCompat.getColor(this, R.color.primary_text_default_material_light)
badge.badgeGravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
badge.setGravityOffset(16f, 0f, true)
ServiceForegroundConnector(this, ClientMonitorService::class)
} }
override fun onNavigationItemSelected(item: MenuItem) = when (item.itemId) { override fun onNavigationItemSelected(item: MenuItem) = when (item.itemId) {
@@ -43,6 +61,17 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
else -> false else -> false
} }
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
clients = service as ClientMonitorService.Binder
service.clientsChanged[this] = { badge.badgeNumber = it.size }
}
override fun onServiceDisconnected(name: ComponentName?) {
val clients = clients ?: return
this.clients = null
clients.clientsChanged -= this
}
private fun displayFragment(fragment: Fragment) = private fun displayFragment(fragment: Fragment) =
supportFragmentManager.beginTransaction().replace(R.id.fragmentHolder, fragment).commitAllowingStateLoss() supportFragmentManager.beginTransaction().replace(R.id.fragmentHolder, fragment).commitAllowingStateLoss()
} }

View File

@@ -1,11 +1,13 @@
package be.mygod.vpnhotspot package be.mygod.vpnhotspot
import android.content.* import android.content.ComponentName
import android.content.DialogInterface
import android.content.Intent
import android.content.ServiceConnection
import android.databinding.BaseObservable import android.databinding.BaseObservable
import android.databinding.Bindable import android.databinding.Bindable
import android.databinding.DataBindingUtil import android.databinding.DataBindingUtil
import android.net.wifi.WifiConfiguration import android.net.wifi.WifiConfiguration
import android.net.wifi.p2p.WifiP2pDevice
import android.net.wifi.p2p.WifiP2pGroup import android.net.wifi.p2p.WifiP2pGroup
import android.os.Bundle import android.os.Bundle
import android.os.IBinder import android.os.IBinder
@@ -14,7 +16,6 @@ import android.support.v4.content.ContextCompat
import android.support.v7.app.AlertDialog import android.support.v7.app.AlertDialog
import android.support.v7.app.AppCompatDialog import android.support.v7.app.AppCompatDialog
import android.support.v7.recyclerview.extensions.ListAdapter import android.support.v7.recyclerview.extensions.ListAdapter
import android.support.v7.util.DiffUtil
import android.support.v7.widget.DefaultItemAnimator import android.support.v7.widget.DefaultItemAnimator
import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView import android.support.v7.widget.RecyclerView
@@ -23,22 +24,19 @@ import android.view.*
import android.widget.EditText import android.widget.EditText
import android.widget.Toast import android.widget.Toast
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.client.Client
import be.mygod.vpnhotspot.client.ClientMonitorService
import be.mygod.vpnhotspot.databinding.FragmentRepeaterBinding import be.mygod.vpnhotspot.databinding.FragmentRepeaterBinding
import be.mygod.vpnhotspot.databinding.ListitemClientBinding import be.mygod.vpnhotspot.databinding.ListitemClientBinding
import be.mygod.vpnhotspot.net.IpNeighbour
import be.mygod.vpnhotspot.net.IpNeighbourMonitor import be.mygod.vpnhotspot.net.IpNeighbourMonitor
import be.mygod.vpnhotspot.net.TetherType
import be.mygod.vpnhotspot.net.TetheringManager
import be.mygod.vpnhotspot.net.wifi.P2pSupplicantConfiguration import be.mygod.vpnhotspot.net.wifi.P2pSupplicantConfiguration
import be.mygod.vpnhotspot.net.wifi.WifiP2pDialog import be.mygod.vpnhotspot.net.wifi.WifiP2pDialog
import be.mygod.vpnhotspot.util.ServiceForegroundConnector import be.mygod.vpnhotspot.util.ServiceForegroundConnector
import be.mygod.vpnhotspot.util.broadcastReceiver
import be.mygod.vpnhotspot.util.formatAddresses import be.mygod.vpnhotspot.util.formatAddresses
import java.net.NetworkInterface import java.net.NetworkInterface
import java.net.SocketException import java.net.SocketException
import java.util.*
class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickListener, IpNeighbourMonitor.Callback { class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickListener {
inner class Data : BaseObservable() { inner class Data : BaseObservable() {
val switchEnabled: Boolean val switchEnabled: Boolean
@Bindable get() = when (binder?.service?.status) { @Bindable get() = when (binder?.service?.status) {
@@ -76,70 +74,19 @@ class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickL
fun onStatusChanged() { fun onStatusChanged() {
notifyPropertyChanged(BR.switchEnabled) notifyPropertyChanged(BR.switchEnabled)
notifyPropertyChanged(BR.serviceStarted) notifyPropertyChanged(BR.serviceStarted)
if (binder?.active != true) onGroupChanged() notifyPropertyChanged(BR.addresses)
} }
fun onGroupChanged(group: WifiP2pGroup? = null) { fun onGroupChanged(group: WifiP2pGroup? = null) {
notifyPropertyChanged(BR.ssid) notifyPropertyChanged(BR.ssid)
p2pInterface = group?.`interface` p2pInterface = group?.`interface`
notifyPropertyChanged(BR.addresses) notifyPropertyChanged(BR.addresses)
adapter.p2p = group?.clientList ?: emptyList()
adapter.recreate()
} }
} }
inner class Client(p2p: WifiP2pDevice? = null, neighbour: IpNeighbour? = null) {
val iface = neighbour?.dev ?: p2pInterface
val mac = p2p?.deviceAddress ?: neighbour?.lladdr!!
val ip = TreeMap<String, IpNeighbour.State>()
val icon get() = TetherType.ofInterface(iface, p2pInterface).icon
val title get() = "$mac%$iface"
val description get() = ip.entries.joinToString("\n") { (ip, state) ->
getString(when (state) {
IpNeighbour.State.INCOMPLETE -> R.string.connected_state_incomplete
IpNeighbour.State.VALID -> R.string.connected_state_valid
IpNeighbour.State.FAILED -> R.string.connected_state_failed
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<Client>() {
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 class ClientViewHolder(val binding: ListitemClientBinding) : RecyclerView.ViewHolder(binding.root)
private inner class ClientAdapter : ListAdapter<Client, ClientViewHolder>(ClientDiffCallback) { private inner class ClientAdapter : ListAdapter<Client, ClientViewHolder>(Client) {
var p2p: Collection<WifiP2pDevice> = emptyList() override fun submitList(list: MutableList<Client>?) {
var neighbours = emptyList<IpNeighbour>() super.submitList(list)
fun recreate() {
val p2p = HashMap(p2p.associateBy({ Pair(p2pInterface, it.deviceAddress) }, { Client(it) }))
for (neighbour in neighbours) {
val key = Pair(neighbour.dev, neighbour.lladdr)
var client = p2p[key]
if (client == null) {
if (!tetheredInterfaces.contains(neighbour.dev)) continue
client = Client(neighbour = neighbour)
p2p[key] = client
}
client.ip += Pair(neighbour.ip, neighbour.state)
}
submitList(p2p.values.sortedWith(compareBy<Client> { it.iface }.thenBy { it.mac }))
binding.swipeRefresher.isRefreshing = false binding.swipeRefresher.isRefreshing = false
} }
@@ -157,12 +104,7 @@ class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickL
private val adapter = ClientAdapter() private val adapter = ClientAdapter()
private var binder: RepeaterService.Binder? = null private var binder: RepeaterService.Binder? = null
private var p2pInterface: String? = null private var p2pInterface: String? = null
private var tetheredInterfaces = emptySet<String>() private var clients: ClientMonitorService.Binder? = null
private val receiver = broadcastReceiver { _, intent ->
tetheredInterfaces = TetheringManager.getTetheredIfaces(intent.extras).toSet() +
TetheringManager.getLocalOnlyTetheredIfaces(intent.extras)
adapter.recreate()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_repeater, container, false) binding = DataBindingUtil.inflate(inflater, R.layout.fragment_repeater, container, false)
@@ -175,28 +117,19 @@ class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickL
IpNeighbourMonitor.instance?.flush() IpNeighbourMonitor.instance?.flush()
val binder = binder val binder = binder
if (binder?.active == false) binder.requestGroupUpdate() if (binder?.active == false) binder.requestGroupUpdate()
adapter.recreate()
} }
binding.toolbar.inflateMenu(R.menu.repeater) binding.toolbar.inflateMenu(R.menu.repeater)
binding.toolbar.setOnMenuItemClickListener(this) binding.toolbar.setOnMenuItemClickListener(this)
ServiceForegroundConnector(this, RepeaterService::class) ServiceForegroundConnector(this, RepeaterService::class, ClientMonitorService::class)
return binding.root return binding.root
} }
override fun onStart() {
super.onStart()
IpNeighbourMonitor.registerCallback(this)
requireContext().registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
}
override fun onStop() {
requireContext().unregisterReceiver(receiver)
IpNeighbourMonitor.unregisterCallback(this)
onServiceDisconnected(null)
super.onStop()
}
override fun onServiceConnected(name: ComponentName?, service: IBinder?) { override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
if (service is ClientMonitorService.Binder) {
clients = service
service.clientsChanged[this] = { adapter.submitList(it.toMutableList()) }
return
}
val binder = service as RepeaterService.Binder val binder = service as RepeaterService.Binder
this.binder = binder this.binder = binder
binder.statusChanged[this] = data::onStatusChanged binder.statusChanged[this] = data::onStatusChanged
@@ -204,10 +137,16 @@ class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickL
} }
override fun onServiceDisconnected(name: ComponentName?) { override fun onServiceDisconnected(name: ComponentName?) {
if (name == ComponentName(requireContext(), ClientMonitorService::class.java)) {
val clients = clients ?: return
this.clients = null
clients.clientsChanged -= this
return
}
val binder = binder ?: return val binder = binder ?: return
this.binder = null
binder.statusChanged -= this binder.statusChanged -= this
binder.groupChanged -= this binder.groupChanged -= this
this.binder = null
data.onStatusChanged() data.onStatusChanged()
} }
@@ -261,9 +200,4 @@ class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickL
} }
Toast.makeText(context, R.string.repeater_configure_failure, Toast.LENGTH_LONG).show() Toast.makeText(context, R.string.repeater_configure_failure, Toast.LENGTH_LONG).show()
} }
override fun onIpNeighbourAvailable(neighbours: Map<String, IpNeighbour>) {
adapter.neighbours = neighbours.values.toList()
}
override fun postIpNeighbourAvailable() = adapter.recreate()
} }

View File

@@ -68,7 +68,7 @@ class TetheringService : IpNeighbourMonitoringService(), VpnMonitor.Callback {
VpnMonitor.registerCallback(this) VpnMonitor.registerCallback(this)
receiverRegistered = true receiverRegistered = true
} }
postIpNeighbourAvailable() updateNotification()
} }
if (routings.isEmpty()) { if (routings.isEmpty()) {
unregisterReceiver() unregisterReceiver()

View File

@@ -0,0 +1,45 @@
package be.mygod.vpnhotspot.client
import android.support.v7.util.DiffUtil
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.net.IpNeighbour
import be.mygod.vpnhotspot.net.TetherType
import java.util.*
abstract class Client {
companion object : DiffUtil.ItemCallback<Client>() {
override fun areItemsTheSame(oldItem: Client, newItem: Client) =
oldItem.iface == newItem.iface && oldItem.mac == newItem.mac
override fun areContentsTheSame(oldItem: Client, newItem: Client) = oldItem == newItem
}
abstract val iface: String
abstract val mac: String
val ip = TreeMap<String, IpNeighbour.State>()
open val icon get() = TetherType.ofInterface(iface).icon
val title get() = "$mac%$iface"
val description get() = ip.entries.joinToString("\n") { (ip, state) ->
app.getString(when (state) {
IpNeighbour.State.INCOMPLETE -> R.string.connected_state_incomplete
IpNeighbour.State.VALID -> R.string.connected_state_valid
IpNeighbour.State.FAILED -> R.string.connected_state_failed
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)
}

View File

@@ -0,0 +1,95 @@
package be.mygod.vpnhotspot.client
import android.app.Service
import android.content.*
import android.net.wifi.p2p.WifiP2pDevice
import android.os.IBinder
import be.mygod.vpnhotspot.RepeaterService
import be.mygod.vpnhotspot.net.IpNeighbour
import be.mygod.vpnhotspot.net.IpNeighbourMonitor
import be.mygod.vpnhotspot.net.TetheringManager
import be.mygod.vpnhotspot.util.StickyEvent1
import be.mygod.vpnhotspot.util.broadcastReceiver
class ClientMonitorService : Service(), ServiceConnection, IpNeighbourMonitor.Callback {
inner class Binder : android.os.Binder() {
val clientsChanged = StickyEvent1 { clients }
}
private val binder = Binder()
override fun onBind(intent: Intent?) = binder
private var tetheredInterfaces = emptySet<String>()
private val receiver = broadcastReceiver { _, intent ->
tetheredInterfaces = TetheringManager.getTetheredIfaces(intent.extras).toSet() +
TetheringManager.getLocalOnlyTetheredIfaces(intent.extras)
populateClients()
}
private var repeater: RepeaterService.Binder? = null
private var p2p: Collection<WifiP2pDevice> = emptyList()
private var neighbours = emptyList<IpNeighbour>()
private var clients = emptyList<Client>()
private set(value) {
field = value
binder.clientsChanged(value)
}
private fun populateClients() {
val clients = HashMap<Pair<String, String>, Client>()
val group = repeater?.service?.group
val p2pInterface = group?.`interface`
if (p2pInterface != null) {
for (client in p2p) clients[Pair(p2pInterface, client.deviceAddress)] = WifiP2pClient(p2pInterface, client)
}
for (neighbour in neighbours) {
val key = Pair(neighbour.dev, neighbour.lladdr)
var client = clients[key]
if (client == null) {
if (!tetheredInterfaces.contains(neighbour.dev)) continue
client = TetheringClient(neighbour)
clients[key] = client
}
client.ip += Pair(neighbour.ip, neighbour.state)
}
this.clients = clients.values.sortedWith(compareBy<Client> { it.iface }.thenBy { it.mac })
}
private fun refreshP2p() {
val repeater = repeater
p2p = (if (repeater?.active != true) null else repeater.service.group?.clientList) ?: emptyList()
populateClients()
}
override fun onCreate() {
super.onCreate()
bindService(Intent(this, RepeaterService::class.java), this, Context.BIND_AUTO_CREATE)
IpNeighbourMonitor.registerCallback(this)
registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
}
override fun onDestroy() {
unregisterReceiver(receiver)
IpNeighbourMonitor.unregisterCallback(this)
unbindService(this)
super.onDestroy()
}
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
val binder = service as RepeaterService.Binder
repeater = binder
binder.statusChanged[this] = this::refreshP2p
binder.groupChanged[this] = { refreshP2p() }
}
override fun onServiceDisconnected(name: ComponentName?) {
val binder = repeater ?: return
repeater = null
binder.statusChanged -= this
binder.groupChanged -= this
}
override fun onIpNeighbourAvailable(neighbours: List<IpNeighbour>) {
this.neighbours = neighbours
populateClients()
}
}

View File

@@ -0,0 +1,8 @@
package be.mygod.vpnhotspot.client
import be.mygod.vpnhotspot.net.IpNeighbour
class TetheringClient(private val neighbour: IpNeighbour) : Client() {
override val iface get() = neighbour.dev
override val mac get() = neighbour.lladdr
}

View File

@@ -0,0 +1,10 @@
package be.mygod.vpnhotspot.client
import android.net.wifi.p2p.WifiP2pDevice
import be.mygod.vpnhotspot.net.TetherType
class WifiP2pClient(p2pInterface: String, p2p: WifiP2pDevice) : Client() {
override val iface = p2pInterface
override val mac = p2p.deviceAddress ?: ""
override val icon: Int get() = TetherType.WIFI_P2P.icon
}

View File

@@ -25,8 +25,7 @@ class IpNeighbourMonitor private constructor() : Runnable {
instance = monitor instance = monitor
monitor.flush() monitor.flush()
} else { } else {
synchronized(monitor.neighbours) { callback.onIpNeighbourAvailable(monitor.neighbours) } callback.onIpNeighbourAvailable(synchronized(monitor.neighbours) { monitor.neighbours.values.toList() })
callback.postIpNeighbourAvailable()
} }
} }
fun unregisterCallback(callback: Callback) { fun unregisterCallback(callback: Callback) {
@@ -37,8 +36,7 @@ class IpNeighbourMonitor private constructor() : Runnable {
} }
interface Callback { interface Callback {
fun onIpNeighbourAvailable(neighbours: Map<String, IpNeighbour>) fun onIpNeighbourAvailable(neighbours: List<IpNeighbour>)
fun postIpNeighbourAvailable()
} }
private val handler = Handler() private val handler = Handler()
@@ -112,11 +110,11 @@ class IpNeighbourMonitor private constructor() : Runnable {
private fun postUpdateLocked() { private fun postUpdateLocked() {
if (updatePosted || instance != this) return if (updatePosted || instance != this) return
handler.post { handler.post {
synchronized(neighbours) { val neighbours = synchronized(neighbours) {
for (callback in callbacks) callback.onIpNeighbourAvailable(neighbours)
updatePosted = false updatePosted = false
neighbours.values.toList()
} }
for (callback in callbacks) callback.postIpNeighbourAvailable() for (callback in callbacks) callback.onIpNeighbourAvailable(neighbours)
} }
updatePosted = true updatePosted = true
} }

View File

@@ -5,7 +5,7 @@
<data> <data>
<variable <variable
name="client" name="client"
type="be.mygod.vpnhotspot.RepeaterFragment.Client"/> type="be.mygod.vpnhotspot.client.Client"/>
</data> </data>
<LinearLayout <LinearLayout