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
+