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
|
package info.nightscout.pump.combov2.activities
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.databinding.DataBindingUtil
|
import androidx.databinding.DataBindingUtil
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
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.AAPSLogger
|
||||||
import info.nightscout.rx.logging.LTag
|
import info.nightscout.rx.logging.LTag
|
||||||
import info.nightscout.shared.interfaces.ResourceHelper
|
import info.nightscout.shared.interfaces.ResourceHelper
|
||||||
|
import kotlinx.coroutines.CompletableJob
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import javax.inject.Inject
|
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() {
|
class ComboV2PairingActivity : DaggerAppCompatActivity() {
|
||||||
|
|
||||||
@Inject lateinit var aapsLogger: AAPSLogger
|
@Inject lateinit var aapsLogger: AAPSLogger
|
||||||
@Inject lateinit var rh: ResourceHelper
|
@Inject lateinit var rh: ResourceHelper
|
||||||
@Inject lateinit var combov2Plugin: ComboV2Plugin
|
@Inject lateinit var combov2Plugin: ComboV2Plugin
|
||||||
|
|
||||||
|
private var uiInitialized = false
|
||||||
|
private var unregisterActivityLauncher = {}
|
||||||
|
private var bluetoothPermissionChecks: BluetoothPermissionChecks? = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
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
|
// Install an activity result caller for when the user presses
|
||||||
// "deny" or "reject" in the dialog that pops up when Android
|
// "deny" or "reject" in the dialog that pops up when Android
|
||||||
// asks for permission to enable device discovery. In such a
|
// asks for permission to enable device discovery. In such a
|
||||||
|
@ -74,7 +103,131 @@ class ComboV2PairingActivity : DaggerAppCompatActivity() {
|
||||||
combov2Plugin.customDiscoveryActivityStartCallback = { intent ->
|
combov2Plugin.customDiscoveryActivityStartCallback = { intent ->
|
||||||
startPairingActivityLauncher.launch(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 {
|
binding.combov2PairingFinishedOk.setOnClickListener {
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
@ -197,112 +350,75 @@ class ComboV2PairingActivity : DaggerAppCompatActivity() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
lifecycleScope.launch {
|
combov2Plugin.getPairingProgressFlow()
|
||||||
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
.onEach { progressReport ->
|
||||||
combov2Plugin.getPairingProgressFlow()
|
val stage = progressReport.stage
|
||||||
.onEach { progressReport ->
|
|
||||||
val stage = progressReport.stage
|
|
||||||
|
|
||||||
binding.combov2PairingSectionInitial.visibility =
|
binding.combov2PairingSectionInitial.visibility =
|
||||||
if (stage == BasicProgressStage.Idle) View.VISIBLE else View.GONE
|
if (stage == BasicProgressStage.Idle) View.VISIBLE else View.GONE
|
||||||
binding.combov2PairingSectionFinished.visibility =
|
binding.combov2PairingSectionFinished.visibility =
|
||||||
if (stage == BasicProgressStage.Finished) View.VISIBLE else View.GONE
|
if (stage == BasicProgressStage.Finished) View.VISIBLE else View.GONE
|
||||||
binding.combov2PairingSectionAborted.visibility =
|
binding.combov2PairingSectionAborted.visibility =
|
||||||
if (stage is BasicProgressStage.Aborted) View.VISIBLE else View.GONE
|
if (stage is BasicProgressStage.Aborted) View.VISIBLE else View.GONE
|
||||||
binding.combov2PairingSectionMain.visibility = when (stage) {
|
binding.combov2PairingSectionMain.visibility = when (stage) {
|
||||||
BasicProgressStage.Idle,
|
BasicProgressStage.Idle,
|
||||||
BasicProgressStage.Finished,
|
BasicProgressStage.Finished,
|
||||||
is BasicProgressStage.Aborted -> View.GONE
|
is BasicProgressStage.Aborted -> View.GONE
|
||||||
else -> View.VISIBLE
|
else -> View.VISIBLE
|
||||||
}
|
|
||||||
|
|
||||||
if (stage is BasicProgressStage.Aborted) {
|
|
||||||
binding.combov2PairingAbortedReasonText.text = when (stage) {
|
|
||||||
is BasicProgressStage.Cancelled -> rh.gs(R.string.combov2_pairing_cancelled)
|
|
||||||
is BasicProgressStage.Timeout -> rh.gs(R.string.combov2_pairing_combo_scan_timeout_reached)
|
|
||||||
is BasicProgressStage.Error -> rh.gs(R.string.combov2_pairing_failed_due_to_error, stage.cause.toString())
|
|
||||||
else -> rh.gs(R.string.combov2_pairing_aborted_unknown_reasons)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.combov2CurrentPairingStepDesc.text = when (val progStage = stage) {
|
|
||||||
BasicProgressStage.ScanningForPumpStage ->
|
|
||||||
rh.gs(R.string.combov2_scanning_for_pump)
|
|
||||||
|
|
||||||
is BasicProgressStage.EstablishingBtConnection -> {
|
|
||||||
rh.gs(
|
|
||||||
R.string.combov2_establishing_bt_connection,
|
|
||||||
progStage.currentAttemptNr
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
BasicProgressStage.PerformingConnectionHandshake ->
|
|
||||||
rh.gs(R.string.combov2_pairing_performing_handshake)
|
|
||||||
|
|
||||||
BasicProgressStage.ComboPairingKeyAndPinRequested ->
|
|
||||||
rh.gs(R.string.combov2_pairing_pump_requests_pin)
|
|
||||||
|
|
||||||
BasicProgressStage.ComboPairingFinishing ->
|
|
||||||
rh.gs(R.string.combov2_pairing_finishing)
|
|
||||||
|
|
||||||
else -> ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stage == BasicProgressStage.ComboPairingKeyAndPinRequested) {
|
|
||||||
binding.combov2PinEntryUi.visibility = View.VISIBLE
|
|
||||||
} else
|
|
||||||
binding.combov2PinEntryUi.visibility = View.GONE
|
|
||||||
|
|
||||||
// Scanning for the pump can take a long time and happens at the
|
|
||||||
// beginning, so set the progress bar to indeterminate during that
|
|
||||||
// time to show _something_ to the user.
|
|
||||||
binding.combov2PairingProgressBar.isIndeterminate =
|
|
||||||
(stage == BasicProgressStage.ScanningForPumpStage)
|
|
||||||
|
|
||||||
binding.combov2PairingProgressBar.progress = (progressReport.overallProgress * 100).toInt()
|
|
||||||
}
|
|
||||||
.launchIn(this)
|
|
||||||
|
|
||||||
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
|
if (stage is BasicProgressStage.Aborted) {
|
||||||
}
|
binding.combov2PairingAbortedReasonText.text = when (stage) {
|
||||||
}
|
is BasicProgressStage.Cancelled -> rh.gs(R.string.combov2_pairing_cancelled)
|
||||||
|
is BasicProgressStage.Timeout -> rh.gs(R.string.combov2_pairing_combo_scan_timeout_reached)
|
||||||
|
is BasicProgressStage.Error -> rh.gs(R.string.combov2_pairing_failed_due_to_error, stage.cause.toString())
|
||||||
|
else -> rh.gs(R.string.combov2_pairing_aborted_unknown_reasons)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
super.onDestroy()
|
binding.combov2CurrentPairingStepDesc.text = when (val progStage = stage) {
|
||||||
|
BasicProgressStage.ScanningForPumpStage ->
|
||||||
|
rh.gs(R.string.combov2_scanning_for_pump)
|
||||||
|
|
||||||
|
is BasicProgressStage.EstablishingBtConnection -> {
|
||||||
|
rh.gs(
|
||||||
|
R.string.combov2_establishing_bt_connection,
|
||||||
|
progStage.currentAttemptNr
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BasicProgressStage.PerformingConnectionHandshake ->
|
||||||
|
rh.gs(R.string.combov2_pairing_performing_handshake)
|
||||||
|
|
||||||
|
BasicProgressStage.ComboPairingKeyAndPinRequested ->
|
||||||
|
rh.gs(R.string.combov2_pairing_pump_requests_pin)
|
||||||
|
|
||||||
|
BasicProgressStage.ComboPairingFinishing ->
|
||||||
|
rh.gs(R.string.combov2_pairing_finishing)
|
||||||
|
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stage == BasicProgressStage.ComboPairingKeyAndPinRequested) {
|
||||||
|
binding.combov2PinEntryUi.visibility = View.VISIBLE
|
||||||
|
} else
|
||||||
|
binding.combov2PinEntryUi.visibility = View.GONE
|
||||||
|
|
||||||
|
// Scanning for the pump can take a long time and happens at the
|
||||||
|
// beginning, so set the progress bar to indeterminate during that
|
||||||
|
// time to show _something_ to the user.
|
||||||
|
binding.combov2PairingProgressBar.isIndeterminate =
|
||||||
|
(stage == BasicProgressStage.ScanningForPumpStage)
|
||||||
|
|
||||||
|
binding.combov2PairingProgressBar.progress = (progressReport.overallProgress * 100).toInt()
|
||||||
|
}
|
||||||
|
.launchIn(lifecycleScope)
|
||||||
|
|
||||||
|
combov2Plugin.previousPairingAttemptFailedFlow
|
||||||
|
.onEach { previousAttemptFailed ->
|
||||||
|
binding.combov2PinFailureIndicator.visibility =
|
||||||
|
if (previousAttemptFailed) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
.launchIn(lifecycleScope)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue