Android Kotlin future notification with worker with method from main activity - android

I want to schedule my MainActivty to perform checks on its data periodically without showing UI to the user. When a condition is true, I want to show a notification.
Furthermore, I need to check for this notification only once per day, without an exact timing and I want this check to remain scheduled even if the device is rebooted /after poweron.
Looks like the worker model with TimeUnit.ONE_DAY is the best fit for me (Here I am setting the interval at one minute so I don't need to wait for one day to test).
I create in "onCreate" method the Periodic Request Builder and the enqueue the request in the work manager.
Outside onCreate method I define the sendNotification method and provide my class implementatio on Notification Worker, where I define the action to be performed when the woker callback is fired.
In main class I have:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
[...]
val CheckRequest = PeriodicWorkRequestBuilder<NotificationWorker>(1, TimeUnit.MINUTES).build()
WorkManager.getInstance(context).enqueue(CheckRequest)
}
fun sendNotification(){
with(NotificationManagerCompat.from(this)) {
notify(notificationId, builder.build())
}
class NotificationWorker(appContext: Context, workerParams: WorkerParameters):
Worker(appContext, workerParams) {
override fun doWork(): Result {
// Do the work here--in this case, upload the images.
MainActivity().sendNotification()
// Indicate whether the work finished successfully with the Result
return Result.success()
}
}
}
My Issue is that I encounter the following error
java.util.concurrent.ExecutionException: java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()
I think that what's happening is that I am calling a method from the super class of the worker when the UI is not active, so the app context etc is not availlable to the called method.
Any hin on this point would be gladly appreciated.
Kind regards,

Related

Collect Flows in Service

So, I'm trying to collect data from flows in my Foreground service (LifecycleService) in onCreate(), but after the first callback, it is not giving new data.
The code is :
override fun onCreate() {
super.onCreate()
lifecycleScope.launchWhenStarted {
repeatOnLifecycle(Lifecycle.State.STARTED) {
observeCoinsPrices()
}
}
}
I couldn't get lifecycleScope.launch to work in the LifecycleService.onCreate method without it freezing the app, so what I did instead was moved the collector into a method that I use to start the service, assign the Job into a property so I can cancel it when the Service is destroyed.
import kotlinx.coroutines.Job
//...
class MyService : LifecycleService() {
//...
private lateinit var myJob: Job
// my custom method for starting The Foreground service
fun startTheService() {
// call startForeground()
//...
myJob = lifecycleScope.launch {
collectFromFlow()
}
}
override fun onDestroy() {
myJob.cancel()
}
}
In my case, I was wanting to update text in the foreground notification every time a value was emitted to my Flow collector.
Because Flow used in observeCoinsPrices() not replay the latest value (replay < 1). You should change Flow logic

Android Kotlin - Unit test a delayed coroutine that invokes a lambda action when delay is finished

