A Never-Ending Service: Android

Deepak Gahlot
8 min readJul 25, 2022

--

Recently I came across a requirement where I have to do some background work when the application is not in the foreground or let’s say it has been killed by Android OS or by the User.

Photo by Clark Tibbs on Unsplash

There are a couple of ways by which we can achieve this functionality in Android. But in this article, we will discuss how to do this using services.

Let’s create a Never-Ending Background Service

So we will be building a very simple app which will be including three components

  • Activity(This will be just be containing a plain Text showing the counter)
  • A service — we all know what this will be doing
  • A broadcast Receiver — This will enable us to restart the service whenever the application is killed or the Service itself is being killed or stopped

Our Service Class

public class AutoStartService extends Service {    private static final String TAG = "AutoService";
public int counter = 0;
private Timer timer;
private TimerTask timerTask;
public AutoStartService(int counter) {
Log.i(TAG, "AutoStartService: Here we go.....");
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
super.onStartCommand(intent, flags, startId);
startTimer();
return START_STICKY;
}
@Override
public void onDestroy() {
super.onDestroy();
Log.i(TAG, "onDestroy: Service is destroyed :( ");
Intent broadcastIntent = new Intent(this, RestarterBroadcastReceiver.class);
sendBroadcast(broadcastIntent);
stoptimertask();
}
public void startTimer() {
timer = new Timer();
//initialize the TimerTask's job
initialiseTimerTask();
//schedule the timer, to wake up every 1 second
timer.schedule(timerTask, 1000, 1000); //
}
public void initialiseTimerTask() {
timerTask = new TimerTask() {
@Override
public void run() {
Log.i(TAG, "Timer is running " + counter++);
}
}
;
}
public void stoptimertask() {
//stop the timer, if it's not already null
if (timer != null) {
timer.cancel();
timer = null;
}
}
}

There are three parts relevant here:

  1. The creation of the service and the initialisation of a counter (which is used to display if the service is alive or not)
  2. onStartCommand that will start the timer which will print the value of the counter every second (note: it returns START_STICKY — that is used to tell Android to try not to kill the service when resources are scarce: note Android can ignore this)
  3. onDestroy which will restart the service using a Broadcast Receiver when killed.

Our Broadcast Receiver

public class RestartBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Log.i(RestartBroadcastReceiver.class.getSimpleName(), "Service Stopped, but this is a never ending service.");context.startService(new Intent(context, AutoStartService.class));;
}
}

Here we are not doing much, whenever the onReceive method is called we are just starting our service again.

Our MainActivity

public class MainActivity extends AppCompatActivity {    Intent mServiceIntent;
private AutoStartService mAutoStartService;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mAutoStartService = new AutoStartService(this);
mServiceIntent = new Intent(this, AutoStartService.class);
if (!isMyServiceRunning(AutoStartService.class)) {
startService(mServiceIntent);
}
}
private boolean isMyServiceRunning(Class<?> serviceClass) {
ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
if (serviceClass.getName().equals(service.service.getClassName())) {
Log.i ("isMyServiceRunning?", true+"");
return true;
}
}
Log.i ("isMyServiceRunning?", false+"");
return false;
}
@Override
protected void onDestroy() {
stopService(mServiceIntent);
Log.i("main", "onDestroy!");
super.onDestroy();
}
}

So there few points to make a note of, we will only start our service if it is not running already. Also, we are stopping the service out self in the onDestroy method, because if the application is killed or destroyed the service will die with it. But here we are calling the onStop method of service, in which we are sending our broadcast to start the service again.

Last thing we need to add the service and broadcast receiver to our manifest file.

<service
android:name=".AutoStartService"
android:enabled="true" >
</
service>
<receiver
android:name=".RestartBroadcastReceiver"
android:enabled="true"
android:exported="true"
android:label="RestartServiceWhenStopped">
</
receiver>

You can see the logcat output and verify that counter is running even after the application is killed. But if you are using a Device which is running on Android OS > 10, we will be getting a crash in the Logout and service will be killed.

