Tutoriel pour créer un service web pour envoyer des SMS sous Android

Alain Mouflet

Ce tutoriel présente comment héberger un petit serveur de SMS dans un smartphone avec OS android.

Des précisions sur ce support de cours, un espace de dialogue vous est proposé sur le forum : Commentez .

- Titre : Tutoriel pour créer un service web pour envoyer des SMS sous Android

- Auteur : Alain Mouflet

- Parution : 1 mai 2017

- Mise à jour : 15 mai 2017

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2017 Alain Mouflet. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Objectif

- II. Implémentation du service web

- II-A. Bibliothèques

- II-B. Les permissions

- II-C. L'IHM

- II-D. Le programme

- II-D-1. La classe MainActivity

- II-D-2. La classe serveur héritière de NanoHTTPD

- II-D-3. La classe parser

- III. Test

- IV. Conclusion

- V. Remerciements

I - Objectif

Cet article montre comment mettre en place un serveur de SMS local avec un budget très réduit (un vieux téléphone Android et une carte SIM à 2 €).

L'objectif de cette présentation est de développer un service web permettant l'envoi de SMS.

Dans le cas d'une supervision de réseau, l'administrateur est prévenu par email d'un défaut, mais lorsqu'il s'agit d'un problème demandant une intervention immédiate, il faut recourir à un fournisseur qui relaiera par SMS le défaut.

On peut retrouver ce besoin dans le monde des objets où une minicarte à base d'ESP8266 demande à prévenir d'un événement dans une smart house, un élevage agricole…

L'interface graphique renseigne du socket d'écoute et donne le contenu du dernier SMS envoyé ainsi que le numéro cible.

L'activity : la classe Android gérant l'application est nommée MainActivity et, dans notre cas, gère le serveur MyHTTPD. Le serveur minimaliste MyHTTPD hérite de NanoHttpd. Cette dernière classe s'appuie sur le parser JSONDecodeur qui est là pour extraire du POST les champs contenant les informations du SMS à envoyer.

Ainsi n'importe quel client REST/JSON connecté au réseau local pourra envoyer un SMS via cette passerelle.

Côté smartphone, un simple appareil vieillissant avec une carte SIM à forfait minimaliste fera l'affaire.

Les fichiers source sont présentés ici par fonction. Une archive est également disponible ici.

II - Implémentation du service web

La présentation utilise Android Studio.

Une introduction à Android Studio est disponible ici (tuto android). Pour notre application, un contexte Empty Activity est suffisant avec une version d'Android de cible Android correspondant à votre téléphone (cf. version api android). Aucun besoin d'avoir le dernier cri !

II-A - Bibliothèques

Notre application aura besoin des bibliothèques de nanohttpd (serveur léger HTTP) et de json-simple (bibliothèque d'encodage et de décodage de JSON). Afin de les ajouter à Android Studio, il faut ajouter ces deux lignes dans la rubrique dependencies du fichier build.gradle de l'app :

Ajout bibliothèque au gradle

compile group: 'org.nanohttpd', name: 'nanohttpd-webserver', version: '2.3.1'
compile group: 'com.googlecode.json-simple', name: 'json-simple', version: '1.1.1'

II-B - Les permissions

Pour avoir les permissions d'accéder au Wi-Fi, Internet (réseau) et l'envoi de SMS, il faut ajouter dans le manifest :

Autorisations dans le manifest

<uses-permission android:name="android.permission.ACCESS_Wi-Fi_STATE"/>
<uses-permission android:name="android.permission.SEND_SMS"/>
<uses-permission android:name="android.permission.INTERNET"/>

II-C - L'IHM

L'UI est décrite par six widgets à insérer dans un LinearLayout.

Le socket est affiché ainsi que le numéro et le contenu du dernier SMS envoyé.

L'interface graphique est spartiate, mais cette application ne nécessite pas plus.

II-D - Le programme

II-D-1 - La classe MainActivity

La classe principale remplit comme habituellement les rôles de liaison avec les widgets graphiques ainsi que la gestion du cycle de vie de l'activity.

Les objets liés aux widgets sont déclarés en portée privée pour l'ensemble de la classe. Ces variables permettent ensuite d'accéder et de mettre à jour l'écran depuis l'activity ou une autre classe à partir d'un Handler.

MainActivity.java

public class MainActivity extends Activity {
private TextView numTexto;
private TextView txtTexto;
private TextView edPort;
private MyHTTPD server;
private TextView textIpaddr;
private static final int PERMISSION_REQUEST_CODE = 1;

La méthode onCreate connecte les widgets aux objets du programme afin de pouvoir interagir avec. On y demande également la permission spécifique pour l'envoi de SMS ; obligatoire selon l'API, car de niveau « dangerous ».

MainActivity.java

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textIpaddr = (TextView) findViewById(R.id.ipaddr);
numTexto = (TextView) findViewById(R.id.numSMS);
txtTexto = (TextView) findViewById(R.id.txtSMS);
edPort = (EditText) findViewById(R.id.port);
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.SEND_SMS}, 1);
}

