ex0ns

about
A Computer Science Student's blog, about programming, algorithms, hack and a lot more.

[Android] Liste noire SMS

15 Jun 2014

Après avoir retrouvé au fin fond d'un dossier une vielle application Android, j'ai décidé d'écrire un article à son sujet. A l'heure ou cet article est publié Android KitKat est déjà disponible et toutes les techniques présentées ici ne fonctionnent plus sous cette version (une seule et unique application doit être choisie pour recevoir des SMS). Je n'ai pas encore trouvé d'alternative pour faire fonctionner ces fonctions sur cette version, mais je suis certain que ceci est possible. Pour en revenir à l'application, son but (simple) était de pouvoir bloquer la réception de messages (SMS) en fonction du numéro, ou bien d'un inficatif de pays.

Mise en place de l'application

D'un point de vue des permissions, cette application nécessite android.permission.RECEIVE_SMS , afin de pouvoir récuperer les SMS entrants. Il faut également ajouter un receiver (BroadcastReceiver à notre manifest, afin de définir quelle classe à le droit de procéder à la récupération du message.

Le receveur

Cette classe est définie dans com.example.smslistener.SmsReceived, elle hérite de BroadcastReceiver. La méthode : public void onReceive(Context context, Intent arg1), est appelée lors de l'émission d'un signal par le système (ou une application), correspondant au signal declaré dans le manifest (SMSRECEIVED_). Toutes les informations contenues dans le signal se trouvent dans les "extras" de notre Intent (cf documentation android), et en particulier la clé pdus (protocol data unit) contenant toutes les informations relatives au message.

La méthode createFromPdu de la classe android.telephony.SmsMessage permet de créer un objet du type SmsMessage à partir de ces informations, ce dernier contient, entre-autre le numéro de l'expéditeur : getOriginatingAddress. Une fois ce numéro en notre possession, nous avons achevé la plus grosse partie du travail, il ne nous reste plus qu'a vérifier qu'il n'est lié ni à un pays bloqué ni d'un numéro bloqué. Afin de déterminer le pays d'origine, la méthode la plus rapide est de comparer le MCC (Mobile Country Code), il n'y a pas de méthode fournie par android pour effectuer cette comparaison, cependant, nous savons que leur taille est comprise entre 1 et 4 chiffres. Ainsi, il nous suffit d'essayer toutes les longueurs possibles de code dans l'ordre décroissant et de regarder s'ils existent, c'est ce que fait la méthode String getPrefix(String number):

private String getPrefix(String number){
    for(int i=4; i > 1; i--){
        String prefix = number.substring(1, i);
        if(this.db.findByPrefix(prefix)){
            return prefix;
        }
    }
    return null;
}

Dans notre exemple, nous utilisons une base de donnée afin de stocker tous les indicatifs des pays, ceci sera detaillé par la suite, mais l'important est de comprendre la détéction d'un MCC. Il faut maintenant comparer le numéro de téléphone ceux bloqués, contenus en base de donnée, dans le cas ou il s'y trouve, on arrête le broadcast à l'aide de la fonction abortBroadcast, ainsi, aucun autre application ayant un récepteur du type SMSRECEIVED_ ne l'obtiendra (sous hypothèse d'avoir la prioritée la plus grande possible sur cet évenement).

La base de donnée

Le modèle

Je voulais écrire cet article en grande partie afin de présenter le fonctionnement d'une base de donnée sous Android, qui, même si bien illustré dans la documentation, manque un peu d'exemples pratiques. Tout d'abord, il nous faut un modèle (un objet qui modélisé par les différentes colonnes de notre base de donnée), ici, le modèle est Country, modélisant un pays, par son nom, son MCC (indicatif téléphonique) ainsi que par un champs "blocked", indiquant si le messages provenant de ce pays doivent ou non, être bloqués.

Création de la table

Afin de créer notre table modélisant nos pays, il faut créer une classe héritant de SQliteOpenHelper, donc l'objectif est de créer et mettre à jour la base de donnée, la méthode onCreate ne sera appelée qu'une unique fois si la base de donnée n'est pas présente (au premier lancement de l'application par exemple), la méthode onUpgrade, elle sera appelée dans le cas d'un changement de version de la base de donnée (par exemple l'ajout ou la suppression d'un champs lors de la mise à jour de l'application), nous n'utiliserons pas cette méthode. La création de la base de donnée se déroule à l'aide de commandes SQL déjà bien connues (rien de spécifique pour Android):