java.lang.IllegalStateException: Not allowed to start service Intent { cmp=com.******.neverendingservice/.AutoStartService }: app is in background uid UidRecord{8b19d52 u0a92 RCVR bg:+1m0s38ms idle procs:1 seq(0,0,0)}

Now this is happening because of the background limitation that was imposed by Android after Oreo. More info can be found about this on the below link.

Please do go through this as this is widely asked in Android Interviews

https://developer.android.com/about/versions/oreo/background.html

So to fix this we will be using JobServices and Job Scheduler below is a really good article that you can go through to learn more about them.

@Override
public void onReceive(Context context, Intent intent) {
Log.i(RestartBroadcastReceiver.class.getSimpleName(), "Service Stopped, but this is a never ending service.");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
scheduleJob(
context);
} else {
registerRestarterReceiver(context);
ServiceAdmin bck = new ServiceAdmin();
bck.launchService(context);
}
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public static void scheduleJob(Context context) {
if (jobScheduler == null) {
jobScheduler
= (JobScheduler) context
.getSystemService(JOB_SCHEDULER_SERVICE);
}
ComponentName componentName = new ComponentName(context,
JobService.class);
JobInfo jobInfo = new JobInfo.Builder(1, componentName)
// setOverrideDeadline runs it immediately - you must have at least one constraint
// https://stackoverflow.com/questions/51064731/firing-jobservice-without-constraints
.setOverrideDeadline(0)
.setPersisted(true).build();
jobScheduler.schedule(jobInfo);
}

So we put a conditional check for Android version if it is greater or equal than Lollipop, we then don’t call service directly but we go through the Job Service.

The Two properties that we are setting for

As you will see, if the Android version is greater or equal than Lollipop (minimum requirement for JobServices), instead of calling the service creator right away, we go through the job service. We schedule the job.
Note here two parameters:
• setOverrideDeadline(0) which is a dummy constraint asking to start immediately (at least one is required)
• setPersisted(true) which requires to restart the Job in case the phone is rebooted. It is really not necessary in our code as the Broadcast will receive a message when the phone is rebooted but good to have.

New JobService Class

@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public class JobService extends android.app.job.JobService {
private static String TAG = "JobService";
private static RestartBroadcastReceiver restartBroadcastReceiver;
private static JobService instance;
private static JobParameters jobParameters;
@Override
public boolean onStartJob(JobParameters jobParameters) {
ServiceAdmin serviceAdmin = new ServiceAdmin();
serviceAdmin.launchService(this);
instance = this;
JobService.jobParameters = jobParameters;
return false;
}
private void registerRestarterReceiver() { // the context can be null if app just installed and this is called from restartsensorservice
// https://stackoverflow.com/questions/24934260/intentreceiver-components-are-not-allowed-to-register-to-receive-intents-when
// Final decision: in case it is called from installation of new version (i.e. from manifest, the application is
// null. So we must use context.registerReceiver. Otherwise this will crash and we try with context.getApplicationContext
if (restartBroadcastReceiver == null)
restartBroadcastReceiver
= new RestartBroadcastReceiver();
else try{
unregisterReceiver(restartBroadcastReceiver);
} catch (Exception e){
// not registered
}
//give the time to run
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
IntentFilter filter = new IntentFilter();
filter.addAction(Globals.RESTART_INTENT);
try {
registerReceiver(restartBroadcastReceiver, filter);
} catch (Exception e) {
try {
getApplicationContext().registerReceiver(restartBroadcastReceiver, filter);
} catch (Exception ex) {
}
}
}
}
, 1000);
}
@Override
public boolean onStopJob(JobParameters jobParameters) {
Log.i(TAG, "Stopping job");
Intent broadcastIntent = new Intent(Globals.RESTART_INTENT);
sendBroadcast(broadcastIntent);
// give the time to run
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
unregisterReceiver(restartBroadcastReceiver);
}
}
, 1000);
return false;
}
/**
* called when the tracker is stopped for whatever reason
*
@param context
*/
public static void stopJob(Context context) {
if (instance!=null && jobParameters!=null) {
try{
instance
.unregisterReceiver(restartBroadcastReceiver);
} catch (Exception e){
// not registered
}
Log.i(TAG, "Finishing job");
instance.jobFinished(jobParameters, true);
}
}
}

