First draft

This commit is contained in:
Mygod
2020-06-02 20:04:05 -04:00
parent 0366fc6bc6
commit 6ab763013b
14 changed files with 474 additions and 213 deletions

View File

@@ -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;->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;->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` * (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;->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/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` * `Landroid/net/wifi/p2p/WifiP2pGroupList;->getGroupList()Ljava/util/List;,system-api,whitelist`

View File

@@ -5,12 +5,14 @@ import android.content.IntentFilter
import android.net.wifi.WifiManager import android.net.wifi.WifiManager
import android.os.Build import android.os.Build
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.os.BuildCompat
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.net.IpNeighbour import be.mygod.vpnhotspot.net.IpNeighbour
import be.mygod.vpnhotspot.net.TetheringManager import be.mygod.vpnhotspot.net.TetheringManager
import be.mygod.vpnhotspot.net.TetheringManager.localOnlyTetheredIfaces import be.mygod.vpnhotspot.net.TetheringManager.localOnlyTetheredIfaces
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor 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.net.wifi.WifiApManager
import be.mygod.vpnhotspot.util.StickyEvent1 import be.mygod.vpnhotspot.util.StickyEvent1
import be.mygod.vpnhotspot.util.broadcastReceiver import be.mygod.vpnhotspot.util.broadcastReceiver
@@ -31,7 +33,11 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
} }
val ifaceChanged = StickyEvent1 { iface } 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() { fun stop() {
when (iface) { when (iface) {

View File

@@ -18,12 +18,12 @@ import androidx.core.content.getSystemService
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.net.MacAddressCompat import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor 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
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.deletePersistentGroup import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.deletePersistentGroup
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.requestPersistentGroupInfo import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.requestPersistentGroupInfo
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.setWifiP2pChannels import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.setWifiP2pChannels
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.startWps 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.util.*
import be.mygod.vpnhotspot.widget.SmartSnackbar import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.* import kotlinx.coroutines.*
@@ -315,7 +315,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
setPassphrase(passphrase) setPassphrase(passphrase)
operatingChannel.let { oc -> operatingChannel.let { oc ->
if (oc == 0) setGroupOperatingBand(operatingBand) if (oc == 0) setGroupOperatingBand(operatingBand)
else setGroupOperatingFrequency(channelToFrequency(oc)) else setGroupOperatingFrequency(SoftApConfigurationCompat.channelToFrequency(oc))
} }
}.build().run { }.build().run {
useParcel { p -> useParcel { p ->

View File

@@ -47,7 +47,7 @@ class LocalOnlyHotspotTileService : KillableTileService() {
label = getText(R.string.tethering_temp_hotspot) label = getText(R.string.tethering_temp_hotspot)
} else { } else {
state = Tile.STATE_ACTIVE 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() updateTile()
} }

View File

@@ -5,7 +5,7 @@ import android.content.ComponentName
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.content.ServiceConnection 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.WifiP2pConfig
import android.net.wifi.p2p.WifiP2pGroup import android.net.wifi.p2p.WifiP2pGroup
import android.os.Build import android.os.Build
@@ -26,7 +26,9 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import be.mygod.vpnhotspot.* import be.mygod.vpnhotspot.*
import be.mygod.vpnhotspot.databinding.ListitemRepeaterBinding 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.ServiceForegroundConnector
import be.mygod.vpnhotspot.util.formatAddresses import be.mygod.vpnhotspot.util.formatAddresses
import be.mygod.vpnhotspot.util.showAllowingStateLoss import be.mygod.vpnhotspot.util.showAllowingStateLoss
@@ -60,7 +62,8 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
val title: CharSequence @Bindable get() { val title: CharSequence @Bindable get() {
if (Build.VERSION.SDK_INT >= 29) binder?.group?.frequency?.let { 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) return parent.getString(R.string.title_repeater)
} }
@@ -173,20 +176,22 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
} }
@MainThread @MainThread
private suspend fun getConfiguration(): WifiConfiguration? { private suspend fun getConfiguration(): SoftApConfigurationCompat? {
if (RepeaterService.safeMode) { if (RepeaterService.safeMode) {
val networkName = RepeaterService.networkName val networkName = RepeaterService.networkName
val passphrase = RepeaterService.passphrase val passphrase = RepeaterService.passphrase
if (networkName != null && passphrase != null) { if (networkName != null && passphrase != null) {
return newWifiApConfiguration(networkName, passphrase).apply { return SoftApConfigurationCompat.empty().apply {
allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK) // is not actually used ssid = networkName
apBand = when (RepeaterService.operatingBand) { securityType = SoftApConfiguration.SECURITY_TYPE_WPA2_PSK // is not actually used
WifiP2pConfig.GROUP_OWNER_BAND_AUTO -> AP_BAND_ANY this.passphrase = passphrase
WifiP2pConfig.GROUP_OWNER_BAND_2GHZ -> AP_BAND_2GHZ band = when (RepeaterService.operatingBand) {
WifiP2pConfig.GROUP_OWNER_BAND_5GHZ -> AP_BAND_5GHZ 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") else -> throw IllegalArgumentException("Unknown operatingBand")
} }
apChannel = RepeaterService.operatingChannel channel = RepeaterService.operatingChannel
} }
} }
} else { } else {
@@ -196,11 +201,13 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
P2pSupplicantConfiguration(group, binder?.thisDevice?.deviceAddress) P2pSupplicantConfiguration(group, binder?.thisDevice?.deviceAddress)
} }
holder.config = config holder.config = config
return newWifiApConfiguration(group.networkName, config.psk).apply { return SoftApConfigurationCompat.empty().apply {
allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK) // is not actually used ssid = group.networkName
securityType = SoftApConfiguration.SECURITY_TYPE_WPA2_PSK // is not actually used
passphrase = config.psk
if (Build.VERSION.SDK_INT >= 23) { if (Build.VERSION.SDK_INT >= 23) {
apBand = AP_BAND_ANY band = SoftApConfigurationCompat.BAND_ANY
apChannel = RepeaterService.operatingChannel channel = RepeaterService.operatingChannel
} }
} }
} catch (e: RuntimeException) { } catch (e: RuntimeException) {
@@ -210,20 +217,20 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
SmartSnackbar.make(R.string.repeater_configure_failure).show() SmartSnackbar.make(R.string.repeater_configure_failure).show()
return null return null
} }
private suspend fun updateConfiguration(config: WifiConfiguration) { private suspend fun updateConfiguration(config: SoftApConfigurationCompat) {
if (RepeaterService.safeMode) { if (RepeaterService.safeMode) {
RepeaterService.networkName = config.SSID RepeaterService.networkName = config.ssid
RepeaterService.passphrase = config.preSharedKey RepeaterService.passphrase = config.passphrase
RepeaterService.operatingBand = when (config.apBand) { RepeaterService.operatingBand = when (config.band) {
AP_BAND_ANY -> WifiP2pConfig.GROUP_OWNER_BAND_AUTO SoftApConfigurationCompat.BAND_ANY -> WifiP2pConfig.GROUP_OWNER_BAND_AUTO
AP_BAND_2GHZ -> WifiP2pConfig.GROUP_OWNER_BAND_2GHZ SoftApConfigurationCompat.BAND_2GHZ -> WifiP2pConfig.GROUP_OWNER_BAND_2GHZ
AP_BAND_5GHZ -> WifiP2pConfig.GROUP_OWNER_BAND_5GHZ SoftApConfigurationCompat.BAND_5GHZ -> WifiP2pConfig.GROUP_OWNER_BAND_5GHZ
else -> throw IllegalArgumentException("Unknown apBand") else -> throw IllegalArgumentException("Unknown band")
} }
} else holder.config?.let { master -> } else holder.config?.let { master ->
val binder = binder val binder = binder
if (binder?.group?.networkName != config.SSID || master.psk != config.preSharedKey) try { if (binder?.group?.networkName != config.ssid || master.psk != config.passphrase) try {
withContext(Dispatchers.Default) { master.update(config.SSID, config.preSharedKey) } withContext(Dispatchers.Default) { master.update(config.ssid!!, config.passphrase!!) }
(this.binder ?: binder)?.group = null (this.binder ?: binder)?.group = null
} catch (e: Exception) { } catch (e: Exception) {
Timber.w(e) Timber.w(e)
@@ -231,6 +238,6 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
} }
holder.config = null holder.config = null
} }
if (Build.VERSION.SDK_INT >= 23) RepeaterService.operatingChannel = config.apChannel if (Build.VERSION.SDK_INT >= 23) RepeaterService.operatingChannel = config.channel
} }
} }

