Fixed DFU redirection screen

This commit is contained in:
himalia416
2025-10-06 16:13:19 +02:00
committed by Himali Aryal
parent 0785dba03d
commit ebf6acf96c
9 changed files with 255 additions and 102 deletions

View File

@@ -0,0 +1,19 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="330dp"
android:height="205dp"
android:viewportWidth="330"
android:viewportHeight="205">
<path
android:pathData="M149.42,124.62c-1.02,0.45 -2.26,0.64 -3.61,0.5 -0.09,0 -0.19,-0.02 -0.28,-0.04 -0.25,-0.03 -0.51,-0.08 -0.76,-0.14 -0.13,-0.03 -0.26,-0.06 -0.39,-0.1 -0.3,-0.08 -0.61,-0.19 -0.91,-0.3 -0.07,-0.03 -0.13,-0.05 -0.2,-0.07 -0.38,-0.16 -0.77,-0.33 -1.16,-0.55 -0.02,-0.01 -0.03,-0.02 -0.05,-0.03 -0.02,-0.01 -0.04,-0.02 -0.05,-0.03 -0.39,-0.21 -0.74,-0.45 -1.08,-0.69 -0.06,-0.04 -0.11,-0.09 -0.17,-0.13 -0.26,-0.2 -0.51,-0.4 -0.74,-0.61 -0.1,-0.09 -0.2,-0.19 -0.29,-0.28 -0.19,-0.19 -0.36,-0.38 -0.52,-0.57 -0.06,-0.07 -0.12,-0.15 -0.18,-0.22 -0.83,-1.07 -1.33,-2.22 -1.49,-3.32 -0.37,-2.55 -0.13,-5.21 6.03,-16.33 6.16,-11.11 8.29,-12.73 10.65,-13.77 0.99,-0.43 2.19,-0.63 3.5,-0.51 0.04,0 0.08,0.01 0.12,0.02 0.31,0.03 0.63,0.08 0.96,0.16 0.13,0.03 0.25,0.06 0.38,0.09 0.31,0.08 0.61,0.18 0.92,0.3 0.08,0.03 0.16,0.06 0.24,0.09 0.4,0.16 0.8,0.34 1.2,0.56 0.02,0.01 0.03,0.02 0.05,0.03 0.02,0.01 0.04,0.02 0.05,0.03 0.4,0.22 0.77,0.47 1.11,0.72 0.07,0.05 0.14,0.11 0.21,0.16 0.26,0.2 0.51,0.41 0.74,0.63 0.1,0.09 0.19,0.18 0.28,0.27 0.23,0.24 0.45,0.48 0.64,0.73 0.02,0.03 0.05,0.06 0.08,0.09 0.79,1.04 1.26,2.16 1.42,3.24 0.15,1.01 0.19,2.05 -0.19,3.62 -0.13,0.52 0.1,1.07 0.57,1.33 2.74,1.51 7.01,3.86 9.4,5.18 0.7,0.39 1.58,-0.02 1.74,-0.8 2.24,-10.56 -2.52,-18.86 -11.24,-23.7 -6.29,-3.49 -11.71,-3.94 -17.44,-2.3 -8.2,2.35 -11.85,8.93 -16.66,17.6 -4.81,8.67 -8.45,15.25 -6.1,23.45 1.64,5.73 4.9,10.08 11.19,13.57 8.63,4.79 18.09,4.48 25.81,-2.79 0.59,-0.55 0.47,-1.53 -0.24,-1.92 -2.35,-1.3 -6.62,-3.67 -9.37,-5.2 -0.46,-0.26 -1.03,-0.17 -1.41,0.2 -1.03,1 -1.87,1.46 -2.74,1.84Z"
android:strokeWidth="0"
android:fillColor="#00a1c5"/>
<path
android:pathData="M179.71,77.43c1.02,-0.45 2.26,-0.64 3.61,-0.5 0.09,0 0.19,0.02 0.28,0.04 0.25,0.03 0.51,0.08 0.76,0.14 0.13,0.03 0.26,0.06 0.39,0.1 0.3,0.08 0.61,0.19 0.91,0.3 0.07,0.03 0.13,0.05 0.2,0.07 0.38,0.16 0.77,0.33 1.16,0.55 0.02,0.01 0.03,0.02 0.05,0.03 0.02,0.01 0.04,0.02 0.05,0.03 0.39,0.21 0.74,0.45 1.08,0.69 0.06,0.04 0.11,0.09 0.17,0.13 0.26,0.2 0.51,0.4 0.74,0.61 0.1,0.09 0.2,0.19 0.29,0.28 0.19,0.19 0.36,0.38 0.52,0.57 0.06,0.07 0.12,0.15 0.18,0.22 0.83,1.07 1.33,2.22 1.49,3.32 0.37,2.55 0.13,5.21 -6.03,16.33 -6.16,11.11 -8.29,12.73 -10.65,13.77 -0.99,0.43 -2.19,0.63 -3.5,0.51 -0.04,0 -0.08,-0.01 -0.12,-0.02 -0.31,-0.03 -0.63,-0.08 -0.96,-0.16 -0.13,-0.03 -0.25,-0.06 -0.38,-0.09 -0.31,-0.08 -0.61,-0.18 -0.92,-0.3 -0.08,-0.03 -0.16,-0.06 -0.24,-0.09 -0.4,-0.16 -0.8,-0.34 -1.2,-0.56 -0.02,-0.01 -0.03,-0.02 -0.05,-0.03 -0.02,-0.01 -0.04,-0.02 -0.05,-0.03 -0.4,-0.22 -0.77,-0.47 -1.11,-0.72 -0.07,-0.05 -0.14,-0.11 -0.21,-0.16 -0.26,-0.2 -0.51,-0.41 -0.74,-0.63 -0.1,-0.09 -0.19,-0.18 -0.28,-0.27 -0.23,-0.24 -0.45,-0.48 -0.64,-0.73 -0.02,-0.03 -0.05,-0.06 -0.08,-0.09 -0.79,-1.04 -1.26,-2.16 -1.42,-3.24 -0.15,-1.01 -0.19,-2.05 0.18,-3.61 0.13,-0.53 -0.1,-1.08 -0.57,-1.34 -2.74,-1.51 -7.01,-3.86 -9.39,-5.17 -0.7,-0.39 -1.58,0.02 -1.75,0.8 -2.24,10.56 2.52,18.86 11.24,23.69 6.29,3.49 11.71,3.94 17.44,2.3 8.2,-2.35 11.85,-8.93 16.66,-17.6 4.81,-8.67 8.45,-15.25 6.1,-23.45 -1.64,-5.73 -4.9,-10.08 -11.19,-13.57 -8.63,-4.79 -18.09,-4.48 -25.81,2.79 -0.59,0.55 -0.47,1.52 0.24,1.91 2.35,1.31 6.63,3.67 9.38,5.2 0.46,0.25 1.03,0.17 1.4,-0.2 1.03,-1 1.87,-1.46 2.75,-1.85Z"
android:strokeWidth="0"
android:fillColor="#00a1c5"/>
<path
android:pathData="M248.07,114.76v-25.05l-19.74,-7.4c-1.2,-3.84 -2.77,-7.5 -4.61,-11.01l8.75,-19.25 -17.71,-17.71 -19.25,8.75c-3.51,-1.84 -7.17,-3.41 -11.01,-4.61l-7.4,-19.74h-25.05l-7.4,19.74c-3.84,1.2 -7.5,2.77 -11.01,4.61l-19.25,-8.75 -17.71,17.71 8.75,19.25c-1.84,3.51 -3.41,7.17 -4.61,11.01l-19.74,7.4v25.05l19.74,7.4c1.2,3.83 2.77,7.5 4.61,11.01l-8.75,19.25 17.71,17.71 19.25,-8.75c3.51,1.84 7.17,3.41 11.01,4.61l7.4,19.74h25.05l7.4,-19.74c3.84,-1.2 7.5,-2.77 11.01,-4.61l19.25,8.75 17.71,-17.71 -8.75,-19.25c1.84,-3.51 3.41,-7.17 4.61,-11.01l19.74,-7.4Z"
android:strokeWidth="8"
android:fillColor="#00000000"
android:strokeColor="#00a1c5"/>
</vector>

