Files
vpnhotspotmod/mobile/src/main/java/be/mygod/vpnhotspot/root/WifiApCommands.kt

179 lines
8.5 KiB
Kotlin

package be.mygod.vpnhotspot.root
import android.annotation.TargetApi
import android.content.ClipData
import android.os.Parcelable
import androidx.annotation.RequiresApi
import androidx.core.os.BuildCompat
import be.mygod.librootkotlinx.ParcelableBoolean
import be.mygod.librootkotlinx.RootCommand
import be.mygod.librootkotlinx.RootCommandChannel
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat
import be.mygod.vpnhotspot.net.wifi.WifiApManager
import be.mygod.vpnhotspot.net.wifi.WifiClient
import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
import kotlinx.parcelize.Parcelize
import timber.log.Timber
object WifiApCommands {
@RequiresApi(28)
sealed class SoftApCallbackParcel : Parcelable {
abstract fun dispatch(callback: WifiApManager.SoftApCallbackCompat)
@Parcelize
data class OnStateChanged(val state: Int, val failureReason: Int) : SoftApCallbackParcel() {
override fun dispatch(callback: WifiApManager.SoftApCallbackCompat) =
callback.onStateChanged(state, failureReason)
}
@Parcelize
data class OnNumClientsChanged(val numClients: Int) : SoftApCallbackParcel() {
override fun dispatch(callback: WifiApManager.SoftApCallbackCompat) =
callback.onNumClientsChanged(numClients)
}
@Parcelize
@RequiresApi(30)
data class OnConnectedClientsChanged(val clients: List<Parcelable>) : SoftApCallbackParcel() {
override fun dispatch(callback: WifiApManager.SoftApCallbackCompat) =
callback.onConnectedClientsChanged(clients)
}
@Parcelize
@RequiresApi(30)
data class OnInfoChanged(val info: List<Parcelable>) : SoftApCallbackParcel() {
override fun dispatch(callback: WifiApManager.SoftApCallbackCompat) = callback.onInfoChanged(info)
}
@Parcelize
@RequiresApi(30)
data class OnCapabilityChanged(val capability: Parcelable) : SoftApCallbackParcel() {
override fun dispatch(callback: WifiApManager.SoftApCallbackCompat) =
callback.onCapabilityChanged(capability)
}
@Parcelize
@RequiresApi(30)
data class OnBlockedClientConnecting(val client: Parcelable, val blockedReason: Int) : SoftApCallbackParcel() {
override fun dispatch(callback: WifiApManager.SoftApCallbackCompat) =
callback.onBlockedClientConnecting(client, blockedReason)
}
}
@Parcelize
@RequiresApi(28)
class RegisterSoftApCallback : RootCommandChannel<SoftApCallbackParcel> {
override fun create(scope: CoroutineScope) = scope.produce(capacity = capacity) {
val finish = CompletableDeferred<Unit>()
val key = WifiApManager.registerSoftApCallback(object : WifiApManager.SoftApCallbackCompat {
private fun push(parcel: SoftApCallbackParcel) {
trySend(parcel).onClosed {
finish.completeExceptionally(it ?: ClosedSendChannelException("Channel was closed normally"))
}.onFailure { throw it!! }
}
override fun onStateChanged(state: Int, failureReason: Int) =
push(SoftApCallbackParcel.OnStateChanged(state, failureReason))
override fun onNumClientsChanged(numClients: Int) =
push(SoftApCallbackParcel.OnNumClientsChanged(numClients))
@RequiresApi(30)
override fun onConnectedClientsChanged(clients: List<Parcelable>) =
push(SoftApCallbackParcel.OnConnectedClientsChanged(clients))
@RequiresApi(30)
override fun onInfoChanged(info: List<Parcelable>) = push(SoftApCallbackParcel.OnInfoChanged(info))
@RequiresApi(30)
override fun onCapabilityChanged(capability: Parcelable) =
push(SoftApCallbackParcel.OnCapabilityChanged(capability))
@RequiresApi(30)
override fun onBlockedClientConnecting(client: Parcelable, blockedReason: Int) =
push(SoftApCallbackParcel.OnBlockedClientConnecting(client, blockedReason))
}) {
scope.launch {
try {
it.run()
} catch (e: Throwable) {
finish.completeExceptionally(e)
}
}
}
try {
finish.await()
} finally {
WifiApManager.unregisterSoftApCallback(key)
}
}
}
private data class AutoFiringCallbacks(
var state: SoftApCallbackParcel.OnStateChanged? = null,
var numClients: SoftApCallbackParcel.OnNumClientsChanged? = null,
var connectedClients: SoftApCallbackParcel.OnConnectedClientsChanged? = null,
var info: SoftApCallbackParcel.OnInfoChanged? = null,
var capability: SoftApCallbackParcel.OnCapabilityChanged? = null) {
fun toSequence() = sequenceOf(state, numClients, connectedClients, info, capability)
}
private val callbacks = mutableSetOf<WifiApManager.SoftApCallbackCompat>()
private val lastCallback = AutoFiringCallbacks()
private var rootCallbackJob: Job? = null
@RequiresApi(28)
private suspend fun handleChannel(channel: ReceiveChannel<SoftApCallbackParcel>) = channel.consumeEach { parcel ->
when (parcel) {
is SoftApCallbackParcel.OnStateChanged -> synchronized(callbacks) { lastCallback.state = parcel }
is SoftApCallbackParcel.OnNumClientsChanged -> synchronized(callbacks) { lastCallback.numClients = parcel }
is SoftApCallbackParcel.OnConnectedClientsChanged -> synchronized(callbacks) {
lastCallback.connectedClients = parcel
}
is SoftApCallbackParcel.OnInfoChanged -> synchronized(callbacks) { lastCallback.info = parcel }
is SoftApCallbackParcel.OnCapabilityChanged -> synchronized(callbacks) { lastCallback.capability = parcel }
is SoftApCallbackParcel.OnBlockedClientConnecting -> @TargetApi(30) { // passively consume events
val client = WifiClient(parcel.client)
val macAddress = client.macAddress
var name = macAddress.toString()
if (BuildCompat.isAtLeastS()) client.apInstanceIdentifier?.let { name += "%$it" }
val reason = WifiApManager.clientBlockLookup(parcel.blockedReason, true)
Timber.i("$name blocked from connecting: $reason (${parcel.blockedReason})")
SmartSnackbar.make(app.getString(R.string.tethering_manage_wifi_client_blocked, name, reason)).apply {
action(R.string.tethering_manage_wifi_copy_mac) {
app.clipboard.setPrimaryClip(ClipData.newPlainText(null, macAddress.toString()))
}
}.show()
}
}
for (callback in synchronized(callbacks) { callbacks.toList() }) parcel.dispatch(callback)
}
@RequiresApi(28)
fun registerSoftApCallback(callback: WifiApManager.SoftApCallbackCompat) = synchronized(callbacks) {
val wasEmpty = callbacks.isEmpty()
callbacks.add(callback)
if (wasEmpty) {
rootCallbackJob = GlobalScope.launch {
try {
RootManager.use { server -> handleChannel(server.create(RegisterSoftApCallback(), this)) }
} catch (_: CancellationException) {
} catch (e: Exception) {
Timber.w(e)
SmartSnackbar.make(e).show()
}
}
null
} else lastCallback
}?.toSequence()?.forEach { it?.dispatch(callback) }
@RequiresApi(28)
fun unregisterSoftApCallback(callback: WifiApManager.SoftApCallbackCompat) = synchronized(callbacks) {
if (callbacks.remove(callback) && callbacks.isEmpty()) {
rootCallbackJob!!.cancel()
rootCallbackJob = null
}
}
@Parcelize
class GetConfiguration : RootCommand<SoftApConfigurationCompat> {
override suspend fun execute() = WifiApManager.configuration
}
@Parcelize
data class SetConfiguration(val configuration: SoftApConfigurationCompat) : RootCommand<ParcelableBoolean> {
override suspend fun execute() = ParcelableBoolean(WifiApManager.setConfiguration(configuration))
}
}