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 {
google()
jcenter()
maven { url 'https://jitpack.io' }
}
}

View File

@@ -34,6 +34,7 @@ dependencies {
implementation "com.android.support:design:$supportLibraryVersion"
implementation "com.android.support:preference-v14:$supportLibraryVersion"
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.takisoft.fix:preference-v7:$takisoftFixVersion"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"

View File

@@ -69,6 +69,7 @@
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<service android:name=".client.ClientMonitorService"/>
<provider
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>
override fun onIpNeighbourAvailable(neighbours: Map<String, IpNeighbour>) {
this.neighbours = neighbours.values.toList()
override fun onIpNeighbourAvailable(neighbours: List<IpNeighbour>) {
this.neighbours = neighbours
updateNotification()
}
override fun postIpNeighbourAvailable() {
protected fun updateNotification() {
val sizeLookup = neighbours.groupBy { it.dev }.mapValues { (_, neighbours) ->
neighbours
.filter { it.state != IpNeighbour.State.FAILED }

View File

@@ -1,21 +1,39 @@
package be.mygod.vpnhotspot
import android.content.ComponentName
import android.content.ServiceConnection
import android.databinding.DataBindingUtil
import android.os.Bundle
import android.os.IBinder
import android.support.design.internal.BottomNavigationMenuView
import android.support.design.widget.BottomNavigationView
import android.support.v4.app.Fragment
import android.support.v4.content.ContextCompat
import android.support.v7.app.AppCompatActivity
import android.view.Gravity
import android.view.MenuItem
import be.mygod.vpnhotspot.client.ClientMonitorService
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 badge: QBadgeView
private var clients: ClientMonitorService.Binder? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.navigation.setOnNavigationItemSelectedListener(this)
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) {
@@ -43,6 +61,17 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
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) =
supportFragmentManager.beginTransaction().replace(R.id.fragmentHolder, fragment).commitAllowingStateLoss()
}

View File

@@ -1,11 +1,13 @@
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.Bindable
import android.databinding.DataBindingUtil
import android.net.wifi.WifiConfiguration
import android.net.wifi.p2p.WifiP2pDevice
import android.net.wifi.p2p.WifiP2pGroup
import android.os.Bundle
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.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
@@ -23,22 +24,19 @@ import android.view.*
import android.widget.EditText
import android.widget.Toast
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.ListitemClientBinding
import be.mygod.vpnhotspot.net.IpNeighbour
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.WifiP2pDialog
import be.mygod.vpnhotspot.util.ServiceForegroundConnector
import be.mygod.vpnhotspot.util.broadcastReceiver
import be.mygod.vpnhotspot.util.formatAddresses
import java.net.NetworkInterface
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() {
val switchEnabled: Boolean
@Bindable get() = when (binder?.service?.status) {
@@ -76,70 +74,19 @@ class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickL
fun onStatusChanged() {
notifyPropertyChanged(BR.switchEnabled)
notifyPropertyChanged(BR.serviceStarted)
if (binder?.active != true) onGroupChanged()
notifyPropertyChanged(BR.addresses)
}
fun onGroupChanged(group: WifiP2pGroup? = null) {
notifyPropertyChanged(BR.ssid)
p2pInterface = group?.`interface`
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 inner class ClientAdapter : ListAdapter<Client, ClientViewHolder>(ClientDiffCallback) {
var p2p: Collection<WifiP2pDevice> = emptyList()
var neighbours = emptyList<IpNeighbour>()
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 }))
private inner class ClientAdapter : ListAdapter<Client, ClientViewHolder>(Client) {
override fun submitList(list: MutableList<Client>?) {
super.submitList(list)
binding.swipeRefresher.isRefreshing = false
}
@@ -157,12 +104,7 @@ class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickL
private val adapter = ClientAdapter()
private var binder: RepeaterService.Binder? = null
private var p2pInterface: String? = null
private var tetheredInterfaces = emptySet<String>()
private val receiver = broadcastReceiver { _, intent ->
tetheredInterfaces = TetheringManager.getTetheredIfaces(intent.extras).toSet() +
TetheringManager.getLocalOnlyTetheredIfaces(intent.extras)
adapter.recreate()
}
private var clients: ClientMonitorService.Binder? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_repeater, container, false)
@@ -175,28 +117,19 @@ class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickL
IpNeighbourMonitor.instance?.flush()
val binder = binder
if (binder?.active == false) binder.requestGroupUpdate()
adapter.recreate()
}
binding.toolbar.inflateMenu(R.menu.repeater)
binding.toolbar.setOnMenuItemClickListener(this)
ServiceForegroundConnector(this, RepeaterService::class)
ServiceForegroundConnector(this, RepeaterService::class, ClientMonitorService::class)
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?) {
if (service is ClientMonitorService.Binder) {
clients = service
service.clientsChanged[this] = { adapter.submitList(it.toMutableList()) }
return
}
val binder = service as RepeaterService.Binder
this.binder = binder
binder.statusChanged[this] = data::onStatusChanged
@@ -204,10 +137,16 @@ class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickL
}
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
this.binder = null
binder.statusChanged -= this
binder.groupChanged -= this
this.binder = null
data.onStatusChanged()
}
@@ -261,9 +200,4 @@ class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickL
}
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)
receiverRegistered = true
}
postIpNeighbourAvailable()
updateNotification()
}
if (routings.isEmpty()) {
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
monitor.flush()
} else {
synchronized(monitor.neighbours) { callback.onIpNeighbourAvailable(monitor.neighbours) }
callback.postIpNeighbourAvailable()
callback.onIpNeighbourAvailable(synchronized(monitor.neighbours) { monitor.neighbours.values.toList() })
}
}
fun unregisterCallback(callback: Callback) {
@@ -37,8 +36,7 @@ class IpNeighbourMonitor private constructor() : Runnable {
}
interface Callback {
fun onIpNeighbourAvailable(neighbours: Map<String, IpNeighbour>)
fun postIpNeighbourAvailable()
fun onIpNeighbourAvailable(neighbours: List<IpNeighbour>)
}
private val handler = Handler()
@@ -112,11 +110,11 @@ class IpNeighbourMonitor private constructor() : Runnable {
private fun postUpdateLocked() {
if (updatePosted || instance != this) return
handler.post {
synchronized(neighbours) {
for (callback in callbacks) callback.onIpNeighbourAvailable(neighbours)
val neighbours = synchronized(neighbours) {
updatePosted = false
neighbours.values.toList()
}
for (callback in callbacks) callback.postIpNeighbourAvailable()
for (callback in callbacks) callback.onIpNeighbourAvailable(neighbours)
}
updatePosted = true
}

View File

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