Il est préférable de lancer le serveur ailleurs que dans cette méthode, car celle-ci n'est parcourue que lorsque l'application est lancée, pas quand elle repasse au premier plan.

La méthode onResume est appelée quand l'application s'ouvre ou quand elle revient au premier plan. Cette méthode dialogue avec le Wi-FiManager et diffuse un toast si l'accès Wi-Fi n'est pas activé ou si l'IP du téléphone n'est pas valable (=0). Cette méthode affiche le socket d'écoute pour tous les clients. Enfin, elle ouvre un serveur de type NanoHTTPD et le lance dans la foulée.

La classe Toast permet de faire apparaître temporairement un message d'information : ici que le Wi-Fi est désactivé.

MainActivity.java

@Override
protected void onResume() { // lance apres onCreate
super.onResume();
Wi-FiManager Wi-FiManager = (Wi-FiManager) getApplicationContext().getSystemService(Wi-Fi_SERVICE);
if (!Wi-FiManager.isWi-FiEnabled()) { // test si Wi-Fi actif
Toast.makeText(this, "Wi-Fi désactivé", Toast.LENGTH_LONG).show();
}
int ipAddress = Wi-FiManager.getConnectionInfo().getIpAddress();
if (ipAddress == 0) {  // test si @IP affectee
Toast.makeText(this, "Pas d'adresse IP", Toast.LENGTH_LONG).show();
}
// formate l'@ en norme IPV4 en faisant confiance a l'utilisateur sur les données saisies
final String formatedIpAddress = String.format("%d.%d.%d.%d", (ipAddress & 0xff),
(ipAddress >> 8 & 0xff), (ipAddress >> 16 & 0xff), (ipAddress >> 24 & 0xff));
int port = Integer.parseInt(edPort.getText().toString()); // on recupere le port choisi par l'utilisateur
// affiche le socket d'ecoute
textIpaddr.setText("Accès : http://" + formatedIpAddress + ":" + port);
try {
if (server == null) {
server = new MyHTTPD(port, numTexto, txtTexto); // cree le serveur NanoHTTPD si inexistant
}
server.start();             // lance ou relance le serveur
} catch (IOException e) {
e.printStackTrace();
}
}

La méthode onPause désactive le serveur et libère le port dès que l'application n'est plus active. Il suffira de remettre l'application au premier plan pour repasser par la méthode onResume.

MainActivity.java

@Override
protected void onPause() {
super.onPause();
if (server != null) {
server.stop();   // serveur non operationnel quand l'appli est en pause
}
}

II-D-2 - La classe serveur héritière de NanoHTTPD

Cette classe écoutera et répondra aux clients postant un message JSON. Elle se comportera comme un serveur REST qui une fois l'échange terminé répondra par un code de fin de connexion.

On y trouve un Handler, nécessaire pour accéder aux widgets de l'UI, et deux objets à connecter à l'UI.

Le constructeur MyHTTPD récupère le port passé en argument et appelle le constructeur de la super-classe. Le Handler et les deux objets des widgets sont initialisés.

MainActivity.java