db.execSQL("CREATE TABLE blockednumbers (_id INTEGER PRIMARY KEY AUTOINCREMENT, number TEXT unique);");
db.execSQL("CREATE TABLE blockedmcc (_id INTEGER PRIMARY KEY AUTOINCREMENT, country TEXT, mcc INTEGER unique, blocked INTEGER DEFAULT 0);");

Deux tables sont crées, la première contenenant les numéros de la liste noire, et l'autre contenant la liste des pays (nous devons extraire la liste des pays existants ainsi que leur code MCC correspondant d'un fichier XML récupéré sur Internet, c'est ce que fait la classe XMLCountries, cette classe sera detaillée par la suite, mais son unique méthode retourne une liste de pays qui peuvent par la suite être inserés en base de donnée (le XML est donc à usage unique, tout comme la classe DatabaseHelper).

Gestionnaire de base de donnée

Afin de faciliter l'intéraction avec la base de donnée, la documentation Android recommande la création d'une classe "DatabaseManager", qui sera chargée d'effectuer les requêtes. Par exemple, la fonction:

public void triggerMCC(Country country){
    ContentValues values = new ContentValues();
    values.put("blocked", country.triggerBlocked());
    this.database.update("blockedmcc", values, "mcc=?", new String[]{Integer.toString(country.getMcc())});
}

Se charge d'ajouter/retirer un pays de la liste noire, ContentValues est une classe ressemblant fortement à une Map, permettant d'associer une valeur à une clé. La fonction update prend en paramètre le nom de la table à modifier, une table associative colonnes/valeurs des colonnes à mettre à jour ainsi qu'une chaine de caractère représentant le selecteur "WHERE" d'une requête SQL (notez que la requête est "préparée", la rendant ainsi invulnérable aux attaques d'injection de code SQL), ainsi qu'un dernier argument, un tableau contenant la liste des paramètres requis par le "WHERE" (peut être null); Les méthodes query, insert et delete fonctionnent d'une façon très similaire.

Extraction des données XML

La classe XMLCountries sert à extraire les données relatives aux pays, c'est à dire nom et MCC. Pour cela, il faut se diriger du coté de XmlPullParser, et d'itérer sur tous les "tags" disponibles, en regardant s'il s'agit du nom ou d'un identifiant de pays, à partir de ces informations, nous pouvons créer une liste de Country, qui sera utilisée pour l'insertion en base de donnée. Une fois de plus, cette classe est à usage unique, et n'est instanciée que dans la méthode onCreate de DatabaseHelper.

Tous les éléments sont maintenant en place, l'application se contente de lister les pays disponible qui peuvent être bloqués/débloqués par un simple clique, ainsi qu'une entrée de texte afin d'ajouter à la liste noire un numéro spécifique. L'application sera par la suite "reveillée" lors de la réception d'un SMS et vérifiera si oui ou non ce message doit être transmis aux autres applications.

Quelques idées d'amélioration

  • Compteur du nombre de messages bloqués
  • Liste des numéros bloqués
  • Une option pour exporter les messages bloqués (pour gérer les exceptions).
Bibliographie

Android SDK : Démineur

21 Nov 2013

Après une longue absence (rentrée universitaire), j'ai décidé de faire re-vivre ce blog. J'ai recommencé la programmation Android étant donné que nos cours d'informatiques sont en Java, je me suis dit que cela pouvait être interessant. Concernant l'ouvrage que j'ai acquéri, il s'agit de L'art du développement android, tout est expliqué depuis le début, c'est vraiment accessible je vous le conseil !. Pour l'occasion, j'ai crée un repo sur github sur lequel je compte partager la plupart de mes projets (du moins ceux que je juge interessants).

Le premier projet que je souhaite vous présenter est un démineur, le code est disponible sur github, mais il n'est malheureusement pas commenté (résultat d'une procrastination professionnelle), je vais donc tenter d'expliquer le plus possible des concepts qui m'ont permis de mener à bien ce projet. Une fois de plus, je partage mon avancé dans ce monde qu'est Android et je n'ai donc aucune capacité à enseigner la programmation sur cet environnement, c'est pour cela que ces articles n'auront pas comme objectif de vous enseigner comment utiliser une fonctionnalité précise du SDK android. Je pense que ces publications doivent être utilisées comme support ou exemple pour avancer.

Le démineur