View File

@@ -26,8 +26,8 @@ import be.mygod.vpnhotspot.net.TetherType
import be.mygod.vpnhotspot.net.TetheringManager import be.mygod.vpnhotspot.net.TetheringManager
import be.mygod.vpnhotspot.net.TetheringManager.localOnlyTetheredIfaces import be.mygod.vpnhotspot.net.TetheringManager.localOnlyTetheredIfaces
import be.mygod.vpnhotspot.net.TetheringManager.tetheredIfaces 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.WifiApManager
import be.mygod.vpnhotspot.net.wifi.configuration.WifiApDialogFragment
import be.mygod.vpnhotspot.util.ServiceForegroundConnector import be.mygod.vpnhotspot.util.ServiceForegroundConnector
import be.mygod.vpnhotspot.util.broadcastReceiver import be.mygod.vpnhotspot.util.broadcastReceiver
import be.mygod.vpnhotspot.util.isNotGone import be.mygod.vpnhotspot.util.isNotGone

View File

@@ -1,14 +1,17 @@
package be.mygod.vpnhotspot.net package be.mygod.vpnhotspot.net
import android.net.MacAddress import android.net.MacAddress
import android.os.Parcelable
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import kotlinx.android.parcel.Parcelize
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.ByteOrder import java.nio.ByteOrder
/** /**
* Compat support class for [MacAddress]. * Compat support class for [MacAddress].
*/ */
inline class MacAddressCompat(val addr: Long) { @Parcelize
inline class MacAddressCompat(val addr: Long) : Parcelable {
companion object { companion object {
private const val ETHER_ADDR_LEN = 6 private const val ETHER_ADDR_LEN = 6
/** /**
@@ -28,17 +31,44 @@ inline class MacAddressCompat(val addr: Long) {
return addr.joinToString(":") { "%02x".format(it) } 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) order(ByteOrder.LITTLE_ENDIAN)
mark()
try { try {
put(addr.split(':').map { Integer.parseInt(it, 16).toByte() }.toByteArray()) put(addr.split(':').map { Integer.parseInt(it, 16).toByte() }.toByteArray())
} catch (e: NumberFormatException) { } catch (e: NumberFormatException) {
throw IllegalArgumentException(e) throw IllegalArgumentException(e)
} }
reset() rewind()
long MacAddressCompat(long)
}) }
@RequiresApi(28)
fun MacAddress.toCompat() = fromBytes(toByteArray())
} }
fun toList() = ByteBuffer.allocate(8).run { fun toList() = ByteBuffer.allocate(8).run {

View File

@@ -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. * 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 @Deprecated("Use SoftApConfigurationCompat instead")
private val timeout by lazy { 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 { val delay = try {
app.resources.getInteger(Resources.getSystem().getIdentifier( app.resources.getInteger(Resources.getSystem().getIdentifier(
"config_wifi_framework_soft_ap_timeout_delay", "integer", "android")) "config_wifi_framework_soft_ap_timeout_delay", "integer", "android"))

View File

@@ -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.net.wifi.p2p.WifiP2pGroup
import android.os.Build import android.os.Build

View File

@@ -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<Parcelable>?,
var blockedClientList: List<Parcelable>?,
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<Parcelable>?,
getBlockedClientList(this) as List<Parcelable>?,
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()
}

View File

@@ -1,10 +1,9 @@
package be.mygod.vpnhotspot.net.wifi.configuration package be.mygod.vpnhotspot.net.wifi
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.content.ClipData import android.content.ClipData
import android.content.DialogInterface import android.content.DialogInterface
import android.net.wifi.WifiConfiguration import android.net.wifi.SoftApConfiguration
import android.os.Build import android.os.Build
import android.os.Parcelable import android.os.Parcelable
import android.text.Editable import android.text.Editable
@@ -14,8 +13,10 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.AdapterView import android.widget.AdapterView
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.core.os.BuildCompat
import androidx.core.view.isGone import androidx.core.view.isGone
import be.mygod.vpnhotspot.AlertDialogFragment import be.mygod.vpnhotspot.AlertDialogFragment
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
@@ -43,7 +44,7 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
} }
@Parcelize @Parcelize
data class Arg(val configuration: WifiConfiguration, data class Arg(val configuration: SoftApConfigurationCompat,
val readOnly: Boolean = false, val readOnly: Boolean = false,
/** /**
* KeyMgmt is enforced to WPA_PSK. * KeyMgmt is enforced to WPA_PSK.
@@ -51,41 +52,43 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
*/ */
val p2pMode: Boolean = false) : Parcelable val p2pMode: Boolean = false) : Parcelable
@TargetApi(23) @RequiresApi(23)
private sealed class BandOption { private sealed class BandOption {
open val apBand get() = AP_BAND_2GHZ open val band get() = SoftApConfigurationCompat.BAND_ANY
open val apChannel get() = 0 open val channel get() = SoftApConfigurationCompat.CH_INVALID
object BandAny : BandOption() { object BandAny : BandOption() {
override val apBand get() = AP_BAND_ANY
override fun toString() = app.getString(R.string.wifi_ap_choose_auto) override fun toString() = app.getString(R.string.wifi_ap_choose_auto)
} }
object Band2GHz : BandOption() { object Band2GHz : BandOption() {
override val band get() = SoftApConfigurationCompat.BAND_2GHZ
override fun toString() = app.getString(R.string.wifi_ap_choose_2G) override fun toString() = app.getString(R.string.wifi_ap_choose_2G)
} }
object Band5GHz : BandOption() { object Band5GHz : BandOption() {
override val apBand get() = AP_BAND_5GHZ override val band get() = SoftApConfigurationCompat.BAND_5GHZ
override fun toString() = app.getString(R.string.wifi_ap_choose_5G) override fun toString() = app.getString(R.string.wifi_ap_choose_5G)
} }
class Channel(override val apChannel: Int) : BandOption() { @RequiresApi(30)
override fun toString() = "${channelToFrequency(apChannel)} MHz ($apChannel)" object Band6GHz : BandOption() {
override val band get() = SoftApConfigurationCompat.BAND_6GHZ
override fun toString() = app.getString(R.string.wifi_ap_choose_6G)
}
class Channel(override val channel: Int) : BandOption() {
override fun toString() = "${SoftApConfigurationCompat.channelToFrequency(channel)} MHz ($channel)"
} }
} }
private lateinit var dialogView: DialogWifiApBinding private lateinit var dialogView: DialogWifiApBinding
private lateinit var bandOptions: MutableList<BandOption> private lateinit var bandOptions: MutableList<BandOption>
private var started = false private var started = false
private val selectedSecurity get() = override val ret get() = Arg(arg.configuration.copy(
if (arg.p2pMode) WifiConfiguration.KeyMgmt.WPA_PSK else dialogView.security.selectedItemPosition ssid = dialogView.ssid.text.toString(),
override val ret get() = Arg(WifiConfiguration().apply { passphrase = if (dialogView.password.length() != 0) dialogView.password.text.toString() else null).apply {
SSID = dialogView.ssid.text.toString() if (!arg.p2pMode) securityType = dialogView.security.selectedItemPosition
allowedKeyManagement.set(selectedSecurity)
allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN)
if (dialogView.password.length() != 0) preSharedKey = dialogView.password.text.toString()
if (Build.VERSION.SDK_INT >= 23) { if (Build.VERSION.SDK_INT >= 23) {
val bandOption = dialogView.band.selectedItem as BandOption val bandOption = dialogView.band.selectedItem as BandOption
apBand = bandOption.apBand band = bandOption.band
apChannel = bandOption.apChannel channel = bandOption.channel
} }
}) })
@@ -101,13 +104,13 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
if (!arg.readOnly) dialogView.ssid.addTextChangedListener(this@WifiApDialogFragment) if (!arg.readOnly) dialogView.ssid.addTextChangedListener(this@WifiApDialogFragment)
if (arg.p2pMode) dialogView.securityWrapper.isGone = true else dialogView.security.apply { if (arg.p2pMode) dialogView.securityWrapper.isGone = true else dialogView.security.apply {
adapter = ArrayAdapter(activity, android.R.layout.simple_spinner_item, 0, adapter = ArrayAdapter(activity, android.R.layout.simple_spinner_item, 0,
WifiConfiguration.KeyMgmt.strings).apply { SoftApConfigurationCompat.securityTypes).apply {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
} }
onItemSelectedListener = object : AdapterView.OnItemSelectedListener { onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onNothingSelected(parent: AdapterView<*>?) = error("Must select something") override fun onNothingSelected(parent: AdapterView<*>?) = error("Must select something")
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { 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<WifiApDialogFragment.Arg, WifiA
if (Build.VERSION.SDK_INT >= 28) add(BandOption.BandAny) if (Build.VERSION.SDK_INT >= 28) add(BandOption.BandAny)
add(BandOption.Band2GHz) add(BandOption.Band2GHz)
add(BandOption.Band5GHz) add(BandOption.Band5GHz)
if (BuildCompat.isAtLeastR()) add (BandOption.Band6GHz)
} }
addAll(channels) addAll(channels)
} }
@@ -131,21 +135,22 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
} }
if (Build.VERSION.SDK_INT < 23) { if (Build.VERSION.SDK_INT < 23) {
setSelection(bandOptions.indexOfFirst { it.apChannel == RepeaterService.operatingChannel }) setSelection(bandOptions.indexOfFirst { it.channel == RepeaterService.operatingChannel })
} }
} else dialogView.bandWrapper.isGone = true } else dialogView.bandWrapper.isGone = true
populateFromConfiguration(arg.configuration) populateFromConfiguration(arg.configuration)
} }
private fun populateFromConfiguration(configuration: WifiConfiguration) { private fun populateFromConfiguration(configuration: SoftApConfigurationCompat) {
dialogView.ssid.setText(configuration.SSID) dialogView.ssid.setText(configuration.ssid)
if (!arg.p2pMode) dialogView.security.setSelection(configuration.apKeyManagement) if (!arg.p2pMode) dialogView.security.setSelection(configuration.securityType)
dialogView.password.setText(configuration.preSharedKey) dialogView.password.setText(configuration.passphrase)
if (Build.VERSION.SDK_INT >= 23) { if (Build.VERSION.SDK_INT >= 23) {
dialogView.band.setSelection(if (configuration.apChannel in 1..165) { dialogView.band.setSelection(if (configuration.channel in 1..165) {
bandOptions.indexOfFirst { it.apChannel == configuration.apChannel } bandOptions.indexOfFirst { it.channel == configuration.channel }
} else bandOptions.indexOfFirst { it.apBand == configuration.apBand }) } else bandOptions.indexOfFirst { it.band == configuration.band })
} }
// TODO support more fields from SACC
} }
override fun onStart() { override fun onStart() {
@@ -163,8 +168,12 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
dialogView.ssidWrapper.error = if (RepeaterService.safeModeConfigurable && ssidLength < 9) { dialogView.ssidWrapper.error = if (RepeaterService.safeModeConfigurable && ssidLength < 9) {
requireContext().getString(R.string.settings_service_repeater_safe_mode_warning) requireContext().getString(R.string.settings_service_repeater_safe_mode_warning)
} else null } else null
val selectedSecurity = if (arg.p2pMode) {
SoftApConfiguration.SECURITY_TYPE_WPA2_PSK
} else dialogView.security.selectedItemPosition
val passwordValid = when (selectedSecurity) { val passwordValid = when (selectedSecurity) {
WifiConfiguration.KeyMgmt.WPA_PSK, WPA2_PSK -> dialogView.password.length() >= 8 // TODO
SoftApConfiguration.SECURITY_TYPE_WPA2_PSK -> dialogView.password.length() >= 8
else -> true // do not try to validate else -> true // do not try to validate
} }
dialogView.passwordWrapper.error = if (passwordValid) null else { dialogView.passwordWrapper.error = if (passwordValid) null else {
@@ -187,7 +196,7 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
} }
android.R.id.paste -> try { android.R.id.paste -> try {
app.clipboard.primaryClip?.getItemAt(0)?.text?.apply { app.clipboard.primaryClip?.getItemAt(0)?.text?.apply {
Base64.decode(toString(), BASE64_FLAGS).toParcelable<WifiConfiguration>() Base64.decode(toString(), BASE64_FLAGS).toParcelable<SoftApConfigurationCompat>()
?.let { populateFromConfiguration(it) } ?.let { populateFromConfiguration(it) }
} }
true true
@@ -197,7 +206,7 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
} }
R.id.share_qr -> { R.id.share_qr -> {
val qrString = try { val qrString = try {
ret.configuration.toQRString() ret.configuration.toQrCode()
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
SmartSnackbar.make(e).show() SmartSnackbar.make(e).show()
return false return false
@@ -212,7 +221,7 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
override fun onClick(dialog: DialogInterface?, which: Int) { override fun onClick(dialog: DialogInterface?, which: Int) {
super.onClick(dialog, which) super.onClick(dialog, which)
if (Build.VERSION.SDK_INT < 23 && arg.p2pMode && which == DialogInterface.BUTTON_POSITIVE) { if (Build.VERSION.SDK_INT < 23 && arg.p2pMode && which == DialogInterface.BUTTON_POSITIVE) {
RepeaterService.operatingChannel = (dialogView.band.selectedItem as BandOption).apChannel RepeaterService.operatingChannel = (dialogView.band.selectedItem as BandOption).channel
} }
} }
} }

View File

@@ -1,18 +1,38 @@
package be.mygod.vpnhotspot.net.wifi package be.mygod.vpnhotspot.net.wifi
import android.net.wifi.WifiConfiguration import android.net.wifi.SoftApConfiguration
import android.net.wifi.WifiManager import android.net.wifi.WifiManager
import androidx.annotation.RequiresApi
import androidx.core.os.BuildCompat
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.toCompat
object WifiApManager { object WifiApManager {
private val getWifiApConfiguration by lazy { WifiManager::class.java.getDeclaredMethod("getWifiApConfiguration") } private val getWifiApConfiguration by lazy { WifiManager::class.java.getDeclaredMethod("getWifiApConfiguration") }
@Suppress("DEPRECATION")
private val setWifiApConfiguration by lazy { private val setWifiApConfiguration by lazy {
WifiManager::class.java.getDeclaredMethod("setWifiApConfiguration", WifiConfiguration::class.java) WifiManager::class.java.getDeclaredMethod("setWifiApConfiguration",
android.net.wifi.WifiConfiguration::class.java)
} }
var configuration: WifiConfiguration @get:RequiresApi(30)
get() = getWifiApConfiguration(app.wifi) as? WifiConfiguration ?: WifiConfiguration() private val getSoftApConfiguration by lazy { WifiManager::class.java.getDeclaredMethod("getSoftApConfiguration") }
set(value) = require(setWifiApConfiguration(app.wifi, value) as? Boolean == true) { @get:RequiresApi(30)
"setWifiApConfiguration failed" private val setSoftApConfiguration by lazy {
WifiManager::class.java.getDeclaredMethod("setSoftApConfiguration", SoftApConfiguration::class.java)
}
var configuration: SoftApConfigurationCompat
get() = if (BuildCompat.isAtLeastR()) {
(getSoftApConfiguration(app.wifi) as SoftApConfiguration).toCompat()
} else @Suppress("DEPRECATION") {
(getWifiApConfiguration(app.wifi) as android.net.wifi.WifiConfiguration?)?.toCompat()
?: SoftApConfigurationCompat.empty()
}
set(value) = if (BuildCompat.isAtLeastR()) {
require(setSoftApConfiguration(app.wifi, value.toPlatform()) as Boolean) { "setSoftApConfiguration failed" }
} else @Suppress("DEPRECATION") {
require(setWifiApConfiguration(app.wifi,
value.toWifiConfiguration()) as Boolean) { "setWifiApConfiguration failed" }
} }
private val cancelLocalOnlyHotspotRequest by lazy { private val cancelLocalOnlyHotspotRequest by lazy {
@@ -20,9 +40,10 @@ object WifiApManager {
} }
fun cancelLocalOnlyHotspotRequest() = cancelLocalOnlyHotspotRequest(app.wifi) fun cancelLocalOnlyHotspotRequest() = cancelLocalOnlyHotspotRequest(app.wifi)
@Suppress("DEPRECATION")
private val setWifiApEnabled by lazy { private val setWifiApEnabled by lazy {
WifiManager::class.java.getDeclaredMethod("setWifiApEnabled", WifiManager::class.java.getDeclaredMethod("setWifiApEnabled",
WifiConfiguration::class.java, Boolean::class.java) android.net.wifi.WifiConfiguration::class.java, Boolean::class.java)
} }
/** /**
* Start AccessPoint mode with the specified * Start AccessPoint mode with the specified
@@ -34,7 +55,8 @@ object WifiApManager {
* part of WifiConfiguration * part of WifiConfiguration
* @return {@code true} if the operation succeeds, {@code false} otherwise * @return {@code true} if the operation succeeds, {@code false} otherwise
*/ */
private fun WifiManager.setWifiApEnabled(wifiConfig: WifiConfiguration?, enabled: Boolean) = @Suppress("DEPRECATION")
private fun WifiManager.setWifiApEnabled(wifiConfig: android.net.wifi.WifiConfiguration?, enabled: Boolean) =
setWifiApEnabled(this, wifiConfig, enabled) as Boolean setWifiApEnabled(this, wifiConfig, enabled) as Boolean
/** /**
@@ -44,7 +66,7 @@ object WifiApManager {
*/ */
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
@Deprecated("Not usable since API 26, malfunctioning on API 25") @Deprecated("Not usable since API 26, malfunctioning on API 25")
fun start(wifiConfig: WifiConfiguration? = null) { fun start(wifiConfig: android.net.wifi.WifiConfiguration? = null) {
app.wifi.isWifiEnabled = false app.wifi.isWifiEnabled = false
app.wifi.setWifiApEnabled(wifiConfig, true) app.wifi.setWifiApEnabled(wifiConfig, true)
} }

View File

@@ -1,129 +0,0 @@
package be.mygod.vpnhotspot.net.wifi.configuration
import android.net.wifi.WifiConfiguration
import androidx.annotation.RequiresApi
import be.mygod.vpnhotspot.net.wifi.WifiApManager
import timber.log.Timber
import java.lang.reflect.InvocationTargetException
/**
* WPA2 pre-shared key for use with soft access point
* (requires {@code preSharedKey} to be specified).
*/
val WPA2_PSK = WifiConfiguration.KeyMgmt.strings.indexOf("WPA2_PSK")
/**
* 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
*/
private val apBandField by lazy { WifiConfiguration::class.java.getDeclaredField("apBand") }
private val apChannelField by lazy { WifiConfiguration::class.java.getDeclaredField("apChannel") }
/**
* 2GHz band.
*
* https://android.googlesource.com/platform/frameworks/base/+/android-7.0.0_r1/wifi/java/android/net/wifi/WifiConfiguration.java#241
*/
@RequiresApi(23)
const val AP_BAND_2GHZ = 0
/**
* 5GHz band.
*/
@RequiresApi(23)
const val AP_BAND_5GHZ = 1
/**
* Device is allowed to choose the optimal band (2Ghz or 5Ghz) based on device capability,
* operating country code and current radio conditions.
*
* Introduced in 9.0, but we will abuse this constant anyway.
* https://android.googlesource.com/platform/frameworks/base/+/android-9.0.0_r1/wifi/java/android/net/wifi/WifiConfiguration.java#295
*/
@RequiresApi(23)
const val AP_BAND_ANY = -1
/**
* The band which AP resides on
* -1:Any 0:2G 1:5G
* By default, 2G is chosen
*/
var WifiConfiguration.apBand: Int
@RequiresApi(23) get() = apBandField.get(this) as Int
@RequiresApi(23) set(value) = apBandField.set(this, value)
/**
* 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
*/
var WifiConfiguration.apChannel: Int
@RequiresApi(23) get() = apChannelField.get(this) as Int
@RequiresApi(23) set(value) = apChannelField.set(this, value)
/**
* 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")
}
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)
}

View File

@@ -162,6 +162,7 @@
<string name="wifi_ap_choose_auto">Auto</string> <string name="wifi_ap_choose_auto">Auto</string>
<string name="wifi_ap_choose_2G">2.4 GHz Band</string> <string name="wifi_ap_choose_2G">2.4 GHz Band</string>
<string name="wifi_ap_choose_5G">5.0 GHz Band</string> <string name="wifi_ap_choose_5G">5.0 GHz Band</string>
<string name="wifi_ap_choose_6G">6.0 GHz Band</string>
<string name="wifi_save">Save</string> <string name="wifi_save">Save</string>
<!-- Based on: https://github.com/PrivacyApps/donations/blob/747d36a18433c7e9329691054122a8ad337a62d2/Donations/src/main/res/values/donations__strings.xml --> <!-- Based on: https://github.com/PrivacyApps/donations/blob/747d36a18433c7e9329691054122a8ad337a62d2/Donations/src/main/res/values/donations__strings.xml -->