Support editing native Wi-Fi AP configurations

Support for repeater channel on Android 5 has been dropped because I am lazy.
This commit is contained in:
Mygod
2019-04-04 16:43:48 +08:00
parent 834498b1ff
commit 1145b0f23b
21 changed files with 488 additions and 295 deletions

View File

@@ -52,10 +52,12 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
val supported get() = p2pManager != null
var persistentSupported = false
val operatingChannel: Int get() {
val result = app.pref.getString(KEY_OPERATING_CHANNEL, null)?.toIntOrNull() ?: 0
return if (result in 1..165) result else 0
}
var operatingChannel: Int
get() {
val result = app.pref.getString(KEY_OPERATING_CHANNEL, null)?.toIntOrNull() ?: 0
return if (result in 1..165) result else 0
}
set(value) = app.pref.edit().putString(RepeaterService.KEY_OPERATING_CHANNEL, value.toString()).apply()
}
enum class Status {

View File

@@ -7,14 +7,10 @@ import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.graphics.Typeface
import android.location.LocationManager
import android.os.Build
import android.os.IBinder
import android.provider.Settings
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.style.TypefaceSpan
import android.view.View
import android.widget.Toast
import androidx.core.content.getSystemService
@@ -24,7 +20,6 @@ import be.mygod.vpnhotspot.DebugHelper
import be.mygod.vpnhotspot.LocalOnlyHotspotService
import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.databinding.ListitemInterfaceBinding
import be.mygod.vpnhotspot.net.TetherType
import be.mygod.vpnhotspot.util.ServiceForegroundConnector
import be.mygod.vpnhotspot.util.formatAddresses
import be.mygod.vpnhotspot.widget.SmartSnackbar
@@ -76,15 +71,7 @@ class LocalOnlyHotspotManager(private val parent: TetheringFragment) : Manager()
private val lookup: Map<String, NetworkInterface> get() = parent.ifaceLookup
override val icon get() = R.drawable.ic_action_perm_scan_wifi
override val title: CharSequence get() {
val configuration = binder?.configuration ?: return parent.getString(R.string.tethering_temp_hotspot)
return SpannableStringBuilder("${configuration.SSID} - ").apply {
val start = length
append(configuration.preSharedKey)
setSpan(if (Build.VERSION.SDK_INT >= 28) TypefaceSpan(Typeface.MONOSPACE) else
TypefaceSpan("monospace"), start, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
override val title: CharSequence get() = parent.getString(R.string.tethering_temp_hotspot)
override val text: CharSequence get() {
return lookup[binder?.iface ?: return ""]?.formatAddresses() ?: ""
}
@@ -99,7 +86,7 @@ class LocalOnlyHotspotManager(private val parent: TetheringFragment) : Manager()
override val type get() = VIEW_TYPE_LOCAL_ONLY_HOTSPOT
private val data = Data()
private var binder: LocalOnlyHotspotService.Binder? = null
internal var binder: LocalOnlyHotspotService.Binder? = null
override fun bindTo(viewHolder: RecyclerView.ViewHolder) {
viewHolder as ViewHolder

View File

@@ -6,6 +6,7 @@ import android.content.Intent
import android.content.ServiceConnection
import android.net.wifi.WifiConfiguration
import android.net.wifi.p2p.WifiP2pGroup
import android.os.Build
import android.os.Bundle
import android.os.IBinder
import android.os.Parcelable
@@ -21,10 +22,8 @@ import androidx.lifecycle.ViewModelProviders
import androidx.lifecycle.get
import androidx.recyclerview.widget.RecyclerView
import be.mygod.vpnhotspot.*
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.databinding.ListitemRepeaterBinding
import be.mygod.vpnhotspot.net.wifi.P2pSupplicantConfiguration
import be.mygod.vpnhotspot.net.wifi.WifiP2pDialogFragment
import be.mygod.vpnhotspot.net.wifi.configuration.*
import be.mygod.vpnhotspot.util.ServiceForegroundConnector
import be.mygod.vpnhotspot.util.formatAddresses
import be.mygod.vpnhotspot.widget.SmartSnackbar
@@ -51,7 +50,6 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
else -> false
}
val ssid @Bindable get() = binder?.group?.networkName ?: ""
val addresses: CharSequence @Bindable get() {
return try {
NetworkInterface.getByName(p2pInterface ?: return "")?.formatAddresses() ?: ""
@@ -59,12 +57,6 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
""
}
}
var oc: CharSequence
@Bindable get() {
val oc = RepeaterService.operatingChannel
return if (oc in 1..165) oc.toString() else ""
}
set(value) = app.pref.edit().putString(RepeaterService.KEY_OPERATING_CHANNEL, value.toString()).apply()
fun onStatusChanged() {
notifyPropertyChanged(BR.switchEnabled)
@@ -72,7 +64,6 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
notifyPropertyChanged(BR.addresses)
}
fun onGroupChanged(group: WifiP2pGroup? = null) {
notifyPropertyChanged(BR.ssid)
p2pInterface = group?.`interface`
notifyPropertyChanged(BR.addresses)
}
@@ -92,22 +83,6 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
fun wps() {
if (binder?.active == true) WpsDialogFragment().show(parent, TetheringFragment.REPEATER_WPS)
}
fun editConfigurations() {
val group = binder?.group
if (group != null) try {
val config = P2pSupplicantConfiguration(group, binder?.thisDevice?.deviceAddress)
holder.config = config
WifiP2pDialogFragment().withArg(WifiP2pDialogFragment.Arg(WifiConfiguration().apply {
SSID = group.networkName
preSharedKey = config.psk
})).show(parent, TetheringFragment.REPEATER_EDIT_CONFIGURATION)
return
} catch (e: RuntimeException) {
Timber.w(e)
}
SmartSnackbar.make(R.string.repeater_configure_failure).show()
}
}
@Parcelize
@@ -168,16 +143,35 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
}
}
fun onEditResult(data: Intent?) {
val master = holder.config ?: return
try {
val config = AlertDialogFragment.getRet<WifiP2pDialogFragment.Arg>(data!!).configuration
master.update(config.SSID, config.preSharedKey)
binder!!.group = null
} catch (e: Exception) {
val configuration: WifiConfiguration? get() {
val group = binder?.group
if (group != null) try {
val config = P2pSupplicantConfiguration(group, binder?.thisDevice?.deviceAddress)
holder.config = config
return newWifiApConfiguration(group.networkName, config.psk).apply {
allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK) // is not actually used
if (Build.VERSION.SDK_INT >= 23) {
apBand = AP_BAND_ANY
apChannel = RepeaterService.operatingChannel
}
}
} catch (e: RuntimeException) {
Timber.w(e)
SmartSnackbar.make(e).show()
}
holder.config = null
SmartSnackbar.make(R.string.repeater_configure_failure).show()
return null
}
fun updateConfiguration(config: WifiConfiguration) {
holder.config?.let { master ->
if (binder?.group?.networkName != config.SSID || master.psk != config.preSharedKey) try {
master.update(config.SSID, config.preSharedKey)
binder!!.group = null
} catch (e: Exception) {
Timber.w(e)
SmartSnackbar.make(e).show()
}
holder.config = null
}
RepeaterService.operatingChannel = config.apChannel
}
}

View File

@@ -1,15 +1,16 @@
package be.mygod.vpnhotspot.manage
import android.annotation.TargetApi
import android.content.ComponentName
import android.content.Intent
import android.content.IntentFilter
import android.content.ServiceConnection
import android.content.*
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.os.IBinder
import android.view.*
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
@@ -23,24 +24,30 @@ import be.mygod.vpnhotspot.net.TetherType
import be.mygod.vpnhotspot.net.TetheringManager
import be.mygod.vpnhotspot.net.TetheringManager.localOnlyTetheredIfaces
import be.mygod.vpnhotspot.net.TetheringManager.tetheredIfaces
import be.mygod.vpnhotspot.net.wifi.WifiApManager
import be.mygod.vpnhotspot.net.wifi.configuration.WifiApDialogFragment
import be.mygod.vpnhotspot.util.ServiceForegroundConnector
import be.mygod.vpnhotspot.util.broadcastReceiver
import be.mygod.vpnhotspot.util.isNotGone
import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.android.synthetic.main.activity_main.*
import timber.log.Timber
import java.lang.IllegalArgumentException
import java.lang.reflect.InvocationTargetException
import java.net.NetworkInterface
import java.net.SocketException
class TetheringFragment : Fragment(), ServiceConnection, MenuItem.OnMenuItemClickListener {
class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickListener {
companion object {
const val START_LOCAL_ONLY_HOTSPOT = 1
const val REPEATER_EDIT_CONFIGURATION = 2
const val REPEATER_WPS = 3
const val CONFIGURE_REPEATER = 2
const val CONFIGURE_AP = 4
}
inner class ManagerAdapter : ListAdapter<Manager, RecyclerView.ViewHolder>(Manager) {
internal val repeaterManager by lazy { RepeaterManager(this@TetheringFragment) }
private val localOnlyHotspotManager by lazy @TargetApi(26) { LocalOnlyHotspotManager(this@TetheringFragment) }
internal val localOnlyHotspotManager by lazy @TargetApi(26) { LocalOnlyHotspotManager(this@TetheringFragment) }
private val tetherManagers by lazy @TargetApi(24) {
listOf(TetherManager.Wifi(this@TetheringFragment),
TetherManager.Usb(this@TetheringFragment),
@@ -100,13 +107,47 @@ class TetheringFragment : Fragment(), ServiceConnection, MenuItem.OnMenuItemClic
item.isNotGone = canMonitor.isNotEmpty()
item.subMenu.apply {
clear()
canMonitor.sorted().forEach { add(it).setOnMenuItemClickListener(this@TetheringFragment) }
for (iface in canMonitor.sorted()) add(iface).setOnMenuItemClickListener {
ContextCompat.startForegroundService(requireContext(), Intent(context, TetheringService::class.java)
.putExtra(TetheringService.EXTRA_ADD_INTERFACE_MONITOR, iface))
true
}
}
}
override fun onMenuItemClick(item: MenuItem?): Boolean {
ContextCompat.startForegroundService(requireContext(), Intent(context, TetheringService::class.java)
.putExtra(TetheringService.EXTRA_ADD_INTERFACE_MONITOR, item?.title ?: return false))
return true
return when (item?.itemId) {
R.id.configuration -> item.subMenu.run {
findItem(R.id.configuration_repeater).isNotGone = RepeaterService.supported
findItem(R.id.configuration_temp_hotspot).isNotGone =
adapter.localOnlyHotspotManager.binder?.configuration != null
true
}
R.id.configuration_repeater -> {
WifiApDialogFragment().withArg(WifiApDialogFragment.Arg(
adapter.repeaterManager.configuration ?: return false,
p2pMode = true
)).show(this, CONFIGURE_REPEATER)
true
}
R.id.configuration_temp_hotspot -> {
WifiApDialogFragment().withArg(WifiApDialogFragment.Arg(
adapter.localOnlyHotspotManager.binder?.configuration ?: return false,
readOnly = true
)).show(this, 0) // read-only, no callback needed
true
}
R.id.configuration_ap -> try {
WifiApDialogFragment().withArg(WifiApDialogFragment.Arg(
WifiApManager.configuration
)).show(this, CONFIGURE_AP)
true
} catch (e: InvocationTargetException) {
if (e.targetException !is SecurityException) Timber.w(e)
SmartSnackbar.make(e.targetException).show()
false
}
else -> false
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@@ -117,13 +158,19 @@ class TetheringFragment : Fragment(), ServiceConnection, MenuItem.OnMenuItemClic
binding.interfaces.adapter = adapter
adapter.update(emptyList(), emptyList(), emptyList())
ServiceForegroundConnector(this, this, TetheringService::class)
requireActivity().toolbar.inflateMenu(R.menu.toolbar_tethering)
requireActivity().toolbar.apply {
inflateMenu(R.menu.toolbar_tethering)
setOnMenuItemClickListener(this@TetheringFragment)
}
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
requireActivity().toolbar.menu.clear()
requireActivity().toolbar.apply {
menu.clear()
setOnMenuItemClickListener(null)
}
}
override fun onResume() {
@@ -131,10 +178,20 @@ class TetheringFragment : Fragment(), ServiceConnection, MenuItem.OnMenuItemClic
if (Build.VERSION.SDK_INT >= 27) ManageBar.Data.notifyChange()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) = when (requestCode) {
REPEATER_WPS -> adapter.repeaterManager.onWpsResult(resultCode, data)
REPEATER_EDIT_CONFIGURATION -> adapter.repeaterManager.onEditResult(data)
else -> super.onActivityResult(requestCode, resultCode, data)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
val configuration by lazy { AlertDialogFragment.getRet<WifiApDialogFragment.Arg>(data!!).configuration }
when (requestCode) {
REPEATER_WPS -> adapter.repeaterManager.onWpsResult(resultCode, data)
CONFIGURE_REPEATER -> if (resultCode == DialogInterface.BUTTON_POSITIVE) {
adapter.repeaterManager.updateConfiguration(configuration)
}
CONFIGURE_AP -> if (resultCode == DialogInterface.BUTTON_POSITIVE) try {
WifiApManager.configuration = configuration
} catch (e: IllegalArgumentException) {
SmartSnackbar.make(R.string.configuration_rejected).show()
}
else -> super.onActivityResult(requestCode, resultCode, data)
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {

View File

@@ -3,15 +3,23 @@ package be.mygod.vpnhotspot.net.wifi
import android.net.wifi.WifiConfiguration
import android.net.wifi.WifiManager
import be.mygod.vpnhotspot.App.Companion.app
import java.lang.IllegalArgumentException
/**
* 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/
*/
object WifiApManager {
private val setWifiApEnabled = WifiManager::class.java.getDeclaredMethod("setWifiApEnabled",
WifiConfiguration::class.java, Boolean::class.java)
private val getWifiApConfiguration by lazy { WifiManager::class.java.getDeclaredMethod("getWifiApConfiguration") }
private val setWifiApConfiguration by lazy {
WifiManager::class.java.getDeclaredMethod("setWifiApConfiguration", WifiConfiguration::class.java)
}
var configuration: WifiConfiguration
get() = getWifiApConfiguration.invoke(app.wifi) as WifiConfiguration
set(value) {
if (setWifiApConfiguration.invoke(app.wifi, value) as? Boolean != true) throw IllegalArgumentException()
}
private val setWifiApEnabled by lazy {
WifiManager::class.java.getDeclaredMethod("setWifiApEnabled",
WifiConfiguration::class.java, Boolean::class.java)
}
/**
* Start AccessPoint mode with the specified
* configuration. If the radio is already running in
@@ -25,6 +33,11 @@ object WifiApManager {
private fun WifiManager.setWifiApEnabled(wifiConfig: WifiConfiguration?, enabled: Boolean) =
setWifiApEnabled.invoke(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/
*/
@Deprecated("Not usable since API 26, malfunctioning on API 25")
fun start(wifiConfig: WifiConfiguration? = null) {
app.wifi.isWifiEnabled = false

View File

@@ -1,74 +0,0 @@
package be.mygod.vpnhotspot.net.wifi
import android.content.DialogInterface
import android.net.wifi.WifiConfiguration
import android.net.wifi.WifiConfiguration.AuthAlgorithm
import android.os.Parcelable
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import android.widget.EditText
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import be.mygod.vpnhotspot.AlertDialogFragment
import be.mygod.vpnhotspot.R
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.dialog_wifi_ap.view.*
import java.nio.charset.Charset
/**
* Based on: https://android.googlesource.com/platform/packages/apps/Settings/+/39b4674/src/com/android/settings/wifi/WifiApDialog.java
*
* This dialog has been deprecated in API 28, but we are still using it since it works better for our purposes.
* Related: https://android.googlesource.com/platform/packages/apps/Settings/+/defb1183ecb00d6231bac7d934d07f58f90261ea
*/
class WifiP2pDialogFragment : AlertDialogFragment<WifiP2pDialogFragment.Arg, WifiP2pDialogFragment.Arg>(), TextWatcher {
@Parcelize
data class Arg(val configuration: WifiConfiguration) : Parcelable
private lateinit var mView: View
private lateinit var mSsid: TextView
private lateinit var mPassword: EditText
override val ret: Arg? get() {
val config = WifiConfiguration()
config.SSID = mSsid.text.toString()
config.allowedAuthAlgorithms.set(AuthAlgorithm.OPEN)
if (mPassword.length() != 0) {
val password = mPassword.text.toString()
config.preSharedKey = password
}
return Arg(config)
}
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
mView = requireActivity().layoutInflater.inflate(R.layout.dialog_wifi_ap, null)
setView(mView)
setTitle(R.string.repeater_configure)
mSsid = mView.ssid
mPassword = mView.password
setPositiveButton(context.getString(R.string.wifi_save), listener)
setNegativeButton(context.getString(R.string.wifi_cancel), null)
mSsid.text = arg.configuration.SSID
mSsid.addTextChangedListener(this@WifiP2pDialogFragment)
mPassword.setText(arg.configuration.preSharedKey)
mPassword.addTextChangedListener(this@WifiP2pDialogFragment)
}
override fun onStart() {
super.onStart()
validate()
}
private fun validate() {
val mSsidString = mSsid.text.toString()
val ssidValid = mSsid.length() != 0 && Charset.forName("UTF-8").encode(mSsidString).limit() <= 32
val passwordValid = mPassword.length() >= 8
mView.password_wrapper.error =
if (passwordValid) null else requireContext().getString(R.string.credentials_password_too_short)
(dialog as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE).isEnabled = ssidValid && passwordValid
}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { }
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { }
override fun afterTextChanged(editable: Editable) = validate()
}

View File

@@ -1,4 +1,4 @@
package be.mygod.vpnhotspot.net.wifi
package be.mygod.vpnhotspot.net.wifi.configuration
import android.net.wifi.p2p.WifiP2pGroup
import android.os.Build

View File

@@ -0,0 +1,149 @@
package be.mygod.vpnhotspot.net.wifi.configuration
import android.annotation.TargetApi
import android.content.DialogInterface
import android.net.wifi.WifiConfiguration
import android.net.wifi.WifiConfiguration.AuthAlgorithm
import android.os.Build
import android.os.Parcelable
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isGone
import be.mygod.vpnhotspot.AlertDialogFragment
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.R
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.dialog_wifi_ap.view.*
import java.lang.IllegalStateException
import java.nio.charset.Charset
/**
* Based on: https://android.googlesource.com/platform/packages/apps/Settings/+/39b4674/src/com/android/settings/wifi/WifiApDialog.java
*
* This dialog has been deprecated in API 28, but we are still using it since it works better for our purposes.
* Related: https://android.googlesource.com/platform/packages/apps/Settings/+/defb1183ecb00d6231bac7d934d07f58f90261ea
*/
class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiApDialogFragment.Arg>(), TextWatcher {
@Parcelize
data class Arg(val configuration: WifiConfiguration,
val readOnly: Boolean = false,
/**
* KeyMgmt is enforced to WPA_PSK.
* Various values for apBand are allowed according to different rules.
*/
val p2pMode: Boolean = false) : Parcelable
@TargetApi(23)
private sealed class BandOption {
open val apBand get() = AP_BAND_2GHZ
open val apChannel get() = 0
object BandAny : BandOption() {
override val apBand get() = AP_BAND_ANY
override fun toString() = app.getString(R.string.wifi_ap_choose_auto)
}
object Band2GHz : BandOption() {
override fun toString() = app.getString(R.string.wifi_ap_choose_2G)
}
object Band5GHz : BandOption() {
override val apBand get() = AP_BAND_5GHZ
override fun toString() = app.getString(R.string.wifi_ap_choose_5G)
}
class Channel(override val apChannel: Int) : BandOption() {
override fun toString() = "${channelToFrequency(apChannel)} MHz ($apChannel)"
}
}
private lateinit var dialogView: View
override val ret: Arg? get() {
return Arg(WifiConfiguration().apply {
SSID = dialogView.ssid.text.toString()
allowedKeyManagement.set(
if (arg.p2pMode) WifiConfiguration.KeyMgmt.WPA_PSK else dialogView.security.selectedItemPosition)
allowedAuthAlgorithms.set(AuthAlgorithm.OPEN)
if (dialogView.password.length() != 0) preSharedKey = dialogView.password.text.toString()
if (Build.VERSION.SDK_INT >= 23) {
val bandOption = dialogView.band.selectedItem as BandOption
apBand = bandOption.apBand
apChannel = bandOption.apChannel
}
})
}
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
val activity = requireActivity()
dialogView = activity.layoutInflater.inflate(R.layout.dialog_wifi_ap, null)
setView(dialogView)
setTitle(R.string.configuration_view)
if (!arg.readOnly) setPositiveButton(R.string.wifi_save, listener)
setNegativeButton(R.string.donations__button_close, null)
dialogView.ssid.setText(arg.configuration.SSID)
if (!arg.readOnly) dialogView.ssid.addTextChangedListener(this@WifiApDialogFragment)
if (arg.p2pMode) dialogView.security_wrapper.isGone = true else dialogView.security.apply {
adapter = ArrayAdapter(activity, android.R.layout.simple_spinner_item, 0,
WifiConfiguration.KeyMgmt.strings).apply {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
}
onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onNothingSelected(parent: AdapterView<*>?) =
throw IllegalStateException("Must select something")
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
dialogView.password_wrapper.isGone = position == WifiConfiguration.KeyMgmt.NONE
}
}
val selected = arg.configuration.allowedKeyManagement.nextSetBit(0)
check(selected >= 0) { "No key management selected" }
check(arg.configuration.allowedKeyManagement.nextSetBit(selected + 1) < 0) {
"More than 1 key managements supplied"
}
setSelection(selected)
}
dialogView.password.setText(arg.configuration.preSharedKey)
if (!arg.readOnly) dialogView.password.addTextChangedListener(this@WifiApDialogFragment)
if (Build.VERSION.SDK_INT >= 23) dialogView.band.apply {
val options = mutableListOf<BandOption>().apply {
if (arg.p2pMode) add(BandOption.BandAny) else {
if (Build.VERSION.SDK_INT >= 28) add(BandOption.BandAny)
add(BandOption.Band2GHz)
add(BandOption.Band5GHz)
}
addAll((1..165).map { BandOption.Channel(it) })
}
adapter = ArrayAdapter(activity, android.R.layout.simple_spinner_item, 0, options).apply {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
}
setSelection(if (arg.configuration.apChannel in 1..165) {
options.indexOfFirst { it.apChannel == arg.configuration.apChannel }
} else options.indexOfFirst { it.apBand == arg.configuration.apBand })
} else dialogView.band_wrapper.isGone = true
}
override fun onResume() {
super.onResume()
if (!arg.readOnly) validate()
}
/**
* This function is reached only if not arg.readOnly.
*/
private fun validate() {
val ssidValid = dialogView.ssid.length() != 0 &&
Charset.forName("UTF-8").encode(dialogView.ssid.text.toString()).limit() <= 32
val passwordValid = when (dialogView.security.selectedItemPosition) {
WifiConfiguration.KeyMgmt.WPA_PSK, WPA2_PSK -> dialogView.password.length() >= 8
else -> true // do not try to validate
}
dialogView.password_wrapper.error = if (passwordValid) null else {
requireContext().getString(R.string.credentials_password_too_short)
}
(dialog as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE).isEnabled = ssidValid && passwordValid
}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { }
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { }
override fun afterTextChanged(editable: Editable) = validate()
}

View File

@@ -0,0 +1,84 @@
package be.mygod.vpnhotspot.net.wifi.configuration
import android.net.wifi.WifiConfiguration
import androidx.annotation.RequiresApi
import be.mygod.vpnhotspot.net.wifi.WifiApManager
import timber.log.Timber
import java.lang.reflect.InvocationTargetException
val WPA2_PSK = WifiConfiguration.KeyMgmt.strings.indexOf("WPA2_PSK")
/**
* apBand and apChannel is available since API 23.
*
* https://android.googlesource.com/platform/frameworks/base/+/android-6.0.0_r1/wifi/java/android/net/wifi/WifiConfiguration.java#242
*/
private val apBandField by lazy { WifiConfiguration::class.java.getDeclaredField("apBand") }
private val apChannelField by lazy { WifiConfiguration::class.java.getDeclaredField("apChannel") }
/**
* 2GHz band.
*
* https://android.googlesource.com/platform/frameworks/base/+/android-7.0.0_r1/wifi/java/android/net/wifi/WifiConfiguration.java#241
*/
@RequiresApi(23)
const val AP_BAND_2GHZ = 0
/**
* 5GHz band.
*/
@RequiresApi(23)
const val AP_BAND_5GHZ = 1
/**
* Device is allowed to choose the optimal band (2Ghz or 5Ghz) based on device capability,
* operating country code and current radio conditions.
*
* Introduced in 9.0, but we will abuse this constant anyway.
* https://android.googlesource.com/platform/frameworks/base/+/android-9.0.0_r1/wifi/java/android/net/wifi/WifiConfiguration.java#295
*/
@RequiresApi(23)
const val AP_BAND_ANY = -1
/**
* The band which AP resides on
* -1:Any 0:2G 1:5G
* By default, 2G is chosen
*/
var WifiConfiguration.apBand: Int
@RequiresApi(23) get() = apBandField.get(this) as Int
@RequiresApi(23) set(value) = apBandField.set(this, value)
/**
* The channel which AP resides on
* 2G 1-11
* 5G 36,40,44,48,149,153,157,161,165
* 0 - find a random available channel according to the apBand
*/
var WifiConfiguration.apChannel: Int
@RequiresApi(23) get() = apChannelField.get(this) as Int
@RequiresApi(23) set(value) = apChannelField.set(this, value)
/**
* The frequency which AP resides on (MHz). Resides in range [2412, 5815].
*/
fun channelToFrequency(channel: Int) = when (channel) {
in 1..14 -> 2407 + 5 * channel
in 15..165 -> 5000 + 5 * channel
else -> throw IllegalArgumentException("Invalid channel $channel")
}
/**
* Based on:
* https://android.googlesource.com/platform/packages/apps/Settings/+/android-5.0.0_r1/src/com/android/settings/wifi/WifiApDialog.java#88
* https://android.googlesource.com/platform/packages/apps/Settings/+/b1af85d/src/com/android/settings/wifi/tether/WifiTetherSettings.java#162
*/
fun newWifiApConfiguration(ssid: String, passphrase: String?) = try {
WifiApManager.configuration
} catch (e: InvocationTargetException) {
if (e.targetException !is SecurityException) Timber.w(e)
WifiConfiguration()
}.apply {
SSID = ssid
preSharedKey = passphrase
allowedKeyManagement.clear()
allowedAuthAlgorithms.clear()
allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN)
}

View File

@@ -1,8 +1,8 @@
package be.mygod.vpnhotspot.room
import android.os.Parcel
import android.text.TextUtils
import androidx.room.TypeConverter
import be.mygod.vpnhotspot.util.useParcel
import java.net.InetAddress
import java.nio.ByteBuffer
import java.nio.ByteOrder
@@ -10,27 +10,17 @@ import java.nio.ByteOrder
object Converters {
@JvmStatic
@TypeConverter
fun persistCharSequence(cs: CharSequence): ByteArray {
val p = Parcel.obtain()
try {
TextUtils.writeToParcel(cs, p, 0)
return p.marshall()
} finally {
p.recycle()
}
fun persistCharSequence(cs: CharSequence) = useParcel { p ->
TextUtils.writeToParcel(cs, p, 0)
p.marshall()
}
@JvmStatic
@TypeConverter
fun unpersistCharSequence(data: ByteArray): CharSequence {
val p = Parcel.obtain()
try {
p.unmarshall(data, 0, data.size)
p.setDataPosition(0)
return TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(p)
} finally {
p.recycle()
}
fun unpersistCharSequence(data: ByteArray) = useParcel { p ->
p.unmarshall(data, 0, data.size)
p.setDataPosition(0)
TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(p)
}
@JvmStatic

View File

@@ -1,7 +1,9 @@
package be.mygod.vpnhotspot.util
import android.annotation.SuppressLint
import android.content.*
import android.os.Build
import android.os.Parcel
import android.text.Spannable
import android.text.SpannableString
import android.text.SpannableStringBuilder
@@ -15,7 +17,6 @@ import androidx.databinding.BindingAdapter
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.room.macToString
import be.mygod.vpnhotspot.widget.SmartSnackbar
import java.lang.RuntimeException
import java.net.InetAddress
import java.net.NetworkInterface
import java.net.SocketException
@@ -32,6 +33,15 @@ fun Long.toPluralInt(): Int {
return (this % 1000000000).toInt() + 1000000000
}
@SuppressLint("Recycle")
fun <T> useParcel(block: (Parcel) -> T) = Parcel.obtain().run {
try {
block(this)
} finally {
recycle()
}
}
fun broadcastReceiver(receiver: (Context, Intent) -> Unit) = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) = receiver(context, intent)
}