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"/>