View File

@@ -208,7 +208,7 @@ internal fun DeviceConnectedView(
Profile.BATTERY -> BatteryScreen()
Profile.THROUGHPUT -> ThroughputScreen(state.maxValueLength)
Profile.UART -> UARTScreen(state.maxValueLength)
Profile.DFU -> DFUScreen(modifier = Modifier.padding(16.dp))
Profile.DFU -> DFUScreen()
}
}
}

View File

@@ -7,110 +7,93 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Download
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import no.nordicsemi.android.common.theme.NordicTheme
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import no.nordicsemi.android.toolbox.profile.R
import no.nordicsemi.android.toolbox.profile.viewmodel.DFUViewModel
@Composable
internal fun DFUScreen(
modifier: Modifier = Modifier
) {
Column(
modifier = Modifier
.fillMaxSize()
.then(modifier),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
OutlinedCard(
internal fun DFUScreen() {
val dfuViewModel = hiltViewModel<DFUViewModel>()
val dfuServiceState by dfuViewModel.dfuServiceState.collectAsStateWithLifecycle()
val context = LocalContext.current
val uriHandler = LocalUriHandler.current
dfuServiceState.dfuAppName?.let { dfuApp ->
val intent = context.packageManager.getLaunchIntentForPackage(dfuApp.packageName)
val description = intent?.let { "Open ${dfuApp.appName}" } ?: "Download from Play Store"
Column(
modifier = Modifier
.widthIn(max = 460.dp),
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
painter = painterResource(R.drawable.ic_dfu),
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(48.dp)
)
Text(
text = "DFU is not supported",
style = MaterialTheme.typography.titleMedium
)
Text(
text = "DFU service is not available in the current version of the app. " +
"Please use the DFU app from Nordic Semiconductor to update your devices firmware.",
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyMedium
)
}
}
val uriHandler = LocalUriHandler.current
val context = LocalContext.current
val packageManger = context.packageManager
val intent = packageManger.getLaunchIntentForPackage(DFU_PACKAGE_NAME)
val description = intent?.let {
"Open DFU"
} ?: "Download from Play Store"
Button(
onClick = {
if (intent != null) {
context.startActivity(intent)
} else {
uriHandler.openUri(DFU_APP_LINK)
}
},
) {
Row {
intent?.let {
OutlinedCard {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
painter = painterResource(R.drawable.ic_dfu),
contentDescription = null,
modifier = Modifier
.size(24.dp)
.padding(end = 8.dp)
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(56.dp)
)
} ?: Icon(imageVector = Icons.Default.Download, contentDescription = null)
Text(text = description)
Text(
text = "DFU is not supported",
style = MaterialTheme.typography.titleMedium
)
Text(
text = "DFU service is not available in the current version of the app. " +
"Please use the DFU app from Nordic Semiconductor to update your devices firmware.",
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyMedium
)
}
}
Button(
onClick = {
intent?.let { context.startActivity(it) }
?: uriHandler.openUri(dfuApp.appLink)
}
) {
Row(verticalAlignment = Alignment.CenterVertically) {
val icon = intent?.let { dfuApp.appIcon } ?: R.drawable.google_play_2022_icon
val size = if (intent != null) 56.dp else 28.dp
Icon(
painter = painterResource(icon),
contentDescription = null,
modifier = Modifier
.size(size)
.padding(end = 8.dp),
tint = if (intent == null) Color.Unspecified else MaterialTheme.colorScheme.onPrimary
)
Text(text = description)
}
}
}
}
}
@Preview(showBackground = true)
@Composable
private fun DeviceDisconnectedViewPreview() {
NordicTheme {
DFUScreen(
modifier = Modifier.padding(16.dp)
)
}
}

