Cliquer ici pour imprimer

Dernière modification : 14/10/2021 à 14:27

5E AE1

Cycle de vie d'une Activity

  • Créez un nouveau projet

  • Modifiez le layout pour avoir un EditText et deux boutons (conseil : préférez un LinearLayout au ConstraintLayout créé par défaut). Pour aller plus vite, vous pouvez vous inspirer du code ci dessous

    <TableRow android:id="@+id/tableRow1" android:layout_width="match_parent" android:layout_height="wrap_content">
    <Button android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:onClick="onClickStart" 
    android:text="start"/>
    <Button android:id="@+id/button2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:onClick="onClickFinish" 
    android:text="finish"/>
    </TableRow>
    <ScrollView android:id="@+id/ScrollView01" android:layout_width="wrap_content" android:layout_height="match_parent">
    <EditText android:id="@+id/editTextId" android:layout_width="match_parent" android:layout_height="wrap_content" 
    android:ems="10" 
    android:gravity="left|top" android:inputType="textMultiLine">
    <requestFocus/>
    </EditText>
    </ScrollView>
  • Notez que les fonctions onClickStart et onClickFinish n'existent pas dans votre classe MainActivity. Demandez à AndroidStudio de les créér, et complètez les avec le code ci dessous

    public void onClickStart(View v){
      AlertDialog alertDialog = new AlertDialog.Builder(MainActivity.this).create();
      alertDialog.setTitle("onClickStart");
      alertDialog.setMessage("Démarrage d'une nouvelle activité, même code");
      alertDialog.setButton(
              AlertDialog.BUTTON_POSITIVE, 
              "OK",
              new DialogInterface.OnClickListener() {
                  public void onClick(DialogInterface dialog, int which) {
                      dialog.dismiss();
                      Intent intent = new Intent(MainActivity.this,MainActivity.class); 
                      startActivity(intent); // création d'une autre activité avec le même code
                  }
              }
      );
    
      alertDialog.setButton(
              AlertDialog.BUTTON_NEGATIVE, 
              "Annuler",
              new DialogInterface.OnClickListener() {
                  public void onClick(DialogInterface dialog, int which) {
                      dialog.dismiss();
                  }
              }
      );
      alertDialog.show();
    }