class MyHTTPD extends NanoHTTPD {
private Handler handler; // necessaire pour actualiser l'UI
private TextView numWidget;
private TextView txtWidget;
final int MAXCHAR = 200;  // taille maximale du JSON recu
/**
* Constructeur qui initialise les variables
* et cree un handler pour se connecter a l'UI
*/
public MyHTTPD(int port, TextView numWid, TextView textWid) throws IOException {
super(port);
this.numWidget = numWid;
this.txtWidget = textWid;
handler = new Handler();
}

La méthode centrale de cette classe est serve, elle retourne la réponse à une connexion.

On y déclare un objet de la classe du décodeur/parser ainsi qu'un tableau d'octets.

Cette méthode est automatiquement appelée par la classe mère dès qu'un client s'est connecté. On va extraire le tableau d'octets envoyé par le client et contenu dans l'argument : session. Ce tableau représentant une chaîne de caractères au format Json est tout de suite converti en String.

Cette chaîne est décodée par un objet de la classe JSONdecodeur présenté ensuite.

Si la trame JSON est valide (isParsed()), les valeurs du numéro de téléphone cible et le texte du SMS sont alors envoyés à l'UI et par texto à l'intérieur du Handler précédemment créé.

Enfin, on renvoie systématiquement le code 200 de bonne fin au client.

Il n'y a pas de retour dans cette méthode d'une mauvaise trame venant du client. Les messages JSON étant postés par des clients informatiques, il y a peu de chances, une fois débogué, d'avoir à traiter une demande sans numéro de téléphone par exemple.

MainActivity.java

@Override
public Response serve(IHTTPSession session) {
final JSONdecodeur jsonDec;
byte[] messageDuClient = new byte[MAXCHAR];
int tailleMessageClient = 0;
try {  // conversion d'un tableau en String
tailleMessageClient = session.getInputStream().read(messageDuClient,0,MAXCHAR);
String jsonMsg = new String(messageDuClient);
jsonMsg = jsonMsg.substring(0, tailleMessageClient);
jsonDec = new JSONdecodeur(jsonMsg);
handler.post(new Runnable() {
@Override
public void run() {   // affiche le numero et le texte du SMS envoye
if (jsonDec.isParsed()) {  // seulement si JSON valide
numWidget.setText("Numéro : " + jsonDec.getNumTel()); // met a jour l'UI
txtWidget.setText("Message : " + jsonDec.getTxtSMS());
SmsManager smsManager = SmsManager.getDefault();  // envoi du SMS
smsManager.sendTextMessage(jsonDec.getNumTel(), null, jsonDec.getTxtSMS(),
null, null);
}
else {
numWidget.setText("Format du JSON"); // met a jour l'UI
txtWidget.setText("Revoir la syntaxe");
}
}
});
} catch (IOException e) {
e.printStackTrace();
}
// renvoie code de fin au client
return newFixedLengthResponse(java.net.HttpURLConnection.HTTP_OK + "\n");
}

II-D-3 - La classe parser

Cette dernière classe a pour rôle de trouver dans la trame reçue la valeur des champs number et text.

La méthode parser utilise comme argument la chaîne JSON reçue. Dans un premier temps, elle extrait la valeur du champ SMS qui est lui-même un objet JSON. Cet objet est ensuite analysé et on en retire deux champs : number et text.

La méthode getNumTel retourne le numéro de téléphone une fois extrait par parser.

La méthode getTextSMS retourne le texte du message une fois extrait par parser.

Si les champs SMS, number et text sont présents, la méthode isParsed() retourne true.

MainActivity.java

class JSONdecodeur {
private String numTel, txtSMS;
private boolean isParsed = false;
/**
* Methode d'analyse d'une String au format JSON
* et qui extrait les valeurs des champs "number" et "text"
*
* @param jsonString
*/
JSONdecodeur(String jsonString) {
JSONParser jsonParser = new JSONParser();
try {
JSONObject jsonObject = (JSONObject) jsonParser.parse(jsonString);
// retourne un JSONObject qui est la valeur du champ SMS
JSONObject jsonSMS = (JSONObject) jsonObject.get("SMS");
if (jsonSMS != null) {
numTel = (String) jsonSMS.get("number");  // retourne la valeur du champ number
txtSMS = (String) jsonSMS.get("text");    // retourne la valeur du champ text
if (numTel != null && txtSMS != null) {
isParsed = true;
}
}
} catch (ParseException e) {
e.printStackTrace();
}
}
/**
* Retourne le numero du SMS à envoyer, extrait du code JSON
*
* @return String numero du SMS
*/
String getNumTel() {return numTel;}
/**
* Retourne le texte du SMS à envoyer, extrait du code JSON
*
* @return String message du SMS
*/
String getTxtSMS() {return txtSMS;}
/**
* Retourne true si le traitement est correct et exploitable
*
* @return Boolean
*/
Boolean isParsed() {return isParsed;}
}

III - Test

Il suffit pour tester le serveur d'utiliser une extension de navigateur telle que Client REST simple disponible dans Chrome, d'indiquer dans l'URL l'adresse IP du smartphone donnée sur l'écran au format : http://IP:PORT.

De choisir la méthode POST et d'écrire un corps sur ce format :

Corps du POST

{"SMS": {
"number": "06XXXXXXXX",
"text": "mon message SMS"
}}

Ne pas oublier de spécifier http:// devant l'adresse ip de l'URL.

IV - Conclusion

Les premiers forfaits de fournisseurs de SMS sont à plus de 5 € par mois, il est donc intéressant de penser à cette solution.

Une connexion sécurisée implémentée permettra de rendre accessible ce serveur derrière une box et de transmettre des textos depuis n'importe quel navigateur ou application « maison ».

Cette application est simple et peut être améliorée en traitant l'arrivée des SMS afin de demander des actions aux clients du réseau local.

On peut aussi lancer un service et non gérer le serveur dans l'activity, cette manière permettra de toujours garder le service à l'écoute et de s'affranchir de la mise en veille du téléphone.

V - Remerciements

Merci à Mickael pour la supervision de l'article et son implication à developpez.com, à fearyourself pour ses remarques pertinentes et à Claude Leloup pour les fautes qui nous échappent toujours.