Add a lot more QS tiles

Fix #53. Also fix Bluetooth connection leaks.
This commit is contained in:
Mygod
2018-12-25 11:37:36 +08:00
parent 365287202e
commit 426b93226d
15 changed files with 481 additions and 163 deletions

View File

@@ -65,21 +65,79 @@
</intent-filter>
</activity>
<service android:name=".LocalOnlyHotspotService"/>
<service
android:name=".LocalOnlyHotspotService"
android:directBootAware="true"/>
<service
android:name=".RepeaterService"
android:directBootAware="true"/>
<service android:name=".TetheringService"/>
<service
android:name=".RepeaterTileService"
android:name=".TetheringService"
android:directBootAware="true"/>
<service
android:name=".manage.RepeaterTileService"
android:directBootAware="true"
android:icon="@drawable/ic_quick_settings_tile_off"
android:icon="@drawable/ic_action_settings_input_antenna"
android:label="@string/title_repeater"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<service
android:name=".manage.LocalOnlyHotspotTileService"
android:directBootAware="true"
android:enabled="@bool/api_ge_26"
android:icon="@drawable/ic_device_wifi_tethering"
android:label="@string/tethering_temp_hotspot"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<service
android:name=".manage.TetheringTileService$Wifi"
android:directBootAware="true"
android:icon="@drawable/ic_device_wifi_tethering"
android:label="@string/tethering_manage_wifi"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<service
android:name=".manage.TetheringTileService$Usb"
android:directBootAware="true"
android:icon="@drawable/ic_device_usb"
android:label="@string/tethering_manage_usb"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<service
android:name=".manage.TetheringTileService$Bluetooth"
android:directBootAware="true"
android:icon="@drawable/ic_device_bluetooth"
android:label="@string/tethering_manage_bluetooth"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<!--suppress DeprecatedClassUsageInspection -->
<service
android:name=".manage.TetheringTileService$WifiLegacy"
android:directBootAware="true"
android:enabled="@bool/api_lt_26"
android:icon="@drawable/ic_device_wifi_tethering"
android:label="@string/tethering_manage_wifi_legacy"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<receiver
android:name=".BootReceiver"

View File

@@ -1,85 +0,0 @@
package be.mygod.vpnhotspot
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.util.stopAndUnbind
@RequiresApi(24)
class RepeaterTileService : TileService(), ServiceConnection {
private val tileOff by lazy { Icon.createWithResource(application, R.drawable.ic_quick_settings_tile_off) }
private val tileOn by lazy { Icon.createWithResource(application, R.drawable.ic_quick_settings_tile_on) }
private var binder: RepeaterService.Binder? = null
override fun onStartListening() {
super.onStartListening()
if (RepeaterService.supported) {
bindService(Intent(this, RepeaterService::class.java), this, Context.BIND_AUTO_CREATE)
}
}
override fun onStopListening() {
super.onStopListening()
if (RepeaterService.supported) stopAndUnbind(this)
}
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) {
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()
}
}

View File

@@ -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()

View File

@@ -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
}
}

View File

@@ -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() ?: ""

View File

@@ -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<LocationManager>()?.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)
}

View File

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

View File

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

View File

@@ -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<String> = 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()
}

View File

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

View File

@@ -88,8 +88,8 @@ class TetheringFragment : Fragment(), ServiceConnection {
var ifaceLookup: Map<String, NetworkInterface> = emptyMap()
var enabledTypes = emptySet<TetherType>()
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)
}
}

View File

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

View File

@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#fff"
android:pathData="M14.36,10.22 A5.33,5.33,0,1,0,14.36,13.78 L18.22,13.78 L18.22,17.33 L21.78,17.33
L21.78,13.78 L23.56,13.78 L23.56,10.22 Z M9.36,13.78 A1.78,1.78,0,1,1,11.11,12
A1.78,1.78,0,0,1,9.33,13.78 Z" />
</vector>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="api_ge_26">true</bool>
<bool name="api_lt_26">false</bool>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="api_ge_26">false</bool>
<bool name="api_lt_26">true</bool>
</resources>