Creating a never-ending background service in Android > 7

In this post above I explained how to create a never-ending background service. That solution worked for Android up to Version 7. After that, that method has started misbehaving on an increasing number of vendor-specific Android implementations (e.g. Xiaomi) and then finally most of the vendors.
So it is time to give an update.
In order to understand this post, you must understand the previous one as I will build on that code.
The part that does not work any more in Android > 7 is the sequence Service dying calling the Broadcast receiver which in turn will recreate the server. Since Android 7 (but officially since Lollipop!), any process called by a BroadcastReceiver is run at low priority and hence eventually killed by Android. So the service will end up being killed. Depending on the specific vendor implementation, this may happen in some days (e.g. Samsung) or in some hours (e.g. Xiaomi).

In order to solve this issue, we must create a service in the BroadcastReceiver in a way that keeps high priority. The suggested way to do it is by use of job service. A job service guarantees that the process will finish. job services are cool because they allow also to set a number of conditions attached (e.g. running only if some conditions are true e.g. there is an internet connection). We can also require that the job service is restarted if the phone is rebooted.

So the code to make the magic happens is the following. In the Broadcaster Receiver, we will add the following code:

@Overridepublic void onReceive(final Context context, Intent intent) {Log.d(TAG, "about to start timer " + context.toString());if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){scheduleJob(context);} else {ProcessMainClass bck = new ProcessMainClass();bck.launchService(context);}}@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)public static void scheduleJob(Context context) {if (jobScheduler == null) {jobScheduler = (JobScheduler) context.getSystemService(JOB_SCHEDULER_SERVICE);}ComponentName componentName = new ComponentName(context,JobService.class);JobInfo jobInfo = new JobInfo.Builder(1, componentName)// setOverrideDeadline runs it immediately - you must have at least one constraint// https://stackoverflow.com/questions/51064731/firing-jobservice-without-constraints.setOverrideDeadline(0).setPersisted(true).build();jobScheduler.schedule(jobInfo);}

​As you will see, if the Android version is greater or equal than Lollipop (minimum requirement for JobServices), instead of calling the service creator right away, we go through the job service. We schedule the job.
Note here two parameters:
• setOverrideDeadline(0) which is a dummy constraint asking to start immediately (at least one is required)
• setPersisted(true) which requires to restart the Job in case the phone is rebooted. It is really not necessary in our code as the Broadcast will receive a message when the phone is rebooted but good to have
The code for the Job Service is:

private static JobParameters jobParameters;@Overridepublic boolean onStartJob(JobParameters jobParameters) {ProcessMainClass bck = new ProcessMainClass();bck.launchService(this);registerRestarterReceiver();instance= this;JobService.jobParameters= jobParameters;return false;}.../*** called if Android kills the job service* @param jobParameters* @return*/@Overridepublic boolean onStopJob(JobParameters jobParameters) {Log.i(TAG, "Stopping job");Intent broadcastIntent = new Intent(Globals.RESTART_INTENT);sendBroadcast(broadcastIntent);// give the time to runnew Handler().postDelayed(new Runnable() {@Overridepublic void run() {unregisterReceiver(restartSensorServiceReceiver);
}

It will invoke the creation of the service (via the class ServiceAdmin and will register the receiver ready for any request of the restart. It will have a method that is called when Android kills the Job Service (it may happen). This method is in charge of requesting to restart itself via the Broadcast Receiver and after a while, afterwards will deregister the receiver (by delaying a bit to avoid unregistering before the receiver receives the message.

Thank Everyone for going through the article, I hope it would have been helpful.

  1. onDestroy which will restart the service when killed

--

--