mirror of
https://github.com/aljazceru/Android-nRF-Toolbox.git
synced 2025-12-21 08:24:22 +01:00
Add fixes to DFU screen
This commit is contained in:
@@ -2,4 +2,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="no.nordicsemi.dfu">
|
||||
|
||||
<application>
|
||||
<service android:name=".repository.DFUService"/>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -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()
|
||||
|
||||
24
profile_dfu/src/main/java/no/nordicsemi/dfu/data/DFUFile.kt
Normal file
24
profile_dfu/src/main/java/no/nordicsemi/dfu/data/DFUFile.kt
Normal file
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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<DFUServiceStatus>(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)
|
||||
}
|
||||
}
|
||||
@@ -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<DFUData>(NoFileSelectedState)
|
||||
private val _data = MutableStateFlow<DFUData>(NoFileSelectedState())
|
||||
val data: StateFlow<DFUData> = _data
|
||||
|
||||
fun initFile(file: File) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<string name="dfu_stop">Stop</string>
|
||||
<string name="dfu_install">Install</string>
|
||||
|
||||
<string name="dfu_select_dat">Select .dat</string>
|
||||
<string name="dfu_select_zip">Select .zip</string>
|
||||
<string name="dfu_select_hex">Select .hex</string>
|
||||
|
||||
@@ -21,6 +22,7 @@
|
||||
|
||||
<string name="dfu_choose_file">Choose file</string>
|
||||
<string name="dfu_choose_info">Please select .zip or .hex file with bootloader, application or soft device.</string>
|
||||
<string name="dfu_choose_dat_info">Please select .dat file.</string>
|
||||
|
||||
<string name="dfu_macro_dialog_title">File managers</string>
|
||||
<string name="dfu_macro_dialog_info">Please select </string>
|
||||
@@ -29,4 +31,19 @@
|
||||
|
||||
<string name="dfu_success_icon_description">Operation success</string>
|
||||
<string name="dfu_failure_icon_description">Operation failed</string>
|
||||
|
||||
<string name="dfu_display_status_aborted">Aborted</string>
|
||||
<string name="dfu_display_status_completed">Completed</string>
|
||||
<string name="dfu_display_status_connected">Connected</string>
|
||||
<string name="dfu_display_status_connecting">Connecting</string>
|
||||
<string name="dfu_display_status_disconnected">Disconnected</string>
|
||||
<string name="dfu_display_status_disconnecting">Disconnecting</string>
|
||||
<string name="dfu_display_status_enabling">Enabling DFU</string>
|
||||
<string name="dfu_display_status_error">Error</string>
|
||||
<string name="dfu_display_status_idle">Idle</string>
|
||||
<string name="dfu_display_status_progress_update">%d%</string>
|
||||
<string name="dfu_display_status_started">Started</string>
|
||||
<string name="dfu_display_status_starting">Starting</string>
|
||||
<string name="dfu_display_status_validating">Validating</string>
|
||||
<string name="dfu_display_status">Status: %s</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user