Sergei Yendiyarov
Sergei Yendiyarov

Reputation: 535

ListenableWorker does not remove notification icon

I am using ListenableWorker to perform background tasks. Also I want OS to be aware of my service importance, so I call

 setForegroundAsync(new ForegroundInfo(WorkerConstants.NOTIFICATION_FOREGROUND_ID,
 builder.build()));

As suggested in the google documentation.

But when my service stopped or cancelled, i can still see foreground service notification and i cannot remove it.

Furthermore, I added cancel button to this notification, but it does nothing, I cannot close it:

PendingIntent intent = WorkManager.getInstance(getApplicationContext())
                            .createCancelPendingIntent(getId());
                    builder.addAction(R.drawable.ic_redesign_exit,"Stop",intent);

Any suggestions?

Upvotes: 9

Views: 3443

Answers (5)

Hamza Khalid
Hamza Khalid

Reputation: 311

Official Doc as follows:

ListenableWorker now supports the setForegroundAsync() API, and CoroutineWorker supports a suspending setForeground() APi

So the Solution to the above problem is that you must perform the busy job before returning Result.success(). Otherwise this exception will be thrown

Calls to setForegroundAsync() must complete before a ListenableWorker signals completion of work by returning an instance of Result.

Check my code snippet for your Solution:

    public static void startCloudWorker() {
    OneTimeWorkRequest oneTimeWorkRequest =
            new OneTimeWorkRequest.Builder(CloudWorker.class)
                    .setInitialDelay(..., TimeUnit.MILLISECONDS)
                    ...
                    .build();

    WorkManager workManager = getWorkManager();
    workManager.enqueue(oneTimeWorkRequest);
}


public class CloudWorker extends Worker {
    public CloudWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
        super(context, workerParams);
    }

    @NonNull
    @Override
    public Result doWork() {
        final ForegroundInfo foregroundInfo = createForegroundInfo(...)

        setForegroundAsync(foregroundInfo);
        
        // Some busy job here...

        return Result.success();
    }
}

Upvotes: 0

Jakoss
Jakoss

Reputation: 5255

If you have some branch that can end worker immedietaly - add 1 second delay for notification to propagate. setForeground claims to be synchronous, but it's clearly not - there's always time notification needs to show. And calling cancel on not shown notification does nothing. It's clearly a race condition, one not handled by google (i'm not even sure if that can be handled by app).

Extended explanation:

I have code that in single scenario can return immediately. Worker looks like that:

setForeground(getForegroundInfo())
loggingRepository.uploadLogs()

That's it, could not be simpler. BUT, logging repository have this check on start:

if (!userIdentity.isLoggedIn) return

So if user is not loggedIn - do nothing. And if this scenario happend - notification just stayed on. The only thing i had to do is to add delay like that (it's a CoroutineWorker)

setForeground(getForegroundInfo())
loggingRepository.uploadLogs()
delay(1000)

Upvotes: 2

JaviCasa
JaviCasa

Reputation: 738

There's a ktx suspending function called setForeground() on the latest version of the Work Manager library which does an .await() on the listener and turns it synchronous, if you use Coroutines.

Upvotes: 0

Sergei Yendiyarov
Sergei Yendiyarov

Reputation: 535

I filed an issue to google tracker with this. And found out that was totally worng.

Official response as follows:

Looking at your sample app, you are introducing a race between completion of your ListenableFuture returned by CallbackToFutureAdaptor, and the main looper.

Even without the use of REPLACE one of your Notification updates is posted after your Worker is marked as successful.

However, you can actually avoid the race condition by waiting for the call to setForegroundAsync() to be completed (because that also returns a ListenableFuture). Once you do that - you never have a notification that shows up after your Worker is done.

I will also go ahead and file an improvement on how we can automatically do this even when developers get this wrong.

try {
    // This ensures that you waiting for the Notification update to be done.
    setForegroundAsync(createForegroundInfo(0)).get();
} catch (Throwable throwable) {
    // Handle this exception gracefully
    Log.e("FgLW", "Something bad happened", throwable);
}

Upvotes: 9

Lotfi
Lotfi

Reputation: 721

Better forget the setForegroundAsync and go for the traditional way as explained here: https://developer.android.com/training/notify-user/build-notification

Here is what I used:

    Context context = getApplicationContext();
    String title = context.getString(R.string.str_synchronisation);
    String cancel = context.getString(R.string.str_cancel_synchronisation);
    // This PendingIntent can be used to cancel the worker
    PendingIntent intent = WorkManager.getInstance(context)
            .createCancelPendingIntent(getId());
    //notificationManager.cancel(notification_ID);

    NotificationCompat.Builder builder = new NotificationCompat.Builder(context, notif_ch_id)
            .setSmallIcon(R.drawable.ic_sync_black_24dp)
            .setContentTitle(title)
            .setContentText("Use this Content")
            .setOngoing(true)
            .setPriority(NotificationCompat.PRIORITY_DEFAULT)
//                // Add the cancel action to the notification which can
//                // be used to cancel the worker
            .addAction(android.R.drawable.ic_delete, cancel, intent);
    notificationManager.notify(notification_ID, builder.build());

don't forget to keep an instance of the notification manager:

notificationManager = (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE);

Update your notification with:

builder.setContentText(context.getString(R.string.str_veuiller_patienter)
                + ", fichier " + String.valueOf(i + 1) + totalfiles);
notificationManager.notify(notification_ID, builder.build());

cancel your notification with:

notificationManager.cancel(notification_ID);

Upvotes: -2

Related Questions