Files
vpnhotspotmod/mobile/src/main/java/be/mygod/vpnhotspot/manage/BluetoothTethering.kt

152 lines
6.0 KiB
Kotlin

package be.mygod.vpnhotspot.manage
import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothProfile
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.core.content.getSystemService
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.lang.reflect.InvocationTargetException
class BluetoothTethering(context: Context, val stateListener: () -> Unit) :
BluetoothProfile.ServiceListener, AutoCloseable {
companion object : BroadcastReceiver() {
/**
* PAN Profile
*/
private const val PAN = 5
private val clazz by lazy { Class.forName("android.bluetooth.BluetoothPan") }
private val constructor by lazy {
(if (BuildCompat.isAtLeastS()) {
clazz.getDeclaredConstructor(Context::class.java, BluetoothProfile.ServiceListener::class.java,
BluetoothAdapter::class.java)
} else {
clazz.getDeclaredConstructor(Context::class.java, BluetoothProfile.ServiceListener::class.java)
}).apply {
isAccessible = true
}
}
private val isTetheringOn by lazy { clazz.getDeclaredMethod("isTetheringOn") }
private val adapter = app.getSystemService<BluetoothManager>()?.adapter
fun pan(context: Context, serviceListener: BluetoothProfile.ServiceListener) = (if (BuildCompat.isAtLeastS()) {
constructor.newInstance(context, serviceListener, adapter)
} else constructor.newInstance(context, serviceListener)) as BluetoothProfile
val BluetoothProfile.isTetheringOn get() = isTetheringOn(this) as Boolean
fun BluetoothProfile.closePan() = adapter!!.closeProfileProxy(PAN, this)
private fun registerBluetoothStateListener(receiver: BroadcastReceiver) =
app.registerReceiver(receiver, IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED))
private var pendingCallback: TetheringManager.StartTetheringCallback? = 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?.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)) {
BluetoothAdapter.STATE_ON -> {
TetheringManager.startTethering(TetheringManager.TETHERING_BLUETOOTH, true, pendingCallback!!)
}
BluetoothAdapter.STATE_OFF, BluetoothAdapter.ERROR -> { }
else -> return // ignore transition states
}
pendingCallback = null
app.unregisterReceiver(this)
}
}
private var connected = false
private var pan: BluetoothProfile? = null
private var stoppedByUser = false
var activeFailureCause: Throwable? = 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
if (!connected) return null
activeFailureCause = null
val on = adapter?.state == BluetoothAdapter.STATE_ON && try {
pan.isTetheringOn
} catch (e: InvocationTargetException) {
activeFailureCause = e.cause ?: e
if (e.cause is SecurityException && Build.VERSION.SDK_INT >= 30) Timber.d(e.readableMessage)
else Timber.w(e)
return null
}
return if (stoppedByUser) {
if (!on) stoppedByUser = false
false
} else on
}
private val receiver = broadcastReceiver { _, _ -> stateListener() }
fun ensureInit(context: Context) {
activeFailureCause = null
if (pan == null) try {
pan = pan(context, this)
} catch (e: ReflectiveOperationException) {
if (e.cause is SecurityException && BuildCompat.isAtLeastS()) Timber.d(e.readableMessage)
else Timber.w(e)
activeFailureCause = e
}
}
init {
ensureInit(context)
registerBluetoothStateListener(receiver)
}
override fun onServiceDisconnected(profile: Int) {
connected = false
stoppedByUser = false
}
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
connected = true
stateListener()
}
/**
* https://android.googlesource.com/platform/packages/apps/Settings/+/b1af85d/src/com/android/settings/TetherSettings.java#384
*/
@SuppressLint("MissingPermission")
@RequiresApi(24)
fun start(callback: TetheringManager.StartTetheringCallback) {
if (pendingCallback == null) try {
if (adapter?.state == BluetoothAdapter.STATE_OFF) {
registerBluetoothStateListener(BluetoothTethering)
pendingCallback = callback
adapter.enable()
} else TetheringManager.startTethering(TetheringManager.TETHERING_BLUETOOTH, true, callback)
} catch (e: SecurityException) {
SmartSnackbar.make(e.readableMessage).shortToast().show()
pendingCallback = null
}
}
@RequiresApi(24)
fun stop(callback: (Exception) -> Unit) {
TetheringManager.stopTethering(TetheringManager.TETHERING_BLUETOOTH, callback)
stoppedByUser = true
}
override fun close() {
app.unregisterReceiver(receiver)
pan?.closePan()
}
}