Support hex-encoded SSID

This commit is contained in:
Mygod
2023-02-11 23:09:49 -05:00
parent 51672dea16
commit 71a7ac508c
14 changed files with 189 additions and 33 deletions

View File

@@ -261,8 +261,9 @@ Greylisted/blacklisted APIs or internal constants: (some constants are hardcoded
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setMaxNumberOfClients(I)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setPassphrase(Ljava/lang/String;I)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setShutdownTimeoutMillis(J)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration$Builder;->setSsid(Ljava/lang/String;)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (since API 30, prior to API 33) `Landroid/net/wifi/SoftApConfiguration$Builder;->setSsid(Ljava/lang/String;)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (since API 33) `Landroid/net/wifi/SoftApConfiguration$Builder;->setVendorElements(Ljava/util/List;)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (since API 33) `Landroid/net/wifi/SoftApConfiguration$Builder;->setWifiSsid(Landroid/net/wifi/WifiSsid;)Landroid/net/wifi/SoftApConfiguration$Builder;,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->BAND_2GHZ:I,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/SoftApConfiguration;->BAND_5GHZ:I,sdk,system-api,test-api`
* (since API 31) `Landroid/net/wifi/SoftApConfiguration;->BAND_60GHZ:I,sdk,system-api,test-api`

View File

@@ -34,6 +34,7 @@ import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.requestPersistentGroupI
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.setVendorElements
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.setWifiP2pChannels
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.startWps
import be.mygod.vpnhotspot.net.wifi.WifiSsidCompat
import be.mygod.vpnhotspot.root.RepeaterCommands
import be.mygod.vpnhotspot.root.RootManager
import be.mygod.vpnhotspot.util.*
@@ -53,6 +54,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
const val KEY_SAFE_MODE = "service.repeater.safeMode"
private const val KEY_NETWORK_NAME = "service.repeater.networkName"
private const val KEY_NETWORK_NAME_HEX = "service.repeater.networkNameHex"
private const val KEY_PASSPHRASE = "service.repeater.passphrase"
private const val KEY_OPERATING_BAND = "service.repeater.band.v4"
private const val KEY_OPERATING_CHANNEL = "service.repeater.oc.v3"
@@ -76,9 +78,16 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
@get:RequiresApi(29)
private val mNetworkName by lazy @TargetApi(29) { UnblockCentral.WifiP2pConfig_Builder_mNetworkName }
var networkName: String?
get() = app.pref.getString(KEY_NETWORK_NAME, null)
set(value) = app.pref.edit { putString(KEY_NETWORK_NAME, value) }
var networkName: WifiSsidCompat?
get() = app.pref.getString(KEY_NETWORK_NAME, null).let { legacy ->
if (legacy != null) WifiSsidCompat.fromUtf8Text(legacy).also {
app.pref.edit {
putString(KEY_NETWORK_NAME_HEX, it!!.hex)
remove(KEY_NETWORK_NAME)
}
} else WifiSsidCompat.fromHex(app.pref.getString(KEY_NETWORK_NAME_HEX, null))
}
set(value) = app.pref.edit { putString(KEY_NETWORK_NAME_HEX, value?.hex) }
var passphrase: String?
get() = app.pref.getString(KEY_PASSPHRASE, null)
set(value) = app.pref.edit { putString(KEY_PASSPHRASE, value) }
@@ -447,10 +456,10 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
setOperatingChannel()
}
if (Build.VERSION.SDK_INT >= 33) setVendorElements()
val networkName = networkName
val networkName = networkName?.toString()
val passphrase = passphrase
@SuppressLint("MissingPermission") // missing permission will simply leading to returning ERROR
if (!safeMode || networkName.isNullOrEmpty() || passphrase.isNullOrEmpty()) {
if (!safeMode || networkName == null || passphrase.isNullOrEmpty()) {
persistNextGroup = true
p2pManager.createGroup(channel, listener)
} else @TargetApi(29) {
@@ -510,7 +519,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
}
binder.group = group
if (persistNextGroup) {
networkName = group.networkName
networkName = WifiSsidCompat.fromUtf8Text(group.networkName)
passphrase = group.passphrase
persistNextGroup = false
}

View File

@@ -36,7 +36,7 @@ class LocalOnlyHotspotTileService : IpNeighbourMonitoringTileService() {
label = getText(R.string.tethering_temp_hotspot)
} else {
state = Tile.STATE_ACTIVE
label = binder.configuration?.ssid ?: getText(R.string.tethering_temp_hotspot)
label = binder.configuration?.ssid?.toString() ?: getText(R.string.tethering_temp_hotspot)
subtitleDevices { it == iface }
}
updateTile()

View File

@@ -33,6 +33,7 @@ import be.mygod.vpnhotspot.net.wifi.P2pSupplicantConfiguration
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat
import be.mygod.vpnhotspot.net.wifi.WifiApDialogFragment
import be.mygod.vpnhotspot.net.wifi.WifiApManager
import be.mygod.vpnhotspot.net.wifi.WifiSsidCompat
import be.mygod.vpnhotspot.util.ServiceForegroundConnector
import be.mygod.vpnhotspot.util.formatAddresses
import be.mygod.vpnhotspot.util.showAllowingStateLoss
@@ -215,7 +216,7 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
} else binder?.let { binder ->
val group = binder.group ?: binder.fetchPersistentGroup().let { binder.group }
if (group != null) return SoftApConfigurationCompat(
ssid = group.networkName,
ssid = WifiSsidCompat.fromUtf8Text(group.networkName),
securityType = SoftApConfiguration.SECURITY_TYPE_WPA2_PSK, // is not actually used
isAutoShutdownEnabled = RepeaterService.isAutoShutdownEnabled,
shutdownTimeoutMillis = RepeaterService.shutdownTimeoutMillis,
@@ -253,8 +254,12 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
RepeaterService.passphrase = config.passphrase
} else holder.config?.let { master ->
val binder = binder
if (binder?.group?.networkName != config.ssid || master.psk != config.passphrase ||
master.bssid != config.bssid) try {
val mayBeModified = master.psk != config.passphrase || master.bssid != config.bssid || config.ssid.run {
if (this != null) decode().let {
it == null || binder?.group?.networkName != it
} else binder?.group?.networkName != null
}
if (mayBeModified) try {
withContext(Dispatchers.Default) { master.update(config.ssid!!, config.passphrase!!, config.bssid) }
(this.binder ?: binder)?.group = null
} catch (e: Exception) {

View File

@@ -145,10 +145,9 @@ class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null) {
content.target.bssid?.let { MacAddress.fromString(it) }
}
suspend fun update(ssid: String, psk: String, bssid: MacAddress?) {
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 }

View File

@@ -5,6 +5,7 @@ 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
@@ -12,6 +13,7 @@ import androidx.annotation.RequiresApi
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
@@ -20,7 +22,7 @@ import java.lang.reflect.InvocationTargetException
@Parcelize
data class SoftApConfigurationCompat(
var ssid: String? = null,
var ssid: WifiSsidCompat? = null,
var bssid: MacAddress? = null,
var passphrase: String? = null,
var isHiddenSsid: Boolean = false,
@@ -116,8 +118,6 @@ data class SoftApConfigurationCompat(
"WPA3-OWE",
)
private val qrSanitizer = Regex("([\\\\\":;,])")
/**
* Based on:
* https://elixir.bootlin.com/linux/v5.12.8/source/net/wireless/util.c#L75
@@ -344,11 +344,15 @@ data class SoftApConfigurationCompat(
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,
WifiSsidCompat.fromUtf8Text(SSID),
BSSID?.let { MacAddress.fromString(it) },
preSharedKey,
hiddenSSID,
@@ -388,7 +392,9 @@ data class SoftApConfigurationCompat(
@RequiresApi(30)
@Suppress("UNCHECKED_CAST")
fun SoftApConfiguration.toCompat() = SoftApConfigurationCompat(
ssid,
if (Build.VERSION.SDK_INT >= 33) wifiSsid?.toCompat() else @Suppress("DEPRECATION") {
WifiSsidCompat.fromUtf8Text(ssid)
},
bssid,
passphrase,
isHiddenSsid,
@@ -483,7 +489,7 @@ data class SoftApConfigurationCompat(
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
apBand.setInt(result, when (band) {
@@ -520,7 +526,9 @@ 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)
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,
@@ -581,7 +589,6 @@ 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_WPA3_OWE_TRANSITION,
SoftApConfiguration.SECURITY_TYPE_WPA3_OWE -> { }
@@ -592,11 +599,11 @@ data class SoftApConfigurationCompat(
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;")

View File

@@ -36,6 +36,7 @@ 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 com.google.android.material.textfield.TextInputLayout
import kotlinx.parcelize.Parcelize
import timber.log.Timber
import java.text.DecimalFormat
@@ -129,6 +130,14 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
)
}
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) {
@@ -137,7 +146,7 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
(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
@@ -189,7 +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 { it.toString().toByteArray().size }
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,
@@ -293,7 +326,14 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
} else selection
}
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
@@ -334,11 +374,18 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
private fun validate() {
if (!started) return
val ssidLength = dialogView.ssid.text.toString().toByteArray().size
val ssidLengthOk = ssidLength in 1..32
dialogView.ssidWrapper.error = if (arg.p2pMode && RepeaterService.safeMode && ssidLength < 9) {
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 if (ssidLengthOk) null else " "
} else null else false to " "
}
dialogView.ssidWrapper.error = ssidError
val selectedSecurity = if (arg.p2pMode) {
SoftApConfiguration.SECURITY_TYPE_WPA2_PSK
} else dialogView.security.selectedItemPosition
@@ -466,7 +513,7 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
bridgedTimeoutError == null && vendorElementsError == null && persistentRandomizedMacValid &&
acsNoError && bandwidthError == null
(dialog as? AlertDialog)?.getButton(DialogInterface.BUTTON_POSITIVE)?.isEnabled =
ssidLengthOk && passwordValid && bandError == null && canCopy
ssidOk && passwordValid && bandError == null && canCopy
dialogView.toolbar.menu.findItem(android.R.id.copy).isEnabled = canCopy
}

View File

@@ -0,0 +1,69 @@
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)
}
@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(): Int {
return bytes.contentHashCode()
}
}

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M19,4L5,4c-1.11,0 -2,0.9 -2,2v12c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,6c0,-1.1 -0.9,-2 -2,-2zM11,11L9.5,11v-0.5h-2v3h2L9.5,13L11,13v1c0,0.55 -0.45,1 -1,1L7,15c-0.55,0 -1,-0.45 -1,-1v-4c0,-0.55 0.45,-1 1,-1h3c0.55,0 1,0.45 1,1v1zM18,11h-1.5v-0.5h-2v3h2L16.5,13L18,13v1c0,0.55 -0.45,1 -1,1h-3c-0.55,0 -1,-0.45 -1,-1v-4c0,-0.55 0.45,-1 1,-1h3c0.55,0 1,0.45 1,1v1z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M19.5,5.5v13h-15v-13h15zM19,4L5,4c-1.11,0 -2,0.9 -2,2v12c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,6c0,-1.1 -0.9,-2 -2,-2zM11,11L9.5,11v-0.5h-2v3h2L9.5,13L11,13v1c0,0.55 -0.45,1 -1,1L7,15c-0.55,0 -1,-0.45 -1,-1v-4c0,-0.55 0.45,-1 1,-1h3c0.55,0 1,0.45 1,1v1zM18,11h-1.5v-0.5h-2v3h2L16.5,13L18,13v1c0,0.55 -0.45,1 -1,1h-3c-0.55,0 -1,-0.45 -1,-1v-4c0,-0.55 0.45,-1 1,-1h3c0.55,0 1,0.45 1,1v1z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_av_closed_caption" android:state_activated="true"/>
<item android:drawable="@drawable/ic_av_closed_caption_off"/>
</selector>

View File

@@ -49,6 +49,8 @@
android:hint="@string/wifi_ssid"
app:counterEnabled="true"
app:counterMaxLength="32"
app:endIconContentDescription="@string/wifi_ssid_toggle_hex"
app:endIconDrawable="@drawable/toggle_hex"
app:errorEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/ssid"

View File

@@ -175,6 +175,7 @@
<string name="configuration_share">使用 QR 码分享</string>
<string name="configuration_rejected">Android 系统拒绝使用此配置。(详情参见日志)</string>
<string name="wifi_ssid" msgid="5519636102673067319">"网络名称"</string>
<string name="wifi_ssid_toggle_hex">切换十六进制显示</string>
<string name="wifi_security" msgid="6603611185592956936">"安全性"</string>
<string name="wifi_password" msgid="5948219759936151048">"密码"</string>
<string name="wifi_hotspot_auto_off">未连接任何设备时自动关闭 WLAN 热点</string>

View File

@@ -199,6 +199,7 @@
<string name="configuration_share">Share via QR code</string>
<string name="configuration_rejected">Android system refuses such configuration. (see logcat)</string>
<string name="wifi_ssid">Network name</string>
<string name="wifi_ssid_toggle_hex">Toggle hex display</string>
<string name="wifi_security">Security</string>
<string name="wifi_password">Password</string>
<string name="wifi_hotspot_auto_off">Turn off hotspot automatically when no devices are connected</string>