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