Dans le démineur, j'ai utilisé les techniques suivantes :

  • Re-implémentation d'un élément graphique (héritage bouton)
  • FramgentActivity
  • Interface de communication entre les fragments
  • Création dynamique d'élements graphique
  • Shade afin de personnaliser un élément graphique

Je dois vous avouer qu'au début, j'ai cru que je n'allais jamais finir ce projet, en effet, j'avais réalisé toute la logique de jeu (propre au jeu du démineur) sans m'interesser à la partie interface graphique. Il s'avère que ce fonctionnement s'est montré contre-productif. Au moment de commencer à créer l'interface, rien ne fonctionnait comme je le souhaitais et le débuguage d'interface graphique est plus que pénible. Pour créer la grille, on la déclare dans notre XML, en la laissant vide (sans aucune ligne). Le but est ensuite d'ajouter dynamiquement une ligne, elle même composé de cases (qui héritent de la class Button), pour cela on utilise la fonction showField():

for(int i=0; i < SIZE; i++){
        TableRow row = new TableRow(this);
        row.setPadding(0, 0, 0, 0);
        for(int j=0; j < SIZE; j++){
                TableRow.LayoutParams ly = new TableRow.LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
                ly.setMargins(0, 0, 0, 0);
                this.mines[i][j].setLayoutParams(ly);
                row.addView(this.mines[i][j]);
        }
        this.grid.addView(row);
}

Pour ce qui est du LayoutParams, ce qui est vraiment important est de supprimer les marges qui sont présentes par défaut autour des différents éléments, afin de s'assurer qu'il n'y a aucun décalage entre les colonnes/lignes.

Afin de rester dans le domaine de l'interface graphique, je vais continuer sur un exemple d'utilisation des shades. Ces derniers sont utilisés afin de styliser un élement graphique, dans le cas d'un démineur, il s'agit des boutons qui sont les cases, nous avons trois types de cases: les cases non découvertes, les cases découvertes, et les cases marquées d'un drapeau, par conséquent nous aurons besoin de trois shades. Dont nous allons faire varier la couleur de fond (lien, l'attribut stroke nous permet de définir les caractéristiques de la bordure, l'attribut solid, lui, nous permet, de définir la couleur du fond de la forme.

To be continued...

Android Snippets : WebView

27 Jun 2013

Cela fait un bout de temps que je n'ai plus rien écris ici, non pas par manque d'idée (ce qui est souvent le cas) mais bien pas manque de temps.

Cet article est, à priori, le premier d'une nouvelle série : Android Snippets, voici toutefois quelques précisions:

  • Je n'ai jamais programmé sur Smartphone
  • Ces articles ont comme principal objectif de partager mes découvertes
  • les codes ne sont probablement pas parfait, une correction est bienvenue !

Ceci étant dit, je peux maintenant introduire ce que nous allons aborder aujourd'hui. J'ai toujours été un grand fan de GrooveShark, mais je crois que c'est JackTheRipper qui m'a définivement fait adopter ce service. En effet, il a analysé le player flash de GrooveShark et a retracé tout le fonctionnement du site (plutôt complexe) pour en créer une bibliothèque python ! Un délice.

Le défis était, en une semaine, sans avoir aucune connaissance préalable, une application Android utilisant cette librairie (adaptée en Java, bien évidemment), c'est chose faite, et certains "privilégiés" (quelques personnes sur un chan IRC perdu) ont pu la tester, j'ai préféré la garder privée car je doute que d'un point de vue légal, cette application soit la bienvenue.

Pour autant, cette semaine m'a donné envie de continuer dans le developpement Android, que j'ai trouvé sympathique, abordable et bien documenté. Je souhaite donc partager au fur et à mesure, mon avancement dans ce domaine (en espérant faire naitre en vous un interet pour la chose). Bon j'en ai trop dit, passons aux choses sérieuses !

Ce premier article est consacré au WebView, munissez vous d'eclipse et du SDK android, le tout disponible ici !

WebView tricks

Interception de ressource

Android nous fourni une grande quantité de fonctions qui vous nous permettre d'intercepter les différentes ressources avant l'affichage dans la WebView, ce qui nous permet, par exemple, de modifier sur le tas, le style de la page, ou encore (et c'est le plus interessant) les scripts ! Par simplicité, et pour économiser de la place, voici le code de base, dans la suite de l'article, le code que nous écrirons sera à placer au milieu des balises

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    final WebView browser = new WebView(this);
    setContentView(browser);
    browser.getSettings().setJavaScriptEnabled(true);

    // <ici>
    //  Inserer le code ici !
    // </ici>

    browser.loadUrl("http://amazon.fr");

}

