Android Services from scratch (II): started services

We said in the first post of this series that one of the classic modes of working with Service components is as started services. Started services support the fundamental mission of the Service class, ensuring that the service will remain active indefinitely until it indicates that finished its work or until another component explicitly stops it.

Let’s see this time a simple example of how to define and use a service in started mode.

Starting the engine

In this repository you can find the StartedService project, which we will use to illustrate this post. The project contains a single activity called TheActivity that is shown when the application is started and displays an interface with a numeric input field and a button with the text Start. The user can enter a number in the input field and, when the button is pressed, the number is sent to a service in background that counts one at a time until it reaches the entered number, dumping the count in the application log and waiting a second between two consecutive numbers.

The action begins here:

 findViewById(R.id.startButton).setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
       int target = 0;
       String input = ((TextView)findViewById(R.id.numberInput)).getText().toString();
       if (input.length() > 0) {
           target = Integer.parseInt(input);
       }
       CountService.startCount(target, TheActivity.this);
   }
 });

This block of code includes the method called when the user presses the Start button. In the last line the static method startCount of the class CountService is called, passing as parameters the number entered by the user (target) and a reference to the instance of TheActivity.

The startCount method hides the client components from the details regarding the build and dispatch of requests to the service. It prevents each client class from having to know the necessary “ceremony”. Let’s look at its contents.

 public static void startCount(int countTarget, Context clientContext) {
   Intent requestIntent = new Intent(clientContext, CountService.class);
   requestIntent.setAction(ACTION_COUNT_TO);
   requestIntent.putExtra(EXTRA_COUNT_TARGET, countTarget);
   clientContext.startService(requestIntent);
 }

To launch a request to a service in started mode, the startService method of the Context class is used. In the last line of startCount this call is run, passing as a parameter an Intent object created for this purpose. Said Intent acts as a container of all the information necessary to express a request for work to the service. In our case the information includes:

  • The class implementing the service that will process the request, CountService.class, indicated in the Intent constructor.
  • Also in the constructor, the TheActivity instance received in the clientContext parameter, the activity that performs the request. At this point it is used only to determine the application containing the service implementation. Any other instance of Context from the same application would do the work.
  • The setAction method identifies the action that is expected to be run by CountService in background. It acts as a command identifier. In our example, CountService only knows how to execute one action, but in general a service could execute any number of operations, commands or actions we need.
  • In the Intent extras we add the parameters associated with the action to be executed. In this case, the number which the service is desired to count to, is attached.

Once all the request information is packaged into the Intent, the client context can send it to the service with startService.

Under the hood

To implement an Android service, the CountService class must extend Service, either directly or by extending any of its inheriting classes. In this example the extension is direct.

import android.app.Service;
... 
public class CountService extends Service {

When extending Service it is necessary to implement the abstract method onBind. This method is critical for launching services in bound mode, but this is not our goal now, so we simply complete it by returning null.

 @Nullable
 @Override
 public IBinder onBind(Intent intent) {
   // no binding today, null is OK
   return null;
 }

Another essential point is to declare the service in the manifest file of the application. Be careful about forgetting it, the application build will not fail in such a case. It is too easy to forget including it and then go crazy trying to figure out why the service does nothing at runtime.

<application
...
>
  <service android:name=".CountService" />
</application>

The key method to overload for a service in started mode is onStartCommand. When a service request is launched through startService the system guarantees that the delivered Intent object will be sent to a call to the onStartCommand method on an instance of the service. In case a live instance already exists, the call is executed on it. Otherwise, the framework automatically creates a new object in the CountService class, inits it by executing its onCreate method, and finally calls the onStartCommand.

Threading the needle

A very important detail to consider is that the onStartCommand method is executed in the main thread of the application. Therefore, if our service starts the requested task directly in it, it will most likely produce an ANR (Application Not Responding) error in our application. When a service is created by extending the Service class directly, it is our code’s responsibility to ensure that the executed task occurs in a background thread.

In our example the problem is dealt with from the bottom of the trenches.

@Override
 public int onStartCommand(Intent intent, int flags, int startId) {
   Log.i(TAG, "Received start id " + startId + ": " + intent);
   Log.i(TAG, "Current thread is " + Thread.currentThread().getName());

   Message msg = mServiceHandler.obtainMessage();
   msg.arg1 = startId;
   msg.obj = intent;
   mServiceHandler.sendMessage(msg);

   return START_NOT_STICKY;
 }

Implementation of onStartCommand inserts into a message (object of the Message class) both the Intent received with the information about the requested job, and the parameter startId, an identifier automatically generated by the Android framework for each request made to the same service instance. The message is sent to the same object as that obtained from, mServiceHandler, an element created during service init by overloading the onCreate method.

@Override
  public void onCreate() {
    Log.i(TAG, "Creating...");

    HandlerThread backgroundThread = new HandlerThread(
      "CounterThread", 
      Process.THREAD_PRIORITY_BACKGROUND
    );
    backgroundThread.start();

    mServiceHandler = new CountHandler(backgroundThread.getLooper());
}

The service handler is created by passing as a parameter a Looper associated with the thread in background. This is created as an instance of HandlerThread, a type of thread provided by the Android framework that runs indefinitely a loop that at each iteration can process a message or a runnable object sent from other threads. In the onCreate method of CounterService the new thread is created, then started, and its Looper is used to connect it to the service handler, an instance of CountHandler, class that is delegated the responsibility of executing the tasks requested to CountService.

CountHandler extends Handler, and it does this by overloading the handleMessage method, which always runs in the background thread. The message sent from the onStartCommand method each time a request is received in CountService is received in handleMessage, which is the appropriate point to start the requested task.

@Override
 public void handleMessage(Message msg) {
   Log.i(TAG, "Current thread is " + Thread.currentThread().getName());

   Intent request = (Intent) msg.obj;
   if (ACTION_COUNT_TO.equals(request.getAction())) {
     int target = request.getIntExtra(EXTRA_COUNT_TARGET, 0);
     countTo(target);
     reportResult(target);
   }

   // Stop the service using the startId, so that we don't stop
   // the service in the middle of handling another job
   stopSelf(msg.arg1);
 }

