mirror of
https://github.com/aljazceru/Android-nRF-Toolbox.git
synced 2025-12-18 23:14:22 +01:00
Remove DFU profile
This commit is contained in:
@@ -1,162 +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.android.service
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.widget.Toast
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import no.nordicsemi.android.ble.BleManager
|
||||
import no.nordicsemi.android.log.ILogSession
|
||||
import no.nordicsemi.android.log.Logger
|
||||
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
|
||||
|
||||
@AndroidEntryPoint
|
||||
abstract class BleProfileService : Service() {
|
||||
|
||||
protected val scope = CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
||||
|
||||
protected abstract val manager: BleManager
|
||||
|
||||
/**
|
||||
* Returns a handler that is created in onCreate().
|
||||
* The handler may be used to postpone execution of some operations or to run them in UI thread.
|
||||
*/
|
||||
private var handler: Handler? = null
|
||||
|
||||
private var activityIsChangingConfiguration = false
|
||||
|
||||
/**
|
||||
* Returns the log session that can be used to append log entries. The method returns `null` if the nRF Logger app was not installed. It is safe to use logger when
|
||||
* [.onServiceStarted] has been called.
|
||||
*
|
||||
* @return the log session
|
||||
*/
|
||||
private var logSession: ILogSession? = null
|
||||
private set
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
handler = Handler()
|
||||
}
|
||||
|
||||
protected fun stopIfDisconnected(status: BleManagerStatus) {
|
||||
if (status == BleManagerStatus.DISCONNECTED) {
|
||||
scope.close()
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns whether autoConnect option should be used.
|
||||
*
|
||||
* @return true to use autoConnect feature, false (default) otherwise.
|
||||
*/
|
||||
protected open fun shouldAutoConnect(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
super.onStartCommand(intent, flags, startId)
|
||||
|
||||
val device = intent!!.getParcelableExtra<DiscoveredBluetoothDevice>(DEVICE_DATA)!!.device
|
||||
|
||||
manager.connect(device)
|
||||
.useAutoConnect(shouldAutoConnect())
|
||||
.retry(3, 100)
|
||||
.enqueue()
|
||||
|
||||
return START_REDELIVER_INTENT
|
||||
}
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent) {
|
||||
super.onTaskRemoved(rootIntent)
|
||||
// This method is called when user removed the app from Recents.
|
||||
// By default, the service will be killed and recreated immediately after that.
|
||||
// However, all managed devices will be lost and devices will be disconnected.
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
// shutdown the manager
|
||||
manager.disconnect().enqueue()
|
||||
Logger.i(logSession, "Service destroyed")
|
||||
logSession = null
|
||||
handler = null
|
||||
}
|
||||
|
||||
/**
|
||||
* This method should return false if the service needs to do some asynchronous work after if has disconnected from the device.
|
||||
* In that case the [.stopService] method must be called when done.
|
||||
*
|
||||
* @return true (default) to automatically stop the service when device is disconnected. False otherwise.
|
||||
*/
|
||||
protected fun stopWhenDisconnected(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
private fun stopService() {
|
||||
// user requested disconnection. We must stop the service
|
||||
Logger.v(logSession, "Stopping service...")
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a message as a Toast notification. This method is thread safe, you can call it from any thread
|
||||
*
|
||||
* @param messageResId an resource id of the message to be shown
|
||||
*/
|
||||
protected fun showToast(messageResId: Int) {
|
||||
handler?.post {
|
||||
Toast.makeText(this@BleProfileService, messageResId, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a message as a Toast notification. This method is thread safe, you can call it from any thread
|
||||
*
|
||||
* @param message a message to be shown
|
||||
*/
|
||||
protected fun showToast(message: String?) {
|
||||
handler?.post {
|
||||
Toast.makeText(this@BleProfileService, message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if the device is connected to the sensor.
|
||||
*
|
||||
* @return `true` if device is connected to the sensor, `false` otherwise
|
||||
*/
|
||||
protected val isConnected: Boolean
|
||||
get() = manager.isConnected
|
||||
}
|
||||
@@ -1,128 +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.android.service
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
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
|
||||
|
||||
private const val CHANNEL_ID = "FOREGROUND_BLE_SERVICE"
|
||||
|
||||
abstract class ForegroundBleService : BleProfileService() {
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
val result = super.onStartCommand(intent, flags, startId)
|
||||
startForegroundService()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
// when user has disconnected from the sensor, we have to cancel the notification that we've created some milliseconds before using unbindService
|
||||
cancelNotification()
|
||||
stopForegroundService()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the service as a foreground service
|
||||
*/
|
||||
private fun startForegroundService() {
|
||||
// when the activity closes we need to show the notification that user is connected to the peripheral sensor
|
||||
// We start the service as a foreground service as Android 8.0 (Oreo) onwards kills any running background services
|
||||
val notification = createNotification(R.string.csc_notification_connected_message, 0)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
startForeground(NOTIFICATION_ID, notification)
|
||||
} else {
|
||||
val nm = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||
nm.notify(NOTIFICATION_ID, notification)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the service as a foreground service
|
||||
*/
|
||||
private fun stopForegroundService() {
|
||||
// when the activity rebinds to the service, remove the notification and stop the foreground service
|
||||
// on devices running Android 8.0 (Oreo) or above
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
stopForeground(true)
|
||||
} else {
|
||||
cancelNotification()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the notification
|
||||
*
|
||||
* @param messageResId the message resource id. The message must have one String parameter,<br></br>
|
||||
* f.e. `<string name="name">%s is connected</string>`
|
||||
* @param defaults
|
||||
*/
|
||||
private fun createNotification(messageResId: Int, defaults: Int): Notification {
|
||||
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)
|
||||
|
||||
return NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle(getString(R.string.app_name))
|
||||
.setContentText(getString(messageResId, manager.bluetoothDevice?.name ?: "Device"))
|
||||
.setSmallIcon(R.drawable.ic_notification_icon)
|
||||
.setColor(ContextCompat.getColor(this, R.color.md_theme_primary))
|
||||
.setContentIntent(pendingIntent)
|
||||
.build()
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private fun createNotificationChannel(channelName: String) {
|
||||
val channel = NotificationChannel(
|
||||
channelName,
|
||||
getString(R.string.channel_connected_devices_title),
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
)
|
||||
channel.description = getString(R.string.channel_connected_devices_description)
|
||||
channel.setShowBadge(false)
|
||||
channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
||||
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the existing notification. If there is no active notification this method does nothing
|
||||
*/
|
||||
private fun cancelNotification() {
|
||||
val nm = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||
nm.cancel(NOTIFICATION_ID)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val NOTIFICATION_ID = 200
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
apply from: rootProject.file("library.gradle")
|
||||
apply plugin: 'kotlin-parcelize'
|
||||
|
||||
dependencies {
|
||||
implementation project(":lib_service")
|
||||
implementation project(":lib_theme")
|
||||
implementation project(":lib_utils")
|
||||
|
||||
implementation libs.chart
|
||||
|
||||
implementation libs.nordic.ble.common
|
||||
implementation libs.nordic.theme
|
||||
implementation libs.nordic.navigation
|
||||
|
||||
implementation libs.nordic.log
|
||||
implementation libs.nordic.dfu
|
||||
implementation libs.nordic.ui.scanner
|
||||
|
||||
implementation libs.bundles.compose
|
||||
implementation libs.androidx.core
|
||||
implementation libs.material
|
||||
implementation libs.lifecycle.activity
|
||||
implementation libs.lifecycle.service
|
||||
implementation libs.compose.lifecycle
|
||||
implementation libs.compose.activity
|
||||
|
||||
testImplementation libs.test.junit
|
||||
androidTestImplementation libs.android.test.junit
|
||||
androidTestImplementation libs.android.test.espresso
|
||||
androidTestImplementation libs.android.test.compose.ui
|
||||
debugImplementation libs.android.test.compose.tooling
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package no.nordicsemi.dfu
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("no.nordicsemi.dfu.test", appContext.packageName)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="no.nordicsemi.dfu">
|
||||
|
||||
<application>
|
||||
<service android:name=".repository.DFUService"/>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -1,22 +0,0 @@
|
||||
package no.nordicsemi.dfu.data
|
||||
|
||||
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
|
||||
|
||||
internal sealed class DFUData
|
||||
|
||||
internal data class NoFileSelectedState(
|
||||
val isError: Boolean = false
|
||||
) : DFUData()
|
||||
|
||||
internal data class FileReadyState(
|
||||
val file: ZipFile,
|
||||
val device: DiscoveredBluetoothDevice
|
||||
) : DFUData()
|
||||
|
||||
internal data class FileInstallingState(
|
||||
val status: DFUServiceStatus = Idle
|
||||
) : DFUData()
|
||||
|
||||
internal object UploadSuccessState : DFUData()
|
||||
|
||||
internal data class UploadFailureState(val message: String?) : DFUData()
|
||||
@@ -1,10 +0,0 @@
|
||||
package no.nordicsemi.dfu.data
|
||||
|
||||
import android.net.Uri
|
||||
|
||||
data class ZipFile(
|
||||
val uri: Uri,
|
||||
val name: String,
|
||||
val path: String?,
|
||||
val size: Long
|
||||
)
|
||||
@@ -1,62 +0,0 @@
|
||||
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
|
||||
|
||||
class DFUFileManager @Inject constructor(
|
||||
@ApplicationContext
|
||||
private val context: Context
|
||||
) {
|
||||
|
||||
private val TAG = "DFU_FILE_MANAGER"
|
||||
|
||||
fun createFile(uri: Uri): ZipFile? {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createFromFile(uri: Uri): ZipFile {
|
||||
val file = uri.toFile()
|
||||
return ZipFile(uri, file.name, file.path, file.length())
|
||||
}
|
||||
|
||||
private fun createFromContentResolver(uri: Uri): ZipFile? {
|
||||
val data = context.contentResolver.query(uri, null, null, null, null)
|
||||
|
||||
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 fileName = data.getString(displayNameIndex)
|
||||
val fileSize = data.getInt(fileSizeIndex)
|
||||
val filePath = if (dataIndex != -1) {
|
||||
data.getString(dataIndex)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
data.close()
|
||||
|
||||
ZipFile(uri, fileName, filePath, fileSize.toLong())
|
||||
} else {
|
||||
Log.d(TAG, "Data loaded from ContentResolver is empty.")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package no.nordicsemi.dfu.data
|
||||
|
||||
import android.content.Context
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import no.nordicsemi.android.dfu.DfuServiceInitiator
|
||||
import no.nordicsemi.dfu.repository.DFUService
|
||||
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
|
||||
import javax.inject.Inject
|
||||
|
||||
class DFUManager @Inject constructor(
|
||||
@ApplicationContext
|
||||
private val context: Context
|
||||
) {
|
||||
|
||||
fun install(file: ZipFile, device: DiscoveredBluetoothDevice) {
|
||||
val starter = DfuServiceInitiator(device.address())
|
||||
.setDeviceName(device.displayName())
|
||||
// .setKeepBond(keepBond)
|
||||
// .setForceDfu(forceDfu)
|
||||
// .setPacketsReceiptNotificationsEnabled(enablePRNs)
|
||||
// .setPacketsReceiptNotificationsValue(numberOfPackets)
|
||||
.setPrepareDataObjectDelay(400)
|
||||
.setUnsafeExperimentalButtonlessServiceInSecureDfuEnabled(true)
|
||||
|
||||
starter.setZip(file.uri, file.path)
|
||||
starter.start(context, DFUService::class.java)
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package no.nordicsemi.dfu.data
|
||||
|
||||
import android.net.Uri
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import no.nordicsemi.android.service.BleManagerStatus
|
||||
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
internal class DFURepository @Inject constructor(
|
||||
private val fileManger: DFUFileManager
|
||||
) {
|
||||
|
||||
private val _data = MutableStateFlow<DFUData>(NoFileSelectedState())
|
||||
val data: StateFlow<DFUData> = _data.asStateFlow()
|
||||
|
||||
private val _command = MutableSharedFlow<DisconnectCommand>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_LATEST)
|
||||
val command = _command.asSharedFlow()
|
||||
|
||||
private val _status = MutableStateFlow(BleManagerStatus.CONNECTING)
|
||||
val status = _status.asStateFlow()
|
||||
|
||||
fun setZipFile(file: Uri, device: DiscoveredBluetoothDevice) {
|
||||
val currentState = _data.value as NoFileSelectedState
|
||||
_data.value = fileManger.createFile(file)?.let {
|
||||
FileReadyState(it, device)
|
||||
} ?: currentState.copy(isError = true)
|
||||
}
|
||||
|
||||
fun setSuccess() {
|
||||
_data.value = UploadSuccessState
|
||||
}
|
||||
|
||||
fun setError(message: String?) {
|
||||
_data.value = UploadFailureState(message)
|
||||
}
|
||||
|
||||
fun install() {
|
||||
_data.value = FileInstallingState()
|
||||
}
|
||||
|
||||
fun sendNewCommand(command: DisconnectCommand) {
|
||||
_command.tryEmit(command)
|
||||
}
|
||||
|
||||
fun setNewStatus(status: BleManagerStatus) {
|
||||
_status.value = status
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
_status.value = BleManagerStatus.CONNECTING
|
||||
_data.value = NoFileSelectedState()
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
package no.nordicsemi.dfu.data
|
||||
|
||||
internal object DisconnectCommand
|
||||
@@ -1,17 +0,0 @@
|
||||
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()
|
||||
@@ -1,107 +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.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 dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import no.nordicsemi.android.dfu.DfuBaseService
|
||||
import no.nordicsemi.android.service.BleManagerStatus
|
||||
import no.nordicsemi.android.service.CloseableCoroutineScope
|
||||
import no.nordicsemi.dfu.R
|
||||
import no.nordicsemi.dfu.data.DFURepository
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
internal class DFUService : DfuBaseService() {
|
||||
|
||||
private val scope = CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
||||
|
||||
@Inject
|
||||
lateinit var repository: DFURepository
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
createDfuNotificationChannel(this)
|
||||
}
|
||||
|
||||
repository.command.onEach {
|
||||
stopSelf()
|
||||
}.launchIn(scope)
|
||||
|
||||
repository.setNewStatus(BleManagerStatus.OK)
|
||||
}
|
||||
|
||||
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:
|
||||
*
|
||||
* intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
*
|
||||
* when user press it. Using NotificationActivity we can check whether the new activity is a root activity (that means no other activity was open before)
|
||||
* or that there is other activity already open. In the later case the notificationActivity will just be closed. System will restore the previous activity.
|
||||
* However if the application has been closed during upload and user click the notification a NotificationActivity will be launched as a root activity.
|
||||
* It will create and start the main activity and terminate itself.
|
||||
*
|
||||
* 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 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)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
repository.setNewStatus(BleManagerStatus.DISCONNECTED)
|
||||
super.onDestroy()
|
||||
scope.close()
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package no.nordicsemi.dfu.view
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import no.nordicsemi.android.utils.exhaustive
|
||||
import no.nordicsemi.dfu.data.DFUData
|
||||
import no.nordicsemi.dfu.data.FileInstallingState
|
||||
import no.nordicsemi.dfu.data.FileReadyState
|
||||
import no.nordicsemi.dfu.data.NoFileSelectedState
|
||||
import no.nordicsemi.dfu.data.UploadFailureState
|
||||
import no.nordicsemi.dfu.data.UploadSuccessState
|
||||
|
||||
@Composable
|
||||
internal fun DFUContentView(state: DFUData, onEvent: (DFUViewEvent) -> Unit) {
|
||||
Box(modifier = Modifier.padding(16.dp)) {
|
||||
when (state) {
|
||||
is NoFileSelectedState -> DFUSelectMainFileView(state, onEvent)
|
||||
is FileReadyState -> DFUSummaryView(state, onEvent)
|
||||
UploadSuccessState -> DFUSuccessView(onEvent)
|
||||
is UploadFailureState -> DFUErrorView(state, onEvent)
|
||||
is FileInstallingState -> DFUInstallingView(state, onEvent)
|
||||
}.exhaustive
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package no.nordicsemi.dfu.view
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
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(state: UploadFailureState, onEvent: (DFUViewEvent) -> Unit) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
ScreenSection {
|
||||
val errorColor = MaterialTheme.colorScheme.error
|
||||
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_fail_circle),
|
||||
contentDescription = stringResource(id = R.string.dfu_failure_icon_description),
|
||||
tint = errorColor
|
||||
)
|
||||
|
||||
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
|
||||
val error = state.message ?: stringResource(id = R.string.dfu_unknown_error)
|
||||
Text(
|
||||
text = error,
|
||||
color = errorColor,
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
|
||||
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,22 +0,0 @@
|
||||
package no.nordicsemi.dfu.view
|
||||
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import no.nordicsemi.android.theme.view.ScreenSection
|
||||
import no.nordicsemi.dfu.data.FileInstallingState
|
||||
|
||||
@Composable
|
||||
internal fun DFUInstallingView(state: FileInstallingState, onEvent: (DFUViewEvent) -> Unit) {
|
||||
ScreenSection {
|
||||
CircularProgressIndicator()
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(text = state.status.toDisplayString())
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package no.nordicsemi.dfu.view
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import no.nordicsemi.android.theme.view.BackIconAppBar
|
||||
import no.nordicsemi.dfu.R
|
||||
import no.nordicsemi.dfu.viewmodel.DFUViewModel
|
||||
|
||||
@Composable
|
||||
fun DFUScreen() {
|
||||
val viewModel: DFUViewModel = hiltViewModel()
|
||||
val state = viewModel.state.collectAsState().value
|
||||
|
||||
Column {
|
||||
BackIconAppBar(stringResource(id = R.string.dfu_title)) {
|
||||
viewModel.onEvent(OnDisconnectButtonClick)
|
||||
}
|
||||
|
||||
// when (state) {
|
||||
// is DisplayDataState -> DFUContentView(state.data) { viewModel.onEvent(it) }
|
||||
// LoadingState -> DeviceConnectingView()
|
||||
// }.exhaustive
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package no.nordicsemi.dfu.view
|
||||
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.size
|
||||
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.NoFileSelectedState
|
||||
|
||||
@Composable
|
||||
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.size(8.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.dfu_choose_info),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ButtonsRow(onEvent: (DFUViewEvent) -> Unit) {
|
||||
|
||||
val fileType = rememberSaveable { mutableStateOf(DfuBaseService.MIME_TYPE_ZIP) }
|
||||
|
||||
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
|
||||
uri?.let { onEvent(OnZipFileSelected(it)) }
|
||||
}
|
||||
|
||||
Button(onClick = {
|
||||
fileType.value = DfuBaseService.MIME_TYPE_ZIP
|
||||
launcher.launch(fileType.value)
|
||||
}) {
|
||||
Text(text = stringResource(id = R.string.dfu_select_zip))
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package no.nordicsemi.dfu.view
|
||||
|
||||
import no.nordicsemi.dfu.data.DFUData
|
||||
|
||||
internal sealed class DFUViewState
|
||||
|
||||
internal object LoadingState : DFUViewState()
|
||||
|
||||
internal data class DisplayDataState(val data: DFUData) : DFUViewState()
|
||||
@@ -1,47 +0,0 @@
|
||||
package no.nordicsemi.dfu.view
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
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 {
|
||||
val successColor = 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),
|
||||
tint = successColor
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.dfu_success),
|
||||
color = successColor,
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
|
||||
Button(onClick = { onEvent(OnPauseButtonClick) }) {
|
||||
Text(text = stringResource(id = R.string.dfu_done))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
package no.nordicsemi.dfu.view
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
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
|
||||
import androidx.compose.material3.Button
|
||||
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.graphics.ColorFilter
|
||||
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.android.theme.view.SectionTitle
|
||||
import no.nordicsemi.dfu.R
|
||||
import no.nordicsemi.dfu.data.FileReadyState
|
||||
import no.nordicsemi.dfu.data.ZipFile
|
||||
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
|
||||
|
||||
@Composable
|
||||
internal fun DFUSummaryView(state: FileReadyState, onEvent: (DFUViewEvent) -> Unit) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
DeviceDetailsView(state.device)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
FileDetailsView(state.file)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Button(onClick = { onEvent(OnInstallButtonClick) }) {
|
||||
Text(text = stringResource(id = R.string.dfu_install))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun DeviceDetailsView(device: DiscoveredBluetoothDevice) {
|
||||
ScreenSection {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_bluetooth),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSecondary),
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
shape = CircleShape
|
||||
)
|
||||
.padding(8.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = device.displayName() ?: "No name",
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Text(text = device.displayAddress(), style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FileDetailsView(file: ZipFile) {
|
||||
val fileName = file.name
|
||||
val fileLength = file.size
|
||||
|
||||
ScreenSection {
|
||||
SectionTitle(
|
||||
icon = Icons.Default.Notifications,
|
||||
title = stringResource(id = R.string.dfu_zip_file_details)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.dfu_file_name, fileName))
|
||||
|
||||
Spacer(modifier = Modifier.size(4.dp))
|
||||
|
||||
Text(text = stringResource(id = R.string.dfu_file_size, fileLength))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package no.nordicsemi.dfu.view
|
||||
|
||||
import android.net.Uri
|
||||
|
||||
internal sealed class DFUViewEvent
|
||||
|
||||
internal data class OnZipFileSelected(val file: Uri) : DFUViewEvent()
|
||||
|
||||
internal object OnInstallButtonClick : DFUViewEvent()
|
||||
|
||||
internal object OnPauseButtonClick : DFUViewEvent()
|
||||
|
||||
internal object OnStopButtonClick : DFUViewEvent()
|
||||
|
||||
internal object OnDisconnectButtonClick : DFUViewEvent()
|
||||
@@ -1,102 +0,0 @@
|
||||
package no.nordicsemi.dfu.viewmodel
|
||||
|
||||
import androidx.lifecycle.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.navigation.CancelDestinationResult
|
||||
import no.nordicsemi.android.navigation.DestinationResult
|
||||
import no.nordicsemi.android.navigation.NavigationManager
|
||||
import no.nordicsemi.android.navigation.SuccessDestinationResult
|
||||
import no.nordicsemi.android.service.BleManagerStatus
|
||||
import no.nordicsemi.android.service.ServiceManager
|
||||
import no.nordicsemi.android.utils.exhaustive
|
||||
import no.nordicsemi.android.utils.getDevice
|
||||
import no.nordicsemi.dfu.data.*
|
||||
import no.nordicsemi.dfu.repository.DFUService
|
||||
import no.nordicsemi.dfu.view.*
|
||||
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
|
||||
import no.nordicsemi.ui.scanner.ScannerDestinationId
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
internal class DFUViewModel @Inject constructor(
|
||||
private val repository: DFURepository,
|
||||
private val progressManager: DFUProgressManager,
|
||||
private val dfuManager: DFUManager,
|
||||
private val serviceManager: ServiceManager,
|
||||
private val navigationManager: NavigationManager
|
||||
) : ViewModel() {
|
||||
|
||||
private var device: DiscoveredBluetoothDevice? = null
|
||||
|
||||
val state = repository.data.combine(progressManager.status) { state, status ->
|
||||
(state as? FileInstallingState)
|
||||
?.run { createInstallingStateWithNewStatus(state, status) }
|
||||
?: state
|
||||
}.combine(repository.status) { data, status ->
|
||||
// when (status) {
|
||||
// BleManagerStatus.CONNECTING -> LoadingState
|
||||
// BleManagerStatus.OK,
|
||||
// BleManagerStatus.DISCONNECTED -> DisplayDataState(data)
|
||||
// }
|
||||
}.stateIn(viewModelScope, SharingStarted.Lazily, LoadingState)
|
||||
|
||||
init {
|
||||
progressManager.registerListener()
|
||||
}
|
||||
|
||||
private fun handleArgs(args: DestinationResult?) {
|
||||
when (args) {
|
||||
is CancelDestinationResult -> navigationManager.navigateUp()
|
||||
is SuccessDestinationResult -> {
|
||||
device = args.getDevice()
|
||||
serviceManager.startService(DFUService::class.java, args.getDevice())
|
||||
}
|
||||
null -> navigationManager.navigateTo(ScannerDestinationId)
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
fun onEvent(event: DFUViewEvent) {
|
||||
when (event) {
|
||||
OnDisconnectButtonClick -> closeScreen()
|
||||
OnInstallButtonClick -> {
|
||||
dfuManager.install(requireFile(), device!!)
|
||||
repository.install()
|
||||
}
|
||||
OnPauseButtonClick -> closeScreen()
|
||||
OnStopButtonClick -> closeScreen()
|
||||
is OnZipFileSelected -> repository.setZipFile(event.file, device!!)
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
private fun closeScreen() {
|
||||
repository.sendNewCommand(DisconnectCommand)
|
||||
repository.clear()
|
||||
}
|
||||
|
||||
private fun requireFile(): ZipFile {
|
||||
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()
|
||||
repository.clear()
|
||||
progressManager.unregisterListener()
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="80dp"
|
||||
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" />
|
||||
</vector>
|
||||
@@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="80dp"
|
||||
android:height="80dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@color/nordicGrass"
|
||||
android:pathData="M12,2C6.5,2 2,6.5 2,12S6.5,22 12,22 22,17.5 22,12 17.5,2 12,2M10,17L5,12L6.41,10.59L10,14.17L17.59,6.58L19,8L10,17Z" />
|
||||
</vector>
|
||||
@@ -1,57 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="dfu_title">DFU</string>
|
||||
|
||||
<string name="dfu_done">Done</string>
|
||||
<string name="dfu_close">Close</string>
|
||||
<string name="dfu_pause">Pause</string>
|
||||
<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>
|
||||
|
||||
<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_type_zip">Distribution packet (ZIP)</string>
|
||||
<string name="dfu_file_type_soft_device">Soft Device</string>
|
||||
<string name="dfu_file_type_bootloader">Bootloader</string>
|
||||
<string name="dfu_file_type_application">Application</string>
|
||||
|
||||
<string name="dfu_choose_file">Choose file</string>
|
||||
<string name="dfu_choose_info">Please select .zip or .hex file with bootloader, application or soft device.</string>
|
||||
<string name="dfu_choose_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>
|
||||
<string name="dfu_macro_dialog_confirm">Confirm</string>
|
||||
<string name="dfu_macro_dialog_dismiss">Dismiss</string>
|
||||
|
||||
<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>
|
||||
|
||||
<string name="dfu_success">Success!</string>
|
||||
<string name="dfu_unknown_error">Unknown error.</string>
|
||||
|
||||
<string name="dfu_file_name">File name: <b>%s</b></string>
|
||||
<string name="dfu_file_size">File size: <b>%d</b> bytes</string>
|
||||
</resources>
|
||||
@@ -1,16 +0,0 @@
|
||||
package no.nordicsemi.dfu
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
||||
@@ -91,7 +91,6 @@ include ':app'
|
||||
include ':profile_bps'
|
||||
include ':profile_cgms'
|
||||
include ':profile_csc'
|
||||
include ':profile_dfu'
|
||||
include ':profile_gls'
|
||||
include ':profile_hrs'
|
||||
include ':profile_hts'
|
||||
|
||||
Reference in New Issue
Block a user