A service is a task running in the background to perform actions without a user interface and they are used to perform long running operations with a significant use of resources by avoiding delays in the user experience.
The services belong to one or both of the following categories:
- started: the service is launched using the the method startService()
- bound: the service is launched using the the method bindService() and the components of the application that has launched the service can communicate with the same service
Here I show a simple app to compare these two types of service:
- create an Android project without starting activity
- edit the file AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="eu.lucazanini.service" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="18" /> <application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > <activity android:name=".MainActivity" android:label="@string/title_activity_main" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <service android:name=".MyStartedService" android:enabled="true" android:process=":started" /> <service android:name=".MyBoundService" android:enabled="true" /> </application> </manifest>
note the presence of two lines with service tag, you need them to enable the services to be performed and, for the service .MyStartedService, I set the attribute android:process=”:started” to specify a process associated to the service, this will allow you in the test phase to stop the service without stopping the main activity
- edit the files in the folder res/values/, nothing of important here
- dimens.xml
<resources> <!-- Default screen margins, per the Android Design guidelines. --> <dimen name="activity_horizontal_margin">16dp</dimen> <dimen name="activity_vertical_margin">16dp</dimen> </resources>
- strings.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">Service</string> <string name="title_activity_main">Service</string> <string name="action_settings">Settings</string> <string name="hello_world">Hello world!</string> <string name="started_service">Started Service</string> <string name="bound_service">Bound Service</string> <string name="output_bound_service">Bound service output</string> <string name="check_label">onDestroy</string> <string name="radio1_label">START_STICKY</string> <string name="radio2_label">START_NOT_STICKY</string> <string name="button_label">Launch</string> </resources>
- styles.xml
<resources> <!-- Base application theme, dependent on API level. This theme is replaced by AppBaseTheme from res/values-vXX/styles.xml on newer devices. --> <style name="AppBaseTheme" parent="android:Theme.Light"> <!-- Theme customizations available in newer API levels can go in res/values-vXX/styles.xml, while customizations related to backward-compatibility can go here. --> </style> <!-- Application theme. --> <style name="AppTheme" parent="AppBaseTheme"> <!-- All customizations that are NOT specific to a particular API-level can go here. --> </style> </resources>
- dimens.xml
- create the file res/layout/activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context=".MainActivity" > <TextView android:id="@+id/lblStartedService" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:text="@string/started_service" android:textAppearance="?android:attr/textAppearanceLarge" /> <CheckBox android:id="@+id/checkBox1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignLeft="@+id/lblStartedService" android:layout_below="@+id/lblStartedService" android:checked="true" android:text="@string/check_label" /> <RadioGroup android:id="@+id/radioGroup1" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_below="@+id/checkBox1" android:layout_centerHorizontal="true" android:orientation="vertical" > <RadioButton android:id="@+id/radio1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:checked="true" android:text="@string/radio1_label" /> <RadioButton android:id="@+id/radio2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/radio2_label" /> </RadioGroup> <Button android:id="@+id/btn_started" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignLeft="@+id/radioGroup1" android:layout_below="@+id/radioGroup1" android:text="@string/button_label" /> <TextView android:id="@+id/lblBoundService" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignLeft="@+id/btn_started" android:layout_below="@+id/btn_started" android:layout_marginTop="31dp" android:text="@string/bound_service" android:textAppearance="?android:attr/textAppearanceLarge" /> <Button android:id="@+id/btn_bounded" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignLeft="@+id/lblBoundService" android:layout_below="@+id/lblBoundService" android:text="@string/button_label" /> <TextView android:id="@+id/lblOutput" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignLeft="@+id/btn_bounded" android:layout_below="@+id/btn_bounded" android:text="@string/output_bound_service" android:textAppearance="?android:attr/textAppearanceLarge" /> </RelativeLayout>
this resource defines a layout with the following controls:
- Started service:
- option onDestroy: if selected the line StopSelf() is executed ant then the method onDestroy of the class MyStartedService too
- oprions START_STICKY and START_NOT_STICK: to set the value returned from the method onStartCommand of the class MyStartedService
- a button to launch the service “started”
- Bound service:
- a button to launch the service “bound”
- a text box to display the output of the service “bound”
- Started service:
- create file eu/lucazanini/service/MainActivity.java
package eu.lucazanini.service; import java.lang.ref.WeakReference; import android.app.Activity; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.util.Log; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.CheckBox; import android.widget.RadioGroup; import android.widget.TextView; import eu.lucazanini.service.MyBoundService.LocalBinder; public class MainActivity extends Activity implements OnClickListener { /** * Instances of static inner classes do not hold an implicit reference to * their outer class. */ private static class MyHandler extends Handler { private final WeakReference<MainActivity> mActivity; public MyHandler(MainActivity activity) { mActivity = new WeakReference<MainActivity>(activity); } @Override public void handleMessage(Message msg) { MainActivity activity = mActivity.get(); if (activity != null) { Bundle b = msg.getData(); String key = b.getString("timeKey"); activity.tv.setText(key); } } } private final MyHandler handler = new MyHandler(this); private MyBoundService mBoundService; /** Defines callbacks for service binding, passed to bindService() */ private ServiceConnection mConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName className, IBinder service) { // This is called when the connection with the service has been // established, giving us the service object we can use to // interact with the service. Because we have bound to a explicit // service that we know is running in our own process, we can // cast its IBinder to a concrete class and directly access it. mBoundService = ((LocalBinder) service).getService(); // LocalBinder binder = (LocalBinder) service; // mBoundService = binder.getService(); Thread t = new Thread(separateThread); t.start(); } @Override public void onServiceDisconnected(ComponentName className) { // This is called when the connection with the service has been // unexpectedly disconnected -- that is, its process crashed. // Because it is running in our same process, we should never // see this happen. mBoundService = null; } }; private Runnable separateThread = new Runnable() { @Override public void run() { mBoundService.test(tv, handler); doUnbindService(); } }; private final String TAG = this.getClass().getName(); private boolean mIsBound = false; private CheckBox cb; private RadioGroup rg; private TextView tv; private void doBindService() { // Establish a connection with the service. We use an explicit // class name because we want a specific service implementation that // we know will be running in our own process (and thus won't be // supporting component replacement by other applications). bindService(new Intent(MainActivity.this, MyBoundService.class), mConnection, Context.BIND_AUTO_CREATE); mIsBound = true; } private void doUnbindService() { if (mIsBound) { // Detach our existing connection. unbindService(mConnection); mIsBound = false; } } @Override public void onClick(View v) { Intent intent; switch (v.getId()) { case R.id.btn_started: Log.d(TAG, "started service click"); intent = new Intent(MainActivity.this, MyStartedService.class); Bundle extras = new Bundle(); extras.putBoolean("onDestroy", cb.isChecked()); if (rg.getCheckedRadioButtonId() == R.id.radio2) { extras.putString("return", "START_NOT_STICKY"); } else { extras.putString("return", "START_STICKY"); } intent.putExtras(extras); startService(intent); break; case R.id.btn_bounded: Log.d(TAG, "bound service click"); doBindService(); break; } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); tv = (TextView) findViewById(R.id.lblOutput); Button btn1 = (Button) findViewById(R.id.btn_started); btn1.setOnClickListener(this); Button btn2 = (Button) findViewById(R.id.btn_bounded); btn2.setOnClickListener(this); cb = (CheckBox) findViewById(R.id.checkBox1); rg = (RadioGroup) findViewById(R.id.radioGroup1); // Bind to Bound Service Intent intent = new Intent(this, ServiceConnection.class); bindService(intent, mConnection, Context.BIND_AUTO_CREATE); } @Override protected void onDestroy() { super.onDestroy(); doUnbindService(); } }
this is the main activity, you see:
- the onClick event to manage the buttons, btn1 for the “started” service and btn2 for the “bound” service
- the code for MyHandler, ServiceConnection and separateThread is used for “bound” service only
- create the file eu/lucazanini/service/MyStartedService.java
package eu.lucazanini.service; import android.app.Service; import android.content.Intent; import android.os.Bundle; import android.os.IBinder; import android.util.Log; public class MyStartedService extends Service { private final String TAG = this.getClass().getName(); @Override public IBinder onBind(Intent intent) { Log.d(TAG, "onBind"); return null; } @Override public void onCreate() { super.onCreate(); Log.d(TAG, "onCreate"); } @Override public void onDestroy() { super.onDestroy(); Log.d(TAG, "onDestroy"); } @Override public int onStartCommand(Intent intent, int flags, int startId) { Log.d(TAG, "onStartCommand"); Utils.test(TAG); Bundle extras; if (intent != null) { extras = intent.getExtras(); } else { extras = null; } if (extras != null && extras.getBoolean("onDestroy")) { stopSelf(); Log.d(TAG, "stopping service"); } else { Log.d(TAG, "no stopping service"); } if (extras != null && extras.getString("return").equals("START_NOT_STICKY")) { Log.d(TAG, "START_NOT_STICKY"); return Service.START_NOT_STICKY; } else { Log.d(TAG, "START_STICKY"); return Service.START_STICKY; } } }
this class is the “started” service and it is called from the main activity using the StartService() method; extending the Service class you must implement the onBind method but in the case of “started” service put only the statement “return null;” because this method is never called; the job of the service is in the onStartCommand() method, the code line “Utils.test(TAG);”
- create the file eu/lucazanini/service/MyBoundService.java
package eu.lucazanini.service; import android.app.Service; import android.content.Intent; import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.util.Log; import android.widget.TextView; public class MyBoundService extends Service { /** * Class used for the client Binder. Because we know this service always * runs in the same process as its clients, we don't need to deal with IPC. */ public class LocalBinder extends Binder { MyBoundService getService() { // Return this instance of MyBoundService so clients can call public // methods return MyBoundService.this; } } // Binder given to clients private final IBinder mBinder = new LocalBinder(); private final String TAG = this.getClass().getName(); @Override public IBinder onBind(Intent intent) { Log.d(TAG, "onBind"); return mBinder; } @Override public void onCreate() { super.onCreate(); Log.d(TAG, "onCreate"); } @Override public void onDestroy() { super.onDestroy(); Log.d(TAG, "onDestroy"); } public void test(TextView tv, Handler handler) { Utils.test(TAG, handler); } }
this class is the “bound” service and it is called from the main activity using the StartService() method; the onBind method has been overridden to return the object mBinder of the inner class LocalBinder that extends Binder
- create the file eu/lucazanini/service/Utils.java
package eu.lucazanini.service; import java.text.DateFormat; import java.util.Calendar; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.util.Log; public final class Utils { public static String getTime() { Calendar calendar = Calendar.getInstance(); DateFormat formatTime = DateFormat.getTimeInstance(); return formatTime.format(calendar.getTime()); } public static void test(String TAG) { String displayTime, currentTime; displayTime = new String(); long endTime = System.currentTimeMillis() + 10 * 1000; while (System.currentTimeMillis() < endTime) { currentTime = new String(getTime()); if (!displayTime.equals(currentTime)) { displayTime = new String(currentTime); Log.d(TAG, "time " + currentTime); } } } public static void test(String TAG, Handler handler) { String displayTime, currentTime; displayTime = new String(); long endTime = System.currentTimeMillis() + 10 * 1000; while (System.currentTimeMillis() < endTime) { currentTime = new String(getTime()); if (!displayTime.equals(currentTime)) { Message msg = new Message(); Bundle b = new Bundle(); b.putString("timeKey", currentTime); msg.setData(b); handler.sendMessage(msg); displayTime = new String(currentTime); Log.d(TAG, "time " + currentTime); } } } private Utils() { } }
this is a class with static methods doing the job of the services
And now let’s test the app:
- launch the app
- leaving the defalt options launch the service “started” with th first button “Launch”
- see the output in the LogCat view that shows the time for 10 seconds and the call to onDestroy
the service is started using the code startService() in the main Activity, the argument in startService is an Intent for the class MyStartedService that extends the class Service, the work done by the service is contained in the method onStartCommand() - unselect onDestroy and launch again the service “started”, the call to onDestroy doesn’t happen, this means that, even if the service has completed to display the time for 10 seconds, the service is not destroyed
- stop the service, you can do it in two ways:
- from shell: with the comand “adb shell kill [PID]” where [PID] is the PID associated to the service and you can read in the view LogCat in Eclipse
- dalla view Devices in Eclipse: selecting the process eu.lucazanini.service:started and stopping it with the icon “Stop Process”
- wait some seconds and verify that the service restarts
- select START_NOT_STICKY and restart the service
- stop the service and verify that the service doesn’t restart
This test should make clear the difference between START_STICKY and START_NOT_STICKY in the case in which the service is stopped:
- START_STICKY: the service restarts if it is stopped before the call to onDestroy
- START_NOT_STICKY: the service doesn’t restart if it is stopped before the call to onDestroy but it can restart with the code startService()
For more information about the differences between the two parameters you can read Service API changes starting with Android 2.0.
Now let’s test the app for the service “bound”:
- launch the app
- push the second button to launch the service “bound”
- see the output in the text view; the text box is updated every second for 10 seconds displaying the current time without locking the device
You can download the project here.
References:
Android Developers: Service
Android Developers: Services
Android Developers: Bound Services
Leave a Reply