Add fixes to DFU screen

This commit is contained in:
Sylwester Zieliński
2022-01-07 12:44:15 +01:00
parent c57a9c0c98
commit c75f16657b
19 changed files with 477 additions and 99 deletions

View File

@@ -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>

View File

@@ -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()

View 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
)

View File

@@ -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
}
}
}

View File

@@ -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 }
}
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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) {
_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()
}
}

View File

@@ -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()

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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))
}
}
}

View File

@@ -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))
}
}
}

View File

@@ -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()

View File

@@ -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()
}
}

View File

@@ -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>