Support hex-encoded SSID
This commit is contained in:
@@ -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`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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;")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
5
mobile/src/main/res/drawable/ic_av_closed_caption.xml
Normal file
5
mobile/src/main/res/drawable/ic_av_closed_caption.xml
Normal 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>
|
||||
@@ -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>
|
||||
5
mobile/src/main/res/drawable/toggle_hex.xml
Normal file
5
mobile/src/main/res/drawable/toggle_hex.xml
Normal 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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user