Remove DFU profile

This commit is contained in:
Sylwester Zieliński
2022-02-23 16:49:02 +01:00
parent fe358262cb
commit 0078378e6c
30 changed files with 0 additions and 1359 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +0,0 @@
package no.nordicsemi.dfu.data
internal object DisconnectCommand

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -91,7 +91,6 @@ include ':app'
include ':profile_bps' include ':profile_bps'
include ':profile_cgms' include ':profile_cgms'
include ':profile_csc' include ':profile_csc'
include ':profile_dfu'
include ':profile_gls' include ':profile_gls'
include ':profile_hrs' include ':profile_hrs'
include ':profile_hts' include ':profile_hts'