From dad9bc19e3823803c8caf9e1c7d2c7fddb740c0f Mon Sep 17 00:00:00 2001 From: Mygod Date: Wed, 9 May 2018 17:38:49 -0700 Subject: [PATCH] Add client count badge --- build.gradle | 1 + mobile/build.gradle | 1 + mobile/src/main/AndroidManifest.xml | 1 + .../IpNeighbourMonitoringService.kt | 7 +- .../java/be/mygod/vpnhotspot/MainActivity.kt | 31 ++++- .../be/mygod/vpnhotspot/RepeaterFragment.kt | 116 ++++-------------- .../be/mygod/vpnhotspot/TetheringService.kt | 2 +- .../java/be/mygod/vpnhotspot/client/Client.kt | 45 +++++++ .../vpnhotspot/client/ClientMonitorService.kt | 95 ++++++++++++++ .../vpnhotspot/client/TetheringClient.kt | 8 ++ .../mygod/vpnhotspot/client/WifiP2pClient.kt | 10 ++ .../vpnhotspot/net/IpNeighbourMonitor.kt | 12 +- .../src/main/res/layout/listitem_client.xml | 2 +- 13 files changed, 227 insertions(+), 104 deletions(-) create mode 100644 mobile/src/main/java/be/mygod/vpnhotspot/client/Client.kt create mode 100644 mobile/src/main/java/be/mygod/vpnhotspot/client/ClientMonitorService.kt create mode 100644 mobile/src/main/java/be/mygod/vpnhotspot/client/TetheringClient.kt create mode 100644 mobile/src/main/java/be/mygod/vpnhotspot/client/WifiP2pClient.kt diff --git a/build.gradle b/build.gradle index 983a43c9..e57ae649 100644 --- a/build.gradle +++ b/build.gradle @@ -24,6 +24,7 @@ allprojects { repositories { google() jcenter() + maven { url 'https://jitpack.io' } } } diff --git a/mobile/build.gradle b/mobile/build.gradle index 79d9ca13..d45a30dc 100644 --- a/mobile/build.gradle +++ b/mobile/build.gradle @@ -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" diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml index eeee6919..0f759019 100644 --- a/mobile/src/main/AndroidManifest.xml +++ b/mobile/src/main/AndroidManifest.xml @@ -69,6 +69,7 @@ + - override fun onIpNeighbourAvailable(neighbours: Map) { - this.neighbours = neighbours.values.toList() + override fun onIpNeighbourAvailable(neighbours: List) { + 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 } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/MainActivity.kt b/mobile/src/main/java/be/mygod/vpnhotspot/MainActivity.kt index a29702d2..29a4d211 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/MainActivity.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/MainActivity.kt @@ -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() } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterFragment.kt index 051b68e7..cdbeb54d 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterFragment.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterFragment.kt @@ -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() - - 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() { - 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(ClientDiffCallback) { - var p2p: Collection = emptyList() - var neighbours = emptyList() - - 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 { it.iface }.thenBy { it.mac })) + private inner class ClientAdapter : ListAdapter(Client) { + override fun submitList(list: MutableList?) { + 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() - 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) { - adapter.neighbours = neighbours.values.toList() - } - override fun postIpNeighbourAvailable() = adapter.recreate() } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/TetheringService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/TetheringService.kt index 5d5148a1..69bc0079 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/TetheringService.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/TetheringService.kt @@ -68,7 +68,7 @@ class TetheringService : IpNeighbourMonitoringService(), VpnMonitor.Callback { VpnMonitor.registerCallback(this) receiverRegistered = true } - postIpNeighbourAvailable() + updateNotification() } if (routings.isEmpty()) { unregisterReceiver() diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/client/Client.kt b/mobile/src/main/java/be/mygod/vpnhotspot/client/Client.kt new file mode 100644 index 00000000..2d796e85 --- /dev/null +++ b/mobile/src/main/java/be/mygod/vpnhotspot/client/Client.kt @@ -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() { + 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() + + 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) +} \ No newline at end of file diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/client/ClientMonitorService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/client/ClientMonitorService.kt new file mode 100644 index 00000000..51b57b27 --- /dev/null +++ b/mobile/src/main/java/be/mygod/vpnhotspot/client/ClientMonitorService.kt @@ -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() + 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 = emptyList() + private var neighbours = emptyList() + private var clients = emptyList() + private set(value) { + field = value + binder.clientsChanged(value) + } + + private fun populateClients() { + val clients = HashMap, 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 { 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) { + this.neighbours = neighbours + populateClients() + } +} diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/client/TetheringClient.kt b/mobile/src/main/java/be/mygod/vpnhotspot/client/TetheringClient.kt new file mode 100644 index 00000000..c6d65092 --- /dev/null +++ b/mobile/src/main/java/be/mygod/vpnhotspot/client/TetheringClient.kt @@ -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 +} diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/client/WifiP2pClient.kt b/mobile/src/main/java/be/mygod/vpnhotspot/client/WifiP2pClient.kt new file mode 100644 index 00000000..46f4c4d6 --- /dev/null +++ b/mobile/src/main/java/be/mygod/vpnhotspot/client/WifiP2pClient.kt @@ -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 +} diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/IpNeighbourMonitor.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/IpNeighbourMonitor.kt index 1a52170c..9562a740 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/IpNeighbourMonitor.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/IpNeighbourMonitor.kt @@ -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) - fun postIpNeighbourAvailable() + fun onIpNeighbourAvailable(neighbours: List) } 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 } diff --git a/mobile/src/main/res/layout/listitem_client.xml b/mobile/src/main/res/layout/listitem_client.xml index ab75e5d9..d3c4b7bd 100644 --- a/mobile/src/main/res/layout/listitem_client.xml +++ b/mobile/src/main/res/layout/listitem_client.xml @@ -5,7 +5,7 @@ + type="be.mygod.vpnhotspot.client.Client"/>