Files
vpnhotspotmod/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetherManager.kt
2021-05-29 17:50:31 -04:00

381 lines
18 KiB
Kotlin

package be.mygod.vpnhotspot.manage
import android.Manifest
import android.annotation.TargetApi
import android.content.ClipData
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Parcelable
import android.provider.Settings
import android.text.SpannableStringBuilder
import android.text.format.DateUtils
import android.view.View
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.core.net.toUri
import androidx.core.os.BuildCompat
import androidx.core.view.updatePaddingRelative
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.RecyclerView
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.MainActivity
import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.databinding.ListitemInterfaceBinding
import be.mygod.vpnhotspot.net.TetherType
import be.mygod.vpnhotspot.net.TetheringManager
import be.mygod.vpnhotspot.net.wifi.*
import be.mygod.vpnhotspot.root.WifiApCommands
import be.mygod.vpnhotspot.util.format
import be.mygod.vpnhotspot.util.joinToSpanned
import be.mygod.vpnhotspot.util.makeMacSpan
import be.mygod.vpnhotspot.util.readableMessage
import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber
import java.lang.reflect.InvocationTargetException
import java.util.*
sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
TetheringManager.StartTetheringCallback {
class ViewHolder(private val binding: ListitemInterfaceBinding) : RecyclerView.ViewHolder(binding.root),
View.OnClickListener {
init {
itemView.updatePaddingRelative(start = itemView.resources.getDimensionPixelOffset(
R.dimen.listitem_manage_tether_padding_start))
itemView.setOnClickListener(this)
}
var manager: TetherManager? = null
set(value) {
field = value!!
binding.data = value.data
}
override fun onClick(v: View?) {
val manager = manager!!
val mainActivity = manager.parent.activity as MainActivity
if (Build.VERSION.SDK_INT >= 23 && !Settings.System.canWrite(mainActivity)) try {
manager.parent.startActivity(Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS,
"package:${mainActivity.packageName}".toUri()))
return
} catch (e: RuntimeException) {
app.logEvent("manage_write_settings") { param("message", e.toString()) }
}
if (manager.isStarted) try {
manager.stop()
} catch (e: InvocationTargetException) {
if (e.targetException !is SecurityException) Timber.w(e)
manager.onException(e)
} else manager.start()
}
}
/**
* A convenient class to delegate stuff to BaseObservable.
*/
inner class Data : be.mygod.vpnhotspot.manage.Data() {
override val icon get() = tetherType.icon
override val title get() = this@TetherManager.title
override val text get() = this@TetherManager.text
override val active get() = isStarted
}
val data = Data()
abstract val title: CharSequence
abstract val tetherType: TetherType
open val isStarted get() = parent.enabledTypes.contains(tetherType)
protected open val text: CharSequence get() = baseError ?: ""
protected var baseError: String? = null
private set
protected abstract fun start()
protected abstract fun stop()
override fun onTetheringStarted() = data.notifyChange()
override fun onTetheringFailed(error: Int?) {
Timber.d("onTetheringFailed: $error")
if (Build.VERSION.SDK_INT < 30 || error != TetheringManager.TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION) {
error?.let { SmartSnackbar.make("$tetherType: ${TetheringManager.tetherErrorLookup(it)}").show() }
} else GlobalScope.launch(Dispatchers.Main.immediate) {
val context = parent.context ?: app
Toast.makeText(context, R.string.permission_missing, Toast.LENGTH_LONG).show()
ManageBar.start(context)
}
data.notifyChange()
}
override fun onException(e: Exception) {
super.onException(e)
GlobalScope.launch(Dispatchers.Main.immediate) {
val context = parent.context ?: app
Toast.makeText(context, e.readableMessage, Toast.LENGTH_LONG).show()
ManageBar.start(context)
}
}
override fun bindTo(viewHolder: RecyclerView.ViewHolder) {
(viewHolder as ViewHolder).manager = this
}
fun updateErrorMessage(errored: List<String>, lastErrors: Map<String, Int>) {
val interested = errored.filter { TetherType.ofInterface(it) == tetherType }
baseError = if (interested.isEmpty()) null else interested.joinToString("\n") { iface ->
"$iface: " + try {
TetheringManager.tetherErrorLookup(if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
TetheringManager.getLastTetherError(iface)
} else lastErrors[iface] ?: 0)
} catch (e: InvocationTargetException) {
if (Build.VERSION.SDK_INT !in 24..25 || e.cause !is SecurityException) Timber.w(e) else Timber.d(e)
e.readableMessage
}
}
data.notifyChange()
}
@RequiresApi(24)
class Wifi(parent: TetheringFragment) : TetherManager(parent), DefaultLifecycleObserver,
WifiApManager.SoftApCallbackCompat {
private var failureReason: Int? = null
private var numClients: Int? = null
private var info = emptyList<Parcelable>()
private var capability: Parcelable? = null
init {
if (Build.VERSION.SDK_INT >= 28) parent.viewLifecycleOwner.lifecycle.addObserver(this)
}
@TargetApi(28)
override fun onStart(owner: LifecycleOwner) {
WifiApCommands.registerSoftApCallback(this)
}
@TargetApi(28)
override fun onStop(owner: LifecycleOwner) {
WifiApCommands.unregisterSoftApCallback(this)
}
override fun onStateChanged(state: Int, failureReason: Int) {
if (state < 10 || state > 14) {
Timber.w(Exception("Unknown state $state, $failureReason"))
return
}
this.failureReason = if (state == 14) failureReason else null // WIFI_AP_STATE_FAILED
data.notifyChange()
}
override fun onNumClientsChanged(numClients: Int) {
this.numClients = numClients
data.notifyChange()
}
override fun onInfoChanged(info: List<Parcelable>) {
this.info = info
data.notifyChange()
}
override fun onCapabilityChanged(capability: Parcelable) {
this.capability = capability
data.notifyChange()
}
@RequiresApi(30)
override fun onBlockedClientConnecting(client: Parcelable, blockedReason: Int) {
@Suppress("NAME_SHADOWING")
val client = WifiClient(client)
val macAddress = client.macAddress
var name = macAddress.toString()
if (BuildCompat.isAtLeastS()) client.apInstanceIdentifier?.let { name += "%$it" }
val reason = WifiApManager.clientBlockLookup(blockedReason, true)
Timber.i("$name blocked from connecting: $reason ($blockedReason)")
SmartSnackbar.make(parent.getString(R.string.tethering_manage_wifi_client_blocked, name, reason)).apply {
action(R.string.tethering_manage_wifi_copy_mac) {
app.clipboard.setPrimaryClip(ClipData.newPlainText(null, macAddress.toString()))
}
}.show()
}
override val title get() = parent.getString(R.string.tethering_manage_wifi)
override val tetherType get() = TetherType.WIFI
override val type get() = VIEW_TYPE_WIFI
@TargetApi(30)
private fun formatCapability(locale: Locale) = capability?.let { parcel ->
val capability = SoftApCapability(parcel)
val numClients = numClients
val maxClients = capability.maxSupportedClients
var supportedFeatures = capability.supportedFeatures
if (BuildCompat.isAtLeastS()) for ((flag, band) in arrayOf(
SoftApCapability.SOFTAP_FEATURE_BAND_24G_SUPPORTED to SoftApConfigurationCompat.BAND_2GHZ,
SoftApCapability.SOFTAP_FEATURE_BAND_5G_SUPPORTED to SoftApConfigurationCompat.BAND_5GHZ,
SoftApCapability.SOFTAP_FEATURE_BAND_6G_SUPPORTED to SoftApConfigurationCompat.BAND_6GHZ,
SoftApCapability.SOFTAP_FEATURE_BAND_60G_SUPPORTED to SoftApConfigurationCompat.BAND_60GHZ,
)) {
if (capability.getSupportedChannelList(band).isEmpty()) continue
// reduce double reporting
supportedFeatures = supportedFeatures and flag.inv()
}
val result = parent.resources.getQuantityText(R.plurals.tethering_manage_wifi_capabilities, numClients ?: 0)
.format(locale, numClients ?: "?", maxClients, sequence {
var features = supportedFeatures
if (features != 0L) while (features != 0L) {
val bit = features.takeLowestOneBit()
yield(SoftApCapability.featureLookup(bit, true))
features = features and bit.inv()
} else yield(parent.getText(R.string.tethering_manage_wifi_no_features))
}.joinToSpanned())
if (BuildCompat.isAtLeastS()) {
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(')')
} else null
}.filterNotNull()
if (list.isNotEmpty()) result.append(parent.getText(R.string.tethering_manage_wifi_supported_channels)
.format(locale, list.joinToString("; ")))
}
result
} ?: numClients?.let { numClients ->
app.resources.getQuantityText(R.plurals.tethering_manage_wifi_clients, numClients).format(locale,
numClients)
}
override val text get() = parent.resources.configuration.locale.let { locale ->
listOfNotNull(failureReason?.let { WifiApManager.failureReasonLookup(it) }, baseError, info.run {
if (isEmpty()) null else joinToSpanned("\n") @TargetApi(30) { parcel ->
val info = SoftApInfo(parcel)
val frequency = info.frequency
val channel = SoftApConfigurationCompat.frequencyToChannel(frequency)
val bandwidth = SoftApInfo.channelWidthLookup(info.bandwidth, true)
if (BuildCompat.isAtLeastS()) {
var bssid = makeMacSpan(info.bssid.toString())
info.apInstanceIdentifier?.let { // take the fast route if possible
bssid = if (bssid is String) "$bssid%$it" else SpannableStringBuilder(bssid).append("%$it")
}
val timeout = info.autoShutdownTimeoutMillis
parent.getText(if (timeout == 0L) {
R.string.tethering_manage_wifi_info_timeout_disabled
} else R.string.tethering_manage_wifi_info_timeout_enabled).format(locale,
frequency, channel, bandwidth, bssid, info.wifiStandard,
// http://unicode.org/cldr/trac/ticket/3407
DateUtils.formatElapsedTime(timeout / 1000))
} else parent.getText(R.string.tethering_manage_wifi_info).format(locale,
frequency, channel, bandwidth)
}
}, formatCapability(locale)).joinToSpanned("\n")
}
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_WIFI, true, this)
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_WIFI, this::onException)
}
@RequiresApi(24)
class Usb(parent: TetheringFragment) : TetherManager(parent) {
override val title get() = parent.getString(R.string.tethering_manage_usb)
override val tetherType get() = TetherType.USB
override val type get() = VIEW_TYPE_USB
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_USB, true, this)
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_USB, this::onException)
}
@RequiresApi(24)
class Bluetooth(parent: TetheringFragment) : TetherManager(parent), DefaultLifecycleObserver {
private val tethering = BluetoothTethering(parent.requireContext()) { data.notifyChange() }
init {
parent.viewLifecycleOwner.lifecycle.addObserver(this)
}
fun ensureInit(context: Context) = tethering.ensureInit(context)
override fun onResume(owner: LifecycleOwner) {
if (!BuildCompat.isAtLeastS() || parent.requireContext().checkSelfPermission(
Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED) {
ensureInit(parent.requireContext())
} else if (parent.shouldShowRequestPermissionRationale(Manifest.permission.BLUETOOTH_CONNECT)) {
parent.requestBluetooth.launch(Manifest.permission.BLUETOOTH_CONNECT)
}
}
override fun onDestroy(owner: LifecycleOwner) = tethering.close()
override val title get() = parent.getString(R.string.tethering_manage_bluetooth)
override val tetherType get() = TetherType.BLUETOOTH
override val type get() = VIEW_TYPE_BLUETOOTH
override val isStarted get() = tethering.active == true
override val text get() = listOfNotNull(
if (tethering.active == null) tethering.activeFailureCause?.readableMessage else null,
baseError).joinToString("\n")
override fun start() = BluetoothTethering.start(this)
override fun stop() {
TetheringManager.stopTethering(TetheringManager.TETHERING_BLUETOOTH, this::onException)
Thread.sleep(1) // give others a room to breathe
onTetheringStarted() // force flush state
}
}
@RequiresApi(30)
class Ethernet(parent: TetheringFragment) : TetherManager(parent) {
override val title get() = parent.getString(R.string.tethering_manage_ethernet)
override val tetherType get() = TetherType.ETHERNET
override val type get() = VIEW_TYPE_ETHERNET
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_ETHERNET, true, this)
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_ETHERNET, this::onException)
}
@RequiresApi(30)
class Ncm(parent: TetheringFragment) : TetherManager(parent) {
override val title get() = parent.getString(R.string.tethering_manage_ncm)
override val tetherType get() = TetherType.NCM
override val type get() = VIEW_TYPE_NCM
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_NCM, true, this)
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_NCM, this::onException)
}
@RequiresApi(30)
class WiGig(parent: TetheringFragment) : TetherManager(parent) {
override val title get() = parent.getString(R.string.tethering_manage_wigig)
override val tetherType get() = TetherType.WIGIG
override val type get() = VIEW_TYPE_WIGIG
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_WIGIG, true, this)
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_WIGIG, this::onException)
}
@Suppress("DEPRECATION")
@Deprecated("Not usable since API 26, malfunctioning on API 25")
class WifiLegacy(parent: TetheringFragment) : TetherManager(parent) {
override val title get() = parent.getString(R.string.tethering_manage_wifi_legacy)
override val tetherType get() = TetherType.WIFI
override val type get() = VIEW_TYPE_WIFI_LEGACY
override fun start() = try {
WifiApManager.start()
} catch (e: Exception) {
onException(e)
}
override fun stop() = try {
WifiApManager.stop()
} catch (e: Exception) {
onException(e)
}
}
}