Initial support for registerSoftApCallback

This commit is contained in:
Mygod
2020-07-03 07:38:51 +08:00
parent fbb1483969
commit 798275e9c9
10 changed files with 399 additions and 44 deletions

View File

@@ -15,7 +15,6 @@ import kotlinx.coroutines.channels.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.io.*
import java.lang.ref.WeakReference
import java.util.*
import java.util.concurrent.CountDownLatch
import kotlin.system.exitProcess
@@ -191,6 +190,9 @@ class RootServer @JvmOverloads constructor(private val warnLogger: (String) -> U
}
try {
callbackSpin()
} catch (e: Throwable) {
process.destroy()
throw e
} finally {
if (DEBUG) Log.d(TAG, "Waiting for exit")
process.waitFor()
@@ -270,9 +272,13 @@ class RootServer @JvmOverloads constructor(private val warnLogger: (String) -> U
if (active) {
active = false
if (DEBUG) Log.d(TAG, "Shutting down from client")
try {
sendLocked(Shutdown())
output.close()
process.outputStream.close()
} catch (e: IOException) {
Log.i(TAG, "send Shutdown failed", e)
}
if (DEBUG) Log.d(TAG, "Client closed")
}
if (fromWorker) {
@@ -375,7 +381,7 @@ class RootServer @JvmOverloads constructor(private val warnLogger: (String) -> U
CoroutineScope(Dispatchers.Main.immediate + job)
}
val callbackWorker = newSingleThreadContext("callbackWorker")
val channels = LongSparseArray<WeakReference<ReceiveChannel<Parcelable?>>>()
val channels = LongSparseArray<ReceiveChannel<Parcelable?>>()
// thread safety: usage of output should be guarded by callbackWorker
val output = DataOutputStream(System.out.buffered().apply {
@@ -396,7 +402,7 @@ class RootServer @JvmOverloads constructor(private val warnLogger: (String) -> U
val callback = counter
if (DEBUG) Log.d(TAG, "Received #$callback: $command")
when (command) {
is ChannelClosed -> channels[command.index]?.get()?.cancel()
is ChannelClosed -> channels[command.index]?.cancel()
is RootCommandOneWay -> defaultWorker.launch {
try {
command.execute()
@@ -415,10 +421,10 @@ class RootServer @JvmOverloads constructor(private val warnLogger: (String) -> U
}
is RootCommandChannel<*> -> defaultWorker.launch {
val result = try {
command.create(defaultWorker).also {
channels[callback] = WeakReference(it)
}.consumeEach { result ->
coroutineScope {
command.create(this).also { channels[callback] = it }.consumeEach { result ->
withContext(callbackWorker) { output.pushResult(callback, result) }
}
};
@Suppress("BlockingMethodInNonBlockingContext") {
output.writeByte(CHANNEL_CONSUMED)

View File

@@ -66,7 +66,10 @@ class App : Application() {
if (priority != Log.DEBUG || BuildConfig.DEBUG) Log.println(priority, tag, message)
FirebaseCrashlytics.getInstance().log("${"XXVDIWEF".getOrElse(priority) { 'X' }}/$tag: $message")
} else {
if (priority >= Log.WARN || priority == Log.DEBUG) Log.println(priority, tag, message)
if (priority >= Log.WARN || priority == Log.DEBUG) {
Log.println(priority, tag, message)
Log.d(tag, message, t)
}
if (priority >= Log.INFO && t !is NoShellException) {
FirebaseCrashlytics.getInstance().recordException(t)
}

View File

@@ -4,6 +4,7 @@ import android.content.Intent
import android.os.Build
import android.os.Bundle
import androidx.core.content.FileProvider
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreference
@@ -24,6 +25,7 @@ import be.mygod.vpnhotspot.util.showAllowingStateLoss
import be.mygod.vpnhotspot.widget.SmartSnackbar
import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@@ -48,10 +50,11 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
if (Build.VERSION.SDK_INT >= 27) {
isChecked = TetherOffloadManager.enabled
setOnPreferenceChangeListener { _, newValue ->
if (TetherOffloadManager.enabled != newValue) GlobalScope.launch(Dispatchers.Main.immediate) {
if (TetherOffloadManager.enabled != newValue) viewLifecycleOwner.lifecycleScope.launchWhenCreated {
isEnabled = false
try {
TetherOffloadManager.setEnabled(newValue as Boolean)
} catch (_: CancellationException) {
} catch (e: Exception) {
Timber.w(e)
SmartSnackbar.make(e).show()

View File

@@ -1,5 +1,6 @@
package be.mygod.vpnhotspot.manage
import android.annotation.TargetApi
import android.content.Intent
import android.os.Build
import android.provider.Settings
@@ -18,6 +19,7 @@ import be.mygod.vpnhotspot.databinding.ListitemInterfaceBinding
import be.mygod.vpnhotspot.net.TetherType
import be.mygod.vpnhotspot.net.TetheringManager
import be.mygod.vpnhotspot.net.wifi.WifiApManager
import be.mygod.vpnhotspot.root.WifiApCommands
import be.mygod.vpnhotspot.util.readableMessage
import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.Dispatchers
@@ -67,7 +69,7 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
inner class Data : be.mygod.vpnhotspot.manage.Data() {
override val icon get() = tetherType.icon
override val title get() = this@TetherManager.title
override var text: CharSequence = ""
override val text get() = error
override val active get() = isStarted
}
@@ -75,6 +77,10 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
abstract val title: CharSequence
abstract val tetherType: TetherType
open val isStarted get() = parent.enabledTypes.contains(tetherType)
protected open val error: CharSequence get() = baseError ?: ""
protected var baseError: String? = null
private set
protected abstract fun start()
protected abstract fun stop()
@@ -98,27 +104,53 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
(viewHolder as ViewHolder).manager = this
}
private fun getErrorMessage(iface: String): String {
return TetheringManager.tetherErrorMessage(try {
TetheringManager.getLastTetherError(iface)
fun updateErrorMessage(errored: List<String>) {
val interested = errored.filter { TetherType.ofInterface(it) == tetherType }
baseError = if (interested.isEmpty()) null else interested.joinToString("\n") { iface ->
"$iface: " + try {
TetheringManager.tetherErrorMessage(TetheringManager.getLastTetherError(iface))
} catch (e: InvocationTargetException) {
if (Build.VERSION.SDK_INT !in 24..25 || e.cause !is SecurityException) Timber.w(e) else Timber.d(e)
return e.readableMessage
})
e.readableMessage
}
}
protected open fun makeErrorMessage(errored: List<String>): CharSequence = errored
.filter { TetherType.ofInterface(it) == tetherType }
.joinToString("\n") { "$it: ${getErrorMessage(it)}" }
fun updateErrorMessage(errored: List<String>) {
data.text = makeErrorMessage(errored)
data.notifyChange()
}
@RequiresApi(24)
class Wifi(parent: TetheringFragment) : TetherManager(parent) {
class Wifi(parent: TetheringFragment) : TetherManager(parent), DefaultLifecycleObserver,
WifiApManager.SoftApCallbackCompat {
private var failureReason: Int? = null
init {
if (Build.VERSION.SDK_INT >= 28) parent.viewLifecycleOwner.lifecycle.addObserver(this)
}
@TargetApi(28)
override fun onStart(owner: LifecycleOwner) {
WifiApCommands.registerSoftApCallback(this)
}
@TargetApi(28)
override fun onStop(owner: LifecycleOwner) {
WifiApCommands.unregisterSoftApCallback(this)
}
override fun onStateChanged(state: Int, failureReason: Int) {
if (state < 10 || state > 14) {
Timber.w(Exception("Unknown state $state"))
return
}
val newReason = if (state == 14) failureReason else null
if (this.failureReason != newReason) {
this.failureReason = newReason
data.notifyChange()
}
}
override val title get() = parent.getString(R.string.tethering_manage_wifi)
override val tetherType get() = TetherType.WIFI
override val type get() = VIEW_TYPE_WIFI
override val error get() = listOfNotNull(failureReason?.let { WifiApManager.failureReason(it) },
baseError).joinToString("\n")
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_WIFI, true, this)
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_WIFI, this::onException)
@@ -134,10 +166,7 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
}
@RequiresApi(24)
class Bluetooth(parent: TetheringFragment) : TetherManager(parent), DefaultLifecycleObserver {
private val tethering = BluetoothTethering(parent.requireContext()) {
data.text = makeErrorMessage()
data.notifyChange()
}
private val tethering = BluetoothTethering(parent.requireContext()) { data.notifyChange() }
init {
parent.viewLifecycleOwner.lifecycle.addObserver(this)
@@ -149,15 +178,9 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
override val tetherType get() = TetherType.BLUETOOTH
override val type get() = VIEW_TYPE_BLUETOOTH
override val isStarted get() = tethering.active == true
private var baseError: CharSequence? = null
private fun makeErrorMessage(): CharSequence = listOfNotNull(
override val error get() = listOfNotNull(
if (tethering.active == null) tethering.activeFailureCause?.readableMessage else null,
baseError).joinToString("\n")
override fun makeErrorMessage(errored: List<String>): CharSequence {
baseError = super.makeErrorMessage(errored).let { if (it.isEmpty()) null else it }
return makeErrorMessage()
}
override fun start() = BluetoothTethering.start(this)
override fun stop() {

View File

@@ -33,6 +33,7 @@ import be.mygod.vpnhotspot.root.RootManager
import be.mygod.vpnhotspot.root.WifiApCommands
import be.mygod.vpnhotspot.util.*
import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
import timber.log.Timber
import java.lang.reflect.InvocationTargetException
@@ -188,6 +189,8 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
if (e.targetException !is SecurityException) Timber.w(e)
try {
RootManager.use { it.execute(WifiApCommands.GetConfiguration()) }
} catch (_: CancellationException) {
null
} catch (eRoot: Exception) {
eRoot.addSuppressed(e)
Timber.w(eRoot)
@@ -216,6 +219,7 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
} catch (e: InvocationTargetException) {
try {
RootManager.use { it.execute(WifiApCommands.SetConfiguration(ret!!.configuration)) }
} catch (_: CancellationException) {
} catch (eRoot: Exception) {
eRoot.addSuppressed(e)
Timber.w(eRoot)

View File

@@ -18,10 +18,7 @@ import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.root.RootManager
import be.mygod.vpnhotspot.root.StartTethering
import be.mygod.vpnhotspot.root.StopTethering
import be.mygod.vpnhotspot.util.Services
import be.mygod.vpnhotspot.util.broadcastReceiver
import be.mygod.vpnhotspot.util.callSuper
import be.mygod.vpnhotspot.util.ensureReceiverUnregistered
import be.mygod.vpnhotspot.util.*
import com.android.dx.stock.ProxyBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
@@ -225,8 +222,6 @@ object TetheringManager {
@get:RequiresApi(30)
private val stopTethering by lazy { clazz.getDeclaredMethod("stopTethering", Int::class.java) }
private fun Handler?.makeExecutor() = Executor { if (this == null) it.run() else post(it) }
@Deprecated("Legacy API")
@RequiresApi(24)
fun startTetheringLegacy(type: Int, showProvisioningUi: Boolean, callback: StartTetheringCallback,

View File

@@ -3,13 +3,25 @@ package be.mygod.vpnhotspot.net.wifi
import android.annotation.TargetApi
import android.content.Intent
import android.content.pm.PackageManager
import android.net.MacAddress
import android.net.wifi.SoftApConfiguration
import android.net.wifi.WifiManager
import android.os.Build
import android.os.Handler
import android.os.Looper
import androidx.annotation.RequiresApi
import androidx.collection.SparseArrayCompat
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.toCompat
import be.mygod.vpnhotspot.util.Services
import be.mygod.vpnhotspot.util.callSuper
import be.mygod.vpnhotspot.util.makeExecutor
import timber.log.Timber
import java.lang.reflect.InvocationHandler
import java.lang.reflect.Method
import java.lang.reflect.Proxy
import java.util.concurrent.Executor
object WifiApManager {
/**
@@ -48,9 +60,153 @@ object WifiApManager {
setWifiApConfiguration(Services.wifi, value.toWifiConfiguration())
} else setSoftApConfiguration(Services.wifi, value.toPlatform())) as Boolean
@RequiresApi(28)
interface SoftApCallbackCompat {
/**
* Called when soft AP state changes.
*
* @param state new new AP state. One of {@link #WIFI_AP_STATE_DISABLED},
* {@link #WIFI_AP_STATE_DISABLING}, {@link #WIFI_AP_STATE_ENABLED},
* {@link #WIFI_AP_STATE_ENABLING}, {@link #WIFI_AP_STATE_FAILED}
* @param failureReason reason when in failed state. One of
* {@link #SAP_START_FAILURE_GENERAL}, {@link #SAP_START_FAILURE_NO_CHANNEL}
*/
fun onStateChanged(state: Int, failureReason: Int) { }
/**
* Called when number of connected clients to soft AP changes.
*
* @param numClients number of connected clients
*/
@Deprecated("onConnectedClientsChanged")
fun onNumClientsChanged(numClients: Int) { }
@RequiresApi(30)
fun onConnectedClientsChanged(clients: List<MacAddress>) {
@Suppress("DEPRECATION")
onNumClientsChanged(clients.size)
}
@RequiresApi(30)
fun onInfoChanged(frequency: Int, bandwidth: Int) { }
@RequiresApi(30)
fun onCapabilityChanged(maxSupportedClients: Int, supportedFeatures: Long) { }
@RequiresApi(30)
fun onBlockedClientConnecting(client: MacAddress, blockedReason: Int) { }
}
private val startFailures29 = arrayOf("SAP_START_FAILURE_GENERAL", "SAP_START_FAILURE_NO_CHANNEL")
private val startFailures by lazy {
SparseArrayCompat<String>().apply {
for (field in WifiManager::class.java.declaredFields) try {
// all SAP_START_FAILURE_* are system-api since API 30
if (field.name.startsWith("SAP_START_FAILURE_")) put(field.get(null) as Int, field.name)
} catch (e: Exception) {
Timber.w(e)
}
}
}
fun failureReason(reason: Int): String {
if (Build.VERSION.SDK_INT >= 30) try {
startFailures.get(reason)?.let { return it }
} catch (e: ReflectiveOperationException) {
Timber.w(e)
}
return startFailures29.getOrNull(reason) ?: app.getString(R.string.failure_reason_unknown, reason)
}
private val interfaceSoftApCallback by lazy { Class.forName("android.net.wifi.WifiManager\$SoftApCallback") }
private val registerSoftApCallback by lazy {
val parameters = if (Build.VERSION.SDK_INT >= 30) {
arrayOf(Executor::class.java, interfaceSoftApCallback)
} else arrayOf(interfaceSoftApCallback, Handler::class.java)
WifiManager::class.java.getDeclaredMethod("registerSoftApCallback", *parameters)
}
private val unregisterSoftApCallback by lazy {
WifiManager::class.java.getDeclaredMethod("unregisterSoftApCallback", interfaceSoftApCallback)
}
private val getMacAddress by lazy {
Class.forName("android.net.wifi.WifiClient").getDeclaredMethod("getMacAddress")
}
private val classSoftApInfo by lazy { Class.forName("android.net.wifi.SoftApInfo") }
private val getFrequency by lazy { classSoftApInfo.getDeclaredMethod("getFrequency") }
private val getBandwidth by lazy { classSoftApInfo.getDeclaredMethod("getBandwidth") }
private val classSoftApCapability by lazy { Class.forName("android.net.wifi.SoftApCapability") }
private val getMaxSupportedClients by lazy { classSoftApCapability.getDeclaredMethod("getMaxSupportedClients") }
private val areFeaturesSupported by lazy {
classSoftApCapability.getDeclaredMethod("areFeaturesSupported", Long::class.java)
}
@RequiresApi(28)
fun registerSoftApCallback(callback: SoftApCallbackCompat, executor: Executor): Any {
val proxy = Proxy.newProxyInstance(interfaceSoftApCallback.classLoader,
arrayOf(interfaceSoftApCallback), object : InvocationHandler {
override fun invoke(proxy: Any, method: Method, args: Array<out Any?>?): Any? {
return if (Build.VERSION.SDK_INT >= 30) invokeActual(proxy, method, args) else {
executor.execute { invokeActual(proxy, method, args) }
null // no return value as of API 30
}
}
private fun invokeActual(proxy: Any, method: Method, args: Array<out Any?>?): Any? {
val noArgs = args?.size ?: 0
return when (val name = method.name) {
"onStateChanged" -> {
if (noArgs != 2) Timber.w("Unexpected args for $name: $args")
callback.onStateChanged(args!![0] as Int, args[1] as Int)
}
"onNumClientsChanged" -> @Suppress("DEPRECATION") {
if (Build.VERSION.SDK_INT >= 30) Timber.w(Exception("Unexpected onNumClientsChanged"))
if (noArgs != 1) Timber.w("Unexpected args for $name: $args")
callback.onNumClientsChanged(args!![0] as Int)
}
"onConnectedClientsChanged" -> @TargetApi(30) {
if (Build.VERSION.SDK_INT < 30) Timber.w(Exception("Unexpected onConnectedClientsChanged"))
if (noArgs != 1) Timber.w("Unexpected args for $name: $args")
callback.onConnectedClientsChanged((args!![0] as Iterable<*>)
.map { getMacAddress(it) as MacAddress })
}
"onInfoChanged" -> @TargetApi(30) {
if (Build.VERSION.SDK_INT < 30) Timber.w(Exception("Unexpected onInfoChanged"))
if (noArgs != 1) Timber.w("Unexpected args for $name: $args")
val softApInfo = args!![0]
callback.onInfoChanged(getFrequency(softApInfo) as Int, getBandwidth(softApInfo) as Int)
}
"onCapabilityChanged" -> @TargetApi(30) {
if (Build.VERSION.SDK_INT < 30) Timber.w(Exception("Unexpected onCapabilityChanged"))
if (noArgs != 1) Timber.w("Unexpected args for $name: $args")
val softApCapability = args!![0]
var supportedFeatures = 0L
var probe = 1L
while (probe != 0L) {
if (areFeaturesSupported(softApCapability, probe) as Boolean) {
supportedFeatures = supportedFeatures or probe
}
probe += probe
}
callback.onCapabilityChanged(getMaxSupportedClients(softApCapability) as Int, supportedFeatures)
}
"onBlockedClientConnecting" -> @TargetApi(30) {
if (Build.VERSION.SDK_INT < 30) Timber.w(Exception("Unexpected onBlockedClientConnecting"))
if (noArgs != 2) Timber.w("Unexpected args for $name: $args")
callback.onBlockedClientConnecting(getMacAddress(args!![0]) as MacAddress, args[1] as Int)
}
else -> callSuper(interfaceSoftApCallback, proxy, method, args)
}
}
})
if (Build.VERSION.SDK_INT >= 30) {
registerSoftApCallback(Services.wifi, executor, proxy)
} else registerSoftApCallback(Services.wifi, proxy, null)
return proxy
}
@RequiresApi(28)
fun unregisterSoftApCallback(key: Any) = unregisterSoftApCallback(Services.wifi, key)
private val cancelLocalOnlyHotspotRequest by lazy {
WifiManager::class.java.getDeclaredMethod("cancelLocalOnlyHotspotRequest")
}
@RequiresApi(26)
fun cancelLocalOnlyHotspotRequest() = cancelLocalOnlyHotspotRequest(Services.wifi)
@Suppress("DEPRECATION")

View File

@@ -1,6 +1,7 @@
package be.mygod.vpnhotspot.root
import android.os.Parcelable
import android.util.Log
import be.mygod.librootkotlinx.RootCommandNoResult
import be.mygod.librootkotlinx.RootServer
import be.mygod.librootkotlinx.RootSession
@@ -16,6 +17,16 @@ object RootManager : RootSession() {
class RootInit : RootCommandNoResult {
override suspend fun execute(): Parcelable? {
RootServer.DEBUG = BuildConfig.DEBUG
Timber.plant(object : Timber.DebugTree() {
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
if (t == null) {
Log.println(priority, tag, message)
} else {
Log.println(priority, tag, message)
Log.d(tag, message, t)
}
}
})
Services.init(RootJava.getSystemContext())
return null
}

View File

@@ -1,12 +1,162 @@
package be.mygod.vpnhotspot.root
import android.net.MacAddress
import android.os.Parcelable
import androidx.annotation.RequiresApi
import be.mygod.librootkotlinx.ParcelableBoolean
import be.mygod.librootkotlinx.RootCommand
import be.mygod.librootkotlinx.RootCommandChannel
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat
import be.mygod.vpnhotspot.net.wifi.WifiApManager
import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.android.parcel.Parcelize
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.channels.produce
import timber.log.Timber
import java.util.concurrent.Executor
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() {
@Suppress("DEPRECATION")
override fun dispatch(callback: WifiApManager.SoftApCallbackCompat) =
callback.onNumClientsChanged(numClients)
}
@Parcelize
@RequiresApi(30)
data class OnConnectedClientsChanged(val clients: List<MacAddress>) : SoftApCallbackParcel() {
override fun dispatch(callback: WifiApManager.SoftApCallbackCompat) =
callback.onConnectedClientsChanged(clients)
}
@Parcelize
@RequiresApi(30)
data class OnInfoChanged(val frequency: Int, val bandwidth: Int) : SoftApCallbackParcel() {
override fun dispatch(callback: WifiApManager.SoftApCallbackCompat) =
callback.onInfoChanged(frequency, bandwidth)
}
@Parcelize
@RequiresApi(30)
data class OnCapabilityChanged(val maxSupportedClients: Int,
val supportedFeatures: Long) : SoftApCallbackParcel() {
override fun dispatch(callback: WifiApManager.SoftApCallbackCompat) =
callback.onCapabilityChanged(maxSupportedClients, supportedFeatures)
}
@Parcelize
@RequiresApi(30)
data class OnBlockedClientConnecting(val client: MacAddress, 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) = check(try {
offer(parcel)
} catch (closed: Throwable) {
finish.completeExceptionally(closed)
true
})
override fun onStateChanged(state: Int, failureReason: Int) =
push(SoftApCallbackParcel.OnStateChanged(state, failureReason))
@Suppress("OverridingDeprecatedMember")
override fun onNumClientsChanged(numClients: Int) =
push(SoftApCallbackParcel.OnNumClientsChanged(numClients))
@RequiresApi(30)
override fun onConnectedClientsChanged(clients: List<MacAddress>) =
push(SoftApCallbackParcel.OnConnectedClientsChanged(clients))
@RequiresApi(30)
override fun onInfoChanged(frequency: Int, bandwidth: Int) =
push(SoftApCallbackParcel.OnInfoChanged(frequency, bandwidth))
@RequiresApi(30)
override fun onCapabilityChanged(maxSupportedClients: Int, supportedFeatures: Long) =
push(SoftApCallbackParcel.OnCapabilityChanged(maxSupportedClients, supportedFeatures))
@RequiresApi(30)
override fun onBlockedClientConnecting(client: MacAddress, blockedReason: Int) =
push(SoftApCallbackParcel.OnBlockedClientConnecting(client, blockedReason))
}, Executor {
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 }
}
for (callback in synchronized(callbacks) { callbacks }) 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()
}
}
lastCallback
} else null
}?.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

View File

@@ -5,6 +5,7 @@ import android.annotation.TargetApi
import android.content.*
import android.net.InetAddresses
import android.os.Build
import android.os.Handler
import android.text.Spannable
import android.text.SpannableString
import android.text.SpannableStringBuilder
@@ -28,6 +29,7 @@ import java.lang.reflect.Method
import java.net.InetAddress
import java.net.NetworkInterface
import java.net.SocketException
import java.util.concurrent.Executor
val Throwable.readableMessage: String get() = if (this is InvocationTargetException) {
targetException.readableMessage
@@ -49,6 +51,8 @@ fun Context.ensureReceiverUnregistered(receiver: BroadcastReceiver) {
} catch (_: IllegalArgumentException) { }
}
fun Handler?.makeExecutor() = Executor { if (this == null) it.run() else post(it) }
fun DialogFragment.showAllowingStateLoss(manager: FragmentManager, tag: String? = null) {
if (!manager.isStateSaved) show(manager, tag)
}