diff --git a/profile_dfu/src/main/AndroidManifest.xml b/profile_dfu/src/main/AndroidManifest.xml index 47115554..ee941bfa 100644 --- a/profile_dfu/src/main/AndroidManifest.xml +++ b/profile_dfu/src/main/AndroidManifest.xml @@ -2,4 +2,7 @@ + + + \ No newline at end of file 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 05baab23..33cc3b62 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 @@ -1,16 +1,24 @@ package no.nordicsemi.dfu.data import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice -import java.io.File internal sealed class DFUData -internal object NoFileSelectedState : DFUData() +internal data class NoFileSelectedState( + val isError: Boolean = false +) : DFUData() internal data class FileReadyState( - val file: File, - val device: DiscoveredBluetoothDevice, - val isUploading: Boolean = false + val file: DFUFile, + val device: DiscoveredBluetoothDevice +) : DFUData() + +internal data class HexFileReadyState( + val file: DFUFile +) : DFUData() + +internal data class FileInstallingState( + val status: DFUServiceStatus = Idle ) : DFUData() internal object UploadSuccessState : 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 new file mode 100644 index 00000000..7137dac3 --- /dev/null +++ b/profile_dfu/src/main/java/no/nordicsemi/dfu/data/DFUFile.kt @@ -0,0 +1,24 @@ +package no.nordicsemi.dfu.data + +import android.net.Uri + +sealed class DFUFile { + abstract val fileType: DFUFileType +} + +data class ZipFile(val data: FileData) : DFUFile() { + override val fileType: DFUFileType = DFUFileType.TYPE_AUTO +} + +data class HexFile( + val data: FileData, + val datFileData: FileData, + override val fileType: DFUFileType +) : DFUFile() + +data class FileData( + val uri: Uri, + val name: 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 new file mode 100644 index 00000000..868d8b39 --- /dev/null +++ b/profile_dfu/src/main/java/no/nordicsemi/dfu/data/DFUFileManager.kt @@ -0,0 +1,56 @@ +package no.nordicsemi.dfu.data + +import android.content.Context +import android.net.Uri +import android.provider.MediaStore +import androidx.core.net.toFile +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +class DFUFileManager @Inject constructor( + @ApplicationContext + private val context: Context +) { + + fun createFile(uri: Uri): FileData? { + return try { + createFromFile(uri) + } catch (e: Exception) { + try { + createFromContentResolver(uri) + } catch (e: Exception) { + null + } + } + } + + private fun createFromFile(uri: Uri): FileData { + val file = uri.toFile() + return FileData(uri, file.name, file.path, file.length()) + } + + private fun createFromContentResolver(uri: Uri): FileData? { + return try { + val data = context.contentResolver.query(uri, null, null, null, null) + + 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 fileName = data.getString(displayNameIndex) + val fileSize = data.getInt(fileSizeIndex) + val filePath = data.getString(dataIndex) + + data.close() + + FileData(uri, fileName, filePath, fileSize.toLong()) + } else { + null + } + } catch (e: Exception) { + 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 new file mode 100644 index 00000000..37f79065 --- /dev/null +++ b/profile_dfu/src/main/java/no/nordicsemi/dfu/data/DFUFileType.kt @@ -0,0 +1,14 @@ +package no.nordicsemi.dfu.data + +enum class DFUFileType(val id: Int) { + TYPE_AUTO(0x00), + TYPE_SOFT_DEVICE(0x01), + TYPE_BOOTLOADER(0x02), + TYPE_APPLICATION(0x04); + + companion object { + fun create(id: Int): DFUFileType? { + return values().find { it.id == id } + } + } +} 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 new file mode 100644 index 00000000..eb83dea4 --- /dev/null +++ b/profile_dfu/src/main/java/no/nordicsemi/dfu/data/DFUManager.kt @@ -0,0 +1,37 @@ +package no.nordicsemi.dfu.data + +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext +import no.nordicsemi.android.dfu.DfuServiceInitiator +import no.nordicsemi.android.service.SelectedBluetoothDeviceHolder +import no.nordicsemi.dfu.repository.DFUService +import no.nordicsemi.ui.scanner.ui.exhaustive +import javax.inject.Inject + +class DFUManager @Inject constructor( + @ApplicationContext + private val context: Context, + private val deviceHolder: SelectedBluetoothDeviceHolder +) { + + fun install(file: DFUFile) { + val device = deviceHolder.device!! + + val starter = DfuServiceInitiator(device.address) + .setDeviceName(device.displayName()) +// .setKeepBond(keepBond) +// .setForceDfu(forceDfu) +// .setPacketsReceiptNotificationsEnabled(enablePRNs) +// .setPacketsReceiptNotificationsValue(numberOfPackets) + .setPrepareDataObjectDelay(400) + .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) + }.exhaustive + + starter.start(context, DFUService::class.java) + } +} diff --git a/profile_dfu/src/main/java/no/nordicsemi/dfu/data/DFUProgressManager.kt b/profile_dfu/src/main/java/no/nordicsemi/dfu/data/DFUProgressManager.kt new file mode 100644 index 00000000..0ebebea1 --- /dev/null +++ b/profile_dfu/src/main/java/no/nordicsemi/dfu/data/DFUProgressManager.kt @@ -0,0 +1,86 @@ +package no.nordicsemi.dfu.data + +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.android.scopes.ViewModelScoped +import kotlinx.coroutines.flow.MutableStateFlow +import no.nordicsemi.android.dfu.DfuProgressListenerAdapter +import no.nordicsemi.android.dfu.DfuServiceListenerHelper +import javax.inject.Inject + +@ViewModelScoped +internal class DFUProgressManager @Inject constructor( + @ApplicationContext + private val context: Context +) : DfuProgressListenerAdapter() { + + val status = MutableStateFlow(Idle) + + override fun onDeviceConnecting(deviceAddress: String) { + status.value = Connecting + } + + override fun onDeviceConnected(deviceAddress: String) { + status.value = Connected + } + + override fun onDfuProcessStarting(deviceAddress: String) { + status.value = Starting + } + + override fun onDfuProcessStarted(deviceAddress: String) { + status.value = Started + } + + override fun onEnablingDfuMode(deviceAddress: String) { + status.value = EnablingDfu + } + + override fun onProgressChanged( + deviceAddress: String, + percent: Int, + speed: Float, + avgSpeed: Float, + currentPart: Int, + partsTotal: Int + ) { + status.value = ProgressUpdate(percent) + } + + override fun onFirmwareValidating(deviceAddress: String) { + status.value = Validating + } + + override fun onDeviceDisconnecting(deviceAddress: String?) { + status.value = Disconnecting + } + + override fun onDeviceDisconnected(deviceAddress: String) { + status.value = Disconnected + } + + override fun onDfuCompleted(deviceAddress: String) { + status.value = Completed + } + + override fun onDfuAborted(deviceAddress: String) { + status.value = Aborted + } + + override fun onError( + deviceAddress: String, + error: Int, + errorType: Int, + message: String? + ) { + status.value = Error(message) + } + + fun registerListener() { + DfuServiceListenerHelper.registerProgressListener(context, this) + } + + fun unregisterListener() { + DfuServiceListenerHelper.unregisterProgressListener(context, this) + } +} 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 3c288471..89de121a 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 @@ -3,7 +3,6 @@ package no.nordicsemi.dfu.data import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import no.nordicsemi.android.service.SelectedBluetoothDeviceHolder -import java.io.File import javax.inject.Inject import javax.inject.Singleton @@ -12,15 +11,18 @@ internal class DFURepository @Inject constructor( private val deviceHolder: SelectedBluetoothDeviceHolder ) { - private val _data = MutableStateFlow(NoFileSelectedState) + private val _data = MutableStateFlow(NoFileSelectedState()) val data: StateFlow = _data - fun initFile(file: File) { - _data.value = FileReadyState(file, deviceHolder.device!!) + fun initFile(file: DFUFile?) { + if (file == null) { + _data.value = NoFileSelectedState(true) + } else { + _data.value = FileReadyState(file, deviceHolder.device!!) + } } fun install() { - val state = _data.value as FileReadyState - _data.value = state.copy(isUploading = true) + _data.value = FileInstallingState() } } diff --git a/profile_dfu/src/main/java/no/nordicsemi/dfu/data/DFUServiceStatus.kt b/profile_dfu/src/main/java/no/nordicsemi/dfu/data/DFUServiceStatus.kt new file mode 100644 index 00000000..bc011d09 --- /dev/null +++ b/profile_dfu/src/main/java/no/nordicsemi/dfu/data/DFUServiceStatus.kt @@ -0,0 +1,17 @@ +package no.nordicsemi.dfu.data + +internal sealed class DFUServiceStatus + +internal object Idle : DFUServiceStatus() +internal object Connecting : DFUServiceStatus() +internal object Connected : DFUServiceStatus() +internal object Starting : DFUServiceStatus() +internal object Started : DFUServiceStatus() +internal object EnablingDfu : DFUServiceStatus() +internal data class ProgressUpdate(val progress: Int): DFUServiceStatus() +internal object Validating : DFUServiceStatus() +internal object Disconnecting : DFUServiceStatus() +internal object Disconnected : DFUServiceStatus() +internal object Completed : DFUServiceStatus() +internal object Aborted : DFUServiceStatus() +internal data class Error(val message: String?): DFUServiceStatus() 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 634c8a73..8acb01de 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 @@ -48,4 +48,4 @@ class DFUService : DfuBaseService() { // return BuildConfig.DEBUG; return true } -} \ No newline at end of file +} 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 779aeab1..27371fff 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 @@ -12,18 +12,12 @@ import no.nordicsemi.dfu.data.* internal fun DFUContentView(state: DFUData, onEvent: (DFUViewEvent) -> Unit) { Box(modifier = Modifier.padding(16.dp)) { when (state) { - NoFileSelectedState -> DFUSelectFileView(onEvent) - is FileReadyState -> FileReadyView(state, onEvent) + is NoFileSelectedState -> DFUSelectMainFileView(state, onEvent) + is FileReadyState -> DFUSummaryView(state, onEvent) UploadSuccessState -> DFUSuccessView(onEvent) UploadFailureState -> DFUErrorView(onEvent) + is FileInstallingState -> DFUInstallingView(state, onEvent) + is HexFileReadyState -> DFUSelectDatFileView(onEvent) }.exhaustive } } - -@Composable -private fun FileReadyView(state: FileReadyState, onEvent: (DFUViewEvent) -> Unit) { - when (state.isUploading) { - false -> DFUSummaryView(state, onEvent) - true -> DFUInstallingView(state, onEvent) - }.exhaustive -} 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 865af584..62869519 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,68 +1,42 @@ package no.nordicsemi.dfu.view -import android.content.Context 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.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.core.net.toUri -import no.nordicsemi.android.dfu.DfuServiceInitiator +import androidx.compose.ui.unit.dp import no.nordicsemi.android.material.you.CircularProgressIndicator import no.nordicsemi.dfu.R -import no.nordicsemi.dfu.data.FileReadyState -import no.nordicsemi.dfu.repository.DFUService +import no.nordicsemi.dfu.data.FileInstallingState @Composable -internal fun DFUInstallingView(state: FileReadyState, onEvent: (DFUViewEvent) -> Unit) { - - Column { +internal fun DFUInstallingView(state: FileInstallingState, onEvent: (DFUViewEvent) -> Unit) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth() + ) { CircularProgressIndicator() - //todo add percentage indicator + Spacer(modifier = Modifier.height(16.dp)) + + Text(text = state.status.toDisplayString()) + + Spacer(modifier = Modifier.height(16.dp)) Button(onClick = { onEvent(OnPauseButtonClick) }) { Text(text = stringResource(id = R.string.dfu_pause)) } + Spacer(modifier = Modifier.height(16.dp)) + Button(onClick = { onEvent(OnPauseButtonClick) }) { Text(text = stringResource(id = R.string.dfu_stop)) } } - - val context = LocalContext.current - LaunchedEffect(state.isUploading) { - if (state.isUploading) { - install(context, state) - } - } -} - - - -private fun install(context: Context, state: FileReadyState) { - - val device = state.device - - val fileName = state.file.name - val fileLength = state.file.length() - - val starter = DfuServiceInitiator(device.address) - .setDeviceName(device.displayName()) -// .setKeepBond(keepBond) -// .setForceDfu(forceDfu) -// .setPacketsReceiptNotificationsEnabled(enablePRNs) -// .setPacketsReceiptNotificationsValue(numberOfPackets) -// .setPrepareDataObjectDelay(400) -// .setUnsafeExperimentalButtonlessServiceInSecureDfuEnabled(true) -// if (fileType == DfuService.TYPE_AUTO) { - starter.setZip(state.file.toUri(), state.file.path) -// if (scope != null) starter.setScope(scope) -// } else { -// starter.setBinOrHex(fileType, fileStreamUri, filePath) -// .setInitFile(initFileStreamUri, initFilePath) -// } - starter.start(context, DFUService::class.java) } diff --git a/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUMappers.kt b/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUMappers.kt new file mode 100644 index 00000000..cd19de20 --- /dev/null +++ b/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUMappers.kt @@ -0,0 +1,40 @@ +package no.nordicsemi.dfu.view + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import no.nordicsemi.dfu.R +import no.nordicsemi.dfu.data.Aborted +import no.nordicsemi.dfu.data.Completed +import no.nordicsemi.dfu.data.Connected +import no.nordicsemi.dfu.data.Connecting +import no.nordicsemi.dfu.data.DFUServiceStatus +import no.nordicsemi.dfu.data.Disconnected +import no.nordicsemi.dfu.data.Disconnecting +import no.nordicsemi.dfu.data.EnablingDfu +import no.nordicsemi.dfu.data.Error +import no.nordicsemi.dfu.data.Idle +import no.nordicsemi.dfu.data.ProgressUpdate +import no.nordicsemi.dfu.data.Started +import no.nordicsemi.dfu.data.Starting +import no.nordicsemi.dfu.data.Validating + +@Composable +internal fun DFUServiceStatus.toDisplayString(): String { + val displayStatus = when (this) { + Aborted -> stringResource(id = R.string.dfu_display_status_aborted) + Completed -> stringResource(id = R.string.dfu_display_status_completed) + Connected -> stringResource(id = R.string.dfu_display_status_connected) + Connecting -> stringResource(id = R.string.dfu_display_status_connecting) + Disconnected -> stringResource(id = R.string.dfu_display_status_disconnected) + Disconnecting -> stringResource(id = R.string.dfu_display_status_disconnecting) + EnablingDfu -> stringResource(id = R.string.dfu_display_status_enabling) + is Error -> message ?: stringResource(id = R.string.dfu_display_status_error) + Idle -> stringResource(id = R.string.dfu_display_status_idle) + is ProgressUpdate -> stringResource(id = R.string.dfu_display_status_progress_update, progress) + Started -> stringResource(id = R.string.dfu_display_status_started) + Starting -> stringResource(id = R.string.dfu_display_status_starting) + Validating -> stringResource(id = R.string.dfu_display_status_validating) + } + + return stringResource(id = R.string.dfu_display_status, displayStatus) +} 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 f657d6c8..d5664ce2 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,11 +14,12 @@ 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().value + val state = viewModel.state.collectAsState(NoFileSelectedState).value val isScreenActive = viewModel.isActive.collectAsState().value val context = LocalContext.current diff --git a/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUSelectFileView.kt b/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUSelectDatFileView.kt similarity index 58% rename from profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUSelectFileView.kt rename to profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUSelectDatFileView.kt index e764ee3a..a6bd34c7 100644 --- a/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUSelectFileView.kt +++ b/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUSelectDatFileView.kt @@ -2,7 +2,8 @@ package no.nordicsemi.dfu.view import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.Button @@ -18,40 +19,28 @@ import no.nordicsemi.android.theme.view.SectionTitle import no.nordicsemi.dfu.R @Composable -internal fun DFUSelectFileView(onEvent: (DFUViewEvent) -> Unit) { +internal fun DFUSelectDatFileView(onEvent: (DFUViewEvent) -> Unit) { ScreenSection { - SectionTitle(icon = Icons.Default.Settings, title = stringResource(id = R.string.dfu_choose_file)) + SectionTitle( + icon = Icons.Default.Settings, + title = stringResource(id = R.string.dfu_choose_file) + ) Spacer(modifier = Modifier.padding(8.dp)) Text( - text = stringResource(id = R.string.dfu_choose_info), + text = stringResource(id = R.string.dfu_choose_dat_info), style = MaterialTheme.typography.bodyMedium ) Spacer(modifier = Modifier.padding(8.dp)) - ButtonsRow(onEvent) - } -} - -@Composable -private fun ButtonsRow(onEvent: (DFUViewEvent) -> Unit) { - - val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { - onEvent(OnFileSelected(it!!)) - } - - Row( - horizontalArrangement = Arrangement.SpaceEvenly, - modifier = Modifier.fillMaxWidth() - ) { - Button(onClick = { launcher.launch(DfuBaseService.MIME_TYPE_ZIP) }) { - Text(text = stringResource(id = R.string.dfu_select_zip)) + val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> + uri?.let { onEvent(OnDatFileSelected(it)) } } Button(onClick = { launcher.launch(DfuBaseService.MIME_TYPE_OCTET_STREAM) }) { - Text(text = stringResource(id = R.string.dfu_select_hex)) + Text(text = stringResource(id = R.string.dfu_select_dat)) } } } 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 new file mode 100644 index 00000000..a97751c6 --- /dev/null +++ b/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUSelectMainFileView.kt @@ -0,0 +1,81 @@ +package no.nordicsemi.dfu.view + +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +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.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +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 + +@Composable +internal fun DFUSelectMainFileView(state: DFUData, onEvent: (DFUViewEvent) -> Unit) { + ScreenSection { + SectionTitle( + icon = Icons.Default.Settings, + title = stringResource(id = R.string.dfu_choose_file) + ) + + Spacer(modifier = Modifier.padding(8.dp)) + + Text( + text = stringResource(id = R.string.dfu_choose_info), + style = MaterialTheme.typography.bodyMedium + ) + + Spacer(modifier = Modifier.padding(8.dp)) + + ButtonsRow(onEvent) + } +} + +@Composable +private fun ButtonsRow(onEvent: (DFUViewEvent) -> Unit) { + + val fileType = rememberSaveable { mutableStateOf(DfuBaseService.MIME_TYPE_ZIP) } + + val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> + uri?.let { + if (fileType.value == DfuBaseService.MIME_TYPE_ZIP) { + onEvent(OnZipFileSelected(it)) + } else { + onEvent(OnHexFileSelected(it)) + } + } + } + + Row( + horizontalArrangement = Arrangement.SpaceEvenly, + modifier = Modifier.fillMaxWidth() + ) { + Button(onClick = { + fileType.value = DfuBaseService.MIME_TYPE_ZIP + launcher.launch(fileType.value) + }) { + Text(text = stringResource(id = R.string.dfu_select_zip)) + } + + Button(onClick = { + fileType.value = DfuBaseService.MIME_TYPE_OCTET_STREAM + launcher.launch(fileType.value) + }) { + Text(text = stringResource(id = R.string.dfu_select_hex)) + } + } +} diff --git a/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUViewEvent.kt b/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUViewEvent.kt index ec3d30e8..21af155a 100644 --- a/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUViewEvent.kt +++ b/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUViewEvent.kt @@ -4,7 +4,9 @@ import android.net.Uri internal sealed class DFUViewEvent -internal data class OnFileSelected(val uri: Uri) : DFUViewEvent() +internal data class OnZipFileSelected(val file: Uri) : DFUViewEvent() +internal data class OnHexFileSelected(val file: Uri) : DFUViewEvent() +internal data class OnDatFileSelected(val file: Uri) : DFUViewEvent() internal object OnInstallButtonClick : DFUViewEvent() 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 40cb5497..436abd1a 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,32 +1,65 @@ package no.nordicsemi.dfu.viewmodel -import android.net.Uri import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.combine import no.nordicsemi.android.theme.viewmodel.CloseableViewModel import no.nordicsemi.android.utils.exhaustive +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.view.* -import java.io.File +import no.nordicsemi.dfu.data.FileInstallingState +import no.nordicsemi.dfu.data.FileReadyState +import no.nordicsemi.dfu.view.DFUViewEvent +import no.nordicsemi.dfu.view.OnDatFileSelected +import no.nordicsemi.dfu.view.OnDisconnectButtonClick +import no.nordicsemi.dfu.view.OnHexFileSelected +import no.nordicsemi.dfu.view.OnInstallButtonClick +import no.nordicsemi.dfu.view.OnPauseButtonClick +import no.nordicsemi.dfu.view.OnStopButtonClick +import no.nordicsemi.dfu.view.OnZipFileSelected import javax.inject.Inject @HiltViewModel internal class DFUViewModel @Inject constructor( private val repository: DFURepository, + private val progressManager: DFUProgressManager, + private val dfuManager: DFUManager, + private val fileManger: DFUFileManager ) : CloseableViewModel() { - val state = repository.data + val state = repository.data.combine(progressManager.status) { state, status -> + (state as? FileInstallingState)?.run { + state.copy(status = status) + } ?: state + } + + init { + progressManager.registerListener() + } fun onEvent(event: DFUViewEvent) { when (event) { OnDisconnectButtonClick -> finish() - is OnFileSelected -> repository.initFile(createFile(event.uri)) - OnInstallButtonClick -> repository.install() + OnInstallButtonClick -> { + dfuManager.install(requireFile()) + repository.install() + } OnPauseButtonClick -> finish() OnStopButtonClick -> finish() + is OnHexFileSelected -> repository. + is OnZipFileSelected -> TODO() + is OnDatFileSelected -> TODO() }.exhaustive } - private fun createFile(uri: Uri): File { - return File(requireNotNull(uri.path)) + private fun requireFile(): DFUFile { + return (repository.data.value as FileReadyState).file + } + + override fun onCleared() { + super.onCleared() + progressManager.unregisterListener() } } diff --git a/profile_dfu/src/main/res/values/strings.xml b/profile_dfu/src/main/res/values/strings.xml index fde0470b..8dc227e9 100644 --- a/profile_dfu/src/main/res/values/strings.xml +++ b/profile_dfu/src/main/res/values/strings.xml @@ -8,6 +8,7 @@ Stop Install + Select .dat Select .zip Select .hex @@ -21,6 +22,7 @@ Choose file Please select .zip or .hex file with bootloader, application or soft device. + Please select .dat file. File managers Please select @@ -29,4 +31,19 @@ Operation success Operation failed + + Aborted + Completed + Connected + Connecting + Disconnected + Disconnecting + Enabling DFU + Error + Idle + %d% + Started + Starting + Validating + Status: %s