I'm new to Work manager in android and I want to update the timeInterval of PeriodicWorkRequest once I got the response from server. Here is my sample code where I'm trying to update the timeInterval. But it is going to an infinite loop of calling the doWork() function. Please suggest to me where it is wrong.
class RandomNumberGenerator(context: Context, workerParams: WorkerParameters) : Worker(
context,
workerParams) {
private val min = 0
private val max = 100
var randomNumber = 0
var context: Context
init {
Log.d(TAG, "Constructor invoked")
this.context = context
Log.d(TAG, "" + workerParams.id.toString())
}
private fun startRandomNumberGenerator() {
Log.d(TAG, "startRandomNumberGenerator: isStopped: $isStopped")
var i = 0
while (i < 5) {
try {
Thread.sleep(1000)
randomNumber = Random().nextInt(max) + min
Log.d(
TAG,
"Thread Id: " + Thread.currentThread().id + " Random Number: " + randomNumber
)
i++
} catch (e: Exception) {
}
if (i == 5) {
callUpdateSyncTime()
}
}
}
private fun callUpdateSyncTime() {
Log.d(TAG, "callUpdateSyncTime() called")
val periodicWork =
PeriodicWorkRequest.Builder(RandomNumberGenerator::class.java, 20, TimeUnit.MINUTES)
.build()
WorkManager.getInstance(context)
.enqueueUniquePeriodicWork(
"work_manager_random",
ExistingPeriodicWorkPolicy.REPLACE,
periodicWork
)
}
companion object {
const val TAG = "WorkManager"
}
override fun doWork(): Result {
startRandomNumberGenerator()
return Result.success()
}
override fun onStopped() {
Log.d(TAG, "onStopped() called")
super.onStopped()
}
}
Here is the code how I'm enqueuing from my MainActivity
val workRequestRandomNumber: PeriodicWorkRequest =
PeriodicWorkRequestBuilder<RandomNumberGenerator>(
15,
TimeUnit.MINUTES
).build()
workManager.enqueueUniquePeriodicWork(
"work_manager_random",
ExistingPeriodicWorkPolicy.KEEP,
workRequestRandomNumber
)
When you enqueue a PeriodicWorkRequest, it will be executed immeditially if you don't add an initial delay using setInitialDelay().
Given that you are rescheduling your worker each time it's completed, I would suggest to use a OneTimeWorkRequest, instead of a repeating one, with an initial delay:
val workRequestRandomNumber =
OneTimeWorkRequestBuilder<RandomNumberGenerator>()
.setInitialDelay(
15,
TimeUnit.MINUTES)
.addTag("work_manager_random")
.build()
workManager.enqueue(workRequestRandomNumber)
As it is right now, WorkManager is sending a cancellation to your worker that you are ignoring.
Related
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)
}
}
}
So I created a simple Worker class. I would like to start this worker as onTimeWork and Periodic Work as well.
Before WorkManager I used Android Job, and inside the Job there is a dedicated method to decide whetherthe current job is periodic: params.isPeriodic
Is there any way to check this in Worker class inside doWork method?
**Worker:**
override fun doWork(): Result {
var workResult = Result.success()
val isPeriodic = false
if (isPeriodic) {
...
}
launch {
}
return workResult
}
**Schedules:**
un schedulePeriodicAsync(context: Context) {
val constraint =
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED).build()
val newMessageWorker =
PeriodicWorkRequest.Builder(
NewMessageWorker::class.java,5,
TimeUnit.MINUTES)
.setConstraints(constraint)
.setBackoffCriteria(
BackoffPolicy.LINEAR,
OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
TimeUnit.MILLISECONDS
).build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
DailySyncWorker.TAG,
ExistingPeriodicWorkPolicy.REPLACE,newMessageWorker)
}
fun scheduleNowAsync(context: Context, workCallback: JobCallback? = null) {
jobCallback = workCallback
val constraint =
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED).build()
val newMessageWorker =
OneTimeWorkRequest.Builder(NewMessageWorker::class.java)
.setConstraints(constraint)
.setBackoffCriteria(
BackoffPolicy.LINEAR,
OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
TimeUnit.MILLISECONDS
)
.build()
WorkManager.getInstance(context).enqueueUniqueWork(
TAG,
ExistingWorkPolicy.REPLACE,newMessageWorker)
}
So I have an Android Job which handle network request.
This job can started in many ways, so it can run easily parralel, which is bad for me.
I would like to achive that, the job don't started twice, or if it started, than wait before the try catch block, until the first finishes.
So how can I to reach that, just only one object be/run at the same time.
I tried add TAG and setUpdateCurrent false, but it didn't do anything, so when I started twice the job, it rung parallel. After that I tried mutex lock, and unlock. But it did the same thing.
With mutex, I should have to create an atomic mutex, and call lock by uniq tag or uuid?
Ok, so I figured it out what is the problem with my mutex, the job create a new mutex object every time, so it never be the same, and it never will wait.
My Job:
class SendCertificatesJob #Inject constructor(
private val sendSync: SendSync,
private val sharedPreferences: SharedPreferences,
private val userLogger: UserLogger,
private val healthCheckApi: HealthCheckApi
) : Job(), CoroutineScope {
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO
private val countDownLatch = CountDownLatch(1)
private val mutex = Mutex()
override fun onRunJob(params: Params): Result {
var jobResult = Result.SUCCESS
if (!CommonUtils.isApiEnabled(context))
return jobResult
val notificationHelper = NotificationHelper(context)
var nb: NotificationCompat.Builder? = null
if (!params.isPeriodic) {
nb = notificationHelper.defaultNotificationBuilder.apply {
setContentTitle(context.resources.getString(R.string.sending_certificates))
.setTicker(context.resources.getString(R.string.sending_certificates))
.setOngoing(true)
.setProgress(0, 0, true)
.setSmallIcon(android.R.drawable.stat_notify_sync)
.setLargeIcon(
BitmapFactory.decodeResource(
context.resources,
R.mipmap.ic_launcher
)
)
}
notificationHelper.notify(NOTIFICATION_ID, nb)
jobCallback?.jobStart()
}
val failureCount = params.failureCount
if (failureCount >= 3) {
nb?.setOngoing(false)
?.setContentTitle(context.resources.getString(R.string.sending_certificates_failed))
?.setTicker(context.resources.getString(R.string.sending_certificates_failed))
?.setProgress(100, 100, false)
?.setSmallIcon(android.R.drawable.stat_sys_warning)
notificationHelper.notify(NOTIFICATION_ID, nb)
return Result.FAILURE
}
GlobalScope.launch(Dispatchers.IO) {
mutex.lock()
userLogger.writeLogToFile("SendCertificatesJob.onRunJob(), date:" + Calendar.getInstance().time)
try {
//Test
var doIt = true
var count =0
while (doIt){
Timber.d("SendSyncWorker: $count")
count++
delay(10000)
if(count == 12)
doIt = false
}
healthCheckApi.checkHealth(ApiModule.API_KEY).await()
try {
sendSync.syncRecordedClients()
} catch (e: Exception) {
e.printStackTrace()
}
val result = sendSync().forEachParallel2()
result.firstOrNull { it.second != null }?.let { throw Exception(it.second) }
val sb = StringBuilder()
if (nb != null) {
nb.setOngoing(false)
.setContentTitle(context.resources.getString(R.string.sending_certificates_succeeded))
.setTicker(context.resources.getString(R.string.sending_certificates_succeeded))
.setProgress(100, 100, false)
.setStyle(NotificationCompat.BigTextStyle().bigText(sb.toString()))
.setSmallIcon(android.R.drawable.stat_notify_sync_noanim)
notificationHelper.notify(NOTIFICATION_ID, nb)
jobCallback?.jobEnd()
}
sharedPreferences.edit().putLong(KEY_LATEST_CERTIFICATES_SEND_DATE, Date().time)
.apply()
} catch (e: Exception) {
Timber.tag(TAG).e(e)
if (nb != null) {
nb.setOngoing(false)
.setContentTitle(context.resources.getString(R.string.sending_certificates_failed))
.setTicker(context.resources.getString(R.string.sending_certificates_failed))
.setProgress(100, 100, false)
.setSmallIcon(android.R.drawable.stat_sys_warning)
notificationHelper.notify(NOTIFICATION_ID, nb)
jobCallback?.jobEnd()
}
jobResult = Result.RESCHEDULE
} finally {
countDownLatch.countDown()
mutex.unlock()
}
}
countDownLatch.await()
return jobResult
}
Job schedule:
fun scheduleNowAsync(_jobCallback: JobCallback? = null) {
jobCallback = _jobCallback
JobRequest.Builder(TAG_NOW)
.setExecutionWindow(1, 1)
.setBackoffCriteria(30000, JobRequest.BackoffPolicy.LINEAR)
.setRequiredNetworkType(JobRequest.NetworkType.CONNECTED)
.setRequirementsEnforced(true)
.setUpdateCurrent(true)
.build()
.scheduleAsync()
}
fun schedulePeriodicAsync() {
jobCallback = null
JobRequest.Builder(TAG)
.setPeriodic(900000)
.setRequiredNetworkType(JobRequest.NetworkType.CONNECTED)
.setRequirementsEnforced(true)
.setUpdateCurrent(true)
.build()
.scheduleAsync()
}
I found the solution for my problem.
So because I use dagger, I provided a singleton Mutex object, and injected into the job. When the job starts call mutex.lock(), and beacuse there is only 1 object from the mutex, even if another job starts, the second job will waite until the firsjob is done.
I have an IntentService running in my app. I want to stop when user presses a cancel button, but onHandleIntent keeps running, even when onDestroy (IntentService) was called.
I tried stopSelf() in the middle of execution, stopSelf(int) and stopService(intent), but doesn't work.
class DownloadIntentService : IntentService("DownloadIntentService") {
val TAG: String = "DownloadIntentService"
val AVAILABLE_QUALITIES: Array<Int> = Array(5){240; 360; 480; 720; 1080}
// TODO Configurations
val PREFERED_LANGUAGE = "esLA"
val PREFERED_QUALITY = AVAILABLE_QUALITIES[0]
#Inject
lateinit var getVilosDataInteractor: GetVilosInteractor
#Inject
lateinit var getM3U8Interactor: GetM3U8Interactor
#Inject
lateinit var downloadDataSource: DownloadsRoomDataSource
companion object {
var startedId: Int = 0
}
override fun onCreate() {
super.onCreate()
(application as CrunchApplication).component.inject(this)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d(TAG, "onStartComand ${startId}")
startedId = startId
super.onStartCommand(intent, flags, startId)
return Service.START_NOT_STICKY
}
override fun onHandleIntent(intent: Intent?) {
Log.d(TAG, "starting download service")
val download = downloadDataSource.getDownloadById(intent?.getLongExtra(MEDIA_ID_EXTRA, 0) ?: 0)
Log.d(TAG, "A new download was found: ${download.id} ${download.serieName} ${download.collectionName} ${download.episodeName}")
val vilosResponse = getVilosDataInteractor(download.episodeUrl)
val stream: StreamData? = vilosResponse.streams.filter {
it.hardsubLang?.equals(PREFERED_LANGUAGE) ?: false
}.getOrNull(0)
if(stream == null) {
Log.d(TAG, "Stream not found with prefered language ($PREFERED_LANGUAGE)")
return
}
Log.d(TAG, "Best stream option: " + stream.url)
val m3u8Response = getM3U8Interactor(stream.url)
val m3u8Data: M3U8Data? = m3u8Response.playlist.filter { it.height == PREFERED_QUALITY }[0]
if(m3u8Data == null) {
Log.d("M3U8","Resolution ${PREFERED_QUALITY}p not found")
return
}
Log.d(TAG, m3u8Data.url)
val root = Environment.getExternalStorageDirectory().toString()
val myDir = File(root + "/episodes/");
if (!myDir.exists()) {
myDir.mkdirs()
}
val output = myDir.getAbsolutePath() + "EPISODENAME.mp4";
val cmd = "-y -i ${m3u8Data.url} ${output}"
when (val result: Int = FFmpeg.execute(cmd)) {
Config.RETURN_CODE_SUCCESS -> Log.d(TAG, "Success")
Config.RETURN_CODE_CANCEL -> Log.d(TAG, "Cancel")
else -> Log.d(TAG, "Default: $result")
}
}
override fun onDestroy() {
super.onDestroy()
Log.d(TAG, "onDestroy")
}
}
I try to stop from a Fragment
val intent = Intent(requireContext(), DownloadIntentService::class.java)
requireContext().stopService(intent)
Thanks in advance
Basically you can not. OnHandleIntent run on worker thread. To stop it you have to do same thing which one can do with any another Thread i.e. have a boolean flag check inside onHandleIntent and before doing operation check of this flag is true. Now when you want to cancel update the flag to false.
It also depends on what you are doing. IF something is already in process tat will keep on running. Not you have to have state machine to make it stop.
I'm trying to cancel a task that hasn't started yet.
My Worker class code:
class TestWork(context: Context, workerParams: WorkerParameters) :
Worker(context, workerParams) {
override fun doWork(): Result {
val data = inputData.getInt(KEY_OBJ, -1)
runBlocking {
for (i in 1..3){
Log.d("MyTag", "testWork: $data")
delay(1000)
}
}
return Result.SUCCESS
}
override fun onStopped() {
super.onStopped()
Log.d("MyTag", "stopped")
}
companion object {
const val KEY_OBJ = "key"
val WORK_NAME = "name"
}
}
The code sample that running 3 works and canceling second.
But second work continues.
val data1 = Data.Builder()
.putInt(TestWork.KEY_OBJ, 1)
.build()
val workRequest1 = OneTimeWorkRequest.Builder(TestWork::class.java)
.setInputData(data1)
.addTag("1")
.build()
val data2 = Data.Builder()
.putInt(TestWork.KEY_OBJ, 2)
.build()
val workRequest2 = OneTimeWorkRequest.Builder(TestWork::class.java)
.setInputData(data2)
.addTag("2")
.build()
val data3 = Data.Builder()
.putInt(TestWork.KEY_OBJ, 3)
.build()
val workRequest3 = OneTimeWorkRequest.Builder(TestWork::class.java)
.setInputData(data3)
.addTag("3")
.build()
WorkManager.getInstance().enqueueUniqueWork(
TestWork.WORK_NAME,
ExistingWorkPolicy.APPEND,
workRequest1
)
WorkManager.getInstance().enqueueUniqueWork(
TestWork.WORK_NAME,
ExistingWorkPolicy.APPEND,
workRequest2
)
WorkManager.getInstance().enqueueUniqueWork(
TestWork.WORK_NAME,
ExistingWorkPolicy.APPEND,
workRequest3
)
WorkManager.getInstance().cancelWorkById(workRequest2.id)
How to cancel second work only? While the first is executed
You should use the isStopped() method call inside your onWork method
This is explained in the video presented at the Android Developer Summit. Around minute 15 into the presentation there's a whole section on how to stop work in WorkManager that goes through these details
As I see, you have a mistake in your cancelling line, you have to replace cancelWorkById("2") by cancelAllWorkByTag("2") because you are adding tags .addTag("2") to the Work.
I'm currently using WorkManager and I have tried to cancel works with tags and it works.