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(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) { 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() 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() } } } }