Services & Foreground Service

A Service is an application component that can run in the background without a user interface. Unlike an Activity, a Service has no screen of its own – it exists purely to do work: syncing data, playing audio, or reacting to events while the user is doing something else, or not looking at the app at all.

In RecipeBox, a Service is the natural place to put logic like "upload every recipe photo that changed since the last sync" or "push locally-added recipes to the backend" – work that should keep going even if the user has switched to their home screen.

Three Flavors of Service

Android doesn't have just one kind of Service – the right choice depends on how visible and how long-running the work is.

FlavorWhat it's forUser visibilityModern restrictions
Service (plain/background) Short background chores with no UI tie-in Invisible to the user Heavily restricted on modern Android – the OS may stop it soon after the app leaves the foreground
Foreground Service Ongoing work the user should know about (sync, playback, tracking) Must show a persistent notification while running Allowed to run longer, but must call startForeground() promptly and declare a foreground service type
Bound Service Components that need to talk to the Service directly (method calls, callbacks) No notification required by itself Lives only as long as at least one client stays bound via IBinder
Why the restrictions? A background Service with no notification is invisible battery drain from the user's point of view, so recent Android versions aggressively limit how long one can run once the app is backgrounded. If your work is genuinely ongoing and important, promote it to a Foreground Service so the user is informed and the OS gives it more leeway.

Foreground Service: RecipeSyncService

Suppose RecipeBox needs to push newly-added recipes to a backend, even while the app is in the background. Because this is ongoing work the user should be aware of, we build it as a Foreground Service that shows a persistent "Syncing recipes..." notification – exactly the kind of notification channel covered in Notifications & Channels.

// RecipeSyncService.java
public class RecipeSyncService extends Service {

    public static final String CHANNEL_ID = "recipe_sync_channel";
    private static final int NOTIF_ID = 501;

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
                .setContentTitle("RecipeBox")
                .setContentText("Syncing recipes...")
                .setSmallIcon(R.drawable.ic_sync)
                .setOngoing(true)
                .build();

        // Must be called within a few seconds of starting the service
        startForeground(NOTIF_ID, notification);

        syncRecipesInBackground();

        // If the system kills the service, do not automatically recreate it
        return START_NOT_STICKY;
    }

    private void syncRecipesInBackground() {
        new Thread(() -> {
            List<Recipe> pending = RecipeRepository.getInstance(this).getUnsyncedRecipes();
            for (Recipe recipe : pending) {
                RecipeApiClient.upload(recipe);
            }
            stopSelf(); // work is done, stop the foreground service cleanly
        }).start();
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null; // not a bound service
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        // release any resources, listeners, etc. here
    }
}
Explanation:
  • onStartCommand() runs every time the Service is started via startService()/startForegroundService().
  • startForeground() must be called quickly – it both shows the notification and tells the OS "treat this process as foreground-priority".
  • onBind() returns null because nothing needs to bind to this Service directly.
  • onDestroy() is your cleanup hook, called once the Service is stopped.

Starting the Foreground Service

From an Activity or Fragment, you never call startForeground() yourself – that happens inside the Service. What you call is ContextCompat.startForegroundService(), which correctly starts a foreground-eligible service on every supported Android version:

// Kick off the sync, e.g. after the user adds a new recipe
Intent intent = new Intent(this, RecipeSyncService.class);
ContextCompat.startForegroundService(this, intent);
Important: After calling startForegroundService(), the Service must call startForeground() within a few seconds, or the OS will consider it misbehaving and stop it. Declare a foregroundServiceType (e.g. dataSync) in the manifest on recent API levels, as required for foreground services.
<service
    android:name=".RecipeSyncService"
    android:foregroundServiceType="dataSync"
    android:exported="false" />

Stopping the Service Cleanly

A Service stops in one of two ways: it stops itself once its work is finished, or something external stops it. Calling stopSelf() from inside the Service (as done above, right after the sync loop finishes) removes the notification and lets the process return to normal priority.

// From outside, e.g. if the user cancels sync manually
Intent intent = new Intent(this, RecipeSyncService.class);
stopService(intent);
Rule of thumb: the Service that starts long-running work is also the best place to decide when that work is done and to call stopSelf() – don't rely on the caller to remember to stop it.

Bound Service (Brief Look)

If a component needs to call methods directly on a running Service – for example, a settings screen that wants to ask the sync service "how many recipes are left to upload?" – you'd implement onBind() to return a real IBinder instead of null, and the caller would use bindService()/unbindService() instead of startService(). RecipeBox's sync doesn't need this level of direct interaction, so RecipeSyncService stays a plain (unbound) foreground service.

Declaring the Service in the Manifest

Every Service, like every Activity, must be declared in AndroidManifest.xml before Android will let you start it:

<application ... >
    <service
        android:name=".RecipeSyncService"
        android:foregroundServiceType="dataSync"
        android:exported="false" />
</application>

Troubleshooting

Service crashes with "startForegroundService() did not call startForeground()"? → Call startForeground() as the very first thing in onStartCommand(), not after a long background task.

Notification never disappears? → Make sure stopSelf() or stopService() actually runs; a crashed background thread can leave the Service alive.

Service seems to die randomly on newer phones? → That is expected for plain background Services; if the work matters to the user, promote it to a Foreground Service as shown above.

Services give background work a home, but reacting to system-wide events – like the battery getting low – is the job of a different component. Continue to Broadcast Receivers next.