Files
vpnhotspotmod/mobile/src/main/java/be/mygod/vpnhotspot/LocalOnlyHotspotService.kt
2021-10-29 23:13:52 -04:00

177 lines
7.6 KiB
Kotlin

package be.mygod.vpnhotspot
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.wifi.WifiManager
import android.os.Build
import androidx.annotation.RequiresApi
import be.mygod.vpnhotspot.net.IpNeighbour
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.toCompat
import be.mygod.vpnhotspot.net.wifi.WifiApManager
import be.mygod.vpnhotspot.net.wifi.WifiApManager.wifiApState
import be.mygod.vpnhotspot.util.Services
import be.mygod.vpnhotspot.util.StickyEvent1
import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.*
import kotlinx.parcelize.Parcelize
import timber.log.Timber
import java.net.Inet4Address
@RequiresApi(26)
class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
inner class Binder : android.os.Binder() {
/**
* null represents IDLE, "" represents CONNECTING, "something" represents CONNECTED.
*/
var iface: String? = null
set(value) {
field = value
ifaceChanged(value)
}
val ifaceChanged = StickyEvent1 { iface }
val configuration get() = if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
reservation?.wifiConfiguration?.toCompat()
} else reservation?.softApConfiguration?.toCompat()
fun stop() {
when (iface) {
null -> return // stopped
"" -> WifiApManager.cancelLocalOnlyHotspotRequest()
}
reservation?.close()
stopService()
}
}
@Parcelize
class Starter : BootReceiver.Startable {
override fun start(context: Context) {
context.startForegroundService(Intent(context, LocalOnlyHotspotService::class.java))
}
}
private val binder = Binder()
private var reservation: WifiManager.LocalOnlyHotspotReservation? = null
/**
* Writes and critical reads to routingManager should be protected with this context.
*/
private val dispatcher = newSingleThreadContext("LocalOnlyHotspotService")
override val coroutineContext = dispatcher + Job()
private var routingManager: RoutingManager? = null
private var timeoutMonitor: TetherTimeoutMonitor? = null
override val activeIfaces get() = binder.iface.let { if (it.isNullOrEmpty()) emptyList() else listOf(it) }
override fun onBind(intent: Intent?) = binder
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
BootReceiver.startIfEnabled()
if (binder.iface != null) return START_STICKY
binder.iface = ""
updateNotification() // show invisible foreground notification to avoid being killed
try {
Services.wifi.startLocalOnlyHotspot(object : WifiManager.LocalOnlyHotspotCallback() {
override fun onStarted(reservation: WifiManager.LocalOnlyHotspotReservation?) {
if (reservation == null) return onFailed(-2)
this@LocalOnlyHotspotService.reservation = reservation
val configuration = binder.configuration!!
if (Build.VERSION.SDK_INT < 30 && configuration.isAutoShutdownEnabled) {
timeoutMonitor = TetherTimeoutMonitor(configuration.shutdownTimeoutMillis, coroutineContext) {
reservation.close()
}
}
// based on: https://android.googlesource.com/platform/packages/services/Car/+/df5cd06/service/src/com/android/car/CarProjectionService.java#160
val sticky = registerReceiver(null, IntentFilter(WifiApManager.WIFI_AP_STATE_CHANGED_ACTION))!!
val apState = sticky.wifiApState
val iface = sticky.getStringExtra(WifiApManager.EXTRA_WIFI_AP_INTERFACE_NAME)
if (apState != WifiApManager.WIFI_AP_STATE_ENABLED || iface.isNullOrEmpty()) {
if (apState == WifiApManager.WIFI_AP_STATE_FAILED) {
SmartSnackbar.make(getString(R.string.tethering_temp_hotspot_failure,
WifiApManager.failureReasonLookup(sticky.getIntExtra(
WifiApManager.EXTRA_WIFI_AP_FAILURE_REASON, 0)))).show()
}
return stopService()
}
binder.iface = iface
BootReceiver.add<LocalOnlyHotspotService>(Starter())
launch {
check(routingManager == null)
routingManager = RoutingManager.LocalOnly(this@LocalOnlyHotspotService, iface).apply { start() }
IpNeighbourMonitor.registerCallback(this@LocalOnlyHotspotService)
}
}
override fun onStopped() {
Timber.d("LOHCallback.onStopped")
reservation?.close()
reservation = null
}
override fun onFailed(reason: Int) {
SmartSnackbar.make(getString(R.string.tethering_temp_hotspot_failure, when (reason) {
ERROR_NO_CHANNEL -> getString(R.string.tethering_temp_hotspot_failure_no_channel)
ERROR_GENERIC -> getString(R.string.tethering_temp_hotspot_failure_generic)
ERROR_INCOMPATIBLE_MODE -> getString(R.string.tethering_temp_hotspot_failure_incompatible_mode)
ERROR_TETHERING_DISALLOWED -> {
getString(R.string.tethering_temp_hotspot_failure_tethering_disallowed)
}
else -> getString(R.string.failure_reason_unknown, reason)
})).show()
stopService()
}
}, null)
} catch (e: IllegalStateException) {
// throws IllegalStateException if the caller attempts to start the LocalOnlyHotspot while they
// have an outstanding request.
// https://android.googlesource.com/platform/frameworks/opt/net/wifi/+/53e0284/service/java/com/android/server/wifi/WifiServiceImpl.java#1192
WifiApManager.cancelLocalOnlyHotspotRequest()
SmartSnackbar.make(e).show()
stopService()
} catch (e: SecurityException) {
SmartSnackbar.make(e).show()
stopService()
}
return START_STICKY
}
override fun onIpNeighbourAvailable(neighbours: Collection<IpNeighbour>) {
super.onIpNeighbourAvailable(neighbours)
if (Build.VERSION.SDK_INT >= 28) timeoutMonitor?.onClientsChanged(neighbours.none {
it.ip is Inet4Address && it.state == IpNeighbour.State.VALID
})
}
override fun onDestroy() {
binder.stop()
unregisterReceiver(true)
super.onDestroy()
}
private fun stopService() {
BootReceiver.delete<LocalOnlyHotspotService>()
binder.iface = null
unregisterReceiver()
ServiceNotification.stopForeground(this)
stopSelf()
}
private fun unregisterReceiver(exit: Boolean = false) {
IpNeighbourMonitor.unregisterCallback(this)
if (Build.VERSION.SDK_INT >= 28) {
timeoutMonitor?.close()
timeoutMonitor = null
}
launch {
routingManager?.stop()
routingManager = null
if (exit) {
cancel()
dispatcher.close()
}
}
}
}