Ive been struggling with this for quite some time now, perhaps someone could help...
I have this function in my class under test:
fun launchForegroundTimer(context: Context) {
helper.log("AppRate", "[$TAG] Launching foreground count down [10 seconds]")
timerJob = helper.launchActionInMillisWithBundle(Dispatchers.Main, TimeUnit.SECOND.toMillis(10), context, this::showGoodPopupIfAllowed)
}
So in that function, I first write to some log and then I call a coroutine function that expects a Dispatcher param, how long to wait before running the action, Any object that I would like to pass on to the action and the actual action function that is invoked when time has passed.
So in this case, the this::showGoodPopupIfAllowed which is a private method in the class, gets called when the 10,000 ms have passed.
Here is that function:
private fun showGoodPopupIfAllowed(context: Context?) {
if (isAllowedToShowAppRate()) {
showGoodPopup(context)
}
}
In that first if, there are a bunch of checks that occur before I can call showGoodPopup(context)
Now, here is the helper.launchActionInMillisWithBundle function:
fun <T> launchActionInMillisWithBundle(dispatcher: CoroutineContext, inMillis: Long, bundle: T, action: (T) -> Unit): Job = CoroutineScope(dispatcher).launchInMillisWithBundle(inMillis, bundle, action)
And here is the actual CoroutineScope extension function:
fun <T> CoroutineScope.launchInMillisWithBundle(inMillisFromNow: Long, bundle: T, action: (T) -> Unit) = this.launch {
delay(inMillisFromNow)
action(bundle)
}
What I am trying to achieve is a UnitTest that calls the launchForegroundTimer function, calls the helper function with the appropriate arguments and also continue through and call that lambda showGoodPopupIfAllowed function where I can also provide mocked behaviour to all the IF statments that occur in isAllowedToShowAppRate.
Currently my test stops right after the launchActionInMillisWithBundle is called and the test just ends. I assume there is no real call to any coroutine because I am mocking the helper class... not sure how to continue here.
I read a few interesting articles but none seems to resolve such state.
My current test function looks like this:
private val appRaterManagerHelperMock = mockkClass(AppRaterManagerHelper::class)
private val timerJobMock = mockkClass(Job::class)
private val contextMock = mockkClass(Context::class)
#Test
fun `launch foreground timer`() {
every { appRaterManagerHelperMock.launchActionInMillisWithBundle(Dispatchers.Main, TimeUnit.SECOND.toMillis(10), contextMock, any()) } returns timerJobMock
val appRaterManager = AppRaterManager(appRaterManagerHelperMock)
appRaterManager.launchForegroundTimer(contextMock)
verify(exactly = 1) { appRaterManagerHelperMock.log("AppRate", "[AppRaterManager] Launching foreground count down [10 seconds]") }
}
I'm using mockk as my Mocking lib.
AppRaterManager is the class under test
I'd like to also mention that, in theory I could have moved the coroutine invocation outside the class under test. So an external class like activity.onResume() could launch some sort of countdown and then call directly a function that checks showGoodPopupIfAllowed(). But currently, please assume that I do not have any way to change the calling code so the timer and coroutine should remain in the class under test domain.
Thank you!
Alright, I read a bit deeper into capturing/answers over at https://mockk.io/#capturing and saw there is a capture function.
So I captured the lambda function in a slot which enables me invoke the lambda and then the actual code continues in the class under test. I can mock the rest of the behavior from there.
Here is my test function for this case (for anyone who gets stuck):
#Test
fun `launch foreground timer, not participating, not showing good popup`() {
val slot = slot<(Context) -> Unit>()
every { appRaterManagerHelperMock.launchActionInMillisWithBundle(Dispatchers.Main, TimeUnit.SECOND.toMillis(10), contextMock, capture(slot)) } answers {
slot.captured.invoke(contextMock)
timerJobMock
}
every { appRaterManagerHelperMock.isParticipating() } returns false
val appRaterManager = AppRaterManager(appRaterManagerHelperMock)
appRaterManager.launchForegroundTimer(contextMock)
verify(exactly = 1) { appRaterManagerHelperMock.log("AppRate", "[AppRaterManager] Launching foreground count down [10 seconds]") }
verify(exactly = 1) { appRaterManagerHelperMock.isParticipating() }
verify(exactly = 0) { appRaterManagerHelperMock.showGoodPopup(contextMock, appRaterManager) }
}
So what's left now is how to test the coroutine actually invokes the lambda after the provided delay time is up.

AndroidViewModel cancel job on demand

I have a job inside my AndroidViewModel class. Job is triggered by viewModelScope.launch. Job is a long running process which return result by lambda functions. According to requirement If user want to cancel the job while remaining in the scope on button click it should cancel the job. The problem is when I cancel the job, process is still running in the background and it is computing the background task. Below is my ViewModelClass with its job and cancel function.
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.*
class SelectionViewModel(val app: Application) : AndroidViewModel(app) {
private var mainJob: Job? = null
private var context: Context? = null
fun performAction(
fileOptions: FileOptions,
onSuccess: (ArrayList<String>?) -> Unit,
onFailure: (String?) -> Unit,
onProgress: (Pair<Int, Int>) -> Unit
) {
mainJob = viewModelScope.launch {
withContext(Dispatchers.IO) {
kotlin.runCatching {
while (isActive) {
val mOutputFilePaths: ArrayList<String> = ArrayList()
// Long running Background task
.. progress
OnProgress.invoke(pair)
// resul success
onSuccess.invoke(mOutputFilePaths)
}
}.onFailure {
withContext(Dispatchers.Main) {
onFailure.invoke(it.localizedMessage)
}
}
}
}
}
fun cancelJob() {
mainJob?.cancel()
}
}
Here it is I am initiating my ViewModel
val viewModel: SelectionViewModelby lazy {
ViewModelProviders.of(this).get(SelectionViewModel::class.java)
}
and when I start the job I call the following method
viewModel.performAction(fileOptions,{success->},{failure->},{progress->})
When I want to cancel the task. I call the following method.
viewModel.cancelJob()
Problem is even after canceling the job I am still receiving the progress as it is being invoked. This means job has not been canceled.
I want to implement the correct way to start and cancel the job while remaining in the viewmodel scope.
So what is the proper way to implement the viewmodel to start and cancel the job?
In order to cancel the job you have to have a suspending function call.
This means that if your job has code like
while (canRead) {
read()
addResults()
}
return result
this can never be cancelled the way you wish it to be cancelled.
there are two ways you can cancel this code
a) add a delay function (this will check for cancelling and cancel your job)
b) (which in the above case is the correct way) periodically add a yield() function
so the above code should look like this:
while(canRead) {
yield()
read()
addResults()
}
return result
edit: some further explanations are probably necessary to make this clear
just because you run something withContext, does not mean that coroutines can stop or break it at any time
what coroutines do is basically change the old way of doing things with callbacks and replace it with suspending functions
what we used to do for complex calculations was start a thread ,which would execute the calculations and then get a callback with the results.
at any point you could cancel the thread and the work would stop.
cancelling coroutines is not the same
if you cancel a coroutine what you basically do is tell it that the job is cancelled , and at the next opportune moment it should stop
but if you don't use yield() delay() or any suspending function such an opportune moment will never arrive
it is the equivalent of running something like this with threads
while(canRead && !cancelled) {
doStuff
}
where you would manually set the cancelled flag, if you set it but didn't check it in your code , it would never stop
as a side note, be careful because right now you have a big block of calculations running code, this will run on one thread because you never called a suspending function. When you add the yield() call , it could change threads or context (within what you defined ofc) so make sure it is thread safe

