diff --git a/lib_utils/src/main/java/no/nordicsemi/android/toolbox/lib/utils/Profile.kt b/lib_utils/src/main/java/no/nordicsemi/android/toolbox/lib/utils/Profile.kt
index 6de75e910..c45eb3e63 100644
--- a/lib_utils/src/main/java/no/nordicsemi/android/toolbox/lib/utils/Profile.kt
+++ b/lib_utils/src/main/java/no/nordicsemi/android/toolbox/lib/utils/Profile.kt
@@ -6,6 +6,7 @@ enum class Profile {
CHANNEL_SOUNDING,
CSC,
DFS,
+ DFU,
GLS,
HRS,
HTS,
@@ -32,6 +33,7 @@ enum class Profile {
BATTERY -> "Battery Service"
THROUGHPUT -> "Throughput Service"
UART -> "UART Service"
+ DFU -> "Device Firmware Update"
}
}
\ No newline at end of file
diff --git a/lib_utils/src/main/java/no/nordicsemi/android/toolbox/lib/utils/spec/Spec.kt b/lib_utils/src/main/java/no/nordicsemi/android/toolbox/lib/utils/spec/Spec.kt
index 704b6de4f..4fea22ea1 100644
--- a/lib_utils/src/main/java/no/nordicsemi/android/toolbox/lib/utils/spec/Spec.kt
+++ b/lib_utils/src/main/java/no/nordicsemi/android/toolbox/lib/utils/spec/Spec.kt
@@ -16,3 +16,8 @@ val BATTERY_SERVICE_UUID: UUID = UUID.fromString("0000180F-0000-1000-8000-00805f
val THROUGHPUT_SERVICE_UUID: UUID = UUID.fromString("0483DADD-6C9D-6CA9-5D41-03AD4FFF4ABB")
val CHANNEL_SOUND_SERVICE_UUID: UUID = UUID.fromString("0000185B-0000-1000-8000-00805F9B34FB")
val LBS_SERVICE_UUID: UUID = UUID.fromString("00001523-1212-EFDE-1523-785FEABCD123")
+val DFU_SERVICE_UUID: UUID = UUID.fromString("0000FE59-0000-1000-8000-00805F9B34FB")
+val LEGACY_DFU_SERVICE_UUID: UUID = UUID.fromString("00001530-1212-EFDE-1523-785FEABCD123")
+val EXPERIMENTAL_BUTTONLESS_DFU_SERVICE_UUID: UUID = UUID.fromString("E2A00001-EC31-4EC3-A97A-1C34D87E9878")
+val SMP_SERVICE_UUID: UUID = UUID.fromString("8D53DC1D-1DB7-4CD3-868B-8A527460AA84")
+val MDS_SERVICE_UUID: UUID = UUID.fromString("54220000-F6A5-4007-A371-722F4EBD8436")
diff --git a/lib_utils/src/main/res/drawable/ic_device_manager.xml b/lib_utils/src/main/res/drawable/ic_device_manager.xml
new file mode 100644
index 000000000..854e204e0
--- /dev/null
+++ b/lib_utils/src/main/res/drawable/ic_device_manager.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
diff --git a/lib_ui/src/main/res/drawable/ic_dfu.xml b/lib_utils/src/main/res/drawable/ic_dfu.xml
similarity index 100%
rename from lib_ui/src/main/res/drawable/ic_dfu.xml
rename to lib_utils/src/main/res/drawable/ic_dfu.xml
diff --git a/lib_utils/src/main/res/values/string.xml b/lib_utils/src/main/res/values/string.xml
new file mode 100644
index 000000000..508ae8e69
--- /dev/null
+++ b/lib_utils/src/main/res/values/string.xml
@@ -0,0 +1,13 @@
+
+
+ Device Firmware Update
+ DFU
+ nRF Connect Device Manager
+ SMP
+ nRF Connect Device Manager
+ MDS
+ Legacy DFU
+ Legacy DFU
+ Buttonless DFU
+ Buttonless DFU
+
\ No newline at end of file
diff --git a/permissions-ranging/build.gradle.kts b/permissions-ranging/build.gradle.kts
deleted file mode 100644
index 8d251185d..000000000
--- a/permissions-ranging/build.gradle.kts
+++ /dev/null
@@ -1,11 +0,0 @@
-plugins {
- alias(libs.plugins.nordic.feature)
-}
-
-android {
- namespace = "no.nordicsemi.android.permissions_ranging"
-}
-
-dependencies {
- implementation(libs.accompanist.permissions)
-}
\ No newline at end of file
diff --git a/permissions-ranging/module-rules.pro b/permissions-ranging/module-rules.pro
deleted file mode 100644
index 481bb4348..000000000
--- a/permissions-ranging/module-rules.pro
+++ /dev/null
@@ -1,21 +0,0 @@
-# Add project specific ProGuard rules here.
-# You can control the set of applied configuration files using the
-# proguardFiles setting in build.gradle.
-#
-# For more details, see
-# http://developer.android.com/guide/developing/tools/proguard.html
-
-# If your project uses WebView with JS, uncomment the following
-# and specify the fully qualified class name to the JavaScript interface
-# class:
-#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
-# public *;
-#}
-
-# Uncomment this to preserve the line number information for
-# debugging stack traces.
-#-keepattributes SourceFile,LineNumberTable
-
-# If you keep the line number information, uncomment this to
-# hide the original source file name.
-#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/permissions-ranging/src/main/AndroidManifest.xml b/permissions-ranging/src/main/AndroidManifest.xml
deleted file mode 100644
index b406fd5be..000000000
--- a/permissions-ranging/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/RequestRangingPermission.kt b/permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/RequestRangingPermission.kt
deleted file mode 100644
index 3b0350dcf..000000000
--- a/permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/RequestRangingPermission.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-package no.nordicsemi.android.permissions_ranging
-
-import android.app.Activity
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.ui.platform.LocalContext
-import androidx.hilt.navigation.compose.hiltViewModel
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import no.nordicsemi.android.permissions_ranging.utils.RangingNotAvailableReason
-import no.nordicsemi.android.permissions_ranging.utils.RangingPermissionState
-import no.nordicsemi.android.permissions_ranging.view.RangingPermissionRequestView
-import no.nordicsemi.android.permissions_ranging.viewmodel.RangingPermissionViewModel
-
-@Composable
-fun RequestRangingPermission(
- onChanged: (Boolean) -> Unit = {},
- content: @Composable (Boolean) -> Unit,
-) {
- val permissionViewModel = hiltViewModel()
- val context = LocalContext.current
- val activity = context as? Activity
-
- val state by activity?.let { permissionViewModel.requestRangingPermission(it) }!!
- .collectAsStateWithLifecycle()
-
-
- LaunchedEffect(state) {
- onChanged(state is RangingPermissionState.Available)
- }
-
- when (val s = state) {
- is RangingPermissionState.Available -> content(true)
- is RangingPermissionState.NotAvailable -> {
- when (s.reason) {
- RangingNotAvailableReason.NOT_AVAILABLE -> RangingPermissionRequestView(content)
- RangingNotAvailableReason.PERMISSION_DENIED -> content(false)
- }
- }
- }
-
-}
\ No newline at end of file
diff --git a/permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/repository/RangingStateManager.kt b/permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/repository/RangingStateManager.kt
deleted file mode 100644
index 8380a2ee5..000000000
--- a/permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/repository/RangingStateManager.kt
+++ /dev/null
@@ -1,89 +0,0 @@
-package no.nordicsemi.android.permissions_ranging.repository
-
-import android.app.Activity
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.Intent
-import android.content.IntentFilter
-import androidx.core.app.ActivityCompat
-import androidx.core.content.ContextCompat
-import dagger.hilt.android.qualifiers.ApplicationContext
-import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.flow.callbackFlow
-import no.nordicsemi.android.permissions_ranging.utils.LocalDataProvider
-import no.nordicsemi.android.permissions_ranging.utils.RangingNotAvailableReason
-import no.nordicsemi.android.permissions_ranging.utils.RangingPermissionState
-import no.nordicsemi.android.permissions_ranging.utils.RangingPermissionUtils
-import javax.inject.Inject
-import javax.inject.Singleton
-
-private const val REFRESH_PERMISSIONS =
- "no.nordicsemi.android.permissions_ranging.repository.REFRESH_RANGING_PERMISSIONS"
-private const val RANGING_PERMISSION_REQUEST_CODE = 1001
-
-@Singleton
-internal class RangingStateManager @Inject constructor(
- @param:ApplicationContext private val context: Context,
-) {
- private val dataProvider = LocalDataProvider(context)
- private val utils = RangingPermissionUtils(context, dataProvider)
-
- fun rangingPermissionState(activity: Activity) = callbackFlow {
- trySend(getRangingPermissionState())
-
- val rangingStateChangeHandler = object : BroadcastReceiver() {
- override fun onReceive(context: Context?, intent: Intent?) {
- trySend(getRangingPermissionState())
- }
- }
- ContextCompat.registerReceiver(
- context,
- rangingStateChangeHandler,
- IntentFilter(),
- ContextCompat.RECEIVER_EXPORTED
- )
-
- ActivityCompat.requestPermissions(
- activity,
- arrayOf("android.permission.RANGING"),
- RANGING_PERMISSION_REQUEST_CODE
- )
-
- awaitClose {
-
- context.unregisterReceiver(rangingStateChangeHandler)
- }
-
- }
-
- fun refreshRangingPermissionState() {
- val intent = Intent(REFRESH_PERMISSIONS)
- context.sendBroadcast(intent)
- }
-
- fun markRangingPermissionAsRequested() {
- dataProvider.isRangingPermissionRequested = true
- }
-
- fun isRangingPermissionDenied(): Boolean {
- return try {
- utils.isRangingPermissionDenied()
- } catch (_: Exception) {
- false
- }
- }
-
- private fun getRangingPermissionState(): RangingPermissionState {
- return when {
- !utils.isRangingPermissionAvailable -> RangingPermissionState.NotAvailable(
- RangingNotAvailableReason.NOT_AVAILABLE
- )
-
- utils.isRangingPermissionAvailable && !utils.isRangingPermissionGranted -> RangingPermissionState.NotAvailable(
- RangingNotAvailableReason.PERMISSION_DENIED
- )
-
- else -> RangingPermissionState.Available
- }
- }
-}
\ No newline at end of file
diff --git a/permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/utils/LocalDataProvider.kt b/permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/utils/LocalDataProvider.kt
deleted file mode 100644
index 2420d8253..000000000
--- a/permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/utils/LocalDataProvider.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-package no.nordicsemi.android.permissions_ranging.utils
-
-import android.content.Context
-import android.content.SharedPreferences
-import android.os.Build
-import androidx.annotation.ChecksSdkIntAtLeast
-import androidx.core.app.ActivityCompat
-import androidx.core.content.edit
-import javax.inject.Inject
-import javax.inject.Singleton
-
-private const val SHARED_PREFS_NAME = "SHARED_PREFS_RANGING"
-private const val PREFS_PERMISSION_REQUESTED = "ranging_permission_requested"
-
-@Singleton
-internal class LocalDataProvider @Inject constructor(
- private val context: Context,
-) {
- private val sharedPrefs: SharedPreferences
- get() = context.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE)
-
- val isBaklavaOrAbove: Boolean
- @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.BAKLAVA)
- get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA
-
- /**
- * The first time an app requests a permission there is no 'Don't Allow' checkbox and
- * [ActivityCompat.shouldShowRequestPermissionRationale] returns false.
- */
- var isRangingPermissionRequested: Boolean
- get() = sharedPrefs.getBoolean(PREFS_PERMISSION_REQUESTED, false)
- set(value) {
- sharedPrefs.edit { putBoolean(PREFS_PERMISSION_REQUESTED, value) }
- }
-}
\ No newline at end of file
diff --git a/permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/utils/RangingPermissionState.kt b/permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/utils/RangingPermissionState.kt
deleted file mode 100644
index f6521b95b..000000000
--- a/permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/utils/RangingPermissionState.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-package no.nordicsemi.android.permissions_ranging.utils
-
-internal sealed class RangingPermissionState {
- /**
- * Ranging is available and the app has the required permissions.
- */
- data object Available : RangingPermissionState()
-
- /**
- * Ranging is not available.
- */
- data class NotAvailable(
- val reason: RangingNotAvailableReason,
- ) : RangingPermissionState()
-}
-
-internal enum class RangingNotAvailableReason {
- /** Ranging is not available on this device. */
- NOT_AVAILABLE,
-
- /** The app does not have the required permissions. */
- PERMISSION_DENIED,
-}
\ No newline at end of file
diff --git a/permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/utils/RangingPermissionUtils.kt b/permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/utils/RangingPermissionUtils.kt
deleted file mode 100644
index 231e73513..000000000
--- a/permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/utils/RangingPermissionUtils.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-package no.nordicsemi.android.permissions_ranging.utils
-
-import android.Manifest
-import android.app.Activity
-import android.content.Context
-import android.content.ContextWrapper
-import android.content.pm.PackageManager
-import android.os.Build
-import androidx.annotation.ChecksSdkIntAtLeast
-import androidx.core.content.ContextCompat
-
-internal class RangingPermissionUtils(
- private val context: Context,
- private val dataProvider: LocalDataProvider,
-) {
- val isRangingPermissionAvailable: Boolean
- @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.BAKLAVA)
- get() = Build.VERSION.SDK_INT >= 36
-
-
- val isRangingPermissionGranted: Boolean
- get() = isRangingPermissionAvailable &&
- ContextCompat.checkSelfPermission(
- context,
- Manifest.permission.RANGING
- ) == PackageManager.PERMISSION_GRANTED
-
- fun isRangingPermissionDenied(): Boolean {
- return dataProvider.isBaklavaOrAbove &&
- dataProvider.isRangingPermissionRequested && // Ranging permission was requested.
- !isRangingPermissionGranted // Ranging permission is not granted
- && !context.findActivity()
- ?.shouldShowRequestPermissionRationale(Manifest.permission.RANGING)!!
-
- }
-
- /**
- * Finds the activity from the given context.
- *
- * https://github.com/google/accompanist/blob/6611ebda55eb2948eca9e1c89c2519e80300855a/permissions/src/main/java/com/google/accompanist/permissions/PermissionsUtil.kt#L99
- *
- * @throws IllegalStateException if no activity was found.
- * @return the activity.
- */
- private fun Context.findActivity(): Activity? {
- return try {
- var context = this
- while (context is ContextWrapper) {
- if (context is Activity) return context
- context = context.baseContext
- }
- null // no activity found
- } catch (e: Exception) {
- null
- }
- }
-}
\ No newline at end of file
diff --git a/permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/view/RangingPermissionRequestView.kt b/permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/view/RangingPermissionRequestView.kt
deleted file mode 100644
index 72a95df87..000000000
--- a/permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/view/RangingPermissionRequestView.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-package no.nordicsemi.android.permissions_ranging.view
-
-import android.Manifest
-import android.os.Build
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.hilt.navigation.compose.hiltViewModel
-import com.google.accompanist.permissions.ExperimentalPermissionsApi
-import com.google.accompanist.permissions.PermissionStatus
-import com.google.accompanist.permissions.isGranted
-import com.google.accompanist.permissions.rememberPermissionState
-import no.nordicsemi.android.permissions_ranging.viewmodel.RangingPermissionViewModel
-
-@OptIn(ExperimentalPermissionsApi::class)
-@Composable
-internal fun RangingPermissionRequestView(
- content: @Composable (Boolean) -> Unit,
-) {
- val rangingPermissionViewModel = hiltViewModel()
-
- val permission = if (Build.VERSION.SDK_INT >= 36)
- Manifest.permission.RANGING else null
-
- val rangingPermission = permission?.let {
- rememberPermissionState(it)
- }
-
- if (rangingPermission != null) {
- when (rangingPermission.status) {
- is PermissionStatus.Denied -> {
- LaunchedEffect(!rangingPermission.status.isGranted) {
- rangingPermissionViewModel.markRangingPermissionRequested()
- rangingPermission.launchPermissionRequest()
- if (!rangingPermission.status.isGranted) {
- rangingPermissionViewModel.markRangingPermissionDenied()
- }
- rangingPermissionViewModel.refreshRangingPermissionState()
- }
- content(rangingPermission.status.isGranted)
- }
-
- PermissionStatus.Granted -> content(true)
- }
- } else {
- rangingPermissionViewModel.refreshRangingPermissionState()
- content(true)
- }
-
-}
\ No newline at end of file
diff --git a/permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/viewmodel/RangingPermissionViewModel.kt b/permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/viewmodel/RangingPermissionViewModel.kt
deleted file mode 100644
index 87a3103dd..000000000
--- a/permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/viewmodel/RangingPermissionViewModel.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-package no.nordicsemi.android.permissions_ranging.viewmodel
-
-import android.app.Activity
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.stateIn
-import no.nordicsemi.android.permissions_ranging.repository.RangingStateManager
-import no.nordicsemi.android.permissions_ranging.utils.RangingNotAvailableReason
-import no.nordicsemi.android.permissions_ranging.utils.RangingPermissionState
-import javax.inject.Inject
-
-@HiltViewModel
-internal class RangingPermissionViewModel @Inject constructor(
- private val rangingStateManager: RangingStateManager,
-) : ViewModel() {
- fun requestRangingPermission(activity: Activity) =
- rangingStateManager.rangingPermissionState(activity)
- .stateIn(
- viewModelScope,
- SharingStarted.Lazily,
- RangingPermissionState.NotAvailable(RangingNotAvailableReason.NOT_AVAILABLE),
- )
-
- fun refreshRangingPermissionState() {
- rangingStateManager.refreshRangingPermissionState()
- }
-
- fun markRangingPermissionRequested() {
- rangingStateManager.markRangingPermissionAsRequested()
- }
-
- fun markRangingPermissionDenied() {
- rangingStateManager.isRangingPermissionDenied()
- }
-}
\ No newline at end of file
diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/ProfileScreen.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/ProfileScreen.kt
index b4b685688..315498a95 100644
--- a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/ProfileScreen.kt
+++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/ProfileScreen.kt
@@ -29,6 +29,7 @@ import no.nordicsemi.android.common.permissions.ble.RequireLocation
import no.nordicsemi.android.common.permissions.notification.RequestNotificationPermission
import no.nordicsemi.android.toolbox.lib.utils.Profile
import no.nordicsemi.android.toolbox.profile.data.displayMessage
+import no.nordicsemi.android.toolbox.profile.view.dfu.DFUScreen
import no.nordicsemi.android.toolbox.profile.view.battery.BatteryScreen
import no.nordicsemi.android.toolbox.profile.view.bps.BPSScreen
import no.nordicsemi.android.toolbox.profile.view.cgms.CGMScreen
@@ -192,7 +193,10 @@ internal fun DeviceConnectedView(
// Display the appropriate screen for each profile.
when (serviceManager.profile) {
Profile.HTS -> HTSScreen()
- Profile.CHANNEL_SOUNDING -> ChannelSoundingScreen(isNotificationPermissionGranted)
+ Profile.CHANNEL_SOUNDING -> ChannelSoundingScreen(
+ isNotificationPermissionGranted
+ )
+
Profile.BPS -> BPSScreen()
Profile.CSC -> CSCScreen()
Profile.CGM -> CGMScreen()
@@ -204,9 +208,10 @@ internal fun DeviceConnectedView(
Profile.BATTERY -> BatteryScreen()
Profile.THROUGHPUT -> ThroughputScreen(state.maxValueLength)
Profile.UART -> UARTScreen(state.maxValueLength)
+ Profile.DFU -> DFUScreen()
}
}
}
}
}
-}
\ No newline at end of file
+}
diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/dfu/DFUScreen.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/dfu/DFUScreen.kt
new file mode 100644
index 000000000..c66c3a507
--- /dev/null
+++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/dfu/DFUScreen.kt
@@ -0,0 +1,113 @@
+package no.nordicsemi.android.toolbox.profile.view.dfu
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+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.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.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+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() {
+ val dfuViewModel = hiltViewModel()
+ 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 {
+ stringResource(
+ R.string.dfu_description_open,
+ stringResource(dfuApp.appName)
+ )
+ }
+ ?: stringResource(R.string.dfu_description_download)
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ OutlinedCard {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Icon(
+ painter = painterResource(dfuApp.appIcon),
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.size(56.dp)
+ )
+
+ Text(
+ text = stringResource(
+ R.string.dfu_not_supported_title,
+ stringResource(dfuApp.appShortName)
+ ),
+ style = MaterialTheme.typography.titleMedium
+ )
+
+ Text(
+ text = stringResource(
+ R.string.dfu_not_supported_text,
+ stringResource(dfuApp.appShortName),
+ stringResource(dfuApp.appName)
+ ),
+ 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
+
+ Icon(
+ painter = painterResource(icon),
+ contentDescription = null,
+ modifier = Modifier
+ .size(40.dp)
+ .padding(end = 8.dp),
+ tint = if (intent == null) Color.Unspecified else MaterialTheme.colorScheme.onPrimary
+ )
+
+ Text(text = description)
+ }
+ }
+ }
+ }
+}
diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/DFUViewModel.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/DFUViewModel.kt
new file mode 100644
index 000000000..dca5a35cd
--- /dev/null
+++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/DFUViewModel.kt
@@ -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)
+
+}
\ No newline at end of file
diff --git a/profile/src/main/res/drawable/google_play_2022_icon.xml b/profile/src/main/res/drawable/google_play_2022_icon.xml
new file mode 100644
index 000000000..75073ebb4
--- /dev/null
+++ b/profile/src/main/res/drawable/google_play_2022_icon.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
diff --git a/profile/src/main/res/values/dfuStrings.xml b/profile/src/main/res/values/dfuStrings.xml
new file mode 100644
index 000000000..ae269ba4a
--- /dev/null
+++ b/profile/src/main/res/values/dfuStrings.xml
@@ -0,0 +1,8 @@
+
+
+ Open %s
+ Download from Play Store
+ %s is not supported
+ %1s service is not available in the current version of the app. Please use the %2s app from Nordic Semiconductor to update your device’s firmware.
+
+
\ No newline at end of file
diff --git a/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/DFUServiceData.kt b/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/DFUServiceData.kt
new file mode 100644
index 000000000..ade594efb
--- /dev/null
+++ b/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/DFUServiceData.kt
@@ -0,0 +1,63 @@
+package no.nordicsemi.android.toolbox.profile.data
+
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+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()
+
+enum class DFUsAvailable(
+ val packageName: String,
+ val appLink: String,
+ @param:StringRes val appName: Int,
+ @param:DrawableRes val appIcon: Int,
+ @param:StringRes val appShortName: Int,
+) {
+ DFU_SERVICE(
+ packageName = DFU_PACKAGE_NAME,
+ appLink = DFU_APP_LINK,
+ appName = R.string.dfu_app_name,
+ appIcon = R.drawable.ic_dfu,
+ appShortName = R.string.dfu_short_name,
+ ),
+ SMP_SERVICE(
+ packageName = SMP_PACKAGE_NAME,
+ appLink = SMP_APP_LINK,
+ appName = R.string.smp_app_name,
+ appIcon = R.drawable.ic_device_manager,
+ appShortName = R.string.smp_short_name,
+ ),
+ MDS_SERVICE(
+ packageName = SMP_PACKAGE_NAME,
+ appLink = SMP_APP_LINK,
+ appName = R.string.mds_app_name,
+ appIcon = R.drawable.ic_device_manager,
+ appShortName = R.string.mds_app_name,
+ ),
+ LEGACY_DFU_SERVICE(
+ packageName = DFU_PACKAGE_NAME,
+ appLink = DFU_APP_LINK,
+ appName = R.string.legacy_dfu_app_name,
+ appIcon = R.drawable.ic_dfu,
+ appShortName = R.string.legacy_dfu_short_name,
+ ),
+ EXPERIMENTAL_BUTTONLESS_DFU_SERVICE(
+ packageName = DFU_PACKAGE_NAME,
+ appLink = DFU_APP_LINK,
+ appName = R.string.buttonless_dfu_app_name,
+ appIcon = R.drawable.ic_dfu,
+ appShortName = R.string.buttonless_dfu_short_name,
+ )
+}
\ No newline at end of file
diff --git a/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/DFUManager.kt b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/DFUManager.kt
new file mode 100644
index 000000000..767d93593
--- /dev/null
+++ b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/DFUManager.kt
@@ -0,0 +1,60 @@
+package no.nordicsemi.android.toolbox.profile.manager
+
+import kotlinx.coroutines.CoroutineScope
+import no.nordicsemi.android.toolbox.lib.utils.Profile
+import no.nordicsemi.android.toolbox.lib.utils.spec.DFU_SERVICE_UUID
+import no.nordicsemi.android.toolbox.lib.utils.spec.EXPERIMENTAL_BUTTONLESS_DFU_SERVICE_UUID
+import no.nordicsemi.android.toolbox.lib.utils.spec.LEGACY_DFU_SERVICE_UUID
+import no.nordicsemi.android.toolbox.lib.utils.spec.MDS_SERVICE_UUID
+import no.nordicsemi.android.toolbox.lib.utils.spec.SMP_SERVICE_UUID
+import no.nordicsemi.android.toolbox.profile.data.DFUsAvailable
+import no.nordicsemi.android.toolbox.profile.manager.repository.DFURepository
+import no.nordicsemi.kotlin.ble.client.RemoteService
+import kotlin.uuid.ExperimentalUuidApi
+import kotlin.uuid.toKotlinUuid
+
+internal class DFUManager : ServiceManager {
+ override val profile: Profile
+ get() = Profile.DFU
+
+ @OptIn(ExperimentalUuidApi::class)
+ override suspend fun observeServiceInteractions(
+ deviceId: String,
+ remoteService: RemoteService,
+ scope: CoroutineScope
+ ) {
+ try {
+ when (remoteService.uuid) {
+ DFU_SERVICE_UUID.toKotlinUuid() -> DFURepository.updateAppName(
+ deviceId,
+ DFUsAvailable.DFU_SERVICE
+ )
+
+ 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)
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/ServiceManagerFactory.kt b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/ServiceManagerFactory.kt
index 77688cb0d..975b3a367 100644
--- a/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/ServiceManagerFactory.kt
+++ b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/ServiceManagerFactory.kt
@@ -5,12 +5,17 @@ import no.nordicsemi.android.toolbox.lib.utils.spec.BPS_SERVICE_UUID
import no.nordicsemi.android.toolbox.lib.utils.spec.CGMS_SERVICE_UUID
import no.nordicsemi.android.toolbox.lib.utils.spec.CHANNEL_SOUND_SERVICE_UUID
import no.nordicsemi.android.toolbox.lib.utils.spec.CSC_SERVICE_UUID
+import no.nordicsemi.android.toolbox.lib.utils.spec.DFU_SERVICE_UUID
import no.nordicsemi.android.toolbox.lib.utils.spec.DF_SERVICE_UUID
+import no.nordicsemi.android.toolbox.lib.utils.spec.EXPERIMENTAL_BUTTONLESS_DFU_SERVICE_UUID
import no.nordicsemi.android.toolbox.lib.utils.spec.GLS_SERVICE_UUID
import no.nordicsemi.android.toolbox.lib.utils.spec.HRS_SERVICE_UUID
import no.nordicsemi.android.toolbox.lib.utils.spec.HTS_SERVICE_UUID
import no.nordicsemi.android.toolbox.lib.utils.spec.LBS_SERVICE_UUID
+import no.nordicsemi.android.toolbox.lib.utils.spec.LEGACY_DFU_SERVICE_UUID
+import no.nordicsemi.android.toolbox.lib.utils.spec.MDS_SERVICE_UUID
import no.nordicsemi.android.toolbox.lib.utils.spec.RSCS_SERVICE_UUID
+import no.nordicsemi.android.toolbox.lib.utils.spec.SMP_SERVICE_UUID
import no.nordicsemi.android.toolbox.lib.utils.spec.THROUGHPUT_SERVICE_UUID
import no.nordicsemi.android.toolbox.lib.utils.spec.UART_SERVICE_UUID
import kotlin.uuid.ExperimentalUuidApi
@@ -34,6 +39,12 @@ object ServiceManagerFactory {
UART_SERVICE_UUID to ::UARTManager,
CHANNEL_SOUND_SERVICE_UUID to ::ChannelSoundingManager,
LBS_SERVICE_UUID to ::LBSManager,
+ DFU_SERVICE_UUID to ::DFUManager,
+ SMP_SERVICE_UUID to ::DFUManager,
+ MDS_SERVICE_UUID to ::DFUManager,
+ LEGACY_DFU_SERVICE_UUID to ::DFUManager,
+ EXPERIMENTAL_BUTTONLESS_DFU_SERVICE_UUID to ::DFUManager,
+
// Add more service UUIDs to handler mappings as needed
).mapKeys { it.key.toKotlinUuid() }
diff --git a/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/DFURepository.kt b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/DFURepository.kt
new file mode 100644
index 000000000..7fbd68640
--- /dev/null
+++ b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/DFURepository.kt
@@ -0,0 +1,30 @@
+package no.nordicsemi.android.toolbox.profile.manager.repository
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.update
+import no.nordicsemi.android.toolbox.profile.data.DFUServiceData
+import no.nordicsemi.android.toolbox.profile.data.DFUsAvailable
+
+object DFURepository {
+ private val _dataMap = mutableMapOf>()
+
+ fun getData(deviceId: String): Flow =
+ _dataMap.getOrPut(deviceId) { MutableStateFlow(DFUServiceData()) }
+
+
+ fun updateAppName(deviceId: String, appName: DFUsAvailable) {
+ _dataMap[deviceId]?.let {
+ it.update { dFUServiceData ->
+ dFUServiceData.copy(dfuAppName = appName)
+ }
+ } ?: run {
+ _dataMap[deviceId] = MutableStateFlow(DFUServiceData(dfuAppName = appName))
+ }
+ }
+
+ fun clear(deviceId: String) {
+ _dataMap.remove(deviceId)
+ }
+
+}
\ No newline at end of file