Implementation for loading DFU file.

This commit is contained in:
Sylwester Zieliński
2022-01-10 09:36:38 +01:00
parent c75f16657b
commit 0993799e42
29 changed files with 472 additions and 313 deletions

View File

@@ -22,5 +22,4 @@
</intent-filter>
</activity>
</application>
</manifest>
</manifest>

View File

@@ -4,8 +4,12 @@ import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@@ -29,12 +33,11 @@ fun FeatureButton(
@StringRes name: Int,
onClick: () -> Unit
) {
ScreenSection {
ScreenSection(onClick = onClick) {
Column(
modifier = Modifier
.fillMaxWidth()
.height(150.dp)
.clickable { onClick() }
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {

View File

@@ -136,156 +136,161 @@ fun HomeView(callback: (NavDestination) -> Unit) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp)
.verticalScroll(rememberScrollState())
) {
Spacer(modifier = Modifier.height(16.dp))
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp)
) {
Spacer(modifier = Modifier.height(16.dp))
Row(horizontalArrangement = Arrangement.SpaceEvenly) {
Box(
modifier = Modifier
.weight(1f)
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
FeatureButton(
R.drawable.ic_csc,
R.string.csc_module,
R.string.csc_module_full
) { callback(NavDestination.CSC) }
Row(horizontalArrangement = Arrangement.SpaceEvenly) {
Box(
modifier = Modifier
.weight(1f)
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
FeatureButton(
R.drawable.ic_csc,
R.string.csc_module,
R.string.csc_module_full
) { callback(NavDestination.CSC) }
}
Spacer(modifier = Modifier.width(16.dp))
Box(
modifier = Modifier
.weight(1f)
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
FeatureButton(
R.drawable.ic_hrs, R.string.hrs_module,
R.string.hrs_module_full
) { callback(NavDestination.HRS) }
}
}
Spacer(modifier = Modifier.width(16.dp))
Box(
modifier = Modifier
.weight(1f)
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
FeatureButton(
R.drawable.ic_hrs, R.string.hrs_module,
R.string.hrs_module_full
) { callback(NavDestination.HRS) }
Spacer(modifier = Modifier.height(16.dp))
Row(horizontalArrangement = Arrangement.SpaceEvenly) {
Box(
modifier = Modifier
.weight(1f)
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
FeatureButton(
R.drawable.ic_gls, R.string.gls_module,
R.string.gls_module_full
) { callback(NavDestination.GLS) }
}
Spacer(modifier = Modifier.width(16.dp))
Box(
modifier = Modifier
.weight(1f)
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
FeatureButton(
R.drawable.ic_hts, R.string.hts_module,
R.string.hts_module_full
) { callback(NavDestination.HTS) }
}
}
Spacer(modifier = Modifier.height(16.dp))
Row(horizontalArrangement = Arrangement.SpaceEvenly) {
Box(
modifier = Modifier
.weight(1f)
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
FeatureButton(
R.drawable.ic_bps, R.string.bps_module,
R.string.bps_module_full
) { callback(NavDestination.BPS) }
}
Spacer(modifier = Modifier.width(16.dp))
Box(
modifier = Modifier
.weight(1f)
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
FeatureButton(
R.drawable.ic_rscs,
R.string.rscs_module,
R.string.rscs_module_full
) { callback(NavDestination.RSCS) }
}
}
Spacer(modifier = Modifier.height(16.dp))
Row(horizontalArrangement = Arrangement.SpaceEvenly) {
Box(
modifier = Modifier
.weight(1f)
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
FeatureButton(
R.drawable.ic_prx, R.string.prx_module,
R.string.prx_module_full
) { callback(NavDestination.PRX) }
}
Spacer(modifier = Modifier.width(16.dp))
Box(
modifier = Modifier
.weight(1f)
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
FeatureButton(
R.drawable.ic_cgm, R.string.cgm_module,
R.string.cgm_module_full
) { callback(NavDestination.CGMS) }
}
}
Spacer(modifier = Modifier.height(16.dp))
Row(horizontalArrangement = Arrangement.SpaceEvenly) {
Box(
modifier = Modifier
.weight(1f)
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
FeatureButton(
R.drawable.ic_uart, R.string.uart_module,
R.string.uart_module_full
) { callback(NavDestination.UART) }
}
}
Spacer(modifier = Modifier.height(16.dp))
Row(horizontalArrangement = Arrangement.SpaceEvenly) {
Box(
modifier = Modifier
.weight(1f)
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
FeatureButton(
R.drawable.ic_dfu, R.string.dfu_module,
R.string.uart_module_full
) { callback(NavDestination.DFU) }
}
}
Spacer(modifier = Modifier.height(16.dp))
}
Spacer(modifier = Modifier.height(16.dp))
Row(horizontalArrangement = Arrangement.SpaceEvenly) {
Box(
modifier = Modifier
.weight(1f)
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
FeatureButton(
R.drawable.ic_gls, R.string.gls_module,
R.string.gls_module_full
) { callback(NavDestination.GLS) }
}
Spacer(modifier = Modifier.width(16.dp))
Box(
modifier = Modifier
.weight(1f)
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
FeatureButton(
R.drawable.ic_hts, R.string.hts_module,
R.string.hts_module_full
) { callback(NavDestination.HTS) }
}
}
Spacer(modifier = Modifier.height(16.dp))
Row(horizontalArrangement = Arrangement.SpaceEvenly) {
Box(
modifier = Modifier
.weight(1f)
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
FeatureButton(
R.drawable.ic_bps, R.string.bps_module,
R.string.bps_module_full
) { callback(NavDestination.BPS) }
}
Spacer(modifier = Modifier.width(16.dp))
Box(
modifier = Modifier
.weight(1f)
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
FeatureButton(
R.drawable.ic_rscs,
R.string.rscs_module,
R.string.rscs_module_full
) { callback(NavDestination.RSCS) }
}
}
Spacer(modifier = Modifier.height(16.dp))
Row(horizontalArrangement = Arrangement.SpaceEvenly) {
Box(
modifier = Modifier
.weight(1f)
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
FeatureButton(
R.drawable.ic_prx, R.string.prx_module,
R.string.prx_module_full
) { callback(NavDestination.PRX) }
}
Spacer(modifier = Modifier.width(16.dp))
Box(
modifier = Modifier
.weight(1f)
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
FeatureButton(
R.drawable.ic_cgm, R.string.cgm_module,
R.string.cgm_module_full
) { callback(NavDestination.CGMS) }
}
}
Spacer(modifier = Modifier.height(16.dp))
Row(horizontalArrangement = Arrangement.SpaceEvenly) {
Box(
modifier = Modifier
.weight(1f)
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
FeatureButton(
R.drawable.ic_uart, R.string.uart_module,
R.string.uart_module_full
) { callback(NavDestination.UART) }
}
}
Spacer(modifier = Modifier.height(16.dp))
Row(horizontalArrangement = Arrangement.SpaceEvenly) {
Box(
modifier = Modifier
.weight(1f)
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
FeatureButton(
R.drawable.ic_dfu, R.string.dfu_module,
R.string.uart_module_full
) { callback(NavDestination.DFU) }
}
}
Spacer(modifier = Modifier.height(16.dp))
}
}
}

View File

@@ -27,6 +27,7 @@ import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Intent
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
@@ -83,7 +84,9 @@ abstract class ForegroundBleService : BleProfileService() {
* @param defaults
*/
private fun createNotification(messageResId: Int, defaults: Int): Notification {
createNotificationChannel(CHANNEL_ID)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createNotificationChannel(CHANNEL_ID)
}
val intent: Intent? = packageManager.getLaunchIntentForPackage(packageName)
val pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE)
@@ -97,6 +100,7 @@ abstract class ForegroundBleService : BleProfileService() {
.build()
}
@RequiresApi(Build.VERSION_CODES.O)
private fun createNotificationChannel(channelName: String) {
val channel = NotificationChannel(
channelName,

View File

@@ -41,7 +41,7 @@ fun SectionTitle(
)
.padding(8.dp)
)
Spacer(modifier = Modifier.padding(8.dp))
Spacer(modifier = Modifier.size(8.dp))
Text(
text = title,
textAlign = TextAlign.Center,
@@ -73,7 +73,7 @@ fun SectionTitle(
)
.padding(8.dp)
)
Spacer(modifier = Modifier.padding(8.dp))
Spacer(modifier = Modifier.size(8.dp))
Text(
text = title,
textAlign = TextAlign.Center,

View File

@@ -1,5 +1,6 @@
package no.nordicsemi.android.theme.view
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@@ -12,14 +13,26 @@ import androidx.compose.ui.unit.dp
import no.nordicsemi.android.material.you.Card
@Composable
fun ScreenSection(content: @Composable () -> Unit) {
fun ScreenSection(onClick: (() -> Unit)? = null, content: @Composable () -> Unit) {
Card(
backgroundColor = MaterialTheme.colorScheme.secondaryContainer,
shape = RoundedCornerShape(16.dp),
elevation = 0.dp,
) {
val modifier = if (onClick != null) {
Modifier
.clickable { onClick.invoke() }
.fillMaxWidth()
.padding(16.dp)
} else {
Modifier
.fillMaxWidth()
.padding(16.dp)
}
Column(
modifier = Modifier.fillMaxWidth().padding(16.dp),
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
content()

View File

@@ -104,7 +104,7 @@ private fun RecordsViewWithData(state: CGMData) {
RecordItem(it)
if (i < state.records.size - 1) {
Spacer(modifier = Modifier.padding(8.dp))
Spacer(modifier = Modifier.size(8.dp))
}
}
}
@@ -129,7 +129,7 @@ private fun RecordItem(record: CGMRecord) {
)
}
Spacer(modifier = Modifier.padding(16.dp))
Spacer(modifier = Modifier.size(16.dp))
Text(
text = record.glucoseConcentration(),

View File

@@ -13,8 +13,9 @@ internal data class FileReadyState(
val device: DiscoveredBluetoothDevice
) : DFUData()
internal data class HexFileReadyState(
val file: DFUFile
internal data class HexFileLoadedState(
val file: PartialHexFile,
val isDatFileError: Boolean = false
) : DFUData()
internal data class FileInstallingState(
@@ -23,4 +24,4 @@ internal data class FileInstallingState(
internal object UploadSuccessState : DFUData()
internal object UploadFailureState : DFUData()
internal data class UploadFailureState(val message: String?) : DFUData()

View File

@@ -10,7 +10,12 @@ data class ZipFile(val data: FileData) : DFUFile() {
override val fileType: DFUFileType = DFUFileType.TYPE_AUTO
}
data class HexFile(
data class PartialHexFile(
val data: FileData,
val fileType: DFUFileType
)
data class FullHexFile(
val data: FileData,
val datFileData: FileData,
override val fileType: DFUFileType
@@ -19,6 +24,6 @@ data class HexFile(
data class FileData(
val uri: Uri,
val name: String,
val path: String,
val path: String?,
val size: Long
)

View File

@@ -3,6 +3,7 @@ package no.nordicsemi.dfu.data
import android.content.Context
import android.net.Uri
import android.provider.MediaStore
import android.util.Log
import androidx.core.net.toFile
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
@@ -12,13 +13,17 @@ class DFUFileManager @Inject constructor(
private val context: Context
) {
private val TAG = "DFU_FILE_MANAGER"
fun createFile(uri: Uri): FileData? {
return try {
createFromFile(uri)
} catch (e: Exception) {
Log.e(TAG, "Error during creation file from uri.", e)
try {
createFromContentResolver(uri)
} catch (e: Exception) {
Log.e(TAG, "Error during loading file from content resolver.", e)
null
}
}
@@ -30,26 +35,27 @@ class DFUFileManager @Inject constructor(
}
private fun createFromContentResolver(uri: Uri): FileData? {
return try {
val data = context.contentResolver.query(uri, null, null, null, null)
val data = context.contentResolver.query(uri, null, null, null, null)
if (data != null && data.moveToNext()) {
return if (data != null && data.moveToNext()) {
val displayNameIndex = data.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME)
val fileSizeIndex = data.getColumnIndex(MediaStore.MediaColumns.SIZE)
val dataIndex = data.getColumnIndex(MediaStore.MediaColumns.DATA)
val displayNameIndex = data.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME)
val fileSizeIndex = data.getColumnIndex(MediaStore.MediaColumns.SIZE)
val dataIndex = data.getColumnIndex(MediaStore.MediaColumns.DATA)
val fileName = data.getString(displayNameIndex)
val fileSize = data.getInt(fileSizeIndex)
val filePath = data.getString(dataIndex)
data.close()
FileData(uri, fileName, filePath, fileSize.toLong())
val fileName = data.getString(displayNameIndex)
val fileSize = data.getInt(fileSizeIndex)
val filePath = if (dataIndex != -1) {
data.getString(dataIndex)
} else {
null
}
} catch (e: Exception) {
data.close()
FileData(uri, fileName, filePath, fileSize.toLong())
} else {
Log.d(TAG, "Data loaded from ContentResolver is empty.")
null
}
}

View File

@@ -1,10 +1,12 @@
package no.nordicsemi.dfu.data
import no.nordicsemi.android.dfu.DfuBaseService
enum class DFUFileType(val id: Int) {
TYPE_AUTO(0x00),
TYPE_SOFT_DEVICE(0x01),
TYPE_BOOTLOADER(0x02),
TYPE_APPLICATION(0x04);
TYPE_AUTO(DfuBaseService.TYPE_AUTO),
TYPE_SOFT_DEVICE(DfuBaseService.TYPE_SOFT_DEVICE),
TYPE_BOOTLOADER(DfuBaseService.TYPE_BOOTLOADER),
TYPE_APPLICATION(DfuBaseService.TYPE_APPLICATION);
companion object {
fun create(id: Int): DFUFileType? {

View File

@@ -17,7 +17,7 @@ class DFUManager @Inject constructor(
fun install(file: DFUFile) {
val device = deviceHolder.device!!
val starter = DfuServiceInitiator(device.address)
val starter = DfuServiceInitiator(device.address())
.setDeviceName(device.displayName())
// .setKeepBond(keepBond)
// .setForceDfu(forceDfu)
@@ -27,9 +27,9 @@ class DFUManager @Inject constructor(
.setUnsafeExperimentalButtonlessServiceInSecureDfuEnabled(true)
when (file) {
is ZipFile -> starter.setZip(file.uri, file.path)
is HexFile -> starter.setBinOrHex(file.fileType.id, file.uri, file.path)
.setInitFile(file.datFile.uri, file.datFile.path)
is ZipFile -> starter.setZip(file.data.uri, file.data.path)
is FullHexFile -> starter.setBinOrHex(file.fileType.id, file.data.uri, file.data.path)
.setInitFile(file.datFileData.uri, file.datFileData.path)
}.exhaustive
starter.start(context, DFUService::class.java)

View File

@@ -1,28 +1,56 @@
package no.nordicsemi.dfu.data
import android.net.Uri
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import no.nordicsemi.android.service.SelectedBluetoothDeviceHolder
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
internal class DFURepository @Inject constructor(
private val deviceHolder: SelectedBluetoothDeviceHolder
private val deviceHolder: SelectedBluetoothDeviceHolder,
private val fileManger: DFUFileManager
) {
private val _data = MutableStateFlow<DFUData>(NoFileSelectedState())
val data: StateFlow<DFUData> = _data
val data: StateFlow<DFUData> = _data.asStateFlow()
fun initFile(file: DFUFile?) {
if (file == null) {
_data.value = NoFileSelectedState(true)
} else {
_data.value = FileReadyState(file, deviceHolder.device!!)
}
fun setZipFile(file: Uri) {
val currentState = _data.value as NoFileSelectedState
_data.value = fileManger.createFile(file)?.let {
FileReadyState(ZipFile(it), requireNotNull(deviceHolder.device))
} ?: currentState.copy(isError = true)
}
fun setHexFile(file: Uri) {
val currentState = _data.value as NoFileSelectedState
_data.value = fileManger.createFile(file)?.let {
HexFileLoadedState(PartialHexFile(it, DFUFileType.TYPE_APPLICATION))
} ?: currentState.copy(isError = true)
}
fun setDatFile(file: Uri) {
val currentState = _data.value as HexFileLoadedState
_data.value = fileManger.createFile(file)?.let {
FileReadyState(FullHexFile(it, currentState.file.data, DFUFileType.TYPE_APPLICATION), requireNotNull(deviceHolder.device))
} ?: currentState.copy(isDatFileError = true)
}
fun setSuccess() {
_data.value = UploadSuccessState
}
fun setError(message: String?) {
_data.value = UploadFailureState(message)
}
fun install() {
_data.value = FileInstallingState()
}
fun clear() {
_data.value = NoFileSelectedState()
}
}

View File

@@ -22,11 +22,25 @@
package no.nordicsemi.dfu.repository
import android.app.Activity
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.os.Build
import androidx.annotation.RequiresApi
import no.nordicsemi.android.dfu.DfuBaseService
import no.nordicsemi.dfu.view.NotificationActivity
import no.nordicsemi.dfu.R
class DFUService : DfuBaseService() {
override fun onCreate() {
super.onCreate()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createDfuNotificationChannel(this)
}
}
override fun getNotificationTarget(): Class<out Activity?>? {
/*
* As a target activity the NotificationActivity is returned, not the MainActivity. This is because the notification must create a new task:
@@ -41,11 +55,26 @@ class DFUService : DfuBaseService() {
* This method may be used to restore the target activity in case the application was closed or is open. It may also be used to recreate an activity
* history (see NotificationActivity).
*/
return NotificationActivity::class.java
return Class.forName("no.nordicsemi.android.nrftoolbox.MainActivity") as Class<out Activity>
}
override fun isDebug(): Boolean {
// return BuildConfig.DEBUG;
return true
}
@RequiresApi(api = Build.VERSION_CODES.O)
private fun createDfuNotificationChannel(context: Context) {
val channel = NotificationChannel(
NOTIFICATION_CHANNEL_DFU,
context.getString(R.string.dfu_channel_name),
NotificationManager.IMPORTANCE_LOW
)
channel.description = context.getString(R.string.dfu_channel_description)
channel.setShowBadge(false)
channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
val notificationManager =
context.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
notificationManager?.createNotificationChannel(channel)
}
}

View File

@@ -14,10 +14,10 @@ internal fun DFUContentView(state: DFUData, onEvent: (DFUViewEvent) -> Unit) {
when (state) {
is NoFileSelectedState -> DFUSelectMainFileView(state, onEvent)
is FileReadyState -> DFUSummaryView(state, onEvent)
is HexFileLoadedState -> DFUSelectDatFileView(state, onEvent)
UploadSuccessState -> DFUSuccessView(onEvent)
UploadFailureState -> DFUErrorView(onEvent)
is UploadFailureState -> DFUErrorView(state, onEvent)
is FileInstallingState -> DFUInstallingView(state, onEvent)
is HexFileReadyState -> DFUSelectDatFileView(onEvent)
}.exhaustive
}
}

View File

@@ -2,27 +2,43 @@ package no.nordicsemi.dfu.view
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import no.nordicsemi.android.theme.view.ScreenSection
import no.nordicsemi.dfu.R
import no.nordicsemi.dfu.data.UploadFailureState
@Composable
internal fun DFUErrorView(onEvent: (DFUViewEvent) -> Unit) {
internal fun DFUErrorView(state: UploadFailureState, onEvent: (DFUViewEvent) -> Unit) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
ScreenSection {
Icon(
painter = painterResource(id = R.drawable.ic_fail_circle),
contentDescription = stringResource(id = R.string.dfu_failure_icon_description),
tint = MaterialTheme.colorScheme.error
)
Column {
Icon(
painter = painterResource(id = R.drawable.ic_fail_circle),
contentDescription = stringResource(id = R.string.dfu_failure_icon_description)
)
Spacer(modifier = Modifier.size(16.dp))
Spacer(modifier = Modifier.padding(16.dp))
val error = state.message ?: stringResource(id = R.string.dfu_unknown_error)
Text(
text = error,
color = MaterialTheme.colorScheme.error
)
Spacer(modifier = Modifier.size(16.dp))
}
Spacer(modifier = Modifier.size(16.dp))
Button(onClick = { onEvent(OnPauseButtonClick) }) {
Text(text = stringResource(id = R.string.dfu_close))

View File

@@ -1,26 +1,24 @@
package no.nordicsemi.dfu.view
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import no.nordicsemi.android.material.you.CircularProgressIndicator
import no.nordicsemi.android.theme.view.ScreenSection
import no.nordicsemi.dfu.R
import no.nordicsemi.dfu.data.FileInstallingState
@Composable
internal fun DFUInstallingView(state: FileInstallingState, onEvent: (DFUViewEvent) -> Unit) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
ScreenSection {
CircularProgressIndicator()
Spacer(modifier = Modifier.height(16.dp))
@@ -29,14 +27,16 @@ internal fun DFUInstallingView(state: FileInstallingState, onEvent: (DFUViewEven
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { onEvent(OnPauseButtonClick) }) {
Text(text = stringResource(id = R.string.dfu_pause))
}
Row(horizontalArrangement = Arrangement.SpaceEvenly) {
Button(onClick = { onEvent(OnPauseButtonClick) }) {
Text(text = stringResource(id = R.string.dfu_pause))
}
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.size(16.dp))
Button(onClick = { onEvent(OnPauseButtonClick) }) {
Text(text = stringResource(id = R.string.dfu_stop))
Button(onClick = { onEvent(OnPauseButtonClick) }) {
Text(text = stringResource(id = R.string.dfu_stop))
}
}
}
}

View File

@@ -14,12 +14,11 @@ import no.nordicsemi.dfu.R
import no.nordicsemi.dfu.data.DFUData
import no.nordicsemi.dfu.repository.DFUService
import no.nordicsemi.dfu.viewmodel.DFUViewModel
import no.nordicsemi.dfu.data.NoFileSelectedState
@Composable
fun DFUScreen(finishAction: () -> Unit) {
val viewModel: DFUViewModel = hiltViewModel()
val state = viewModel.state.collectAsState(NoFileSelectedState).value
val state = viewModel.state.collectAsState().value
val isScreenActive = viewModel.isActive.collectAsState().value
val context = LocalContext.current

View File

@@ -4,6 +4,7 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Button
@@ -17,23 +18,34 @@ import no.nordicsemi.android.dfu.DfuBaseService
import no.nordicsemi.android.theme.view.ScreenSection
import no.nordicsemi.android.theme.view.SectionTitle
import no.nordicsemi.dfu.R
import no.nordicsemi.dfu.data.HexFileLoadedState
@Composable
internal fun DFUSelectDatFileView(onEvent: (DFUViewEvent) -> Unit) {
internal fun DFUSelectDatFileView(state: HexFileLoadedState, onEvent: (DFUViewEvent) -> Unit) {
ScreenSection {
SectionTitle(
icon = Icons.Default.Settings,
title = stringResource(id = R.string.dfu_choose_file)
)
Spacer(modifier = Modifier.padding(8.dp))
Spacer(modifier = Modifier.size(8.dp))
Text(
text = stringResource(id = R.string.dfu_choose_dat_info),
style = MaterialTheme.typography.bodyMedium
)
Spacer(modifier = Modifier.padding(8.dp))
if (state.isDatFileError) {
Spacer(modifier = Modifier.size(8.dp))
Text(
text = stringResource(id = R.string.dfu_load_file_error),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.error
)
}
Spacer(modifier = Modifier.size(8.dp))
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
uri?.let { onEvent(OnDatFileSelected(it)) }

View File

@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Button
@@ -22,24 +23,34 @@ import no.nordicsemi.android.dfu.DfuBaseService
import no.nordicsemi.android.theme.view.ScreenSection
import no.nordicsemi.android.theme.view.SectionTitle
import no.nordicsemi.dfu.R
import no.nordicsemi.dfu.data.DFUData
import no.nordicsemi.dfu.data.NoFileSelectedState
@Composable
internal fun DFUSelectMainFileView(state: DFUData, onEvent: (DFUViewEvent) -> Unit) {
internal fun DFUSelectMainFileView(state: NoFileSelectedState, onEvent: (DFUViewEvent) -> Unit) {
ScreenSection {
SectionTitle(
icon = Icons.Default.Settings,
title = stringResource(id = R.string.dfu_choose_file)
)
Spacer(modifier = Modifier.padding(8.dp))
Spacer(modifier = Modifier.size(8.dp))
Text(
text = stringResource(id = R.string.dfu_choose_info),
style = MaterialTheme.typography.bodyMedium
)
Spacer(modifier = Modifier.padding(8.dp))
if (state.isError) {
Spacer(modifier = Modifier.size(8.dp))
Text(
text = stringResource(id = R.string.dfu_load_file_error),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.error
)
}
Spacer(modifier = Modifier.size(8.dp))
ButtonsRow(onEvent)
}

View File

@@ -2,28 +2,40 @@ package no.nordicsemi.dfu.view
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import no.nordicsemi.android.theme.view.ScreenSection
import no.nordicsemi.dfu.R
@Composable
internal fun DFUSuccessView(onEvent: (DFUViewEvent) -> Unit) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
ScreenSection {
Icon(
painter = painterResource(id = R.drawable.ic_success_circle),
contentDescription = stringResource(id = R.string.dfu_success_icon_description),
tint = colorResource(id = no.nordicsemi.android.material.you.R.color.nordicGrass)
)
}
Icon(
painter = painterResource(id = R.drawable.ic_success_circle),
contentDescription = stringResource(id = R.string.dfu_success_icon_description)
Spacer(modifier = Modifier.size(16.dp))
Text(
text = stringResource(id = R.string.dfu_success),
color = MaterialTheme.colorScheme.error
)
Spacer(modifier = Modifier.padding(16.dp))
Spacer(modifier = Modifier.size(16.dp))
Button(onClick = { onEvent(OnPauseButtonClick) }) {
Text(text = stringResource(id = R.string.dfu_done))

View File

@@ -2,7 +2,14 @@ package no.nordicsemi.dfu.view
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Notifications
@@ -20,8 +27,10 @@ import no.nordicsemi.android.theme.view.ScreenSection
import no.nordicsemi.android.theme.view.SectionTitle
import no.nordicsemi.dfu.R
import no.nordicsemi.dfu.data.FileReadyState
import no.nordicsemi.dfu.data.FullHexFile
import no.nordicsemi.dfu.data.ZipFile
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
import java.io.File
import no.nordicsemi.ui.scanner.ui.exhaustive
@Composable
internal fun DFUSummaryView(state: FileReadyState, onEvent: (DFUViewEvent) -> Unit) {
@@ -30,7 +39,10 @@ internal fun DFUSummaryView(state: FileReadyState, onEvent: (DFUViewEvent) -> Un
Spacer(modifier = Modifier.height(16.dp))
FileDetailsView(state.file)
when (state.file) {
is FullHexFile -> FileDetailsView(state.file)
is ZipFile -> FileDetailsView(state.file)
}.exhaustive
Spacer(modifier = Modifier.height(16.dp))
@@ -59,7 +71,7 @@ internal fun DeviceDetailsView(device: DiscoveredBluetoothDevice) {
.padding(8.dp)
)
Spacer(modifier = Modifier.padding(8.dp))
Spacer(modifier = Modifier.size(8.dp))
Column(modifier = Modifier
.fillMaxWidth()
@@ -75,19 +87,37 @@ internal fun DeviceDetailsView(device: DiscoveredBluetoothDevice) {
}
@Composable
private fun FileDetailsView(file: File) {
val fileName = file.name
val fileLength = file.length()
private fun FileDetailsView(file: ZipFile) {
val fileName = file.data.name
val fileLength = file.data.size
ScreenSection {
SectionTitle(icon = Icons.Default.Notifications, title = stringResource(id = R.string.dfu_file_details))
SectionTitle(icon = Icons.Default.Notifications, title = stringResource(id = R.string.dfu_zip_file_details))
Spacer(modifier = Modifier.padding(16.dp))
Spacer(modifier = Modifier.size(16.dp))
Text(text = fileName)
Spacer(modifier = Modifier.padding(4.dp))
Spacer(modifier = Modifier.size(4.dp))
Text(text = stringResource(id = R.string.dfu_file_size, fileLength))
}
}
@Composable
private fun FileDetailsView(file: FullHexFile) {
val fileName = file.data.name
val fileLength = file.data.size
ScreenSection {
SectionTitle(icon = Icons.Default.Notifications, title = stringResource(id = R.string.dfu_hex_file_details))
Spacer(modifier = Modifier.size(16.dp))
Text(text = fileName)
Spacer(modifier = Modifier.size(4.dp))
Text(text = stringResource(id = R.string.dfu_file_size, fileLength))
}
}

View File

@@ -1,49 +0,0 @@
/*
* Copyright (c) 2015, Nordic Semiconductor
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
* USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package no.nordicsemi.dfu.view
import android.app.Activity
import android.os.Bundle
class NotificationActivity : Activity() {
protected override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// If this activity is the root activity of the task, the app is not running
if (isTaskRoot()) {
// Start the app before finishing
//TODO
// val parentIntent = Intent(this, FeaturesActivity::class.java)
// parentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
// val startAppIntent = Intent(this, DfuActivity::class.java)
// if (getIntent() != null && getIntent().getExtras() != null) startAppIntent.putExtras(
// getIntent().getExtras()
// )
// startActivities(arrayOf<Intent>(parentIntent, startAppIntent))
}
// Now finish, which will drop the user in to the activity that was at the top
// of the task stack
finish()
}
}

View File

@@ -1,16 +1,23 @@
package no.nordicsemi.dfu.viewmodel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import no.nordicsemi.android.service.SelectedBluetoothDeviceHolder
import no.nordicsemi.android.theme.viewmodel.CloseableViewModel
import no.nordicsemi.android.utils.exhaustive
import no.nordicsemi.dfu.data.Completed
import no.nordicsemi.dfu.data.DFUFile
import no.nordicsemi.dfu.data.DFUFileManager
import no.nordicsemi.dfu.data.DFUManager
import no.nordicsemi.dfu.data.DFUProgressManager
import no.nordicsemi.dfu.data.DFURepository
import no.nordicsemi.dfu.data.DFUServiceStatus
import no.nordicsemi.dfu.data.Error
import no.nordicsemi.dfu.data.FileInstallingState
import no.nordicsemi.dfu.data.FileReadyState
import no.nordicsemi.dfu.data.NoFileSelectedState
import no.nordicsemi.dfu.view.DFUViewEvent
import no.nordicsemi.dfu.view.OnDatFileSelected
import no.nordicsemi.dfu.view.OnDisconnectButtonClick
@@ -25,15 +32,15 @@ import javax.inject.Inject
internal class DFUViewModel @Inject constructor(
private val repository: DFURepository,
private val progressManager: DFUProgressManager,
private val dfuManager: DFUManager,
private val fileManger: DFUFileManager
private val deviceHolder: SelectedBluetoothDeviceHolder,
private val dfuManager: DFUManager
) : CloseableViewModel() {
val state = repository.data.combine(progressManager.status) { state, status ->
(state as? FileInstallingState)?.run {
state.copy(status = status)
} ?: state
}
(state as? FileInstallingState)
?.run { createInstallingStateWithNewStatus(state, status) }
?: state
}.stateIn(viewModelScope, SharingStarted.Eagerly, NoFileSelectedState())
init {
progressManager.registerListener()
@@ -41,23 +48,42 @@ internal class DFUViewModel @Inject constructor(
fun onEvent(event: DFUViewEvent) {
when (event) {
OnDisconnectButtonClick -> finish()
OnDisconnectButtonClick -> closeScreen()
OnInstallButtonClick -> {
dfuManager.install(requireFile())
repository.install()
}
OnPauseButtonClick -> finish()
OnStopButtonClick -> finish()
is OnHexFileSelected -> repository.
is OnZipFileSelected -> TODO()
is OnDatFileSelected -> TODO()
OnPauseButtonClick -> closeScreen()
OnStopButtonClick -> closeScreen()
is OnHexFileSelected -> repository.setHexFile(event.file)
is OnZipFileSelected -> repository.setZipFile(event.file)
is OnDatFileSelected -> repository.setDatFile(event.file)
}.exhaustive
}
private fun closeScreen() {
repository.clear()
deviceHolder.forgetDevice()
finish()
}
private fun requireFile(): DFUFile {
return (repository.data.value as FileReadyState).file
}
private fun createInstallingStateWithNewStatus(
state: FileInstallingState,
status: DFUServiceStatus
): FileInstallingState {
if (status is Error) {
repository.setError(status.message)
}
if (status is Completed) {
repository.setSuccess()
}
return state.copy(status = status)
}
override fun onCleared() {
super.onCleared()
progressManager.unregisterListener()

View File

@@ -3,7 +3,7 @@
android:height="80dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/nordicRed"
android:pathData="M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z"/>
<path
android:fillColor="@color/nordicRed"
android:pathData="M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z" />
</vector>

View File

@@ -12,7 +12,10 @@
<string name="dfu_select_zip">Select .zip</string>
<string name="dfu_select_hex">Select .hex</string>
<string name="dfu_file_details">File details</string>
<string name="dfu_load_file_error">An error occurred during loading the file. Please try with another file.</string>
<string name="dfu_zip_file_details">Zip file details</string>
<string name="dfu_hex_file_details">Hex file details</string>
<string name="dfu_file_size">%d bytes</string>
<string name="dfu_file_type_zip">Distribution packet (ZIP)</string>
@@ -41,9 +44,12 @@
<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_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>
<string name="dfu_success">Success!</string>
<string name="dfu_unknown_error">Unknown error.</string>
</resources>

View File

@@ -107,7 +107,7 @@ private fun RecordsViewWithData(state: GLSData) {
RecordItem(it)
if (i < state.records.size-1) {
Spacer(modifier = Modifier.padding(8.dp))
Spacer(modifier = Modifier.size(8.dp))
}
}
}
@@ -134,7 +134,7 @@ private fun RecordItem(record: GLSRecord) {
)
}
Spacer(modifier = Modifier.padding(16.dp))
Spacer(modifier = Modifier.size(16.dp))
Text(
text = glucoseConcentrationDisplayValue(record.glucoseConcentration, record.unit),

View File

@@ -3,6 +3,7 @@ package no.nordicsemi.android.uart.view
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
@@ -33,7 +34,7 @@ internal fun UARTAddMacroDialog(onDismiss: () -> Unit, onEvent: (UARTViewEvent)
alias.value = it
}
Spacer(modifier = Modifier.padding(16.dp))
Spacer(modifier = Modifier.size(16.dp))
TextField(text = command.value, hint = stringResource(id = R.string.uart_macro_dialog_command)) {
command.value = it

View File

@@ -32,7 +32,7 @@ internal fun MacroItem(macro: UARTMacro, onEvent: (UARTViewEvent) -> Unit) {
.clickable { onEvent(OnRunMacro(macro)) }
)
Spacer(modifier = Modifier.padding(16.dp))
Spacer(modifier = Modifier.size(16.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
@@ -46,7 +46,7 @@ internal fun MacroItem(macro: UARTMacro, onEvent: (UARTViewEvent) -> Unit) {
)
}
Spacer(modifier = Modifier.padding(16.dp))
Spacer(modifier = Modifier.size(16.dp))
Icon(
imageVector = Icons.Default.Delete,