Nous devons maintenant créer un WebClienView qui recevra toutes les notifications et requetes, le notre ressemble à :

browser.setWebViewClient(new WebViewClient() {  
    @Override  
    public WebResourceResponse shouldInterceptRequest (WebView view, String url) 
    {  
         // Execution lorsque la page est affichée
    }  

});  

ShouldInterceptRequest est une méthode appellée automatiquement par un système de callback dès qu'une ressource est téléchargée, l'url de la ressource lui est passé en paramètre. Cette fonction doit retourner soit "null" auquel cas la resource interceptée sera utilisée ou alors, un objet de type WebResourceResponse, qui, comme son nom l'indique, représente une ressource, telle quelle soit (image, script, style) et qui remplacera la ressource interceptée !

Voici une bonne chose de faite, il ne nous reste plus qu'a modifer notre ressource avant de l'injecter dans la WebView ! N'ayant pas trouvé d'exemple de Javascript parlant pour faire cela, nous allons modifier le CSS d'un site bien connu, de telle sorte que le rendu soit flagrand, la méthode reste la même ! Notre cible sera donc Amazon, qui possède un fond blanc trop commun, et à qui la couleur #a72277 va à ravir ! En étudiant le site, on remarque que le CSS qui nous interesse est : websiteGridCSS-websiteGridCSS-14209.V1.css . Changeons notre script de façon à ce qu'il détecte cette ressource !

 @Override
 public WebResourceResponse shouldInterceptRequest (WebView view, String url)
 {
    if(url.indexOf("websiteGridCSS") != -1)
    {
        // Modification de la ressource !
    }
    return null; // Ressource non utile, transmise sans être modifiée
 }

Un problème se pose cependant, nous ne possédons que l'url de la ressource et pas son contenu, il nous faut donc faire une seconde requete vers l'url afin de récupérer son contenu, voici un bout de code qui réalise cette opération :

try{
    BufferedReader in = new BufferedReader(new InputStreamReader(new URL(url).openStream()));
    StringBuilder sb = new StringBuilder();
    String line = null;
    while ((line = in.readLine()) != null) {
        sb.append(line + "\n");
    }
    String ressource = sb.toString();
}catch(IOException e){

}

Maintenant que nous avons notre ressource (le CSS dans notre cas), nous allons l'éditer à coup de regex, afin de transformer "background: #fff;" en "background: #a72277;" ! (il n'est présent qu'une fois dans le style donc nous n'avons pas besoin de cibler notre requête).

ressource = ressource.replaceAll("background: #fff","background: #a72277");         

Notre ressource est maintenant modifiée, la dernière étape consiste à la transmettre à la vue, en créant un objet WebResourceResponse ! Le troisième paramètre du constructeur de WebResourceResponse est de type InputStream, il faut donc penser à convertir notre String en InputStream (ou ByteArrayInputStream, qui hérite de InputStream).

WebResourceResponse wr = new WebResourceResponse("text/css", "UTF-8", new ByteArrayInputStream(ressource.getBytes()));
return wr;

