I need some clarification regarding using WorkManager instead of Foreground service for long running and non ending service which helps to scan and advertise the BLE data even the device in standby.
I am extending the ListenableWorker and not setting any future result in startWork which helps to me run my work as long running. I need guidelines on
Is it ok to use WM like this?
Will there be any performance issue?
How long VM will run?
Please clarify.
Sample code :
class InternalBLEWorker(private val appContext: Context, private val params:WorkerParameters) : ListenableWorker(appContext, params)
{
private var workThread: LooperThread? = null
private var bluetoothManager: BluetoothManager? = null
internal open fun createBluetoothManager(workThread: LooperThread
): BluetoothManager {
return BluetoothManager(applicationContext, workThread)
}
companion object {
private val running = AtomicBoolean(false)
const val NOTIFICATION_ID = 42
}
override fun onStopped() {
super.onStopped()
callbackToFutureAdapter?.setCancelled()
future.cancel(true)
bluetoothManager?.apply {
tearDown {
workThread?.apply { post { quit() } }
}
}
}
override fun startWork(): ListenableFuture<Result> {
setForegroundAsync(ForegroundInfo(NOTIFICATION_ID, InternalBLEWorkManager.getAppNotification()!!, FOREGROUND_SERVICE_TYPE_LOCATION))
return future
}
private var callbackToFutureAdapter: CallbackToFutureAdapter.Completer<Result>? = null
private val future: ListenableFuture<Result> = CallbackToFutureAdapter.getFuture { completer ->
callbackToFutureAdapter = completer
val firstStart = running.compareAndSet(false, true)
// Stop service if we are in a state where we cannot use it.
if (!BleSupportHelper.hasBlePermission(appContext)) {
logger.warn("Bluetooth LE permission denied, stopping BLE service")
}else if (firstStart) {
// Everything is OK. Set up bluetooth manager
(LooperThread.createAndStart("Bluetooth") as LooperThread).also {
workThread = it
bluetoothManager = createBluetoothManager(it).apply {
create()
start()
}
}
}
}
}
Related
I have been developing this Android app for some time and have realized that my Activities are way too bloated so I have been trying to switch to MVVM with Clean Architecture but am running into an issue. I have this BluetoothLe service that provides the data to my application and it seems like I can only bind to my service from my Activities which is a problem because I am trying to separate the presentation from the data layer.
Here is some code from my activity which uses the service:
#AndroidEntryPoint
class DeviceActivity: AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
// No NightMode allowed.
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
super.onCreate(savedInstanceState)
binding = ActivityDeviceBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
setSupportActionBar(binding.myToolbar)
if (intent.extras!!.get("btDeviceName") != null) {
deviceName = intent.extras!!.get("btDeviceName").toString()
binding.deviceTitle.text = deviceName
}
setupActionBar()
// Binding to the service via gattServiceIntent.
val gattServiceIntent = Intent(this, BluetoothLeService::class.java)
bindService(gattServiceIntent, serviceConnection, Context.BIND_AUTO_CREATE)
...
}
// Code to manage Service lifecycle.
private val serviceConnection: ServiceConnection = object : ServiceConnection {
override fun onServiceConnected(componentName: ComponentName?, service: IBinder?) {
bluetoothService = (service as BluetoothLeService.LocalBinder).getService()
bluetoothService?.let { bluetooth ->
// Call functions on service to check connection and connect to devices
if (!bluetooth.initialize()) {
Log.e("blah", "Unable to initialize Bluetooth.")
finish()
}
// Perform device connection
if (bluetoothDevice != null) {
bluetooth.connect(bluetoothDevice!!)
}
}
}
override fun onServiceDisconnected(name: ComponentName?) {
bluetoothService = null
}
}
Then I listen for the data via update receiver:
// Connect to device after registering for the receiver.
private fun registerBluetoothLeServiceReceiver() {
try {
registerReceiver(gattUpdateReceiver, makeGattUpdateIntentFilter())
if (bluetoothService != null) {
connectToBluetoothDevice()
}
} catch (e: Exception) {
Log.d(TAG, "onResume: $e Couldn't register receiver.")
}
}
private val gattUpdateReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
BluetoothLeService.ACTION_GATT_CONNECTED -> {
connected = true
updateConnectionState(context.resources.getString(R.string.connected))
}
BluetoothLeService.ACTION_GATT_DISCONNECTED -> {
connected = false
updateConnectionState(context.resources.getString(R.string.disconnected))
leaveActivity()
binding.progressBarCyclic.visibility = GONE
}
// When services are discovered on the device, we request gatt to update the mtu size.
BluetoothLeService.ACTION_MTU_UPDATED -> {
bluetoothService?.enableNotification()
}
// We write the characteristic only when notifications have been enabled.
BluetoothLeService.ACTION_NOTIFICATION_ENABLED -> {
bluetoothService?.writeGattCharacteristic()
}
// This is where the data stream is received.
BluetoothLeService.ACTION_DATA_AVAILABLE -> {
val byteArr = intent.getByteArrayExtra("byteArray")
Log.d(TAG, "onReceive: ${byteArr.contentToString()}")
if (byteArr != null) {
// Firmware versions greater than 3634 have different data streams.
binding.recyclerviewDevices.visibility = View.VISIBLE
if (isLessThan3634 && boardVersion == 2.1) {
dataProcessor.processData(byteArr)
} else {
dataProcessor.processData(byteArr, 1)
}
}
}
}
}
}
After this is a bunch of data manipulation and processing which I would much rather do in my ViewModel or another class for separation purposes.
Is there a way that I can get my service to bind to a repository or something that will allow me to separate the presentation and data layers so I can listen for data in something other than my Activity?
I think you will need to add a new android module and name it "device" that implements your Bluetooth datasource and inject this module in your data layer.
This article could help you
https://five.agency/android-architecture-part-1-every-new-beginning-is-hard/
I'm trying to make an application for wearable in which I get various metrics from health services such as the heart rate for different sports. However I get the following error:: lateinit property healthServicesManager has not been initialized.
I have declared the lateinit as:
#Inject
lateinit var healthServicesManager: HealthServicesManagerTaekwondo
The complete code of the class:
#AndroidEntryPoint
class ExerciseServiceTaekwondo : LifecycleService() {
#Inject
lateinit var healthServicesManager: HealthServicesManagerTaekwondo
private val localBinder = LocalBinder()
private var isBound = false
private var isStarted = false
private var isForeground = false
private val _exerciseState = MutableStateFlow(ExerciseState.USER_ENDED)
val exerciseState: StateFlow<ExerciseState> = _exerciseState
private val _exerciseMetrics = MutableStateFlow(emptyMap<DataType, List<DataPoint>>())
val exerciseMetrics: StateFlow<Map<DataType, List<DataPoint>>> = _exerciseMetrics
private val _aggregateMetrics = MutableStateFlow(emptyMap<DataType, AggregateDataPoint>())
val aggregateMetrics: StateFlow<Map<DataType, AggregateDataPoint>> = _aggregateMetrics
private val _exerciseLaps = MutableStateFlow(0)
val exerciseLaps: StateFlow<Int> = _exerciseLaps
private val _exerciseDurationUpdate = MutableStateFlow(ActiveDurationUpdate())
val exerciseDurationUpdate: StateFlow<ActiveDurationUpdate> = _exerciseDurationUpdate
private val _locationAvailabilityState = MutableStateFlow(LocationAvailability.UNKNOWN)
val locationAvailabilityState: StateFlow<LocationAvailability> = _locationAvailabilityState
fun prepareExercise() {
lifecycleScope.launch {
healthServicesManager.prepareExercise()
}
}
fun startExercise() {
lifecycleScope.launch {
healthServicesManager.startExercise()
}
postOngoingActivityNotification()
}
fun pauseExercise() {
lifecycleScope.launch {
healthServicesManager.pauseExercise()
}
}
fun resumeExercise() {
lifecycleScope.launch {
healthServicesManager.resumeExercise()
}
}
/**
* End exercise in this service's coroutine context.
*/
fun endExercise() {
lifecycleScope.launch {
healthServicesManager.endExercise()
}
removeOngoingActivityNotification()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
Log.d(TAG, "onStartCommand")
if (!isStarted) {
isStarted = true
if (!isBound) {
// We may have been restarted by the system. Manage our lifetime accordingly.
stopSelfIfNotRunning()
}
// Start collecting exercise information. We might stop shortly (see above), in which
// case launchWhenStarted takes care of canceling this coroutine.
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
healthServicesManager.exerciseUpdateFlow.collect {
when (it) {
is ExerciseMessage.ExerciseUpdateMessage ->
processExerciseUpdate(it.exerciseUpdate)
is ExerciseMessage.LapSummaryMessage ->
_exerciseLaps.value = it.lapSummary.lapCount
is ExerciseMessage.LocationAvailabilityMessage ->
_locationAvailabilityState.value = it.locationAvailability
}
}
}
}
}
}
// If our process is stopped, we might have an active exercise. We want the system to
// recreate our service so that we can present the ongoing notification in that case.
return Service.START_STICKY
}
private fun stopSelfIfNotRunning() {
lifecycleScope.launch {
// We may have been restarted by the system. Check for an ongoing exercise.
if (!healthServicesManager.isExerciseInProgress()) {
// Need to cancel [prepareExercise()] to prevent battery drain.
if (_exerciseState.value == ExerciseState.PREPARING) {
lifecycleScope.launch {
healthServicesManager.endExercise()
}
}
// We have nothing to do, so we can stop.
stopSelf()
}
}
}
private fun processExerciseUpdate(exerciseUpdate: ExerciseUpdate) {
val oldState = _exerciseState.value
if (!oldState.isEnded && exerciseUpdate.state.isEnded) {
// Our exercise ended. Gracefully handle this termination be doing the following:
// TODO Save partial workout state, show workout summary, and let the user know why the exercise was ended.
// Dismiss any ongoing activity notification.
removeOngoingActivityNotification()
// Custom flow for the possible states captured by the isEnded boolean
when (exerciseUpdate.state) {
ExerciseState.TERMINATED -> {
// TODO Send the user a notification (another app ended their workout)
Log.i(
TAG,
"Your exercise was terminated because another app started tracking an exercise"
)
}
ExerciseState.AUTO_ENDED -> {
// TODO Send the user a notification
Log.i(
TAG,
"Your exercise was auto ended because there were no registered listeners"
)
}
ExerciseState.AUTO_ENDED_PERMISSION_LOST -> {
// TODO Send the user a notification
Log.w(
TAG,
"Your exercise was auto ended because it lost the required permissions"
)
}
else -> {
}
}
} else if (oldState.isEnded && exerciseUpdate.state == ExerciseState.ACTIVE) {
// Reset laps.
_exerciseLaps.value = 0
}
_exerciseState.value = exerciseUpdate.state
_exerciseMetrics.value = exerciseUpdate.latestMetrics
_aggregateMetrics.value = exerciseUpdate.latestAggregateMetrics
_exerciseDurationUpdate.value =
ActiveDurationUpdate(exerciseUpdate.activeDuration, Instant.now())
}
override fun onBind(intent: Intent): IBinder {
super.onBind(intent)
handleBind()
return localBinder
}
override fun onRebind(intent: Intent?) {
super.onRebind(intent)
handleBind()
}
private fun handleBind() {
if (!isBound) {
isBound = true
// Start ourself. This will begin collecting exercise state if we aren't already.
startService(Intent(this, this::class.java))
}
}
override fun onUnbind(intent: Intent?): Boolean {
isBound = false
lifecycleScope.launch {
// Client can unbind because it went through a configuration change, in which case it
// will be recreated and bind again shortly. Wait a few seconds, and if still not bound,
// manage our lifetime accordingly.
delay(UNBIND_DELAY_MILLIS)
if (!isBound) {
stopSelfIfNotRunning()
}
}
// Allow clients to re-bind. We will be informed of this in onRebind().
return true
}
private fun removeOngoingActivityNotification() {
if (isForeground) {
Log.d(TAG, "Removing ongoing activity notification")
isForeground = false
stopForeground(true)
}
}
private fun postOngoingActivityNotification() {
if (!isForeground) {
isForeground = true
Log.d(TAG, "Posting ongoing activity notification")
createNotificationChannel()
startForeground(NOTIFICATION_ID, buildNotification())
}
}
private fun createNotificationChannel() {
val notificationChannel = NotificationChannel(
NOTIFICATION_CHANNEL,
NOTIFICATION_CHANNEL_DISPLAY,
NotificationManager.IMPORTANCE_DEFAULT
)
val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
manager.createNotificationChannel(notificationChannel)
}
private fun buildNotification(): Notification {
// Make an intent that will take the user straight to the exercise UI.
val pendingIntent = NavDeepLinkBuilder(this)
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.exerciseFragment)
.createPendingIntent()
// Build the notification.
val notificationBuilder = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL)
.setContentTitle(NOTIFICATION_TITLE)
.setContentText(NOTIFICATION_TEXT)
.setSmallIcon(R.drawable.ic_run)
.setContentIntent(pendingIntent)
.setOngoing(true)
.setCategory(NotificationCompat.CATEGORY_WORKOUT)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
// Ongoing Activity allows an ongoing Notification to appear on additional surfaces in the
// Wear OS user interface, so that users can stay more engaged with long running tasks.
val lastUpdate = exerciseDurationUpdate.value
val duration = lastUpdate.duration + Duration.between(lastUpdate.timestamp, Instant.now())
val startMillis = SystemClock.elapsedRealtime() - duration.toMillis()
val ongoingActivityStatus = Status.Builder()
.addTemplate(ONGOING_STATUS_TEMPLATE)
.addPart("duration", Status.StopwatchPart(startMillis))
.build()
val ongoingActivity =
OngoingActivity.Builder(applicationContext, NOTIFICATION_ID, notificationBuilder)
.setAnimatedIcon(R.drawable.ic_run)
.setStaticIcon(R.drawable.ic_run)
.setTouchIntent(pendingIntent)
.setStatus(ongoingActivityStatus)
.build()
ongoingActivity.apply(applicationContext)
return notificationBuilder.build()
}
/** Local clients will use this to access the service. */
inner class LocalBinder : Binder() {
fun getService() = this#ExerciseServiceTaekwondo
}
companion object {
private const val NOTIFICATION_ID = 1
private const val NOTIFICATION_CHANNEL = "com.example.exercise.ONGOING_EXERCISE"
private const val NOTIFICATION_CHANNEL_DISPLAY = "Ongoing Exercise"
private const val NOTIFICATION_TITLE = "Exercise Sample"
private const val NOTIFICATION_TEXT = "Ongoing Exercise"
private const val ONGOING_STATUS_TEMPLATE = "Ongoing Exercise #duration#"
private const val UNBIND_DELAY_MILLIS = 3_000L
fun bindService(context: Context, serviceConnection: ServiceConnection) {
val serviceIntent = Intent(context, ExerciseServiceTaekwondo::class.java)
context.bindService(serviceIntent, serviceConnection, Context.BIND_AUTO_CREATE)
}
fun unbindService(context: Context, serviceConnection: ServiceConnection) {
context.unbindService(serviceConnection)
}
}
}
I am new to the coroutine concept. I'm not very confident in the code I've written. Basically I have a queue of user events that I send to my server. Adding an event is done through the main thread and sends it into a coroutine. If the event was successfully sent, I delete it from the queue, if the send failed, it is not deleted and will be retried for the next cycle. To solve the concurrency issues I used a mutex. Can you tell me if pretty good or horrible and a solution in this case?
My code:
data class GeoDataEvent(
val location : Location,
val category : Int
)
// This class is instantiated in my android service
class GeoDataManager(private var mainRepository: MainRepository) {
private var eventQueue : Queue<GeoDataEvent> = LinkedList()
private val eventQueueMutex : Mutex = Mutex()
fun enqueueEvent(location: Location, category : Int){
CoroutineScope(Dispatchers.IO).launch {
eventQueueMutex.withLock {
eventQueue.add(
GeoDataEvent(
location = location,
category = category
)
)
}
}
}
// Called at each new location by Android location service
private fun processNewLocation(location: Location){
/* Some code */
handleEventQueue()
}
private fun handleEventQueue(){
CoroutineScope(Dispatchers.IO).launch {
eventQueueMutex.withLock {
if (eventQueue.isNotEmpty()) {
mainRepository.getAuthToken()?.let { token ->
eventQueue.peek()?.let { event ->
if (sendEvent(token, event)){
eventQueue.remove()
}
}
}
}
}
}
}
private suspend fun sendEvent(token : String, event : GeoDataEvent) : Boolean {
mainRepository.sendGeoDataEvent(token, event).let { res ->
return res.isSuccessful
}
}
}
Thank you for your help
I'm trying to create a BLE service that will scan for devices and using rxKotlin create an observable that will allow another class to observe when a device is found. I'm confused on how to create the observable that will allow another class to subscribe and tutorials are all over the place. Can someone give me a pointer on how to do so or a good tutorial.
Bluetoothservice class callback where devices are discovered
var foundDeviceObservable: Observable<BluetoothDevice> = Observable.create { }
private val scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
with(result.device) {
var foundName = if (name == null) "N/A" else name
foundDevice = BluetoothDevice(
foundName,
address,
address,
result.device.type.toString()
)
foundDeviceObservable.subscribe {
//Update Observable value?
}
}
}
}
class DeviceListViewModel(application: Application) : AndroidViewModel(application) {
private val bluetoothService = BLEService()
//Where I am trying to do logic with device
fun getDeviceObservable(){
bluetoothService.getDeviceObservable().subscribe{ it ->
}
}
Solution
Was able to find the solution after reading user4097210's reply. Just had to change the found device to
var foundDeviceObservable: BehaviorSubject<BluetoothDevice> = BehaviorSubject.create()
and then call the next method in the callback
private val scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
with(result.device) {
var foundName = if (name == null) "N/A" else name
foundDevice = BluetoothDevice(
foundName,
address,
address,
result.device.type.toString()
)
foundDeviceObservable.onNext(foundDevice)
}
}
}
use BehaviorSubject
// create a BehaviorSubject
var foundDeviceObservable: BehaviorSubject<BluetoothDevice> = BehaviorSubject()
// call onNext() to send new found device
foundDeviceObservable.onNext(foundDevice)
// do your logic use foundDeviceObservable
foundDeviceObservable.subscribe(...)
This is My Code :
#RequiresApi(Build.VERSION_CODES.M)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.home_activity)
loadProductForTheFirst()
#RequiresApi(Build.VERSION_CODES.M)
private fun hasNetworkAvilable(context: Context): Boolean {
val service = Context.CONNECTIVITY_SERVICE
val manager = context.getSystemService(service) as ConnectivityManager
val network = manager.activeNetwork
return (network != null)
}
#RequiresApi(Build.VERSION_CODES.M)
fun loadProductForTheFirst(){
swipeRefreshMain.isRefreshing = true
viewModel.getalldata().observe(this, Observer {
if (!it.isNullOrEmpty()) {
recycler_main.apply {
layoutManager = GridLayoutManager(this#HomeActivity, 2)
adapter = RecyclerAdapterMain(it, this#HomeActivity)
swipeRefreshMain.isRefreshing = false
}
} else {
if (hasNetworkAvilable(this)) {
viewModel.products.observe(this, Observer {
recycler_main.apply {
layoutManager = GridLayoutManager(this#HomeActivity, 2)
adapter = RecyclerAdapterMain(it, this#HomeActivity)
swipeRefreshMain.isRefreshing = false
}
})
viewModel.setup()
} else {
/// in here if the user not internet for loading the products
/// the alert dialog displays .
AlertDialog.Builder(this)
.setTitle("Internet State")
.setMessage("please turn on your internet connection")
.create()
.show()
/// in here I want a method ( workmanager )
// that as soon as the internet be accessible
/// my product will be updated .
}
}
})
}
well , For the first time that user open my app need the internet to load product from api .
So I just want the method like WorkManager to check if the intenrnet avalibility is accessible .
And after that my method will be load from api .
I did some search but could'nt find any useful example of work with workmanager.
anyone can help me with this . ?
I did this code and work for me .
I put it here if someone looking for this method .
I used work manager to get data from api whenever the network is on .
val constraints = Constriants.builder(this)
.setRequiredNetworkType(NetworkType.Connected)
val workManager : WorkManager = WorkManager.getInstance(this)
val oneRequestWork = OneRequestWorker.build(UploadWorker::class.java)
.setconstrints(constraints)
.build
workmanager.enqueue(oneRequestWork)
the Upload worker class :
class UploadWorker(context : Context , param : WorkerParameters) : Worker(context , param)
private val viewModel: ViewModelRoom by lazy {
ViewModelProvider(
ViewModelStore(),
FactoryRoom(RepositoryCart(DataBaseRoom.invoke(applicationContext)))
)
.get(ViewModelRoom::class.java)
}
override fun dowork() : Result {
return try {
viewModel.setup()
Result.success()
} catch (e: Exception) {
Result.failure()
}