mirror of
https://github.com/aljazceru/Android-nRF-Toolbox.git
synced 2026-01-29 11:34:35 +01:00
Implementation for loading DFU file.
This commit is contained in:
@@ -22,5 +22,4 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
||||
@@ -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
|
||||
) {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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? {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)) }
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user