Introduction
-
Service = composante d'application exécutant du code sans exposition graphique
- Opérations en arrière plan au long cours
- Exposition d'une API d'une application à d'autres applications
-
Implantation par dérivation de classe abstraite Service avec redéfinition possible de :
- IBinder onBind() ; peut être redéfini en retournant null (ou lever une exception) si l'on ne souhaite pas offrir d'interface
- void onCreate() : événement de création du service
- int onStartCommand(Intent intent, int flags, int startID)
- void onDestroy()
- Par défaut, un service partage le processus et la thread principale de son application hôte
- L'exécution des méthodes de cycle de vie onX() doit être brève (comme celles des activités)
- Les travaux de calcul doivent être réalisés par de nouvelles threads
- Un processus contenant un service et aucune activité en avant-plan peut être tué par le système en cas de pénurie de mémoire
-
Deux modes d'utilisation de service :
- Soumission de travaux en asynchrone par Intent
- Évocation de méthodes distantes après connexion au service
- Un service en arrière-plan peut être détruit pour économiser la batterie
Soumission de travaux à un service
- On envoie un Intent avec Context.startService(Intent i) ; le service est créé si inexistant
-
L'Intent est récupérable par le service avec onStartCommand(Intent intent, int flags, int startID) :
- flags indique 0, START_FLAG_REDELIVERY ou START_FLAG_RETRY
- onStartCommand peut retourner 0, START_STICKY, START_NOT_STICKY ou START_REDELIVER_INTENT (comportement pour le redémarrage si le service est stoppé par le système)
- Pas de retour d'Intent (communication unidirectionnelle)
-
Le service peut être stoppé :
- par lui-même avec stopSelf()
- par le composant l'ayant lancé avec stopService(Intent i)
-
La classe dérivée IntentService est utile pour implanter un service avec une thread de travail traitant les Intent
- Il faut redéfinir onHandleIntent(Intent i)
- Le service sera tué automatiquement si son exécution est trop longue (> 10 secondes)...
- ... sauf s'il est passé en avant-plan avec startForeground(int id, Notification notification)
Retour graphique d'un service
- Normalement un service ne peut afficher une vue graphique directement
-
Solutions néanmoins possible pour retour graphique :
- Usage d'un Toast (Toast.makeToast("texte", duration).show())
- Utilisation d'une Notification (message apparaissant dans la tiroir escamotable de notification)
- Récupération de données par une activité et affichage
Exemple : un service toaster
- Réalisation d'un service affichant un toast lorsqu'il reçoit une commande
- Ne pas oublier de déclarer le service dans le manifeste (si ce n'est pas fait automatiquement par Android Studio)
Ecriture du service :
public class ToastService extends Service {
/** An action that has an unique name */
public static final String TOAST_ACTION = ToastService.class.getName() + ".displayToast";
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent.getAction().equals(TOAST_ACTION)) {
String id = intent.getStringExtra("id");
String message = intent.getStringExtra("message");
boolean longDuration = intent.getBooleanExtra("longDuration", false);
Toast t = Toast.makeText(this, message, longDuration ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT);
t.show();
}
return START_NOT_STICKY; // if the service is killed, the intent will not be redelivered
}
@Override
public IBinder onBind(Intent intent) {
throw new UnsupportedOperationException("Not implemented (since we do not use RPC methods)");
}
}
Ecriture de l'activité envoyant des tâches :
/** An activity submitting messages to a service displaying them as toasts */
class ToastServiceActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_toast_service)
sendButton.setOnClickListener {
val intent = Intent(this, ToastService::class.java)
intent.action = ToastService.TOAST_ACTION
intent.putExtra("message", messageView.text.toString())
intent.putExtra("longDuration", longDurationView.isChecked)
startService(intent)
}
}
}
Notifications
Utilisation
-
Zone centralisée d'affichage d'informations (tiroir de notifications) pour l'utilisateur sous trois formats :
- petite icône dans la barre de notification
- vue normale dans le tiroir ouvert
- vue étendue dans le tiroir ouvert
- notification plein écran lançant une activité (avec setFullScreenIntent() et la permission USE_FULL_SCREEN_INTENT) ; le système peut refuser ce mode selon le contexte car trop dérangeant pour l'utilisateur
-
Ajouter ou mettre à jour une notification :
- Créer un canal de notification (NotificationChannel) : obligatoire si l'API cible ≥ 26
- Créer un objet Notification avec NotificationCompat.Builder.build()
-
Soumission de la notification au NotificationManager avec NotificationManager.notify(String tag, int id, Notification notification) : (tag, id) est un identificateur unique de notification dans l'application
- On obtient une instance de NotificationManager avec Context.getSystemService(Context.NOTIFICATION_SERVICE)
-
Supprimer une notification :
- NotificationManager.cancel(String tag, int id)

