diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7569ad71..f22df8f5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -22,5 +22,4 @@ - - \ No newline at end of file + diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/FeatureButton.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/FeatureButton.kt index d9134a06..11b1f5bd 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/FeatureButton.kt +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/FeatureButton.kt @@ -4,8 +4,12 @@ import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -29,12 +33,11 @@ fun FeatureButton( @StringRes name: Int, onClick: () -> Unit ) { - ScreenSection { + ScreenSection(onClick = onClick) { Column( modifier = Modifier .fillMaxWidth() .height(150.dp) - .clickable { onClick() } .padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/HomeScreen.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/HomeScreen.kt index 19848e7a..b9b1af44 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/HomeScreen.kt +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/HomeScreen.kt @@ -136,156 +136,161 @@ fun HomeView(callback: (NavDestination) -> Unit) { Column( modifier = Modifier .fillMaxSize() - .padding(horizontal = 16.dp) .verticalScroll(rememberScrollState()) ) { - Spacer(modifier = Modifier.height(16.dp)) + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp) + ) { + Spacer(modifier = Modifier.height(16.dp)) - Row(horizontalArrangement = Arrangement.SpaceEvenly) { - Box( - modifier = Modifier - .weight(1f) - .fillMaxSize(), - contentAlignment = Alignment.Center - ) { - FeatureButton( - R.drawable.ic_csc, - R.string.csc_module, - R.string.csc_module_full - ) { callback(NavDestination.CSC) } + Row(horizontalArrangement = Arrangement.SpaceEvenly) { + Box( + modifier = Modifier + .weight(1f) + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + FeatureButton( + R.drawable.ic_csc, + R.string.csc_module, + R.string.csc_module_full + ) { callback(NavDestination.CSC) } + } + Spacer(modifier = Modifier.width(16.dp)) + Box( + modifier = Modifier + .weight(1f) + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + FeatureButton( + R.drawable.ic_hrs, R.string.hrs_module, + R.string.hrs_module_full + ) { callback(NavDestination.HRS) } + } } - Spacer(modifier = Modifier.width(16.dp)) - Box( - modifier = Modifier - .weight(1f) - .fillMaxSize(), - contentAlignment = Alignment.Center - ) { - FeatureButton( - R.drawable.ic_hrs, R.string.hrs_module, - R.string.hrs_module_full - ) { callback(NavDestination.HRS) } + + Spacer(modifier = Modifier.height(16.dp)) + + Row(horizontalArrangement = Arrangement.SpaceEvenly) { + Box( + modifier = Modifier + .weight(1f) + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + FeatureButton( + R.drawable.ic_gls, R.string.gls_module, + R.string.gls_module_full + ) { callback(NavDestination.GLS) } + } + Spacer(modifier = Modifier.width(16.dp)) + Box( + modifier = Modifier + .weight(1f) + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + FeatureButton( + R.drawable.ic_hts, R.string.hts_module, + R.string.hts_module_full + ) { callback(NavDestination.HTS) } + } } + + Spacer(modifier = Modifier.height(16.dp)) + + Row(horizontalArrangement = Arrangement.SpaceEvenly) { + Box( + modifier = Modifier + .weight(1f) + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + FeatureButton( + R.drawable.ic_bps, R.string.bps_module, + R.string.bps_module_full + ) { callback(NavDestination.BPS) } + } + Spacer(modifier = Modifier.width(16.dp)) + Box( + modifier = Modifier + .weight(1f) + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + FeatureButton( + R.drawable.ic_rscs, + R.string.rscs_module, + R.string.rscs_module_full + ) { callback(NavDestination.RSCS) } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + Row(horizontalArrangement = Arrangement.SpaceEvenly) { + Box( + modifier = Modifier + .weight(1f) + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + FeatureButton( + R.drawable.ic_prx, R.string.prx_module, + R.string.prx_module_full + ) { callback(NavDestination.PRX) } + } + Spacer(modifier = Modifier.width(16.dp)) + Box( + modifier = Modifier + .weight(1f) + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + FeatureButton( + R.drawable.ic_cgm, R.string.cgm_module, + R.string.cgm_module_full + ) { callback(NavDestination.CGMS) } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + Row(horizontalArrangement = Arrangement.SpaceEvenly) { + Box( + modifier = Modifier + .weight(1f) + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + FeatureButton( + R.drawable.ic_uart, R.string.uart_module, + R.string.uart_module_full + ) { callback(NavDestination.UART) } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + Row(horizontalArrangement = Arrangement.SpaceEvenly) { + Box( + modifier = Modifier + .weight(1f) + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + FeatureButton( + R.drawable.ic_dfu, R.string.dfu_module, + R.string.uart_module_full + ) { callback(NavDestination.DFU) } + } + } + + Spacer(modifier = Modifier.height(16.dp)) } - - Spacer(modifier = Modifier.height(16.dp)) - - Row(horizontalArrangement = Arrangement.SpaceEvenly) { - Box( - modifier = Modifier - .weight(1f) - .fillMaxSize(), - contentAlignment = Alignment.Center - ) { - FeatureButton( - R.drawable.ic_gls, R.string.gls_module, - R.string.gls_module_full - ) { callback(NavDestination.GLS) } - } - Spacer(modifier = Modifier.width(16.dp)) - Box( - modifier = Modifier - .weight(1f) - .fillMaxSize(), - contentAlignment = Alignment.Center - ) { - FeatureButton( - R.drawable.ic_hts, R.string.hts_module, - R.string.hts_module_full - ) { callback(NavDestination.HTS) } - } - } - - Spacer(modifier = Modifier.height(16.dp)) - - Row(horizontalArrangement = Arrangement.SpaceEvenly) { - Box( - modifier = Modifier - .weight(1f) - .fillMaxSize(), - contentAlignment = Alignment.Center - ) { - FeatureButton( - R.drawable.ic_bps, R.string.bps_module, - R.string.bps_module_full - ) { callback(NavDestination.BPS) } - } - Spacer(modifier = Modifier.width(16.dp)) - Box( - modifier = Modifier - .weight(1f) - .fillMaxSize(), - contentAlignment = Alignment.Center - ) { - FeatureButton( - R.drawable.ic_rscs, - R.string.rscs_module, - R.string.rscs_module_full - ) { callback(NavDestination.RSCS) } - } - } - - Spacer(modifier = Modifier.height(16.dp)) - - Row(horizontalArrangement = Arrangement.SpaceEvenly) { - Box( - modifier = Modifier - .weight(1f) - .fillMaxSize(), - contentAlignment = Alignment.Center - ) { - FeatureButton( - R.drawable.ic_prx, R.string.prx_module, - R.string.prx_module_full - ) { callback(NavDestination.PRX) } - } - Spacer(modifier = Modifier.width(16.dp)) - Box( - modifier = Modifier - .weight(1f) - .fillMaxSize(), - contentAlignment = Alignment.Center - ) { - FeatureButton( - R.drawable.ic_cgm, R.string.cgm_module, - R.string.cgm_module_full - ) { callback(NavDestination.CGMS) } - } - } - - Spacer(modifier = Modifier.height(16.dp)) - - Row(horizontalArrangement = Arrangement.SpaceEvenly) { - Box( - modifier = Modifier - .weight(1f) - .fillMaxSize(), - contentAlignment = Alignment.Center - ) { - FeatureButton( - R.drawable.ic_uart, R.string.uart_module, - R.string.uart_module_full - ) { callback(NavDestination.UART) } - } - } - - Spacer(modifier = Modifier.height(16.dp)) - - Row(horizontalArrangement = Arrangement.SpaceEvenly) { - Box( - modifier = Modifier - .weight(1f) - .fillMaxSize(), - contentAlignment = Alignment.Center - ) { - FeatureButton( - R.drawable.ic_dfu, R.string.dfu_module, - R.string.uart_module_full - ) { callback(NavDestination.DFU) } - } - } - - Spacer(modifier = Modifier.height(16.dp)) } } } diff --git a/lib_service/src/main/java/no/nordicsemi/android/service/ForegroundBleService.kt b/lib_service/src/main/java/no/nordicsemi/android/service/ForegroundBleService.kt index 204f1cc7..437b1f42 100644 --- a/lib_service/src/main/java/no/nordicsemi/android/service/ForegroundBleService.kt +++ b/lib_service/src/main/java/no/nordicsemi/android/service/ForegroundBleService.kt @@ -27,6 +27,7 @@ import android.app.NotificationManager import android.app.PendingIntent import android.content.Intent import android.os.Build +import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat @@ -83,7 +84,9 @@ abstract class ForegroundBleService : BleProfileService() { * @param defaults */ private fun createNotification(messageResId: Int, defaults: Int): Notification { - createNotificationChannel(CHANNEL_ID) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + createNotificationChannel(CHANNEL_ID) + } val intent: Intent? = packageManager.getLaunchIntentForPackage(packageName) val pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE) @@ -97,6 +100,7 @@ abstract class ForegroundBleService : BleProfileService() { .build() } + @RequiresApi(Build.VERSION_CODES.O) private fun createNotificationChannel(channelName: String) { val channel = NotificationChannel( channelName, diff --git a/lib_theme/src/main/java/no/nordicsemi/android/theme/view/SectionTitle.kt b/lib_theme/src/main/java/no/nordicsemi/android/theme/view/SectionTitle.kt index 9957248a..1afa8f98 100644 --- a/lib_theme/src/main/java/no/nordicsemi/android/theme/view/SectionTitle.kt +++ b/lib_theme/src/main/java/no/nordicsemi/android/theme/view/SectionTitle.kt @@ -41,7 +41,7 @@ fun SectionTitle( ) .padding(8.dp) ) - Spacer(modifier = Modifier.padding(8.dp)) + Spacer(modifier = Modifier.size(8.dp)) Text( text = title, textAlign = TextAlign.Center, @@ -73,7 +73,7 @@ fun SectionTitle( ) .padding(8.dp) ) - Spacer(modifier = Modifier.padding(8.dp)) + Spacer(modifier = Modifier.size(8.dp)) Text( text = title, textAlign = TextAlign.Center, diff --git a/lib_theme/src/main/java/no/nordicsemi/android/theme/view/SensorRecordCard.kt b/lib_theme/src/main/java/no/nordicsemi/android/theme/view/SensorRecordCard.kt index 26624f0d..08d5e384 100644 --- a/lib_theme/src/main/java/no/nordicsemi/android/theme/view/SensorRecordCard.kt +++ b/lib_theme/src/main/java/no/nordicsemi/android/theme/view/SensorRecordCard.kt @@ -1,5 +1,6 @@ package no.nordicsemi.android.theme.view +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -12,14 +13,26 @@ import androidx.compose.ui.unit.dp import no.nordicsemi.android.material.you.Card @Composable -fun ScreenSection(content: @Composable () -> Unit) { +fun ScreenSection(onClick: (() -> Unit)? = null, content: @Composable () -> Unit) { Card( backgroundColor = MaterialTheme.colorScheme.secondaryContainer, shape = RoundedCornerShape(16.dp), elevation = 0.dp, ) { + + val modifier = if (onClick != null) { + Modifier + .clickable { onClick.invoke() } + .fillMaxWidth() + .padding(16.dp) + } else { + Modifier + .fillMaxWidth() + .padding(16.dp) + } + Column( - modifier = Modifier.fillMaxWidth().padding(16.dp), + modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally ) { content() diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMContentView.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMContentView.kt index 4ee79d73..21011594 100644 --- a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMContentView.kt +++ b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMContentView.kt @@ -104,7 +104,7 @@ private fun RecordsViewWithData(state: CGMData) { RecordItem(it) if (i < state.records.size - 1) { - Spacer(modifier = Modifier.padding(8.dp)) + Spacer(modifier = Modifier.size(8.dp)) } } } @@ -129,7 +129,7 @@ private fun RecordItem(record: CGMRecord) { ) } - Spacer(modifier = Modifier.padding(16.dp)) + Spacer(modifier = Modifier.size(16.dp)) Text( text = record.glucoseConcentration(), diff --git a/profile_dfu/src/main/java/no/nordicsemi/dfu/data/DFUData.kt b/profile_dfu/src/main/java/no/nordicsemi/dfu/data/DFUData.kt index 33cc3b62..bc21534a 100644 --- a/profile_dfu/src/main/java/no/nordicsemi/dfu/data/DFUData.kt +++ b/profile_dfu/src/main/java/no/nordicsemi/dfu/data/DFUData.kt @@ -13,8 +13,9 @@ internal data class FileReadyState( val device: DiscoveredBluetoothDevice ) : DFUData() -internal data class HexFileReadyState( - val file: DFUFile +internal data class HexFileLoadedState( + val file: PartialHexFile, + val isDatFileError: Boolean = false ) : DFUData() internal data class FileInstallingState( @@ -23,4 +24,4 @@ internal data class FileInstallingState( internal object UploadSuccessState : DFUData() -internal object UploadFailureState : DFUData() +internal data class UploadFailureState(val message: String?) : DFUData() diff --git a/profile_dfu/src/main/java/no/nordicsemi/dfu/data/DFUFile.kt b/profile_dfu/src/main/java/no/nordicsemi/dfu/data/DFUFile.kt index 7137dac3..45cf23ce 100644 --- a/profile_dfu/src/main/java/no/nordicsemi/dfu/data/DFUFile.kt +++ b/profile_dfu/src/main/java/no/nordicsemi/dfu/data/DFUFile.kt @@ -10,7 +10,12 @@ data class ZipFile(val data: FileData) : DFUFile() { override val fileType: DFUFileType = DFUFileType.TYPE_AUTO } -data class HexFile( +data class PartialHexFile( + val data: FileData, + val fileType: DFUFileType +) + +data class FullHexFile( val data: FileData, val datFileData: FileData, override val fileType: DFUFileType @@ -19,6 +24,6 @@ data class HexFile( data class FileData( val uri: Uri, val name: String, - val path: String, + val path: String?, val size: Long ) diff --git a/profile_dfu/src/main/java/no/nordicsemi/dfu/data/DFUFileManager.kt b/profile_dfu/src/main/java/no/nordicsemi/dfu/data/DFUFileManager.kt index 868d8b39..427bda54 100644 --- a/profile_dfu/src/main/java/no/nordicsemi/dfu/data/DFUFileManager.kt +++ b/profile_dfu/src/main/java/no/nordicsemi/dfu/data/DFUFileManager.kt @@ -3,6 +3,7 @@ package no.nordicsemi.dfu.data import android.content.Context import android.net.Uri import android.provider.MediaStore +import android.util.Log import androidx.core.net.toFile import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject @@ -12,13 +13,17 @@ class DFUFileManager @Inject constructor( private val context: Context ) { + private val TAG = "DFU_FILE_MANAGER" + fun createFile(uri: Uri): FileData? { return try { createFromFile(uri) } catch (e: Exception) { + Log.e(TAG, "Error during creation file from uri.", e) try { createFromContentResolver(uri) } catch (e: Exception) { + Log.e(TAG, "Error during loading file from content resolver.", e) null } } @@ -30,26 +35,27 @@ class DFUFileManager @Inject constructor( } private fun createFromContentResolver(uri: Uri): FileData? { - return try { - val data = context.contentResolver.query(uri, null, null, null, null) + val data = context.contentResolver.query(uri, null, null, null, null) - if (data != null && data.moveToNext()) { + return if (data != null && data.moveToNext()) { - val displayNameIndex = data.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME) - val fileSizeIndex = data.getColumnIndex(MediaStore.MediaColumns.SIZE) - val dataIndex = data.getColumnIndex(MediaStore.MediaColumns.DATA) + val displayNameIndex = data.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME) + val fileSizeIndex = data.getColumnIndex(MediaStore.MediaColumns.SIZE) + val dataIndex = data.getColumnIndex(MediaStore.MediaColumns.DATA) - val fileName = data.getString(displayNameIndex) - val fileSize = data.getInt(fileSizeIndex) - val filePath = data.getString(dataIndex) - - data.close() - - FileData(uri, fileName, filePath, fileSize.toLong()) + val fileName = data.getString(displayNameIndex) + val fileSize = data.getInt(fileSizeIndex) + val filePath = if (dataIndex != -1) { + data.getString(dataIndex) } else { null } - } catch (e: Exception) { + + data.close() + + FileData(uri, fileName, filePath, fileSize.toLong()) + } else { + Log.d(TAG, "Data loaded from ContentResolver is empty.") null } } diff --git a/profile_dfu/src/main/java/no/nordicsemi/dfu/data/DFUFileType.kt b/profile_dfu/src/main/java/no/nordicsemi/dfu/data/DFUFileType.kt index 37f79065..196b6143 100644 --- a/profile_dfu/src/main/java/no/nordicsemi/dfu/data/DFUFileType.kt +++ b/profile_dfu/src/main/java/no/nordicsemi/dfu/data/DFUFileType.kt @@ -1,10 +1,12 @@ package no.nordicsemi.dfu.data +import no.nordicsemi.android.dfu.DfuBaseService + enum class DFUFileType(val id: Int) { - TYPE_AUTO(0x00), - TYPE_SOFT_DEVICE(0x01), - TYPE_BOOTLOADER(0x02), - TYPE_APPLICATION(0x04); + TYPE_AUTO(DfuBaseService.TYPE_AUTO), + TYPE_SOFT_DEVICE(DfuBaseService.TYPE_SOFT_DEVICE), + TYPE_BOOTLOADER(DfuBaseService.TYPE_BOOTLOADER), + TYPE_APPLICATION(DfuBaseService.TYPE_APPLICATION); companion object { fun create(id: Int): DFUFileType? { diff --git a/profile_dfu/src/main/java/no/nordicsemi/dfu/data/DFUManager.kt b/profile_dfu/src/main/java/no/nordicsemi/dfu/data/DFUManager.kt index eb83dea4..f0e9a753 100644 --- a/profile_dfu/src/main/java/no/nordicsemi/dfu/data/DFUManager.kt +++ b/profile_dfu/src/main/java/no/nordicsemi/dfu/data/DFUManager.kt @@ -17,7 +17,7 @@ class DFUManager @Inject constructor( fun install(file: DFUFile) { val device = deviceHolder.device!! - val starter = DfuServiceInitiator(device.address) + val starter = DfuServiceInitiator(device.address()) .setDeviceName(device.displayName()) // .setKeepBond(keepBond) // .setForceDfu(forceDfu) @@ -27,9 +27,9 @@ class DFUManager @Inject constructor( .setUnsafeExperimentalButtonlessServiceInSecureDfuEnabled(true) when (file) { - is ZipFile -> starter.setZip(file.uri, file.path) - is HexFile -> starter.setBinOrHex(file.fileType.id, file.uri, file.path) - .setInitFile(file.datFile.uri, file.datFile.path) + is ZipFile -> starter.setZip(file.data.uri, file.data.path) + is FullHexFile -> starter.setBinOrHex(file.fileType.id, file.data.uri, file.data.path) + .setInitFile(file.datFileData.uri, file.datFileData.path) }.exhaustive starter.start(context, DFUService::class.java) diff --git a/profile_dfu/src/main/java/no/nordicsemi/dfu/data/DFURepository.kt b/profile_dfu/src/main/java/no/nordicsemi/dfu/data/DFURepository.kt index 89de121a..dda65262 100644 --- a/profile_dfu/src/main/java/no/nordicsemi/dfu/data/DFURepository.kt +++ b/profile_dfu/src/main/java/no/nordicsemi/dfu/data/DFURepository.kt @@ -1,28 +1,56 @@ package no.nordicsemi.dfu.data +import android.net.Uri import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import no.nordicsemi.android.service.SelectedBluetoothDeviceHolder import javax.inject.Inject import javax.inject.Singleton @Singleton internal class DFURepository @Inject constructor( - private val deviceHolder: SelectedBluetoothDeviceHolder + private val deviceHolder: SelectedBluetoothDeviceHolder, + private val fileManger: DFUFileManager ) { private val _data = MutableStateFlow(NoFileSelectedState()) - val data: StateFlow = _data + val data: StateFlow = _data.asStateFlow() - fun initFile(file: DFUFile?) { - if (file == null) { - _data.value = NoFileSelectedState(true) - } else { - _data.value = FileReadyState(file, deviceHolder.device!!) - } + fun setZipFile(file: Uri) { + val currentState = _data.value as NoFileSelectedState + _data.value = fileManger.createFile(file)?.let { + FileReadyState(ZipFile(it), requireNotNull(deviceHolder.device)) + } ?: currentState.copy(isError = true) + } + + fun setHexFile(file: Uri) { + val currentState = _data.value as NoFileSelectedState + _data.value = fileManger.createFile(file)?.let { + HexFileLoadedState(PartialHexFile(it, DFUFileType.TYPE_APPLICATION)) + } ?: currentState.copy(isError = true) + } + + fun setDatFile(file: Uri) { + val currentState = _data.value as HexFileLoadedState + _data.value = fileManger.createFile(file)?.let { + FileReadyState(FullHexFile(it, currentState.file.data, DFUFileType.TYPE_APPLICATION), requireNotNull(deviceHolder.device)) + } ?: currentState.copy(isDatFileError = true) + } + + fun setSuccess() { + _data.value = UploadSuccessState + } + + fun setError(message: String?) { + _data.value = UploadFailureState(message) } fun install() { _data.value = FileInstallingState() } + + fun clear() { + _data.value = NoFileSelectedState() + } } diff --git a/profile_dfu/src/main/java/no/nordicsemi/dfu/repository/DFUService.kt b/profile_dfu/src/main/java/no/nordicsemi/dfu/repository/DFUService.kt index 8acb01de..d450d6b9 100644 --- a/profile_dfu/src/main/java/no/nordicsemi/dfu/repository/DFUService.kt +++ b/profile_dfu/src/main/java/no/nordicsemi/dfu/repository/DFUService.kt @@ -22,11 +22,25 @@ package no.nordicsemi.dfu.repository import android.app.Activity +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.os.Build +import androidx.annotation.RequiresApi import no.nordicsemi.android.dfu.DfuBaseService -import no.nordicsemi.dfu.view.NotificationActivity +import no.nordicsemi.dfu.R class DFUService : DfuBaseService() { + override fun onCreate() { + super.onCreate() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + createDfuNotificationChannel(this) + } + } + override fun getNotificationTarget(): Class? { /* * As a target activity the NotificationActivity is returned, not the MainActivity. This is because the notification must create a new task: @@ -41,11 +55,26 @@ class DFUService : DfuBaseService() { * This method may be used to restore the target activity in case the application was closed or is open. It may also be used to recreate an activity * history (see NotificationActivity). */ - return NotificationActivity::class.java + return Class.forName("no.nordicsemi.android.nrftoolbox.MainActivity") as Class } override fun isDebug(): Boolean { // return BuildConfig.DEBUG; return true } + + @RequiresApi(api = Build.VERSION_CODES.O) + private fun createDfuNotificationChannel(context: Context) { + val channel = NotificationChannel( + NOTIFICATION_CHANNEL_DFU, + context.getString(R.string.dfu_channel_name), + NotificationManager.IMPORTANCE_LOW + ) + channel.description = context.getString(R.string.dfu_channel_description) + channel.setShowBadge(false) + channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC + val notificationManager = + context.getSystemService(NOTIFICATION_SERVICE) as NotificationManager + notificationManager?.createNotificationChannel(channel) + } } diff --git a/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUContentView.kt b/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUContentView.kt index 27371fff..2cec133c 100644 --- a/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUContentView.kt +++ b/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUContentView.kt @@ -14,10 +14,10 @@ internal fun DFUContentView(state: DFUData, onEvent: (DFUViewEvent) -> Unit) { when (state) { is NoFileSelectedState -> DFUSelectMainFileView(state, onEvent) is FileReadyState -> DFUSummaryView(state, onEvent) + is HexFileLoadedState -> DFUSelectDatFileView(state, onEvent) UploadSuccessState -> DFUSuccessView(onEvent) - UploadFailureState -> DFUErrorView(onEvent) + is UploadFailureState -> DFUErrorView(state, onEvent) is FileInstallingState -> DFUInstallingView(state, onEvent) - is HexFileReadyState -> DFUSelectDatFileView(onEvent) }.exhaustive } } diff --git a/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUErrorView.kt b/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUErrorView.kt index 5830d657..f2e2809e 100644 --- a/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUErrorView.kt +++ b/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUErrorView.kt @@ -2,27 +2,43 @@ package no.nordicsemi.dfu.view import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer -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.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import no.nordicsemi.android.theme.view.ScreenSection import no.nordicsemi.dfu.R +import no.nordicsemi.dfu.data.UploadFailureState @Composable -internal fun DFUErrorView(onEvent: (DFUViewEvent) -> Unit) { +internal fun DFUErrorView(state: UploadFailureState, onEvent: (DFUViewEvent) -> Unit) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + ScreenSection { + Icon( + painter = painterResource(id = R.drawable.ic_fail_circle), + contentDescription = stringResource(id = R.string.dfu_failure_icon_description), + tint = MaterialTheme.colorScheme.error + ) - Column { - Icon( - painter = painterResource(id = R.drawable.ic_fail_circle), - contentDescription = stringResource(id = R.string.dfu_failure_icon_description) - ) + Spacer(modifier = Modifier.size(16.dp)) - Spacer(modifier = Modifier.padding(16.dp)) + val error = state.message ?: stringResource(id = R.string.dfu_unknown_error) + Text( + text = error, + color = MaterialTheme.colorScheme.error + ) + + Spacer(modifier = Modifier.size(16.dp)) + } + + Spacer(modifier = Modifier.size(16.dp)) Button(onClick = { onEvent(OnPauseButtonClick) }) { Text(text = stringResource(id = R.string.dfu_close)) diff --git a/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUInstallingView.kt b/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUInstallingView.kt index 62869519..20111c55 100644 --- a/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUInstallingView.kt +++ b/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUInstallingView.kt @@ -1,26 +1,24 @@ package no.nordicsemi.dfu.view -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import no.nordicsemi.android.material.you.CircularProgressIndicator +import no.nordicsemi.android.theme.view.ScreenSection import no.nordicsemi.dfu.R import no.nordicsemi.dfu.data.FileInstallingState @Composable internal fun DFUInstallingView(state: FileInstallingState, onEvent: (DFUViewEvent) -> Unit) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxWidth() - ) { + ScreenSection { CircularProgressIndicator() Spacer(modifier = Modifier.height(16.dp)) @@ -29,14 +27,16 @@ internal fun DFUInstallingView(state: FileInstallingState, onEvent: (DFUViewEven Spacer(modifier = Modifier.height(16.dp)) - Button(onClick = { onEvent(OnPauseButtonClick) }) { - Text(text = stringResource(id = R.string.dfu_pause)) - } + Row(horizontalArrangement = Arrangement.SpaceEvenly) { + Button(onClick = { onEvent(OnPauseButtonClick) }) { + Text(text = stringResource(id = R.string.dfu_pause)) + } - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.size(16.dp)) - Button(onClick = { onEvent(OnPauseButtonClick) }) { - Text(text = stringResource(id = R.string.dfu_stop)) + Button(onClick = { onEvent(OnPauseButtonClick) }) { + Text(text = stringResource(id = R.string.dfu_stop)) + } } } } diff --git a/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUScreen.kt b/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUScreen.kt index d5664ce2..f657d6c8 100644 --- a/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUScreen.kt +++ b/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUScreen.kt @@ -14,12 +14,11 @@ import no.nordicsemi.dfu.R import no.nordicsemi.dfu.data.DFUData import no.nordicsemi.dfu.repository.DFUService import no.nordicsemi.dfu.viewmodel.DFUViewModel -import no.nordicsemi.dfu.data.NoFileSelectedState @Composable fun DFUScreen(finishAction: () -> Unit) { val viewModel: DFUViewModel = hiltViewModel() - val state = viewModel.state.collectAsState(NoFileSelectedState).value + val state = viewModel.state.collectAsState().value val isScreenActive = viewModel.isActive.collectAsState().value val context = LocalContext.current diff --git a/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUSelectDatFileView.kt b/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUSelectDatFileView.kt index a6bd34c7..a92c19cd 100644 --- a/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUSelectDatFileView.kt +++ b/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUSelectDatFileView.kt @@ -4,6 +4,7 @@ import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.Button @@ -17,23 +18,34 @@ import no.nordicsemi.android.dfu.DfuBaseService import no.nordicsemi.android.theme.view.ScreenSection import no.nordicsemi.android.theme.view.SectionTitle import no.nordicsemi.dfu.R +import no.nordicsemi.dfu.data.HexFileLoadedState @Composable -internal fun DFUSelectDatFileView(onEvent: (DFUViewEvent) -> Unit) { +internal fun DFUSelectDatFileView(state: HexFileLoadedState, onEvent: (DFUViewEvent) -> Unit) { ScreenSection { SectionTitle( icon = Icons.Default.Settings, title = stringResource(id = R.string.dfu_choose_file) ) - Spacer(modifier = Modifier.padding(8.dp)) + Spacer(modifier = Modifier.size(8.dp)) Text( text = stringResource(id = R.string.dfu_choose_dat_info), style = MaterialTheme.typography.bodyMedium ) - Spacer(modifier = Modifier.padding(8.dp)) + if (state.isDatFileError) { + Spacer(modifier = Modifier.size(8.dp)) + + Text( + text = stringResource(id = R.string.dfu_load_file_error), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.error + ) + } + + Spacer(modifier = Modifier.size(8.dp)) val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> uri?.let { onEvent(OnDatFileSelected(it)) } diff --git a/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUSelectMainFileView.kt b/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUSelectMainFileView.kt index a97751c6..fa3b8de6 100644 --- a/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUSelectMainFileView.kt +++ b/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUSelectMainFileView.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.Button @@ -22,24 +23,34 @@ import no.nordicsemi.android.dfu.DfuBaseService import no.nordicsemi.android.theme.view.ScreenSection import no.nordicsemi.android.theme.view.SectionTitle import no.nordicsemi.dfu.R -import no.nordicsemi.dfu.data.DFUData +import no.nordicsemi.dfu.data.NoFileSelectedState @Composable -internal fun DFUSelectMainFileView(state: DFUData, onEvent: (DFUViewEvent) -> Unit) { +internal fun DFUSelectMainFileView(state: NoFileSelectedState, onEvent: (DFUViewEvent) -> Unit) { ScreenSection { SectionTitle( icon = Icons.Default.Settings, title = stringResource(id = R.string.dfu_choose_file) ) - Spacer(modifier = Modifier.padding(8.dp)) + Spacer(modifier = Modifier.size(8.dp)) Text( text = stringResource(id = R.string.dfu_choose_info), style = MaterialTheme.typography.bodyMedium ) - Spacer(modifier = Modifier.padding(8.dp)) + if (state.isError) { + Spacer(modifier = Modifier.size(8.dp)) + + Text( + text = stringResource(id = R.string.dfu_load_file_error), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.error + ) + } + + Spacer(modifier = Modifier.size(8.dp)) ButtonsRow(onEvent) } diff --git a/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUSuccessView.kt b/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUSuccessView.kt index 6f8d9982..187db34e 100644 --- a/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUSuccessView.kt +++ b/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUSuccessView.kt @@ -2,28 +2,40 @@ package no.nordicsemi.dfu.view import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer -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.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import no.nordicsemi.android.theme.view.ScreenSection import no.nordicsemi.dfu.R @Composable internal fun DFUSuccessView(onEvent: (DFUViewEvent) -> Unit) { Column(horizontalAlignment = Alignment.CenterHorizontally) { + ScreenSection { + Icon( + painter = painterResource(id = R.drawable.ic_success_circle), + contentDescription = stringResource(id = R.string.dfu_success_icon_description), + tint = colorResource(id = no.nordicsemi.android.material.you.R.color.nordicGrass) + ) + } - Icon( - painter = painterResource(id = R.drawable.ic_success_circle), - contentDescription = stringResource(id = R.string.dfu_success_icon_description) + Spacer(modifier = Modifier.size(16.dp)) + + Text( + text = stringResource(id = R.string.dfu_success), + color = MaterialTheme.colorScheme.error ) - Spacer(modifier = Modifier.padding(16.dp)) + Spacer(modifier = Modifier.size(16.dp)) Button(onClick = { onEvent(OnPauseButtonClick) }) { Text(text = stringResource(id = R.string.dfu_done)) diff --git a/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUSummaryView.kt b/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUSummaryView.kt index 899f3bad..870af747 100644 --- a/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUSummaryView.kt +++ b/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUSummaryView.kt @@ -2,7 +2,14 @@ package no.nordicsemi.dfu.view import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Notifications @@ -20,8 +27,10 @@ import no.nordicsemi.android.theme.view.ScreenSection import no.nordicsemi.android.theme.view.SectionTitle import no.nordicsemi.dfu.R import no.nordicsemi.dfu.data.FileReadyState +import no.nordicsemi.dfu.data.FullHexFile +import no.nordicsemi.dfu.data.ZipFile import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice -import java.io.File +import no.nordicsemi.ui.scanner.ui.exhaustive @Composable internal fun DFUSummaryView(state: FileReadyState, onEvent: (DFUViewEvent) -> Unit) { @@ -30,7 +39,10 @@ internal fun DFUSummaryView(state: FileReadyState, onEvent: (DFUViewEvent) -> Un Spacer(modifier = Modifier.height(16.dp)) - FileDetailsView(state.file) + when (state.file) { + is FullHexFile -> FileDetailsView(state.file) + is ZipFile -> FileDetailsView(state.file) + }.exhaustive Spacer(modifier = Modifier.height(16.dp)) @@ -59,7 +71,7 @@ internal fun DeviceDetailsView(device: DiscoveredBluetoothDevice) { .padding(8.dp) ) - Spacer(modifier = Modifier.padding(8.dp)) + Spacer(modifier = Modifier.size(8.dp)) Column(modifier = Modifier .fillMaxWidth() @@ -75,19 +87,37 @@ internal fun DeviceDetailsView(device: DiscoveredBluetoothDevice) { } @Composable -private fun FileDetailsView(file: File) { - val fileName = file.name - val fileLength = file.length() +private fun FileDetailsView(file: ZipFile) { + val fileName = file.data.name + val fileLength = file.data.size ScreenSection { - SectionTitle(icon = Icons.Default.Notifications, title = stringResource(id = R.string.dfu_file_details)) + SectionTitle(icon = Icons.Default.Notifications, title = stringResource(id = R.string.dfu_zip_file_details)) - Spacer(modifier = Modifier.padding(16.dp)) + Spacer(modifier = Modifier.size(16.dp)) Text(text = fileName) - Spacer(modifier = Modifier.padding(4.dp)) + Spacer(modifier = Modifier.size(4.dp)) Text(text = stringResource(id = R.string.dfu_file_size, fileLength)) } } + +@Composable +private fun FileDetailsView(file: FullHexFile) { + val fileName = file.data.name + val fileLength = file.data.size + + ScreenSection { + SectionTitle(icon = Icons.Default.Notifications, title = stringResource(id = R.string.dfu_hex_file_details)) + + Spacer(modifier = Modifier.size(16.dp)) + + Text(text = fileName) + + Spacer(modifier = Modifier.size(4.dp)) + + Text(text = stringResource(id = R.string.dfu_file_size, fileLength)) + } +} diff --git a/profile_dfu/src/main/java/no/nordicsemi/dfu/view/NotificationActivity.kt b/profile_dfu/src/main/java/no/nordicsemi/dfu/view/NotificationActivity.kt deleted file mode 100644 index 207d4370..00000000 --- a/profile_dfu/src/main/java/no/nordicsemi/dfu/view/NotificationActivity.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (c) 2015, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE - * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -package no.nordicsemi.dfu.view - -import android.app.Activity -import android.os.Bundle - -class NotificationActivity : Activity() { - - protected override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - // If this activity is the root activity of the task, the app is not running - if (isTaskRoot()) { - // Start the app before finishing - //TODO -// val parentIntent = Intent(this, FeaturesActivity::class.java) -// parentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) -// val startAppIntent = Intent(this, DfuActivity::class.java) -// if (getIntent() != null && getIntent().getExtras() != null) startAppIntent.putExtras( -// getIntent().getExtras() -// ) -// startActivities(arrayOf(parentIntent, startAppIntent)) - } - - // Now finish, which will drop the user in to the activity that was at the top - // of the task stack - finish() - } -} \ No newline at end of file diff --git a/profile_dfu/src/main/java/no/nordicsemi/dfu/viewmodel/DFUViewModel.kt b/profile_dfu/src/main/java/no/nordicsemi/dfu/viewmodel/DFUViewModel.kt index 436abd1a..2bf3f57a 100644 --- a/profile_dfu/src/main/java/no/nordicsemi/dfu/viewmodel/DFUViewModel.kt +++ b/profile_dfu/src/main/java/no/nordicsemi/dfu/viewmodel/DFUViewModel.kt @@ -1,16 +1,23 @@ package no.nordicsemi.dfu.viewmodel +import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import no.nordicsemi.android.service.SelectedBluetoothDeviceHolder import no.nordicsemi.android.theme.viewmodel.CloseableViewModel import no.nordicsemi.android.utils.exhaustive +import no.nordicsemi.dfu.data.Completed import no.nordicsemi.dfu.data.DFUFile -import no.nordicsemi.dfu.data.DFUFileManager import no.nordicsemi.dfu.data.DFUManager import no.nordicsemi.dfu.data.DFUProgressManager import no.nordicsemi.dfu.data.DFURepository +import no.nordicsemi.dfu.data.DFUServiceStatus +import no.nordicsemi.dfu.data.Error import no.nordicsemi.dfu.data.FileInstallingState import no.nordicsemi.dfu.data.FileReadyState +import no.nordicsemi.dfu.data.NoFileSelectedState import no.nordicsemi.dfu.view.DFUViewEvent import no.nordicsemi.dfu.view.OnDatFileSelected import no.nordicsemi.dfu.view.OnDisconnectButtonClick @@ -25,15 +32,15 @@ import javax.inject.Inject internal class DFUViewModel @Inject constructor( private val repository: DFURepository, private val progressManager: DFUProgressManager, - private val dfuManager: DFUManager, - private val fileManger: DFUFileManager + private val deviceHolder: SelectedBluetoothDeviceHolder, + private val dfuManager: DFUManager ) : CloseableViewModel() { val state = repository.data.combine(progressManager.status) { state, status -> - (state as? FileInstallingState)?.run { - state.copy(status = status) - } ?: state - } + (state as? FileInstallingState) + ?.run { createInstallingStateWithNewStatus(state, status) } + ?: state + }.stateIn(viewModelScope, SharingStarted.Eagerly, NoFileSelectedState()) init { progressManager.registerListener() @@ -41,23 +48,42 @@ internal class DFUViewModel @Inject constructor( fun onEvent(event: DFUViewEvent) { when (event) { - OnDisconnectButtonClick -> finish() + OnDisconnectButtonClick -> closeScreen() OnInstallButtonClick -> { dfuManager.install(requireFile()) repository.install() } - OnPauseButtonClick -> finish() - OnStopButtonClick -> finish() - is OnHexFileSelected -> repository. - is OnZipFileSelected -> TODO() - is OnDatFileSelected -> TODO() + OnPauseButtonClick -> closeScreen() + OnStopButtonClick -> closeScreen() + is OnHexFileSelected -> repository.setHexFile(event.file) + is OnZipFileSelected -> repository.setZipFile(event.file) + is OnDatFileSelected -> repository.setDatFile(event.file) }.exhaustive } + private fun closeScreen() { + repository.clear() + deviceHolder.forgetDevice() + finish() + } + private fun requireFile(): DFUFile { return (repository.data.value as FileReadyState).file } + private fun createInstallingStateWithNewStatus( + state: FileInstallingState, + status: DFUServiceStatus + ): FileInstallingState { + if (status is Error) { + repository.setError(status.message) + } + if (status is Completed) { + repository.setSuccess() + } + return state.copy(status = status) + } + override fun onCleared() { super.onCleared() progressManager.unregisterListener() diff --git a/profile_dfu/src/main/res/drawable/ic_fail_circle.xml b/profile_dfu/src/main/res/drawable/ic_fail_circle.xml index 3b451189..2a183c48 100644 --- a/profile_dfu/src/main/res/drawable/ic_fail_circle.xml +++ b/profile_dfu/src/main/res/drawable/ic_fail_circle.xml @@ -3,7 +3,7 @@ android:height="80dp" android:viewportWidth="24" android:viewportHeight="24"> - + diff --git a/profile_dfu/src/main/res/values/strings.xml b/profile_dfu/src/main/res/values/strings.xml index 8dc227e9..4a43bd02 100644 --- a/profile_dfu/src/main/res/values/strings.xml +++ b/profile_dfu/src/main/res/values/strings.xml @@ -12,7 +12,10 @@ Select .zip Select .hex - File details + An error occurred during loading the file. Please try with another file. + + Zip file details + Hex file details %d bytes Distribution packet (ZIP) @@ -41,9 +44,12 @@ Enabling DFU Error Idle - %d% + %d\%% Started Starting Validating Status: %s + + Success! + Unknown error. diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/view/GLSContentView.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/view/GLSContentView.kt index c2cae599..4e71036a 100644 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/view/GLSContentView.kt +++ b/profile_gls/src/main/java/no/nordicsemi/android/gls/view/GLSContentView.kt @@ -107,7 +107,7 @@ private fun RecordsViewWithData(state: GLSData) { RecordItem(it) if (i < state.records.size-1) { - Spacer(modifier = Modifier.padding(8.dp)) + Spacer(modifier = Modifier.size(8.dp)) } } } @@ -134,7 +134,7 @@ private fun RecordItem(record: GLSRecord) { ) } - Spacer(modifier = Modifier.padding(16.dp)) + Spacer(modifier = Modifier.size(16.dp)) Text( text = glucoseConcentrationDisplayValue(record.glucoseConcentration, record.unit), diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTAddMacroDialog.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTAddMacroDialog.kt index acd3d3ae..a9c93833 100644 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTAddMacroDialog.kt +++ b/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTAddMacroDialog.kt @@ -3,6 +3,7 @@ package no.nordicsemi.android.uart.view import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.material3.AlertDialog import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -33,7 +34,7 @@ internal fun UARTAddMacroDialog(onDismiss: () -> Unit, onEvent: (UARTViewEvent) alias.value = it } - Spacer(modifier = Modifier.padding(16.dp)) + Spacer(modifier = Modifier.size(16.dp)) TextField(text = command.value, hint = stringResource(id = R.string.uart_macro_dialog_command)) { command.value = it diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTViews.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTViews.kt index 76b637b8..dca55b42 100644 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTViews.kt +++ b/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTViews.kt @@ -32,7 +32,7 @@ internal fun MacroItem(macro: UARTMacro, onEvent: (UARTViewEvent) -> Unit) { .clickable { onEvent(OnRunMacro(macro)) } ) - Spacer(modifier = Modifier.padding(16.dp)) + Spacer(modifier = Modifier.size(16.dp)) Column(modifier = Modifier.weight(1f)) { Text( @@ -46,7 +46,7 @@ internal fun MacroItem(macro: UARTMacro, onEvent: (UARTViewEvent) -> Unit) { ) } - Spacer(modifier = Modifier.padding(16.dp)) + Spacer(modifier = Modifier.size(16.dp)) Icon( imageVector = Icons.Default.Delete,