combov2: Ask for Bluetooth permissions before pairing
This commit is contained in:
parent
d280a2bec5
commit
a0e65370dc
|
@ -1,11 +1,17 @@
|
|||
package info.nightscout.pump.combov2.activities
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.View
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
|
@ -20,42 +26,65 @@ import info.nightscout.pump.combov2.databinding.Combov2PairingActivityBinding
|
|||
import info.nightscout.rx.logging.AAPSLogger
|
||||
import info.nightscout.rx.logging.LTag
|
||||
import info.nightscout.shared.interfaces.ResourceHelper
|
||||
import kotlinx.coroutines.CompletableJob
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import javax.inject.Inject
|
||||
|
||||
// A counterpart to BlePreCheckImpl that is designed for coroutines.
|
||||
private class BluetoothPermissionChecks(
|
||||
private val activity: ComponentActivity,
|
||||
private val permissions: List<String>,
|
||||
private val aapsLogger: AAPSLogger
|
||||
) {
|
||||
private val activityResultLauncher: ActivityResultLauncher<Array<String>>
|
||||
private var waitForCompletion: CompletableJob? = null
|
||||
|
||||
init {
|
||||
activityResultLauncher = activity.registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {
|
||||
waitForCompletion?.complete()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun requestAndCheck() {
|
||||
val missingPermissions = permissions
|
||||
.filter {
|
||||
ContextCompat.checkSelfPermission(activity, it) != PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
.toTypedArray()
|
||||
|
||||
if (missingPermissions.isEmpty())
|
||||
return
|
||||
|
||||
aapsLogger.debug(LTag.PUMP, "Missing permissions: " + missingPermissions.joinToString(", "))
|
||||
|
||||
waitForCompletion = Job()
|
||||
activityResultLauncher.launch(missingPermissions)
|
||||
waitForCompletion?.join()
|
||||
waitForCompletion = null
|
||||
}
|
||||
|
||||
fun unregister() {
|
||||
activityResultLauncher.unregister()
|
||||
}
|
||||
}
|
||||
|
||||
class ComboV2PairingActivity : DaggerAppCompatActivity() {
|
||||
|
||||
@Inject lateinit var aapsLogger: AAPSLogger
|
||||
@Inject lateinit var rh: ResourceHelper
|
||||
@Inject lateinit var combov2Plugin: ComboV2Plugin
|
||||
|
||||
private var uiInitialized = false
|
||||
private var unregisterActivityLauncher = {}
|
||||
private var bluetoothPermissionChecks: BluetoothPermissionChecks? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val binding: Combov2PairingActivityBinding = DataBindingUtil.setContentView(
|
||||
this, R.layout.combov2_pairing_activity)
|
||||
|
||||
// In the NotInitialized state, the PumpManager is unavailable because it cannot
|
||||
// function without Bluetooth permissions. Several of ComboV2Plugin's functions
|
||||
// such as getPairingProgressFlow() depend on PumpManager though. To prevent UI
|
||||
// controls from becoming active without having a PumpManager, show instead a
|
||||
// view on the activity that explains why pairing is currently not possible.
|
||||
if (combov2Plugin.driverStateUIFlow.value == ComboV2Plugin.DriverState.NotInitialized) {
|
||||
aapsLogger.info(LTag.PUMP, "Cannot pair right now; disabling pairing UI controls, showing message instead")
|
||||
|
||||
binding.combov2PairingSectionInitial.visibility = View.GONE
|
||||
binding.combov2PairingSectionCannotPairDriverNotInitialized.visibility = View.VISIBLE
|
||||
|
||||
binding.combov2CannotPairGoBack.setOnClickListener {
|
||||
finish()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Install an activity result caller for when the user presses
|
||||
// "deny" or "reject" in the dialog that pops up when Android
|
||||
// asks for permission to enable device discovery. In such a
|
||||
|
@ -74,7 +103,131 @@ class ComboV2PairingActivity : DaggerAppCompatActivity() {
|
|||
combov2Plugin.customDiscoveryActivityStartCallback = { intent ->
|
||||
startPairingActivityLauncher.launch(intent)
|
||||
}
|
||||
unregisterActivityLauncher = {
|
||||
startPairingActivityLauncher.unregister()
|
||||
}
|
||||
|
||||
val binding: Combov2PairingActivityBinding = DataBindingUtil.setContentView(
|
||||
this, R.layout.combov2_pairing_activity)
|
||||
|
||||
val thisActivity = this
|
||||
|
||||
// Set the pairing sections to initially show the "not initialized" one
|
||||
// in case the Bluetooth permissions haven't been granted yet by the user.
|
||||
binding.combov2PairingSectionInitial.visibility = View.GONE
|
||||
binding.combov2PairingSectionCannotPairDriverNotInitialized.visibility = View.VISIBLE
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
// Launch the BluetoothPermissionChecks in the CREATED lifecycle state.
|
||||
// This is important, because registering an activity (which the
|
||||
// BluetoothPermissionChecks class does) must take place _before_ the
|
||||
// STARTED state is reached.
|
||||
lifecycleScope.launch {
|
||||
lifecycle.repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||
aapsLogger.debug(LTag.PUMP, "Creating and registering BT permissions check object")
|
||||
bluetoothPermissionChecks = BluetoothPermissionChecks(
|
||||
thisActivity,
|
||||
listOf(
|
||||
Manifest.permission.BLUETOOTH_SCAN,
|
||||
Manifest.permission.BLUETOOTH_CONNECT
|
||||
),
|
||||
aapsLogger
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Unregister any activity that BluetoothPermissionChecks previously
|
||||
// registered if this pairing activity is getting destroyed.
|
||||
lifecycleScope.launch {
|
||||
lifecycle.repeatOnLifecycle(Lifecycle.State.DESTROYED) {
|
||||
aapsLogger.debug(LTag.PUMP, "Unregistering BT permissions check object")
|
||||
bluetoothPermissionChecks?.unregister()
|
||||
bluetoothPermissionChecks = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
bluetoothPermissionChecks?.let {
|
||||
aapsLogger.debug(LTag.PUMP, "Requesting and checking BT permissions")
|
||||
it.requestAndCheck()
|
||||
}
|
||||
combov2Plugin.driverStateUIFlow
|
||||
.onEach { driverState ->
|
||||
if (!uiInitialized) {
|
||||
when (driverState) {
|
||||
// In the NotInitialized state, the PumpManager is unavailable because it cannot
|
||||
// function without Bluetooth permissions. Several of ComboV2Plugin's functions
|
||||
// such as getPairingProgressFlow() depend on PumpManager though. To prevent UI
|
||||
// controls from becoming active without having a PumpManager, show instead a
|
||||
// view on the activity that explains why pairing is currently not possible.
|
||||
ComboV2Plugin.DriverState.NotInitialized -> {
|
||||
aapsLogger.info(LTag.PUMP, "Cannot pair right now; disabling pairing UI controls, showing message instead")
|
||||
|
||||
binding.combov2PairingSectionInitial.visibility = View.GONE
|
||||
binding.combov2PairingSectionCannotPairDriverNotInitialized.visibility = View.VISIBLE
|
||||
|
||||
binding.combov2CannotPairGoBack.setOnClickListener {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
binding.combov2PairingSectionCannotPairDriverNotInitialized.visibility = View.GONE
|
||||
setupUi(binding)
|
||||
uiInitialized = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.launchIn(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
aapsLogger.info(LTag.PUMP, "User pressed the back button; cancelling any ongoing pairing")
|
||||
combov2Plugin.cancelPairing()
|
||||
@Suppress("DEPRECATION")
|
||||
super.onBackPressed()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
// In the NotInitialized state, getPairingProgressFlow() crashes because there
|
||||
// is no PumpManager present. But in that state, the pairing progress flow needs
|
||||
// no reset because no pairing can happen in that state anyway.
|
||||
if (combov2Plugin.driverStateUIFlow.value != ComboV2Plugin.DriverState.NotInitialized) {
|
||||
// Reset the pairing progress reported to allow for future pairing attempts.
|
||||
// Do this only after pairing was finished or aborted. onDestroy() can be
|
||||
// called in the middle of a pairing process, and we do not want to reset
|
||||
// the progress reporter mid-pairing.
|
||||
when (combov2Plugin.getPairingProgressFlow().value.stage) {
|
||||
BasicProgressStage.Finished,
|
||||
is BasicProgressStage.Aborted -> {
|
||||
aapsLogger.debug(
|
||||
LTag.PUMP,
|
||||
"Resetting pairing progress reporter after pairing was finished/aborted"
|
||||
)
|
||||
combov2Plugin.resetPairingProgress()
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the activity start callback and unregister the activity
|
||||
// launcher to make sure that future registerForActivityResult()
|
||||
// calls start from a blank slate. (This is about the discovery
|
||||
// activity, not about the BluetoothPermissionChecks ones.)
|
||||
combov2Plugin.customDiscoveryActivityStartCallback = null
|
||||
unregisterActivityLauncher.invoke()
|
||||
unregisterActivityLauncher = {}
|
||||
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun setupUi(binding: Combov2PairingActivityBinding) {
|
||||
binding.combov2PairingFinishedOk.setOnClickListener {
|
||||
finish()
|
||||
}
|
||||
|
@ -197,8 +350,6 @@ class ComboV2PairingActivity : DaggerAppCompatActivity() {
|
|||
})
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
combov2Plugin.getPairingProgressFlow()
|
||||
.onEach { progressReport ->
|
||||
val stage = progressReport.stage
|
||||
|
@ -261,48 +412,13 @@ class ComboV2PairingActivity : DaggerAppCompatActivity() {
|
|||
|
||||
binding.combov2PairingProgressBar.progress = (progressReport.overallProgress * 100).toInt()
|
||||
}
|
||||
.launchIn(this)
|
||||
.launchIn(lifecycleScope)
|
||||
|
||||
combov2Plugin.previousPairingAttemptFailedFlow
|
||||
.onEach { previousAttemptFailed ->
|
||||
binding.combov2PinFailureIndicator.visibility =
|
||||
if (previousAttemptFailed) View.VISIBLE else View.GONE
|
||||
}
|
||||
.launchIn(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
aapsLogger.info(LTag.PUMP, "User pressed the back button; cancelling any ongoing pairing")
|
||||
combov2Plugin.cancelPairing()
|
||||
@Suppress("DEPRECATION")
|
||||
super.onBackPressed()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
// In the NotInitialized state, getPairingProgressFlow() crashes because there
|
||||
// is no PumpManager present. But in that state, the pairing progress flow needs
|
||||
// no reset because no pairing can happen in that state anyway.
|
||||
if (combov2Plugin.driverStateUIFlow.value != ComboV2Plugin.DriverState.NotInitialized) {
|
||||
// Reset the pairing progress reported to allow for future pairing attempts.
|
||||
// Do this only after pairing was finished or aborted. onDestroy() can be
|
||||
// called in the middle of a pairing process, and we do not want to reset
|
||||
// the progress reporter mid-pairing.
|
||||
when (combov2Plugin.getPairingProgressFlow().value.stage) {
|
||||
BasicProgressStage.Finished,
|
||||
is BasicProgressStage.Aborted -> {
|
||||
aapsLogger.debug(
|
||||
LTag.PUMP,
|
||||
"Resetting pairing progress reporter after pairing was finished/aborted"
|
||||
)
|
||||
combov2Plugin.resetPairingProgress()
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
super.onDestroy()
|
||||
.launchIn(lifecycleScope)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue