diff --git a/README.md b/README.md index 2c1cf632..becfe915 100644 --- a/README.md +++ b/README.md @@ -193,6 +193,8 @@ Hidden whitelisted APIs: (same catch as above, however, things in this list are * (since API 30) `Landroid/net/TetheringManager;->startTethering(Landroid/net/TetheringManager$TetheringRequest;Ljava/util/concurrent/Executor;Landroid/net/TetheringManager$StartTetheringCallback;)V,system-api,test-api,whitelist` * (since API 30) `Landroid/net/TetheringManager;->stopTethering(I)V,system-api,test-api,whitelist` * (since API 30) `Landroid/net/TetheringManager;->unregisterTetheringEventCallback(Landroid/net/TetheringManager$TetheringEventCallback;)V,system-api,test-api,whitelist` +* [`Landroid/net/wifi/WifiConfiguration$KeyMgmt;->WPA2_PSK:I,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#131365) +* [`Landroid/net/wifi/WifiConfiguration$KeyMgmt;->WPA_PSK_SHA256:I,blacklist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#131367) * [`Landroid/net/wifi/WifiManager;->getWifiApConfiguration()Landroid/net/wifi/WifiConfiguration;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#132289) * [`Landroid/net/wifi/WifiManager;->setWifiApConfiguration(Landroid/net/wifi/WifiConfiguration;)Z,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#132358) * `Landroid/net/wifi/p2p/WifiP2pGroupList;->getGroupList()Ljava/util/List;,system-api,whitelist` diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/LocalOnlyHotspotService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/LocalOnlyHotspotService.kt index 35e0e2d2..ccbc785c 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/LocalOnlyHotspotService.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/LocalOnlyHotspotService.kt @@ -5,12 +5,14 @@ import android.content.IntentFilter import android.net.wifi.WifiManager import android.os.Build import androidx.annotation.RequiresApi +import androidx.core.os.BuildCompat import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.net.IpNeighbour import be.mygod.vpnhotspot.net.TetheringManager import be.mygod.vpnhotspot.net.TetheringManager.localOnlyTetheredIfaces import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor +import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.toCompat import be.mygod.vpnhotspot.net.wifi.WifiApManager import be.mygod.vpnhotspot.util.StickyEvent1 import be.mygod.vpnhotspot.util.broadcastReceiver @@ -31,7 +33,11 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope { } val ifaceChanged = StickyEvent1 { iface } - val configuration get() = reservation?.wifiConfiguration + val configuration get() = if (BuildCompat.isAtLeastR()) { + reservation?.softApConfiguration?.toCompat() + } else @Suppress("DEPRECATION") { + reservation?.wifiConfiguration?.toCompat() + } fun stop() { when (iface) { diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt index ca04d317..22f5dea7 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt @@ -18,12 +18,12 @@ import androidx.core.content.getSystemService import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.net.MacAddressCompat import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor +import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.deletePersistentGroup import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.requestPersistentGroupInfo import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.setWifiP2pChannels import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.startWps -import be.mygod.vpnhotspot.net.wifi.configuration.channelToFrequency import be.mygod.vpnhotspot.util.* import be.mygod.vpnhotspot.widget.SmartSnackbar import kotlinx.coroutines.* @@ -315,7 +315,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene setPassphrase(passphrase) operatingChannel.let { oc -> if (oc == 0) setGroupOperatingBand(operatingBand) - else setGroupOperatingFrequency(channelToFrequency(oc)) + else setGroupOperatingFrequency(SoftApConfigurationCompat.channelToFrequency(oc)) } }.build().run { useParcel { p -> diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/LocalOnlyHotspotTileService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/LocalOnlyHotspotTileService.kt index 0519bdda..4645b8a9 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/manage/LocalOnlyHotspotTileService.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/LocalOnlyHotspotTileService.kt @@ -47,7 +47,7 @@ class LocalOnlyHotspotTileService : KillableTileService() { label = getText(R.string.tethering_temp_hotspot) } else { state = Tile.STATE_ACTIVE - label = service.configuration?.SSID ?: getText(R.string.tethering_temp_hotspot) + label = service.configuration?.ssid ?: getText(R.string.tethering_temp_hotspot) } updateTile() } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/RepeaterManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/RepeaterManager.kt index 8d02af14..1004b897 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/manage/RepeaterManager.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/RepeaterManager.kt @@ -5,7 +5,7 @@ import android.content.ComponentName import android.content.DialogInterface import android.content.Intent import android.content.ServiceConnection -import android.net.wifi.WifiConfiguration +import android.net.wifi.SoftApConfiguration import android.net.wifi.p2p.WifiP2pConfig import android.net.wifi.p2p.WifiP2pGroup import android.os.Build @@ -26,7 +26,9 @@ import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import be.mygod.vpnhotspot.* import be.mygod.vpnhotspot.databinding.ListitemRepeaterBinding -import be.mygod.vpnhotspot.net.wifi.configuration.* +import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat +import be.mygod.vpnhotspot.net.wifi.P2pSupplicantConfiguration +import be.mygod.vpnhotspot.net.wifi.WifiApDialogFragment import be.mygod.vpnhotspot.util.ServiceForegroundConnector import be.mygod.vpnhotspot.util.formatAddresses import be.mygod.vpnhotspot.util.showAllowingStateLoss @@ -60,7 +62,8 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic val title: CharSequence @Bindable get() { if (Build.VERSION.SDK_INT >= 29) binder?.group?.frequency?.let { - if (it != 0) return parent.getString(R.string.repeater_channel, it, frequencyToChannel(it)) + if (it != 0) return parent.getString(R.string.repeater_channel, + it, SoftApConfigurationCompat.frequencyToChannel(it)) } return parent.getString(R.string.title_repeater) } @@ -173,20 +176,22 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic } @MainThread - private suspend fun getConfiguration(): WifiConfiguration? { + private suspend fun getConfiguration(): SoftApConfigurationCompat? { if (RepeaterService.safeMode) { val networkName = RepeaterService.networkName val passphrase = RepeaterService.passphrase if (networkName != null && passphrase != null) { - return newWifiApConfiguration(networkName, passphrase).apply { - allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK) // is not actually used - apBand = when (RepeaterService.operatingBand) { - WifiP2pConfig.GROUP_OWNER_BAND_AUTO -> AP_BAND_ANY - WifiP2pConfig.GROUP_OWNER_BAND_2GHZ -> AP_BAND_2GHZ - WifiP2pConfig.GROUP_OWNER_BAND_5GHZ -> AP_BAND_5GHZ + return SoftApConfigurationCompat.empty().apply { + ssid = networkName + securityType = SoftApConfiguration.SECURITY_TYPE_WPA2_PSK // is not actually used + this.passphrase = passphrase + band = when (RepeaterService.operatingBand) { + WifiP2pConfig.GROUP_OWNER_BAND_AUTO -> SoftApConfigurationCompat.BAND_ANY + WifiP2pConfig.GROUP_OWNER_BAND_2GHZ -> SoftApConfigurationCompat.BAND_2GHZ + WifiP2pConfig.GROUP_OWNER_BAND_5GHZ -> SoftApConfigurationCompat.BAND_5GHZ else -> throw IllegalArgumentException("Unknown operatingBand") } - apChannel = RepeaterService.operatingChannel + channel = RepeaterService.operatingChannel } } } else { @@ -196,11 +201,13 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic P2pSupplicantConfiguration(group, binder?.thisDevice?.deviceAddress) } holder.config = config - return newWifiApConfiguration(group.networkName, config.psk).apply { - allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK) // is not actually used + return SoftApConfigurationCompat.empty().apply { + ssid = group.networkName + securityType = SoftApConfiguration.SECURITY_TYPE_WPA2_PSK // is not actually used + passphrase = config.psk if (Build.VERSION.SDK_INT >= 23) { - apBand = AP_BAND_ANY - apChannel = RepeaterService.operatingChannel + band = SoftApConfigurationCompat.BAND_ANY + channel = RepeaterService.operatingChannel } } } catch (e: RuntimeException) { @@ -210,20 +217,20 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic SmartSnackbar.make(R.string.repeater_configure_failure).show() return null } - private suspend fun updateConfiguration(config: WifiConfiguration) { + private suspend fun updateConfiguration(config: SoftApConfigurationCompat) { if (RepeaterService.safeMode) { - RepeaterService.networkName = config.SSID - RepeaterService.passphrase = config.preSharedKey - RepeaterService.operatingBand = when (config.apBand) { - AP_BAND_ANY -> WifiP2pConfig.GROUP_OWNER_BAND_AUTO - AP_BAND_2GHZ -> WifiP2pConfig.GROUP_OWNER_BAND_2GHZ - AP_BAND_5GHZ -> WifiP2pConfig.GROUP_OWNER_BAND_5GHZ - else -> throw IllegalArgumentException("Unknown apBand") + RepeaterService.networkName = config.ssid + RepeaterService.passphrase = config.passphrase + RepeaterService.operatingBand = when (config.band) { + SoftApConfigurationCompat.BAND_ANY -> WifiP2pConfig.GROUP_OWNER_BAND_AUTO + SoftApConfigurationCompat.BAND_2GHZ -> WifiP2pConfig.GROUP_OWNER_BAND_2GHZ + SoftApConfigurationCompat.BAND_5GHZ -> WifiP2pConfig.GROUP_OWNER_BAND_5GHZ + else -> throw IllegalArgumentException("Unknown band") } } else holder.config?.let { master -> val binder = binder - if (binder?.group?.networkName != config.SSID || master.psk != config.preSharedKey) try { - withContext(Dispatchers.Default) { master.update(config.SSID, config.preSharedKey) } + if (binder?.group?.networkName != config.ssid || master.psk != config.passphrase) try { + withContext(Dispatchers.Default) { master.update(config.ssid!!, config.passphrase!!) } (this.binder ?: binder)?.group = null } catch (e: Exception) { Timber.w(e) @@ -231,6 +238,6 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic } holder.config = null } - if (Build.VERSION.SDK_INT >= 23) RepeaterService.operatingChannel = config.apChannel + if (Build.VERSION.SDK_INT >= 23) RepeaterService.operatingChannel = config.channel } } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringFragment.kt index d3eb1267..a7d7e062 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringFragment.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringFragment.kt @@ -26,8 +26,8 @@ import be.mygod.vpnhotspot.net.TetherType import be.mygod.vpnhotspot.net.TetheringManager import be.mygod.vpnhotspot.net.TetheringManager.localOnlyTetheredIfaces import be.mygod.vpnhotspot.net.TetheringManager.tetheredIfaces +import be.mygod.vpnhotspot.net.wifi.WifiApDialogFragment import be.mygod.vpnhotspot.net.wifi.WifiApManager -import be.mygod.vpnhotspot.net.wifi.configuration.WifiApDialogFragment import be.mygod.vpnhotspot.util.ServiceForegroundConnector import be.mygod.vpnhotspot.util.broadcastReceiver import be.mygod.vpnhotspot.util.isNotGone diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/MacAddressCompat.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/MacAddressCompat.kt index a8c076a2..a2d35c90 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/MacAddressCompat.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/MacAddressCompat.kt @@ -1,14 +1,17 @@ package be.mygod.vpnhotspot.net import android.net.MacAddress +import android.os.Parcelable import androidx.annotation.RequiresApi +import kotlinx.android.parcel.Parcelize import java.nio.ByteBuffer import java.nio.ByteOrder /** * Compat support class for [MacAddress]. */ -inline class MacAddressCompat(val addr: Long) { +@Parcelize +inline class MacAddressCompat(val addr: Long) : Parcelable { companion object { private const val ETHER_ADDR_LEN = 6 /** @@ -28,17 +31,44 @@ inline class MacAddressCompat(val addr: Long) { return addr.joinToString(":") { "%02x".format(it) } } - fun fromString(addr: String) = MacAddressCompat(ByteBuffer.allocate(8).run { + /** + * Creates a MacAddress from the given byte array representation. + * A valid byte array representation for a MacAddress is a non-null array of length 6. + * + * @param addr a byte array representation of a MAC address. + * @return the MacAddress corresponding to the given byte array representation. + * @throws IllegalArgumentException if the given byte array is not a valid representation. + */ + fun fromBytes(addr: ByteArray): MacAddressCompat { + require(addr.size == ETHER_ADDR_LEN) { addr.joinToString() + " was not a valid MAC address" } + return ByteBuffer.allocate(Long.SIZE_BYTES).run { + put(addr) + rewind() + MacAddressCompat(long) + } + } + /** + * Creates a MacAddress from the given String representation. A valid String representation + * for a MacAddress is a series of 6 values in the range [0,ff] printed in hexadecimal + * and joined by ':' characters. + * + * @param addr a String representation of a MAC address. + * @return the MacAddress corresponding to the given String representation. + * @throws IllegalArgumentException if the given String is not a valid representation. + */ + fun fromString(addr: String) = ByteBuffer.allocate(Long.SIZE_BYTES).run { order(ByteOrder.LITTLE_ENDIAN) - mark() try { put(addr.split(':').map { Integer.parseInt(it, 16).toByte() }.toByteArray()) } catch (e: NumberFormatException) { throw IllegalArgumentException(e) } - reset() - long - }) + rewind() + MacAddressCompat(long) + } + + @RequiresApi(28) + fun MacAddress.toCompat() = fromBytes(toByteArray()) } fun toList() = ByteBuffer.allocate(8).run { diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/TetherTimeoutMonitor.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/TetherTimeoutMonitor.kt index 786f1044..bbb90cd7 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/TetherTimeoutMonitor.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/TetherTimeoutMonitor.kt @@ -35,10 +35,17 @@ class TetherTimeoutMonitor(private val context: Context, private val onTimeout: /** * Minimum limit to use for timeout delay if the value from overlay setting is too small. */ - private const val MIN_SOFT_AP_TIMEOUT_DELAY_MS = 600_000 // 10 minutes + @RequiresApi(21) + const val MIN_SOFT_AP_TIMEOUT_DELAY_MS = 600_000 // 10 minutes - private val enabled get() = Settings.Global.getInt(app.contentResolver, SOFT_AP_TIMEOUT_ENABLED, 1) == 1 - private val timeout by lazy { + @Deprecated("Use SoftApConfigurationCompat instead") + var enabled + get() = Settings.Global.getInt(app.contentResolver, SOFT_AP_TIMEOUT_ENABLED, 1) == 1 + set(value) { + check(Settings.Global.putInt(app.contentResolver, SOFT_AP_TIMEOUT_ENABLED, if (value) 1 else 0)) + } + @Deprecated("Use SoftApConfigurationCompat instead") + val timeout by lazy { val delay = try { app.resources.getInteger(Resources.getSystem().getIdentifier( "config_wifi_framework_soft_ap_timeout_delay", "integer", "android")) diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/configuration/P2pSupplicantConfiguration.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/P2pSupplicantConfiguration.kt similarity index 99% rename from mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/configuration/P2pSupplicantConfiguration.kt rename to mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/P2pSupplicantConfiguration.kt index 74fafa2e..6873d856 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/configuration/P2pSupplicantConfiguration.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/P2pSupplicantConfiguration.kt @@ -1,4 +1,4 @@ -package be.mygod.vpnhotspot.net.wifi.configuration +package be.mygod.vpnhotspot.net.wifi import android.net.wifi.p2p.WifiP2pGroup import android.os.Build diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/SoftApConfigurationCompat.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/SoftApConfigurationCompat.kt new file mode 100644 index 00000000..073ce00c --- /dev/null +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/SoftApConfigurationCompat.kt @@ -0,0 +1,306 @@ +package be.mygod.vpnhotspot.net.wifi + +import android.annotation.TargetApi +import android.net.MacAddress +import android.net.wifi.SoftApConfiguration +import android.os.Build +import android.os.Parcelable +import androidx.annotation.RequiresApi +import be.mygod.vpnhotspot.net.MacAddressCompat +import be.mygod.vpnhotspot.net.MacAddressCompat.Companion.toCompat +import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor +import kotlinx.android.parcel.Parcelize + +@Parcelize +data class SoftApConfigurationCompat( + var ssid: String?, + var securityType: Int, + var passphrase: String?, + @RequiresApi(23) + var band: Int, + @RequiresApi(23) + var channel: Int, + var bssid: MacAddressCompat?, + var maxNumberOfClients: Int, + @RequiresApi(28) + var shutdownTimeoutMillis: Long, + @RequiresApi(28) + var isAutoShutdownEnabled: Boolean, + var isClientControlByUserEnabled: Boolean, + var isHiddenSsid: Boolean, + // TODO: WifiClient? nullable? + var allowedClientList: List?, + var blockedClientList: List?, + val underlying: Parcelable? = null) : Parcelable { + companion object { + /** + * TODO + */ + const val BAND_ANY = -1 + const val BAND_2GHZ = 0 + const val BAND_5GHZ = 1 + const val BAND_6GHZ = 2 + const val CH_INVALID = 0 + + // TODO: localize? + val securityTypes = arrayOf("OPEN", "WPA2-PSK", "WPA3-SAE", "WPA3-SAE Transition mode") + + private val qrSanitizer = Regex("([\\\\\":;,])") + + /** + * The frequency which AP resides on (MHz). Resides in range [2412, 5815]. + */ + fun channelToFrequency(channel: Int) = when (channel) { + in 1..14 -> 2407 + 5 * channel + in 15..165 -> 5000 + 5 * channel + else -> throw IllegalArgumentException("Invalid channel $channel") + } + fun frequencyToChannel(frequency: Int) = when (frequency % 5) { + 2 -> ((frequency - 2407) / 5).also { check(it in 1..14) { "Invalid 2.4 GHz frequency $frequency" } } + 0 -> ((frequency - 5000) / 5).also { check(it in 15..165) { "Invalid 5 GHz frequency $frequency" } } + else -> throw IllegalArgumentException("Invalid frequency $frequency") + } + + /** + * apBand and apChannel is available since API 23. + * + * https://android.googlesource.com/platform/frameworks/base/+/android-6.0.0_r1/wifi/java/android/net/wifi/WifiConfiguration.java#242 + */ + @get:RequiresApi(23) + @Suppress("DEPRECATION") + /** + * The band which AP resides on + * -1:Any 0:2G 1:5G + * By default, 2G is chosen + */ + private val apBand by lazy { android.net.wifi.WifiConfiguration::class.java.getDeclaredField("apBand") } + @get:RequiresApi(23) + @Suppress("DEPRECATION") + /** + * The channel which AP resides on + * 2G 1-11 + * 5G 36,40,44,48,149,153,157,161,165 + * 0 - find a random available channel according to the apBand + */ + private val apChannel by lazy { + android.net.wifi.WifiConfiguration::class.java.getDeclaredField("apChannel") + } + + @get:RequiresApi(30) + private val getAllowedClientList by lazy { + SoftApConfiguration::class.java.getDeclaredMethod("getAllowedClientList") + } + @get:RequiresApi(30) + private val getBand by lazy { SoftApConfiguration::class.java.getDeclaredMethod("getBand") } + @get:RequiresApi(30) + private val getBlockedClientList by lazy { + SoftApConfiguration::class.java.getDeclaredMethod("getBlockedClientList") + } + @get:RequiresApi(30) + private val getChannel by lazy { SoftApConfiguration::class.java.getDeclaredMethod("getChannel") } + @get:RequiresApi(30) + private val getMaxNumberOfClients by lazy { + SoftApConfiguration::class.java.getDeclaredMethod("getMaxNumberOfClients") + } + @get:RequiresApi(30) + private val getShutdownTimeoutMillis by lazy { + SoftApConfiguration::class.java.getDeclaredMethod("getShutdownTimeoutMillis") + } + @get:RequiresApi(30) + private val isAutoShutdownEnabled by lazy { + SoftApConfiguration::class.java.getDeclaredMethod("isAutoShutdownEnabled") + } + @get:RequiresApi(30) + private val isClientControlByUserEnabled by lazy { + SoftApConfiguration::class.java.getDeclaredMethod("isClientControlByUserEnabled") + } + + @get:RequiresApi(30) + private val classBuilder by lazy { Class.forName("android.net.wifi.SoftApConfiguration\$Builder") } + @get:RequiresApi(30) + private val newBuilder by lazy { classBuilder.getConstructor(SoftApConfiguration::class.java) } + @get:RequiresApi(30) + private val build by lazy { classBuilder.getDeclaredMethod("build") } + @get:RequiresApi(30) + private val setAllowedClientList by lazy { + classBuilder.getDeclaredMethod("setAllowedClientList", java.util.List::class.java) + } + @get:RequiresApi(30) + private val setAutoShutdownEnabled by lazy { + classBuilder.getDeclaredMethod("setAutoShutdownEnabled", Boolean::class.java) + } + @get:RequiresApi(30) + private val setBand by lazy { classBuilder.getDeclaredMethod("setBand", Int::class.java) } + @get:RequiresApi(30) + private val setBlockedClientList by lazy { + classBuilder.getDeclaredMethod("setBlockedClientList", java.util.List::class.java) + } + @get:RequiresApi(30) + private val setBssid by lazy @TargetApi(30) { + classBuilder.getDeclaredMethod("setBssid", MacAddress::class.java) + } + @get:RequiresApi(30) + private val setChannel by lazy { classBuilder.getDeclaredMethod("setChannel", Int::class.java) } + @get:RequiresApi(30) + private val setClientControlByUserEnabled by lazy { + classBuilder.getDeclaredMethod("setClientControlByUserEnabled", Boolean::class.java) + } + @get:RequiresApi(30) + private val setHiddenSsid by lazy { classBuilder.getDeclaredMethod("setHiddenSsid", Boolean::class.java) } + @get:RequiresApi(30) + private val setMaxNumberOfClients by lazy { + classBuilder.getDeclaredMethod("setMaxNumberOfClients", Int::class.java) + } + @get:RequiresApi(30) + private val setPassphrase by lazy { classBuilder.getDeclaredMethod("setPassphrase", String::class.java) } + @get:RequiresApi(30) + private val setShutdownTimeoutMillis by lazy { + classBuilder.getDeclaredMethod("setShutdownTimeoutMillis", Long::class.java) + } + @get:RequiresApi(30) + private val setSsid by lazy { classBuilder.getDeclaredMethod("setSsid", String::class.java) } + + @Deprecated("Class deprecated in framework") + @Suppress("DEPRECATION") + fun android.net.wifi.WifiConfiguration.toCompat() = SoftApConfigurationCompat( + SSID, + allowedKeyManagement.nextSetBit(0).let { selected -> + require(allowedKeyManagement.nextSetBit(selected + 1) < 0) { + "More than 1 key managements supplied: $allowedKeyManagement" + } + when (if (selected < 0) -1 else selected) { + -1, // getAuthType returns NONE if nothing is selected + android.net.wifi.WifiConfiguration.KeyMgmt.NONE -> SoftApConfiguration.SECURITY_TYPE_OPEN + android.net.wifi.WifiConfiguration.KeyMgmt.WPA_PSK, + 4, // WPA2_PSK + 11 -> { // WPA_PSK_SHA256 + SoftApConfiguration.SECURITY_TYPE_WPA2_PSK + } + android.net.wifi.WifiConfiguration.KeyMgmt.SAE -> SoftApConfiguration.SECURITY_TYPE_WPA3_SAE + // TODO: check source code + else -> throw IllegalArgumentException("Unrecognized key management: $allowedKeyManagement") + } + }, + preSharedKey, + if (Build.VERSION.SDK_INT >= 23) apBand.getInt(this) else BAND_ANY, // TODO + if (Build.VERSION.SDK_INT >= 23) apChannel.getInt(this) else CH_INVALID, // TODO + BSSID?.let { MacAddressCompat.fromString(it) }, + 0, // TODO: unsupported field should have @RequiresApi? + if (Build.VERSION.SDK_INT >= 28) { + TetherTimeoutMonitor.timeout.toLong() + } else TetherTimeoutMonitor.MIN_SOFT_AP_TIMEOUT_DELAY_MS.toLong(), + if (Build.VERSION.SDK_INT >= 28) TetherTimeoutMonitor.enabled else false, + false, // TODO + hiddenSSID, + null, + null, + this) + + @RequiresApi(30) + @Suppress("UNCHECKED_CAST") + fun SoftApConfiguration.toCompat() = SoftApConfigurationCompat( + ssid, + securityType, + passphrase, + getBand(this) as Int, + getChannel(this) as Int, + bssid?.toCompat(), + getMaxNumberOfClients(this) as Int, + getShutdownTimeoutMillis(this) as Long, + isAutoShutdownEnabled(this) as Boolean, + isClientControlByUserEnabled(this) as Boolean, + isHiddenSsid, + getAllowedClientList(this) as List?, + getBlockedClientList(this) as List?, + this) + + fun empty() = SoftApConfigurationCompat( + null, SoftApConfiguration.SECURITY_TYPE_OPEN, null, BAND_ANY, CH_INVALID, null, 0, + if (Build.VERSION.SDK_INT >= 28) { + TetherTimeoutMonitor.timeout.toLong() + } else TetherTimeoutMonitor.MIN_SOFT_AP_TIMEOUT_DELAY_MS.toLong(), + if (Build.VERSION.SDK_INT >= 28) TetherTimeoutMonitor.enabled else false, false, false, null, null) + } + + /** + * Based on: + * https://android.googlesource.com/platform/packages/apps/Settings/+/android-5.0.0_r1/src/com/android/settings/wifi/WifiApDialog.java#88 + * https://android.googlesource.com/platform/packages/apps/Settings/+/b1af85d/src/com/android/settings/wifi/tether/WifiTetherSettings.java#162 + */ + @Deprecated("Class deprecated in framework") + @Suppress("DEPRECATION") + fun toWifiConfiguration(): android.net.wifi.WifiConfiguration { + val wc = underlying as? android.net.wifi.WifiConfiguration + val result = if (wc == null) android.net.wifi.WifiConfiguration() else android.net.wifi.WifiConfiguration(wc) + val original = wc?.toCompat() + result.SSID = ssid + if (original?.securityType != securityType) { + result.allowedKeyManagement.clear() + result.allowedKeyManagement.set(when (securityType) { + SoftApConfiguration.SECURITY_TYPE_OPEN -> android.net.wifi.WifiConfiguration.KeyMgmt.NONE + // not actually used on API 30- + SoftApConfiguration.SECURITY_TYPE_WPA2_PSK -> android.net.wifi.WifiConfiguration.KeyMgmt.WPA_PSK + SoftApConfiguration.SECURITY_TYPE_WPA3_SAE, + SoftApConfiguration.SECURITY_TYPE_WPA3_SAE_TRANSITION -> android.net.wifi.WifiConfiguration.KeyMgmt.SAE + else -> throw IllegalArgumentException("Unsupported securityType $securityType") + }) + result.allowedAuthAlgorithms.clear() + result.allowedAuthAlgorithms.set(android.net.wifi.WifiConfiguration.AuthAlgorithm.OPEN) + } + result.preSharedKey = passphrase + if (Build.VERSION.SDK_INT >= 23) { + apBand.setInt(result, band) + apChannel.setInt(result, channel) + } + if (bssid != original?.bssid) result.BSSID = bssid?.toString() + result.hiddenSSID = isHiddenSsid + return result + } + + @RequiresApi(30) + fun toPlatform(): SoftApConfiguration { + val sac = underlying as? SoftApConfiguration + // TODO: can we always call copy constructor? + val builder = if (sac == null) classBuilder.newInstance() else newBuilder.newInstance(sac) + setSsid(builder, ssid) + // TODO: setSecurityType + setPassphrase(builder, passphrase) + setBand(builder, band) + setChannel(builder, channel) + setBssid(builder, bssid?.toPlatform()) + setMaxNumberOfClients(builder, maxNumberOfClients) + setShutdownTimeoutMillis(builder, shutdownTimeoutMillis) + setAutoShutdownEnabled(builder, isAutoShutdownEnabled) + setClientControlByUserEnabled(builder, isClientControlByUserEnabled) + setHiddenSsid(builder, isHiddenSsid) + setAllowedClientList(builder, allowedClientList) + setBlockedClientList(builder, blockedClientList) + return build(builder) as SoftApConfiguration + } + + /** + * Documentation: https://github.com/zxing/zxing/wiki/Barcode-Contents#wi-fi-network-config-android-ios-11 + * Based on: https://android.googlesource.com/platform/packages/apps/Settings/+/4a5ff58/src/com/android/settings/wifi/dpp/WifiNetworkConfig.java#161 + */ + fun toQrCode() = StringBuilder("WIFI:").apply { + fun String.sanitize() = qrSanitizer.replace(this) { "\\${it.groupValues[1]}" } + when (securityType) { + SoftApConfiguration.SECURITY_TYPE_OPEN -> { } + SoftApConfiguration.SECURITY_TYPE_WPA2_PSK -> append("T:WPA;") + SoftApConfiguration.SECURITY_TYPE_WPA3_SAE, SoftApConfiguration.SECURITY_TYPE_WPA3_SAE_TRANSITION -> { + append("T:SAE;") + } + else -> throw IllegalArgumentException("Unsupported authentication type") + } + append("S:") + append(ssid!!.sanitize()) + append(';') + passphrase?.let { passphrase -> + append("P:") + append(passphrase.sanitize()) + append(';') + } + if (isHiddenSsid) append("H:true;") + append(';') + }.toString() +} diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/configuration/WifiApDialogFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApDialogFragment.kt similarity index 76% rename from mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/configuration/WifiApDialogFragment.kt rename to mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApDialogFragment.kt index b5ce9f19..b4bec538 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/configuration/WifiApDialogFragment.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApDialogFragment.kt @@ -1,10 +1,9 @@ -package be.mygod.vpnhotspot.net.wifi.configuration +package be.mygod.vpnhotspot.net.wifi import android.annotation.SuppressLint -import android.annotation.TargetApi import android.content.ClipData import android.content.DialogInterface -import android.net.wifi.WifiConfiguration +import android.net.wifi.SoftApConfiguration import android.os.Build import android.os.Parcelable import android.text.Editable @@ -14,8 +13,10 @@ import android.view.MenuItem import android.view.View import android.widget.AdapterView import android.widget.ArrayAdapter +import androidx.annotation.RequiresApi import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.Toolbar +import androidx.core.os.BuildCompat import androidx.core.view.isGone import be.mygod.vpnhotspot.AlertDialogFragment import be.mygod.vpnhotspot.App.Companion.app @@ -43,7 +44,7 @@ class WifiApDialogFragment : AlertDialogFragment private var started = false - private val selectedSecurity get() = - if (arg.p2pMode) WifiConfiguration.KeyMgmt.WPA_PSK else dialogView.security.selectedItemPosition - override val ret get() = Arg(WifiConfiguration().apply { - SSID = dialogView.ssid.text.toString() - allowedKeyManagement.set(selectedSecurity) - allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN) - if (dialogView.password.length() != 0) preSharedKey = dialogView.password.text.toString() + override val ret get() = Arg(arg.configuration.copy( + ssid = dialogView.ssid.text.toString(), + passphrase = if (dialogView.password.length() != 0) dialogView.password.text.toString() else null).apply { + if (!arg.p2pMode) securityType = dialogView.security.selectedItemPosition if (Build.VERSION.SDK_INT >= 23) { val bandOption = dialogView.band.selectedItem as BandOption - apBand = bandOption.apBand - apChannel = bandOption.apChannel + band = bandOption.band + channel = bandOption.channel } }) @@ -101,13 +104,13 @@ class WifiApDialogFragment : AlertDialogFragment?) = error("Must select something") override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { - dialogView.passwordWrapper.isGone = position == WifiConfiguration.KeyMgmt.NONE + dialogView.passwordWrapper.isGone = position == SoftApConfiguration.SECURITY_TYPE_OPEN } } } @@ -124,6 +127,7 @@ class WifiApDialogFragment : AlertDialogFragment= 28) add(BandOption.BandAny) add(BandOption.Band2GHz) add(BandOption.Band5GHz) + if (BuildCompat.isAtLeastR()) add (BandOption.Band6GHz) } addAll(channels) } @@ -131,21 +135,22 @@ class WifiApDialogFragment : AlertDialogFragment= 23) { - dialogView.band.setSelection(if (configuration.apChannel in 1..165) { - bandOptions.indexOfFirst { it.apChannel == configuration.apChannel } - } else bandOptions.indexOfFirst { it.apBand == configuration.apBand }) + dialogView.band.setSelection(if (configuration.channel in 1..165) { + bandOptions.indexOfFirst { it.channel == configuration.channel } + } else bandOptions.indexOfFirst { it.band == configuration.band }) } + // TODO support more fields from SACC } override fun onStart() { @@ -163,8 +168,12 @@ class WifiApDialogFragment : AlertDialogFragment dialogView.password.length() >= 8 + // TODO + SoftApConfiguration.SECURITY_TYPE_WPA2_PSK -> dialogView.password.length() >= 8 else -> true // do not try to validate } dialogView.passwordWrapper.error = if (passwordValid) null else { @@ -187,7 +196,7 @@ class WifiApDialogFragment : AlertDialogFragment try { app.clipboard.primaryClip?.getItemAt(0)?.text?.apply { - Base64.decode(toString(), BASE64_FLAGS).toParcelable() + Base64.decode(toString(), BASE64_FLAGS).toParcelable() ?.let { populateFromConfiguration(it) } } true @@ -197,7 +206,7 @@ class WifiApDialogFragment : AlertDialogFragment { val qrString = try { - ret.configuration.toQRString() + ret.configuration.toQrCode() } catch (e: IllegalArgumentException) { SmartSnackbar.make(e).show() return false @@ -212,7 +221,7 @@ class WifiApDialogFragment : AlertDialogFragment 2407 + 5 * channel - in 15..165 -> 5000 + 5 * channel - else -> throw IllegalArgumentException("Invalid channel $channel") -} -fun frequencyToChannel(frequency: Int) = when (frequency % 5) { - 2 -> ((frequency - 2407) / 5).also { check(it in 1..14) { "Invalid 2.4 GHz frequency $frequency" } } - 0 -> ((frequency - 5000) / 5).also { check(it in 15..165) { "Invalid 5 GHz frequency $frequency" } } - else -> throw IllegalArgumentException("Invalid frequency $frequency") -} - -val WifiConfiguration.apKeyManagement get() = allowedKeyManagement.nextSetBit(0).let { selected -> - check(allowedKeyManagement.nextSetBit(selected + 1) < 0) { - "More than 1 key managements supplied: $allowedKeyManagement" - } - if (selected < 0) WifiConfiguration.KeyMgmt.NONE else selected // getAuthType returns NONE if nothing is selected -} - -private val qrSanitizer = Regex("([\\\\\":;,])") -/** - * Documentation: https://github.com/zxing/zxing/wiki/Barcode-Contents#wi-fi-network-config-android-ios-11 - * Based on: https://android.googlesource.com/platform/packages/apps/Settings/+/4a5ff58/src/com/android/settings/wifi/dpp/WifiNetworkConfig.java#161 - */ -fun WifiConfiguration.toQRString() = StringBuilder("WIFI:").apply { - fun String.sanitize() = qrSanitizer.replace(this) { "\\${it.groupValues[1]}" } - var password: String? = preSharedKey - when (apKeyManagement) { - WifiConfiguration.KeyMgmt.NONE -> if (wepKeys != null) { - password = wepKeys[0] - append("T:WEP;") - } - WifiConfiguration.KeyMgmt.WPA_PSK, WifiConfiguration.KeyMgmt.WPA_EAP, WPA2_PSK -> append("T:WPA;") - WifiConfiguration.KeyMgmt.SAE -> append("T:SAE;") - else -> throw IllegalArgumentException("Unsupported authentication type") - } - append("S:") - append(SSID.sanitize()) - append(';') - if (password != null) { - append("P:") - append(password.sanitize()) - append(';') - } - if (hiddenSSID) append("H:true;") - append(';') -}.toString() - -/** - * Based on: - * https://android.googlesource.com/platform/packages/apps/Settings/+/android-5.0.0_r1/src/com/android/settings/wifi/WifiApDialog.java#88 - * https://android.googlesource.com/platform/packages/apps/Settings/+/b1af85d/src/com/android/settings/wifi/tether/WifiTetherSettings.java#162 - */ -fun newWifiApConfiguration(ssid: String?, passphrase: String?) = try { - WifiApManager.configuration -} catch (e: InvocationTargetException) { - if (e.targetException !is SecurityException) Timber.w(e) - WifiConfiguration() -}.apply { - SSID = ssid - preSharedKey = passphrase - allowedKeyManagement.clear() - allowedAuthAlgorithms.clear() - allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN) -} diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml index 61639c14..2a70fd19 100644 --- a/mobile/src/main/res/values/strings.xml +++ b/mobile/src/main/res/values/strings.xml @@ -162,6 +162,7 @@ Auto 2.4 GHz Band 5.0 GHz Band + 6.0 GHz Band Save