Table of Contents
What is a service
A service is simply code which is running , and this code , does not have a GUI . The code runs , on the main thread of its application (hosted process) , unless the code creates , a runnable thread , or is run in a separate process .
The code can either show a notification indicating that it is running , in such a case , it is foreground code . A notification , must be shown when accessing , for example the microphone , or the camera .
The code can show no indication , that it is running , in such a case , it is background code , like for example , emptying the cache folder of an application .
The code can be interacted with , like for example to send a specific request , or to receive a given result . In such a case , the code is available , as long , as the code that it is interacting with , is available . This is bound code .
A code can be bound , and show a notification , or show no notification , or a code can be unbound , and show a notification , or show no notification .
A service , so the code that can be bound , or unbound , or showing , or not showing a notification , is created using the Service
class , and is started using an Intent
.
Service lifetime and lifecycle
The code of an instance of a class , has a life time , which is the period during which the instance exists . So a service also , has a lifetime .
A lifetime has a cycle , which is when the instance is first created , lastly destroyed , and what happens in between.
From android developers , the service lifecycle is as follows :
public class ServiceLifeCycle extends Service { @Override public void onCreate() { /* This method is only called once , when the service is being created , before onStartCommand or onBind are called .*/ } @Override public int onStartCommand(Intent intent, int flags, int startId) { /* When a call to the startService method is made , this method is called . It must return a lifetime flag , which control the lifetime of a service , started using startService .*/ return startMode; } @Override public IBinder onBind(Intent intent) { /* When a call to the bindService method is made , this method is called .*/ return binder; } @Override public boolean onUnbind(Intent intent) { /* All clients have unbound by calling the unbindService method .*/ return allowRebind; } @Override public void onRebind(Intent intent) { /* A client is binding to the service by calling the bindService method , after the onUnbind method has been called .*/ } @Override public void onDestroy() { /* This method is called , when the service is being destroyed . It should be used to clean any resources , for example threads .*/ } int startMode; /* A lifetime flag , which indicates how to behave if the service is killed .*/ IBinder binder; /* interface for clients that bind .*/ boolean allowRebind; /* indicates whether onRebind should be used .*/ }
So for example , when startService
is called , the code specified in onStartCommand
will get executed , and it can be used to show a notification , so the service will be a foreground service , or no notification , so the service will be a background service .
After that , a call to bindService
, can be made . The code specified in onBindService
, will get executed . It must return an interface , which allows communication with a code . The code is bound to the lifetime of the clients , hence this is known as a bound service .
Lifetime flags
An instance of a Service
, can have some flags set to it . These flags control the lifetime of the instance , of the Service
.
These flags are only applicable , to a service which is started by calling startService
. A service which has only the bindService()
method called , is destroyed , when all the bound clients , have unbound .
The START_STICKY
flag states , that when an instance of a Service
is destroyed , for example on low memory , another instance of this service , will be created , as such the onCreate
, and on onStartCommand
will be called . The executing code is restarted from scratch , and no state is preserved . The original Intent
, which started the first instance of the Service
, is not re-passed to the onStartCommand
, hence it is null .
The START_REDELIVER_INTENT
flag dictates , that when an instance of a Service
is destroyed , what happens , is that another instance of the Service
is created . The executing code , is restarted from scratch , the onCreate
, and onStartCommand
functions are executed , in this case , the onStartCommand
will receive the original Intent
, that started the earlier instance , of the service .
The START_NOT_STICKY
flag states , that when an instance of a Service
is destroyed , it is not recreated , so it must be manually recreated .
A service which is showing a notification , has the same priority as an application which is running in the foreground , so it is less likely that it will be killed on low memory , but the important thing to remember , is that this is not important .
Registering a service
A Service must be declared in the manifest of an application , by adding a service
element , as a child , to the application
element .
<?xml version = "1.0" encoding = "utf-8" ?> <manifest xmlns:android = "http://schemas.android.com/apk/res/android" package = "com.twiserandom.mobileapps.demo.coding_demo" > ... <uses-permission android:name = "android.permission.FOREGROUND_SERVICE" /> <application ... > <service android:name = "string" android:foregroundServiceType = "camera | connectedDevice | dataSync | location | mediaPlayback | mediaProjection | microphone | phoneCall" android:process="string" > . . . </service> ... </application> </manifest>
If the service is a foreground service , then uses-permission
foreground service , must be specified in the manifest .
Also if the service is a foreground service , then using foregroundServiceType
, it must state its type , which can be one or more , for example camera
, or camera | microphone
The only required attribute , is the name
attribute , which specifies the name of the class , that implements the service . The name of the class can either be fully qualified , by specifying the package to which this class belongs , or it can start with a dot .
, in this case , it belongs to the package specified in the manifest .
A service can run on a different process , then the default process , created for the application , by using the process
attribute . If the string name , in the process attribute , starts with a colon :
, then the service will run in a newly created process , private to its application . If the string name , starts with a lower case letter , than the service will run in a global process , having the indicated name .
Starting , and stopping a service
A service can be started by calling startService
, a service started by calling startService
, is called a started service .
Intent intent = new Intent (this , Service_ClassName.class ); startService (intent );
Data can be added to an intent , like to specify some options , to the Service .
Intent intent = new Intent (this , Service_ClassName.class ); intent .putExtra ("do" , "what" ); intent .putExtra ("what-arg" , 0.5 ); startService (intent );
The startService
method , can be called multiple times , as such the onStartCommand
, can also be called multiple times .
A service started using the startService
method , can be stopped using the stopService
method .
Intent intent = new Intent (this , Service_ClassName.class ); stopService (intent );
It can also stop itself , using : stopSelf (int startId )
. The startId
in stopSelf
, is the one received from the onStartCommand
. It is used to prevent the service from stopping itself , if new startService
calls , have been issued , but if the most recent startId
is used , then the service will stop itself , disregarding any previous startService
calls .
A started service can display a notification that it is running , and promote itself to a foreground service , by using the startForeground
method , which takes as parameters , a user defined notification id , and the actual notification , to display to the user .
startForeground (int id, Notification notification)
To demote a foreground service , back to being a background service , the stopForeground
method can be called . This does not stop the service from running . The stopForeground
method , can also be passed a boolean
true , to remove any displayed notifications . Now that the service is demoted , to being back a background service , it can be stopped as explained earlier , using stopSelf
, or stopService
.
A service can be started by calling the bindService
method , which has the following signature :
public abstract boolean bindService (Intent intent, ServiceConnection events, int options)
The intent is just a regular intent to start a service , as shown earlier .
events , is just an instance of an interface , which must respond to the different binding events , that occurs , for example , onBindingDied
, or onServiceConnected
, or onServiceDisconnected
.
options is just the options to control the binding process , for example the flag BIND_AUTO_CREATE
, will automatically create the service .
If the service class exists , and the client is allowed to bind to it , the bindService
method , returns true , otherwise it returns false .
When the binding is being performed , the onBind
method is called , and it must return an instance of the IBinder
interface . Binding is more related to interprocess communication , so to communicate between different process , but can be used in all cases . The binder class , implements the methods , that allow performing the interprocess communication , and we can extend it , to provide our own methods , as a service to a client .
A bound service , can be unbound , and stopped by using the unbindService
method .
A service which has only its bindService method called , is automatically destroyed , when all the bound clients unbind , so after the onUnbind
method is called .
If a service has been started using the startService
method , and also started using the bindService
method , then calling the stopService
or stopSelf
methods , will not destroy this service , until all bound clients have unbound , and vice versa , when all clients have unbound , but the stopSelf
or stopService
methods , have not been called , the service will not be destroyed .
Getting results from a service
A service started using the startService
method , can use for example a toast , or a notification , or a broadcast … to provide or show some results . It can also be made bound , in order to retrieve results when necessary .
Demo application
This application illustrates all the points , talked about earlier . It has three services , one is used to purge the content of the cache directory , the second is used to record audio , and the third is used to interact with some code , so just to calculate the addition of n
numbers , and to stop a started recording session .
The Service source code is as follows :
package com .twiserandom .mobileapps .demo .coding_demo; import android .app .Notification; import android .app .NotificationChannel; import android .app .NotificationManager; import android .app .Service; import android .content .Intent; import android .media .MediaRecorder; import android .os .Binder; import android .os .CountDownTimer; import android .os .IBinder; import android .util .Log; import android .widget .Toast; import androidx .localbroadcastmanager .content .LocalBroadcastManager; import java .io .IOException; import java .nio .file .DirectoryStream; import java .nio .file .Files; import java .nio .file .LinkOption; import java .nio .file .Path; import java .util .UUID; public class Service_Demo extends Service { final String fnlStr_debug_tag = "Service Demo Debug"; IBinder binder ; boolean allowRebind = false; boolean stop_recording = false; Thread thr_rmCache , thr_recordSound; @Override public void onCreate() { binder = new LocalBinder ( ); } @Override public int onStartCommand (Intent intent , int flags , int startId ){ if(intent == null ){ /* The service has been killed , and the startMode has been set set to START_STICKY . As such the service is now being restarted , with a null Intent , so the original intent is not delivered again . When START_STICKY is set , the intent itself is not primordial , for the service to run .*/ thr_rmCache = new Thread (new Purge_Cache (startId ) ) ; thr_rmCache .run(); return START_STICKY; } else { /*An intent has been delivered . Either , the service is starting fresh , or it has been killed , and its start mode has been set to START_REDELIVER_INTENT . So the original intent is being redelivered .*/ switch (intent .getStringExtra ("do" ) ){ case "Clear Cache": if (thr_rmCache == null ){ thr_rmCache = new Thread (new Purge_Cache (startId ) ) ; thr_rmCache .run(); return START_STICKY; } break; case "Record Sound" : if (thr_recordSound == null ){ thr_recordSound = new Thread (new Record_Sound ( ) ); thr_recordSound .run ( ); return START_REDELIVER_INTENT; } break; } } return START_NOT_STICKY; } @Override public IBinder onBind(Intent intent ){ /* extras are not delivered on binding , unbinding , or rebinding . The intent delivered , on binding , unbinding , rebiding , is the original intent delivered when binding .*/ return binder; } @Override public boolean onUnbind(Intent intent) { return allowRebind; } @Override public void onRebind(Intent intent) { } @Override public void onDestroy( ) { if (thr_rmCache != null && thr_rmCache .isAlive ( ) ) thr_rmCache .interrupt ( ); if (thr_recordSound != null ) stop_recording = true ; } /* A class to Purge the cache directory , * it will show a toast when starting , * the process , and when it is done * doing its job , it will show a * notification , and send a local * broadcast .*/ class Purge_Cache implements Runnable { int startId; public Purge_Cache (int starId ){ this .startId = starId; } @Override public void run ( ){ //show a toast on start Toast .makeText(Service_Demo .this , "Clearing the cache directory" , Toast .LENGTH_LONG ) .show ( ); //purge the cache purge_Cache (getCacheDir ( ) .toPath ( ) ); //show a notification after cash is purged cleared_Cache_Notification ( ); //Create a local broadcast Intent intent = new Intent ("Cache Purged" ); intent .putExtra ("msg" , "The cache is purged" ); LocalBroadcastManager .getInstance (Service_Demo .this .getApplicationContext ( ) ) .sendBroadcast (intent ); //done thr_rmCache = null; if (thr_recordSound == null ) stopSelf (startId ); } public void purge_Cache (Path pathD ){ //Do not remove the directory , only its content recursively Log .d (fnlStr_debug_tag , pathD .toString ( ) ); if (Thread .interrupted ( ) ){ return ;} try (DirectoryStream<Path > paths = Files .newDirectoryStream (pathD ) ){ for (Path td_path : paths ){ Log .d (fnlStr_debug_tag , td_path . toString ( ) ); if (Files .isDirectory (td_path , LinkOption .NOFOLLOW_LINKS ) ){ purge_Cache (td_path ); } Files .delete (td_path ); }} catch (IOException | SecurityException exception ){ Log .d (fnlStr_debug_tag , exception .toString ( ) );}} public void cleared_Cache_Notification ( ){ //Create notification channel String notification_channel_id = "coding_demo_cache_purged"; NotificationChannel channel = new NotificationChannel (notification_channel_id , "coding demo cache purged" , NotificationManager .IMPORTANCE_DEFAULT ); channel .setDescription ("Coding demo cache purged channel" ); NotificationManager notificationManager = getSystemService (NotificationManager .class ); notificationManager .createNotificationChannel (channel ); //create notification int notification_id = 1; Notification .Builder notification_builder = new Notification .Builder (Service_Demo .this , notification_channel_id ) .setSmallIcon (R .mipmap .ic_launcher ) .setContentTitle ("Code demo" ) .setContentText("The application cache is cleared" ); notificationManager .notify (notification_id , notification_builder .build ( ) ); } } /* Record sound class */ class Record_Sound implements Runnable{ CountDownTimer audioCapture_countDownTimer; MediaRecorder audioCapture_recorder; int audioCapture_duration = 25; @Override public void run ( ) { Toast .makeText(Service_Demo .this , "Starting to record Sound !" , Toast .LENGTH_SHORT ) .show ( ); start_Recording ( ); startForeground (2 , sound_Recording_Notification ( ) ); } public void start_Recording ( ) { try { //Start recording String audioCapture_fileName = getCacheDir( ) .getAbsoluteFile( ) + "/" + UUID .randomUUID( ) .toString( ) + ".m4a"; audioCapture_recorder = new MediaRecorder ( ); audioCapture_recorder .setAudioSource (MediaRecorder .AudioSource .MIC ) ; audioCapture_recorder .setOutputFormat (MediaRecorder .OutputFormat .MPEG_4 ); audioCapture_recorder .setAudioEncoder (MediaRecorder .AudioEncoder .AAC ); audioCapture_recorder .setAudioSamplingRate (44100 ); audioCapture_recorder .setAudioChannels (2 ); audioCapture_recorder .setAudioEncodingBitRate (32000 ); audioCapture_recorder .setOutputFile (audioCapture_fileName ); audioCapture_recorder .prepare ( ); audioCapture_recorder .start ( ); //Cancel recording after audioCapture_duration , or on interrupt audioCapture_countDownTimer = new CountDownTimer (audioCapture_duration * 1000 , 1000 ){ public void onTick(long millisUntilFinished ){ if(stop_recording ){ audioCapture_countDownTimer .cancel ( ) ; stop_Recording ( ); }} public void onFinish ( ){ Toast .makeText(Service_Demo .this , "Finished recording audio" , Toast .LENGTH_SHORT ) .show(); stop_Recording ( ); }}; audioCapture_countDownTimer .start( ); } catch (Exception exception ){ Log .d (fnlStr_debug_tag , exception .toString ( ) ); Toast .makeText(Service_Demo .this , "Failed to record sound " , Toast .LENGTH_SHORT ); }} public void stop_Recording ( ){ audioCapture_recorder .stop( ); audioCapture_recorder .release( ); thr_recordSound = null ; stop_recording = false ; stopForeground (true ); } public Notification sound_Recording_Notification ( ){ // Create notification channel String notification_channel_id = "coding_demo_audio_capture"; NotificationChannel channel = new NotificationChannel ( notification_channel_id , "coding demo audio capture" , NotificationManager .IMPORTANCE_DEFAULT ); channel .setDescription("Coding demo audio capture channel" ); NotificationManager notificationManager = getSystemService (NotificationManager .class ); notificationManager .createNotificationChannel (channel ); // Create the notification Notification .Builder notification_builder = new Notification .Builder (Service_Demo .this , notification_channel_id ) .setSmallIcon (R .mipmap .ic_launcher_round ) .setContentTitle ("Code demo" ) .setContentText ("Sound is being recorded" ); return notification_builder .build ( ); }} class LocalBinder extends Binder{ public int add (int ... vars_i ){ int sum = 0; for (int var_i : vars_i ){ sum += var_i ; } return sum ; } public void stopRecordSound (){ if(thr_recordSound != null ){ stop_recording = true ; Log .d (fnlStr_debug_tag , "Audio recording is stopped " ); }}}}
The main activity source code is as follows :
package com .twiserandom .mobileapps .demo .coding_demo; import androidx .annotation .NonNull; import androidx .appcompat .app .AppCompatActivity; import androidx .localbroadcastmanager .content .LocalBroadcastManager; import android .Manifest; import android .content .BroadcastReceiver; import android .content .ComponentName; import android .content .Context; import android .content .Intent; import android .content .IntentFilter; import android .content .ServiceConnection; import android .content .pm .PackageManager; import android .os .Bundle; import android .os .IBinder; import android .util .Log; import android .view .View; import android .view .ViewGroup; import android .widget .Button; import android .widget .LinearLayout; import android .widget .Toast; public class Activity_Main extends AppCompatActivity { final String fnlStr_debug_tag = "Activity Main Debug"; final int fnlInt_request_audio_permissions = 1013; final Button_Model arr_btnModels [ ] = { new Button_Model (Button_Model .fnlStr_code_notification ) , new Button_Model (Button_Model .fnlStr_code_noNotification ) , new Button_Model (Button_Model .fnlStr_codeBound_notification_noNotification ) } ; @Override protected void onCreate (Bundle savedInstanceState ){ super .onCreate (savedInstanceState ); create_Interface ( ); register_Broadcast_Receiver ( ); } public void create_Interface( ){ LinearLayout ll = new LinearLayout (this ); ll .setOrientation (LinearLayout .VERTICAL); Click_Listener_Buttons click_listener_buttons = new Click_Listener_Buttons ( ); Button btn ; for (Button_Model btnModel : arr_btnModels ){ btn = new Button (this ) ; btn .setText (btnModel .btn_text ); btn .setTag (btnModel .btn_text ); btn .setOnClickListener (click_listener_buttons ); ll .addView (btn , ViewGroup .LayoutParams .MATCH_PARENT , ViewGroup .LayoutParams .WRAP_CONTENT ); } setContentView(ll ); } public void register_Broadcast_Receiver ( ){ CachePurged_Receiver cachePurged_receiver = new CachePurged_Receiver ( ); IntentFilter intentFilter = new IntentFilter ("Cache Purged" ); LocalBroadcastManager .getInstance (getApplicationContext ( ) ) .registerReceiver (cachePurged_receiver , intentFilter ); } /*Cache Purged Receiver Class */ class CachePurged_Receiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent ){ Log .d (fnlStr_debug_tag , "Cache Purged Broadcast Received" ); Toast .makeText (context , intent .getStringExtra ("msg" ) , Toast .LENGTH_LONG ) .show ( ); }} /* Buttons Click Listeners class */ class Click_Listener_Buttons implements View .OnClickListener{ Service_Demo .LocalBinder localBinder; Service_Connection service_connection; @Override public void onClick(View view ){ Intent intent = new Intent (Activity_Main .this , Service_Demo .class ); ; switch ((String ) view .getTag( ) ){ case Button_Model .fnlStr_code_noNotification: Log .d (fnlStr_debug_tag , "running some code without a notification" ); //start service purge cache intent .putExtra ("do" , "Clear Cache" ); startService (intent ); break; case Button_Model .fnlStr_code_notification: Log .d (fnlStr_debug_tag , "running some code showing a notification" ); //check for permissions if (checkSelfPermission (Manifest .permission .RECORD_AUDIO ) == PackageManager .PERMISSION_GRANTED ){ // granted start audio recording service intent .putExtra ("do" , "Record Sound" ); startService (intent ); } else{ // not granted , request the permissions requestPermissions (new String [ ]{Manifest .permission .RECORD_AUDIO } , fnlInt_request_audio_permissions );} break; case Button_Model .fnlStr_codeBound_notification_noNotification: /* Click the third button , which is this switch case , only after starting an audio recording service , since it will automatically stop it , If you click it a second time , it is not necessary , to start recording , it will just unding the service . */ Log .d (fnlStr_debug_tag , "Bound code interacting with code showing and not showing a notification" ); if (service_connection != null ){ unbindService (service_connection ); service_connection = null; } else{ service_connection = new Service_Connection ( ); bindService(intent , service_connection , Context.BIND_AUTO_CREATE); } break; } } class Service_Connection implements ServiceConnection{ @Override public void onServiceConnected (ComponentName name , IBinder service){ // Called when binding has been established . localBinder = (Service_Demo .LocalBinder ) service; Log .d (fnlStr_debug_tag , "" + localBinder .add (1 , 2 , 3 , 4 ) ); // call the add method in the bound service . localBinder .stopRecordSound(); /* Stop sound recording in the bound service */ } @Override public void onServiceDisconnected (ComponentName name ){ //Called when binding is lost . localBinder = null; } @Override public void onBindingDied (ComponentName name ){ // Called when binding is dead. localBinder = null; } @Override public void onNullBinding (ComponentName name ){ // Called when onBind returns null localBinder = null; } }} @Override public void onRequestPermissionsResult (int requestCode , @NonNull String [ ] permissions , @NonNull int [ ] grantResults ){ super .onRequestPermissionsResult (requestCode , permissions , grantResults ); switch (requestCode ){ case fnlInt_request_audio_permissions : if(grantResults .length > 0 && grantResults [0 ] == PackageManager .PERMISSION_GRANTED ){ //audio record permission has been granted Intent intent = new Intent(Activity_Main .this , Service_Demo .class); intent .putExtra ("do" , "Record Sound" ); startService (intent ); } else{ Log .d (fnlStr_debug_tag , "Permission to record sound not granted" ); }}} } class Button_Model{ final static String fnlStr_code_noNotification = "Code no notification Service"; final static String fnlStr_code_notification = "Code notification Service"; final static String fnlStr_codeBound_notification_noNotification = "Code bound notification and no notification Service"; String btn_text; public Button_Model (String btn_text ){ this.btn_text = btn_text; } }
And the manifest source code is as follows :
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.twiserandom.mobileapps.demo.coding_demo"> <uses-permission android:name = "android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".Activity_Main"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <service android:name = ".Service_Demo" android:foregroundServiceType = "microphone"> </service> </application> </manifest>
IntentService
An IntentService , is a service which is an instance , of the IntentService
class . The IntentService
class is a subclass of the Service
class .
The IntentService
class , uses a single worker thread , when a call to startService
is made , as not to implement one’s own thread . This thread is different from the application main thread .
Calls made to startService
, are processed sequentially , on this newly created thread , one after another , and the IntentService
automatically stops itself , when no more work is to be done .
IntentService
is deprecated , and JobIntentService
must be used instead .