diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetherManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetherManager.kt index d6981ae2..8cd19745 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetherManager.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetherManager.kt @@ -2,6 +2,7 @@ package be.mygod.vpnhotspot.manage import android.annotation.TargetApi import android.content.Intent +import android.net.MacAddress import android.os.Build import android.provider.Settings import android.view.View @@ -114,12 +115,17 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(), e.readableMessage } } + data.notifyChange() } @RequiresApi(24) class Wifi(parent: TetheringFragment) : TetherManager(parent), DefaultLifecycleObserver, WifiApManager.SoftApCallbackCompat { private var failureReason: Int? = null + private var numClients: Int? = null + private var frequency = 0 + private var bandwidth = WifiApManager.CHANNEL_WIDTH_INVALID + private var capability: Pair? = null init { if (Build.VERSION.SDK_INT >= 28) parent.viewLifecycleOwner.lifecycle.addObserver(this) @@ -139,17 +145,41 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(), Timber.w(Exception("Unknown state $state")) return } - val newReason = if (state == 14) failureReason else null - if (this.failureReason != newReason) { - this.failureReason = newReason - data.notifyChange() - } + this.failureReason = if (state == 14) failureReason else null // WIFI_AP_STATE_FAILED + data.notifyChange() + } + override fun onNumClientsChanged(numClients: Int) { + this.numClients = numClients + if (Build.VERSION.SDK_INT >= 30) data.notifyChange() // only emits when onCapabilityChanged can be called + } + override fun onInfoChanged(frequency: Int, bandwidth: Int) { + this.frequency = frequency + this.bandwidth = bandwidth + data.notifyChange() + } + override fun onCapabilityChanged(maxSupportedClients: Int, supportedFeatures: Long) { + capability = maxSupportedClients to supportedFeatures + 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 text get() = listOfNotNull(failureReason?.let { WifiApManager.failureReasonLookup(it) }, + if (frequency != 0 || bandwidth != WifiApManager.CHANNEL_WIDTH_INVALID) { + "$frequency MHz, ${WifiApManager.channelWidthLookup(bandwidth, true)}" + } else null, + capability?.let { (maxSupportedClients, supportedFeatures) -> + "${numClients ?: "?"}/$maxSupportedClients clients connected\nSupported features: " + sequence { + var features = supportedFeatures + while (features != 0L) { + @OptIn(ExperimentalStdlibApi::class) + val bit = features.takeLowestOneBit() + yield(WifiApManager.featureLookup(bit, true)) + features = features and bit.inv() + } + }.joinToString() + }, baseError).joinToString("\n") override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_WIFI, true, this) diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApManager.kt index 215c03b6..3bbfa7e1 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApManager.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApManager.kt @@ -12,6 +12,7 @@ import androidx.annotation.RequiresApi import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.toCompat import be.mygod.vpnhotspot.util.ConstantLookup +import be.mygod.vpnhotspot.util.LongConstantLookup import be.mygod.vpnhotspot.util.Services import be.mygod.vpnhotspot.util.callSuper import timber.log.Timber @@ -93,6 +94,7 @@ object WifiApManager { @RequiresApi(30) fun onBlockedClientConnecting(client: MacAddress, blockedReason: Int) { } } + @RequiresApi(28) val failureReasonLookup = ConstantLookup("SAP_START_FAILURE_", "SAP_START_FAILURE_GENERAL", "SAP_START_FAILURE_NO_CHANNEL") @@ -106,17 +108,26 @@ object WifiApManager { 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") } + @RequiresApi(30) + val channelWidthLookup = ConstantLookup(classSoftApInfo, "CHANNEL_WIDTH_") + const val CHANNEL_WIDTH_INVALID = 0 + 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(30) + val featureLookup = LongConstantLookup(classSoftApCapability, "SOFTAP_FEATURE_") + @RequiresApi(28) fun registerSoftApCallback(callback: SoftApCallbackCompat, executor: Executor): Any { val proxy = Proxy.newProxyInstance(interfaceSoftApCallback.classLoader, diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/util/ConstantLookup.kt b/mobile/src/main/java/be/mygod/vpnhotspot/util/ConstantLookup.kt index 032145a5..942a60ac 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/util/ConstantLookup.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/util/ConstantLookup.kt @@ -1,6 +1,7 @@ package be.mygod.vpnhotspot.util import android.os.Build +import androidx.collection.LongSparseArray import androidx.collection.SparseArrayCompat import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.R @@ -10,19 +11,21 @@ class ConstantLookup(private val clazz: Class<*>, private val prefix: String, pr private val lookup by lazy { SparseArrayCompat().apply { for (field in clazz.declaredFields) try { - if (field.name.startsWith(prefix)) put(field.get(null) as Int, field.name) + if (field.name.startsWith(prefix)) put(field.getInt(null), field.name) } catch (e: Exception) { Timber.w(e) } } } - operator fun invoke(reason: Int): String { + + operator fun invoke(reason: Int, trimPrefix: Boolean = false): String { if (Build.VERSION.SDK_INT >= 30) try { - lookup.get(reason)?.let { return it } + lookup.get(reason)?.let { return if (trimPrefix) it.substring(prefix.length) else it } } catch (e: ReflectiveOperationException) { Timber.w(e) } - return lookup29.getOrNull(reason) ?: app.getString(R.string.failure_reason_unknown, reason) + return lookup29.getOrNull(reason)?.let { if (trimPrefix) it.substring(prefix.length) else it } + ?: app.getString(R.string.failure_reason_unknown, reason) } } @@ -31,3 +34,22 @@ fun ConstantLookup(clazz: Class<*>, prefix: String, vararg lookup29: String) = C @Suppress("FunctionName") inline fun ConstantLookup(prefix: String, vararg lookup29: String) = ConstantLookup(T::class.java, prefix, lookup29) + +class LongConstantLookup(private val clazz: Class<*>, private val prefix: String) { + private val lookup = LongSparseArray().apply { + for (field in clazz.declaredFields) try { + if (field.name.startsWith(prefix)) put(field.getLong(null), field.name) + } catch (e: Exception) { + Timber.w(e) + } + } + + operator fun invoke(reason: Long, trimPrefix: Boolean = false): String { + try { + lookup.get(reason)?.let { return if (trimPrefix) it.substring(prefix.length) else it } + } catch (e: ReflectiveOperationException) { + Timber.w(e) + } + return app.getString(R.string.failure_reason_unknown, reason) + } +}