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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
package be.mygod.vpnhotspot.manage package be.mygod.vpnhotspot.manage
import android.annotation.TargetApi
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.provider.Settings 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.TetherType
import be.mygod.vpnhotspot.net.TetheringManager import be.mygod.vpnhotspot.net.TetheringManager
import be.mygod.vpnhotspot.net.wifi.WifiApManager import be.mygod.vpnhotspot.net.wifi.WifiApManager
import be.mygod.vpnhotspot.root.WifiApCommands
import be.mygod.vpnhotspot.util.readableMessage import be.mygod.vpnhotspot.util.readableMessage
import be.mygod.vpnhotspot.widget.SmartSnackbar import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -67,7 +69,7 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
inner class Data : be.mygod.vpnhotspot.manage.Data() { inner class Data : be.mygod.vpnhotspot.manage.Data() {
override val icon get() = tetherType.icon override val icon get() = tetherType.icon
override val title get() = this@TetherManager.title override val title get() = this@TetherManager.title
override var text: CharSequence = "" override val text get() = error
override val active get() = isStarted override val active get() = isStarted
} }
@@ -75,6 +77,10 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
abstract val title: CharSequence abstract val title: CharSequence
abstract val tetherType: TetherType abstract val tetherType: TetherType
open val isStarted get() = parent.enabledTypes.contains(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 start()
protected abstract fun stop() protected abstract fun stop()
@@ -98,27 +104,53 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
(viewHolder as ViewHolder).manager = this (viewHolder as ViewHolder).manager = this
} }
private fun getErrorMessage(iface: String): String {
return TetheringManager.tetherErrorMessage(try {
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
})
}
protected open fun makeErrorMessage(errored: List<String>): CharSequence = errored
.filter { TetherType.ofInterface(it) == tetherType }
.joinToString("\n") { "$it: ${getErrorMessage(it)}" }
fun updateErrorMessage(errored: List<String>) { fun updateErrorMessage(errored: List<String>) {
data.text = makeErrorMessage(errored) val interested = errored.filter { TetherType.ofInterface(it) == tetherType }
data.notifyChange() 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)
e.readableMessage
}
}
} }
@RequiresApi(24) @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 title get() = parent.getString(R.string.tethering_manage_wifi)
override val tetherType get() = TetherType.WIFI override val tetherType get() = TetherType.WIFI
override val type get() = VIEW_TYPE_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 start() = TetheringManager.startTethering(TetheringManager.TETHERING_WIFI, true, this)
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_WIFI, this::onException) override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_WIFI, this::onException)
@@ -134,10 +166,7 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
} }
@RequiresApi(24) @RequiresApi(24)
class Bluetooth(parent: TetheringFragment) : TetherManager(parent), DefaultLifecycleObserver { class Bluetooth(parent: TetheringFragment) : TetherManager(parent), DefaultLifecycleObserver {
private val tethering = BluetoothTethering(parent.requireContext()) { private val tethering = BluetoothTethering(parent.requireContext()) { data.notifyChange() }
data.text = makeErrorMessage()
data.notifyChange()
}
init { init {
parent.viewLifecycleOwner.lifecycle.addObserver(this) 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 tetherType get() = TetherType.BLUETOOTH
override val type get() = VIEW_TYPE_BLUETOOTH override val type get() = VIEW_TYPE_BLUETOOTH
override val isStarted get() = tethering.active == true override val isStarted get() = tethering.active == true
override val error get() = listOfNotNull(
private var baseError: CharSequence? = null
private fun makeErrorMessage(): CharSequence = listOfNotNull(
if (tethering.active == null) tethering.activeFailureCause?.readableMessage else null, if (tethering.active == null) tethering.activeFailureCause?.readableMessage else null,
baseError).joinToString("\n") 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 start() = BluetoothTethering.start(this)
override fun stop() { 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.root.WifiApCommands
import be.mygod.vpnhotspot.util.* import be.mygod.vpnhotspot.util.*
import be.mygod.vpnhotspot.widget.SmartSnackbar import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import timber.log.Timber import timber.log.Timber
import java.lang.reflect.InvocationTargetException import java.lang.reflect.InvocationTargetException
@@ -188,6 +189,8 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
if (e.targetException !is SecurityException) Timber.w(e) if (e.targetException !is SecurityException) Timber.w(e)
try { try {
RootManager.use { it.execute(WifiApCommands.GetConfiguration()) } RootManager.use { it.execute(WifiApCommands.GetConfiguration()) }
} catch (_: CancellationException) {
null
} catch (eRoot: Exception) { } catch (eRoot: Exception) {
eRoot.addSuppressed(e) eRoot.addSuppressed(e)
Timber.w(eRoot) Timber.w(eRoot)
@@ -216,6 +219,7 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
} catch (e: InvocationTargetException) { } catch (e: InvocationTargetException) {
try { try {
RootManager.use { it.execute(WifiApCommands.SetConfiguration(ret!!.configuration)) } RootManager.use { it.execute(WifiApCommands.SetConfiguration(ret!!.configuration)) }
} catch (_: CancellationException) {
} catch (eRoot: Exception) { } catch (eRoot: Exception) {
eRoot.addSuppressed(e) eRoot.addSuppressed(e)
Timber.w(eRoot) 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.RootManager
import be.mygod.vpnhotspot.root.StartTethering import be.mygod.vpnhotspot.root.StartTethering
import be.mygod.vpnhotspot.root.StopTethering import be.mygod.vpnhotspot.root.StopTethering
import be.mygod.vpnhotspot.util.Services import be.mygod.vpnhotspot.util.*
import be.mygod.vpnhotspot.util.broadcastReceiver
import be.mygod.vpnhotspot.util.callSuper
import be.mygod.vpnhotspot.util.ensureReceiverUnregistered
import com.android.dx.stock.ProxyBuilder import com.android.dx.stock.ProxyBuilder
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@@ -225,8 +222,6 @@ object TetheringManager {
@get:RequiresApi(30) @get:RequiresApi(30)
private val stopTethering by lazy { clazz.getDeclaredMethod("stopTethering", Int::class.java) } 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") @Deprecated("Legacy API")
@RequiresApi(24) @RequiresApi(24)
fun startTetheringLegacy(type: Int, showProvisioningUi: Boolean, callback: StartTetheringCallback, 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.annotation.TargetApi
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.MacAddress
import android.net.wifi.SoftApConfiguration import android.net.wifi.SoftApConfiguration
import android.net.wifi.WifiManager import android.net.wifi.WifiManager
import android.os.Build import android.os.Build
import android.os.Handler
import android.os.Looper
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.collection.SparseArrayCompat
import be.mygod.vpnhotspot.App.Companion.app 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.net.wifi.SoftApConfigurationCompat.Companion.toCompat
import be.mygod.vpnhotspot.util.Services 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 { object WifiApManager {
/** /**
@@ -48,9 +60,153 @@ object WifiApManager {
setWifiApConfiguration(Services.wifi, value.toWifiConfiguration()) setWifiApConfiguration(Services.wifi, value.toWifiConfiguration())
} else setSoftApConfiguration(Services.wifi, value.toPlatform())) as Boolean } 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 { private val cancelLocalOnlyHotspotRequest by lazy {
WifiManager::class.java.getDeclaredMethod("cancelLocalOnlyHotspotRequest") WifiManager::class.java.getDeclaredMethod("cancelLocalOnlyHotspotRequest")
} }
@RequiresApi(26)
fun cancelLocalOnlyHotspotRequest() = cancelLocalOnlyHotspotRequest(Services.wifi) fun cancelLocalOnlyHotspotRequest() = cancelLocalOnlyHotspotRequest(Services.wifi)
@Suppress("DEPRECATION") @Suppress("DEPRECATION")

View File

@@ -1,6 +1,7 @@
package be.mygod.vpnhotspot.root package be.mygod.vpnhotspot.root
import android.os.Parcelable import android.os.Parcelable
import android.util.Log
import be.mygod.librootkotlinx.RootCommandNoResult import be.mygod.librootkotlinx.RootCommandNoResult
import be.mygod.librootkotlinx.RootServer import be.mygod.librootkotlinx.RootServer
import be.mygod.librootkotlinx.RootSession import be.mygod.librootkotlinx.RootSession
@@ -16,6 +17,16 @@ object RootManager : RootSession() {
class RootInit : RootCommandNoResult { class RootInit : RootCommandNoResult {
override suspend fun execute(): Parcelable? { override suspend fun execute(): Parcelable? {
RootServer.DEBUG = BuildConfig.DEBUG 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()) Services.init(RootJava.getSystemContext())
return null return null
} }

View File

@@ -1,12 +1,162 @@
package be.mygod.vpnhotspot.root 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.ParcelableBoolean
import be.mygod.librootkotlinx.RootCommand import be.mygod.librootkotlinx.RootCommand
import be.mygod.librootkotlinx.RootCommandChannel
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat
import be.mygod.vpnhotspot.net.wifi.WifiApManager import be.mygod.vpnhotspot.net.wifi.WifiApManager
import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.android.parcel.Parcelize 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 { 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 @Parcelize
class GetConfiguration : RootCommand<SoftApConfigurationCompat> { class GetConfiguration : RootCommand<SoftApConfigurationCompat> {
override suspend fun execute() = WifiApManager.configuration override suspend fun execute() = WifiApManager.configuration

View File

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