View File

@@ -0,0 +1,66 @@
package no.nordicsemi.android.toolbox.profile.viewmodel
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import no.nordicsemi.android.common.navigation.Navigator
import no.nordicsemi.android.common.navigation.viewmodel.SimpleNavigationViewModel
import no.nordicsemi.android.toolbox.lib.utils.Profile
import no.nordicsemi.android.toolbox.profile.ProfileDestinationId
import no.nordicsemi.android.toolbox.profile.data.DFUServiceData
import no.nordicsemi.android.toolbox.profile.manager.repository.DFURepository
import no.nordicsemi.android.toolbox.profile.repository.DeviceRepository
import javax.inject.Inject
@HiltViewModel
internal class DFUViewModel @Inject constructor(
private val deviceRepository: DeviceRepository,
navigator: Navigator,
savedStateHandle: SavedStateHandle,
) : SimpleNavigationViewModel(navigator, savedStateHandle) {
val address = parameterOf(ProfileDestinationId)
// StateFlow to hold the selected temperature unit
private val _dfuServiceState = MutableStateFlow(DFUServiceData())
val dfuServiceState = _dfuServiceState.asStateFlow()
init {
observeDFUProfile()
}
/**
* Observes the [DeviceRepository.profileHandlerFlow] from the [deviceRepository] that contains [Profile.DFU].
*/
private fun observeDFUProfile() = viewModelScope.launch {
deviceRepository.profileHandlerFlow
.onEach { mapOfPeripheralProfiles ->
mapOfPeripheralProfiles.forEach { (peripheral, profiles) ->
if (peripheral.address == address) {
profiles.filter { it.profile == Profile.DFU }
.forEach { _ ->
startDFUService(peripheral.address)
}
}
}
}.launchIn(this)
}
/**
* Starts the DFU Service and observes data changes.
*
* @param address The address of the peripheral device.
*/
private fun startDFUService(address: String) = DFURepository.getData(address)
.onEach { dFUServiceData ->
_dfuServiceState.value = _dfuServiceState.value.copy(
profile = dFUServiceData.profile,
dfuAppName = dFUServiceData.dfuAppName
)
}.launchIn(viewModelScope)
}

