Files
vpnhotspotmod/mobile/src/main/java/be/mygod/vpnhotspot/manage/RepeaterManager.kt
2023-02-11 23:09:49 -05:00

278 lines
12 KiB
Kotlin

package be.mygod.vpnhotspot.manage
import android.Manifest
import android.content.ComponentName
import android.content.DialogInterface
import android.content.Intent
import android.content.ServiceConnection
import android.net.MacAddress
import android.net.wifi.SoftApConfiguration
import android.net.wifi.p2p.WifiP2pGroup
import android.os.Build
import android.os.Bundle
import android.os.IBinder
import android.os.Parcelable
import android.text.method.LinkMovementMethod
import android.view.WindowManager
import android.widget.EditText
import androidx.annotation.MainThread
import androidx.appcompat.app.AlertDialog
import androidx.databinding.BaseObservable
import androidx.databinding.Bindable
import androidx.fragment.app.viewModels
import androidx.lifecycle.ViewModel
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import be.mygod.vpnhotspot.AlertDialogFragment
import be.mygod.vpnhotspot.BR
import be.mygod.vpnhotspot.Empty
import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.RepeaterService
import be.mygod.vpnhotspot.databinding.ListitemRepeaterBinding
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
import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import timber.log.Timber
import java.net.NetworkInterface
import java.net.SocketException
class RepeaterManager(private val parent: TetheringFragment) : Manager(), ServiceConnection {
class ViewHolder(val binding: ListitemRepeaterBinding) : RecyclerView.ViewHolder(binding.root) {
init {
binding.addresses.movementMethod = LinkMovementMethod.getInstance()
}
}
inner class Data : BaseObservable() {
val switchEnabled: Boolean
@Bindable get() = when (binder?.service?.status) {
RepeaterService.Status.IDLE, RepeaterService.Status.ACTIVE -> true
else -> false
}
val serviceStarted: Boolean
@Bindable get() = when (binder?.service?.status) {
RepeaterService.Status.STARTING, RepeaterService.Status.ACTIVE -> true
else -> false
}
val title: CharSequence @Bindable get() {
if (Build.VERSION.SDK_INT >= 29) binder?.group?.frequency?.let {
if (it != 0) return parent.getString(R.string.repeater_channel,
it, SoftApConfigurationCompat.frequencyToChannel(it))
}
return parent.getString(R.string.title_repeater)
}
val addresses: CharSequence @Bindable get() {
return try {
NetworkInterface.getByName(p2pInterface ?: return "")?.formatAddresses() ?: ""
} catch (_: SocketException) {
""
} catch (e: Exception) {
Timber.w(e)
""
}
}
fun onStatusChanged() {
notifyPropertyChanged(BR.switchEnabled)
notifyPropertyChanged(BR.serviceStarted)
notifyPropertyChanged(BR.addresses)
}
fun onGroupChanged(group: WifiP2pGroup? = null) {
p2pInterface = group?.`interface`
if (Build.VERSION.SDK_INT >= 29) notifyPropertyChanged(BR.title)
notifyPropertyChanged(BR.addresses)
}
fun toggle() {
val binder = binder
when (binder?.service?.status) {
RepeaterService.Status.IDLE -> if (Build.VERSION.SDK_INT < 29) parent.requireContext().let { context ->
context.startForegroundService(Intent(context, RepeaterService::class.java))
} else parent.startRepeater.launch(if (Build.VERSION.SDK_INT >= 33) {
Manifest.permission.NEARBY_WIFI_DEVICES
} else Manifest.permission.ACCESS_FINE_LOCATION)
RepeaterService.Status.ACTIVE -> binder.shutdown()
else -> { }
}
}
fun wps() {
if (binder?.active == true) WpsDialogFragment().apply {
key()
}.showAllowingStateLoss(parent.parentFragmentManager)
}
}
@Parcelize
data class WpsRet(val pin: String?) : Parcelable
class WpsDialogFragment : AlertDialogFragment<Empty, WpsRet>() {
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
setTitle(R.string.repeater_wps_dialog_title)
setView(R.layout.dialog_wps)
setPositiveButton(android.R.string.ok, listener)
setNegativeButton(android.R.string.cancel, null)
setNeutralButton(R.string.repeater_wps_dialog_pbc, listener)
}
override val ret get() = WpsRet(dialog!!.findViewById<EditText>(android.R.id.edit)?.text?.toString())
override fun onCreateDialog(savedInstanceState: Bundle?) = super.onCreateDialog(savedInstanceState).apply {
window!!.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)
}
}
class ConfigHolder : ViewModel() {
var config: P2pSupplicantConfiguration? = null
}
init {
ServiceForegroundConnector(parent, this, RepeaterService::class)
AlertDialogFragment.setResultListener<WifiApDialogFragment.Arg>(parent, javaClass.name) { which, ret ->
if (which == DialogInterface.BUTTON_POSITIVE) GlobalScope.launch(Dispatchers.Main.immediate) {
updateConfiguration(ret!!.configuration)
}
}
AlertDialogFragment.setResultListener<WpsDialogFragment, WpsRet>(parent) { which, ret ->
when (which) {
DialogInterface.BUTTON_POSITIVE -> binder!!.startWps(ret!!.pin)
DialogInterface.BUTTON_NEUTRAL -> binder!!.startWps(null)
}
}
}
private var configuring = false
fun configure() {
if (configuring) return
configuring = true
parent.viewLifecycleOwner.lifecycleScope.launchWhenCreated {
getConfiguration()?.let { (config, readOnly) ->
WifiApDialogFragment().apply {
arg(WifiApDialogFragment.Arg(config, readOnly, true))
key(this@RepeaterManager.javaClass.name)
}.showAllowingStateLoss(parent.parentFragmentManager)
}
configuring = false
}
}
override val type get() = VIEW_TYPE_REPEATER
private val data = Data()
internal var binder: RepeaterService.Binder? = null
private var p2pInterface: String? = null
private val holder by parent.viewModels<ConfigHolder>()
override fun bindTo(viewHolder: RecyclerView.ViewHolder) {
(viewHolder as ViewHolder).binding.data = data
}
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
binder = service as RepeaterService.Binder
service.statusChanged[this] = data::onStatusChanged
service.groupChanged[this] = data::onGroupChanged
data.notifyChange()
}
override fun onServiceDisconnected(name: ComponentName?) {
val binder = binder ?: return
this.binder = null
binder.statusChanged -= this
binder.groupChanged -= this
data.onStatusChanged()
}
@MainThread
private suspend fun getConfiguration(): Pair<SoftApConfigurationCompat, Boolean>? {
if (RepeaterService.safeMode) {
val networkName = RepeaterService.networkName
val passphrase = RepeaterService.passphrase
if (networkName != null && passphrase != null) {
return SoftApConfigurationCompat(
ssid = networkName,
passphrase = passphrase,
securityType = SoftApConfiguration.SECURITY_TYPE_WPA2_PSK, // is not actually used
isAutoShutdownEnabled = RepeaterService.isAutoShutdownEnabled,
shutdownTimeoutMillis = RepeaterService.shutdownTimeoutMillis,
macRandomizationSetting = if (WifiApManager.p2pMacRandomizationSupported) {
SoftApConfigurationCompat.RANDOMIZATION_NON_PERSISTENT
} else SoftApConfigurationCompat.RANDOMIZATION_NONE,
vendorElements = RepeaterService.vendorElements,
).apply {
bssid = RepeaterService.deviceAddress
setChannel(RepeaterService.operatingChannel, RepeaterService.operatingBand)
} to false
}
} else binder?.let { binder ->
val group = binder.group ?: binder.fetchPersistentGroup().let { binder.group }
if (group != null) return SoftApConfigurationCompat(
ssid = WifiSsidCompat.fromUtf8Text(group.networkName),
securityType = SoftApConfiguration.SECURITY_TYPE_WPA2_PSK, // is not actually used
isAutoShutdownEnabled = RepeaterService.isAutoShutdownEnabled,
shutdownTimeoutMillis = RepeaterService.shutdownTimeoutMillis,
macRandomizationSetting = if (WifiApManager.p2pMacRandomizationSupported) {
SoftApConfigurationCompat.RANDOMIZATION_NON_PERSISTENT
} else SoftApConfigurationCompat.RANDOMIZATION_NONE,
vendorElements = RepeaterService.vendorElements,
).run {
setChannel(RepeaterService.operatingChannel)
try {
val config = P2pSupplicantConfiguration(group)
config.init(binder.obtainDeviceAddress()?.toString())
holder.config = config
passphrase = config.psk
bssid = config.bssid
this to false
} catch (e: Exception) {
if (e !is CancellationException) Timber.w(e)
passphrase = group.passphrase
try {
bssid = group.owner?.deviceAddress?.let(MacAddress::fromString)
} catch (_: IllegalArgumentException) { }
this to true
}
}
}
SmartSnackbar.make(R.string.repeater_configure_failure).show()
return null
}
private suspend fun updateConfiguration(config: SoftApConfigurationCompat) {
val (band, channel) = SoftApConfigurationCompat.requireSingleBand(config.channels)
if (RepeaterService.safeMode) {
RepeaterService.networkName = config.ssid
RepeaterService.deviceAddress = config.bssid
RepeaterService.passphrase = config.passphrase
} else holder.config?.let { master ->
val binder = binder
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) {
Timber.w(e)
SmartSnackbar.make(e).show()
}
holder.config = null
}
RepeaterService.operatingBand = band
RepeaterService.operatingChannel = channel
RepeaterService.isAutoShutdownEnabled = config.isAutoShutdownEnabled
RepeaterService.shutdownTimeoutMillis = config.shutdownTimeoutMillis
RepeaterService.vendorElements = config.vendorElements
}
}