package be.mygod.vpnhotspot.manage import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.IntentFilter import android.graphics.drawable.Icon import android.os.Build import android.os.IBinder import android.service.quicksettings.Tile import android.widget.Toast import androidx.annotation.RequiresApi import androidx.core.content.ContextCompat import be.mygod.vpnhotspot.R import be.mygod.vpnhotspot.TetheringService import be.mygod.vpnhotspot.net.TetherType import be.mygod.vpnhotspot.net.TetheringManager import be.mygod.vpnhotspot.net.TetheringManager.tetheredIfaces import be.mygod.vpnhotspot.net.wifi.WifiApManager import be.mygod.vpnhotspot.util.broadcastReceiver import be.mygod.vpnhotspot.util.readableMessage import be.mygod.vpnhotspot.util.stopAndUnbind import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import timber.log.Timber import java.lang.reflect.InvocationTargetException @RequiresApi(24) sealed class TetheringTileService : IpNeighbourMonitoringTileService(), TetheringManager.StartTetheringCallback { protected val tileOff by lazy { Icon.createWithResource(application, icon) } protected val tileOn by lazy { Icon.createWithResource(application, R.drawable.ic_quick_settings_tile_on) } protected abstract val labelString: Int protected abstract val tetherType: TetherType protected open val icon get() = tetherType.icon private var tethered: List? = null protected val interested get() = tethered?.filter { TetherType.ofInterface(it) == tetherType } protected var binder: TetheringService.Binder? = null private val receiver = broadcastReceiver { _, intent -> tethered = intent.tetheredIfaces ?: return@broadcastReceiver updateTile() } protected abstract fun start() protected abstract fun stop() override fun onStartListening() { super.onStartListening() bindService(Intent(this, TetheringService::class.java), this, Context.BIND_AUTO_CREATE) // we need to initialize tethered ASAP for onClick, which is not achievable using registerTetheringEventCallback tethered = registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED)) ?.tetheredIfaces if (Build.VERSION.SDK_INT >= 30) TetherType.listener[this] = this::updateTile updateTile() } override fun onStopListening() { if (Build.VERSION.SDK_INT >= 30) TetherType.listener -= this unregisterReceiver(receiver) stopAndUnbind(this) super.onStopListening() } override fun onServiceConnected(name: ComponentName?, service: IBinder?) { binder = service as TetheringService.Binder service.routingsChanged[this] = this::updateTile super.onServiceConnected(name, service) } override fun onServiceDisconnected(name: ComponentName?) { binder?.routingsChanged?.remove(this) binder = null } override fun updateTile() { qsTile?.run { subtitle(null) val interested = interested when { interested == null -> { state = Tile.STATE_UNAVAILABLE icon = tileOff } interested.isEmpty() -> { state = Tile.STATE_INACTIVE icon = tileOff } else -> { val binder = binder ?: return state = Tile.STATE_ACTIVE icon = if (interested.all(binder::isActive)) tileOn else tileOff subtitleDevices(interested::contains) } } label = getText(labelString) updateTile() } } override fun onClick() { val interested = interested ?: return if (interested.isEmpty()) start() else { val binder = binder if (binder == null) tapPending = true else { val inactive = interested.filterNot(binder::isActive) if (inactive.isEmpty()) try { stop() } catch (e: Exception) { onException(e) } else ContextCompat.startForegroundService(this, Intent(this, TetheringService::class.java) .putExtra(TetheringService.EXTRA_ADD_INTERFACES, inactive.toTypedArray())) } } } override fun onTetheringStarted() = updateTile() override fun onTetheringFailed(error: Int?) { Timber.d("onTetheringFailed: $error") error?.let { Toast.makeText(this, TetheringManager.tetherErrorMessage(it), Toast.LENGTH_LONG).show() } updateTile() } override fun onException(e: Exception) { if (e !is InvocationTargetException || e.targetException !is SecurityException) Timber.w(e) GlobalScope.launch(Dispatchers.Main.immediate) { Toast.makeText(this@TetheringTileService, e.readableMessage, Toast.LENGTH_LONG).show() } } class Wifi : TetheringTileService() { override val labelString get() = R.string.tethering_manage_wifi override val tetherType get() = TetherType.WIFI override val icon get() = R.drawable.ic_device_wifi_tethering override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_WIFI, true, this) override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_WIFI, this::onException) } class Usb : TetheringTileService() { override val labelString get() = R.string.tethering_manage_usb override val tetherType get() = TetherType.USB override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_USB, true, this) override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_USB, this::onException) } class Bluetooth : TetheringTileService() { private var tethering: BluetoothTethering? = null override val labelString get() = R.string.tethering_manage_bluetooth override val tetherType get() = TetherType.BLUETOOTH 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 } override fun onStartListening() { tethering = BluetoothTethering(this) { updateTile() } super.onStartListening() } override fun onStopListening() { super.onStopListening() tethering?.close() tethering = null } override fun updateTile() { qsTile?.run { subtitle(null) val interested = interested if (interested == null) { state = Tile.STATE_UNAVAILABLE icon = tileOff } else when (tethering?.active) { true -> { val binder = binder ?: return state = Tile.STATE_ACTIVE icon = if (interested.isNotEmpty() && interested.all(binder::isActive)) tileOn else tileOff subtitleDevices(interested::contains) } false -> { state = Tile.STATE_INACTIVE icon = tileOff } null -> return } label = getText(labelString) updateTile() } } override fun onClick() { when (tethering?.active) { true -> { val binder = binder if (binder == null) tapPending = true else { val inactive = (interested ?: return).filterNot(binder::isActive) if (inactive.isEmpty()) try { stop() } catch (e: Exception) { onException(e) } else ContextCompat.startForegroundService(this, Intent(this, TetheringService::class.java) .putExtra(TetheringService.EXTRA_ADD_INTERFACES, inactive.toTypedArray())) } } false -> start() else -> tapPending = true } } } @RequiresApi(30) class Ethernet : TetheringTileService() { override val labelString get() = R.string.tethering_manage_ethernet override val tetherType get() = TetherType.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 : TetheringTileService() { override val labelString get() = R.string.tethering_manage_ncm override val tetherType get() = TetherType.NCM override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_NCM, true, this) override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_NCM, this::onException) } @Suppress("DEPRECATION") @Deprecated("Not usable since API 25") class WifiLegacy : TetheringTileService() { override val labelString get() = R.string.tethering_manage_wifi_legacy override val tetherType get() = TetherType.WIFI override val icon get() = R.drawable.ic_device_wifi_tethering override fun start() = WifiApManager.start() override fun stop() = WifiApManager.stop() } }