<s Les plus courageux, qui auront essayé de lancer le code auront probablement remarqué que cela ne change rien. En effet, par défaut Amazon envoie une version mobile à notre webview (ce qui ne fait que déplacer le CSS directement dans le HTML d'ailleurs), il faut donc forcer l'affichage du site original, remplacez :

browser.loadUrl("http://amazon.fr");
 par
browser.loadUrl("http://amazon.fr&force-full-site=1");

En voilà ! La source complète est disponible ici. Un petit apercu tout de même !

Pour conclure cette première partie, dans laquelle nous avons vu comment éditer une ressource sur le tas grâce aux fonctions liées au WebView nous avons utilisé une regex pour modifier les CSS existant, mais nous aurions très bien pu rajouter une section dans notre CSS, ou encore générer notre propre CSS de A à Z. Les applications peuvent être multiples. Dans le cas de GrooveShark, le site html5 (mobile) est généré entièrement par JS, et il est possible pas exemple, de rajouter un bouton "Download" à coté du bouton play, de quoi vous donner des idées ! C'est bien beau de pouvoir modifier les ressources sur le tas, mais ca serait encore mieux et plus pratique si on pouvait injecter notre propre Javascript dans la WebView,c'est ce que nous allons voir dans la seconde partie de cet article !

Charger du Javascript

Le code principal est exactement le même que pour la partie précédente, il necéssite un WebViewClient, mais nous n'allons utiliser la méthode onPageFinished afin de detecter la fin du chargement de la page, et y injecter notre Javascript. Ce callback prend en paramètre la vue et l'url de la page chargée ce qui nous donne un code qui ressemble à

browser.setWebViewClient(new WebViewClient() { 
    @Override  
    public void onPageFinished(WebView view, String url)  
    {  

    }  
});

C'est bien beau de detecter la fin du chargement de la page, mais comment on inject le javascript maintenant ? Et bien, l'astuce réside dans une subtilité de la fonction loadUrl. En effet, en placant le mot clé "javascript:" dans le paramètre String Url, nous pouvons éxecuter notre Javascript ! Ce qui donne :

browser.loadUrl("javascript:");  

J'imagine que vous êtes des adeptes du alert(), mais dans notre cas, la fonction alert() (tout comme confirm()) ne fonctionneront pas car notre WebView n'est pas configurée pour, il faudrait définir un webChromeClient pour les faire fonctionner. En restant sur le thème Amazon, voici une facon de faire disparaitre le logo en haut à gauche !

 browser.loadUrl("javascript:var element = document.getElementById(\"nav-logo\");element.parentNode.removeChild(element);");  

LoadUrl nous permet donc d'inserer n'importe quel Javascript dans la view, ce qui est tout de même bien plus pratique (modification du style depuis JS par exemple, plutot que d'éditer le CSS directement). Avec cela tout est possible, ajouter des OnClick, des callbacks, des requêtes ajax, laissez courir votre imagination ! Le code complet est disponible ici.

La cerise sur le gateau

En restant dans les WebView, mais en changeant completement de méthode, nous allons voir comment executer des fonctions Java depuis des actions en Javascript, en d'autres termes, avoir un retour sur l'application depuis des callbacks sur des éléments de la WebView. Voici la base que nous utiliserons pour cette partie

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    final WebView browser = new WebView(this);

    setContentView(browser);
    browser.getSettings().setJavaScriptEnabled(true);

    class JavaScriptInterface   
    {  
        @JavascriptInterface 
        public void run(String text)  
        {  

        }  
    }  

    browser.addJavascriptInterface(new JavaScriptInterface(), "android");  

    browser.setWebViewClient(new WebViewClient() {  
       @Override  
        public void onPageFinished(WebView view, String url)  
        {  

        }       
    });  
    browser.loadUrl("http://amazon.fr?force-full-site=1");
}

La grosse nouveauté dans ce code est la présence de la classe JavaScriptInterface et de la fonction addJavascriptInterface, cette dernière nous permet d'avoir accès aux méthodex de l'interface JavaScriptInterface depuis le javascript.

Voyons maintenant son fonctionnement en détail. Pour cela, nous allons (encore une fois sur Amazon), faire apparaitre un Toast lors de la soumission du formulaire de recherche (le Toast contenant le texte de la recherche), cela illustre bien les échanges possibles grâce à ces interfaces !

Commencons par la partie Javascript, dans la méthode onPageFinished

 browser.loadUrl("javascript:" +
"var element = document.getElementsByClassName(\"nav-submit-input\")[0];" +
"element.setAttribute(\"onClick\",\"window.android.run(" +
"document.getElementById('twotabsearchtextbox').value)\");");  }

Concretement, on ajoute un OnClick sur le bouton de recherche, qu'on lie à la fonction window.android.run, vous l'aurez compris, il s'agit bien de notre méthode contenue dans la classe JavaScriptInterface (que nous avons instancié sous le nom "android"). Codons maintenant cette fonction run qui prend en argument le texte de la barre de recherche et qui doit en afficher un Toast !

Toast.makeText(getApplicationContext(), text, Toast.LENGTH_SHORT).show();

Et voilà ! Rien de bien compliqué là encore, il suffit de connaitre ! Pour en revenir à ce qui m'a poussé à découvrir ces fonctions, et bien je détèste réaliser les GUI, et dans le contexte de mon application GrooveShark, le site propose une très belle interface mobile en HTML5, entièrement genérée par Javascript, je me suis donc amusé à modifier ce Javascript pour ajouter mon bouton de téléchargement aux différentes chansons! La dernière astuce va me permettre de démarrer le téléchargement Java depuis le bouton crée en Javascript ! Et voici mon code pour le moment BigShark, et voici un apercu (avant après):

gs_normal
à
js_aterated

Et voilà :)