View File

@@ -0,0 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28.99dp"
android:height="31.99dp"
android:viewportWidth="28.99"
android:viewportHeight="31.99">
<path
android:pathData="M13.54,15.28 L0.12,29.34a3.66,3.66 0,0 0,5.33 2.16l15.1,-8.6Z"
android:fillColor="#ea4335"/>
<path
android:pathData="m27.11,12.89 l-6.53,-3.74 -7.35,6.45 7.38,7.28 6.48,-3.7a3.54,3.54 0,0 0,1.5 -4.79,3.62 3.62,0 0,0 -1.5,-1.5z"
android:fillColor="#fbbc04"/>
<path
android:pathData="M0.12,2.66a3.57,3.57 0,0 0,-0.12 0.92v24.84a3.57,3.57 0,0 0,0.12 0.92L14,15.64Z"
android:fillColor="#4285f4"/>
<path
android:pathData="m13.64,16 l6.94,-6.85L5.5,0.51A3.73,3.73 0,0 0,3.63 0,3.64 3.64,0 0,0 0.12,2.65Z"
android:fillColor="#34a853"/>
</vector>

View File

@@ -1,16 +1,56 @@
package no.nordicsemi.android.toolbox.profile.data
import androidx.annotation.DrawableRes
import no.nordicsemi.android.toolbox.lib.utils.Profile
import no.nordicsemi.android.toolbox.lib.utils.R
internal const val DFU_PACKAGE_NAME = "no.nordicsemi.android.dfu"
internal const val DFU_APP_LINK =
"https://play.google.com/store/apps/details?id=no.nordicsemi.android.dfu"
internal const val SMP_PACKAGE_NAME = "no.nordicsemi.android.nrfconnectdevicemanager"
internal const val SMP_APP_LINK =
"https://play.google.com/store/apps/details?id=no.nordicsemi.android.nrfconnectdevicemanager"
data class DFUServiceData(
override val profile: Profile = Profile.DFU,
val dfuAppName : DFUsAvailable? = null,
): ProfileServiceData()
val dfuAppName: DFUsAvailable? = null,
) : ProfileServiceData()
enum class DFUsAvailable {
DFU_SERVICE,
SMP_SERVICE,
MDS_SERVICE,
LEGACY_DFU_SERVICE,
EXPERIMENTAL_BUTTONLESS_DFU_SERVICE;
enum class DFUsAvailable(
val packageName: String,
val appLink: String,
val appName: String,
@DrawableRes val appIcon: Int
) {
DFU_SERVICE(
packageName = DFU_PACKAGE_NAME,
appLink = DFU_APP_LINK,
appName = "DFU",
appIcon = R.drawable.ic_dfu
),
SMP_SERVICE(
packageName = SMP_PACKAGE_NAME,
appLink = SMP_APP_LINK,
appName = "nRF Connect Device Manager",
appIcon = R.drawable.ic_device_manager
),
MDS_SERVICE(
packageName = SMP_PACKAGE_NAME,
appLink = SMP_APP_LINK,
appName = "nRF Connect Device Manager",
appIcon = R.drawable.ic_device_manager
),
LEGACY_DFU_SERVICE(
packageName = DFU_PACKAGE_NAME,
appLink = DFU_APP_LINK,
appName = "DFU",
appIcon = R.drawable.ic_dfu
),
EXPERIMENTAL_BUTTONLESS_DFU_SERVICE(
packageName = DFU_PACKAGE_NAME,
appLink = DFU_APP_LINK,
appName = "DFU",
appIcon = R.drawable.ic_dfu
)
}

View File

@@ -23,17 +23,38 @@ internal class DFUManager : ServiceManager {
remoteService: RemoteService,
scope: CoroutineScope
) {
val appName = when (remoteService.uuid) {
DFU_SERVICE_UUID.toKotlinUuid() -> DFUsAvailable.DFU_SERVICE
SMP_SERVICE_UUID.toKotlinUuid() -> DFUsAvailable.SMP_SERVICE
LEGACY_DFU_SERVICE_UUID.toKotlinUuid() -> DFUsAvailable.LEGACY_DFU_SERVICE
EXPERIMENTAL_BUTTONLESS_DFU_SERVICE_UUID.toKotlinUuid() -> DFUsAvailable.EXPERIMENTAL_BUTTONLESS_DFU_SERVICE
MDS_SERVICE_UUID.toKotlinUuid() -> DFUsAvailable.MDS_SERVICE
try {
when (remoteService.uuid) {
DFU_SERVICE_UUID.toKotlinUuid() -> DFURepository.updateAppName(
deviceId,
DFUsAvailable.DFU_SERVICE
)
else -> null
SMP_SERVICE_UUID.toKotlinUuid() -> DFURepository.updateAppName(
deviceId,
DFUsAvailable.SMP_SERVICE
)
LEGACY_DFU_SERVICE_UUID.toKotlinUuid() -> DFURepository.updateAppName(
deviceId,
DFUsAvailable.LEGACY_DFU_SERVICE
)
EXPERIMENTAL_BUTTONLESS_DFU_SERVICE_UUID.toKotlinUuid() -> DFURepository.updateAppName(
deviceId,
DFUsAvailable.EXPERIMENTAL_BUTTONLESS_DFU_SERVICE
)
MDS_SERVICE_UUID.toKotlinUuid() -> DFURepository.updateAppName(
deviceId,
DFUsAvailable.MDS_SERVICE
)
else -> null
}
} catch (_: Exception) {
DFURepository.clear(deviceId)
}
if (appName != null)
DFURepository.updateAppName(deviceId, appName)
}
}

View File

@@ -9,12 +9,18 @@ import no.nordicsemi.android.toolbox.profile.data.DFUsAvailable
object DFURepository {
private val _dataMap = mutableMapOf<String, MutableStateFlow<DFUServiceData>>()
fun getData(deviceId: String): Flow<DFUServiceData> {
return _dataMap.getOrPut(deviceId) { MutableStateFlow(DFUServiceData()) }
}
fun getData(deviceId: String): Flow<DFUServiceData> =
_dataMap.getOrPut(deviceId) { MutableStateFlow(DFUServiceData()) }
fun updateAppName(deviceId: String, appName: DFUsAvailable) {
_dataMap[deviceId]?.update { it.copy(dfuAppName = appName) }
_dataMap[deviceId]?.let {
it.update { dFUServiceData ->
dFUServiceData.copy(dfuAppName = appName)
}
} ?: run {
_dataMap[deviceId] = MutableStateFlow(DFUServiceData(dfuAppName = appName))
}
}
fun clear(deviceId: String) {