diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetherManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetherManager.kt index 1bd7ab44..b209b2e0 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetherManager.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetherManager.kt @@ -229,30 +229,8 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(), if (Build.VERSION.SDK_INT >= 31) { val list = SoftApConfigurationCompat.BAND_TYPES.map { band -> val channels = capability.getSupportedChannelList(band) - if (channels.isNotEmpty()) StringBuilder().apply { - append(SoftApConfigurationCompat.bandLookup(band, true)) - append(" (") - channels.sort() - var pending: Int? = null - var last = channels[0] - append(last) - for (channel in channels.asSequence().drop(1)) { - if (channel == last + 1) pending = channel else { - pending?.let { - append('-') - append(it) - pending = null - } - append(',') - append(channel) - } - last = channel - } - pending?.let { - append('-') - append(it) - } - append(')') + if (channels.isNotEmpty()) { + "${SoftApConfigurationCompat.bandLookup(band, true)} (${RangeInput.toString(channels)})" } else null }.filterNotNull() if (list.isNotEmpty()) result.append(parent.getText(R.string.tethering_manage_wifi_supported_channels) diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/SoftApConfigurationCompat.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/SoftApConfigurationCompat.kt index 50bc267f..d907c621 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/SoftApConfigurationCompat.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/SoftApConfigurationCompat.kt @@ -455,6 +455,13 @@ data class SoftApConfigurationCompat( fun testPlatformValidity(channels: SparseIntArray) = setChannelsCompat(staticBuilder, channels) @RequiresApi(30) fun testPlatformValidity(bssid: MacAddress) = setBssid(staticBuilder, bssid) + @RequiresApi(33) + fun testPlatformValidity(vendorElements: List) = + 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) diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApDialogFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApDialogFragment.kt index 81736452..db279135 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApDialogFragment.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApDialogFragment.kt @@ -5,6 +5,7 @@ import android.annotation.TargetApi import android.content.ClipData import android.content.ClipDescription import android.content.DialogInterface +import android.net.wifi.ScanResult import android.net.wifi.SoftApConfiguration import android.os.Build import android.os.Parcelable @@ -19,6 +20,7 @@ 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.persistableBundleOf @@ -33,6 +35,7 @@ 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 kotlinx.parcelize.Parcelize @@ -78,6 +81,13 @@ class WifiApDialogFragment : AlertDialogFragment + Array(lookup.size()) { BandWidth(lookup.keyAt(it), lookup.valueAt(it).substring(14)) }.apply { sort() } + } + } } @Parcelize @@ -104,6 +114,11 @@ class WifiApDialogFragment : AlertDialogFragment { + 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 @@ -113,6 +128,13 @@ class WifiApDialogFragment : AlertDialogFragment 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 fun generateChannels() = SparseIntArray(2).apply { @@ -121,6 +143,13 @@ class WifiApDialogFragment : AlertDialogFragment + 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) } private fun generateConfig(full: Boolean = true) = base.copy( ssid = dialogView.ssid.text.toString(), passphrase = if (dialogView.password.length() != 0) dialogView.password.text.toString() else null).apply { @@ -153,6 +182,12 @@ class WifiApDialogFragment : AlertDialogFragment if (text.isNullOrEmpty()) -1L else text.toString().toLong() } + vendorElements = generateVendorElements() + persistentRandomizedMacAddress = if (dialogView.persistentRandomizedMac.length() != 0) { + MacAddressCompat.fromString(dialogView.persistentRandomizedMac.text.toString()).toPlatform() + } else null + allowedAcsChannels = acsList.associate { (band, text, _) -> band to RangeInput.fromString(text.text) } + maxChannelBandwidth = dialogView.maxChannelBandwidth.selectedItemId.toInt() } } @@ -233,10 +268,28 @@ class WifiApDialogFragment : AlertDialogFragment + 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")) + } + }) + 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() { @@ -376,8 +446,49 @@ class WifiApDialogFragment : AlertDialogFragment= 33) { + try { + generateVendorElements().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 { + MacAddressCompat.fromString(dialogView.persistentRandomizedMac.text.toString()) + true + } catch (e: Exception) { + dialogView.persistentRandomizedMacWrapper.error = e.readableMessage + false + } + 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 + bridgedTimeoutError == null && vendorElementsError == null && persistentRandomizedMacValid && + acsNoError && bandwidthError == null (dialog as? AlertDialog)?.getButton(DialogInterface.BUTTON_POSITIVE)?.isEnabled = ssidLengthOk && passwordValid && bandError == null && canCopy dialogView.toolbar.menu.findItem(android.R.id.copy).isEnabled = canCopy diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/util/ConstantLookup.kt b/mobile/src/main/java/be/mygod/vpnhotspot/util/ConstantLookup.kt index 62a5237e..ed5b520f 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/util/ConstantLookup.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/util/ConstantLookup.kt @@ -9,7 +9,7 @@ import timber.log.Timber class ConstantLookup(private val prefix: String, private val lookup29: Array, private val clazz: () -> Class<*>) { - private val lookup by lazy { + val lookup by lazy { SparseArrayCompat().apply { for (field in clazz().declaredFields) try { if (field?.type == Int::class.java && field.name.startsWith(prefix)) put(field.getInt(null), field.name) diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/util/RangeInput.kt b/mobile/src/main/java/be/mygod/vpnhotspot/util/RangeInput.kt new file mode 100644 index 00000000..1029507e --- /dev/null +++ b/mobile/src/main/java/be/mygod/vpnhotspot/util/RangeInput.kt @@ -0,0 +1,41 @@ +package be.mygod.vpnhotspot.util + +object RangeInput { + fun toString(input: IntArray) = StringBuilder().apply { + if (input.isEmpty()) return@apply + input.sort() + var pending: Int? = null + var last = input[0] + append(last) + for (channel in input.asSequence().drop(1)) { + if (channel == last + 1) pending = channel else { + pending?.let { + append('-') + append(it) + pending = null + } + append(",\u200b") // zero-width space to save space + append(channel) + } + last = channel + } + pending?.let { + append('-') + append(it) + } + }.toString() + fun toString(input: Set?) = input?.run { toString(toIntArray()) } + + fun fromString(input: CharSequence?, min: Int = 1, max: Int = 999) = mutableSetOf().apply { + if (input == null) return@apply + for (unit in input.split(',')) { + if (unit.isBlank()) continue + val blocks = unit.split('-', limit = 2).map { it.trim().toInt() } + require(blocks[0] in min..max) { "Out of range: ${blocks[0]}" } + if (blocks.size == 2) { + require(blocks[1] in min..max) { "Out of range: ${blocks[1]}" } + addAll(blocks[0]..blocks[1]) + } else add(blocks[0]) + } + } +} diff --git a/mobile/src/main/res/layout/dialog_wifi_ap.xml b/mobile/src/main/res/layout/dialog_wifi_ap.xml index f5c25f06..a2db780c 100644 --- a/mobile/src/main/res/layout/dialog_wifi_ap.xml +++ b/mobile/src/main/res/layout/dialog_wifi_ap.xml @@ -54,8 +54,7 @@ android:id="@+id/ssid" android:layout_width="match_parent" android:layout_height="wrap_content" - style="@style/wifi_item_edit_content" - android:inputType="textMultiLine|textNoSuggestions" /> + style="@style/wifi_item_edit_content" /> + + + + + + + + + + + + + + + + + @@ -258,6 +356,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" style="@style/wifi_item_edit_content" + android:imeOptions="flagForceAscii" android:inputType="textNoSuggestions" android:maxLength="17" /> @@ -274,6 +373,24 @@ android:minHeight="@dimen/touch_target_min" android:entries="@array/wifi_mac_randomization" android:prompt="@string/wifi_mac_randomization" /> + + + - - - - + + + diff --git a/mobile/src/main/res/values-zh-rCN/strings.xml b/mobile/src/main/res/values-zh-rCN/strings.xml index acd36ace..de43fe29 100644 --- a/mobile/src/main/res/values-zh-rCN/strings.xml +++ b/mobile/src/main/res/values-zh-rCN/strings.xml @@ -183,19 +183,28 @@ "AP 频段" Disabled "%s GHz 频段" + 2.4 GHz ACS 可选频段 + 5 GHz ACS 可选频段 + 6 GHz ACS 可选频段 + 最大频宽 访问控制 高级接入点设置 "MAC 地址" + 持久性随机 MAC 地址 "隐藏的网络" 允许连接设备数上限 过滤可以连接的设备 设备黑名单 设备白名单 随机生成 MAC 地址 + + 持久化 + 不持久化 启用桥接模式伺机关闭 启用 Wi\u2011Fi 6 启用 Wi\u2011Fi 7 用户提供配置 + 供应商特定元素 "保存" diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml index 95056233..c2956e8e 100644 --- a/mobile/src/main/res/values/strings.xml +++ b/mobile/src/main/res/values/strings.xml @@ -207,9 +207,14 @@ AP Band Disabled %s GHz Band + Allowed 2.4 GHz ACS channels + Allowed 5 GHz ACS channels + Allowed 6 GHz ACS channels + Maximum channel bandwidth Access Control Advanced AP Options MAC address + Persistent Randomized MAC address Hidden network Maximum number of clients Control which client can use hotspot @@ -223,7 +228,8 @@ Inactive timeout for a bridged instance Enable Wi\u2011Fi 6 Enable Wi\u2011Fi 7 - User Supplied Configuration + User supplied configuration + Vendor elements Save