Merge branch 'master' into temp-hotspot-use-system
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
package be.mygod.vpnhotspot.net
|
||||
|
||||
import android.net.MacAddress
|
||||
import android.os.Build
|
||||
import android.system.ErrnoException
|
||||
import android.system.Os
|
||||
@@ -15,7 +16,7 @@ import java.io.IOException
|
||||
import java.net.Inet4Address
|
||||
import java.net.InetAddress
|
||||
|
||||
data class IpNeighbour(val ip: InetAddress, val dev: String, val lladdr: MacAddressCompat, val state: State) {
|
||||
data class IpNeighbour(val ip: InetAddress, val dev: String, val lladdr: MacAddress, val state: State) {
|
||||
enum class State {
|
||||
INCOMPLETE, VALID, FAILED, DELETING
|
||||
}
|
||||
@@ -27,8 +28,8 @@ data class IpNeighbour(val ip: InetAddress, val dev: String, val lladdr: MacAddr
|
||||
* https://people.cs.clemson.edu/~westall/853/notes/arpstate.pdf
|
||||
* Assumptions: IP addr (key) always present and RTM_GETNEIGH is never used
|
||||
*/
|
||||
private val parser = "^(Deleted )?([^ ]+) dev ([^ ]+) (lladdr ([^ ]*))?.*?( ([INCOMPLET,RAHBSDYF]+))?\$"
|
||||
.toRegex()
|
||||
private val parser = ("^(Deleted )?(?:([^ ]+) )?dev ([^ ]+) (?:lladdr ([^ ]*))?.*?" +
|
||||
"(?: ([INCOMPLET,RAHBSDYF]+))?\$").toRegex()
|
||||
/**
|
||||
* Fallback format will be used if if_indextoname returns null, which some stupid devices do.
|
||||
*
|
||||
@@ -49,14 +50,15 @@ data class IpNeighbour(val ip: InetAddress, val dev: String, val lladdr: MacAddr
|
||||
suspend fun parse(line: String, fullMode: Boolean): List<IpNeighbour> {
|
||||
return if (line.isBlank()) emptyList() else try {
|
||||
val match = parser.matchEntire(line)!!
|
||||
val ip = parseNumericAddress(match.groupValues[2]) // by regex, ip is non-empty
|
||||
val devs = substituteDev(match.groupValues[3]) // by regex, dev is non-empty as well
|
||||
val state = if (match.groupValues[1].isNotEmpty()) State.DELETING else when (match.groupValues[7]) {
|
||||
if (match.groups[2] == null) return emptyList()
|
||||
val ip = parseNumericAddress(match.groupValues[2])
|
||||
val devs = substituteDev(match.groupValues[3]) // by regex, dev is non-empty
|
||||
val state = if (match.groupValues[1].isNotEmpty()) State.DELETING else when (match.groupValues[5]) {
|
||||
"", "INCOMPLETE" -> State.INCOMPLETE
|
||||
"REACHABLE", "DELAY", "STALE", "PROBE", "PERMANENT" -> State.VALID
|
||||
"FAILED" -> State.FAILED
|
||||
"NOARP" -> return emptyList() // skip
|
||||
else -> throw IllegalArgumentException("Unknown state encountered: ${match.groupValues[7]}")
|
||||
else -> throw IllegalArgumentException("Unknown state encountered: ${match.groupValues[5]}")
|
||||
}
|
||||
var lladdr = MacAddressCompat.ALL_ZEROS_ADDRESS
|
||||
if (!fullMode && state != State.VALID) {
|
||||
@@ -64,7 +66,7 @@ data class IpNeighbour(val ip: InetAddress, val dev: String, val lladdr: MacAddr
|
||||
return devs.map { IpNeighbour(ip, it, lladdr, State.DELETING) }
|
||||
}
|
||||
if (match.groups[4] != null) try {
|
||||
lladdr = MacAddressCompat.fromString(match.groupValues[5])
|
||||
lladdr = MacAddress.fromString(match.groupValues[4])
|
||||
} catch (e: IllegalArgumentException) {
|
||||
if (state != State.INCOMPLETE && state != State.DELETING) {
|
||||
Timber.w(IOException("Failed to find MAC address for $line", e))
|
||||
@@ -78,7 +80,7 @@ data class IpNeighbour(val ip: InetAddress, val dev: String, val lladdr: MacAddr
|
||||
val list = arp()
|
||||
.asSequence()
|
||||
.filter { parseNumericAddress(it[ARP_IP_ADDRESS]) == ip && it[ARP_DEVICE] in devs }
|
||||
.map { MacAddressCompat.fromString(it[ARP_HW_ADDRESS]) }
|
||||
.map { MacAddress.fromString(it[ARP_HW_ADDRESS]) }
|
||||
.filter { it != MacAddressCompat.ALL_ZEROS_ADDRESS }
|
||||
.distinct()
|
||||
.toList()
|
||||
@@ -137,5 +139,4 @@ data class IpNeighbour(val ip: InetAddress, val dev: String, val lladdr: MacAddr
|
||||
data class IpDev(val ip: InetAddress, val dev: String) {
|
||||
override fun toString() = "$ip%$dev"
|
||||
}
|
||||
@Suppress("FunctionName")
|
||||
fun IpDev(neighbour: IpNeighbour) = IpDev(neighbour.ip, neighbour.dev)
|
||||
|
||||
@@ -1,97 +1,34 @@
|
||||
package be.mygod.vpnhotspot.net
|
||||
|
||||
import android.net.MacAddress
|
||||
import androidx.annotation.RequiresApi
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
|
||||
/**
|
||||
* Compat support class for [MacAddress].
|
||||
* This used to be a compat support class for [MacAddress].
|
||||
* Now it is just a convenient class for backwards compatibility.
|
||||
*/
|
||||
@JvmInline
|
||||
value class MacAddressCompat(val addr: Long) {
|
||||
companion object {
|
||||
private const val ETHER_ADDR_LEN = 6
|
||||
/**
|
||||
* The MacAddress zero MAC address.
|
||||
*
|
||||
* Not publicly exposed or treated specially since the OUI 00:00:00 is registered.
|
||||
* @hide
|
||||
*/
|
||||
val ALL_ZEROS_ADDRESS = MacAddressCompat(0)
|
||||
val ANY_ADDRESS = MacAddressCompat(2)
|
||||
val ALL_ZEROS_ADDRESS = MacAddress.fromBytes(byteArrayOf(0, 0, 0, 0, 0, 0))
|
||||
val ANY_ADDRESS = MacAddress.fromBytes(byteArrayOf(2, 0, 0, 0, 0, 0))
|
||||
|
||||
/**
|
||||
* 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) = ByteBuffer.allocate(Long.SIZE_BYTES).run {
|
||||
fun MacAddress.toLong() = ByteBuffer.allocate(Long.SIZE_BYTES).apply {
|
||||
order(ByteOrder.LITTLE_ENDIAN)
|
||||
put(when (addr.size) {
|
||||
ETHER_ADDR_LEN -> addr
|
||||
8 -> {
|
||||
require(addr.take(2).all { it == 0.toByte() }) {
|
||||
"Unrecognized padding " + addr.joinToString(":") { "%02x".format(it) }
|
||||
}
|
||||
addr.drop(2).toByteArray()
|
||||
}
|
||||
else -> throw IllegalArgumentException(addr.joinToString(":") { "%02x".format(it) } +
|
||||
" was not a valid MAC address")
|
||||
})
|
||||
put(toByteArray())
|
||||
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)
|
||||
var start = 0
|
||||
var i = 0
|
||||
while (position() < ETHER_ADDR_LEN && start < addr.length) {
|
||||
val end = i
|
||||
if (addr.getOrElse(i) { ':' } == ':') ++i else if (i < start + 2) {
|
||||
++i
|
||||
continue
|
||||
}
|
||||
put(if (start == end) 0 else try {
|
||||
Integer.parseInt(addr.substring(start, end), 16).toByte()
|
||||
} catch (e: NumberFormatException) {
|
||||
throw IllegalArgumentException(e)
|
||||
})
|
||||
start = i
|
||||
}
|
||||
require(position() == ETHER_ADDR_LEN) { "MAC address too short" }
|
||||
rewind()
|
||||
MacAddressCompat(long)
|
||||
}
|
||||
|
||||
@RequiresApi(28)
|
||||
fun MacAddress.toCompat() = fromBytes(toByteArray())
|
||||
}.long
|
||||
}
|
||||
|
||||
fun validate() = require(addr and ((1L shl 48) - 1).inv() == 0L)
|
||||
|
||||
fun toList() = ByteBuffer.allocate(8).run {
|
||||
fun toPlatform() = MacAddress.fromBytes(ByteBuffer.allocate(8).run {
|
||||
order(ByteOrder.LITTLE_ENDIAN)
|
||||
putLong(addr)
|
||||
array().take(6)
|
||||
}
|
||||
|
||||
@RequiresApi(28)
|
||||
fun toPlatform() = MacAddress.fromBytes(toList().toByteArray())
|
||||
|
||||
override fun toString() = toList().joinToString(":") { "%02x".format(it) }
|
||||
|
||||
fun toOui() = toList().joinToString("") { "%02x".format(it) }.substring(0, 9)
|
||||
}.toByteArray())
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package be.mygod.vpnhotspot.net
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.annotation.TargetApi
|
||||
import android.net.LinkProperties
|
||||
import android.net.MacAddress
|
||||
import android.net.RouteInfo
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import android.system.Os
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.R
|
||||
import be.mygod.vpnhotspot.net.monitor.FallbackUpstreamMonitor
|
||||
@@ -15,7 +13,9 @@ import be.mygod.vpnhotspot.net.monitor.UpstreamMonitor
|
||||
import be.mygod.vpnhotspot.room.AppDatabase
|
||||
import be.mygod.vpnhotspot.root.RootManager
|
||||
import be.mygod.vpnhotspot.root.RoutingCommands
|
||||
import be.mygod.vpnhotspot.util.*
|
||||
import be.mygod.vpnhotspot.util.RootSession
|
||||
import be.mygod.vpnhotspot.util.allInterfaceNames
|
||||
import be.mygod.vpnhotspot.util.allRoutes
|
||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import timber.log.Timber
|
||||
@@ -125,7 +125,6 @@ class Routing(private val caller: Any, private val downstream: String) : IpNeigh
|
||||
*
|
||||
* Source: https://android.googlesource.com/platform/system/netd/+/3b47c793ff7ade843b1d85a9be8461c3b4dc693e
|
||||
*/
|
||||
@RequiresApi(28)
|
||||
Netd,
|
||||
}
|
||||
|
||||
@@ -151,35 +150,24 @@ class Routing(private val caller: Any, private val downstream: String) : IpNeigh
|
||||
private val upstreams = HashSet<String>()
|
||||
private class InterfaceGoneException(upstream: String) : IOException("Interface $upstream not found")
|
||||
private open inner class Upstream(val priority: Int) : UpstreamMonitor.Callback {
|
||||
/**
|
||||
* The only case when upstream is null is on API 23- and we are using system default rules.
|
||||
*/
|
||||
inner class Subrouting(priority: Int, val upstream: String) {
|
||||
val ifindex = if (upstream.isEmpty()) 0 else if_nametoindex(upstream).also {
|
||||
val ifindex = Os.if_nametoindex(upstream).also {
|
||||
if (it <= 0) throw InterfaceGoneException(upstream)
|
||||
}
|
||||
val transaction = RootSession.beginTransaction().safeguard {
|
||||
if (upstream.isEmpty()) {
|
||||
ipRule("goto $RULE_PRIORITY_TETHERING", priority) // skip unreachable rule
|
||||
} else ipRuleLookup(ifindex, priority)
|
||||
@TargetApi(28) when (masqueradeMode) {
|
||||
ipRuleLookup(ifindex, priority)
|
||||
when (masqueradeMode) {
|
||||
MasqueradeMode.None -> { } // nothing to be done here
|
||||
MasqueradeMode.Simple -> {
|
||||
// note: specifying -i wouldn't work for POSTROUTING
|
||||
iptablesAdd(if (upstream.isEmpty()) {
|
||||
"vpnhotspot_masquerade -s $hostSubnet -j MASQUERADE"
|
||||
} else "vpnhotspot_masquerade -s $hostSubnet -o $upstream -j MASQUERADE", "nat")
|
||||
}
|
||||
MasqueradeMode.Netd -> {
|
||||
check(upstream.isNotEmpty()) // fallback is only needed for repeater on API 23 < 28
|
||||
/**
|
||||
* 0 means that there are no interface addresses coming after, which is unused anyway.
|
||||
*
|
||||
* https://android.googlesource.com/platform/frameworks/base/+/android-5.0.0_r1/services/core/java/com/android/server/NetworkManagementService.java#1251
|
||||
* https://android.googlesource.com/platform/system/netd/+/android-5.0.0_r1/server/CommandListener.cpp#638
|
||||
*/
|
||||
ndc("Nat", "ndc nat enable $downstream $upstream 0")
|
||||
}
|
||||
// note: specifying -i wouldn't work for POSTROUTING
|
||||
MasqueradeMode.Simple -> iptablesAdd(
|
||||
"vpnhotspot_masquerade -s $hostSubnet -o $upstream -j MASQUERADE", "nat")
|
||||
/**
|
||||
* 0 means that there are no interface addresses coming after, which is unused anyway.
|
||||
*
|
||||
* https://android.googlesource.com/platform/frameworks/base/+/android-5.0.0_r1/services/core/java/com/android/server/NetworkManagementService.java#1251
|
||||
* https://android.googlesource.com/platform/system/netd/+/android-5.0.0_r1/server/CommandListener.cpp#638
|
||||
*/
|
||||
MasqueradeMode.Netd -> ndc("Nat", "ndc nat enable $downstream $upstream 0")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -225,16 +213,10 @@ class Routing(private val caller: Any, private val downstream: String) : IpNeigh
|
||||
updateDnsRoute()
|
||||
}
|
||||
}
|
||||
private val fallbackUpstream = object : Upstream(RULE_PRIORITY_UPSTREAM_FALLBACK) {
|
||||
@SuppressLint("NewApi")
|
||||
override fun onFallback() = onAvailable(LinkProperties().apply {
|
||||
interfaceName = ""
|
||||
setDnsServers(listOf(parseNumericAddress("8.8.8.8")))
|
||||
})
|
||||
}
|
||||
private val fallbackUpstream = Upstream(RULE_PRIORITY_UPSTREAM_FALLBACK)
|
||||
private val upstream = Upstream(RULE_PRIORITY_UPSTREAM)
|
||||
|
||||
private inner class Client(private val ip: Inet4Address, mac: MacAddressCompat) : AutoCloseable {
|
||||
private inner class Client(private val ip: Inet4Address, mac: MacAddress) : AutoCloseable {
|
||||
private val transaction = RootSession.beginTransaction().safeguard {
|
||||
val address = ip.hostAddress
|
||||
iptablesInsert("vpnhotspot_acl -i $downstream -s $address -j ACCEPT")
|
||||
@@ -287,9 +269,9 @@ class Routing(private val caller: Any, private val downstream: String) : IpNeigh
|
||||
* but may be broken when system tethering shutdown before local-only interfaces.
|
||||
*/
|
||||
fun ipForward() {
|
||||
if (Build.VERSION.SDK_INT >= 23) try {
|
||||
try {
|
||||
transaction.ndc("ipfwd", "ndc ipfwd enable vpnhotspot_$downstream",
|
||||
"ndc ipfwd disable vpnhotspot_$downstream")
|
||||
"ndc ipfwd disable vpnhotspot_$downstream")
|
||||
return
|
||||
} catch (e: RoutingCommands.UnexpectedOutputException) {
|
||||
Timber.w(IOException("ndc ipfwd enable failure", e))
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
package be.mygod.vpnhotspot.net
|
||||
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.root.SettingsGlobalPut
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* It's hard to change tethering rules with Tethering hardware acceleration enabled for now.
|
||||
@@ -15,19 +13,6 @@ import timber.log.Timber
|
||||
* https://android.googlesource.com/platform/hardware/qcom/data/ipacfg-mgr/+/master/msm8998/ipacm/src/IPACM_OffloadManager.cpp
|
||||
*/
|
||||
object TetherOffloadManager {
|
||||
val supported by lazy {
|
||||
Build.VERSION.SDK_INT >= 27 || try {
|
||||
Settings.Global::class.java.getDeclaredField("TETHER_OFFLOAD_DISABLED").get(null).let {
|
||||
require(it == TETHER_OFFLOAD_DISABLED) { "Unknown field $it" }
|
||||
}
|
||||
true
|
||||
} catch (_: NoSuchFieldException) {
|
||||
false
|
||||
} catch (e: Exception) {
|
||||
Timber.w(e)
|
||||
false
|
||||
}
|
||||
}
|
||||
private const val TETHER_OFFLOAD_DISABLED = "tether_offload_disabled"
|
||||
val enabled get() = Settings.Global.getInt(app.contentResolver, TETHER_OFFLOAD_DISABLED, 0) == 0
|
||||
suspend fun setEnabled(value: Boolean) = SettingsGlobalPut.int(TETHER_OFFLOAD_DISABLED, if (value) 0 else 1)
|
||||
|
||||
@@ -28,6 +28,8 @@ enum class TetherType(@DrawableRes val icon: Int) {
|
||||
else -> false
|
||||
}
|
||||
|
||||
fun isA(other: TetherType) = this == other || other == USB && this == NCM
|
||||
|
||||
companion object : TetheringManager.TetheringEventCallback {
|
||||
private lateinit var usbRegexs: List<Pattern>
|
||||
private lateinit var wifiRegexs: List<Pattern>
|
||||
@@ -58,6 +60,9 @@ enum class TetherType(@DrawableRes val icon: Int) {
|
||||
private fun updateRegexs() = synchronized(this) {
|
||||
if (!requiresUpdate) return@synchronized
|
||||
requiresUpdate = false
|
||||
usbRegexs = emptyList()
|
||||
wifiRegexs = emptyList()
|
||||
bluetoothRegexs = emptyList()
|
||||
TetheringManager.registerTetheringEventCallback(null, this)
|
||||
val info = TetheringManager.resolvedService.serviceInfo
|
||||
val tethering = "com.android.networkstack.tethering" to
|
||||
@@ -71,9 +76,9 @@ enum class TetherType(@DrawableRes val icon: Int) {
|
||||
}
|
||||
|
||||
@RequiresApi(30)
|
||||
override fun onTetherableInterfaceRegexpsChanged(args: Array<out Any?>?) = synchronized(this) {
|
||||
override fun onTetherableInterfaceRegexpsChanged(reg: Any?) = synchronized(this) {
|
||||
if (requiresUpdate) return@synchronized
|
||||
Timber.i("onTetherableInterfaceRegexpsChanged: ${args?.contentDeepToString()}")
|
||||
Timber.i("onTetherableInterfaceRegexpsChanged: $reg")
|
||||
TetheringManager.unregisterTetheringEventCallback(this)
|
||||
requiresUpdate = true
|
||||
listener()
|
||||
@@ -104,13 +109,20 @@ enum class TetherType(@DrawableRes val icon: Int) {
|
||||
*
|
||||
* Based on: https://android.googlesource.com/platform/frameworks/base/+/5d36f01/packages/Tethering/src/com/android/networkstack/tethering/Tethering.java#479
|
||||
*/
|
||||
fun ofInterface(iface: String?, p2pDev: String? = null) = synchronized(this) { ofInterfaceImpl(iface, p2pDev) }
|
||||
private tailrec fun ofInterfaceImpl(iface: String?, p2pDev: String?): TetherType = when {
|
||||
iface == null -> NONE
|
||||
iface == p2pDev -> WIFI_P2P
|
||||
fun ofInterface(iface: String?, p2pDev: String? = null) = when (iface) {
|
||||
null -> NONE
|
||||
p2pDev -> WIFI_P2P
|
||||
else -> try {
|
||||
synchronized(this) { ofInterfaceImpl(iface) }
|
||||
} catch (e: RuntimeException) {
|
||||
Timber.w(e)
|
||||
NONE
|
||||
}
|
||||
}
|
||||
private tailrec fun ofInterfaceImpl(iface: String): TetherType = when {
|
||||
requiresUpdate -> {
|
||||
if (Build.VERSION.SDK_INT >= 30) updateRegexs() else error("unexpected requiresUpdate")
|
||||
ofInterfaceImpl(iface, p2pDev)
|
||||
ofInterfaceImpl(iface)
|
||||
}
|
||||
wifiRegexs.any { it.matcher(iface).matches() } -> WIFI
|
||||
wigigRegexs.any { it.matcher(iface).matches() } -> WIGIG
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.content.pm.PackageManager
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.os.Build
|
||||
import android.os.DeadObjectException
|
||||
import android.os.Handler
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.os.ExecutorCompat
|
||||
@@ -66,7 +67,11 @@ object TetheringManager {
|
||||
}
|
||||
|
||||
private object InPlaceExecutor : Executor {
|
||||
override fun execute(command: Runnable) = command.run()
|
||||
override fun execute(command: Runnable) = try {
|
||||
command.run()
|
||||
} catch (e: Exception) {
|
||||
Timber.w(e) // prevent Binder stub swallowing the exception
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,9 +94,7 @@ object TetheringManager {
|
||||
* https://android.googlesource.com/platform/frameworks/base.git/+/2a091d7aa0c174986387e5d56bf97a87fe075bdb%5E%21/services/java/com/android/server/connectivity/Tethering.java
|
||||
*/
|
||||
const val ACTION_TETHER_STATE_CHANGED = "android.net.conn.TETHER_STATE_CHANGED"
|
||||
@RequiresApi(26)
|
||||
private const val EXTRA_ACTIVE_LOCAL_ONLY_LEGACY = "localOnlyArray"
|
||||
private const val EXTRA_ACTIVE_TETHER_LEGACY = "activeArray"
|
||||
/**
|
||||
* gives a String[] listing all the interfaces currently in local-only
|
||||
* mode (ie, has DHCPv4+IPv6-ULA support and no packet forwarding)
|
||||
@@ -102,7 +105,6 @@ object TetheringManager {
|
||||
* gives a String[] listing all the interfaces currently tethered
|
||||
* (ie, has DHCPv4 support and packets potentially forwarded/NATed)
|
||||
*/
|
||||
@RequiresApi(26)
|
||||
private const val EXTRA_ACTIVE_TETHER = "tetherArray"
|
||||
/**
|
||||
* gives a String[] listing all the interfaces we tried to tether and
|
||||
@@ -126,7 +128,6 @@ object TetheringManager {
|
||||
* Wifi tethering type.
|
||||
* @see [startTethering].
|
||||
*/
|
||||
@RequiresApi(24)
|
||||
const val TETHERING_WIFI = 0
|
||||
/**
|
||||
* USB tethering type.
|
||||
@@ -134,48 +135,33 @@ object TetheringManager {
|
||||
* Requires MANAGE_USB permission, unfortunately.
|
||||
*
|
||||
* Source: https://android.googlesource.com/platform/frameworks/base/+/7ca5d3a/services/usb/java/com/android/server/usb/UsbService.java#389
|
||||
* @see [startTethering].
|
||||
* @see startTethering
|
||||
*/
|
||||
@RequiresApi(24)
|
||||
const val TETHERING_USB = 1
|
||||
/**
|
||||
* Bluetooth tethering type.
|
||||
*
|
||||
* Requires BLUETOOTH permission.
|
||||
* @see [startTethering].
|
||||
* @see startTethering
|
||||
*/
|
||||
@RequiresApi(24)
|
||||
const val TETHERING_BLUETOOTH = 2
|
||||
/**
|
||||
* Ncm local tethering type.
|
||||
*
|
||||
* @see [startTethering]
|
||||
*/
|
||||
@RequiresApi(30)
|
||||
const val TETHERING_NCM = 4
|
||||
/**
|
||||
* Ethernet tethering type.
|
||||
*
|
||||
* Requires MANAGE_USB permission, also.
|
||||
* @see [startTethering]
|
||||
* @see startTethering
|
||||
*/
|
||||
@RequiresApi(30)
|
||||
const val TETHERING_ETHERNET = 5
|
||||
/**
|
||||
* WIGIG tethering type. Use a separate type to prevent
|
||||
* conflicts with TETHERING_WIFI
|
||||
* This type is only used internally by the tethering module
|
||||
* @hide
|
||||
*/
|
||||
@RequiresApi(30)
|
||||
const val TETHERING_WIGIG = 6
|
||||
@RequiresApi(31) // TETHERING_WIFI_P2P
|
||||
private val expectedTypes = setOf(TETHERING_WIFI, TETHERING_USB, TETHERING_BLUETOOTH, 3, TETHERING_ETHERNET)
|
||||
|
||||
@get:RequiresApi(30)
|
||||
private val clazz by lazy { Class.forName("android.net.TetheringManager") }
|
||||
@get:RequiresApi(30)
|
||||
private val instance by lazy @TargetApi(30) {
|
||||
@SuppressLint("WrongConstant") // hidden services are not included in constants as of R preview 4
|
||||
val service = Services.context.getSystemService(TETHERING_SERVICE)
|
||||
val service = Services.context.getSystemService(TETHERING_SERVICE)!!
|
||||
service
|
||||
}
|
||||
|
||||
@@ -188,20 +174,17 @@ object TetheringManager {
|
||||
}
|
||||
}.first()
|
||||
|
||||
@get:RequiresApi(24)
|
||||
private val classOnStartTetheringCallback by lazy {
|
||||
Class.forName("android.net.ConnectivityManager\$OnStartTetheringCallback")
|
||||
}
|
||||
@get:RequiresApi(24)
|
||||
private val startTetheringLegacy by lazy {
|
||||
ConnectivityManager::class.java.getDeclaredMethod("startTethering",
|
||||
Int::class.java, Boolean::class.java, classOnStartTetheringCallback, Handler::class.java)
|
||||
}
|
||||
@get:RequiresApi(24)
|
||||
private val stopTetheringLegacy by lazy {
|
||||
ConnectivityManager::class.java.getDeclaredMethod("stopTethering", Int::class.java)
|
||||
}
|
||||
private val getLastTetherError by lazy {
|
||||
private val getLastTetherError by lazy @SuppressLint("SoonBlockedPrivateApi") {
|
||||
ConnectivityManager::class.java.getDeclaredMethod("getLastTetherError", String::class.java)
|
||||
}
|
||||
|
||||
@@ -210,50 +193,50 @@ object TetheringManager {
|
||||
Class.forName("android.net.TetheringManager\$TetheringRequest\$Builder")
|
||||
}
|
||||
@get:RequiresApi(30)
|
||||
private val newTetheringRequestBuilder by lazy { classTetheringRequestBuilder.getConstructor(Int::class.java) }
|
||||
private val newTetheringRequestBuilder by lazy @TargetApi(30) {
|
||||
classTetheringRequestBuilder.getConstructor(Int::class.java)
|
||||
}
|
||||
// @get:RequiresApi(30)
|
||||
// private val setStaticIpv4Addresses by lazy {
|
||||
// classTetheringRequestBuilder.getDeclaredMethod("setStaticIpv4Addresses",
|
||||
// LinkAddress::class.java, LinkAddress::class.java)
|
||||
// }
|
||||
@get:RequiresApi(30)
|
||||
private val setExemptFromEntitlementCheck by lazy {
|
||||
private val setExemptFromEntitlementCheck by lazy @TargetApi(30) {
|
||||
classTetheringRequestBuilder.getDeclaredMethod("setExemptFromEntitlementCheck", Boolean::class.java)
|
||||
}
|
||||
@get:RequiresApi(30)
|
||||
private val setShouldShowEntitlementUi by lazy {
|
||||
private val setShouldShowEntitlementUi by lazy @TargetApi(30) {
|
||||
classTetheringRequestBuilder.getDeclaredMethod("setShouldShowEntitlementUi", Boolean::class.java)
|
||||
}
|
||||
@get:RequiresApi(30)
|
||||
private val build by lazy { classTetheringRequestBuilder.getDeclaredMethod("build") }
|
||||
private val build by lazy @TargetApi(30) { classTetheringRequestBuilder.getDeclaredMethod("build") }
|
||||
|
||||
@get:RequiresApi(30)
|
||||
private val interfaceStartTetheringCallback by lazy {
|
||||
Class.forName("android.net.TetheringManager\$StartTetheringCallback")
|
||||
}
|
||||
@get:RequiresApi(30)
|
||||
private val startTethering by lazy {
|
||||
private val startTethering by lazy @TargetApi(30) {
|
||||
clazz.getDeclaredMethod("startTethering", Class.forName("android.net.TetheringManager\$TetheringRequest"),
|
||||
Executor::class.java, interfaceStartTetheringCallback)
|
||||
}
|
||||
@get:RequiresApi(30)
|
||||
private val stopTethering by lazy { clazz.getDeclaredMethod("stopTethering", Int::class.java) }
|
||||
private val stopTethering by lazy @TargetApi(30) { clazz.getDeclaredMethod("stopTethering", Int::class.java) }
|
||||
|
||||
@Deprecated("Legacy API")
|
||||
@RequiresApi(24)
|
||||
fun startTetheringLegacy(type: Int, showProvisioningUi: Boolean, callback: StartTetheringCallback,
|
||||
handler: Handler? = null, cacheDir: File = app.deviceStorage.codeCacheDir) {
|
||||
val reference = WeakReference(callback)
|
||||
val proxy = ProxyBuilder.forClass(classOnStartTetheringCallback).apply {
|
||||
dexCache(cacheDir)
|
||||
handler { proxy, method, args ->
|
||||
if (args.isNotEmpty()) Timber.w("Unexpected args for ${method.name}: $args")
|
||||
@Suppress("NAME_SHADOWING") val callback = reference.get()
|
||||
when (method.name) {
|
||||
"onTetheringStarted" -> callback?.onTetheringStarted()
|
||||
"onTetheringFailed" -> callback?.onTetheringFailed()
|
||||
else -> ProxyBuilder.callSuper(proxy, method, args)
|
||||
if (args.isEmpty()) when (method.name) {
|
||||
"onTetheringStarted" -> return@handler callback?.onTetheringStarted()
|
||||
"onTetheringFailed" -> return@handler callback?.onTetheringFailed()
|
||||
}
|
||||
ProxyBuilder.callSuper(proxy, method, args)
|
||||
}
|
||||
}.build()
|
||||
startTetheringLegacy(Services.connectivity, type, showProvisioningUi, proxy, handler)
|
||||
@@ -275,13 +258,9 @@ object TetheringManager {
|
||||
arrayOf(interfaceStartTetheringCallback), object : InvocationHandler {
|
||||
override fun invoke(proxy: Any, method: Method, args: Array<out Any?>?): Any? {
|
||||
@Suppress("NAME_SHADOWING") val callback = reference.get()
|
||||
return when (val name = method.name) {
|
||||
"onTetheringStarted" -> {
|
||||
if (!args.isNullOrEmpty()) Timber.w("Unexpected args for $name: $args")
|
||||
callback?.onTetheringStarted()
|
||||
}
|
||||
"onTetheringFailed" -> {
|
||||
if (args?.size != 1) Timber.w("Unexpected args for $name: $args")
|
||||
return when {
|
||||
method.matches("onTetheringStarted") -> callback?.onTetheringStarted()
|
||||
method.matches("onTetheringFailed", Integer.TYPE) -> {
|
||||
callback?.onTetheringFailed(args?.get(0) as Int)
|
||||
}
|
||||
else -> callSuper(interfaceStartTetheringCallback, proxy, method, args)
|
||||
@@ -312,7 +291,6 @@ object TetheringManager {
|
||||
* configures tethering with the preferred local IPv4 link address to use.
|
||||
* *@see setStaticIpv4Addresses
|
||||
*/
|
||||
@RequiresApi(24)
|
||||
fun startTethering(type: Int, showProvisioningUi: Boolean, callback: StartTetheringCallback,
|
||||
handler: Handler? = null, cacheDir: File = app.deviceStorage.codeCacheDir) {
|
||||
if (Build.VERSION.SDK_INT >= 30) try {
|
||||
@@ -384,12 +362,10 @@ object TetheringManager {
|
||||
* {@link ConnectivityManager.TETHERING_USB}, or
|
||||
* {@link ConnectivityManager.TETHERING_BLUETOOTH}.
|
||||
*/
|
||||
@RequiresApi(24)
|
||||
fun stopTethering(type: Int) {
|
||||
if (Build.VERSION.SDK_INT >= 30) stopTethering(instance, type)
|
||||
else stopTetheringLegacy(Services.connectivity, type)
|
||||
}
|
||||
@RequiresApi(24)
|
||||
fun stopTethering(type: Int, callback: (Exception) -> Unit) {
|
||||
try {
|
||||
stopTethering(type)
|
||||
@@ -423,6 +399,23 @@ object TetheringManager {
|
||||
*/
|
||||
fun onTetheringSupported(supported: Boolean) {}
|
||||
|
||||
/**
|
||||
* Called when tethering supported status changed.
|
||||
*
|
||||
* This will be called immediately after the callback is registered, and may be called
|
||||
* multiple times later upon changes.
|
||||
*
|
||||
* Tethering may be disabled via system properties, device configuration, or device
|
||||
* policy restrictions.
|
||||
*
|
||||
* @param supportedTypes a set of @TetheringType which is supported.
|
||||
*/
|
||||
@TargetApi(31)
|
||||
fun onSupportedTetheringTypes(supportedTypes: Set<Int?>) {
|
||||
if ((supportedTypes - expectedTypes).isNotEmpty()) Timber.w(Exception(
|
||||
"Unexpected supported tethering types: ${supportedTypes.joinToString()}"))
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when tethering upstream changed.
|
||||
*
|
||||
@@ -445,7 +438,7 @@ object TetheringManager {
|
||||
* *@param reg The new regular expressions.
|
||||
* @hide
|
||||
*/
|
||||
fun onTetherableInterfaceRegexpsChanged(args: Array<out Any?>?) {}
|
||||
fun onTetherableInterfaceRegexpsChanged(reg: Any?) {}
|
||||
|
||||
/**
|
||||
* Called when there was a change in the list of tetherable interfaces. Tetherable
|
||||
@@ -507,11 +500,11 @@ object TetheringManager {
|
||||
Class.forName("android.net.TetheringManager\$TetheringEventCallback")
|
||||
}
|
||||
@get:RequiresApi(30)
|
||||
private val registerTetheringEventCallback by lazy {
|
||||
private val registerTetheringEventCallback by lazy @TargetApi(30) {
|
||||
clazz.getDeclaredMethod("registerTetheringEventCallback", Executor::class.java, interfaceTetheringEventCallback)
|
||||
}
|
||||
@get:RequiresApi(30)
|
||||
private val unregisterTetheringEventCallback by lazy {
|
||||
private val unregisterTetheringEventCallback by lazy @TargetApi(30) {
|
||||
clazz.getDeclaredMethod("unregisterTetheringEventCallback", interfaceTetheringEventCallback)
|
||||
}
|
||||
|
||||
@@ -541,40 +534,38 @@ object TetheringManager {
|
||||
override fun invoke(proxy: Any, method: Method, args: Array<out Any?>?): Any? {
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val callback = reference.get()
|
||||
val noArgs = args?.size ?: 0
|
||||
return when (val name = method.name) {
|
||||
"onTetheringSupported" -> {
|
||||
if (noArgs != 1) Timber.w("Unexpected args for $name: $args")
|
||||
return when {
|
||||
method.matches("onTetheringSupported", Boolean::class.java) -> {
|
||||
callback?.onTetheringSupported(args!![0] as Boolean)
|
||||
}
|
||||
"onUpstreamChanged" -> {
|
||||
if (noArgs != 1) Timber.w("Unexpected args for $name: $args")
|
||||
method.matches1<java.util.Set<*>>("onSupportedTetheringTypes") -> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
callback?.onSupportedTetheringTypes(args!![0] as Set<Int?>)
|
||||
}
|
||||
method.matches1<Network>("onUpstreamChanged") -> {
|
||||
callback?.onUpstreamChanged(args!![0] as Network?)
|
||||
}
|
||||
"onTetherableInterfaceRegexpsChanged" -> {
|
||||
if (regexpsSent) callback?.onTetherableInterfaceRegexpsChanged(args)
|
||||
method.name == "onTetherableInterfaceRegexpsChanged" &&
|
||||
method.parameters.singleOrNull()?.type?.name ==
|
||||
"android.net.TetheringManager\$TetheringInterfaceRegexps" -> {
|
||||
if (regexpsSent) callback?.onTetherableInterfaceRegexpsChanged(args!!.single())
|
||||
regexpsSent = true
|
||||
}
|
||||
"onTetherableInterfacesChanged" -> {
|
||||
if (noArgs != 1) Timber.w("Unexpected args for $name: $args")
|
||||
method.matches1<java.util.List<*>>("onTetherableInterfacesChanged") -> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
callback?.onTetherableInterfacesChanged(args!![0] as List<String?>)
|
||||
}
|
||||
"onTetheredInterfacesChanged" -> {
|
||||
if (noArgs != 1) Timber.w("Unexpected args for $name: $args")
|
||||
method.matches1<java.util.List<*>>("onTetheredInterfacesChanged") -> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
callback?.onTetheredInterfacesChanged(args!![0] as List<String?>)
|
||||
}
|
||||
"onError" -> {
|
||||
if (noArgs != 2) Timber.w("Unexpected args for $name: $args")
|
||||
method.matches("onError", String::class.java, Integer.TYPE) -> {
|
||||
callback?.onError(args!![0] as String, args[1] as Int)
|
||||
}
|
||||
"onClientsChanged" -> {
|
||||
if (noArgs != 1) Timber.w("Unexpected args for $name: $args")
|
||||
method.matches1<java.util.Collection<*>>("onClientsChanged") -> {
|
||||
callback?.onClientsChanged(args!![0] as Collection<*>)
|
||||
}
|
||||
"onOffloadStatusChanged" -> {
|
||||
if (noArgs != 1) Timber.w("Unexpected args for $name: $args")
|
||||
method.matches("onOffloadStatusChanged", Integer.TYPE) -> {
|
||||
callback?.onOffloadStatusChanged(args!![0] as Int)
|
||||
}
|
||||
else -> callSuper(interfaceTetheringEventCallback, proxy, method, args)
|
||||
@@ -596,7 +587,11 @@ object TetheringManager {
|
||||
@RequiresApi(30)
|
||||
fun unregisterTetheringEventCallback(callback: TetheringEventCallback) {
|
||||
val proxy = synchronized(callbackMap) { callbackMap.remove(callback) } ?: return
|
||||
unregisterTetheringEventCallback(instance, proxy)
|
||||
try {
|
||||
unregisterTetheringEventCallback(instance, proxy)
|
||||
} catch (e: InvocationTargetException) {
|
||||
if (!e.targetException.let { it is IllegalStateException && it.cause is DeadObjectException }) throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -636,14 +631,11 @@ object TetheringManager {
|
||||
"TETHER_ERROR_UNSUPPORTED", "TETHER_ERROR_UNAVAIL_IFACE", "TETHER_ERROR_MASTER_ERROR",
|
||||
"TETHER_ERROR_TETHER_IFACE_ERROR", "TETHER_ERROR_UNTETHER_IFACE_ERROR", "TETHER_ERROR_ENABLE_NAT_ERROR",
|
||||
"TETHER_ERROR_DISABLE_NAT_ERROR", "TETHER_ERROR_IFACE_CFG_ERROR", "TETHER_ERROR_PROVISION_FAILED",
|
||||
"TETHER_ERROR_DHCPSERVER_ERROR", "TETHER_ERROR_ENTITLEMENT_UNKNOWN") { clazz }
|
||||
"TETHER_ERROR_DHCPSERVER_ERROR", "TETHER_ERROR_ENTITLEMENT_UNKNOWN") @TargetApi(30) { clazz }
|
||||
@RequiresApi(30)
|
||||
const val TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION = 14
|
||||
|
||||
val Intent.tetheredIfaces get() = getStringArrayListExtra(
|
||||
if (Build.VERSION.SDK_INT >= 26) EXTRA_ACTIVE_TETHER else EXTRA_ACTIVE_TETHER_LEGACY)
|
||||
val Intent.localOnlyTetheredIfaces get() = if (Build.VERSION.SDK_INT >= 26) {
|
||||
getStringArrayListExtra(
|
||||
if (Build.VERSION.SDK_INT >= 30) EXTRA_ACTIVE_LOCAL_ONLY else EXTRA_ACTIVE_LOCAL_ONLY_LEGACY)
|
||||
} else emptyList<String>()
|
||||
val Intent.tetheredIfaces get() = getStringArrayListExtra(EXTRA_ACTIVE_TETHER)
|
||||
val Intent.localOnlyTetheredIfaces get() = getStringArrayListExtra(
|
||||
if (Build.VERSION.SDK_INT >= 30) EXTRA_ACTIVE_LOCAL_ONLY else EXTRA_ACTIVE_LOCAL_ONLY_LEGACY)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
package be.mygod.vpnhotspot.net.monitor
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.LinkProperties
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import be.mygod.vpnhotspot.util.Services
|
||||
import be.mygod.vpnhotspot.util.globalNetworkRequestBuilder
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -20,10 +18,10 @@ object DefaultNetworkMonitor : UpstreamMonitor() {
|
||||
* Unfortunately registerDefaultNetworkCallback is going to return VPN interface since Android P DP1:
|
||||
* https://android.googlesource.com/platform/frameworks/base/+/dda156ab0c5d66ad82bdcf76cda07cbc0a9c8a2e
|
||||
*/
|
||||
private val networkRequest = networkRequestBuilder()
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
|
||||
.build()
|
||||
private val networkRequest = globalNetworkRequestBuilder().apply {
|
||||
addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
|
||||
}.build()
|
||||
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
val properties = Services.connectivity.getLinkProperties(network)
|
||||
@@ -53,23 +51,10 @@ object DefaultNetworkMonitor : UpstreamMonitor() {
|
||||
callback.onAvailable(currentLinkProperties)
|
||||
}
|
||||
} else {
|
||||
when (Build.VERSION.SDK_INT) {
|
||||
in 31..Int.MAX_VALUE -> @TargetApi(31) {
|
||||
Services.connectivity.registerBestMatchingNetworkCallback(networkRequest, networkCallback,
|
||||
Handler(Looper.getMainLooper()))
|
||||
}
|
||||
in 24..27 -> @TargetApi(24) {
|
||||
Services.connectivity.registerDefaultNetworkCallback(networkCallback)
|
||||
}
|
||||
else -> try {
|
||||
Services.connectivity.requestNetwork(networkRequest, networkCallback)
|
||||
} catch (e: SecurityException) {
|
||||
// SecurityException would be thrown in requestNetwork on Android 6.0 thanks to Google's stupid bug
|
||||
if (Build.VERSION.SDK_INT != 23) throw e
|
||||
GlobalScope.launch { callback.onFallback() }
|
||||
return
|
||||
}
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= 31) {
|
||||
Services.connectivity.registerBestMatchingNetworkCallback(networkRequest, networkCallback,
|
||||
Services.mainHandler)
|
||||
} else Services.connectivity.requestNetwork(networkRequest, networkCallback, Services.mainHandler)
|
||||
registered = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import be.mygod.vpnhotspot.util.Services
|
||||
import be.mygod.vpnhotspot.util.allInterfaceNames
|
||||
import be.mygod.vpnhotspot.util.globalNetworkRequestBuilder
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
@@ -18,7 +19,7 @@ class InterfaceMonitor(private val ifaceRegex: String) : UpstreamMonitor() {
|
||||
Timber.d(e);
|
||||
{ it == ifaceRegex }
|
||||
}
|
||||
private val request = networkRequestBuilder().apply {
|
||||
private val request = globalNetworkRequestBuilder().apply {
|
||||
removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
|
||||
removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
|
||||
removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
|
||||
@@ -76,7 +77,7 @@ class InterfaceMonitor(private val ifaceRegex: String) : UpstreamMonitor() {
|
||||
callback.onAvailable(currentLinkProperties)
|
||||
}
|
||||
} else {
|
||||
Services.connectivity.registerNetworkCallback(request, networkCallback)
|
||||
Services.registerNetworkCallback(request, networkCallback)
|
||||
registered = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import androidx.core.content.edit
|
||||
import be.mygod.librootkotlinx.RootServer
|
||||
import be.mygod.librootkotlinx.isEBADF
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.BuildConfig
|
||||
import be.mygod.vpnhotspot.R
|
||||
import be.mygod.vpnhotspot.net.Routing
|
||||
import be.mygod.vpnhotspot.root.ProcessData
|
||||
@@ -25,12 +24,12 @@ abstract class IpMonitor {
|
||||
companion object {
|
||||
const val KEY = "service.ipMonitor"
|
||||
// https://android.googlesource.com/platform/external/iproute2/+/7f7a711/lib/libnetlink.c#493
|
||||
private val errorMatcher = ("(^Cannot bind netlink socket: |" +
|
||||
private val errorMatcher = ("(?:^Cannot (?:bind netlink socket|send dump request): |^request send failed: |" +
|
||||
"Dump (was interrupted and may be inconsistent.|terminated)$)").toRegex()
|
||||
var currentMode: Mode
|
||||
get() {
|
||||
val isLegacy = Build.VERSION.SDK_INT < 30 || BuildConfig.TARGET_SDK < 30
|
||||
val defaultMode = if (isLegacy) @Suppress("DEPRECATION") {
|
||||
// Completely restricted on Android 13: https://github.com/termux/termux-app/issues/2993#issuecomment-1250312777
|
||||
val defaultMode = if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
|
||||
Mode.Poll
|
||||
} else Mode.MonitorRoot
|
||||
return Mode.valueOf(app.pref.getString(KEY, defaultMode.toString()) ?: "")
|
||||
@@ -114,8 +113,8 @@ abstract class IpMonitor {
|
||||
try {
|
||||
RootManager.use { server ->
|
||||
// while we only need to use this server once, we need to also keep the server alive
|
||||
handleChannel(server.create(ProcessListener(errorMatcher, Routing.IP, "monitor", monitoredObject),
|
||||
this))
|
||||
handleChannel(server.create(ProcessListener(errorMatcher,
|
||||
Routing.IP, "monitor", monitoredObject), this))
|
||||
}
|
||||
} catch (_: CancellationException) {
|
||||
} catch (e: Exception) {
|
||||
@@ -152,7 +151,7 @@ abstract class IpMonitor {
|
||||
fun flushAsync() = GlobalScope.launch(Dispatchers.IO) { flush() }
|
||||
|
||||
private suspend fun work(server: RootServer?): RootServer? {
|
||||
if (currentMode != Mode.PollRoot) try {
|
||||
if (currentMode != Mode.PollRoot && currentMode != Mode.MonitorRoot) try {
|
||||
poll()
|
||||
return server
|
||||
} catch (e: IOException) {
|
||||
|
||||
@@ -33,30 +33,41 @@ class TetherTimeoutMonitor(private val timeout: Long = 0,
|
||||
private const val MIN_SOFT_AP_TIMEOUT_DELAY_MS = 600_000 // 10 minutes
|
||||
|
||||
@Deprecated("Use SoftApConfigurationCompat instead")
|
||||
@get:RequiresApi(28)
|
||||
val enabled get() = Settings.Global.getInt(app.contentResolver, SOFT_AP_TIMEOUT_ENABLED, 1) == 1
|
||||
@Deprecated("Use SoftApConfigurationCompat instead")
|
||||
suspend fun setEnabled(value: Boolean) = SettingsGlobalPut.int(SOFT_AP_TIMEOUT_ENABLED, if (value) 1 else 0)
|
||||
|
||||
val defaultTimeout: Int get() {
|
||||
val delay = if (Build.VERSION.SDK_INT >= 28) try {
|
||||
val delay = try {
|
||||
if (Build.VERSION.SDK_INT < 30) Resources.getSystem().run {
|
||||
getInteger(getIdentifier("config_wifi_framework_soft_ap_timeout_delay", "integer", "android"))
|
||||
} else {
|
||||
val info = WifiApManager.resolvedActivity.activityInfo
|
||||
val resources = app.packageManager.getResourcesForApplication(info.applicationInfo)
|
||||
resources.getInteger(resources.findIdentifier("config_wifiFrameworkSoftApShutDownTimeoutMilliseconds",
|
||||
"integer", WifiApManager.RESOURCES_PACKAGE, info.packageName))
|
||||
resources.getInteger(resources.findIdentifier(
|
||||
"config_wifiFrameworkSoftApShutDownTimeoutMilliseconds", "integer",
|
||||
WifiApManager.RESOURCES_PACKAGE, info.packageName))
|
||||
}
|
||||
} catch (e: RuntimeException) {
|
||||
Timber.w(e)
|
||||
MIN_SOFT_AP_TIMEOUT_DELAY_MS
|
||||
} else MIN_SOFT_AP_TIMEOUT_DELAY_MS
|
||||
}
|
||||
return if (Build.VERSION.SDK_INT < 30 && delay < MIN_SOFT_AP_TIMEOUT_DELAY_MS) {
|
||||
Timber.w("Overriding timeout delay with minimum limit value: $delay < $MIN_SOFT_AP_TIMEOUT_DELAY_MS")
|
||||
MIN_SOFT_AP_TIMEOUT_DELAY_MS
|
||||
} else delay
|
||||
}
|
||||
@get:RequiresApi(31)
|
||||
val defaultTimeoutBridged: Int get() = try {
|
||||
val info = WifiApManager.resolvedActivity.activityInfo
|
||||
val resources = app.packageManager.getResourcesForApplication(info.applicationInfo)
|
||||
resources.getInteger(resources.findIdentifier(
|
||||
"config_wifiFrameworkSoftApShutDownIdleInstanceInBridgedModeTimeoutMillisecond", "integer",
|
||||
WifiApManager.RESOURCES_PACKAGE, info.packageName))
|
||||
} catch (e: RuntimeException) {
|
||||
Timber.w(e)
|
||||
MIN_SOFT_AP_TIMEOUT_DELAY_MS
|
||||
}
|
||||
}
|
||||
|
||||
private var noClient = true
|
||||
@@ -74,7 +85,7 @@ class TetherTimeoutMonitor(private val timeout: Long = 0,
|
||||
fun onClientsChanged(noClient: Boolean) {
|
||||
this.noClient = noClient
|
||||
if (!noClient) close() else if (timeoutJob == null) timeoutJob = GlobalScope.launch(context) {
|
||||
delay(if (timeout == 0L) defaultTimeout.toLong() else timeout)
|
||||
delay(if (timeout <= 0L) defaultTimeout.toLong() else timeout)
|
||||
onTimeout()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package be.mygod.vpnhotspot.net.monitor
|
||||
|
||||
import android.net.MacAddress
|
||||
import androidx.collection.LongSparseArray
|
||||
import androidx.collection.set
|
||||
import be.mygod.vpnhotspot.net.IpDev
|
||||
import be.mygod.vpnhotspot.net.MacAddressCompat
|
||||
import be.mygod.vpnhotspot.net.Routing.Companion.IPTABLES
|
||||
import be.mygod.vpnhotspot.room.AppDatabase
|
||||
import be.mygod.vpnhotspot.room.TrafficRecord
|
||||
@@ -11,7 +11,12 @@ import be.mygod.vpnhotspot.util.Event2
|
||||
import be.mygod.vpnhotspot.util.RootSession
|
||||
import be.mygod.vpnhotspot.util.parseNumericAddress
|
||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.net.InetAddress
|
||||
import java.util.concurrent.TimeUnit
|
||||
@@ -23,8 +28,8 @@ object TrafficRecorder {
|
||||
private val records = mutableMapOf<IpDev, TrafficRecord>()
|
||||
val foregroundListeners = Event2<Collection<TrafficRecord>, LongSparseArray<TrafficRecord>>()
|
||||
|
||||
fun register(ip: InetAddress, downstream: String, mac: MacAddressCompat) {
|
||||
val record = TrafficRecord(mac = mac.addr, ip = ip, downstream = downstream)
|
||||
fun register(ip: InetAddress, downstream: String, mac: MacAddress) {
|
||||
val record = TrafficRecord(mac = mac, ip = ip, downstream = downstream)
|
||||
AppDatabase.instance.trafficRecordDao.insert(record)
|
||||
synchronized(this) {
|
||||
val key = IpDev(ip, downstream)
|
||||
@@ -107,9 +112,9 @@ object TrafficRecorder {
|
||||
record.sentBytes = columns[1].toLong()
|
||||
}
|
||||
}
|
||||
if (oldRecord.id != null) {
|
||||
oldRecord.id?.let { oldId ->
|
||||
check(records.put(key, record) == oldRecord)
|
||||
oldRecords[oldRecord.id!!] = oldRecord
|
||||
oldRecords[oldId] = oldRecord
|
||||
}
|
||||
}
|
||||
else -> check(false)
|
||||
@@ -130,6 +135,7 @@ object TrafficRecorder {
|
||||
}
|
||||
fun update(timeout: Boolean = false) {
|
||||
synchronized(this) {
|
||||
unscheduleUpdateLocked()
|
||||
if (records.isEmpty()) return
|
||||
val timestamp = System.currentTimeMillis()
|
||||
if (!timeout && timestamp - lastUpdate <= 100) return
|
||||
@@ -141,7 +147,6 @@ object TrafficRecorder {
|
||||
SmartSnackbar.make(e).show()
|
||||
}
|
||||
lastUpdate = timestamp
|
||||
updateJob = null
|
||||
scheduleUpdateLocked()
|
||||
}
|
||||
}
|
||||
@@ -156,5 +161,5 @@ object TrafficRecorder {
|
||||
/**
|
||||
* Possibly inefficient. Don't call this too often.
|
||||
*/
|
||||
fun isWorking(mac: MacAddressCompat) = records.values.any { it.mac == mac.addr }
|
||||
fun isWorking(mac: MacAddress) = records.values.any { it.mac == mac }
|
||||
}
|
||||
|
||||
@@ -2,9 +2,6 @@ package be.mygod.vpnhotspot.net.monitor
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.net.LinkProperties
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkRequest
|
||||
import android.os.Build
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -23,13 +20,6 @@ abstract class UpstreamMonitor {
|
||||
}
|
||||
private var monitor = generateMonitor()
|
||||
|
||||
fun networkRequestBuilder() = NetworkRequest.Builder().apply {
|
||||
if (Build.VERSION.SDK_INT == 23) { // workarounds for OEM bugs
|
||||
removeCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||
removeCapability(NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL)
|
||||
}
|
||||
}
|
||||
|
||||
fun registerCallback(callback: Callback) = synchronized(this) { monitor.registerCallback(callback) }
|
||||
fun unregisterCallback(callback: Callback) = synchronized(this) { monitor.unregisterCallback(callback) }
|
||||
|
||||
@@ -56,15 +46,6 @@ abstract class UpstreamMonitor {
|
||||
* Called if some possibly stacked interface is available
|
||||
*/
|
||||
fun onAvailable(properties: LinkProperties? = null)
|
||||
/**
|
||||
* Called on API 23- from DefaultNetworkMonitor. This indicates that there isn't a good way of telling the
|
||||
* default network (see DefaultNetworkMonitor) and we are using rules at priority 22000
|
||||
* (RULE_PRIORITY_DEFAULT_NETWORK) as our fallback rules, which would work fine until Android 9.0 broke it in
|
||||
* commit: https://android.googlesource.com/platform/system/netd/+/758627c4d93392190b08e9aaea3bbbfb92a5f364
|
||||
*/
|
||||
fun onFallback() {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
}
|
||||
|
||||
val callbacks = mutableSetOf<Callback>()
|
||||
|
||||
@@ -5,15 +5,16 @@ import android.net.LinkProperties
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import be.mygod.vpnhotspot.util.Services
|
||||
import be.mygod.vpnhotspot.util.globalNetworkRequestBuilder
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
object VpnMonitor : UpstreamMonitor() {
|
||||
private val request = networkRequestBuilder()
|
||||
.addTransportType(NetworkCapabilities.TRANSPORT_VPN)
|
||||
.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
|
||||
.build()
|
||||
private val request = globalNetworkRequestBuilder().apply {
|
||||
addTransportType(NetworkCapabilities.TRANSPORT_VPN)
|
||||
removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
|
||||
}.build()
|
||||
private var registered = false
|
||||
|
||||
private val available = HashMap<Network, LinkProperties?>()
|
||||
@@ -60,7 +61,7 @@ object VpnMonitor : UpstreamMonitor() {
|
||||
callback.onAvailable(currentLinkProperties)
|
||||
}
|
||||
} else {
|
||||
Services.connectivity.registerNetworkCallback(request, networkCallback)
|
||||
Services.registerNetworkCallback(request, networkCallback)
|
||||
registered = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package be.mygod.vpnhotspot.net.wifi
|
||||
|
||||
import android.net.MacAddress
|
||||
import android.net.wifi.p2p.WifiP2pGroup
|
||||
import be.mygod.vpnhotspot.RepeaterService
|
||||
import be.mygod.vpnhotspot.net.MacAddressCompat
|
||||
@@ -53,8 +54,8 @@ class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null) {
|
||||
var bssids = listOfNotNull(group?.owner?.deviceAddress, ownerAddress)
|
||||
.distinct()
|
||||
.filter {
|
||||
val mac = MacAddress.fromString(it)
|
||||
try {
|
||||
val mac = MacAddressCompat.fromString(it)
|
||||
mac != MacAddressCompat.ALL_ZEROS_ADDRESS && mac != MacAddressCompat.ANY_ADDRESS
|
||||
} catch (_: IllegalArgumentException) {
|
||||
false
|
||||
@@ -75,7 +76,13 @@ class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null) {
|
||||
if (matchedBssid.isEmpty()) {
|
||||
check(block.pskLine == null && block.psk == null)
|
||||
if (match.groups[5] != null) {
|
||||
block.psk = match.groupValues[5].apply { check(length in 8..63) }
|
||||
block.psk = match.groupValues[5].apply {
|
||||
when (length) {
|
||||
in 8..63 -> { }
|
||||
64 -> error("WPA-PSK hex not supported")
|
||||
else -> error("Unknown length $length")
|
||||
}
|
||||
}
|
||||
}
|
||||
block.pskLine = block.size
|
||||
} else if (bssids.any { matchedBssid.equals(it, true) }) {
|
||||
@@ -120,7 +127,7 @@ class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null) {
|
||||
add("\tmode=3")
|
||||
add("\tdisabled=2")
|
||||
add("}")
|
||||
if (target == null) target = this
|
||||
target = this
|
||||
})
|
||||
}
|
||||
content = Content(result, target!!, persistentMacLine, legacy)
|
||||
@@ -135,13 +142,12 @@ class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null) {
|
||||
}
|
||||
val psk by lazy { group?.passphrase ?: content.target.psk!! }
|
||||
val bssid by lazy {
|
||||
content.target.bssid?.let { MacAddressCompat.fromString(it) }
|
||||
content.target.bssid?.let { MacAddress.fromString(it) }
|
||||
}
|
||||
|
||||
suspend fun update(ssid: String, psk: String, bssid: MacAddressCompat?) {
|
||||
suspend fun update(ssid: WifiSsidCompat, psk: String, bssid: MacAddress?) {
|
||||
val (lines, block, persistentMacLine, legacy) = content
|
||||
block[block.ssidLine!!] = "\tssid=" + ssid.toByteArray()
|
||||
.joinToString("") { (it.toInt() and 255).toString(16).padStart(2, '0') }
|
||||
block[block.ssidLine!!] = "\tssid=${ssid.hex}"
|
||||
block[block.pskLine!!] = "\tpsk=\"$psk\"" // no control chars or weird stuff
|
||||
if (bssid != null) {
|
||||
persistentMacLine?.let { lines[it] = PERSISTENT_MAC + bssid }
|
||||
|
||||
@@ -1,20 +1,27 @@
|
||||
package be.mygod.vpnhotspot.net.wifi
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.os.Build
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.RequiresApi
|
||||
import be.mygod.vpnhotspot.util.LongConstantLookup
|
||||
import be.mygod.vpnhotspot.util.UnblockCentral
|
||||
import timber.log.Timber
|
||||
|
||||
@JvmInline
|
||||
@RequiresApi(30)
|
||||
value class SoftApCapability(val inner: Parcelable) {
|
||||
companion object {
|
||||
private val clazz by lazy { Class.forName("android.net.wifi.SoftApCapability") }
|
||||
val clazz by lazy { Class.forName("android.net.wifi.SoftApCapability") }
|
||||
private val getMaxSupportedClients by lazy { clazz.getDeclaredMethod("getMaxSupportedClients") }
|
||||
private val areFeaturesSupported by lazy { clazz.getDeclaredMethod("areFeaturesSupported", Long::class.java) }
|
||||
@get:RequiresApi(31)
|
||||
private val getSupportedChannelList by lazy {
|
||||
clazz.getDeclaredMethod("getSupportedChannelList", Int::class.java)
|
||||
}
|
||||
@get:RequiresApi(31)
|
||||
@get:TargetApi(33)
|
||||
private val getCountryCode by lazy { UnblockCentral.getCountryCode(clazz) }
|
||||
|
||||
@RequiresApi(31)
|
||||
const val SOFTAP_FEATURE_BAND_24G_SUPPORTED = 32L
|
||||
@@ -38,4 +45,11 @@ value class SoftApCapability(val inner: Parcelable) {
|
||||
return supportedFeatures
|
||||
}
|
||||
fun getSupportedChannelList(band: Int) = getSupportedChannelList(inner, band) as IntArray
|
||||
@get:RequiresApi(31)
|
||||
val countryCode: String? get() = try {
|
||||
getCountryCode(inner) as String?
|
||||
} catch (e: ReflectiveOperationException) {
|
||||
if (Build.VERSION.SDK_INT >= 33) Timber.w(e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,59 +3,70 @@ package be.mygod.vpnhotspot.net.wifi
|
||||
import android.annotation.SuppressLint
|
||||
import android.annotation.TargetApi
|
||||
import android.net.MacAddress
|
||||
import android.net.wifi.ScanResult
|
||||
import android.net.wifi.SoftApConfiguration
|
||||
import android.net.wifi.WifiSsid
|
||||
import android.os.Build
|
||||
import android.os.Parcelable
|
||||
import android.util.SparseIntArray
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.os.BuildCompat
|
||||
import androidx.core.util.keyIterator
|
||||
import be.mygod.vpnhotspot.net.MacAddressCompat
|
||||
import be.mygod.vpnhotspot.net.MacAddressCompat.Companion.toCompat
|
||||
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
|
||||
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.requireSingleBand
|
||||
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.setChannel
|
||||
import be.mygod.vpnhotspot.net.wifi.WifiSsidCompat.Companion.toCompat
|
||||
import be.mygod.vpnhotspot.util.ConstantLookup
|
||||
import be.mygod.vpnhotspot.util.UnblockCentral
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import timber.log.Timber
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
|
||||
@Parcelize
|
||||
data class SoftApConfigurationCompat(
|
||||
var ssid: String? = null,
|
||||
@Deprecated("Workaround for using inline class with Parcelize, use bssid")
|
||||
var bssidAddr: Long? = null,
|
||||
var passphrase: String? = null,
|
||||
var isHiddenSsid: Boolean = false,
|
||||
/**
|
||||
* To read legacy band/channel pair, use [requireSingleBand]. For easy access, see [getChannel].
|
||||
*
|
||||
* You should probably set or modify this field directly only when you want to use bridged AP,
|
||||
* see also [android.net.wifi.WifiManager.isBridgedApConcurrencySupported].
|
||||
* Otherwise, use [optimizeChannels] or [setChannel].
|
||||
*/
|
||||
@TargetApi(23)
|
||||
var channels: SparseIntArray = SparseIntArray(1).apply { put(BAND_2GHZ, 0) },
|
||||
var securityType: Int = SoftApConfiguration.SECURITY_TYPE_OPEN,
|
||||
@TargetApi(30)
|
||||
var maxNumberOfClients: Int = 0,
|
||||
@TargetApi(28)
|
||||
var isAutoShutdownEnabled: Boolean = true,
|
||||
@TargetApi(28)
|
||||
var shutdownTimeoutMillis: Long = 0,
|
||||
@TargetApi(30)
|
||||
var isClientControlByUserEnabled: Boolean = false,
|
||||
@RequiresApi(30)
|
||||
var blockedClientList: List<MacAddress> = emptyList(),
|
||||
@RequiresApi(30)
|
||||
var allowedClientList: List<MacAddress> = emptyList(),
|
||||
@TargetApi(31)
|
||||
var macRandomizationSetting: Int = RANDOMIZATION_PERSISTENT,
|
||||
@TargetApi(31)
|
||||
var isBridgedModeOpportunisticShutdownEnabled: Boolean = true,
|
||||
@TargetApi(31)
|
||||
var isIeee80211axEnabled: Boolean = true,
|
||||
@TargetApi(31)
|
||||
var isUserConfiguration: Boolean = true,
|
||||
var underlying: Parcelable? = null) : Parcelable {
|
||||
var ssid: WifiSsidCompat? = null,
|
||||
var bssid: MacAddress? = null,
|
||||
var passphrase: String? = null,
|
||||
var isHiddenSsid: Boolean = false,
|
||||
/**
|
||||
* You should probably set or modify this field directly only when you want to use bridged AP,
|
||||
* see also [android.net.wifi.WifiManager.isBridgedApConcurrencySupported].
|
||||
* Otherwise, use [requireSingleBand] and [setChannel].
|
||||
*/
|
||||
var channels: SparseIntArray = SparseIntArray(1).apply { append(BAND_2GHZ, 0) },
|
||||
var securityType: Int = SoftApConfiguration.SECURITY_TYPE_OPEN,
|
||||
@TargetApi(30)
|
||||
var maxNumberOfClients: Int = 0,
|
||||
var isAutoShutdownEnabled: Boolean = true,
|
||||
var shutdownTimeoutMillis: Long = 0,
|
||||
@TargetApi(30)
|
||||
var isClientControlByUserEnabled: Boolean = false,
|
||||
@RequiresApi(30)
|
||||
var blockedClientList: List<MacAddress> = emptyList(),
|
||||
@RequiresApi(30)
|
||||
var allowedClientList: List<MacAddress> = emptyList(),
|
||||
@TargetApi(31)
|
||||
var macRandomizationSetting: Int = if (Build.VERSION.SDK_INT >= 33) {
|
||||
RANDOMIZATION_NON_PERSISTENT
|
||||
} else RANDOMIZATION_PERSISTENT,
|
||||
@TargetApi(31)
|
||||
var isBridgedModeOpportunisticShutdownEnabled: Boolean = true,
|
||||
@TargetApi(31)
|
||||
var isIeee80211axEnabled: Boolean = true,
|
||||
@TargetApi(33)
|
||||
var isIeee80211beEnabled: Boolean = true,
|
||||
@TargetApi(31)
|
||||
var isUserConfiguration: Boolean = true,
|
||||
@TargetApi(33)
|
||||
var bridgedModeOpportunisticShutdownTimeoutMillis: Long = -1L,
|
||||
@TargetApi(33)
|
||||
var vendorElements: List<ScanResult.InformationElement> = emptyList(),
|
||||
@TargetApi(33)
|
||||
var persistentRandomizedMacAddress: MacAddress? = null,
|
||||
@TargetApi(33)
|
||||
var allowedAcsChannels: Map<Int, Set<Int>> = emptyMap(),
|
||||
@TargetApi(33)
|
||||
var maxChannelBandwidth: Int = CHANNEL_WIDTH_AUTO,
|
||||
var underlying: Parcelable? = null,
|
||||
) : Parcelable {
|
||||
companion object {
|
||||
const val BAND_2GHZ = 1
|
||||
const val BAND_5GHZ = 2
|
||||
@@ -64,20 +75,32 @@ data class SoftApConfigurationCompat(
|
||||
@TargetApi(31)
|
||||
const val BAND_60GHZ = 8
|
||||
const val BAND_LEGACY = BAND_2GHZ or BAND_5GHZ
|
||||
@TargetApi(30)
|
||||
const val BAND_ANY_30 = BAND_LEGACY or BAND_6GHZ
|
||||
@TargetApi(31)
|
||||
const val BAND_ANY_31 = BAND_ANY_30 or BAND_60GHZ
|
||||
val BAND_TYPES by lazy {
|
||||
if (BuildCompat.isAtLeastS()) try {
|
||||
if (Build.VERSION.SDK_INT >= 31) try {
|
||||
return@lazy UnblockCentral.SoftApConfiguration_BAND_TYPES
|
||||
} catch (e: ReflectiveOperationException) {
|
||||
Timber.w(e)
|
||||
}
|
||||
intArrayOf(BAND_2GHZ, BAND_5GHZ, BAND_6GHZ, BAND_60GHZ)
|
||||
}
|
||||
val bandLookup = ConstantLookup<SoftApConfiguration>("BAND_", null, "2GHZ", "5GHZ")
|
||||
@RequiresApi(31)
|
||||
val bandLookup = ConstantLookup<SoftApConfiguration>("BAND_")
|
||||
|
||||
@TargetApi(31)
|
||||
const val RANDOMIZATION_NONE = 0
|
||||
@TargetApi(31)
|
||||
const val RANDOMIZATION_PERSISTENT = 1
|
||||
@TargetApi(33)
|
||||
const val RANDOMIZATION_NON_PERSISTENT = 2
|
||||
|
||||
@TargetApi(33)
|
||||
const val CHANNEL_WIDTH_AUTO = -1
|
||||
@TargetApi(30)
|
||||
const val CHANNEL_WIDTH_INVALID = 0
|
||||
|
||||
fun isLegacyEitherBand(band: Int) = band and BAND_LEGACY == BAND_LEGACY
|
||||
|
||||
@@ -86,15 +109,19 @@ data class SoftApConfigurationCompat(
|
||||
*/
|
||||
private const val LEGACY_WPA2_PSK = 4
|
||||
|
||||
val securityTypes = arrayOf("OPEN", "WPA2-PSK", "WPA3-SAE", "WPA3-SAE Transition mode")
|
||||
|
||||
private val qrSanitizer = Regex("([\\\\\":;,])")
|
||||
val securityTypes = arrayOf(
|
||||
"OPEN",
|
||||
"WPA2-PSK",
|
||||
"WPA3-SAE Transition mode",
|
||||
"WPA3-SAE",
|
||||
"WPA3-OWE Transition",
|
||||
"WPA3-OWE",
|
||||
)
|
||||
|
||||
/**
|
||||
* Based on:
|
||||
* https://elixir.bootlin.com/linux/v5.12.8/source/net/wireless/util.c#L75
|
||||
* TODO: update for Android 12
|
||||
* https://cs.android.com/android/platform/superproject/+/master:frameworks/base/wifi/java/android/net/wifi/ScanResult.java;l=624;drc=f7ccda05642b55700d67a288462bada488fc7f5e
|
||||
* https://cs.android.com/android/platform/superproject/+/master:packages/modules/Wifi/framework/java/android/net/wifi/ScanResult.java;l=789;drc=71d758698c45984d3f8de981bf98e56902480f16
|
||||
*/
|
||||
fun channelToFrequency(band: Int, chan: Int) = when (band) {
|
||||
BAND_2GHZ -> when (chan) {
|
||||
@@ -109,7 +136,7 @@ data class SoftApConfigurationCompat(
|
||||
}
|
||||
BAND_6GHZ -> when (chan) {
|
||||
2 -> 5935
|
||||
in 1..233 -> 5950 + chan * 5
|
||||
in 1..253 -> 5950 + chan * 5
|
||||
else -> throw IllegalArgumentException("Invalid 6GHz channel $chan")
|
||||
}
|
||||
BAND_60GHZ -> {
|
||||
@@ -134,7 +161,6 @@ data class SoftApConfigurationCompat(
|
||||
*
|
||||
* 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
|
||||
@@ -142,7 +168,6 @@ data class SoftApConfigurationCompat(
|
||||
* 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
|
||||
@@ -154,6 +179,10 @@ data class SoftApConfigurationCompat(
|
||||
android.net.wifi.WifiConfiguration::class.java.getDeclaredField("apChannel")
|
||||
}
|
||||
|
||||
@get:RequiresApi(33)
|
||||
private val getAllowedAcsChannels by lazy @TargetApi(33) {
|
||||
SoftApConfiguration::class.java.getDeclaredMethod("getAllowedAcsChannels", Int::class.java)
|
||||
}
|
||||
@get:RequiresApi(30)
|
||||
private val getAllowedClientList by lazy @TargetApi(30) {
|
||||
SoftApConfiguration::class.java.getDeclaredMethod("getAllowedClientList")
|
||||
@@ -164,6 +193,10 @@ data class SoftApConfigurationCompat(
|
||||
private val getBlockedClientList by lazy @TargetApi(30) {
|
||||
SoftApConfiguration::class.java.getDeclaredMethod("getBlockedClientList")
|
||||
}
|
||||
@get:RequiresApi(33)
|
||||
private val getBridgedModeOpportunisticShutdownTimeoutMillis by lazy @TargetApi(33) {
|
||||
SoftApConfiguration::class.java.getDeclaredMethod("getBridgedModeOpportunisticShutdownTimeoutMillis")
|
||||
}
|
||||
@get:RequiresApi(30)
|
||||
private val getChannel by lazy @TargetApi(30) {
|
||||
SoftApConfiguration::class.java.getDeclaredMethod("getChannel")
|
||||
@@ -176,14 +209,26 @@ data class SoftApConfigurationCompat(
|
||||
private val getMacRandomizationSetting by lazy @TargetApi(31) {
|
||||
SoftApConfiguration::class.java.getDeclaredMethod("getMacRandomizationSetting")
|
||||
}
|
||||
@get:RequiresApi(33)
|
||||
private val getMaxChannelBandwidth by lazy @TargetApi(33) {
|
||||
SoftApConfiguration::class.java.getDeclaredMethod("getMaxChannelBandwidth")
|
||||
}
|
||||
@get:RequiresApi(30)
|
||||
private val getMaxNumberOfClients by lazy @TargetApi(30) {
|
||||
SoftApConfiguration::class.java.getDeclaredMethod("getMaxNumberOfClients")
|
||||
}
|
||||
@get:RequiresApi(33)
|
||||
private val getPersistentRandomizedMacAddress by lazy @TargetApi(33) {
|
||||
SoftApConfiguration::class.java.getDeclaredMethod("getPersistentRandomizedMacAddress")
|
||||
}
|
||||
@get:RequiresApi(30)
|
||||
private val getShutdownTimeoutMillis by lazy @TargetApi(30) {
|
||||
SoftApConfiguration::class.java.getDeclaredMethod("getShutdownTimeoutMillis")
|
||||
}
|
||||
@get:RequiresApi(33)
|
||||
private val getVendorElements by lazy @TargetApi(33) {
|
||||
SoftApConfiguration::class.java.getDeclaredMethod("getVendorElements")
|
||||
}
|
||||
@get:RequiresApi(30)
|
||||
private val isAutoShutdownEnabled by lazy @TargetApi(30) {
|
||||
SoftApConfiguration::class.java.getDeclaredMethod("isAutoShutdownEnabled")
|
||||
@@ -200,6 +245,10 @@ data class SoftApConfigurationCompat(
|
||||
private val isIeee80211axEnabled by lazy @TargetApi(31) {
|
||||
SoftApConfiguration::class.java.getDeclaredMethod("isIeee80211axEnabled")
|
||||
}
|
||||
@get:RequiresApi(33)
|
||||
private val isIeee80211beEnabled by lazy @TargetApi(33) {
|
||||
SoftApConfiguration::class.java.getDeclaredMethod("isIeee80211beEnabled")
|
||||
}
|
||||
@get:RequiresApi(31)
|
||||
private val isUserConfiguration by lazy @TargetApi(31) {
|
||||
SoftApConfiguration::class.java.getDeclaredMethod("isUserConfiguration")
|
||||
@@ -210,78 +259,106 @@ data class SoftApConfigurationCompat(
|
||||
@get:RequiresApi(30)
|
||||
private val newBuilder by lazy @TargetApi(30) { classBuilder.getConstructor(SoftApConfiguration::class.java) }
|
||||
@get:RequiresApi(30)
|
||||
private val build by lazy { classBuilder.getDeclaredMethod("build") }
|
||||
private val build by lazy @TargetApi(30) { classBuilder.getDeclaredMethod("build") }
|
||||
@get:RequiresApi(33)
|
||||
private val setAllowedAcsChannels by lazy @TargetApi(33) {
|
||||
classBuilder.getDeclaredMethod("setAllowedAcsChannels", Int::class.java, IntArray::class.java)
|
||||
}
|
||||
@get:RequiresApi(30)
|
||||
private val setAllowedClientList by lazy {
|
||||
private val setAllowedClientList by lazy @TargetApi(30) {
|
||||
classBuilder.getDeclaredMethod("setAllowedClientList", java.util.List::class.java)
|
||||
}
|
||||
@get:RequiresApi(30)
|
||||
private val setAutoShutdownEnabled by lazy {
|
||||
private val setAutoShutdownEnabled by lazy @TargetApi(30) {
|
||||
classBuilder.getDeclaredMethod("setAutoShutdownEnabled", Boolean::class.java)
|
||||
}
|
||||
@get:RequiresApi(30)
|
||||
private val setBand by lazy { classBuilder.getDeclaredMethod("setBand", Int::class.java) }
|
||||
private val setBand by lazy @TargetApi(30) { classBuilder.getDeclaredMethod("setBand", Int::class.java) }
|
||||
@get:RequiresApi(30)
|
||||
private val setBlockedClientList by lazy {
|
||||
private val setBlockedClientList by lazy @TargetApi(30) {
|
||||
classBuilder.getDeclaredMethod("setBlockedClientList", java.util.List::class.java)
|
||||
}
|
||||
@get:RequiresApi(31)
|
||||
private val setBridgedModeOpportunisticShutdownEnabled by lazy {
|
||||
private val setBridgedModeOpportunisticShutdownEnabled by lazy @TargetApi(31) {
|
||||
classBuilder.getDeclaredMethod("setBridgedModeOpportunisticShutdownEnabled", Boolean::class.java)
|
||||
}
|
||||
@get:RequiresApi(33)
|
||||
private val setBridgedModeOpportunisticShutdownTimeoutMillis by lazy @TargetApi(33) {
|
||||
classBuilder.getDeclaredMethod("setBridgedModeOpportunisticShutdownTimeoutMillis", Long::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 {
|
||||
private val setChannel by lazy @TargetApi(30) {
|
||||
classBuilder.getDeclaredMethod("setChannel", Int::class.java, Int::class.java)
|
||||
}
|
||||
@get:RequiresApi(31)
|
||||
private val setChannels by lazy {
|
||||
private val setChannels by lazy @TargetApi(31) {
|
||||
classBuilder.getDeclaredMethod("setChannels", SparseIntArray::class.java)
|
||||
}
|
||||
@get:RequiresApi(30)
|
||||
private val setClientControlByUserEnabled by lazy {
|
||||
private val setClientControlByUserEnabled by lazy @TargetApi(30) {
|
||||
classBuilder.getDeclaredMethod("setClientControlByUserEnabled", Boolean::class.java)
|
||||
}
|
||||
@get:RequiresApi(30)
|
||||
private val setHiddenSsid by lazy { classBuilder.getDeclaredMethod("setHiddenSsid", Boolean::class.java) }
|
||||
private val setHiddenSsid by lazy @TargetApi(30) {
|
||||
classBuilder.getDeclaredMethod("setHiddenSsid", Boolean::class.java)
|
||||
}
|
||||
@get:RequiresApi(31)
|
||||
private val setIeee80211axEnabled by lazy {
|
||||
private val setIeee80211axEnabled by lazy @TargetApi(31) {
|
||||
classBuilder.getDeclaredMethod("setIeee80211axEnabled", Boolean::class.java)
|
||||
}
|
||||
@get:RequiresApi(33)
|
||||
private val setIeee80211beEnabled by lazy @TargetApi(33) {
|
||||
classBuilder.getDeclaredMethod("setIeee80211beEnabled", Boolean::class.java)
|
||||
}
|
||||
@get:RequiresApi(31)
|
||||
private val setMacRandomizationSetting by lazy {
|
||||
private val setMacRandomizationSetting by lazy @TargetApi(31) {
|
||||
classBuilder.getDeclaredMethod("setMacRandomizationSetting", Int::class.java)
|
||||
}
|
||||
@get:RequiresApi(33)
|
||||
private val setMaxChannelBandwidth by lazy @TargetApi(33) {
|
||||
classBuilder.getDeclaredMethod("setMaxChannelBandwidth", Int::class.java)
|
||||
}
|
||||
@get:RequiresApi(30)
|
||||
private val setMaxNumberOfClients by lazy {
|
||||
private val setMaxNumberOfClients by lazy @TargetApi(31) {
|
||||
classBuilder.getDeclaredMethod("setMaxNumberOfClients", Int::class.java)
|
||||
}
|
||||
@get:RequiresApi(30)
|
||||
private val setPassphrase by lazy {
|
||||
private val setPassphrase by lazy @TargetApi(30) {
|
||||
classBuilder.getDeclaredMethod("setPassphrase", String::class.java, Int::class.java)
|
||||
}
|
||||
@get:RequiresApi(33)
|
||||
private val setRandomizedMacAddress by lazy @TargetApi(33) {
|
||||
UnblockCentral.setRandomizedMacAddress(classBuilder)
|
||||
}
|
||||
@get:RequiresApi(30)
|
||||
private val setShutdownTimeoutMillis by lazy {
|
||||
private val setShutdownTimeoutMillis by lazy @TargetApi(30) {
|
||||
classBuilder.getDeclaredMethod("setShutdownTimeoutMillis", Long::class.java)
|
||||
}
|
||||
@get:RequiresApi(30)
|
||||
private val setSsid by lazy { classBuilder.getDeclaredMethod("setSsid", String::class.java) }
|
||||
@get:RequiresApi(31)
|
||||
private val setUserConfiguration by lazy @TargetApi(31) { UnblockCentral.setUserConfiguration(classBuilder) }
|
||||
private val setSsid by lazy @TargetApi(30) { classBuilder.getDeclaredMethod("setSsid", String::class.java) }
|
||||
@get:RequiresApi(33)
|
||||
private val setVendorElements by lazy @TargetApi(33) {
|
||||
classBuilder.getDeclaredMethod("setVendorElements", java.util.List::class.java)
|
||||
}
|
||||
@get:RequiresApi(33)
|
||||
private val setWifiSsid by lazy @TargetApi(33) {
|
||||
classBuilder.getDeclaredMethod("setWifiSsid", WifiSsid::class.java)
|
||||
}
|
||||
|
||||
@Deprecated("Class deprecated in framework")
|
||||
@Suppress("DEPRECATION")
|
||||
fun android.net.wifi.WifiConfiguration.toCompat() = SoftApConfigurationCompat(
|
||||
SSID,
|
||||
BSSID?.let { MacAddressCompat.fromString(it) }?.addr,
|
||||
WifiSsidCompat.fromUtf8Text(SSID),
|
||||
BSSID?.let { MacAddress.fromString(it) },
|
||||
preSharedKey,
|
||||
hiddenSSID,
|
||||
// https://cs.android.com/android/platform/superproject/+/master:frameworks/base/wifi/java/android/net/wifi/SoftApConfToXmlMigrationUtil.java;l=87;drc=aa6527cf41671d1ed417b8ebdb6b3aa614f62344
|
||||
SparseIntArray(1).apply {
|
||||
if (Build.VERSION.SDK_INT < 23) put(BAND_LEGACY, 0) else put(when (val band = apBand.getInt(this)) {
|
||||
SparseIntArray(1).also {
|
||||
it.append(when (val band = apBand.getInt(this)) {
|
||||
0 -> BAND_2GHZ
|
||||
1 -> BAND_5GHZ
|
||||
-1 -> BAND_LEGACY
|
||||
@@ -302,77 +379,102 @@ data class SoftApConfigurationCompat(
|
||||
SoftApConfiguration.SECURITY_TYPE_WPA2_PSK
|
||||
}
|
||||
android.net.wifi.WifiConfiguration.KeyMgmt.SAE -> SoftApConfiguration.SECURITY_TYPE_WPA3_SAE
|
||||
android.net.wifi.WifiConfiguration.KeyMgmt.OWE -> SoftApConfiguration.SECURITY_TYPE_WPA3_OWE
|
||||
else -> android.net.wifi.WifiConfiguration.KeyMgmt.strings
|
||||
.getOrElse<String>(selected) { "?" }.let {
|
||||
throw IllegalArgumentException("Unrecognized key management $it ($selected)")
|
||||
}
|
||||
}
|
||||
},
|
||||
isAutoShutdownEnabled = if (Build.VERSION.SDK_INT >= 28) TetherTimeoutMonitor.enabled else false,
|
||||
isAutoShutdownEnabled = TetherTimeoutMonitor.enabled,
|
||||
underlying = this)
|
||||
|
||||
@RequiresApi(30)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun SoftApConfiguration.toCompat() = SoftApConfigurationCompat(
|
||||
ssid,
|
||||
bssid?.toCompat()?.addr,
|
||||
passphrase,
|
||||
isHiddenSsid,
|
||||
if (BuildCompat.isAtLeastS()) getChannels(this) as SparseIntArray else SparseIntArray(1).apply {
|
||||
put(getBand(this) as Int, getChannel(this) as Int)
|
||||
},
|
||||
securityType,
|
||||
getMaxNumberOfClients(this) as Int,
|
||||
isAutoShutdownEnabled(this) as Boolean,
|
||||
getShutdownTimeoutMillis(this) as Long,
|
||||
isClientControlByUserEnabled(this) as Boolean,
|
||||
getBlockedClientList(this) as List<MacAddress>,
|
||||
getAllowedClientList(this) as List<MacAddress>,
|
||||
getMacRandomizationSetting(this) as Int,
|
||||
isBridgedModeOpportunisticShutdownEnabled(this) as Boolean,
|
||||
isIeee80211axEnabled(this) as Boolean,
|
||||
isUserConfiguration(this) as Boolean,
|
||||
this)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
inline var bssid: MacAddressCompat?
|
||||
get() = bssidAddr?.let { MacAddressCompat(it) }
|
||||
set(value) {
|
||||
bssidAddr = value?.addr
|
||||
if (Build.VERSION.SDK_INT >= 33) wifiSsid?.toCompat() else @Suppress("DEPRECATION") {
|
||||
WifiSsidCompat.fromUtf8Text(ssid)
|
||||
},
|
||||
bssid,
|
||||
passphrase,
|
||||
isHiddenSsid,
|
||||
if (Build.VERSION.SDK_INT >= 31) getChannels(this) as SparseIntArray else SparseIntArray(1).also {
|
||||
it.append(getBand(this) as Int, getChannel(this) as Int)
|
||||
},
|
||||
securityType,
|
||||
getMaxNumberOfClients(this) as Int,
|
||||
isAutoShutdownEnabled(this) as Boolean,
|
||||
getShutdownTimeoutMillis(this) as Long,
|
||||
isClientControlByUserEnabled(this) as Boolean,
|
||||
getBlockedClientList(this) as List<MacAddress>,
|
||||
getAllowedClientList(this) as List<MacAddress>,
|
||||
underlying = this,
|
||||
).also {
|
||||
if (Build.VERSION.SDK_INT < 31) return@also
|
||||
it.macRandomizationSetting = getMacRandomizationSetting(this) as Int
|
||||
it.isBridgedModeOpportunisticShutdownEnabled = isBridgedModeOpportunisticShutdownEnabled(this) as Boolean
|
||||
it.isIeee80211axEnabled = isIeee80211axEnabled(this) as Boolean
|
||||
it.isUserConfiguration = isUserConfiguration(this) as Boolean
|
||||
if (Build.VERSION.SDK_INT < 33) return@also
|
||||
it.isIeee80211beEnabled = isIeee80211beEnabled(this) as Boolean
|
||||
it.bridgedModeOpportunisticShutdownTimeoutMillis =
|
||||
getBridgedModeOpportunisticShutdownTimeoutMillis(this) as Long
|
||||
it.vendorElements = getVendorElements(this) as List<ScanResult.InformationElement>
|
||||
it.persistentRandomizedMacAddress = getPersistentRandomizedMacAddress(this) as MacAddress?
|
||||
it.allowedAcsChannels = BAND_TYPES.map { bandType ->
|
||||
try {
|
||||
bandType to (getAllowedAcsChannels(this, bandType) as IntArray).toSet()
|
||||
} catch (e: InvocationTargetException) {
|
||||
if (e.targetException !is IllegalArgumentException) throw e
|
||||
null
|
||||
}
|
||||
}.filterNotNull().toMap()
|
||||
it.maxChannelBandwidth = getMaxChannelBandwidth(this) as Int
|
||||
}
|
||||
|
||||
/**
|
||||
* Only single band/channel can be supplied on API 23-30
|
||||
*/
|
||||
fun requireSingleBand(): Pair<Int, Int> {
|
||||
require(channels.size() == 1) { "Unsupported number of bands configured" }
|
||||
return channels.keyAt(0) to channels.valueAt(0)
|
||||
}
|
||||
fun getChannel(band: Int): Int {
|
||||
var result = -1
|
||||
for (b in channels.keyIterator()) if (band and b == band) {
|
||||
require(result == -1) { "Duplicate band found" }
|
||||
result = channels[b]
|
||||
/**
|
||||
* Only single band/channel can be supplied on API 23-30
|
||||
*/
|
||||
fun requireSingleBand(channels: SparseIntArray): Pair<Int, Int> {
|
||||
require(channels.size() == 1) { "Unsupported number of bands configured" }
|
||||
return channels.keyAt(0) to channels.valueAt(0)
|
||||
}
|
||||
return result
|
||||
|
||||
@RequiresApi(30)
|
||||
private fun setChannelsCompat(builder: Any, channels: SparseIntArray) = if (Build.VERSION.SDK_INT < 31) {
|
||||
val (band, channel) = requireSingleBand(channels)
|
||||
if (channel == 0) setBand(builder, band) else setChannel(builder, channel, band)
|
||||
} else setChannels(builder, channels)
|
||||
@get:RequiresApi(30)
|
||||
private val staticBuilder by lazy @TargetApi(30) { classBuilder.newInstance() }
|
||||
@RequiresApi(30)
|
||||
fun testPlatformValidity(channels: SparseIntArray) = setChannelsCompat(staticBuilder, channels)
|
||||
@RequiresApi(30)
|
||||
fun testPlatformValidity(bssid: MacAddress) = setBssid(staticBuilder, bssid)
|
||||
@RequiresApi(33)
|
||||
fun testPlatformValidity(vendorElements: List<ScanResult.InformationElement>) =
|
||||
setVendorElements(staticBuilder, vendorElements)
|
||||
@RequiresApi(33)
|
||||
fun testPlatformValidity(band: Int, channels: IntArray) = setAllowedAcsChannels(staticBuilder, band, channels)
|
||||
@RequiresApi(33)
|
||||
fun testPlatformValidity(bandwidth: Int) = setMaxChannelBandwidth(staticBuilder, bandwidth)
|
||||
@RequiresApi(30)
|
||||
fun testPlatformTimeoutValidity(timeout: Long) = setShutdownTimeoutMillis(staticBuilder, timeout)
|
||||
@RequiresApi(33)
|
||||
fun testPlatformBridgedTimeoutValidity(timeout: Long) =
|
||||
setBridgedModeOpportunisticShutdownTimeoutMillis(staticBuilder, timeout)
|
||||
}
|
||||
|
||||
fun setChannel(channel: Int, band: Int = BAND_LEGACY) {
|
||||
channels = SparseIntArray(1).apply { put(band, channel) }
|
||||
}
|
||||
fun optimizeChannels(channels: SparseIntArray = this.channels) {
|
||||
this.channels = SparseIntArray(channels.size()).apply {
|
||||
var setBand = 0
|
||||
for (band in channels.keyIterator()) if (channels[band] == 0) setBand = setBand or band
|
||||
if (setBand != 0) put(setBand, 0) // merge all bands into one
|
||||
for (band in channels.keyIterator()) if (band and setBand == 0) put(band, channels[band])
|
||||
channels = SparseIntArray(1).apply {
|
||||
append(when {
|
||||
channel <= 0 || band != BAND_LEGACY -> band
|
||||
channel > 14 -> BAND_5GHZ
|
||||
else -> BAND_2GHZ
|
||||
}, channel)
|
||||
}
|
||||
}
|
||||
|
||||
fun setMacRandomizationEnabled(enabled: Boolean) {
|
||||
macRandomizationSetting = if (enabled) RANDOMIZATION_PERSISTENT else RANDOMIZATION_NONE
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on:
|
||||
* https://android.googlesource.com/platform/packages/apps/Settings/+/android-5.0.0_r1/src/com/android/settings/wifi/WifiApDialog.java#88
|
||||
@@ -383,25 +485,22 @@ data class SoftApConfigurationCompat(
|
||||
@Deprecated("Class deprecated in framework, use toPlatform().toWifiConfiguration()")
|
||||
@Suppress("DEPRECATION")
|
||||
fun toWifiConfiguration(): android.net.wifi.WifiConfiguration {
|
||||
val (band, channel) = requireSingleBand()
|
||||
val (band, channel) = requireSingleBand(channels)
|
||||
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
|
||||
result.SSID = ssid?.toString()
|
||||
result.preSharedKey = passphrase
|
||||
result.hiddenSSID = isHiddenSsid
|
||||
if (Build.VERSION.SDK_INT >= 23) {
|
||||
apBand.setInt(result, when (band) {
|
||||
BAND_2GHZ -> 0
|
||||
BAND_5GHZ -> 1
|
||||
else -> {
|
||||
require(Build.VERSION.SDK_INT >= 28) { "A band must be specified on this platform" }
|
||||
require(isLegacyEitherBand(band)) { "Convert fail, unsupported band setting :$band" }
|
||||
-1
|
||||
}
|
||||
})
|
||||
apChannel.setInt(result, channel)
|
||||
} else require(isLegacyEitherBand(band)) { "Specifying band is unsupported on this platform" }
|
||||
apBand.setInt(result, when (band) {
|
||||
BAND_2GHZ -> 0
|
||||
BAND_5GHZ -> 1
|
||||
else -> {
|
||||
require(isLegacyEitherBand(band)) { "Convert fail, unsupported band setting :$band" }
|
||||
-1
|
||||
}
|
||||
})
|
||||
apChannel.setInt(result, channel)
|
||||
if (original?.securityType != securityType) {
|
||||
result.allowedKeyManagement.clear()
|
||||
result.allowedKeyManagement.set(when (securityType) {
|
||||
@@ -411,6 +510,8 @@ data class SoftApConfigurationCompat(
|
||||
// CHANGED: not actually converted in framework-wifi
|
||||
SoftApConfiguration.SECURITY_TYPE_WPA3_SAE,
|
||||
SoftApConfiguration.SECURITY_TYPE_WPA3_SAE_TRANSITION -> android.net.wifi.WifiConfiguration.KeyMgmt.SAE
|
||||
SoftApConfiguration.SECURITY_TYPE_WPA3_OWE,
|
||||
SoftApConfiguration.SECURITY_TYPE_WPA3_OWE_TRANSITION -> android.net.wifi.WifiConfiguration.KeyMgmt.OWE
|
||||
else -> throw IllegalArgumentException("Convert fail, unsupported security type :$securityType")
|
||||
})
|
||||
result.allowedAuthAlgorithms.clear()
|
||||
@@ -425,29 +526,59 @@ data class SoftApConfigurationCompat(
|
||||
fun toPlatform(): SoftApConfiguration {
|
||||
val sac = underlying as? SoftApConfiguration
|
||||
val builder = if (sac == null) classBuilder.newInstance() else newBuilder.newInstance(sac)
|
||||
setSsid(builder, ssid)
|
||||
setPassphrase(builder, if (securityType == SoftApConfiguration.SECURITY_TYPE_OPEN) null else passphrase,
|
||||
securityType)
|
||||
if (BuildCompat.isAtLeastS()) setChannels(builder, channels) else {
|
||||
val (band, channel) = requireSingleBand()
|
||||
if (channel == 0) setBand(builder, band) else setChannel(builder, channel, band)
|
||||
}
|
||||
setBssid(builder, bssid?.toPlatform())
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
setWifiSsid(builder, ssid?.toPlatform())
|
||||
} else setSsid(builder, ssid?.toString())
|
||||
setPassphrase(builder, when (securityType) {
|
||||
SoftApConfiguration.SECURITY_TYPE_OPEN,
|
||||
SoftApConfiguration.SECURITY_TYPE_WPA3_OWE_TRANSITION,
|
||||
SoftApConfiguration.SECURITY_TYPE_WPA3_OWE -> null
|
||||
else -> passphrase
|
||||
}, securityType)
|
||||
setChannelsCompat(builder, channels)
|
||||
setBssid(builder,
|
||||
if (Build.VERSION.SDK_INT < 31 || macRandomizationSetting == RANDOMIZATION_NONE) bssid else null)
|
||||
setMaxNumberOfClients(builder, maxNumberOfClients)
|
||||
setShutdownTimeoutMillis(builder, shutdownTimeoutMillis)
|
||||
try {
|
||||
setShutdownTimeoutMillis(builder, shutdownTimeoutMillis)
|
||||
} catch (e: InvocationTargetException) {
|
||||
if (e.targetException is IllegalArgumentException) try {
|
||||
setShutdownTimeoutMillis(builder, -1 - shutdownTimeoutMillis)
|
||||
} catch (e2: InvocationTargetException) {
|
||||
e2.addSuppressed(e)
|
||||
throw e2
|
||||
} else throw e
|
||||
}
|
||||
setAutoShutdownEnabled(builder, isAutoShutdownEnabled)
|
||||
setClientControlByUserEnabled(builder, isClientControlByUserEnabled)
|
||||
setHiddenSsid(builder, isHiddenSsid)
|
||||
setAllowedClientList(builder, allowedClientList)
|
||||
setBlockedClientList(builder, blockedClientList)
|
||||
if (BuildCompat.isAtLeastS()) {
|
||||
if (Build.VERSION.SDK_INT >= 31) {
|
||||
setMacRandomizationSetting(builder, macRandomizationSetting)
|
||||
setBridgedModeOpportunisticShutdownEnabled(builder, isBridgedModeOpportunisticShutdownEnabled)
|
||||
setIeee80211axEnabled(builder, isIeee80211axEnabled)
|
||||
if (sac?.let { isUserConfiguration(it) as Boolean } != false != isUserConfiguration) try {
|
||||
setUserConfiguration(builder, isUserConfiguration)
|
||||
} catch (e: ReflectiveOperationException) {
|
||||
Timber.w(e) // as far as we are concerned, this field is not used anywhere so ignore for now
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
setIeee80211beEnabled(builder, isIeee80211beEnabled)
|
||||
setBridgedModeOpportunisticShutdownTimeoutMillis(builder, bridgedModeOpportunisticShutdownTimeoutMillis)
|
||||
setVendorElements(builder, vendorElements)
|
||||
val needsUpdate = persistentRandomizedMacAddress != null && sac?.let {
|
||||
getPersistentRandomizedMacAddress(it) as MacAddress
|
||||
} != persistentRandomizedMacAddress
|
||||
if (needsUpdate) try {
|
||||
setRandomizedMacAddress(builder, persistentRandomizedMacAddress)
|
||||
} catch (e: ReflectiveOperationException) {
|
||||
Timber.w(e)
|
||||
}
|
||||
for (bandType in BAND_TYPES) {
|
||||
val value = allowedAcsChannels[bandType] ?: emptySet()
|
||||
try {
|
||||
setAllowedAcsChannels(builder, bandType, value.toIntArray())
|
||||
} catch (e: InvocationTargetException) {
|
||||
if (value.isNotEmpty()) throw e
|
||||
}
|
||||
}
|
||||
setMaxChannelBandwidth(builder, maxChannelBandwidth)
|
||||
}
|
||||
}
|
||||
return build(builder) as SoftApConfiguration
|
||||
@@ -458,21 +589,21 @@ data class SoftApConfigurationCompat(
|
||||
* 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;")
|
||||
SoftApConfiguration.SECURITY_TYPE_OPEN, SoftApConfiguration.SECURITY_TYPE_WPA3_OWE_TRANSITION,
|
||||
SoftApConfiguration.SECURITY_TYPE_WPA3_OWE -> { }
|
||||
SoftApConfiguration.SECURITY_TYPE_WPA2_PSK, SoftApConfiguration.SECURITY_TYPE_WPA3_SAE_TRANSITION -> {
|
||||
append("T:WPA;")
|
||||
}
|
||||
SoftApConfiguration.SECURITY_TYPE_WPA3_SAE -> append("T:SAE;")
|
||||
else -> throw IllegalArgumentException("Unsupported authentication type")
|
||||
}
|
||||
append("S:")
|
||||
append(ssid!!.sanitize())
|
||||
append(ssid!!.toMeCard())
|
||||
append(';')
|
||||
passphrase?.let { passphrase ->
|
||||
append("P:")
|
||||
append(passphrase.sanitize())
|
||||
append(WifiSsidCompat.toMeCard(passphrase))
|
||||
append(';')
|
||||
}
|
||||
if (isHiddenSsid) append("H:true;")
|
||||
|
||||
@@ -12,7 +12,7 @@ import timber.log.Timber
|
||||
@RequiresApi(30)
|
||||
value class SoftApInfo(val inner: Parcelable) {
|
||||
companion object {
|
||||
private val clazz by lazy { Class.forName("android.net.wifi.SoftApInfo") }
|
||||
val clazz by lazy { Class.forName("android.net.wifi.SoftApInfo") }
|
||||
private val getFrequency by lazy { clazz.getDeclaredMethod("getFrequency") }
|
||||
private val getBandwidth by lazy { clazz.getDeclaredMethod("getBandwidth") }
|
||||
@get:RequiresApi(31)
|
||||
@@ -30,7 +30,7 @@ value class SoftApInfo(val inner: Parcelable) {
|
||||
val frequency get() = getFrequency(inner) as Int
|
||||
val bandwidth get() = getBandwidth(inner) as Int
|
||||
@get:RequiresApi(31)
|
||||
val bssid get() = getBssid(inner) as MacAddress
|
||||
val bssid get() = getBssid(inner) as MacAddress?
|
||||
@get:RequiresApi(31)
|
||||
val wifiStandard get() = getWifiStandard(inner) as Int
|
||||
@get:RequiresApi(31)
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package be.mygod.vpnhotspot.net.wifi
|
||||
|
||||
import android.net.wifi.ScanResult
|
||||
import androidx.annotation.RequiresApi
|
||||
import timber.log.Timber
|
||||
|
||||
@RequiresApi(33)
|
||||
object VendorElements {
|
||||
fun serialize(input: List<ScanResult.InformationElement>) = input.joinToString("\n") { element ->
|
||||
element.bytes.let { buffer ->
|
||||
StringBuilder().apply {
|
||||
while (buffer.hasRemaining()) append("%02x".format(buffer.get()))
|
||||
}.toString()
|
||||
}.also {
|
||||
if (element.id != 221 || element.idExt != 0 || it.isEmpty()) Timber.w(Exception(
|
||||
"Unexpected InformationElement ${element.id}, ${element.idExt}, $it"))
|
||||
}
|
||||
}
|
||||
|
||||
fun deserialize(input: CharSequence?) = (input ?: "").split("\n").map { line ->
|
||||
if (line.isBlank()) return@map null
|
||||
require(line.length % 2 == 0) { "Input should be hex: $line" }
|
||||
(0 until line.length / 2).map {
|
||||
Integer.parseInt(line.substring(it * 2, it * 2 + 2), 16).toByte()
|
||||
}.toByteArray()
|
||||
}.filterNotNull().map { ScanResult.InformationElement(221, 0, it) }
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
package be.mygod.vpnhotspot.net.wifi
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.annotation.TargetApi
|
||||
import android.content.ClipData
|
||||
import android.content.ClipDescription
|
||||
import android.content.DialogInterface
|
||||
import android.net.MacAddress
|
||||
import android.net.wifi.SoftApConfiguration
|
||||
import android.os.Build
|
||||
import android.os.Parcelable
|
||||
import android.text.Editable
|
||||
import android.text.InputFilter
|
||||
import android.text.TextWatcher
|
||||
import android.util.Base64
|
||||
import android.util.SparseIntArray
|
||||
@@ -16,10 +18,11 @@ import android.view.View
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Spinner
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.os.BuildCompat
|
||||
import androidx.core.os.persistableBundleOf
|
||||
import androidx.core.view.isGone
|
||||
import be.mygod.librootkotlinx.toByteArray
|
||||
import be.mygod.librootkotlinx.toParcelable
|
||||
@@ -28,14 +31,16 @@ import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.R
|
||||
import be.mygod.vpnhotspot.RepeaterService
|
||||
import be.mygod.vpnhotspot.databinding.DialogWifiApBinding
|
||||
import be.mygod.vpnhotspot.net.MacAddressCompat
|
||||
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
|
||||
import be.mygod.vpnhotspot.util.QRCodeDialog
|
||||
import be.mygod.vpnhotspot.util.RangeInput
|
||||
import be.mygod.vpnhotspot.util.readableMessage
|
||||
import be.mygod.vpnhotspot.util.showAllowingStateLoss
|
||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import timber.log.Timber
|
||||
import java.text.DecimalFormat
|
||||
import java.text.DecimalFormatSymbols
|
||||
|
||||
/**
|
||||
* Based on: https://android.googlesource.com/platform/packages/apps/Settings/+/39b4674/src/com/android/settings/wifi/WifiApDialog.java
|
||||
@@ -48,26 +53,34 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
|
||||
companion object {
|
||||
private const val BASE64_FLAGS = Base64.NO_PADDING or Base64.NO_WRAP
|
||||
private val nonMacChars = "[^0-9a-fA-F:]+".toRegex()
|
||||
private val baseOptions by lazy { listOf(ChannelOption.Disabled, ChannelOption.Auto) }
|
||||
private val channels2G by lazy {
|
||||
baseOptions + (1..14).map { ChannelOption(it, SoftApConfigurationCompat.BAND_2GHZ) }
|
||||
}
|
||||
private val channels2G = (1..14).map { ChannelOption(SoftApConfigurationCompat.BAND_2GHZ, it) }
|
||||
private val channels5G by lazy {
|
||||
baseOptions + (1..196).map { ChannelOption(it, SoftApConfigurationCompat.BAND_5GHZ) }
|
||||
}
|
||||
@get:RequiresApi(30)
|
||||
private val channels6G by lazy {
|
||||
baseOptions + (1..233).map { ChannelOption(it, SoftApConfigurationCompat.BAND_6GHZ) }
|
||||
}
|
||||
@get:RequiresApi(31)
|
||||
private val channels60G by lazy {
|
||||
baseOptions + (1..6).map { ChannelOption(it, SoftApConfigurationCompat.BAND_60GHZ) }
|
||||
channels2G + (1..196).map { ChannelOption(SoftApConfigurationCompat.BAND_5GHZ, it) }
|
||||
}
|
||||
|
||||
private fun genAutoOptions(band: Int) = (1..band).filter { it and band == it }.map { ChannelOption(it) }
|
||||
/**
|
||||
* Source: https://android.googlesource.com/platform/frameworks/opt/net/wifi/+/c2fc6a1/service/java/com/android/server/wifi/p2p/SupplicantP2pIfaceHal.java#1396
|
||||
*/
|
||||
private val p2pChannels by lazy {
|
||||
baseOptions + (15..165).map { ChannelOption(it, SoftApConfigurationCompat.BAND_5GHZ) }
|
||||
private val p2pUnsafeOptions by lazy {
|
||||
listOf(ChannelOption(SoftApConfigurationCompat.BAND_LEGACY)) +
|
||||
channels2G + (15..165).map { ChannelOption(SoftApConfigurationCompat.BAND_5GHZ, it) }
|
||||
}
|
||||
private val p2pSafeOptions by lazy { genAutoOptions(SoftApConfigurationCompat.BAND_LEGACY) + channels5G }
|
||||
private val softApOptions by lazy {
|
||||
if (Build.VERSION.SDK_INT >= 30) {
|
||||
genAutoOptions(SoftApConfigurationCompat.BAND_ANY_31) +
|
||||
channels5G +
|
||||
(1..253).map { ChannelOption(SoftApConfigurationCompat.BAND_6GHZ, it) } +
|
||||
(1..6).map { ChannelOption(SoftApConfigurationCompat.BAND_60GHZ, it) }
|
||||
} else p2pSafeOptions
|
||||
}
|
||||
|
||||
@get:RequiresApi(30)
|
||||
private val bandWidthOptions by lazy {
|
||||
SoftApInfo.channelWidthLookup.lookup.let { lookup ->
|
||||
Array(lookup.size()) { BandWidth(lookup.keyAt(it), lookup.valueAt(it).substring(14)) }.apply { sort() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,62 +93,99 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
|
||||
*/
|
||||
val p2pMode: Boolean = false) : Parcelable
|
||||
|
||||
private open class ChannelOption(val channel: Int = 0, private val band: Int = 0) {
|
||||
private open class ChannelOption(val band: Int = 0, val channel: Int = 0) {
|
||||
object Disabled : ChannelOption(-1) {
|
||||
override fun toString() = app.getString(R.string.wifi_ap_choose_disabled)
|
||||
}
|
||||
object Auto : ChannelOption() {
|
||||
override fun toString() = app.getString(R.string.wifi_ap_choose_auto)
|
||||
}
|
||||
override fun toString() = "${SoftApConfigurationCompat.channelToFrequency(band, channel)} MHz ($channel)"
|
||||
override fun toString() = if (channel == 0) {
|
||||
val format = DecimalFormat("#.#", DecimalFormatSymbols.getInstance(app.resources.configuration.locales[0]))
|
||||
app.getString(R.string.wifi_ap_choose_G, arrayOf(
|
||||
SoftApConfigurationCompat.BAND_2GHZ to 2.4,
|
||||
SoftApConfigurationCompat.BAND_5GHZ to 5,
|
||||
SoftApConfigurationCompat.BAND_6GHZ to 6,
|
||||
SoftApConfigurationCompat.BAND_60GHZ to 60,
|
||||
).filter { (mask, _) -> band and mask == mask }.joinToString("/") { (_, name) -> format.format(name) })
|
||||
} else "${SoftApConfigurationCompat.channelToFrequency(band, channel)} MHz ($channel)"
|
||||
}
|
||||
|
||||
private class BandWidth(val width: Int, val name: String = "") : Comparable<BandWidth> {
|
||||
override fun compareTo(other: BandWidth) = width - other.width
|
||||
override fun toString() = name
|
||||
}
|
||||
|
||||
private lateinit var dialogView: DialogWifiApBinding
|
||||
private lateinit var base: SoftApConfigurationCompat
|
||||
private var pasted = false
|
||||
private var started = false
|
||||
private val currentChannels5G get() = if (arg.p2pMode && !RepeaterService.safeMode) p2pChannels else channels5G
|
||||
private val currentChannels get() = when {
|
||||
!arg.p2pMode -> softApOptions
|
||||
RepeaterService.safeMode -> p2pSafeOptions
|
||||
else -> p2pUnsafeOptions
|
||||
}
|
||||
private val acsList by lazy {
|
||||
listOf(
|
||||
Triple(SoftApConfigurationCompat.BAND_2GHZ, dialogView.acs2g, dialogView.acs2gWrapper),
|
||||
Triple(SoftApConfigurationCompat.BAND_5GHZ, dialogView.acs5g, dialogView.acs5gWrapper),
|
||||
Triple(SoftApConfigurationCompat.BAND_6GHZ, dialogView.acs6g, dialogView.acs6gWrapper),
|
||||
)
|
||||
}
|
||||
override val ret get() = Arg(generateConfig())
|
||||
private val hexToggleable get() = if (arg.p2pMode) !RepeaterService.safeMode else Build.VERSION.SDK_INT >= 33
|
||||
private var hexSsid = false
|
||||
set(value) {
|
||||
field = value
|
||||
dialogView.ssidWrapper.setEndIconActivated(value)
|
||||
}
|
||||
private val ssid get() =
|
||||
if (hexSsid) WifiSsidCompat.fromHex(dialogView.ssid.text) else WifiSsidCompat.fromUtf8Text(dialogView.ssid.text)
|
||||
|
||||
private fun generateChannels() = SparseIntArray(2).apply {
|
||||
if (!arg.p2pMode && Build.VERSION.SDK_INT >= 31) {
|
||||
(dialogView.bandSecondary.selectedItem as ChannelOption?)?.apply { if (band >= 0) put(band, channel) }
|
||||
}
|
||||
(dialogView.bandPrimary.selectedItem as ChannelOption).apply { put(band, channel) }
|
||||
}
|
||||
private fun generateConfig(full: Boolean = true) = base.copy(
|
||||
ssid = dialogView.ssid.text.toString(),
|
||||
ssid = ssid,
|
||||
passphrase = if (dialogView.password.length() != 0) dialogView.password.text.toString() else null).apply {
|
||||
if (!arg.p2pMode) {
|
||||
securityType = dialogView.security.selectedItemPosition
|
||||
isHiddenSsid = dialogView.hiddenSsid.isChecked
|
||||
}
|
||||
if (full) @TargetApi(28) {
|
||||
if (full) {
|
||||
isAutoShutdownEnabled = dialogView.autoShutdown.isChecked
|
||||
shutdownTimeoutMillis = dialogView.timeout.text.let { text ->
|
||||
if (text.isNullOrEmpty()) 0 else text.toString().toLong()
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= 23 || arg.p2pMode) {
|
||||
val channels = SparseIntArray(4)
|
||||
for ((band, spinner) in arrayOf(SoftApConfigurationCompat.BAND_2GHZ to dialogView.band2G,
|
||||
SoftApConfigurationCompat.BAND_5GHZ to dialogView.band5G,
|
||||
SoftApConfigurationCompat.BAND_6GHZ to dialogView.band6G,
|
||||
SoftApConfigurationCompat.BAND_60GHZ to dialogView.band60G)) {
|
||||
val channel = (spinner.selectedItem as ChannelOption?)?.channel
|
||||
if (channel != null && channel >= 0) channels.put(band, channel)
|
||||
}
|
||||
if (!arg.p2pMode && BuildCompat.isAtLeastS() && dialogView.bridgedMode.isChecked) {
|
||||
this.channels = channels
|
||||
} else optimizeChannels(channels)
|
||||
}
|
||||
bssid = if (dialogView.bssid.length() != 0) {
|
||||
MacAddressCompat.fromString(dialogView.bssid.text.toString())
|
||||
} else null
|
||||
channels = generateChannels()
|
||||
maxNumberOfClients = dialogView.maxClient.text.let { text ->
|
||||
if (text.isNullOrEmpty()) 0 else text.toString().toInt()
|
||||
}
|
||||
isClientControlByUserEnabled = dialogView.clientUserControl.isChecked
|
||||
allowedClientList = (dialogView.allowedList.text ?: "").split(nonMacChars)
|
||||
.filter { it.isNotEmpty() }.map { MacAddressCompat.fromString(it).toPlatform() }
|
||||
.filter { it.isNotEmpty() }.map(MacAddress::fromString)
|
||||
blockedClientList = (dialogView.blockedList.text ?: "").split(nonMacChars)
|
||||
.filter { it.isNotEmpty() }.map { MacAddressCompat.fromString(it).toPlatform() }
|
||||
setMacRandomizationEnabled(dialogView.macRandomization.isChecked)
|
||||
.filter { it.isNotEmpty() }.map(MacAddress::fromString)
|
||||
macRandomizationSetting = dialogView.macRandomization.selectedItemPosition
|
||||
bssid = if ((arg.p2pMode || Build.VERSION.SDK_INT < 31 && macRandomizationSetting ==
|
||||
SoftApConfigurationCompat.RANDOMIZATION_NONE) && dialogView.bssid.length() != 0) {
|
||||
MacAddress.fromString(dialogView.bssid.text.toString())
|
||||
} else null
|
||||
isBridgedModeOpportunisticShutdownEnabled = dialogView.bridgedModeOpportunisticShutdown.isChecked
|
||||
isIeee80211axEnabled = dialogView.ieee80211ax.isChecked
|
||||
isIeee80211beEnabled = dialogView.ieee80211be.isChecked
|
||||
isUserConfiguration = dialogView.userConfig.isChecked
|
||||
bridgedModeOpportunisticShutdownTimeoutMillis = dialogView.bridgedTimeout.text.let { text ->
|
||||
if (text.isNullOrEmpty()) -1L else text.toString().toLong()
|
||||
}
|
||||
vendorElements = VendorElements.deserialize(dialogView.vendorElements.text)
|
||||
persistentRandomizedMacAddress = if (dialogView.persistentRandomizedMac.length() != 0) {
|
||||
MacAddress.fromString(dialogView.persistentRandomizedMac.text.toString())
|
||||
} else null
|
||||
allowedAcsChannels = acsList.associate { (band, text, _) -> band to RangeInput.fromString(text.text) }
|
||||
if (!arg.p2pMode && Build.VERSION.SDK_INT >= 33) {
|
||||
maxChannelBandwidth = (dialogView.maxChannelBandwidth.selectedItem as BandWidth).width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,6 +198,31 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
|
||||
setNegativeButton(R.string.donations__button_close, null)
|
||||
dialogView.toolbar.inflateMenu(R.menu.toolbar_configuration)
|
||||
dialogView.toolbar.setOnMenuItemClickListener(this@WifiApDialogFragment)
|
||||
dialogView.ssidWrapper.setLengthCounter {
|
||||
try {
|
||||
ssid?.bytes?.size ?: 0
|
||||
} catch (_: IllegalArgumentException) {
|
||||
0
|
||||
}
|
||||
}
|
||||
if (hexToggleable) dialogView.ssidWrapper.apply {
|
||||
endIconMode = TextInputLayout.END_ICON_CUSTOM
|
||||
setEndIconOnClickListener {
|
||||
val ssid = try {
|
||||
ssid
|
||||
} catch (_: IllegalArgumentException) {
|
||||
return@setEndIconOnClickListener
|
||||
}
|
||||
val newText = if (hexSsid) ssid?.run {
|
||||
decode().also { if (it == null) return@setEndIconOnClickListener }
|
||||
} else ssid?.hex
|
||||
hexSsid = !hexSsid
|
||||
dialogView.ssid.setText(newText)
|
||||
}
|
||||
findViewById<View>(com.google.android.material.R.id.text_input_end_icon).apply {
|
||||
tooltipText = contentDescription
|
||||
}
|
||||
}
|
||||
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,
|
||||
@@ -157,99 +232,115 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
|
||||
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 == SoftApConfiguration.SECURITY_TYPE_OPEN
|
||||
when (position) {
|
||||
SoftApConfiguration.SECURITY_TYPE_OPEN,
|
||||
SoftApConfiguration.SECURITY_TYPE_WPA3_OWE_TRANSITION,
|
||||
SoftApConfiguration.SECURITY_TYPE_WPA3_OWE -> dialogView.passwordWrapper.isGone = true
|
||||
else -> {
|
||||
dialogView.passwordWrapper.isGone = false
|
||||
if (position == SoftApConfiguration.SECURITY_TYPE_WPA3_SAE) {
|
||||
dialogView.passwordWrapper.isCounterEnabled = false
|
||||
dialogView.passwordWrapper.counterMaxLength = 0
|
||||
dialogView.password.filters = emptyArray()
|
||||
} else {
|
||||
dialogView.passwordWrapper.isCounterEnabled = true
|
||||
dialogView.passwordWrapper.counterMaxLength = 63
|
||||
dialogView.password.filters = arrayOf(InputFilter.LengthFilter(63))
|
||||
}
|
||||
}
|
||||
}
|
||||
validate()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!arg.readOnly) dialogView.password.addTextChangedListener(this@WifiApDialogFragment)
|
||||
if (!arg.p2pMode && Build.VERSION.SDK_INT < 28) dialogView.autoShutdown.isGone = true
|
||||
if (arg.p2pMode || Build.VERSION.SDK_INT >= 30) {
|
||||
dialogView.timeoutWrapper.helperText = getString(R.string.wifi_hotspot_timeout_default,
|
||||
TetherTimeoutMonitor.defaultTimeout)
|
||||
dialogView.timeout.addTextChangedListener(this@WifiApDialogFragment)
|
||||
if (!arg.readOnly) dialogView.timeout.addTextChangedListener(this@WifiApDialogFragment)
|
||||
} else dialogView.timeoutWrapper.isGone = true
|
||||
fun Spinner.configure(options: List<ChannelOption>) {
|
||||
adapter = ArrayAdapter(activity, android.R.layout.simple_spinner_item, 0, options).apply {
|
||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
}
|
||||
onItemSelectedListener = this@WifiApDialogFragment
|
||||
if (!arg.readOnly) onItemSelectedListener = this@WifiApDialogFragment
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= 23 || arg.p2pMode) {
|
||||
dialogView.band2G.configure(channels2G)
|
||||
dialogView.band5G.configure(currentChannels5G)
|
||||
} else {
|
||||
dialogView.bandWrapper2G.isGone = true
|
||||
dialogView.bandWrapper5G.isGone = true
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= 30 && !arg.p2pMode) dialogView.band6G.configure(channels6G)
|
||||
else dialogView.bandWrapper6G.isGone = true
|
||||
if (BuildCompat.isAtLeastS() && !arg.p2pMode) dialogView.band60G.configure(channels60G)
|
||||
else dialogView.bandWrapper60G.isGone = true
|
||||
dialogView.bssid.addTextChangedListener(this@WifiApDialogFragment)
|
||||
if (arg.p2pMode) dialogView.hiddenSsid.isGone = true
|
||||
if (arg.p2pMode || Build.VERSION.SDK_INT < 30) {
|
||||
dialogView.maxClientWrapper.isGone = true
|
||||
dialogView.clientUserControl.isGone = true
|
||||
dialogView.blockedListWrapper.isGone = true
|
||||
dialogView.allowedListWrapper.isGone = true
|
||||
} else {
|
||||
dialogView.bandPrimary.configure(currentChannels)
|
||||
if (Build.VERSION.SDK_INT >= 31 && !arg.p2pMode) {
|
||||
dialogView.bandSecondary.configure(listOf(ChannelOption.Disabled) + currentChannels)
|
||||
} else dialogView.bandSecondary.isGone = true
|
||||
if (arg.p2pMode || Build.VERSION.SDK_INT < 30) dialogView.accessControlGroup.isGone = true
|
||||
else if (!arg.readOnly) {
|
||||
dialogView.maxClient.addTextChangedListener(this@WifiApDialogFragment)
|
||||
dialogView.blockedList.addTextChangedListener(this@WifiApDialogFragment)
|
||||
dialogView.allowedList.addTextChangedListener(this@WifiApDialogFragment)
|
||||
}
|
||||
if (!arg.readOnly) dialogView.bssid.addTextChangedListener(this@WifiApDialogFragment)
|
||||
if (arg.p2pMode) dialogView.hiddenSsid.isGone = true
|
||||
if (arg.p2pMode && Build.VERSION.SDK_INT >= 29) dialogView.macRandomization.isEnabled = false
|
||||
else if (arg.p2pMode || !BuildCompat.isAtLeastS()) dialogView.macRandomization.isGone = true
|
||||
if (arg.p2pMode || !BuildCompat.isAtLeastS()) {
|
||||
dialogView.bridgedMode.isGone = true
|
||||
dialogView.bridgedModeOpportunisticShutdown.isGone = true
|
||||
else if (arg.p2pMode || Build.VERSION.SDK_INT < 31) dialogView.macRandomizationWrapper.isGone = true
|
||||
else dialogView.macRandomization.onItemSelectedListener = this@WifiApDialogFragment
|
||||
if (arg.p2pMode || Build.VERSION.SDK_INT < 31) {
|
||||
dialogView.ieee80211ax.isGone = true
|
||||
dialogView.bridgedModeOpportunisticShutdown.isGone = true
|
||||
dialogView.userConfig.isGone = true
|
||||
dialogView.bridgedTimeoutWrapper.isGone = true
|
||||
} else {
|
||||
dialogView.bridgedTimeoutWrapper.helperText = getString(R.string.wifi_hotspot_timeout_default,
|
||||
TetherTimeoutMonitor.defaultTimeoutBridged)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT < 33) dialogView.vendorElementsWrapper.isGone = true
|
||||
else if (!arg.readOnly) dialogView.vendorElements.addTextChangedListener(this@WifiApDialogFragment)
|
||||
if (arg.p2pMode || Build.VERSION.SDK_INT < 33) {
|
||||
dialogView.ieee80211be.isGone = true
|
||||
dialogView.bridgedTimeout.isEnabled = false
|
||||
dialogView.persistentRandomizedMacWrapper.isGone = true
|
||||
for ((_, _, wrapper) in acsList) wrapper.isGone = true
|
||||
dialogView.maxChannelBandwidthWrapper.isGone = true
|
||||
} else {
|
||||
dialogView.maxChannelBandwidth.adapter = ArrayAdapter(activity, android.R.layout.simple_spinner_item, 0,
|
||||
bandWidthOptions).apply {
|
||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
}
|
||||
if (!arg.readOnly) {
|
||||
dialogView.bridgedTimeout.addTextChangedListener(this@WifiApDialogFragment)
|
||||
dialogView.persistentRandomizedMac.addTextChangedListener(this@WifiApDialogFragment)
|
||||
for ((_, text, _) in acsList) text.addTextChangedListener(this@WifiApDialogFragment)
|
||||
dialogView.acs5g.addTextChangedListener(this@WifiApDialogFragment)
|
||||
dialogView.acs6g.addTextChangedListener(this@WifiApDialogFragment)
|
||||
dialogView.maxChannelBandwidth.onItemSelectedListener = this@WifiApDialogFragment
|
||||
}
|
||||
}
|
||||
base = arg.configuration
|
||||
populateFromConfiguration()
|
||||
}
|
||||
|
||||
private fun locate(band: Int, channels: List<ChannelOption>): Int {
|
||||
val channel = base.getChannel(band)
|
||||
val selection = channels.indexOfFirst { it.channel == channel }
|
||||
private fun locate(i: Int): Int {
|
||||
val band = base.channels.keyAt(i)
|
||||
val channel = base.channels.valueAt(i)
|
||||
val selection = currentChannels.indexOfFirst { it.band == band && it.channel == channel }
|
||||
return if (selection == -1) {
|
||||
Timber.w(Exception("Unable to locate $band, $channel, ${arg.p2pMode && !RepeaterService.safeMode}"))
|
||||
val msg = "Unable to locate $band, $channel, ${arg.p2pMode && !RepeaterService.safeMode}"
|
||||
if (pasted || arg.p2pMode) Timber.w(msg) else Timber.w(Exception(msg))
|
||||
0
|
||||
} else selection
|
||||
}
|
||||
private var userBridgedMode = false
|
||||
private fun setBridgedMode() {
|
||||
var auto = 0
|
||||
var set = 0
|
||||
for (s in arrayOf(dialogView.band2G, dialogView.band5G, dialogView.band6G)) when (s.selectedItem) {
|
||||
is ChannelOption.Auto -> auto = 1
|
||||
!is ChannelOption.Disabled -> ++set
|
||||
}
|
||||
if (auto + set > 1) {
|
||||
if (dialogView.bridgedMode.isEnabled) {
|
||||
userBridgedMode = dialogView.bridgedMode.isChecked
|
||||
dialogView.bridgedMode.isEnabled = false
|
||||
dialogView.bridgedMode.isChecked = true
|
||||
}
|
||||
} else if (!dialogView.bridgedMode.isEnabled) {
|
||||
dialogView.bridgedMode.isEnabled = true
|
||||
dialogView.bridgedMode.isChecked = userBridgedMode
|
||||
}
|
||||
}
|
||||
private fun populateFromConfiguration() {
|
||||
dialogView.ssid.setText(base.ssid)
|
||||
dialogView.ssid.setText(base.ssid.let { ssid ->
|
||||
when {
|
||||
ssid == null -> null
|
||||
hexSsid -> ssid.hex
|
||||
hexToggleable -> ssid.decode() ?: ssid.hex.also { hexSsid = true }
|
||||
else -> ssid.toString()
|
||||
}
|
||||
})
|
||||
if (!arg.p2pMode) dialogView.security.setSelection(base.securityType)
|
||||
dialogView.password.setText(base.passphrase)
|
||||
dialogView.autoShutdown.isChecked = base.isAutoShutdownEnabled
|
||||
dialogView.timeout.setText(base.shutdownTimeoutMillis.let { if (it == 0L) "" else it.toString() })
|
||||
if (Build.VERSION.SDK_INT >= 23 || arg.p2pMode) {
|
||||
dialogView.band2G.setSelection(locate(SoftApConfigurationCompat.BAND_2GHZ, channels2G))
|
||||
dialogView.band5G.setSelection(locate(SoftApConfigurationCompat.BAND_5GHZ, currentChannels5G))
|
||||
dialogView.band6G.setSelection(locate(SoftApConfigurationCompat.BAND_6GHZ, channels6G))
|
||||
dialogView.band60G.setSelection(locate(SoftApConfigurationCompat.BAND_60GHZ, channels60G))
|
||||
userBridgedMode = base.channels.size() > 1
|
||||
dialogView.bridgedMode.isChecked = userBridgedMode
|
||||
setBridgedMode()
|
||||
dialogView.timeout.setText(base.shutdownTimeoutMillis.let { if (it <= 0) "" else it.toString() })
|
||||
dialogView.bandPrimary.setSelection(locate(0))
|
||||
if (Build.VERSION.SDK_INT >= 31 && !arg.p2pMode) {
|
||||
dialogView.bandSecondary.setSelection(if (base.channels.size() > 1) locate(1) + 1 else 0)
|
||||
}
|
||||
dialogView.bssid.setText(base.bssid?.toString())
|
||||
dialogView.hiddenSsid.isChecked = base.isHiddenSsid
|
||||
@@ -257,11 +348,22 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
|
||||
dialogView.clientUserControl.isChecked = base.isClientControlByUserEnabled
|
||||
dialogView.blockedList.setText(base.blockedClientList.joinToString("\n"))
|
||||
dialogView.allowedList.setText(base.allowedClientList.joinToString("\n"))
|
||||
dialogView.macRandomization.isChecked =
|
||||
base.macRandomizationSetting == SoftApConfigurationCompat.RANDOMIZATION_PERSISTENT
|
||||
dialogView.macRandomization.setSelection(base.macRandomizationSetting)
|
||||
dialogView.bridgedModeOpportunisticShutdown.isChecked = base.isBridgedModeOpportunisticShutdownEnabled
|
||||
dialogView.ieee80211ax.isChecked = base.isIeee80211axEnabled
|
||||
dialogView.ieee80211be.isChecked = base.isIeee80211beEnabled
|
||||
dialogView.userConfig.isChecked = base.isUserConfiguration
|
||||
dialogView.bridgedTimeout.setText(base.bridgedModeOpportunisticShutdownTimeoutMillis.let {
|
||||
if (it == -1L) "" else it.toString()
|
||||
})
|
||||
dialogView.vendorElements.setText(VendorElements.serialize(base.vendorElements))
|
||||
dialogView.persistentRandomizedMac.setText(base.persistentRandomizedMacAddress?.toString())
|
||||
for ((band, text, _) in acsList) text.setText(RangeInput.toString(base.allowedAcsChannels[band]))
|
||||
if (Build.VERSION.SDK_INT >= 33) bandWidthOptions.binarySearch(BandWidth(base.maxChannelBandwidth)).let {
|
||||
if (it < 0) {
|
||||
Timber.w(Exception("Cannot locate bandwidth ${base.maxChannelBandwidth}"))
|
||||
} else dialogView.maxChannelBandwidth.setSelection(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
@@ -270,64 +372,62 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
|
||||
validate()
|
||||
}
|
||||
|
||||
@TargetApi(28)
|
||||
private fun validate() {
|
||||
if (!started) return
|
||||
val ssidLength = dialogView.ssid.text.toString().toByteArray().size
|
||||
dialogView.ssidWrapper.error = if (arg.p2pMode && RepeaterService.safeMode && ssidLength < 9) {
|
||||
requireContext().getString(R.string.settings_service_repeater_safe_mode_warning)
|
||||
} else null
|
||||
val (ssidOk, ssidError) = 0.let {
|
||||
val ssid = try {
|
||||
ssid
|
||||
} catch (e: IllegalArgumentException) {
|
||||
return@let false to e.readableMessage
|
||||
}
|
||||
val ssidLength = ssid?.bytes?.size ?: 0
|
||||
if (ssidLength in 1..32) true to if (arg.p2pMode && RepeaterService.safeMode && ssidLength < 9) {
|
||||
requireContext().getString(R.string.settings_service_repeater_safe_mode_warning)
|
||||
} else null else false to " "
|
||||
}
|
||||
dialogView.ssidWrapper.error = ssidError
|
||||
val selectedSecurity = if (arg.p2pMode) {
|
||||
SoftApConfiguration.SECURITY_TYPE_WPA2_PSK
|
||||
} else dialogView.security.selectedItemPosition
|
||||
// see also: https://android.googlesource.com/platform/frameworks/base/+/92c8f59/wifi/java/android/net/wifi/SoftApConfiguration.java#688
|
||||
val passwordValid = when (selectedSecurity) {
|
||||
SoftApConfiguration.SECURITY_TYPE_OPEN,
|
||||
SoftApConfiguration.SECURITY_TYPE_WPA3_OWE_TRANSITION,
|
||||
SoftApConfiguration.SECURITY_TYPE_WPA3_OWE -> true
|
||||
SoftApConfiguration.SECURITY_TYPE_WPA2_PSK, SoftApConfiguration.SECURITY_TYPE_WPA3_SAE_TRANSITION -> {
|
||||
dialogView.password.length() >= 8
|
||||
dialogView.password.length() in 8..63
|
||||
}
|
||||
else -> true // do not try to validate
|
||||
else -> dialogView.password.length() > 0
|
||||
}
|
||||
dialogView.passwordWrapper.error = if (passwordValid) null else " "
|
||||
val timeoutError = dialogView.timeout.text.let { text ->
|
||||
if (text.isNullOrEmpty()) null else try {
|
||||
text.toString().toLong()
|
||||
SoftApConfigurationCompat.testPlatformTimeoutValidity(text.toString().toLong())
|
||||
null
|
||||
} catch (e: NumberFormatException) {
|
||||
} catch (e: Exception) {
|
||||
e.readableMessage
|
||||
}
|
||||
}
|
||||
dialogView.timeoutWrapper.error = timeoutError
|
||||
val isBandValid = when {
|
||||
arg.p2pMode || Build.VERSION.SDK_INT in 23 until 30 -> {
|
||||
val option5G = dialogView.band5G.selectedItem
|
||||
when (dialogView.band2G.selectedItem) {
|
||||
is ChannelOption.Disabled -> option5G !is ChannelOption.Disabled &&
|
||||
(!arg.p2pMode || RepeaterService.safeMode || option5G !is ChannelOption.Auto)
|
||||
is ChannelOption.Auto ->
|
||||
(arg.p2pMode || Build.VERSION.SDK_INT >= 28) && option5G is ChannelOption.Auto ||
|
||||
(!arg.p2pMode || RepeaterService.safeMode) && option5G is ChannelOption.Disabled
|
||||
else -> option5G is ChannelOption.Disabled
|
||||
}
|
||||
val bandError = if (!arg.p2pMode && Build.VERSION.SDK_INT >= 30) {
|
||||
try {
|
||||
SoftApConfigurationCompat.testPlatformValidity(generateChannels())
|
||||
null
|
||||
} catch (e: Exception) {
|
||||
e.readableMessage
|
||||
}
|
||||
Build.VERSION.SDK_INT == 30 && !BuildCompat.isAtLeastS() -> {
|
||||
var expected = 1
|
||||
var set = 0
|
||||
for (s in arrayOf(dialogView.band2G, dialogView.band5G, dialogView.band6G)) when (s.selectedItem) {
|
||||
is ChannelOption.Auto -> expected = 0
|
||||
!is ChannelOption.Disabled -> ++set
|
||||
}
|
||||
set == expected
|
||||
}
|
||||
else -> {
|
||||
setBridgedMode()
|
||||
true
|
||||
}
|
||||
}
|
||||
} else null
|
||||
dialogView.bandError.isGone = bandError.isNullOrEmpty()
|
||||
dialogView.bandError.text = bandError
|
||||
val hideBssid = !arg.p2pMode && Build.VERSION.SDK_INT >= 31 &&
|
||||
dialogView.macRandomization.selectedItemPosition != SoftApConfigurationCompat.RANDOMIZATION_NONE
|
||||
dialogView.bssidWrapper.isGone = hideBssid
|
||||
dialogView.bssidWrapper.error = null
|
||||
val bssidValid = dialogView.bssid.length() == 0 || try {
|
||||
MacAddressCompat.fromString(dialogView.bssid.text.toString())
|
||||
val bssidValid = hideBssid || dialogView.bssid.length() == 0 || try {
|
||||
val mac = MacAddress.fromString(dialogView.bssid.text.toString())
|
||||
if (Build.VERSION.SDK_INT >= 30 && !arg.p2pMode) SoftApConfigurationCompat.testPlatformValidity(mac)
|
||||
true
|
||||
} catch (e: IllegalArgumentException) {
|
||||
} catch (e: Exception) {
|
||||
dialogView.bssidWrapper.error = e.readableMessage
|
||||
false
|
||||
}
|
||||
@@ -340,26 +440,80 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
|
||||
}
|
||||
}
|
||||
dialogView.maxClientWrapper.error = maxClientError
|
||||
val blockedListError = try {
|
||||
(dialogView.blockedList.text ?: "").split(nonMacChars)
|
||||
.filter { it.isNotEmpty() }.forEach { MacAddressCompat.fromString(it).toPlatform() }
|
||||
null
|
||||
} catch (e: IllegalArgumentException) {
|
||||
e.readableMessage
|
||||
val listsNoError = if (Build.VERSION.SDK_INT >= 30) {
|
||||
val (blockedList, blockedListError) = try {
|
||||
(dialogView.blockedList.text ?: "").split(nonMacChars).filter { it.isNotEmpty() }
|
||||
.map(MacAddress::fromString).toSet() to null
|
||||
} catch (e: IllegalArgumentException) {
|
||||
null to e.readableMessage
|
||||
}
|
||||
dialogView.blockedListWrapper.error = blockedListError
|
||||
val allowedListError = try {
|
||||
(dialogView.allowedList.text ?: "").split(nonMacChars).filter { it.isNotEmpty() }.forEach {
|
||||
val mac = MacAddress.fromString(it)
|
||||
require(blockedList?.contains(mac) != true) { "A MAC address exists in both client lists" }
|
||||
}
|
||||
null
|
||||
} catch (e: IllegalArgumentException) {
|
||||
e.readableMessage
|
||||
}
|
||||
dialogView.allowedListWrapper.error = allowedListError
|
||||
blockedListError == null && allowedListError == null
|
||||
} else true
|
||||
val bridgedTimeoutError = dialogView.bridgedTimeout.text.let { text ->
|
||||
if (text.isNullOrEmpty()) null else try {
|
||||
SoftApConfigurationCompat.testPlatformBridgedTimeoutValidity(text.toString().toLong())
|
||||
null
|
||||
} catch (e: Exception) {
|
||||
e.readableMessage
|
||||
}
|
||||
}
|
||||
dialogView.blockedListWrapper.error = blockedListError
|
||||
val allowedListError = try {
|
||||
(dialogView.allowedList.text ?: "").split(nonMacChars)
|
||||
.filter { it.isNotEmpty() }.forEach { MacAddressCompat.fromString(it).toPlatform() }
|
||||
null
|
||||
dialogView.bridgedTimeoutWrapper.error = bridgedTimeoutError
|
||||
val vendorElementsError = if (Build.VERSION.SDK_INT >= 33) {
|
||||
try {
|
||||
VendorElements.deserialize(dialogView.vendorElements.text).also {
|
||||
if (!arg.p2pMode) SoftApConfigurationCompat.testPlatformValidity(it)
|
||||
}
|
||||
null
|
||||
} catch (e: Exception) {
|
||||
e.readableMessage
|
||||
}
|
||||
} else null
|
||||
dialogView.vendorElementsWrapper.error = vendorElementsError
|
||||
dialogView.persistentRandomizedMacWrapper.error = null
|
||||
val persistentRandomizedMacValid = dialogView.persistentRandomizedMac.length() == 0 || try {
|
||||
MacAddress.fromString(dialogView.persistentRandomizedMac.text.toString())
|
||||
true
|
||||
} catch (e: IllegalArgumentException) {
|
||||
e.readableMessage
|
||||
dialogView.persistentRandomizedMacWrapper.error = e.readableMessage
|
||||
false
|
||||
}
|
||||
dialogView.allowedListWrapper.error = allowedListError
|
||||
val canCopy = timeoutError == null && bssidValid && maxClientError == null && blockedListError == null &&
|
||||
allowedListError == null
|
||||
val acsNoError = if (!arg.p2pMode && Build.VERSION.SDK_INT >= 33) acsList.all { (band, text, wrapper) ->
|
||||
try {
|
||||
wrapper.error = null
|
||||
SoftApConfigurationCompat.testPlatformValidity(band, RangeInput.fromString(text.text).toIntArray())
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
wrapper.error = e.readableMessage
|
||||
false
|
||||
}
|
||||
} else true
|
||||
val bandwidthError = if (!arg.p2pMode && Build.VERSION.SDK_INT >= 33) {
|
||||
try {
|
||||
SoftApConfigurationCompat.testPlatformValidity(
|
||||
(dialogView.maxChannelBandwidth.selectedItem as BandWidth).width)
|
||||
null
|
||||
} catch (e: Exception) {
|
||||
e.readableMessage
|
||||
}
|
||||
} else null
|
||||
dialogView.maxChannelBandwidthError.isGone = bandwidthError.isNullOrEmpty()
|
||||
dialogView.maxChannelBandwidthError.text = bandwidthError
|
||||
val canCopy = timeoutError == null && bssidValid && maxClientError == null && listsNoError &&
|
||||
bridgedTimeoutError == null && vendorElementsError == null && persistentRandomizedMacValid &&
|
||||
acsNoError && bandwidthError == null
|
||||
(dialog as? AlertDialog)?.getButton(DialogInterface.BUTTON_POSITIVE)?.isEnabled =
|
||||
ssidLength in 1..32 && passwordValid && isBandValid && canCopy
|
||||
ssidOk && passwordValid && bandError == null && canCopy
|
||||
dialogView.toolbar.menu.findItem(android.R.id.copy).isEnabled = canCopy
|
||||
}
|
||||
|
||||
@@ -372,10 +526,15 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
|
||||
|
||||
override fun onMenuItemClick(item: MenuItem?): Boolean {
|
||||
return when (item?.itemId) {
|
||||
android.R.id.copy -> {
|
||||
android.R.id.copy -> try {
|
||||
app.clipboard.setPrimaryClip(ClipData.newPlainText(null,
|
||||
Base64.encodeToString(generateConfig().toByteArray(), BASE64_FLAGS)))
|
||||
Base64.encodeToString(generateConfig().toByteArray(), BASE64_FLAGS)).apply {
|
||||
description.extras = persistableBundleOf(ClipDescription.EXTRA_IS_SENSITIVE to true)
|
||||
})
|
||||
true
|
||||
} catch (e: RuntimeException) {
|
||||
Toast.makeText(context, e.readableMessage, Toast.LENGTH_LONG).show()
|
||||
false
|
||||
}
|
||||
android.R.id.paste -> try {
|
||||
app.clipboard.primaryClip?.getItemAt(0)?.text?.apply {
|
||||
@@ -385,12 +544,13 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
|
||||
arg.configuration.underlying?.let { check(it.javaClass == newUnderlying.javaClass) }
|
||||
} else config.underlying = arg.configuration.underlying
|
||||
base = config
|
||||
pasted = true
|
||||
populateFromConfiguration()
|
||||
}
|
||||
}
|
||||
true
|
||||
} catch (e: IllegalArgumentException) {
|
||||
SmartSnackbar.make(e).show()
|
||||
} catch (e: RuntimeException) {
|
||||
Toast.makeText(context, e.readableMessage, Toast.LENGTH_LONG).show()
|
||||
false
|
||||
}
|
||||
R.id.share_qr -> {
|
||||
|
||||
@@ -10,13 +10,8 @@ import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Parcelable
|
||||
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
|
||||
import be.mygod.vpnhotspot.util.ConstantLookup
|
||||
import be.mygod.vpnhotspot.util.Services
|
||||
import be.mygod.vpnhotspot.util.callSuper
|
||||
import be.mygod.vpnhotspot.util.findIdentifier
|
||||
import be.mygod.vpnhotspot.util.*
|
||||
import timber.log.Timber
|
||||
import java.lang.reflect.InvocationHandler
|
||||
import java.lang.reflect.Method
|
||||
@@ -39,17 +34,22 @@ object WifiApManager {
|
||||
PackageManager.MATCH_SYSTEM_ONLY).single()
|
||||
|
||||
private const val CONFIG_P2P_MAC_RANDOMIZATION_SUPPORTED = "config_wifi_p2p_mac_randomization_supported"
|
||||
val p2pMacRandomizationSupported get() = when (Build.VERSION.SDK_INT) {
|
||||
29 -> Resources.getSystem().run {
|
||||
getBoolean(getIdentifier(CONFIG_P2P_MAC_RANDOMIZATION_SUPPORTED, "bool", "android"))
|
||||
val p2pMacRandomizationSupported get() = try {
|
||||
when (Build.VERSION.SDK_INT) {
|
||||
29 -> Resources.getSystem().run {
|
||||
getBoolean(getIdentifier(CONFIG_P2P_MAC_RANDOMIZATION_SUPPORTED, "bool", "android"))
|
||||
}
|
||||
in 30..Int.MAX_VALUE -> @TargetApi(30) {
|
||||
val info = resolvedActivity.activityInfo
|
||||
val resources = app.packageManager.getResourcesForApplication(info.applicationInfo)
|
||||
resources.getBoolean(resources.findIdentifier(CONFIG_P2P_MAC_RANDOMIZATION_SUPPORTED, "bool",
|
||||
RESOURCES_PACKAGE, info.packageName))
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
in 30..Int.MAX_VALUE -> @TargetApi(30) {
|
||||
val info = resolvedActivity.activityInfo
|
||||
val resources = app.packageManager.getResourcesForApplication(info.applicationInfo)
|
||||
resources.getBoolean(resources.findIdentifier(CONFIG_P2P_MAC_RANDOMIZATION_SUPPORTED, "bool",
|
||||
RESOURCES_PACKAGE, info.packageName))
|
||||
}
|
||||
else -> false
|
||||
} catch (e: RuntimeException) {
|
||||
Timber.w(e)
|
||||
false
|
||||
}
|
||||
|
||||
@get:RequiresApi(30)
|
||||
@@ -59,6 +59,92 @@ object WifiApManager {
|
||||
@get:RequiresApi(30)
|
||||
val isApMacRandomizationSupported get() = apMacRandomizationSupported(Services.wifi) as Boolean
|
||||
|
||||
/**
|
||||
* Broadcast intent action indicating that Wi-Fi AP has been enabled, disabled,
|
||||
* enabling, disabling, or failed.
|
||||
*/
|
||||
const val WIFI_AP_STATE_CHANGED_ACTION = "android.net.wifi.WIFI_AP_STATE_CHANGED"
|
||||
/**
|
||||
* The lookup key for an int that indicates whether Wi-Fi AP is enabled,
|
||||
* disabled, enabling, disabling, or failed. Retrieve it with [Intent.getIntExtra].
|
||||
*
|
||||
* @see WIFI_AP_STATE_DISABLED
|
||||
* @see WIFI_AP_STATE_DISABLING
|
||||
* @see WIFI_AP_STATE_ENABLED
|
||||
* @see WIFI_AP_STATE_ENABLING
|
||||
* @see WIFI_AP_STATE_FAILED
|
||||
*/
|
||||
private const val EXTRA_WIFI_AP_STATE = "wifi_state"
|
||||
/**
|
||||
* An extra containing the int error code for Soft AP start failure.
|
||||
* Can be obtained from the [WIFI_AP_STATE_CHANGED_ACTION] using [Intent.getIntExtra].
|
||||
* This extra will only be attached if [EXTRA_WIFI_AP_STATE] is
|
||||
* attached and is equal to [WIFI_AP_STATE_FAILED].
|
||||
*
|
||||
* The error code will be one of:
|
||||
* {@link #SAP_START_FAILURE_GENERAL},
|
||||
* {@link #SAP_START_FAILURE_NO_CHANNEL},
|
||||
* {@link #SAP_START_FAILURE_UNSUPPORTED_CONFIGURATION}
|
||||
*
|
||||
* Source: https://android.googlesource.com/platform/frameworks/base/+/android-6.0.0_r1/wifi/java/android/net/wifi/WifiManager.java#210
|
||||
*/
|
||||
val EXTRA_WIFI_AP_FAILURE_REASON get() =
|
||||
if (Build.VERSION.SDK_INT >= 30) "android.net.wifi.extra.WIFI_AP_FAILURE_REASON" else "wifi_ap_error_code"
|
||||
/**
|
||||
* The lookup key for a String extra that stores the interface name used for the Soft AP.
|
||||
* This extra is included in the broadcast [WIFI_AP_STATE_CHANGED_ACTION].
|
||||
* Retrieve its value with [Intent.getStringExtra].
|
||||
*
|
||||
* Source: https://android.googlesource.com/platform/frameworks/base/+/android-8.0.0_r1/wifi/java/android/net/wifi/WifiManager.java#413
|
||||
*/
|
||||
val EXTRA_WIFI_AP_INTERFACE_NAME get() =
|
||||
if (Build.VERSION.SDK_INT >= 30) "android.net.wifi.extra.WIFI_AP_INTERFACE_NAME" else "wifi_ap_interface_name"
|
||||
|
||||
fun checkWifiApState(state: Int) = if (state < WIFI_AP_STATE_DISABLING || state > WIFI_AP_STATE_FAILED) {
|
||||
Timber.w(Exception("Unknown state $state"))
|
||||
false
|
||||
} else true
|
||||
val Intent.wifiApState get() =
|
||||
getIntExtra(EXTRA_WIFI_AP_STATE, WIFI_AP_STATE_DISABLED).also { checkWifiApState(it) }
|
||||
/**
|
||||
* Wi-Fi AP is currently being disabled. The state will change to
|
||||
* [WIFI_AP_STATE_DISABLED] if it finishes successfully.
|
||||
*
|
||||
* @see WIFI_AP_STATE_CHANGED_ACTION
|
||||
* @see #getWifiApState()
|
||||
*/
|
||||
const val WIFI_AP_STATE_DISABLING = 10
|
||||
/**
|
||||
* Wi-Fi AP is disabled.
|
||||
*
|
||||
* @see WIFI_AP_STATE_CHANGED_ACTION
|
||||
* @see #getWifiState()
|
||||
*/
|
||||
const val WIFI_AP_STATE_DISABLED = 11
|
||||
/**
|
||||
* Wi-Fi AP is currently being enabled. The state will change to
|
||||
* {@link #WIFI_AP_STATE_ENABLED} if it finishes successfully.
|
||||
*
|
||||
* @see WIFI_AP_STATE_CHANGED_ACTION
|
||||
* @see #getWifiApState()
|
||||
*/
|
||||
const val WIFI_AP_STATE_ENABLING = 12
|
||||
/**
|
||||
* Wi-Fi AP is enabled.
|
||||
*
|
||||
* @see WIFI_AP_STATE_CHANGED_ACTION
|
||||
* @see #getWifiApState()
|
||||
*/
|
||||
const val WIFI_AP_STATE_ENABLED = 13
|
||||
/**
|
||||
* Wi-Fi AP is in a failed state. This state will occur when an error occurs during
|
||||
* enabling or disabling
|
||||
*
|
||||
* @see WIFI_AP_STATE_CHANGED_ACTION
|
||||
* @see #getWifiApState()
|
||||
*/
|
||||
const val WIFI_AP_STATE_FAILED = 14
|
||||
|
||||
private val getWifiApConfiguration by lazy { WifiManager::class.java.getDeclaredMethod("getWifiApConfiguration") }
|
||||
@Suppress("DEPRECATION")
|
||||
private val setWifiApConfiguration by lazy {
|
||||
@@ -72,29 +158,29 @@ object WifiApManager {
|
||||
WifiManager::class.java.getDeclaredMethod("setSoftApConfiguration", SoftApConfiguration::class.java)
|
||||
}
|
||||
|
||||
@get:RequiresApi(30)
|
||||
val configuration get() = getSoftApConfiguration(Services.wifi) as SoftApConfiguration
|
||||
/**
|
||||
* Requires NETWORK_SETTINGS permission (or root) on API 30+, and OVERRIDE_WIFI_CONFIG on API 29-.
|
||||
*/
|
||||
val configurationCompat get() = if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
|
||||
(getWifiApConfiguration(Services.wifi) as android.net.wifi.WifiConfiguration?)?.toCompat()
|
||||
?: SoftApConfigurationCompat()
|
||||
} else configuration.toCompat()
|
||||
fun setConfigurationCompat(value: SoftApConfigurationCompat) = (if (Build.VERSION.SDK_INT >= 30) {
|
||||
setSoftApConfiguration(Services.wifi, value.toPlatform())
|
||||
} else @Suppress("DEPRECATION") {
|
||||
setWifiApConfiguration(Services.wifi, value.toWifiConfiguration())
|
||||
}) as Boolean
|
||||
@Deprecated("Use configuration instead", ReplaceWith("configuration"))
|
||||
@Suppress("DEPRECATION")
|
||||
val configurationLegacy get() = getWifiApConfiguration(Services.wifi) as android.net.wifi.WifiConfiguration?
|
||||
/**
|
||||
* Requires NETWORK_SETTINGS permission (or root).
|
||||
*/
|
||||
@get:RequiresApi(30)
|
||||
val configuration get() = getSoftApConfiguration(Services.wifi) as SoftApConfiguration
|
||||
@Deprecated("Use SoftApConfiguration instead")
|
||||
@Suppress("DEPRECATION")
|
||||
fun setConfiguration(value: android.net.wifi.WifiConfiguration?) =
|
||||
setWifiApConfiguration(Services.wifi, value) as Boolean
|
||||
fun setConfiguration(value: SoftApConfiguration) = setSoftApConfiguration(Services.wifi, value) as Boolean
|
||||
|
||||
@RequiresApi(28)
|
||||
interface SoftApCallbackCompat {
|
||||
/**
|
||||
* Called when soft AP state changes.
|
||||
*
|
||||
* @param state the new AP state. One of {@link #WIFI_AP_STATE_DISABLED},
|
||||
* {@link #WIFI_AP_STATE_DISABLING}, {@link #WIFI_AP_STATE_ENABLED},
|
||||
* {@link #WIFI_AP_STATE_ENABLING}, {@link #WIFI_AP_STATE_FAILED}
|
||||
* @param state the new AP state. One of [WIFI_AP_STATE_DISABLED], [WIFI_AP_STATE_DISABLING],
|
||||
* [WIFI_AP_STATE_ENABLED], [WIFI_AP_STATE_ENABLING], [WIFI_AP_STATE_FAILED]
|
||||
* @param failureReason reason when in failed state. One of
|
||||
* {@link #SAP_START_FAILURE_GENERAL},
|
||||
* {@link #SAP_START_FAILURE_NO_CHANNEL},
|
||||
@@ -150,7 +236,6 @@ object WifiApManager {
|
||||
@RequiresApi(30)
|
||||
fun onBlockedClientConnecting(client: Parcelable, blockedReason: Int) { }
|
||||
}
|
||||
@RequiresApi(28)
|
||||
val failureReasonLookup = ConstantLookup<WifiManager>("SAP_START_FAILURE_", "GENERAL", "NO_CHANNEL")
|
||||
@get:RequiresApi(30)
|
||||
val clientBlockLookup by lazy { ConstantLookup<WifiManager>("SAP_CLIENT_") }
|
||||
@@ -166,7 +251,6 @@ object WifiApManager {
|
||||
WifiManager::class.java.getDeclaredMethod("unregisterSoftApCallback", interfaceSoftApCallback)
|
||||
}
|
||||
|
||||
@RequiresApi(28)
|
||||
fun registerSoftApCallback(callback: SoftApCallbackCompat, executor: Executor): Any {
|
||||
val proxy = Proxy.newProxyInstance(interfaceSoftApCallback.classLoader,
|
||||
arrayOf(interfaceSoftApCallback), object : InvocationHandler {
|
||||
@@ -177,55 +261,36 @@ object WifiApManager {
|
||||
} else invokeActual(proxy, method, args)
|
||||
|
||||
private fun invokeActual(proxy: Any, method: Method, args: Array<out Any?>?): Any? {
|
||||
val noArgs = args?.size ?: 0
|
||||
return when (val name = method.name) {
|
||||
"onStateChanged" -> {
|
||||
if (noArgs != 2) Timber.w("Unexpected args for $name: ${args?.contentToString()}")
|
||||
return when {
|
||||
method.matches("onStateChanged", Integer.TYPE, Integer.TYPE) -> {
|
||||
callback.onStateChanged(args!![0] as Int, args[1] as Int)
|
||||
}
|
||||
"onNumClientsChanged" -> @Suppress("DEPRECATION") {
|
||||
method.matches("onNumClientsChanged", Integer.TYPE) -> {
|
||||
if (Build.VERSION.SDK_INT >= 30) Timber.w(Exception("Unexpected onNumClientsChanged"))
|
||||
if (noArgs != 1) Timber.w("Unexpected args for $name: ${args?.contentToString()}")
|
||||
callback.onNumClientsChanged(args!![0] as Int)
|
||||
}
|
||||
"onConnectedClientsChanged" -> @TargetApi(30) {
|
||||
method.matches1<java.util.List<*>>("onConnectedClientsChanged") -> @TargetApi(30) {
|
||||
if (Build.VERSION.SDK_INT < 30) Timber.w(Exception("Unexpected onConnectedClientsChanged"))
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
when (noArgs) {
|
||||
1 -> callback.onConnectedClientsChanged(args!![0] as List<Parcelable>)
|
||||
2 -> null // we use the old method which returns all clients in one call
|
||||
else -> {
|
||||
Timber.w("Unexpected args for $name: ${args?.contentToString()}")
|
||||
null
|
||||
}
|
||||
}
|
||||
callback.onConnectedClientsChanged(args!![0] as List<Parcelable>)
|
||||
}
|
||||
"onInfoChanged" -> @TargetApi(30) {
|
||||
if (noArgs != 1) Timber.w("Unexpected args for $name: ${args?.contentToString()}")
|
||||
method.matches1<java.util.List<*>>("onInfoChanged") -> @TargetApi(31) {
|
||||
if (Build.VERSION.SDK_INT < 31) Timber.w(Exception("Unexpected onInfoChanged API 31+"))
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
callback.onInfoChanged(args!![0] as List<Parcelable>)
|
||||
}
|
||||
Build.VERSION.SDK_INT >= 30 && method.matches("onInfoChanged", SoftApInfo.clazz) -> {
|
||||
if (Build.VERSION.SDK_INT >= 31) return null // ignore old version calls
|
||||
val arg = args!![0]
|
||||
if (arg is List<*>) {
|
||||
if (!BuildCompat.isAtLeastS()) Timber.w(Exception("Unexpected onInfoChanged API 31+"))
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
callback.onInfoChanged(arg as List<Parcelable>)
|
||||
} else {
|
||||
when (Build.VERSION.SDK_INT) {
|
||||
30 -> { }
|
||||
in 31..Int.MAX_VALUE -> return null // ignore old version calls
|
||||
else -> Timber.w(Exception("Unexpected onInfoChanged API 30"))
|
||||
}
|
||||
val info = SoftApInfo(arg as Parcelable)
|
||||
callback.onInfoChanged( // check for legacy empty info with CHANNEL_WIDTH_INVALID
|
||||
if (info.frequency == 0 && info.bandwidth == 0) emptyList() else listOf(arg))
|
||||
}
|
||||
val info = SoftApInfo(arg as Parcelable)
|
||||
callback.onInfoChanged(if (info.frequency == 0 && info.bandwidth ==
|
||||
SoftApConfigurationCompat.CHANNEL_WIDTH_INVALID) emptyList() else listOf(arg))
|
||||
}
|
||||
"onCapabilityChanged" -> @TargetApi(30) {
|
||||
if (Build.VERSION.SDK_INT < 30) Timber.w(Exception("Unexpected onCapabilityChanged"))
|
||||
if (noArgs != 1) Timber.w("Unexpected args for $name: ${args?.contentToString()}")
|
||||
Build.VERSION.SDK_INT >= 30 && method.matches("onCapabilityChanged", SoftApCapability.clazz) -> {
|
||||
callback.onCapabilityChanged(args!![0] as Parcelable)
|
||||
}
|
||||
"onBlockedClientConnecting" -> @TargetApi(30) {
|
||||
if (Build.VERSION.SDK_INT < 30) Timber.w(Exception("Unexpected onBlockedClientConnecting"))
|
||||
if (noArgs != 2) Timber.w("Unexpected args for $name: ${args?.contentToString()}")
|
||||
Build.VERSION.SDK_INT >= 30 && method.matches("onBlockedClientConnecting", WifiClient.clazz,
|
||||
Int::class.java) -> {
|
||||
callback.onBlockedClientConnecting(args!![0] as Parcelable, args[1] as Int)
|
||||
}
|
||||
else -> callSuper(interfaceSoftApCallback, proxy, method, args)
|
||||
@@ -237,7 +302,6 @@ object WifiApManager {
|
||||
} else registerSoftApCallback(Services.wifi, proxy, null)
|
||||
return proxy
|
||||
}
|
||||
@RequiresApi(28)
|
||||
fun unregisterSoftApCallback(key: Any) = unregisterSoftApCallback(Services.wifi, key)
|
||||
|
||||
@get:RequiresApi(30)
|
||||
@@ -253,43 +317,9 @@ object WifiApManager {
|
||||
private val cancelLocalOnlyHotspotRequest by lazy {
|
||||
WifiManager::class.java.getDeclaredMethod("cancelLocalOnlyHotspotRequest")
|
||||
}
|
||||
@RequiresApi(26)
|
||||
/**
|
||||
* This is the only way to unregister requests besides app exiting.
|
||||
* Therefore, we are happy with crashing the app if reflection fails.
|
||||
*/
|
||||
fun cancelLocalOnlyHotspotRequest() = cancelLocalOnlyHotspotRequest(Services.wifi)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private val setWifiApEnabled by lazy {
|
||||
WifiManager::class.java.getDeclaredMethod("setWifiApEnabled",
|
||||
android.net.wifi.WifiConfiguration::class.java, Boolean::class.java)
|
||||
}
|
||||
/**
|
||||
* Start AccessPoint mode with the specified
|
||||
* configuration. If the radio is already running in
|
||||
* AP mode, update the new configuration
|
||||
* Note that starting in access point mode disables station
|
||||
* mode operation
|
||||
* @param wifiConfig SSID, security and channel details as
|
||||
* part of WifiConfiguration
|
||||
* @return {@code true} if the operation succeeds, {@code false} otherwise
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
private fun WifiManager.setWifiApEnabled(wifiConfig: android.net.wifi.WifiConfiguration?, enabled: Boolean) =
|
||||
setWifiApEnabled(this, wifiConfig, enabled) as Boolean
|
||||
|
||||
/**
|
||||
* Although the functionalities were removed in API 26, it is already not functioning correctly on API 25.
|
||||
*
|
||||
* See also: https://android.googlesource.com/platform/frameworks/base/+/5c0b10a4a9eecc5307bb89a271221f2b20448797%5E%21/
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated("Not usable since API 26, malfunctioning on API 25")
|
||||
fun start(wifiConfig: android.net.wifi.WifiConfiguration? = null) {
|
||||
Services.wifi.isWifiEnabled = false
|
||||
Services.wifi.setWifiApEnabled(wifiConfig, true)
|
||||
}
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated("Not usable since API 26")
|
||||
fun stop() {
|
||||
Services.wifi.setWifiApEnabled(null, false)
|
||||
Services.wifi.isWifiEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import timber.log.Timber
|
||||
@RequiresApi(30)
|
||||
value class WifiClient(val inner: Parcelable) {
|
||||
companion object {
|
||||
private val clazz by lazy { Class.forName("android.net.wifi.WifiClient") }
|
||||
val clazz by lazy { Class.forName("android.net.wifi.WifiClient") }
|
||||
private val getMacAddress by lazy { clazz.getDeclaredMethod("getMacAddress") }
|
||||
@get:RequiresApi(31)
|
||||
private val getApInstanceIdentifier by lazy @TargetApi(31) { UnblockCentral.getApInstanceIdentifier(clazz) }
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
package be.mygod.vpnhotspot.net.wifi
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.net.MacAddress
|
||||
import android.net.wifi.ScanResult
|
||||
import android.net.wifi.WpsInfo
|
||||
import android.net.wifi.p2p.WifiP2pGroup
|
||||
import android.net.wifi.p2p.WifiP2pInfo
|
||||
import android.net.wifi.p2p.WifiP2pManager
|
||||
import androidx.annotation.RequiresApi
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.net.MacAddressCompat
|
||||
import be.mygod.vpnhotspot.util.callSuper
|
||||
import be.mygod.vpnhotspot.util.matches
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import timber.log.Timber
|
||||
import java.lang.reflect.InvocationHandler
|
||||
import java.lang.reflect.Method
|
||||
import java.lang.reflect.Proxy
|
||||
@@ -53,6 +56,15 @@ object WifiP2pManagerHelper {
|
||||
return result.future.await()
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission") // this method will fail correctly if permission is missing
|
||||
@RequiresApi(33)
|
||||
suspend fun WifiP2pManager.setVendorElements(c: WifiP2pManager.Channel,
|
||||
ve: List<ScanResult.InformationElement>): Int? {
|
||||
val result = ResultListener()
|
||||
setVendorElements(c, ve, result)
|
||||
return result.future.await()
|
||||
}
|
||||
|
||||
/**
|
||||
* Available since Android 4.3.
|
||||
*
|
||||
@@ -98,9 +110,8 @@ object WifiP2pManagerHelper {
|
||||
private val interfacePersistentGroupInfoListener by lazy {
|
||||
Class.forName("android.net.wifi.p2p.WifiP2pManager\$PersistentGroupInfoListener")
|
||||
}
|
||||
private val getGroupList by lazy {
|
||||
Class.forName("android.net.wifi.p2p.WifiP2pGroupList").getDeclaredMethod("getGroupList")
|
||||
}
|
||||
private val classWifiP2pGroupList by lazy { Class.forName("android.net.wifi.p2p.WifiP2pGroupList") }
|
||||
private val getGroupList by lazy { classWifiP2pGroupList.getDeclaredMethod("getGroupList") }
|
||||
private val requestPersistentGroupInfo by lazy {
|
||||
WifiP2pManager::class.java.getDeclaredMethod("requestPersistentGroupInfo",
|
||||
WifiP2pManager.Channel::class.java, interfacePersistentGroupInfoListener)
|
||||
@@ -116,9 +127,8 @@ object WifiP2pManagerHelper {
|
||||
val result = CompletableDeferred<Collection<WifiP2pGroup>>()
|
||||
requestPersistentGroupInfo(this, c, Proxy.newProxyInstance(interfacePersistentGroupInfoListener.classLoader,
|
||||
arrayOf(interfacePersistentGroupInfoListener), object : InvocationHandler {
|
||||
override fun invoke(proxy: Any, method: Method, args: Array<out Any?>?): Any? = when (method.name) {
|
||||
"onPersistentGroupInfoAvailable" -> {
|
||||
if (args?.size != 1) Timber.w(IllegalArgumentException("Unexpected args: $args"))
|
||||
override fun invoke(proxy: Any, method: Method, args: Array<out Any?>?): Any? = when {
|
||||
method.matches("onPersistentGroupInfoAvailable", classWifiP2pGroupList) -> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
result.complete(getGroupList(args!![0]) as Collection<WifiP2pGroup>)
|
||||
}
|
||||
@@ -128,14 +138,22 @@ object WifiP2pManagerHelper {
|
||||
return result.await()
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
suspend fun WifiP2pManager.requestConnectionInfo(c: WifiP2pManager.Channel) =
|
||||
CompletableDeferred<WifiP2pInfo?>().apply { requestConnectionInfo(c) { complete(it) } }.await()
|
||||
@SuppressLint("MissingPermission") // missing permission simply leads to null result
|
||||
@RequiresApi(29)
|
||||
suspend fun WifiP2pManager.requestDeviceAddress(c: WifiP2pManager.Channel): MacAddressCompat? {
|
||||
suspend fun WifiP2pManager.requestDeviceAddress(c: WifiP2pManager.Channel): MacAddress? {
|
||||
val future = CompletableDeferred<String?>()
|
||||
requestDeviceInfo(c) { future.complete(it?.deviceAddress) }
|
||||
return future.await()?.let {
|
||||
val address = if (it.isEmpty()) null else MacAddressCompat.fromString(it)
|
||||
val address = if (it.isEmpty()) null else MacAddress.fromString(it)
|
||||
if (address == MacAddressCompat.ANY_ADDRESS) null else address
|
||||
}
|
||||
}
|
||||
@SuppressLint("MissingPermission") // missing permission simply leads to null result
|
||||
suspend fun WifiP2pManager.requestGroupInfo(c: WifiP2pManager.Channel) =
|
||||
CompletableDeferred<WifiP2pGroup?>().apply { requestGroupInfo(c) { complete(it) } }.await()
|
||||
@RequiresApi(29)
|
||||
suspend fun WifiP2pManager.requestP2pState(c: WifiP2pManager.Channel) =
|
||||
CompletableDeferred<Int>().apply { requestP2pState(c) { complete(it) } }.await()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package be.mygod.vpnhotspot.net.wifi
|
||||
|
||||
import android.net.wifi.WifiSsid
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.RequiresApi
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.jetbrains.annotations.Contract
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.CharBuffer
|
||||
import java.nio.charset.Charset
|
||||
import java.nio.charset.CodingErrorAction
|
||||
|
||||
@Parcelize
|
||||
data class WifiSsidCompat(val bytes: ByteArray) : Parcelable {
|
||||
companion object {
|
||||
private val hexTester = Regex("^(?:[0-9a-f]{2})*$", RegexOption.IGNORE_CASE)
|
||||
private val qrSanitizer = Regex("([\\\\\":;,])")
|
||||
|
||||
fun fromHex(hex: CharSequence?) = hex?.run {
|
||||
require(length % 2 == 0) { "Input should be hex: $hex" }
|
||||
WifiSsidCompat((0 until length / 2).map {
|
||||
Integer.parseInt(substring(it * 2, it * 2 + 2), 16).toByte()
|
||||
}.toByteArray())
|
||||
}
|
||||
|
||||
@Contract("null -> null; !null -> !null")
|
||||
fun fromUtf8Text(text: CharSequence?) = text?.toString()?.toByteArray()?.let { WifiSsidCompat(it) }
|
||||
|
||||
fun toMeCard(text: String) = qrSanitizer.replace(text) { "\\${it.groupValues[1]}" }
|
||||
|
||||
@RequiresApi(33)
|
||||
fun WifiSsid.toCompat() = WifiSsidCompat(bytes)
|
||||
}
|
||||
|
||||
init {
|
||||
require(bytes.size <= 32) { "${bytes.size} > 32" }
|
||||
}
|
||||
|
||||
@RequiresApi(31)
|
||||
fun toPlatform() = WifiSsid.fromBytes(bytes)
|
||||
|
||||
fun decode(charset: Charset = Charsets.UTF_8) = CharBuffer.allocate(32).run {
|
||||
val result = charset.newDecoder().apply {
|
||||
onMalformedInput(CodingErrorAction.REPORT)
|
||||
onUnmappableCharacter(CodingErrorAction.REPORT)
|
||||
}.decode(ByteBuffer.wrap(bytes), this, true)
|
||||
if (result.isError) null else flip().toString()
|
||||
}
|
||||
val hex get() = bytes.joinToString("") { "%02x".format(it.toUByte().toInt()) }
|
||||
|
||||
fun toMeCard(): String {
|
||||
val utf8 = decode() ?: return hex
|
||||
return if (hexTester.matches(utf8)) "\"$utf8\"" else toMeCard(utf8)
|
||||
}
|
||||
|
||||
override fun toString() = String(bytes)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
other as WifiSsidCompat
|
||||
if (!bytes.contentEquals(other.bytes)) return false
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode() = bytes.contentHashCode()
|
||||
}
|
||||
Reference in New Issue
Block a user