From 426b93226dd4acc33bad0f25603fbf7f8c200611 Mon Sep 17 00:00:00 2001 From: Mygod Date: Tue, 25 Dec 2018 11:37:36 +0800 Subject: [PATCH] Add a lot more QS tiles Fix #53. Also fix Bluetooth connection leaks. --- mobile/src/main/AndroidManifest.xml | 66 ++++++- .../mygod/vpnhotspot/RepeaterTileService.kt | 85 --------- .../be/mygod/vpnhotspot/TetheringService.kt | 14 +- .../vpnhotspot/manage/BluetoothTethering.kt | 51 +++++ .../vpnhotspot/manage/InterfaceManager.kt | 4 +- .../manage/LocalOnlyHotspotManager.kt | 6 +- .../manage/LocalOnlyHotspotTileService.kt | 54 ++++++ .../vpnhotspot/manage/RepeaterTileService.kt | 78 ++++++++ .../manage/TetherListeningTileService.kt | 29 +++ .../mygod/vpnhotspot/manage/TetherManager.kt | 48 +---- .../vpnhotspot/manage/TetheringFragment.kt | 12 +- .../vpnhotspot/manage/TetheringTileService.kt | 174 ++++++++++++++++++ .../drawable/ic_quick_settings_tile_off.xml | 13 -- mobile/src/main/res/values-v26/bools.xml | 5 + mobile/src/main/res/values/bools.xml | 5 + 15 files changed, 481 insertions(+), 163 deletions(-) delete mode 100644 mobile/src/main/java/be/mygod/vpnhotspot/RepeaterTileService.kt create mode 100644 mobile/src/main/java/be/mygod/vpnhotspot/manage/BluetoothTethering.kt create mode 100644 mobile/src/main/java/be/mygod/vpnhotspot/manage/LocalOnlyHotspotTileService.kt create mode 100644 mobile/src/main/java/be/mygod/vpnhotspot/manage/RepeaterTileService.kt create mode 100644 mobile/src/main/java/be/mygod/vpnhotspot/manage/TetherListeningTileService.kt create mode 100644 mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringTileService.kt delete mode 100644 mobile/src/main/res/drawable/ic_quick_settings_tile_off.xml create mode 100644 mobile/src/main/res/values-v26/bools.xml create mode 100644 mobile/src/main/res/values/bools.xml diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml index ca063196..33f225cc 100644 --- a/mobile/src/main/AndroidManifest.xml +++ b/mobile/src/main/AndroidManifest.xml @@ -65,21 +65,79 @@ - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + binder.shutdown() - RepeaterService.Status.IDLE -> - ContextCompat.startForegroundService(this, Intent(this, RepeaterService::class.java)) - else -> { } - } - } - - override fun onServiceConnected(name: ComponentName?, service: IBinder) { - val binder = service as RepeaterService.Binder - this.binder = binder - binder.statusChanged[this] = { updateTile() } - binder.groupChanged[this] = this::updateTile - } - - override fun onServiceDisconnected(name: ComponentName?) { - val binder = binder ?: return - this.binder = null - binder.statusChanged -= this - binder.groupChanged -= this - } - - private fun updateTile(group: WifiP2pGroup? = binder?.group) { - val qsTile = qsTile ?: return - when (binder?.service?.status) { - RepeaterService.Status.IDLE -> { - qsTile.state = Tile.STATE_INACTIVE - qsTile.icon = tileOff - qsTile.label = getString(R.string.title_repeater) - } - RepeaterService.Status.STARTING -> { - qsTile.state = Tile.STATE_UNAVAILABLE - qsTile.icon = tileOn - qsTile.label = getString(R.string.title_repeater) - } - RepeaterService.Status.ACTIVE -> { - qsTile.state = Tile.STATE_ACTIVE - qsTile.icon = tileOn - qsTile.label = group?.networkName - } - else -> { // null or DESTROYED, which should never occur - qsTile.state = Tile.STATE_UNAVAILABLE - qsTile.icon = tileOff - qsTile.label = getString(R.string.title_repeater) - } - } - qsTile.updateTile() - } -} diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/TetheringService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/TetheringService.kt index a02ae704..871a014a 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/TetheringService.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/TetheringService.kt @@ -3,26 +3,26 @@ package be.mygod.vpnhotspot import android.content.Intent import android.content.IntentFilter import be.mygod.vpnhotspot.App.Companion.app -import be.mygod.vpnhotspot.manage.TetheringFragment import be.mygod.vpnhotspot.net.Routing import be.mygod.vpnhotspot.net.TetherType import be.mygod.vpnhotspot.net.TetheringManager import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor import be.mygod.vpnhotspot.net.wifi.WifiDoubleLock +import be.mygod.vpnhotspot.util.Event0 import be.mygod.vpnhotspot.util.broadcastReceiver import be.mygod.vpnhotspot.widget.SmartSnackbar import timber.log.Timber class TetheringService : IpNeighbourMonitoringService() { companion object { - const val EXTRA_ADD_INTERFACE = "interface.add" + const val EXTRA_ADD_INTERFACES = "interface.add" const val EXTRA_REMOVE_INTERFACE = "interface.remove" } inner class Binder : android.os.Binder() { - var fragment: TetheringFragment? = null + val routingsChanged = Event0() - fun isActive(iface: String): Boolean = synchronized(routings) { routings.keys.contains(iface) } + fun isActive(iface: String): Boolean = synchronized(routings) { routings.containsKey(iface) } } private val binder = Binder() @@ -90,16 +90,16 @@ class TetheringService : IpNeighbourMonitoringService() { } updateNotification() } - app.handler.post { binder.fragment?.adapter?.notifyDataSetChanged() } + app.handler.post { binder.routingsChanged() } } override fun onBind(intent: Intent?) = binder override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { if (intent != null) { - val iface = intent.getStringExtra(EXTRA_ADD_INTERFACE) + val ifaces = intent.getStringArrayExtra(EXTRA_ADD_INTERFACES) ?: emptyArray() synchronized(routings) { - if (iface != null) { + for (iface in ifaces) { routings[iface] = null if (TetherType.ofInterface(iface).isWifi && !locked) { WifiDoubleLock.acquire() diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/BluetoothTethering.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/BluetoothTethering.kt new file mode 100644 index 00000000..420d1e30 --- /dev/null +++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/BluetoothTethering.kt @@ -0,0 +1,51 @@ +package be.mygod.vpnhotspot.manage + +import android.annotation.SuppressLint +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothProfile +import android.content.Context +import be.mygod.vpnhotspot.widget.SmartSnackbar +import timber.log.Timber + +class BluetoothTethering(context: Context) : BluetoothProfile.ServiceListener, AutoCloseable { + companion object { + /** + * PAN Profile + * From BluetoothProfile.java. + */ + private const val PAN = 5 + private val isTetheringOn by lazy @SuppressLint("PrivateApi") { + Class.forName("android.bluetooth.BluetoothPan").getDeclaredMethod("isTetheringOn") + } + } + + private val adapter = BluetoothAdapter.getDefaultAdapter() + private var pan: BluetoothProfile? = null + /** + * Based on: https://android.googlesource.com/platform/packages/apps/Settings/+/78d5efd/src/com/android/settings/TetherSettings.java + */ + val active: Boolean get() { + val pan = pan + return adapter?.state == BluetoothAdapter.STATE_ON && pan != null && isTetheringOn.invoke(pan) as Boolean + } + + init { + try { + adapter?.getProfileProxy(context, this, PAN) + } catch (e: SecurityException) { + Timber.w(e) + SmartSnackbar.make(e).show() + } + } + + override fun onServiceDisconnected(profile: Int) { + pan = null + } + override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) { + pan = proxy + } + override fun close() { + adapter?.closeProfileProxy(PAN, pan) + pan = null + } +} diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/InterfaceManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/InterfaceManager.kt index fc41c644..ff6f4aca 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/manage/InterfaceManager.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/InterfaceManager.kt @@ -25,14 +25,14 @@ class InterfaceManager(private val parent: TetheringFragment, val iface: String) if (data.active) context.startService(Intent(context, TetheringService::class.java) .putExtra(TetheringService.EXTRA_REMOVE_INTERFACE, iface)) else ContextCompat.startForegroundService(context, Intent(context, TetheringService::class.java) - .putExtra(TetheringService.EXTRA_ADD_INTERFACE, iface)) + .putExtra(TetheringService.EXTRA_ADD_INTERFACES, arrayOf(iface))) } } private inner class Data : be.mygod.vpnhotspot.manage.Data() { override val icon get() = TetherType.ofInterface(iface).icon override val title get() = iface override val text get() = addresses - override val active get() = parent.tetheringBinder?.isActive(iface) == true + override val active get() = parent.binder?.isActive(iface) == true } val addresses = parent.ifaceLookup[iface]?.formatAddresses() ?: "" diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/LocalOnlyHotspotManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/LocalOnlyHotspotManager.kt index ff6f4021..2511fe43 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/manage/LocalOnlyHotspotManager.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/LocalOnlyHotspotManager.kt @@ -54,12 +54,12 @@ class LocalOnlyHotspotManager(private val parent: TetheringFragment) : Manager() * https://android.googlesource.com/platform/frameworks/opt/net/wifi/+/53e0284/service/java/com/android/server/wifi/WifiSettingsStore.java#228 */ if (if (Build.VERSION.SDK_INT < 28) @Suppress("DEPRECATION") { - Settings.Secure.getInt(view.context.contentResolver, Settings.Secure.LOCATION_MODE, + Settings.Secure.getInt(context.contentResolver, Settings.Secure.LOCATION_MODE, Settings.Secure.LOCATION_MODE_OFF) == Settings.Secure.LOCATION_MODE_OFF } else context.getSystemService()?.isLocationEnabled != true) { - Toast.makeText(view.context, R.string.tethering_temp_hotspot_location, Toast.LENGTH_LONG).show() + Toast.makeText(context, R.string.tethering_temp_hotspot_location, Toast.LENGTH_LONG).show() try { - view.context.startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)) + context.startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)) } catch (e: ActivityNotFoundException) { Timber.w(e) } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/LocalOnlyHotspotTileService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/LocalOnlyHotspotTileService.kt new file mode 100644 index 00000000..0b0c3bb9 --- /dev/null +++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/LocalOnlyHotspotTileService.kt @@ -0,0 +1,54 @@ +package be.mygod.vpnhotspot.manage + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.graphics.drawable.Icon +import android.os.IBinder +import android.service.quicksettings.Tile +import androidx.annotation.RequiresApi +import androidx.core.content.ContextCompat +import be.mygod.vpnhotspot.LocalOnlyHotspotService +import be.mygod.vpnhotspot.R +import be.mygod.vpnhotspot.util.stopAndUnbind + +@RequiresApi(26) +class LocalOnlyHotspotTileService : TetherListeningTileService(), ServiceConnection { + private val tile by lazy { Icon.createWithResource(application, R.drawable.ic_device_wifi_tethering) } + private var binder: LocalOnlyHotspotService.Binder? = null + + override fun onStartListening() { + super.onStartListening() + bindService(Intent(this, LocalOnlyHotspotService::class.java), this, Context.BIND_AUTO_CREATE) + } + + override fun onStopListening() { + stopAndUnbind(this) + super.onStopListening() + } + + override fun onClick() { + val binder = binder + if (binder?.iface != null) binder.stop() + else ContextCompat.startForegroundService(this, Intent(this, LocalOnlyHotspotService::class.java)) + } + + override fun onServiceConnected(name: ComponentName?, service: IBinder) { + binder = service as LocalOnlyHotspotService.Binder + updateTile() + } + + override fun onServiceDisconnected(name: ComponentName?) { + binder = null + } + + override fun updateTile() { + qsTile?.run { + state = if (binder?.iface == null) Tile.STATE_INACTIVE else Tile.STATE_ACTIVE + icon = tile + label = getText(R.string.tethering_temp_hotspot) + updateTile() + } + } +} diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/RepeaterTileService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/RepeaterTileService.kt new file mode 100644 index 00000000..48dbf05c --- /dev/null +++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/RepeaterTileService.kt @@ -0,0 +1,78 @@ +package be.mygod.vpnhotspot.manage + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.graphics.drawable.Icon +import android.net.wifi.p2p.WifiP2pGroup +import android.os.IBinder +import android.service.quicksettings.Tile +import android.service.quicksettings.TileService +import androidx.annotation.RequiresApi +import androidx.core.content.ContextCompat +import be.mygod.vpnhotspot.R +import be.mygod.vpnhotspot.RepeaterService +import be.mygod.vpnhotspot.util.stopAndUnbind + +@RequiresApi(24) +class RepeaterTileService : TileService(), ServiceConnection { + private val tile by lazy { Icon.createWithResource(application, R.drawable.ic_action_settings_input_antenna) } + + private var binder: RepeaterService.Binder? = null + + override fun onStartListening() { + super.onStartListening() + if (!RepeaterService.supported) updateTile() + else bindService(Intent(this, RepeaterService::class.java), this, Context.BIND_AUTO_CREATE) + } + + override fun onStopListening() { + if (RepeaterService.supported) stopAndUnbind(this) + super.onStopListening() + } + + override fun onClick() { + val binder = binder + when (binder?.service?.status) { + RepeaterService.Status.ACTIVE -> binder.shutdown() + RepeaterService.Status.IDLE -> + ContextCompat.startForegroundService(this, Intent(this, RepeaterService::class.java)) + else -> { } + } + } + + override fun onServiceConnected(name: ComponentName?, service: IBinder) { + binder = service as RepeaterService.Binder + service.statusChanged[this] = { updateTile() } + service.groupChanged[this] = this::updateTile + } + + override fun onServiceDisconnected(name: ComponentName?) { + val binder = binder ?: return + this.binder = null + binder.statusChanged -= this + binder.groupChanged -= this + } + + private fun updateTile(group: WifiP2pGroup? = binder?.group) { + qsTile?.run { + when (binder?.service?.status) { + RepeaterService.Status.IDLE -> { + state = Tile.STATE_INACTIVE + label = getText(R.string.title_repeater) + } + RepeaterService.Status.ACTIVE -> { + state = Tile.STATE_ACTIVE + label = group?.networkName + } + else -> { // STARTING, null or DESTROYED, which should never occur + state = Tile.STATE_UNAVAILABLE + label = getText(R.string.title_repeater) + } + } + icon = tile + updateTile() + } + } +} diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetherListeningTileService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetherListeningTileService.kt new file mode 100644 index 00000000..1e29589f --- /dev/null +++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetherListeningTileService.kt @@ -0,0 +1,29 @@ +package be.mygod.vpnhotspot.manage + +import android.content.IntentFilter +import android.service.quicksettings.TileService +import androidx.annotation.RequiresApi +import be.mygod.vpnhotspot.net.TetheringManager +import be.mygod.vpnhotspot.util.broadcastReceiver + +@RequiresApi(24) +abstract class TetherListeningTileService : TileService() { + protected var tethered: List = emptyList() + + private val receiver = broadcastReceiver { _, intent -> + tethered = TetheringManager.getTetheredIfaces(intent.extras ?: return@broadcastReceiver) + updateTile() + } + + override fun onStartListening() { + super.onStartListening() + registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED)) + } + + override fun onStopListening() { + unregisterReceiver(receiver) + super.onStopListening() + } + + protected abstract fun updateTile() +} diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetherManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetherManager.kt index 877cea3c..45912478 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetherManager.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetherManager.kt @@ -1,8 +1,5 @@ package be.mygod.vpnhotspot.manage -import android.annotation.SuppressLint -import android.bluetooth.BluetoothAdapter -import android.bluetooth.BluetoothProfile import android.content.ActivityNotFoundException import android.content.Intent import android.os.Build @@ -95,8 +92,7 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(), protected abstract fun stop() override fun onTetheringStarted() = data.notifyChange() - override fun onTetheringFailed() = - SmartSnackbar.make(R.string.tethering_manage_failed).show() + override fun onTetheringFailed() = SmartSnackbar.make(R.string.tethering_manage_failed).show() override fun bindTo(viewHolder: RecyclerView.ViewHolder) { (viewHolder as ViewHolder).manager = this @@ -148,54 +144,20 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(), override fun stop() = TetheringManager.stop(TetheringManager.TETHERING_USB) } @RequiresApi(24) - class Bluetooth(parent: TetheringFragment) : TetherManager(parent), LifecycleObserver, - BluetoothProfile.ServiceListener { - companion object { - /** - * PAN Profile - * From BluetoothProfile.java. - */ - private const val PAN = 5 - private val isTetheringOn by lazy @SuppressLint("PrivateApi") { - Class.forName("android.bluetooth.BluetoothPan").getDeclaredMethod("isTetheringOn") - } - } - - private var pan: BluetoothProfile? = null + class Bluetooth(parent: TetheringFragment) : TetherManager(parent), LifecycleObserver { + private val tethering = BluetoothTethering(parent.requireContext()) init { parent.lifecycle.addObserver(this) - try { - BluetoothAdapter.getDefaultAdapter()?.getProfileProxy(parent.requireContext(), this, PAN) - } catch (e: SecurityException) { - Timber.w(e) - SmartSnackbar.make(e).show() - } } - override fun onServiceDisconnected(profile: Int) { - pan = null - } - override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) { - pan = proxy - } @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) - fun onDestroy() { - pan = null - } + fun onDestroy() = 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 - /** - * Based on: https://android.googlesource.com/platform/packages/apps/Settings/+/78d5efd/src/com/android/settings/TetherSettings.java - */ - override val isStarted: Boolean - get() { - val pan = pan - return BluetoothAdapter.getDefaultAdapter()?.state == BluetoothAdapter.STATE_ON && pan != null && - isTetheringOn.invoke(pan) as Boolean - } + override val isStarted get() = tethering.active override fun start() = TetheringManager.start(TetheringManager.TETHERING_BLUETOOTH, true, this) override fun stop() { diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringFragment.kt index cecb4aa9..48e57501 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringFragment.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringFragment.kt @@ -88,8 +88,8 @@ class TetheringFragment : Fragment(), ServiceConnection { var ifaceLookup: Map = emptyMap() var enabledTypes = emptySet() private lateinit var binding: FragmentTetheringBinding - var tetheringBinder: TetheringService.Binder? = null - val adapter = ManagerAdapter() + var binder: TetheringService.Binder? = null + private val adapter = ManagerAdapter() private val receiver = broadcastReceiver { _, intent -> val extras = intent.extras ?: return@broadcastReceiver adapter.update(TetheringManager.getTetheredIfaces(extras), @@ -130,14 +130,14 @@ class TetheringFragment : Fragment(), ServiceConnection { } override fun onServiceConnected(name: ComponentName?, service: IBinder?) { - tetheringBinder = service as TetheringService.Binder - service.fragment = this + binder = service as TetheringService.Binder + service.routingsChanged[this] = { adapter.notifyDataSetChanged() } requireContext().registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED)) } override fun onServiceDisconnected(name: ComponentName?) { - (tetheringBinder ?: return).fragment = null - tetheringBinder = null + (binder ?: return).routingsChanged -= this + binder = null requireContext().unregisterReceiver(receiver) } } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringTileService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringTileService.kt new file mode 100644 index 00000000..d8c452a9 --- /dev/null +++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringTileService.kt @@ -0,0 +1,174 @@ +package be.mygod.vpnhotspot.manage + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.graphics.drawable.Icon +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.wifi.WifiApManager +import be.mygod.vpnhotspot.util.stopAndUnbind +import be.mygod.vpnhotspot.widget.SmartSnackbar +import timber.log.Timber +import java.io.IOException +import java.lang.reflect.InvocationTargetException + +@RequiresApi(24) +sealed class TetheringTileService : TetherListeningTileService(), ServiceConnection, + TetheringManager.OnStartTetheringCallback { + 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 + protected val interested get() = tethered.filter { TetherType.ofInterface(it) == tetherType } + protected var binder: TetheringService.Binder? = null + + protected abstract fun start() + protected abstract fun stop() + + override fun onStartListening() { + bindService(Intent(this, TetheringService::class.java), this, Context.BIND_AUTO_CREATE) + super.onStartListening() + } + + override fun onStopListening() { + super.onStopListening() + stopAndUnbind(this) + } + + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + binder = service as TetheringService.Binder + service.routingsChanged[this] = { updateTile() } + } + + override fun onServiceDisconnected(name: ComponentName?) { + binder = null + } + + override fun updateTile() { + qsTile?.run { + val interested = interested + if (interested.isEmpty()) { + state = Tile.STATE_INACTIVE + icon = tileOff + } else { + state = Tile.STATE_ACTIVE + icon = if (interested.all { binder?.isActive(it) == true }) tileOn else tileOff + } + label = getText(labelString) + updateTile() + } + } + + protected inline fun safeInvoker(func: () -> Unit) = try { + func() + } catch (e: IOException) { + Timber.w(e) + Toast.makeText(this, e.localizedMessage, Toast.LENGTH_LONG).show() + } catch (e: InvocationTargetException) { + if (e.targetException !is SecurityException) Timber.w(e) + var cause: Throwable? = e + while (cause != null) { + cause = cause.cause + if (cause != null && cause !is InvocationTargetException) { + Toast.makeText(this, cause.localizedMessage, Toast.LENGTH_LONG).show() + break + } + } + } + override fun onClick() { + val interested = interested + if (interested.isEmpty()) safeInvoker { start() } else { + val inactive = interested.filter { binder?.isActive(it) != true } + if (inactive.isEmpty()) safeInvoker { stop() } + else ContextCompat.startForegroundService(this, Intent(this, TetheringService::class.java) + .putExtra(TetheringService.EXTRA_ADD_INTERFACES, inactive.toTypedArray())) + } + } + + override fun onTetheringStarted() = updateTile() + override fun onTetheringFailed() = SmartSnackbar.make(R.string.tethering_manage_failed).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.start(TetheringManager.TETHERING_WIFI, true, this) + override fun stop() = TetheringManager.stop(TetheringManager.TETHERING_WIFI) + } + class Usb : TetheringTileService() { + override val labelString get() = R.string.tethering_manage_usb + override val tetherType get() = TetherType.USB + + override fun start() = TetheringManager.start(TetheringManager.TETHERING_USB, true, this) + override fun stop() = TetheringManager.stop(TetheringManager.TETHERING_USB) + } + 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() = TetheringManager.start(TetheringManager.TETHERING_BLUETOOTH, true, this) + override fun stop() { + TetheringManager.stop(TetheringManager.TETHERING_BLUETOOTH) + Thread.sleep(1) // give others a room to breathe + onTetheringStarted() // force flush state + } + + override fun onStartListening() { + tethering = BluetoothTethering(this) + super.onStartListening() + } + override fun onStopListening() { + super.onStopListening() + tethering?.close() + tethering = null + } + + override fun updateTile() { + qsTile?.run { + if (tethering?.active == true) { + state = Tile.STATE_ACTIVE + val interested = interested + icon = if (interested.isNotEmpty() && interested.all { binder?.isActive(it) == true }) + tileOn else tileOff + } else { + state = Tile.STATE_INACTIVE + icon = tileOff + } + label = getText(labelString) + updateTile() + } + } + + override fun onClick() = if (tethering?.active == true) { + val inactive = interested.filter { binder?.isActive(it) != true } + if (inactive.isEmpty()) safeInvoker { stop() } + else ContextCompat.startForegroundService(this, Intent(this, TetheringService::class.java) + .putExtra(TetheringService.EXTRA_ADD_INTERFACES, inactive.toTypedArray())) + } else safeInvoker { start() } + } + + @Suppress("DEPRECATION") + @Deprecated("Not usable since API 26") + 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() + } +} diff --git a/mobile/src/main/res/drawable/ic_quick_settings_tile_off.xml b/mobile/src/main/res/drawable/ic_quick_settings_tile_off.xml deleted file mode 100644 index 0f8c8471..00000000 --- a/mobile/src/main/res/drawable/ic_quick_settings_tile_off.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - diff --git a/mobile/src/main/res/values-v26/bools.xml b/mobile/src/main/res/values-v26/bools.xml new file mode 100644 index 00000000..45a5d4b5 --- /dev/null +++ b/mobile/src/main/res/values-v26/bools.xml @@ -0,0 +1,5 @@ + + + true + false + diff --git a/mobile/src/main/res/values/bools.xml b/mobile/src/main/res/values/bools.xml new file mode 100644 index 00000000..b61cf4cf --- /dev/null +++ b/mobile/src/main/res/values/bools.xml @@ -0,0 +1,5 @@ + + + false + true +