diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml
index bc131135..c16a50a7 100644
--- a/mobile/src/main/AndroidManifest.xml
+++ b/mobile/src/main/AndroidManifest.xml
@@ -52,6 +52,8 @@
+
+
Unit) :
fun start(callback: TetheringManager.StartTetheringCallback) {
if (pendingCallback != null) return
val adapter = BluetoothAdapter.getDefaultAdapter()
- if (adapter?.state == BluetoothAdapter.STATE_OFF) {
- registerBluetoothStateListener(this)
- pendingCallback = callback
- adapter.enable()
- } else TetheringManager.startTethering(TetheringManager.TETHERING_BLUETOOTH, true, callback)
+ try {
+ if (adapter?.state == BluetoothAdapter.STATE_OFF) {
+ registerBluetoothStateListener(this)
+ pendingCallback = callback
+ adapter.enable()
+ } else TetheringManager.startTethering(TetheringManager.TETHERING_BLUETOOTH, true, callback)
+ } catch (e: SecurityException) {
+ SmartSnackbar.make(e.readableMessage).shortToast().show()
+ }
}
}
@@ -94,13 +99,16 @@ class BluetoothTethering(context: Context, val stateListener: () -> Unit) :
private val receiver = broadcastReceiver { _, _ -> stateListener() }
- init {
- if (BluetoothAdapter.getDefaultAdapter() != null) try {
+ fun ensureInit(context: Context) {
+ if (pan == null && BluetoothAdapter.getDefaultAdapter() != null) try {
pan = pan(context, this)
} catch (e: InvocationTargetException) {
Timber.w(e)
activeFailureCause = e
}
+ }
+ init {
+ ensureInit(context)
registerBluetoothStateListener(receiver)
}
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 7036a4c8..36b86630 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,11 @@
package be.mygod.vpnhotspot.manage
+import android.Manifest
import android.annotation.TargetApi
import android.content.ClipData
+import android.content.Context
import android.content.Intent
+import android.content.pm.PackageManager
import android.net.MacAddress
import android.os.Build
import android.provider.Settings
@@ -10,11 +13,14 @@ import android.view.View
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.core.net.toUri
+import androidx.core.os.BuildCompat
import androidx.core.view.updatePaddingRelative
+import androidx.fragment.app.Fragment
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.RecyclerView
import be.mygod.vpnhotspot.App.Companion.app
+import be.mygod.vpnhotspot.BuildConfig
import be.mygod.vpnhotspot.MainActivity
import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.databinding.ListitemInterfaceBinding
@@ -215,12 +221,26 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
}
@RequiresApi(24)
class Bluetooth(parent: TetheringFragment) : TetherManager(parent), DefaultLifecycleObserver {
+ companion object {
+ // TODO: migrate to framework Manifest.permission when stable
+ private const val BLUETOOTH_CONNECT = "android.permission.BLUETOOTH_CONNECT"
+ }
+
private val tethering = BluetoothTethering(parent.requireContext()) { data.notifyChange() }
init {
parent.viewLifecycleOwner.lifecycle.addObserver(this)
}
+ fun ensureInit(context: Context) = tethering.ensureInit(context)
+ override fun onResume(owner: LifecycleOwner) {
+ if (!BuildCompat.isAtLeastS() || parent.requireContext().checkSelfPermission(BLUETOOTH_CONNECT) ==
+ PackageManager.PERMISSION_GRANTED) {
+ ensureInit(parent.requireContext())
+ } else if (parent.shouldShowRequestPermissionRationale(BLUETOOTH_CONNECT)) {
+ parent.requestBluetooth.launch(BLUETOOTH_CONNECT)
+ }
+ }
override fun onDestroy(owner: LifecycleOwner) = tethering.close()
override val title get() = parent.getString(R.string.tethering_manage_bluetooth)
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 ace3cc2b..7284ac35 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringFragment.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringFragment.kt
@@ -46,11 +46,12 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
internal val repeaterManager by lazy { RepeaterManager(this@TetheringFragment) }
@get:RequiresApi(26)
internal val localOnlyHotspotManager by lazy @TargetApi(26) { LocalOnlyHotspotManager(this@TetheringFragment) }
+ internal val bluetoothManager by lazy @TargetApi(24) { TetherManager.Bluetooth(this@TetheringFragment) }
@get:RequiresApi(24)
private val tetherManagers by lazy @TargetApi(24) {
listOf(TetherManager.Wifi(this@TetheringFragment),
TetherManager.Usb(this@TetheringFragment),
- TetherManager.Bluetooth(this@TetheringFragment))
+ bluetoothManager)
}
@get:RequiresApi(30)
private val tetherManagers30 by lazy @TargetApi(30) {
@@ -130,6 +131,10 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
val startLocalOnlyHotspot = registerForActivityResult(ActivityResultContracts.RequestPermission()) {
adapter.localOnlyHotspotManager.start(requireContext())
}
+ @RequiresApi(31)
+ val requestBluetooth = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
+ if (granted) adapter.bluetoothManager.ensureInit(requireContext())
+ }
var ifaceLookup: Map = emptyMap()
var enabledTypes = emptySet()
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringTileService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringTileService.kt
index 5a57f4ea..6483beea 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringTileService.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringTileService.kt
@@ -186,7 +186,11 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin
state = Tile.STATE_INACTIVE
icon = tileOff
}
- null -> return
+ null -> {
+ state = Tile.STATE_UNAVAILABLE
+ icon = tileOff
+ subtitle(tethering?.activeFailureCause?.readableMessage)
+ }
}
label = getText(labelString)
updateTile()