How to sync Toggle Button State with Foreground Service?
Asked Answered
M

4

8

I have a Foreground Service which I start and stop with a Toggle Button in an Activity. To keep track if the Service is running or not I have used a variable which I store in SharedPreference.

Here is the flow

When a user enables toggle button

startService()     start background work  
makeForeGround()              
bindService()      to get update

When a user disables toggle button

unbindService()    to stop getting updates    
stopForeGround()          
stopService()      stop background work  

When a user leaves the Activity

unbindService(); To stop getting the update. 
                 But the service is running for background work

When a user reenters the Activity

if(Service is running)
Show toggle button state as enabled 
bindService() 

I have achieved all these.

Only problem I face is this:

Sometimes my Service gets killed by OS. In that case, I have no way to update My SharedPreference variable. Hence no way to sync my Toggle button with Service.

How do I get notified that the service has got killed by OS. (Because onDestroy() is not getting called in this case)? I have also gone through How to check if a service is running on Android?, but sadly the solutions and methods provided are deprecated.

Marijo answered 21/3, 2019 at 9:14 Comment(0)
P
3

You should consider approaching your problem from a different direction.

Trying to infer when the system has killed your Service is a problematic. Apps (Services) are not notified when they are terminated, therefor detecting this condition is difficult. However, finding out when a Service has started is trivial (onCreate()). This is a clue to the right approach.

It's clear that you understand that the proper way to exchange information between an Activity and a Service is via a binding. This is the other critical piece of the solution.

SOLUTION

You should expand the lifetime of your Service. Right now, it appears that you create the Service for the express purpose of conducting a long-running task. The Service is created when you want the task to start, and terminates as soon as it ends. In other words, the scope of the Service and the "task" are the same.

Instead, you should make the "task" a child of the Service; the Service should exist for somewhat longer than the "task". In particular, the Service should be available whenever the Activity is started (if for no other reason than to communicate information about the state of the "task" to the Activity).

Whenever the Activity starts, it should bind to the Service, and register a listener with the Service. The Service calls this listener whenever A.) the listener is newly registered, or B.) the "task" changes states (starts or stops). Thus, the Activity is always informed instantly of state changes, so long as it is bound to the Service.

When the Activity wants the "task" to change states, it should inform the Service via the binding (or possibly a startService() call, even though the Service is already running). The Service would then start/stop the background thread (or whatever it is you do to accomplish the "task"), and notify any listeners.

If the Service has been running the "task" for some time, and the Activity pops up all of a sudden, then the Service clearly knows the "task" exists, and the Activity will get that information via the listener, as soon as it binds.

When the Activity stops, it should clean up by unregistering its listener, and unbinding from the Service.

This approach obviates the need for SharedPreferences, or to detect when the Service has stopped. Whenever the Service is created, obviously the "task" is not running, so the Service will always know the correct answer to give to the Activity. And since the Service must be created before it can bind to the Activity, there is no order-of-operations problem.

Petit answered 21/3, 2019 at 18:50 Comment(2)
Great explanation. Even I was thinking some solution like that. But I still run to deadend when it comes to sync my Toggle if service is killed buy OSMarijo
That shouldn't be a problem, because: If Service is killed by OS, whole app is killed. That means that all Activities will be killed. When a new Activity starts, it will bind. If the Service is bound, that means it must be running. If the Service is running, it must know the state of the task.Petit
M
2

Just a workaround. Not a general solution.

I redesigned. Now the flow is

Entering the Activity

bindService()

Leaving the Activity

unbindService()

Enable Toggle

startService()

Disable Toggle

stopService()

How do I keep track if the service is running?

For that, I am using an int variable SERVICE_START_COUNTER in the service and increase its value in onStartCommand(). And I receive the SERVICE_START_COUNTER when the Activity binds to service. Based on the counter value I can differentiate if the service is running or not

public boolean isServiceRunning(int SERVICE_START_COUNTER)
{
    return SERVICE_START_COUNTER == 0 ? false : true;
}

And based on this value I have written a method which sync-up the UI.

public void syncUI(boolean isServiceRunning)
{
   setToggleState(isServiceRunning);
   setAniamtionState(isServiceRunning);
}

SERVICE_START_COUNTER value in different cases?

1) Enter the Activity for the first time (Toggle is still disabled)

onBind() // SERVICE_START_COUNTER = 0

2) Enter the Activity for the first time (User enables the toggle)

onBind()           // SERVICE_START_COUNTER = 0
onStartCommand()   // SERVICE_START_COUNTER = 1

3) Re-enter the Activity for the second time (Toggle is enabled)

onBind()           // SERVICE_START_COUNTER = 1

4) Service is killed by OS. Re-enter the Activity

onBind()           // SERVICE_START_COUNTER = 0

Why it's not a general solution?

Since binding takes time (because onServiceConnected()) is called asynchronously. Hence the Result might not be available right away. For this, I first check the status of service in the Splash Activity and update a similar counter in a Utility class.

Marijo answered 24/3, 2019 at 10:42 Comment(0)
C
0

Your issue is caused by the fact that you are using a Bounded Foreground service. See: https://developer.android.com/guide/components/bound-services

A bound service typically lives only while it serves another application component and does not run in the background indefinitely.

Basically your service existence depends on whether is binded or not.

The alternative is not to use the Bind method and instead use Context.startService() or Context.startForegroundService() in order to start the service and let it run forever. Something like this:

 Intent intent = new Intent(getApplicationContext(), ForegroundService.class);
 intent.setAction(ForegroundService.ACTION_START_FOREGROUND_SERVICE);
 startService(intent);

Where the ForegroundService class looks like this:

public class ForegroundService extends Service {

private static final String TAG_FOREGROUND_SERVICE = "FOREGROUND_SERVICE";
public static final String ACTION_START_FOREGROUND_SERVICE = "ACTION_START_FOREGROUND_SERVICE";
public static final String ACTION_STOP_FOREGROUND_SERVICE = "ACTION_STOP_FOREGROUND_SERVICE";

public ForegroundService () {
}

@Override
public IBinder onBind(Intent intent) {
    // TODO: Return the communication channel to the service.
    throw new UnsupportedOperationException("Not yet implemented");
}

@Override
public void onCreate() {
    super.onCreate();
    Log.d(TAG_FOREGROUND_SERVICE, "My foreground service onCreate().");
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    if(intent != null)
    {
        String action = intent.getAction();

        switch (action)
        {
            case ACTION_START_FOREGROUND_SERVICE:
                startForegroundService();
                break;
            case ACTION_STOP_FOREGROUND_SERVICE:
                stopForegroundService();
                break;
        }
    }
    return super.onStartCommand(intent, flags, startId);
}

private void startForegroundService()
{
    // Build the notification.
    // ... lot of code ...
    Notification notification = builder.build();

    // Start foreground service.
    startForeground(1, notification);
}

private void stopForegroundService()
{
    // Stop foreground service and remove the notification.
    stopForeground(true);

    // Stop the foreground service.
    stopSelf();
}
}

In this case you don't need to synchronize the service state because it will be active for as long as possible and it will only stop when stopSelf() is called. The service will not stop even when the application goes into the background.

Additionally you can use Broadcast result, livedata or EventBus methods to synchronize/communicate between the service and the activities.

I hope this answer helps. I apologize if this is not what you are looking for. It's just an idea.

Cytotaxonomy answered 30/3, 2019 at 9:6 Comment(0)
A
0

Try overriding onTaskRemoved() method of your Service and update the toggle value to false in the method. Check out this Documentation

Antipater answered 30/3, 2019 at 9:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.