Services en avant-plan
-
Possibilité de mettre en avant-plan un service :
- apparition dans la zone de notification
- susceptibilité faible d'être tué
-
Service.startForeground(int id, Notification notification)
- L'identificateur id ne doit pas être nul
- La notification peut être retardée (10 secondes) depuis Android 12 (sauf cas particuliers)
-
Depuis Android 12, l'utilisation de startForeground est restreinte à certains cas seulement pour économiser l'énergie :
- Si l'utilisateur interagit avec une interface graphique (activité, widget, notification...) liée à l'application
- Après réception d'un message Firebase de haute priorité
- Après réception de certains broadcasts : ACTION_BOOT_COMPLETED, ACTION_LOCKED_BOOT_COMPLETED, ACTION_MY_PACKAGE_REPLACED...
- Si l'utilisateur désactive l'optimisatiom batterie pour l'application
- ...
- Exemples d'utilisation : tout service devant fonctionner en continu (comme enregistreur de traces GPS, lecteur de musique, ...)
Exemple : chronomètre notificateur
Exemple de service proposant un chronomètre avec une notification du temps écoulé (le mode highPriority affiche la notification en heads-up sans nécessité de dérouler le tiroir de notification).
public class ChronoService extends Service
{
public static final String ACTION_START =
NotifiedChronometer.class.getPackage().getName() + ".startChronometer";
public static final String ACTION_STOP =
NotifiedChronometer.class.getPackage().getName() + ".startChronometer";
public static final String ACTION_RESET =
NotifiedChronometer.class.getPackage().getName() + ".resetChronometer";
// We don't use the RPC capability
@Override public IBinder onBind(Intent intent) { return null; }
private ChronoService instance = null;
private long cumulatedTime = 0;
private long startTime = -1;
private static boolean running = false;
private Thread updateThread = null;
/** Return if the chronometer is running */
public static boolean isRunning()
{
return running;
}
/** Identifier of the channel used for notification (required since API 26) */
public static final String CHANNEL_ID =
ChronoService.class.getName() + ".CHRONO_CHANNEL";
/** This method creates a new notification channel (required for API 26+)
* It is copied from https://developer.android.com/training/notify-user/build-notification
*/
private void createNotificationChannel()
{
// Create the NotificationChannel, but only on API 26+ because
// the NotificationChannel class is new and not in the support library
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
CharSequence name = "Chronometer channel";
String description = "Channel for notifications of the chronometer service";
int importance = NotificationManager.IMPORTANCE_DEFAULT;
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance);
channel.setDescription(description);
// Register the channel with the system; you can't change the importance
// or other notification behaviors after this
NotificationManager notificationManager = getSystemService(NotificationManager.class);
notificationManager.createNotificationChannel(channel);
}
}
private Notification createNotification(String text, boolean highPriority)
{
NotificationCompat.Builder mBuilder =
new NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(android.R.drawable.ic_media_play)
.setContentTitle("Chronometer")
.setContentText(text)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setSilent(true);
// if high priority is set, the notification is displayed in a heads-up fashion
if (highPriority) {
mBuilder.setPriority(NotificationCompat.PRIORITY_HIGH);
mBuilder.setDefaults(Notification.DEFAULT_VIBRATE);
}
// Associate an action to the notification to start a linked Activity
Intent resultIntent = new Intent(this, NotifiedChronometer.class)
.putExtra("running", startTime >= 0);
// do not start the activity again if it is already on top
resultIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
mBuilder.setContentIntent(PendingIntent.getActivity(this, 0, resultIntent, 0));
return mBuilder.build();
}
@Override
public void onCreate()
{
super.onCreate();
instance = this; // set the singleton instance
createNotificationChannel();
}
/** Arbitrary ID for the notification (with different IDs a service can manage several notifications) */
public static final int NOTIFICATION_ID = 1;
@Override
public int onStartCommand(Intent intent, int flags, int startId)
{
final NotificationManager nm = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
if (intent == null) return Service.START_STICKY_COMPATIBILITY;
if (intent.getAction().equals(ACTION_START) && startTime == -1)
{
Log.i(getClass().getName(), "Action started intercepted");
final boolean highPriority = intent.getBooleanExtra("highPriority", false);
startTime = System.nanoTime();
// Put in the foreground
running = true;
startForeground(NOTIFICATION_ID, createNotification("Running chrono", highPriority));
// we could post update runnables on an handler instead of using a thread
updateThread = new Thread(() -> {
while (! Thread.interrupted())
{
long time = (cumulatedTime + System.nanoTime() - startTime) / 1000000000;
nm.notify(NOTIFICATION_ID, createNotification("Running: " + time + " s", highPriority));
System.err.println("Notify " + time);
try {
Thread.sleep(1000);
} catch (InterruptedException e)
{
return; // the thread was interrupted (if the service is destroyed for example)
}
}
});
updateThread.start();
}
else if (intent.getAction().equals(ACTION_STOP) && startTime >= 0)
{
cumulatedTime += System.nanoTime() - startTime;
stopForeground(true);
running = false;
// remove the notification and stop the thread
nm.cancel(NOTIFICATION_ID);
updateThread.interrupt();
startTime = -1;
// stopSelf();
}
else if (intent.getAction().equals(ACTION_RESET))
{
if (startTime >= 0) startTime = System.nanoTime();
cumulatedTime = 0;
}
return START_NOT_STICKY; // do not restart automatically the service if it is killed
// however with startForeground, probability of service killing is weak
}
@Override
public void onDestroy()
{
// do not forget to stop the thread if the service is destroyed
if (updateThread != null) updateThread.interrupt();
}
}