How to dispose Rx in WorkManager?

I implemented an AlarmManager to send notifications when user adds a due date to a Task. However, when the user turns off the device, all the alarms are lost. Now I'm updating the BroadcastReceiver to receive an android.intent.action.BOOT_COMPLETED and reschedule all the alarms set to each task.
My first attempt was to get an Rx Single with all the tasks where the due date is higher than the current time inside the BroadcastReceiver, then reschedule all the alarms. The issue is I'm not able to dispose the Observable once the BroadcastReceiver has no lifecycle. Also, it seems that this is not a good approach.
During my researches, the IntentService was a good solution for this case, but I'm getting into the new WorkManager library and the OneTimeWorkRequest looks like a good and simple solution.
The Worker is being called and executing correctly, but I'm not able to dispose the Observable because the onStopped method is never called.
Here is the implementation, based on this snippet:
class TaskAlarmWorker(context: Context, params: WorkerParameters) :
Worker(context, params), KoinComponent {
private val daoRepository: DaoRepository by inject()
private val compositeDisposable = CompositeDisposable()
override fun doWork(): Result {
Timber.d("doWork")
val result = LinkedBlockingQueue<Result>()
val disposable =
daoRepository.getTaskDao().getAllTasks().applySchedulers().subscribe(
{ result.put(Result.SUCCESS) },
{ result.put(Result.FAILURE) }
)
compositeDisposable.add(disposable)
return try {
result.take()
} catch (e: InterruptedException) {
Result.RETRY
}
}
override fun onStopped(cancelled: Boolean) {
Timber.d("onStopped")
compositeDisposable.clear()
}
}
Is WorkManager a good solution for this case?
Is it possible to dispose the Observable correctly?
Yes WorkManager is a good solution(even could be the best one)
You should use RxWorker instead of Worker. here is an example:
To implement it. add androidx.work:work-rxjava2:$work_version to your build.gradle file as dependency.
Extend your class from RxWorker class, then override createWork() function.
class TaskAlarmWorker(context: Context, params: WorkerParameters) :
RxWorker(context, params), KoinComponent {
private val daoRepository: DaoRepository by inject()
override fun createWork(): Single<Result> {
Timber.d("doRxWork")
return daoRepository.getTaskDao().getAllTasks()
.doOnSuccess { /* process result somehow */ }
.map { Result.success() }
.onErrorReturn { Result.failure() }
}
}
Important notes about RxWorker:
The createWork() method is called on the main thread but returned single is subscribed on the background thread.
You don’t need to worry about disposing the Observer since RxWorker will dispose it automatically when the work stops.
Both returning Single with the value Result.failure() and single with an error will cause the worker to enter the failed state.
You can override onStopped function to do more.
Read more :
How to use WorkManager with RxJava
Stackoverflow answer
You can clear it in onStoped() method then compositeDisposable.dispose();
Then call super.onStoped()

How to know that my app was restarted to run a scheduled job

Sometimes my app is forced to quit and then later Android restarts it just to run a scheduled job. I can't find a callback that would correspond to this scenario: that my app got killed in the meantime, so I have to restore all my retained state.
I want to avoid a proliferation of entry points where I have to re-check whether everything is still in place. I already have onUpdate() for a widget, onCreate() for the main activity, onResume() for a view fragment, and onStartJob() for the scheduled job. Any of them could be the first thing to be called after the app is killed, so in all these places I have to repeat the initialization code.
Is there a single point where I can register a callback that will re-initialize my app state?
To be specific, I have a JobService:
class RefreshImageService : JobService() {
override fun onStartJob(params: JobParameters): Boolean {
MyLog.i("RefreshImageService start job")
updateWidgetAndScheduleNext(applicationContext)
return true
}
override fun onStopJob(params: JobParameters): Boolean {
MyLog.i("RefreshImageService stop job")
return true
}
}
The updateWidgetAndScheduleNext() call involves some state I have:
private data class TimestampedBitmap(val bitmap : Bitmap, val timestamp : Long)
private var tsBitmap: TimestampedBitmap? = null
To compute the timestamp, I have to call into further code that has its own state; that other code is called form all the entry points I mentioned. I'd like to centralize my initialization code, if possible.

Categories

Resources