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(); } }