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"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="no.nordicsemi.dfu">
|
package="no.nordicsemi.dfu">
|
||||||
|
|
||||||
|
<application>
|
||||||
|
<service android:name=".repository.DFUService"/>
|
||||||
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -1,16 +1,24 @@
|
|||||||
package no.nordicsemi.dfu.data
|
package no.nordicsemi.dfu.data
|
||||||
|
|
||||||
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
|
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
internal sealed class DFUData
|
internal sealed class DFUData
|
||||||
|
|
||||||
internal object NoFileSelectedState : DFUData()
|
internal data class NoFileSelectedState(
|
||||||
|
val isError: Boolean = false
|
||||||
|
) : DFUData()
|
||||||
|
|
||||||
internal data class FileReadyState(
|
internal data class FileReadyState(
|
||||||
val file: File,
|
val file: DFUFile,
|
||||||
val device: DiscoveredBluetoothDevice,
|
val device: DiscoveredBluetoothDevice
|
||||||
val isUploading: Boolean = false
|
) : DFUData()
|
||||||
|
|
||||||
|
internal data class HexFileReadyState(
|
||||||
|
val file: DFUFile
|
||||||
|
) : DFUData()
|
||||||
|
|
||||||
|
internal data class FileInstallingState(
|
||||||
|
val status: DFUServiceStatus = Idle
|
||||||
) : DFUData()
|
) : DFUData()
|
||||||
|
|
||||||
internal object UploadSuccessState : 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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import no.nordicsemi.android.service.SelectedBluetoothDeviceHolder
|
import no.nordicsemi.android.service.SelectedBluetoothDeviceHolder
|
||||||
import java.io.File
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@@ -12,15 +11,18 @@ internal class DFURepository @Inject constructor(
|
|||||||
private val deviceHolder: SelectedBluetoothDeviceHolder
|
private val deviceHolder: SelectedBluetoothDeviceHolder
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val _data = MutableStateFlow<DFUData>(NoFileSelectedState)
|
private val _data = MutableStateFlow<DFUData>(NoFileSelectedState())
|
||||||
val data: StateFlow<DFUData> = _data
|
val data: StateFlow<DFUData> = _data
|
||||||
|
|
||||||
fun initFile(file: File) {
|
fun initFile(file: DFUFile?) {
|
||||||
_data.value = FileReadyState(file, deviceHolder.device!!)
|
if (file == null) {
|
||||||
|
_data.value = NoFileSelectedState(true)
|
||||||
|
} else {
|
||||||
|
_data.value = FileReadyState(file, deviceHolder.device!!)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun install() {
|
fun install() {
|
||||||
val state = _data.value as FileReadyState
|
_data.value = FileInstallingState()
|
||||||
_data.value = state.copy(isUploading = true)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -48,4 +48,4 @@ class DFUService : DfuBaseService() {
|
|||||||
// return BuildConfig.DEBUG;
|
// return BuildConfig.DEBUG;
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,18 +12,12 @@ import no.nordicsemi.dfu.data.*
|
|||||||
internal fun DFUContentView(state: DFUData, onEvent: (DFUViewEvent) -> Unit) {
|
internal fun DFUContentView(state: DFUData, onEvent: (DFUViewEvent) -> Unit) {
|
||||||
Box(modifier = Modifier.padding(16.dp)) {
|
Box(modifier = Modifier.padding(16.dp)) {
|
||||||
when (state) {
|
when (state) {
|
||||||
NoFileSelectedState -> DFUSelectFileView(onEvent)
|
is NoFileSelectedState -> DFUSelectMainFileView(state, onEvent)
|
||||||
is FileReadyState -> FileReadyView(state, onEvent)
|
is FileReadyState -> DFUSummaryView(state, onEvent)
|
||||||
UploadSuccessState -> DFUSuccessView(onEvent)
|
UploadSuccessState -> DFUSuccessView(onEvent)
|
||||||
UploadFailureState -> DFUErrorView(onEvent)
|
UploadFailureState -> DFUErrorView(onEvent)
|
||||||
|
is FileInstallingState -> DFUInstallingView(state, onEvent)
|
||||||
|
is HexFileReadyState -> DFUSelectDatFileView(onEvent)
|
||||||
}.exhaustive
|
}.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
|
package no.nordicsemi.dfu.view
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.compose.foundation.layout.Column
|
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.Button
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.core.net.toUri
|
import androidx.compose.ui.unit.dp
|
||||||
import no.nordicsemi.android.dfu.DfuServiceInitiator
|
|
||||||
import no.nordicsemi.android.material.you.CircularProgressIndicator
|
import no.nordicsemi.android.material.you.CircularProgressIndicator
|
||||||
import no.nordicsemi.dfu.R
|
import no.nordicsemi.dfu.R
|
||||||
import no.nordicsemi.dfu.data.FileReadyState
|
import no.nordicsemi.dfu.data.FileInstallingState
|
||||||
import no.nordicsemi.dfu.repository.DFUService
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun DFUInstallingView(state: FileReadyState, onEvent: (DFUViewEvent) -> Unit) {
|
internal fun DFUInstallingView(state: FileInstallingState, onEvent: (DFUViewEvent) -> Unit) {
|
||||||
|
Column(
|
||||||
Column {
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
CircularProgressIndicator()
|
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) }) {
|
Button(onClick = { onEvent(OnPauseButtonClick) }) {
|
||||||
Text(text = stringResource(id = R.string.dfu_pause))
|
Text(text = stringResource(id = R.string.dfu_pause))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
Button(onClick = { onEvent(OnPauseButtonClick) }) {
|
Button(onClick = { onEvent(OnPauseButtonClick) }) {
|
||||||
Text(text = stringResource(id = R.string.dfu_stop))
|
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.data.DFUData
|
||||||
import no.nordicsemi.dfu.repository.DFUService
|
import no.nordicsemi.dfu.repository.DFUService
|
||||||
import no.nordicsemi.dfu.viewmodel.DFUViewModel
|
import no.nordicsemi.dfu.viewmodel.DFUViewModel
|
||||||
|
import no.nordicsemi.dfu.data.NoFileSelectedState
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DFUScreen(finishAction: () -> Unit) {
|
fun DFUScreen(finishAction: () -> Unit) {
|
||||||
val viewModel: DFUViewModel = hiltViewModel()
|
val viewModel: DFUViewModel = hiltViewModel()
|
||||||
val state = viewModel.state.collectAsState().value
|
val state = viewModel.state.collectAsState(NoFileSelectedState).value
|
||||||
val isScreenActive = viewModel.isActive.collectAsState().value
|
val isScreenActive = viewModel.isActive.collectAsState().value
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ package no.nordicsemi.dfu.view
|
|||||||
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
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.Icons
|
||||||
import androidx.compose.material.icons.filled.Settings
|
import androidx.compose.material.icons.filled.Settings
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
@@ -18,40 +19,28 @@ import no.nordicsemi.android.theme.view.SectionTitle
|
|||||||
import no.nordicsemi.dfu.R
|
import no.nordicsemi.dfu.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun DFUSelectFileView(onEvent: (DFUViewEvent) -> Unit) {
|
internal fun DFUSelectDatFileView(onEvent: (DFUViewEvent) -> Unit) {
|
||||||
ScreenSection {
|
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))
|
Spacer(modifier = Modifier.padding(8.dp))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = R.string.dfu_choose_info),
|
text = stringResource(id = R.string.dfu_choose_dat_info),
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyMedium
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.padding(8.dp))
|
Spacer(modifier = Modifier.padding(8.dp))
|
||||||
|
|
||||||
ButtonsRow(onEvent)
|
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
|
||||||
}
|
uri?.let { onEvent(OnDatFileSelected(it)) }
|
||||||
}
|
|
||||||
|
|
||||||
@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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Button(onClick = { launcher.launch(DfuBaseService.MIME_TYPE_OCTET_STREAM) }) {
|
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 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()
|
internal object OnInstallButtonClick : DFUViewEvent()
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +1,65 @@
|
|||||||
package no.nordicsemi.dfu.viewmodel
|
package no.nordicsemi.dfu.viewmodel
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
import no.nordicsemi.android.theme.viewmodel.CloseableViewModel
|
import no.nordicsemi.android.theme.viewmodel.CloseableViewModel
|
||||||
import no.nordicsemi.android.utils.exhaustive
|
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.data.DFURepository
|
||||||
import no.nordicsemi.dfu.view.*
|
import no.nordicsemi.dfu.data.FileInstallingState
|
||||||
import java.io.File
|
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
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
internal class DFUViewModel @Inject constructor(
|
internal class DFUViewModel @Inject constructor(
|
||||||
private val repository: DFURepository,
|
private val repository: DFURepository,
|
||||||
|
private val progressManager: DFUProgressManager,
|
||||||
|
private val dfuManager: DFUManager,
|
||||||
|
private val fileManger: DFUFileManager
|
||||||
) : CloseableViewModel() {
|
) : 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) {
|
fun onEvent(event: DFUViewEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
OnDisconnectButtonClick -> finish()
|
OnDisconnectButtonClick -> finish()
|
||||||
is OnFileSelected -> repository.initFile(createFile(event.uri))
|
OnInstallButtonClick -> {
|
||||||
OnInstallButtonClick -> repository.install()
|
dfuManager.install(requireFile())
|
||||||
|
repository.install()
|
||||||
|
}
|
||||||
OnPauseButtonClick -> finish()
|
OnPauseButtonClick -> finish()
|
||||||
OnStopButtonClick -> finish()
|
OnStopButtonClick -> finish()
|
||||||
|
is OnHexFileSelected -> repository.
|
||||||
|
is OnZipFileSelected -> TODO()
|
||||||
|
is OnDatFileSelected -> TODO()
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createFile(uri: Uri): File {
|
private fun requireFile(): DFUFile {
|
||||||
return File(requireNotNull(uri.path))
|
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_stop">Stop</string>
|
||||||
<string name="dfu_install">Install</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_zip">Select .zip</string>
|
||||||
<string name="dfu_select_hex">Select .hex</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_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_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_title">File managers</string>
|
||||||
<string name="dfu_macro_dialog_info">Please select </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_success_icon_description">Operation success</string>
|
||||||
<string name="dfu_failure_icon_description">Operation failed</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>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user