Initial support for registerSoftApCallback
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user