 private void countTo(int target) {
   for (int i=0; i<target; i++) {
     try {
       Thread.sleep(1000);
       Log.i(TAG, "" + (i+1));
     } catch (InterruptedException e) {
       Thread.currentThread().interrupt();
     }
   }
 }

In our example we see how the Intent with the service request is extracted from the msg parameter, it is checked that it corresponds to a known action type, then the parameters of the action are extracted and finally the requested task is started in the countTo method: count one by one to the number entered by the user.

The eye of the beholder

Pretty, right? Do not? Beauty is so subjective…

What is not so subjective is the contradiction of having a component, Service, supposedly designed to run long-term jobs in background and that every time we want to take advantage of it we have to personally take care of it actually works in the background.

This same idea clicked someone’s head on Google a long time ago, and led to the introduction of another class in the Android framework designed to avoid melee fighting with any thread just to boot a minimally functional service in started mode. In our example today we do not use it. Do you dare to look for it? Tell us what you find in the comments.

Everything that starts must end

So far we have seen how to run our long-running task in background independently of the GUI. The user of our example can make multiple requests to count to different numbers from TheActivity without having to wait until the previous count has finished. Or she can leave the application, and let it continue counting for any time that it needs.

So independent are TheActivity and CounterService that there is no way to get a reference to any of them in the other.

Then, how can we show the user the results or the progress of the tasks in a service? There are several possibilities to achieve this. CounterHandler includes three different alternatives to show the results of our work, all in the reportResult method. In future posts we will talk in detail about results, but today I invite you to explore the code in the example and play with it.

Another important detail that we should address right now about the completion of tasks is the call to the stopSelf method, just at the end of handleMessage. We said at the outset that a service in started mode “will remain active indefinitely until it indicates that finished its work or until another component explicitly stops it”.

It is usually preferable to finish the job without others interrupting us, and when this happens the service should call the stopSelf method to indicate to the system that it has finished processing the request that was started with the identifier indicated in startId. When stopSelf is called with the last request identifier that has reached onStartCommand, the system assumes that CounterService has no pending work and destroys it.

To be continued

More things can be commented about services in started mode. In future releases of the blog we will probably touch some of them, but do not get too attached to this mode of operation. Google is putting an end to it.

There have been warnings of this for some time. Google explicitly recommends scheduled services for devices with Android 5 or higher, and there are jewels as this one buried deep in the official documentation. Now, with Android O in sight, Google warns that services started in background will have time constraints, and the system will destroy them if they surpass them even if it is not in a situation resources scarcity. This supposes the end for this mode of execution, since its main reason of existence is the performance of long-running tasks.

Fortunately, there are more tools to work with on the Android background. The next one we will talk about in this blog is bounded services.

Otros enlaces relacionados

Android Services from scratch

Use Android Priority Job Queue library for your background tasks

 

1 thought on “Android Services from scratch (II): started services”

Leave a Comment

¿Necesitas una estimación?

Calcula ahora

Privacy Preference Center

Own cookies

__unam, gdpr 1P_JAR, DV, NID, _icl_current_language

Analytics Cookies

This cookies help us to understand how users interact with our site

_ga, _gat_UA-42883984-1, _gid, _hjIncludedInSample,

Subscription Cookies

These cookies are used to execute functions of the Web, such as not displaying the advertising banner and / or remembering the user's settings within the session.

tl_3832_3832_2 tl_5886_5886_12 tve_leads_unique

Other