First draft
This commit is contained in:
@@ -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`
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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<WifiApDialogFragment.Arg, WifiA
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class Arg(val configuration: WifiConfiguration,
|
||||
data class Arg(val configuration: SoftApConfigurationCompat,
|
||||
val readOnly: Boolean = false,
|
||||
/**
|
||||
* KeyMgmt is enforced to WPA_PSK.
|
||||
@@ -51,41 +52,43 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
|
||||
*/
|
||||
val p2pMode: Boolean = false) : Parcelable
|
||||
|
||||
@TargetApi(23)
|
||||
@RequiresApi(23)
|
||||
private sealed class BandOption {
|
||||
open val apBand get() = AP_BAND_2GHZ
|
||||
open val apChannel get() = 0
|
||||
open val band get() = SoftApConfigurationCompat.BAND_ANY
|
||||
open val channel get() = SoftApConfigurationCompat.CH_INVALID
|
||||
|
||||
object BandAny : BandOption() {
|
||||
override val apBand get() = AP_BAND_ANY
|
||||
override fun toString() = app.getString(R.string.wifi_ap_choose_auto)
|
||||
}
|
||||
object Band2GHz : BandOption() {
|
||||
override val band get() = SoftApConfigurationCompat.BAND_2GHZ
|
||||
override fun toString() = app.getString(R.string.wifi_ap_choose_2G)
|
||||
}
|
||||
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)
|
||||
}
|
||||
class Channel(override val apChannel: Int) : BandOption() {
|
||||
override fun toString() = "${channelToFrequency(apChannel)} MHz ($apChannel)"
|
||||
@RequiresApi(30)
|
||||
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 bandOptions: MutableList<BandOption>
|
||||
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<WifiApDialogFragment.Arg, WifiA
|
||||
if (!arg.readOnly) dialogView.ssid.addTextChangedListener(this@WifiApDialogFragment)
|
||||
if (arg.p2pMode) dialogView.securityWrapper.isGone = true else dialogView.security.apply {
|
||||
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)
|
||||
}
|
||||
onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) = 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<WifiApDialogFragment.Arg, WifiA
|
||||
if (Build.VERSION.SDK_INT >= 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<WifiApDialogFragment.Arg, WifiA
|
||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
}
|
||||
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
|
||||
populateFromConfiguration(arg.configuration)
|
||||
}
|
||||
|
||||
private fun populateFromConfiguration(configuration: WifiConfiguration) {
|
||||
dialogView.ssid.setText(configuration.SSID)
|
||||
if (!arg.p2pMode) dialogView.security.setSelection(configuration.apKeyManagement)
|
||||
dialogView.password.setText(configuration.preSharedKey)
|
||||
private fun populateFromConfiguration(configuration: SoftApConfigurationCompat) {
|
||||
dialogView.ssid.setText(configuration.ssid)
|
||||
if (!arg.p2pMode) dialogView.security.setSelection(configuration.securityType)
|
||||
dialogView.password.setText(configuration.passphrase)
|
||||
if (Build.VERSION.SDK_INT >= 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<WifiApDialogFragment.Arg, WifiA
|
||||
dialogView.ssidWrapper.error = if (RepeaterService.safeModeConfigurable && ssidLength < 9) {
|
||||
requireContext().getString(R.string.settings_service_repeater_safe_mode_warning)
|
||||
} else null
|
||||
val selectedSecurity = if (arg.p2pMode) {
|
||||
SoftApConfiguration.SECURITY_TYPE_WPA2_PSK
|
||||
} else dialogView.security.selectedItemPosition
|
||||
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
|
||||
}
|
||||
dialogView.passwordWrapper.error = if (passwordValid) null else {
|
||||
@@ -187,7 +196,7 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
|
||||
}
|
||||
android.R.id.paste -> try {
|
||||
app.clipboard.primaryClip?.getItemAt(0)?.text?.apply {
|
||||
Base64.decode(toString(), BASE64_FLAGS).toParcelable<WifiConfiguration>()
|
||||
Base64.decode(toString(), BASE64_FLAGS).toParcelable<SoftApConfigurationCompat>()
|
||||
?.let { populateFromConfiguration(it) }
|
||||
}
|
||||
true
|
||||
@@ -197,7 +206,7 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
|
||||
}
|
||||
R.id.share_qr -> {
|
||||
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<WifiApDialogFragment.Arg, WifiA
|
||||
override fun onClick(dialog: DialogInterface?, which: Int) {
|
||||
super.onClick(dialog, which)
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,38 @@
|
||||
package be.mygod.vpnhotspot.net.wifi
|
||||
|
||||
import android.net.wifi.WifiConfiguration
|
||||
import android.net.wifi.SoftApConfiguration
|
||||
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.net.wifi.SoftApConfigurationCompat.Companion.toCompat
|
||||
|
||||
object WifiApManager {
|
||||
private val getWifiApConfiguration by lazy { WifiManager::class.java.getDeclaredMethod("getWifiApConfiguration") }
|
||||
@Suppress("DEPRECATION")
|
||||
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() = getWifiApConfiguration(app.wifi) as? WifiConfiguration ?: WifiConfiguration()
|
||||
set(value) = require(setWifiApConfiguration(app.wifi, value) as? Boolean == true) {
|
||||
"setWifiApConfiguration failed"
|
||||
@get:RequiresApi(30)
|
||||
private val getSoftApConfiguration by lazy { WifiManager::class.java.getDeclaredMethod("getSoftApConfiguration") }
|
||||
@get:RequiresApi(30)
|
||||
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 {
|
||||
@@ -20,9 +40,10 @@ object WifiApManager {
|
||||
}
|
||||
fun cancelLocalOnlyHotspotRequest() = cancelLocalOnlyHotspotRequest(app.wifi)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private val setWifiApEnabled by lazy {
|
||||
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
|
||||
@@ -34,7 +55,8 @@ object WifiApManager {
|
||||
* part of WifiConfiguration
|
||||
* @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
|
||||
|
||||
/**
|
||||
@@ -44,7 +66,7 @@ object WifiApManager {
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
@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.setWifiApEnabled(wifiConfig, true)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -162,6 +162,7 @@
|
||||
<string name="wifi_ap_choose_auto">Auto</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_6G">6.0 GHz Band</string>
|
||||
<string name="wifi_save">Save</string>
|
||||
|
||||
<!-- Based on: https://github.com/PrivacyApps/donations/blob/747d36a18433c7e9329691054122a8ad337a62d2/Donations/src/main/res/values/donations__strings.xml -->
|
||||
|
||||
Reference in New Issue
Block a user