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 .