public void onClickFinish(View v){
   finish(); // Termine l'Activity
}
  • Testez le comportement

  • On veut maintenant "tracer" les appels aux méthodes "crochets" de Activity (voir les cases grises dans le diagramme d'état ici https://developer.android.com/reference/android/app/Activity)

    • onCreate();
    • onStart();
    • onResume();
    • onRestart();
    • onDestroy();
    • onStrop();
    • onPause(); pour cela on va les redéfinir et y ajouter du code pour ajouter du texte dans le EditText, sur le modèle de onStart ici
      @Override
      protected void onStart() {
      super.onStart();
      EditText et = findViewById(R.id.editTextId);
      et.append("onStart\n");
      }
  • Testez le comportement. C'est pas mal, mais on ne sait pas trop quelle instance de notre classe Activity est affiché...

  • Rajoutez dans la classe un champs entier "id", et dans la méthode onCreate un append() sur l'EditText pour afficher un message du style "Activity numéro "+id.

  • Il reste à initialiser id dans onCreate() avant l'affichage du message. Pour cela nous allons utiliser les méthodes getIntExtras() et putExtras() de Intent.

    • dans onCreate, il faut récupérer l'Intent qui a servi à créér l'instance courante : méthode getIntent()
    • sur cet Intent il faut invoquer la méthode getIntExtras() avec une clef que vous choisissez (par ex. mon_identifiant). Si elle est présente, on aura une valeur à affecter à notre id, sinon c'est que l'activity a été initié par le systeme et la valeur par defaut que l'on souhaite est 1
    • dans le code associé au bouton OK du dialog de création que l'on vous a donné plus haut, repéré les lignes où l'intent est créé, puis utilisé pour démarrer une nouvelle activité
    • inséré entre ces lignes un appels à putExtras() sur l'intent, pour ajouter dedans une valuer entiere égale à l'id de l'activity courante plus 1, en utilisant la même clef que celle avec laquelle vous esayez de récupérer l'id dans onCreate() (par ex. mon_identifiant)
  • Bonus : modifier l'apparence de tous les bouttons idépendamment puis en utilisant un theme.

Lanceur d'applis

On souhaite réaliser une application permettant d'appeler 3 composants Logiciels, dont deux prédéfinis : téléphonie, navigation et un troisième qui est une application "maison" permettant de saisir un login et un mot de passe pour s'authentifier.

Appels d'applications existantes, déjà installées sur votre mobile/émulateur

Téléphoner et naviguer sur le web sont deux exemples d'applications prédéfinies, deux exemples de composants logiciels réutilisables.

Vous trouverez dans la documentation de Intent, ou dans les supports de cours de JM. Douain (sur Blackboard) des explications sur les intent implicites. Lisez.

Voici les extraits qui nous interessent :

// démarre une activity pour téléphoner :
String url="tel:123456";
Intent intent = new Intent(Intent.ACTION_CALL,Uri.parse(url));
startActivity(intent);
// démarre une activity pour afficher une apge web :
String url="http://www.esiee.fr";
Intent intent = new Intent(Intent.ACTION_VIEW,Uri.parse(url));
startActivity(intent);

Proposez une IHM et les appels de ces deux activités. Vous pouvez rmeplacer les images des boutons avec des icones, par exemple sur http://www.iconfinder.com. Vous pouvez aussiutiliser cet outil http://angrytools.com/android/ afin de personnalisez votre IHM (bonus).

Note : Afin que les images associées à vos ImageButton soient prises en compte, après leur téléchargement, placez les dans le dossier res/drawable (dossier qu'il faudra peut-être créer), le nom de l'image doit être en minuscule.

Note : l'activité pour téléphoner nécessite l'une des permissions ci-dessous

<uses-permission android:name="android.permission.CALL_PHONE"></uses-permission>

ou bien

<uses-permission android:name="android.permission.CALL_PRIVILEGED"></uses-permission>

à ajouter au manifest de l'application

Note : de nouvelles contraintes liées aux permissions sont apparues avec Android 6.0 et la cible du SDK 23, de nouveaux tests sont imposés https://developer.android.com/training/permissions/requesting.html ... le plus simple est de se laisser guider par AndroidStudio qui installe le code.

Composant Login

Le nouveau composant Login doit être créé, celui-ci affiche à l'écran un dialogue permettant à l'utilisateur d'entrer un identifiant et un mot de passe. Ces deux paramètres sont retournés à l'activité cliente (cf.setResult et RESULT_CANCELED, RESULT_OK).

Ce nouveau composant est créé dans un nouveau projet AndroidStudio, avec un nouveau nom de package et il doit être également possible de déclencher ce composant comme une application standard.

  • Développez ce nouveau composant logiciel, on vous donne le fichier Layout :
    
    <TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
      android:layout_width="fill_parent"
      android:layout_height="fill_parent"
      android:stretchColumns="1">

<EditText android:text="" android:id="@+id/txtUname" android:layout_width="fill_parent" android:layout_height="wrap_content">

<EditText android:text="" android:id="@+id/txtPwd" android:layout_width="fill_parent" android:layout_height="wrap_content" android:password="true">

<Button android:text="Login" android:id="@+id/btnLogin" android:layout_width="fill_parent" android:layout_height="wrap_content">

```
  • Choisissez une ACTION spécifique pour le composant Login,

  • Proposez une autre possibilité de sélection de ce composant, avec une "Intent implicite"

  • Supprimer la possibilité de déclencher ce composant comme une application standard, depuis le bureau.

  • Complétez l'IHM de la question 1 avec l'appel de ce nouveau composant. Déclenchez au clic sur le bouton l'appel du composant login, l'application appelante se contente d'afficher les résultats fournis par le composant Login dans un Toast. Exemple de Toast :

    Context context = getApplicationContext();
    CharSequence text = "Hello toast!";
    int duration = Toast.LENGTH_SHORT;
    Toast toast = Toast.makeText(context, text, duration);
    toast.show();

    Installation d'une permission

  • Ajoutez aux clients du composant Login, la nécessité impérieuse d'une permission pour utiliser ce composant. Le composant Login, le fournisseur, déclare la permission dans le fichier AndroidManifest.xml,

    <permission android:name="composant.permission.CALL_LOGIN"

    ... cf. diapositive 101 du cours de JM Douain. Les clients ajoutent cette permission à l'aide de la clause

    <uses-permission android:name="composant.permission.CALL_LOGIN" />
  • Ajoutez un toast informant les clients n'ayant pas cette permission, de leur impossibilité d'exécuter ce composant.

ListView, ListActivity

  • Proposez une ListActivity/ListView permettant d'afficher la liste des unités d'enseignement que vous suivez cette année. (Veillez à choisir android.R.list pour l'identifiant, c'est à dire utilisez android:id="@android:id/list")

Exemple (à compléter) :

public class MainActivity extends ListActivity {

  private List<String> listeUE;

  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);


   this.listeUE = new ArrayList<String>();
   listeUE.add("INA134"); listeUE.add("SAE125"); listeUE.add("MAA131");
   listeUE.add("ELE102"); listeUE.add("ELA134"); listeUE.add("ELE135");
   listeUE.add("INA136"); listeUE.add("SAE126"); listeUE.add("MAA136");
   listeUE.add("ELA136"); listeUE.add("ELE137");

  }
  • Ajoutez la possibilité d'insérer une nouvelle unité d'enseignement au début de la liste, la mise à jour de la liste est effectuée par l'appel de la méthode notifyDataSetChanged Cet ajout est rendu possible par la présence d'un bouton et d'un champ texte supplémentaires, dans la même activité : e0deb1f3b791e5587171fd7656552900.png

  • Au clic sur l'unité choisie, une seconde activité est déclenchée, celle-ci se contente d'afficher le nom de l'unité, dans un "Toast".

  • Au clic sur l'unité choisie, ajoutez au sein de la seconde activité, la possibilité de supprimer l'unité ainsi sélectionnée. Une fenêtre de dialogue/alerte avec l'utilisateur sera affichée, en cas de suppression la mise à jour de la liste est également assurée par l'appel de la méthode notifyDataSetChanged Un exemple de code : http://www.mkyong.com/android/android-alert-dialog-example/

  • Ajoutez également la possibilité de supprimer une unité lorsqu'on effectue un clic long dessus.

  • Une activité peut-être détruite par le système Android pour moult raisons (ex. modification orientation de l'écran), en conséquence rendez cette liste persistante. La persistance est effectuée l'aide des "Shared preferences" Un exemple de code : http://stackoverflow.com/questions/11193983/android-save-liststring ou bien :

    
    private void saveListeUE(){
    SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
    SharedPreferences.Editor sEdit = prefs.edit();
    sEdit.putInt("listeUE_size",listeUE.size());
    for(int i = 0; i < listeUE.size(); i++){
      sEdit.putString("listeUE_"+i, listeUE.get(i));
    }
    sEdit.commit();
    }

private void loadListeUE(){ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); int size = prefs.getInt("listeUEsize", -1); this.listeUE = new ArrayList(); if(size==-1){ listeUE.add("INA134"); listeUE.add("SAE125"); listeUE.add("MAA131"); listeUE.add("ELE102"); listeUE.add("ELA134"); listeUE.add("ELE135"); listeUE.add("INA136"); listeUE.add("SAE126"); listeUE.add("MAA136"); listeUE.add("ELA136"); listeUE.add("ELE137"); }else{ for(int i = 0; i < size; i++){ listeUE.add(prefs.getString("listeUE"+i, "NULL")); } }

//prefs.edit().clear();

}


*  Modifiez votre application afin que la liste des items apparaisse par ordre alphabétique, cf. java.util.Collections.sort

## Intent, IntentFilter et Receiver

On souahite réaliser une application permettant de démarrer et d'arrêter une horloge et d'enregistrer 3 receveurs locaux.

Ci-dessous, une copie d'écran au bout de 4 secondes, à chaque seconde écoulée un affichage du compteur de secondes est réalisé par les trois receveurs locaux

![0865b92575c889ef992e9f03ebfbc067.png](resources/383aaad79ce14d7bb8a24a21c6730538.png)

Nous allons Installer 3 receveurs locaux, tous sont souscripteurs de l'action "time_action_tic"  Ticker.TIME_ACTION_TIC

Au click sur start ticker, l'horloge démarre et délivre une notification "time.action.tic" toutes les secondes, tous les receveurs reçoivent cette notification et affichent la valeur reçue dans la zone de texte associée, au click sur stop ticker l'horloge est suspendue. Un nouveau click sur start timer permettra de reprendre le compte. Un click sur finish arrête l'horloge et termine l'activité.

Notez que pour ce tp, l'horloge est indépendante du cycle de vie de l'activité principale, i.e. quitter l'activité alors que l'horloge est active n'a aucune incidence sur celle-ci. Par exemple après une rotation de l'écran, (ce qui induit une destruction de l'activité), l'horloge continue l'incrément du compteur et s'exécute en "tache de fond". Deux méthodes assurant la persistance de l'horloge sont fournies : 

```java
public class MainActivity extends Activity {
  private final String TAG = this.getClass().getSimpleName();
  private final boolean I = true;
  private Ticker ticker;
  private static long count;
// ...
private class Ticker extends Thread implements Serializable{  

  public static final String TIME_ACTION_TIC = "time_action_tic";
  public static final String COUNT_EXTRA = "count";
  private Context context;


  public Ticker(Context context){
    this.context = context;
  }
  public void startTicker(){
    this.start();
  }
  public void stopTicker(){
    this.interrupt();
  }
  public void run(){
    Intent intent = new Intent();
    intent.setAction(TIME_ACTION_TIC);
    while(!isInterrupted()){
        SystemClock.sleep(1000L);
        count++;
        intent.putExtra(Ticker.COUNT_EXTRA, count);
        context.sendBroadcast(intent); // à commenter pour q2                          
      //context.sendOrderedBroadcast(intent,null); // à décommenter pour q2

    }
  }
}

private Receiver1 r1;
private Receiver2 r2;
private Receiver3 r3;



private class Receiver1 extends BroadcastReceiver{
  // TODO
}
private class Receiver2 extends BroadcastReceiver{
  // TODO
}
private class Receiver3 extends BroadcastReceiver{
  // TODO
}


@Override
public void onResume(){
  super.onResume();
  if(I)Log.i(TAG,"onResume");

  IntentFilter filter = new IntentFilter();
  filter.addAction(Ticker.TIME_ACTION_TIC);
  filter.setPriority(100);
  //...

}
public void onSaveInstanceState(Bundle outState){
  if(I)Log.i(TAG,"onSaveInstanceState");
  outState.putSerializable("ticker", ticker); 

}

public void onRestoreInstanceState(Bundle outState){
  if(I)Log.i(TAG,"onRestoreInstanceState");
  ticker = (Ticker)outState.getSerializable("ticker"); 

}
  • Proposez une activité et son interface (3 boutons, 3 zones de texte) et ses 3 receveurs. Veillez à ce qu'il n'y ait qu'un seul thread horloge à la fois.

  • Modifier le code pour que les trois receveurs r1,r2 et r3 aient respectivement une priorité de 100, 200 et 300. Dès que le compte atteint la valeur de 20 le receveur r2 doit interrompre la propagation de la notification (cf. isOrderedBroadcast et abortBroadcast).

Ci-dessous une copie d'écran après 32 secondes, r1 ne doit plus être notifié : a255a26d76274c5cafb27b72a4eae6ed.png

Wifi, liste de tous les points d'accès

Thème: Obtenir une liste des noms des points d'accès, évaluer la distance du point d'accès, se localiser.

Attention seul le développement sur mobile est possible, les émulateurs n'ont pas accès aux réseaux wifi.

Nous souhaitons l'affichage sur un mobile de la liste de tous les points d'accès Wifi accessibles, l'essentiel demandé à ce TP sera la mise en œuvre de deux receveurs

  • Un premier receveur se contente d'afficher la liste des points d'accès WiFi obtenue à chaque réactualisation effectuée par Android. Souscription de ce premier receveur par :
    this.wifiScanResultReceiver = new WifiScanResultReceiver();
    IntentFilter filter = new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
    registerReceiver(this.wifiScanResultReceiver, filter);

En cas de doublons, seul sera conservé le point d'accès avec le meilleur signal (WifiManager.compareSignalLevel(rhs.level, lhs.level);)

  • Le second receveur affiche dans un toast, les évènements reçus en mentionnant tout particulièrement la perte de connexion. Souscription de ce second receveur par :
    IntentFilter  filter = new IntentFilter();
    filter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
    filter.addAction(WifiManager.SUPPLICANT_CONNECTION_CHANGE_ACTION);
    filter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
    filter.addAction(WifiManager.RSSI_CHANGED_ACTION);
    wifiStateChangedReceiver =new WifiSateChangedReceiver();
    registerReceiver(wifiStateChangedReceiver, filter);

Exemple : 83a73e6241e0d2e5924def44d3bf2e06.png

  • A chaque réactualisation de la liste le point d'accès le plus proche est mentionné dans un Toast.
  • La liste est maintenant ordonnée selon leur proximité (level) par rapport au mobile Note: Collections.sort permet de trier une liste d'items
    
    Comparator<ScanResult> comparator = new Comparator<ScanResult>() {
    public int compare(ScanResult lhs, ScanResult rhs) {
       return WifiManager.compareSignalLevel(rhs.level, lhs.level);
    };

Collections.sort(wifiScanList, comparator);


 Rappel :
 La liste des permissions, cf. AndroidManifest.xml
``` sans oublier ``` ```

Determining distance from decibel level

There’s a useful concept in physics that lets us mathematically relate the signal level in dB to a real-world distance. Free-space path loss (FSPL) characterizes how the wireless signal degrades over distance (following an inverse square law):

$$ FSPL(dB) = 20 log{10}(d)+20log{10} (f)+92.45$$

The constant there, 92.45, varies depending on the units you’re using for other measurements (right now it’s using GHz for frequency and km for distance). For my application I used the recommended constant -27.55, which treats frequency in MHz and distance in meters (m). We can re-arrange the equation to solve for d, in Java:

public double calculateDistance(double levelInDb, double freqInMHz)    {
   double exp = (27.55 - (20 * Math.log10(freqInMHz)) + Math.abs(levelInDb)) / 20.0;
   return Math.pow(10.0, exp);
}

Now, there are few drawbacks to this rough approximation:

FSPL explicitly requires “free space” for calculation, while most Wifi signals are obstructed by walls and other materials. Ideally, we will want to sample the signal strength many times (10+) to account for varying interference.

  • Problem (1) will be resolved in the future by using the signal-to-noise ratio to more accurately estimate (that sounds like an oxymoron) obstructions to the wifi signal.
  • Problem (2) can be implemented in code by sampling many times and computing the average signal level.

Using the above code along with Android’s WifiManager and ScanResult classes, I can print out our final measurements:

results = wifi.getScanResults();
for (ScanResult s : results)    {
  DecimalFormat df = new DecimalFormat("#.##");
  Log.d(TAG, s.BSSID + ": " + s.level + ", d: " +
             df.format(calculateDistance((double)s.level, s.frequency)) + "m");
}

Réalisation d'une activité de geolocalisation

Ici vous êtes plus libre de faire parler votre créativité. À l'aide de trois points d'acces wifi dont on connait les position, proposez une activité géolocalisant votre appareil. L'activité doit etre mise à jour lorsqu'on se déplace avec le mobile, et donc que la distance au point d'acces change.

Lors de la soutenance de janvier, vous devrez faire une démonstration de cette activité.