Add client count badge
This commit is contained in:
@@ -24,6 +24,7 @@ allprojects {
|
|||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
jcenter()
|
jcenter()
|
||||||
|
maven { url 'https://jitpack.io' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
45
mobile/src/main/java/be/mygod/vpnhotspot/client/Client.kt
Normal file
45
mobile/src/main/java/be/mygod/vpnhotspot/client/Client.kt
Normal 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)
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user