Required for reading bluetooth tethering state since May, 2020 security patch. Related: https://android-review.googlesource.com/c/platform/packages/apps/Bluetooth/+/1253150
124 lines
5.0 KiB
Kotlin
124 lines
5.0 KiB
Kotlin
package be.mygod.vpnhotspot.manage
|
|
|
|
import android.annotation.TargetApi
|
|
import android.bluetooth.BluetoothAdapter
|
|
import android.bluetooth.BluetoothProfile
|
|
import android.content.BroadcastReceiver
|
|
import android.content.Context
|
|
import android.content.Intent
|
|
import android.content.IntentFilter
|
|
import android.widget.Toast
|
|
import androidx.annotation.RequiresApi
|
|
import androidx.core.os.BuildCompat
|
|
import be.mygod.vpnhotspot.App.Companion.app
|
|
import be.mygod.vpnhotspot.net.TetheringManager
|
|
import be.mygod.vpnhotspot.util.broadcastReceiver
|
|
import be.mygod.vpnhotspot.util.readableMessage
|
|
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
|
import timber.log.Timber
|
|
import java.io.IOException
|
|
import java.lang.reflect.InvocationTargetException
|
|
|
|
class BluetoothTethering(context: Context, val stateListener: (Int) -> Unit) :
|
|
BluetoothProfile.ServiceListener, AutoCloseable {
|
|
companion object : BroadcastReceiver() {
|
|
/**
|
|
* PAN Profile
|
|
* From BluetoothProfile.java.
|
|
*/
|
|
private const val PAN = 5
|
|
private val isTetheringOn by lazy {
|
|
Class.forName("android.bluetooth.BluetoothPan").getDeclaredMethod("isTetheringOn")
|
|
}
|
|
|
|
private fun registerBluetoothStateListener(receiver: BroadcastReceiver) =
|
|
app.registerReceiver(receiver, IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED))
|
|
private val Intent.bluetoothState get() = getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)
|
|
|
|
private var pendingCallback: TetheringManager.OnStartTetheringCallback? = null
|
|
|
|
/**
|
|
* https://android.googlesource.com/platform/packages/apps/Settings/+/b1af85d/src/com/android/settings/TetherSettings.java#215
|
|
*/
|
|
@TargetApi(24)
|
|
override fun onReceive(context: Context?, intent: Intent?) {
|
|
when (intent?.bluetoothState) {
|
|
BluetoothAdapter.STATE_ON -> try {
|
|
TetheringManager.start(TetheringManager.TETHERING_BLUETOOTH, true, pendingCallback!!)
|
|
} catch (e: IOException) {
|
|
Timber.w(e)
|
|
Toast.makeText(context, e.readableMessage, Toast.LENGTH_LONG).show()
|
|
pendingCallback!!.onException()
|
|
} 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(context, cause.readableMessage, Toast.LENGTH_LONG).show()
|
|
pendingCallback!!.onException()
|
|
break
|
|
}
|
|
}
|
|
}
|
|
BluetoothAdapter.STATE_OFF, BluetoothAdapter.ERROR -> { }
|
|
else -> return // ignore transition states
|
|
}
|
|
pendingCallback = null
|
|
app.unregisterReceiver(this)
|
|
}
|
|
|
|
/**
|
|
* https://android.googlesource.com/platform/packages/apps/Settings/+/b1af85d/src/com/android/settings/TetherSettings.java#384
|
|
*/
|
|
@RequiresApi(24)
|
|
fun start(callback: TetheringManager.OnStartTetheringCallback) {
|
|
if (pendingCallback != null) return
|
|
val adapter = BluetoothAdapter.getDefaultAdapter()
|
|
if (adapter?.state == BluetoothAdapter.STATE_OFF) {
|
|
registerBluetoothStateListener(this)
|
|
pendingCallback = callback
|
|
adapter.enable()
|
|
} else TetheringManager.start(TetheringManager.TETHERING_BLUETOOTH, true, callback)
|
|
}
|
|
}
|
|
|
|
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 null
|
|
return BluetoothAdapter.getDefaultAdapter()?.state == BluetoothAdapter.STATE_ON && try {
|
|
isTetheringOn.invoke(pan) as Boolean
|
|
} catch (e: InvocationTargetException) {
|
|
if (e.cause is SecurityException && BuildCompat.isAtLeastR()) Timber.d(e) else Timber.w(e)
|
|
return null
|
|
}
|
|
}
|
|
|
|
private val receiver = broadcastReceiver { _, intent -> stateListener(intent.bluetoothState) }
|
|
|
|
init {
|
|
try {
|
|
BluetoothAdapter.getDefaultAdapter()?.getProfileProxy(context, this, PAN)
|
|
} catch (e: SecurityException) {
|
|
Timber.w(e)
|
|
SmartSnackbar.make(e).show()
|
|
}
|
|
registerBluetoothStateListener(receiver)
|
|
}
|
|
|
|
override fun onServiceDisconnected(profile: Int) {
|
|
pan = null
|
|
}
|
|
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
|
|
pan = proxy
|
|
}
|
|
override fun close() {
|
|
app.unregisterReceiver(receiver)
|
|
BluetoothAdapter.getDefaultAdapter()?.closeProfileProxy(PAN, pan)
|
|
pan = null
|
|
}
|
|
}
|