Android, perfectionnement

  • Version Java.
  • Version Kotlin.

Android, perfectionnement

Logo de Android
  • Qui je suis.
  • Qui vous êtes/ce que vous attendez.
  • Présentation du plan.

Qui je suis

  • Tristan SALAÜN.
  • Développeur d'applications mobiles Android.
  • Formateur Android/Java et Kotlin (scolaires et professionels).
  • Co-fondateur de Startup Marseille.
    Logo Startup Marseille
  • Fondateur de Light4Events.

Qui êtes vous ?

Tour de table :
  • Prénom.
  • Expérience (Java, Mobile, Android).
  • Attentes.

Android, perfectionnement

Mise en place

Étapes de mise en place :

Android Studio

Pour commencer, nous allons vérifier que nous avons bien Android Studio fonctionnel avec la dernière version :
Version Android Studio.

ADB

Vérifions que ADB est bien accessible en ligne de commande, cela nous sera utile par la suite.
  • Ouvrons une invite de commande (Windows + R, cmd).
  • Saisissons : adb version.

Téléphone en mode développeur

Nous vérifions que notre téléphone est en mode développeur :
  • En ligne de commande tapons : adb devices
  • Vérifions dans Android Studio que le device apparaît bien.
Dans le cas contraire, nous vérifions que le téléphone a bien le mode développeur activé.

Coloration de logcat

Nous allons colorer le logcat afin qu'il soit plus lisible :
  • File / Settings
  • Cherchons : "logcat"
  • Editor / Color Scheme / Android Logcat
  • Les couleurs que j'ai définies (à titre d'exemple) :
    Assert C00000
    Debug 41BB2E
    Error FF6B68
    Info F5F550
    Verbose BBBBBB
    Warning F4AC40

Récupération des LivesTemplates

  • Récupération des LivesTemplates depuis la clé USB/mail/adresse web.
  • A copier dans :
    • Windows: C:\Users\"user_name"\AppData\Roaming\Google\AndroidStudio4.1\templates
    • Linux: ~/.AndroidStudio"version"/config/templates
    • macOS: ~/Library/Preferences/AndroidStudio"version"/templates
  • Nous verrons leur utilisation au fur et à mesure de la formation.

scrcpy

Nous allons installer scrcpy qui nous permettra de partager l'affichage de notre téléphone.
  • Téléchargeons la dernière version sur GitHub.
  • Décompressons le fichier ZIP dans un répértoire (C:\tools par exemple) ce qui donnera dans notre cas : C:\tools\scrcpy-win64-v1.16.
  • Lançons l'utilitaire avec scrcpy.

Introduction

Rappels des principes de base Android

Les composants de base :

Activités

Présentation

L'activité est le composant de base d'Android.
  • Point d'entrée dans l'application.
  • Plusieurs points d'entrée pour une seule application.
  • Faible dépendance entre activités.
  • Une interface graphique.
  • Pourquoi pas en plein écran (live template : and_fullscreen_immersive, à l'extérieur d'une méthode).
  • Ou encore en mote PIP (Picture In Picture) (live template : and_pip, à l'extérieur d'une méthode).

Déclaration

L'activité est déclarés dans le manifest :

<manifest ... >
  <application ... >
      <activity android:name=".ExampleActivity" />
      ...
  </application ... >
  ...
</manifest >

Intent filters

Il est possible de définir des filtres qui permettrons de lancer l'activité. Écrivons l'exemple qui va nous permettre de récupérer du texte. Nous allons suivre les étapes suivantes :
  • Création d'un bouton qui va partager du texte.
  • Création d'une activité qui va afficher le texte reçu.
  • Ajout d'un Intent Filter à cette seconde activité.
  • Test du résultat.

Création du bouton

  • On déclare le bouton dans le Layout.
  • On ajoute un OnClickListener.
  • On implémente le partage du texte.
Pour partager le texte on va utiliser :

// Create the text message with a string
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.setType("text/plain");
sendIntent.putExtra(Intent.EXTRA_TEXT, "Un texte à partager");
// Start the activity
startActivity(sendIntent);
Pour partager le texte on va utiliser :

// Create the text message with a string
val sendIntent = Intent()
with(sendIntent){
    action = Intent.ACTION_SEND
    type = "text/plain"
    putExtra(Intent.EXTRA_TEXT, "Un texte à partager")
}
// Start the activity
startActivity(sendIntent)

Création d'une nouvelle activité.

  • File / New / Activity / Empty Activity (ReceiveTextActivity).
  • Ajout du filtre dans le manifest
  • Récupération et affichage du texte reçu.
Filtre dans le manifest :

<activity android:name=".ReceiveTextActivity">
    <intent-filter>
        <action android:name="android.intent.action.SEND" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:mimeType="text/plain" />
    </intent-filter>
</activity>
Récupération du texte partagé :

Bundle extras = getIntent().getExtras();
String receivedText = (extras != null) ? extras.getString(Intent.EXTRA_TEXT) : "";
Log.d(TAG, "onCreate received text = " + receivedText);
Récupération du texte partagé :

val receivedText = intent.extras?.getString(Intent.EXTRA_TEXT) ?: ""
Log.d(TAG, "onCreate received text = $receivedText")

Vérification de l'intent.

Si vous changez la valeur de l'action par une valeur quelconque (voir ci-dessous), que se passe-t-il, et pourquoi ?

sendIntent.setAction("aaa");

Comment y remédier ?

Il est préférable de vérifier que l'Intent pourra être récupéré avec le code suivant :

// Verify that the intent will resolve to an activity
if (sendIntent.resolveActivity(getPackageManager()) != null) {
    startActivity(sendIntent);
}

Chooser.

Lors du choix de l'application à lancer, choisissez : Toujours (Always).

Comment faire si l'on veut proposer le choix à chaque fois ?

On va forcer le lancement du chooser, avec le code suivant :


// Always use string resources for UI text.
// This says something like "Share this photo with"
String title = getResources().getString(R.string.chooser_title);
// Create intent to show the chooser dialog
Intent chooser = Intent.createChooser(sendIntent, title);
// Verify the original intent will resolve to at least one activity
if (sendIntent.resolveActivity(getPackageManager()) != null) {
    startActivity(chooser);
}

Chooser.

Lors du choix de l'application à lancer, choisissez : Toujours (Always).

Comment faire si l'on veut proposer le choix à chaque fois ?

On va forcer le lancement du chooser, avec le code suivant :


// Always use string resources for UI text.
// This says something like "Share this photo with"
val title = resources.getString(R.string.chooser_title)
// Create intent to show the chooser dialog
val chooser = Intent.createChooser(sendIntent, title)
// Verify the original intent will resolve to at least one activity
if (sendIntent.resolveActivity(packageManager) != null) {
    startActivity(chooser)
}

Ouverture depuis un lien.

Nous pouvons avoir besoin de lancer notre application depuis un lien sur le web, pour cela nous allons utiliser un Intent Filter :

<intent-filter>
    <action android:name="android.intent.action.VIEW"/>

    <category android:name="android.intent.category.DEFAULT"/>
    <category android:name="android.intent.category.BROWSABLE"/>

    <data android:scheme="http"/>
    <data android:host="test.link.com"/>
</intent-filter>
Essayons de cliquer sur le lien suivant : Ouvrir application.

Cycle de vie

Cycle de vie Activité

Cycle de vie

Nous allons utiliser le Live Template : and_lifecycle_activity, dans notre classe de l'Activity.
Puis nous utilisons le Live Template : and_tag, juste après la déclaration de notre classe, avant la déclaration des méthodes.

Fragments

Nous allons détailler l'exemple : New / Activity / Primary/Detail Flow :
Master Detail Flow
Pour l'exercice sur le téléphone, modifions le répertoire layout-sw600dp par layout-land.
Que constatons nous, et pourquoi ?

Fragments

Ajout d'un fragment statiquement (dans un XML) :

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
          android:orientation="horizontal"
          android:layout_width="match_parent"
          android:layout_height="match_parent">
<fragment android:name="com.example.news.ArticleListFragment"
          android:id="@+id/list"
          android:layout_weight="1"
          android:layout_width="0dp"
          android:layout_height="match_parent" />
<fragment android:name="com.example.news.ArticleReaderFragment"
          android:id="@+id/viewer"
          android:layout_weight="2"
          android:layout_width="0dp"
          android:layout_height="match_parent" />
</LinearLayout>

Fragments

Ajout d'un fragment dynamiquement, via le FragmentManager :

FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
Qui permet par la suite de gérer dynamiquement les fragments :

ExampleFragment fragment = new ExampleFragment();
fragmentTransaction.add(R.id.fragment_container, fragment);
fragmentTransaction.commit();

NDK

Présentation du développement natif avec NDK. JNI.

  • C/C++.
  • Performances.
  • Réutilisation de code.

Création d'un projet.

Nous allons étudier le fonctionnement d'une application simple faisant appel à du code C/C++.
Pour cela nous allons suivre plusieurs étapes :
  • Installation du NDK.
  • Création d'un projet C/C++.
  • Test.

Installation du NDK.

SDK manager

Installation du NDK.

Install NDK

Création d'un projet.

NDK Create project

Nommage du projet.

NDK Name project

Sélection de la toolchain.

NDK Select ToolChain

Gestion erreur CMAKE.

NDK Error CMAKE

CMAKE Installé.

NDK CMAKE Installed

Ajout CMAKE dans le path.

NDK CMAKE Path error

Test.

Il ne reste plus qu'à tester le code, et voir les points les plus importants ensemble.

Fonctionnalités avancées Android Studio

Instant Run

Instant Run n'est plus disponible sur la version actuelle. Mais il y a deux systèmes de patch. Run patch
Rechargement de l'activity, ou patch du code.

Débug

Poser un point d'arrêt :
Debug breakpoint

Débug

Lancer en mode débug :
Debug run debug
Ou attacher le débugger en cours d'exécution :
Debug attach debug

Débug

Debug debugger screen
Debug debugger screen

Débug

Il est possible de modifier les valeurs des variables, même d'exécuter du code en direct dans le contexte courant.

Debug en WiFi

Il est possible de se connecter en WiFi pour ADB. En ligne de commande lancer la commande :

adb tcpip 5555
Puis il suffit de se connecter sur l'adresse IP du téléphone, par exemple :

adb connect 192.168.1.99

Debug en WiFi automatique

Nous pouvons utiliser le script suivant (sous Windows dans un fichier .bat) pour nous connecter automatiquement :

@echo off
echo Activate tcp mode
adb -d tcpip 5555

REM Sleep for 1 seconds
echo Wait a bit
ping -n 2 127.0.0.1>nul

echo Find IP
FOR /F "tokens=2 skip=1" %%A IN ('adb -d shell ip -f inet addr show wlan0') DO (
	FOR /F "tokens=1 delims=/" %%B IN ("%%A") DO (
 	    echo "Connecting to %%B"
		adb connect %%B
	)
	goto :eof
)

:eof

Profile

View > Tool Windows > Profiler.
Profiler

Templates intégrés

Templates

Live Templates

File / Settings
Editor / Live Templates.
Live Templates

Live Templates

Les variables.
Live Templates variable

Gestion des graphiques

Création d'icônes :
Image assets

Gestion des graphiques

Création d'icônes :
Image assets

Gestion des graphiques

Création d'icônes :
Image assets

Gestion des graphiques

Création de ressources vectorielles :
Vector assets

Gestion des graphiques

Création de ressources vectorielles :
Vector assets

Gestion des graphiques

Création de ressources vectorielles :
Vector assets

Changement de la teinte

L'image vectorielle à plusieurs avantages :
  • Est compacte.
  • Être affichée avec différentes tailles sans perte de qualité.
  • Changer de couleur facilement.
Testons tous cela, par exemple :

<ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_cloud"
        android:tint="@color/colorAccent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintBottom_toBottomOf="parent" />

Gestion des graphiques

Images extensibles (9patch) :
Nine patch
Les ressources graphiques sont disponibles ici.

Notifications

Les Toasts

Avons nous besoin de revoir les Toasts ?

Notification

Les notifications, sont bien plus puissantes qu'il n'y parait, voyons un peu plus en détail leur fonctionnement.
Le template n'étant plus disponible, nous allons utiliser directement la documentation officielle.
Create a Notification

Permissions 6.0

Apports du SDK 6.0. Les permissions à la demande.

  • Plus de protection de la vie privée.
  • Granularité des permissions.
  • Permissions à l'exécution.

Type de permissions

  • Normal permissions.
  • Signature permissions.
  • Dangerous permissions.
  • Special permissions (SYSTEM_ALERT_WINDOW et WRITE_SETTINGS).
Les permissions sont regroupées en groupes de permissions. Obtenir une permission d'un groupe permet d'obtenir les autres.

Exemple d'utilisation

Nous voulons pouvoir passer un appel téléphonique, nous allons avoir besoin de la permission CALL_PHONE.
Pour cela nous allons passer par plusieurs étapes :
  • Ajout dans le manifest.
  • Vérification de l'accord.
  • Gestion du retour.
  • Mise en place rapide.
  • Mise en place encore plus rapide.
  • Solution alternative.

Ajout dans le manifest.


<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.example.phonecall">

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

    <application ...>
        ...
    </application>
</manifest>

vérification de la permission.


if (ContextCompat.checkSelfPermission(thisActivity, Manifest.permission.CALL_PHONE)
        != PackageManager.PERMISSION_GRANTED) {
    // Permission is not granted
}

Demande de la permission.


// Here, thisActivity is the current activity
if (ContextCompat.checkSelfPermission(thisActivity,
        Manifest.permission.CALL_PHONE)
        != PackageManager.PERMISSION_GRANTED) {

    // Permission is not granted
    // Should we show an explanation?
    if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,
            Manifest.permission.CALL_PHONE)) {
        // Show an explanation to the user *asynchronously* -- don't block
        // this thread waiting for the user's response! After the user
        // sees the explanation, try again to request the permission.
    } else {
        // No explanation needed; request the permission
        ActivityCompat.requestPermissions(thisActivity,
                new String[]{Manifest.permission.CALL_PHONE},
                MY_PERMISSIONS_REQUEST_CALL_PHONE);

        // MY_PERMISSIONS_REQUEST_CALL_PHONE is an
        // app-defined int constant. The callback method gets the
        // result of the request.
    }
} else {
    // Permission has already been granted
}

Gestion de la réponse


@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
    switch (requestCode) {
        case MY_PERMISSIONS_REQUEST_CALL_PHONE: {
            // If request is cancelled, the result arrays are empty.
            if (grantResults.length > 0
                && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                // permission was granted, yay! Do the job you need to do.
            } else {
                // permission denied, boo! Disable the functionality that depends on this permission.
            }
            return;
        }
        // other 'case' lines to check for other
        // permissions this app might request.
    }
}

Mise en place rapide.

Afin de mettre en place un code similaire, je vous propose d'utiliser un live template.
  • En dehors d'une méthode, utiliser le live template : and_permission_single.
  • Choisir CALL_PHONE.
  • Suivre les instructions dans les commentaires.
  • Implémenter le code dans actionToBeCalled().
  • Ajouter @SuppressLint("MissingPermission") sur cette méthode pour supprimer le warning.
  • Appeler la méthode callAction().

Mise en place encore plus rapide.

Nous pouvons aussi utiliser des librairies externe pour gérer les permissions, voir la liste ici :

Solution alternative.

Utiliser un intent qui n'a pas besoin d'une permission particulière, dans notre cas, utiliser :

String url = "tel:"+"0612345678";
Intent callIntent = new Intent(Intent.ACTION_DIAL, Uri.parse(url));
startActivity(callIntent);

Autres exemples

Dans d'autres cas nous pouvons nous passer des permissions :
  • Accès à Internet (intent, et intent filter pour le retour).
  • Accès aux contact (application de SMS, ...) : mode dégradé.
  • Enregistrement dans la mémoire privée au lieu de la mémoire externe.

Outils avancés de développement

Gradle

Paramètres de base

Détaillons un fichier graddle de base :

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.2"
    defaultConfig {
        applicationId "com.example.services"
        minSdkVersion 15
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test:runner:1.2.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}

Types de build

Par défaut nous avons le type debug qui est définis. Un second type release est aussi définis.

Flavors

Nous pouvons définir plusieurs dérivés de notre application principale, via les flavors.
Tous les détails ici :
https://developer.android.com/studio/build/build-variants#product-flavors.

Ajoutons le code suivant :

buildTypes {
    // ...
}
flavorDimensions "version"
productFlavors {
    demo {
        dimension "version"
    }
    full {
        dimension "version"
    }
}






Dépendances

Nous verrons plus en détails dans la partie ajout des librairies, la gestion des dépendances.

Mise en variable des versions


ext {
    appcompatVersion = '1.1.0'
    constraintLayoutVersion = '1.1.3'
    junitVersion = '4.12'
    runnerVersion = '1.2.0'
    espressoVersion = '3.2.0'
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "androidx.appcompat:appcompat:$appcompatVersion"
    implementation "androidx.constraintlayout:constraintlayout:$constraintLayoutVersion"
    testImplementation "junit:junit:$junitVersion"
    androidTestImplementation "androidx.test:runner:$runnerVersion"
    androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
}

Lint

Améliorer son code source avec Lint

Lancer lint manuellement :

Ouvrez le rapport.

Analyse du code

Nous pouvons aussi lancer l'analyse du code :

Configurer Lint

Nous avons plusieurs méthodes pour configurer Lint :
  • Utiliser l'ampoule jaune ou rouge.
  • Créer un fichier lint.xml à la racine du projet.
  • Utiliser l'annotation @SuppressLint.

Exemple de fichier lint.xml


<?xml version="1.0" encoding="UTF-8"?>
<lint>
    <!-- Disable the given check in this project -->
    <issue id="IconMissingDensityFolder" severity="ignore" />

    <!-- Ignore the ObsoleteLayoutParam issue in the specified files -->
    <issue id="ObsoleteLayoutParam">
        <ignore path="res/layout/activation.xml" />
        <ignore path="res/layout-xlarge/activation.xml" />
    </issue>

    <!-- Ignore the UselessLeaf issue in the specified file -->
    <issue id="UselessLeaf">
        <ignore path="res/layout/main.xml" />
    </issue>

    <!-- Change the severity of hardcoded strings to "error" -->
    <issue id="HardcodedText" severity="error" />
</lint>

Optimisation

Mettre au point et profiler/monitorer une application.

Nous pouvons étudier la consommation en mémoire, énergie, ... de notre application en utilisant les différents volets du profiler. Il est aussi possible d'utiliser les outils présents directement sur le téléphone en mode développeur.

Optimisation de l'APK avec ProGuard.

Nous allons suivre le tutoriel suivant : Getting Started with ProGuard.
Certains ajustement sont à faire :
  • Il faut penser à ajouter le repository maven { url "https://jitpack.io" } (Page du projet).
  • L'erreur Can’t find referenced class org.sl4j ne semble plus être d'actualité.

Création d'IHM avancées

Internationalisation

Voyons le fichier de ressource strings.xml et son utilisation pour internationaliser notre application.
  • Ajouter une langue.
  • Ajouter des clés.
  • Tester dans l'éditeur.
  • Tester sur l'émulateur.
  • Tester sur le téléphone.

Internationalisation

Editeur texte :

Internationalisation

Editeur graphique :

Internationalisation

Ajout d'une langue :

Internationalisation

Ajout d'une langue résultat :

Internationalisation

Ajout d'une clé :

Construction d'IHM avancées suivant les préconisations Material Design.

La base du design de nos applications est décrite ici : Material Design
Nous allons voir un peu plus en détails certains éléments.

Boutons

Par défaut les Buttons sont des boutons de type com.google.android.material.button.MaterialButton.

Champs texte

Ajoutons un champ texte en utilisant le materiel design. On pourra s'aider du LiveTemplate and_xml_material_design_edittext.

Fonts externes

Nous allons utiliser une font externe pour afficher un texte. Pour cela sélectionnons un TextView, All Attributes, fontFamilly et sélectionnons par exemple smokum.

SeekBar

Ajoutons une barre pour sélectionner la taille de notre texte : une SeekBar.
Exemple de tutoriel.

TextView automatique

Ajoutons maintenant un listener sur notre champ texte qui va afficher le texte avec la font, et la taille sera automatique, pour occuper le plus de place possible sur l'écran.

Bouton flottant

Ajoutons un bouton flottant : Référence

Utilisation des styles

Nous allons voir comment bien organiser la présentation de ses écrans, à travers les fichiers ressources :
  • colors.xml
  • dimens.xml
  • styles.xml

Déclaration des couleurs

Nous allons déclarer des couleurs dans le fichiers colors.xml afin de regrouper ce type de ressources.

Déclaration des tailles

Le fichier dimens.xml nous permet de déclarer les différentes tailles (taille du texte, marge, ...) afin de regrouper ce type de ressources.
Les différentes unités de mesure :
  • dp
  • sp
  • px, in, ...

Déclaration des styles

Nous allons déclarer des styles, qui feront référence aux couleurs et tailles, définies dans les autres fichiers de ressources, et regrouperons toutes les caractéristiques d'affichage des éléments. Il est même possible de faire des héritages de styles, comme en CSS.

<style name="DialogTitle" parent="AppTheme">
    <item name="android:padding">@dimen/dialog_title_text_padding</item>
    <item name="android:textSize">@dimen/dialog_title_text_size</item>
    <item name="android:textStyle">bold</item>
</style>

Aller plus loin

Nous allons voir plus en détails les ConstraintLayouts, voir le PDF ici.
Les ressources graphiques sont disponibles ici.

Nous pouvons suivre les tutoriels suivants pour perfectionner notre technique :

Mécanismes des widgets

Nous allons développer un widget qui permet d'afficher la date courante.
Créons un nouveau projet Widget.
Ajoutons un widget :
Paramétrons le template :
Remplaçons :

CharSequence widgetText = context.getString(R.string.appwidget_text);
Par :

Date current = new Date();
SimpleDateFormat dateFormat = new SimpleDateFormat("dd MMM yyy");
String widgetText = dateFormat.format(current);

Présentation OpenGL/ES.

Pour réaliser des applications graphiques, 3D, ... nous pouvons utiliser OpenGl.
Pour plus de détails, un tutoriel d'initiation en français, et un exemple un peu plus complexe en anglais.

Ajout d'animations

Nous pouvons ajouter facilement une animation à nos layouts, pour cela, créons le projet suivant :
Définissons l'attribut :

TextView tvText;
Définissons le code du bouton :

tvText = findViewById(R.id.text);

findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        tvText.setVisibility(tvText.getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE );
    }
});
Testons.
Ajoutons un attribut dans le layout parent :

android:animateLayoutChanges="true"
Testons de nouveau.

Ajout d'un fond en dégradé

Créons un drawable : background.xml :

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <gradient
            android:angle="90"
            android:endColor="#FFF"
            android:startColor="#CCC" />
</shape>
Ajoutons le en background de notre layout principal.

Transition entre les activités

Il est possible d'ajouter des animations lors du lancement d'une activité, mais il est encore plus parlant pour l'utilisateur d'avoir des animations faisant correspondre les éléments d'un écran, vers le second. Voyons cela en détail : ici.

Haut niveau d'animation

Un exemple d'interface animée : Application de pizza.

tools

Dans un layout, il est possible de changer le prefix android par le préfix tools ce qui impactera seulement l'affichage dans l'IDE, mais sera ignoré dans l'application. Testons par exemple en changeant la visibilité d'un élément :

tools:visibility="gone"

Librairies et services

Nous allons voir plusieurs services proposés par la librairie Google Play Services :
  • Google Maps
  • Géolocalisation
  • LeaderBoard

Utiliser les Google Play Services.

Nous allons par exemple mettre en place une Google Maps.

Donnez un nom à l'application :

Suivez les instructions pour obtenir une clé API :

Modifiez le code pour afficher un point sur notre localisation actuelle (la récupérer sur Google Maps par exemple), exemple d'usage :

@Override
public void onMapReady(GoogleMap googleMap) {
    mMap = googleMap;

    // Add a marker in Startup Marseille and move the camera
    LatLng startupMarseille = new LatLng(43.291590, 5.378210);
    mMap.addMarker(new MarkerOptions().position(startupMarseille).title("Marker at Startup Marseille."));
    mMap.moveCamera(CameraUpdateFactory.zoomTo(19));
    mMap.moveCamera(CameraUpdateFactory.newLatLng(startupMarseille));

}

Affichage position courante

Nous allons maintenant afficher la position courante de l'utilisateur en utilisant le GPS. Pour cela nous allons suivre les étapes suivantes :
Ajouter la librairie à notre graddle :

implementation 'com.google.android.gms:play-services-location:17.0.0'
Définir un attribut de type Marker pour garder une référence sur le marker de la position courante :

private Marker currentPosition;
Récupérer la position du smartphone, pour pouvoir l'afficher :

FusedLocationProviderClient client = LocationServices.getFusedLocationProviderClient(this);

// Get the last known location
client.getLastLocation().addOnCompleteListener(this, new OnCompleteListener<Location>() {
    @Override
    public void onComplete(@NonNull Task<Location> task) {
        // TODO
    }
});

Affichage du marker

Complétons le code :

LatLng currentLocation = new LatLng(task.getResult().getLatitude(), task.getResult().getLongitude());
currentPosition = mMap.addMarker(new MarkerOptions().position(currentLocation).title("Current position").icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_GREEN)));

Gestion des autorisations

Nous avons l'erreur suivante :

com.google.android.gms.tasks.RuntimeExecutionException: java.lang.SecurityException: Client must have ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION permission to perform any location operations.
Comment la corriger ?
  • Activer directement dans les paramètres de l'application (pour tester).
  • Demander la permission dynamiquement.

Erreur de position sur l'émulateur

Si vous rencontrez un problème avec la localisation, lancez l'application Maps, et le problème devrait être corrigé.

LeaderBoard

Nous pouvons utiliser le système de gestion des scores, des accomplissements proposé par Google, tous les détails : ICI.

Récapitulatif des services

La liste des services disponibles est accessible ICI. La liste des services disponibles est accessible ICI.

Intégrer des bibliothèques tierces à un projet Android.

Nous allons installer et utiliser une bibliothèque tierce dans un projet. Par exemple un sélecteur de couleur.
Suivez les consignes de la librairie, avec les instructions supplémentaires suivantes :
  • Déclenchez le sélecteur sur le click d'un bouton.
  • Nous modifierons la couleur de fond de l'application.
  • La couleur initiale du sélecteur doit être la couleur du fond de l'application.
Récupération du code couleur du fond de l'application :

int color = Color.RED;
Drawable background = rootView.getBackground();
if (background instanceof ColorDrawable)
    color = ((ColorDrawable) background).getColor();

Intégration d'un scanner de QRCode

Voir le PDF ici.

Intégration d'une push notification

Nous allons utiliser les services de OneSignal, pour cela nous allons créer un compte sur OneSignal, et suivre les étapes suivantes ...

Simplifier l'accès à des ressources REST avec Retrofit.

Nous allons suivre le tutoriel suivant : Using Retrofit 2.x as REST client - Tutorial.
Une version plus simple en Kotlin.

Maîtriser le chargement des images avec Picasso.

Nous allons intégrer la librairie Picasso à notre projet.
  • Ajoutons une ImageView à notre projet.
  • Ajoutons la librairie dans le gradle.
  • Ajoutons la permission INTERNET.
  • Utilisons la librairie comme indiqué dans la documentation.
Ce qui nous donnera le code suivant (avec debug activé) :

ImageView imageView = findViewById(R.id.image);
Picasso.get().setLoggingEnabled(true);
Picasso.get().setIndicatorsEnabled(true);
Picasso.get().load("https://upload.wikimedia.org/wikipedia/commons/d/da/Internet2.jpg").into(imageView);

Glide

Procédons de la même manière avec la librairie Glide.
Ce qui nous donnera le code suivant :

Glide.with(this).load("https://upload.wikimedia.org/wikipedia/commons/d/da/Internet2.jpg").into(imageView);

L'injection de dépendances (Dagger).

Nous allons mettre en place une librairie permettant l'injection de dépendances : Dagger.

Dans un premier temps, pour bien comprendre l'injection de dépendances, nous allons suivre le tutoriel suivant :
https://developer.android.com/training/dependency-injection.

Dans un second temps nous allons suivre le tutoriel suivant :
Using Dagger 2 for dependency injection in Android.
D'autres tutoriels sont disponibles : ici, ou ici.
Ma préférence va à Koin.

Custom View

Définition

Nous pouvons définir des vues que nous allons dessiner sur un Canvas. Pour cela nous allons suivre les étapes suivantes :
  • Création d'une custom view à partir du template.
  • Personnalisation du modèle.

Création du modèle

Pour créér le modèle, nous allons utiliser le menu :
File / New / Ui Component / Custom View

Utilisation du modèle

Nous allons suivre les étapes suivantes :
  • Utilisation de la vue custom.
  • Détail des attributs.
  • Détail du code.

La vue à obtenir

Nous souhaitons obtenir la vue suivante :

Exercice

Écrivez le code qui permet de dessiner la vue.
Nous utiliserons les éléments suivants :
  • Paint.
  • translate.
  • rotate.
  • setColor.
  • Path.
  • drawArc.

Solution

On définit l'attribut suivant :

private int mAngle = 0;
	
Et le setter associé :

public void setmAngle(int mAngle) {
        this.mAngle = mAngle;
        invalidate();
}
	

Solution

Et enfin le code pour dessiner la vue :

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    int cx = getWidth() / 2;
    int cy = getHeight() / 2;

    Paint paint = new Paint();

    // Définir la flèche
    Path northPath = new Path();
    northPath.moveTo(0, -cy);
    northPath.lineTo(-10, 0);
    northPath.lineTo(10, 0);
    northPath.close();

    // Dessiner le cercle de fond
    // Définir le rectangle contenant le cercle que l'on va dessiner
    RectF pitchOval = new RectF(0, 0, getWidth(), getHeight());

    paint.setColor(Color.BLACK);
    canvas.drawArc(pitchOval, 0, 360, false, paint);

    canvas.translate(cx, cy);
    canvas.rotate(mAngle);
    paint.setColor(Color.RED);
    canvas.drawPath(northPath, paint);
}

Solution complète


public class MyView extends View {
    private int mAngle = 0;

    public MyView(Context context) {
        super(context);
    }

    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        int cx = getWidth() / 2;
        int cy = getHeight() / 2;

        Paint paint = new Paint();

        // Définir la flèche
        Path northPath = new Path();
        northPath.moveTo(0, -cy);
        northPath.lineTo(-10, 0);
        northPath.lineTo(10, 0);
        northPath.close();

        // Dessiner le cercle de fond
        // Définir le rectangle contenant le cercle que l'on va dessiner
        RectF pitchOval = new RectF(0, 0, getWidth(), getHeight());

        paint.setColor(Color.BLACK);
        canvas.drawArc(pitchOval, 0, 360, false, paint);

        canvas.translate(cx, cy);
        canvas.rotate(mAngle);
        paint.setColor(Color.RED);
        canvas.drawPath(northPath, paint);
    }

    public void setmAngle(int mAngle) {
        this.mAngle = mAngle;
        invalidate();
    }
}

Utilisation des capteurs

Mise en œuvre de capteurs.

Nous allons développer une application qui va éteindre l'écran quand on est proche de l'écran, et le rallumer quand on est à distance. Pour cela plusieurs étapes :
  • Nous allons lister les capteurs.
  • Récupérer le capteur de proximité.
  • S'enregistrer sur les changements.
  • Réagir aux changements.

Liste des capteurs.

Pour lister tous les capteurs disponibles sur notre smartphone, utilisons le code suivant :

SensorManager sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
List<Sensor> liste = sensorManager.getSensorList(Sensor.TYPE_ALL);
if (BuildConfig.DEBUG) {
    for (Sensor currentSensor : liste) {
        Log.d(TAG, "onCreate sensor " + currentSensor);
    }
}

Liste des capteurs.

Pour lister tous les capteurs disponibles sur notre smartphone, utilisons le code suivant :

val sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
var liste = sensorManager.getSensorList(Sensor.TYPE_ALL)
if (BuildConfig.DEBUG) {
    for (currentSensor in liste) {
        Log.d(TAG, "onCreate sensor $currentSensor")
    }
}

Récupérons le capteur de proximité


private Sensor mProximitySensor = null; // En attribut de classe.
mProximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);

Récupérons le capteur de proximité


var mProximitySensor:Sensor? = null // En attribut de classe.
mProximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY)

Enregistrons nous, sur les changements

Dans un premier temps nous récupérons le manager des capteurs :

private SensorManager mSensorManager = null; // En attribut de classe.
mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
Puis nous nous enregistrons sur les changements uniquement quand l'application est active :

@Override
protected void onResume() {
    super.onResume();
    mSensorManager.registerListener(mSensorEventListener, mProximitySensor, SensorManager.SENSOR_DELAY_NORMAL);
}

@Override
protected void onPause() {
    super.onPause();
    mSensorManager.unregisterListener(mSensorEventListener, mProximitySensor);
}

Le listener

Le listener qui va réagir aux changements est définis par :

final SensorEventListener mSensorEventListener = new SensorEventListener() {
    public void onAccuracyChanged(Sensor sensor, int accuracy) {
        // Que faire en cas de changement de précision ?
    }

    public void onSensorChanged(SensorEvent sensorEvent) {
        if(BuildConfig.DEBUG){
            Log.d(TAG, "onSensorChanged " + sensorEvent.values.length);
            Log.d(TAG, "onSensorChanged " + sensorEvent.values[0]);
        }
    }
};
Il ne nous reste plus qu'à implémenter notre code fonctionnel.
Exemple plus poussé d'utilisation du capteur de proximité pour éteindre l'écran.

Enregistrons nous, sur les changements

Dans un premier temps nous récupérons le manager des capteurs :

var mSensorManager: SensorManager? = null // En attribut de classe.
mSensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
Puis nous nous enregistrons sur les changements uniquement quand l'application est active :

override fun onResume() {
    super.onResume()
    mSensorManager?.registerListener(
        mSensorEventListener,
        mProximitySensor,
        SensorManager.SENSOR_DELAY_NORMAL
    )
}

override fun onPause() {
    super.onPause()
    mSensorManager?.unregisterListener(mSensorEventListener, mProximitySensor)
}

Le listener

Le listener qui va réagir aux changements est définis par :

val mSensorEventListener: SensorEventListener = object : SensorEventListener {
    override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {
        // Que faire en cas de changement de précision ?
    }

    override fun onSensorChanged(sensorEvent: SensorEvent) {
        if (BuildConfig.DEBUG) {
            Log.d(TAG, "onSensorChanged " + sensorEvent.values.size)
            Log.d(TAG, "onSensorChanged " + sensorEvent.values[0])
        }
    }
}
Il ne nous reste plus qu'à implémenter notre code fonctionnel.
Exemple plus poussé d'utilisation du capteur de proximité pour éteindre l'écran.

Exercice

Utiliser le capteur magnétique pour afficher le nord, en utilisant la vue custom précédemment développée.

Exemple complet de jeux

Un exemple plus complet de jeux utilisant les capteurs est disponible ici.
Un exemple reprenant la base de ce que l'on à vu ici.
Un exemple simple pour afficher les valeurs de l'accéléromètre ici.

Paramétrage dans le simulateur des capteurs.

Paramétrage dans le simulateur des capteurs.

Paramétrage dans le simulateur des capteurs.

ContentProvider et Services

Définition

Le content provider permet de partager avec d'autres applications des données, de manière standardisée.
Plus de détails, ici.

Utilisation d'un content provider

Nous allons utiliser un content provider, par exemple le MediaStore. Créons un nouveau projet ContentProviderClient, et listons les sons disponibles sur notre téléphone :

Uri media = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
String[] projection = { MediaStore.Audio.Media._ID,           // 0
        MediaStore.Audio.Media.ARTIST,        // 1
        MediaStore.Audio.Media.TITLE,         // 2
        MediaStore.Audio.Media.ALBUM_ID,      // 3
        MediaStore.Audio.Media.ALBUM,         // 4
        MediaStore.Audio.Media.DATA,          // 5
        MediaStore.Audio.Media.DISPLAY_NAME,  // 6
        MediaStore.Audio.Media.DURATION };    // 7

String selection = MediaStore.Audio.Media.IS_MUSIC + " != 0";

Cursor cursor = getContentResolver().query(media,projection,selection,null,null);
while(cursor.moveToNext()){
    if(BuildConfig.DEBUG){
        Log.d(TAG, "onCreate " +
                        cursor.getString(0) + " " +
                cursor.getString(1) + " " +
                cursor.getString(2) + " " +
                cursor.getString(3) + " " +
                cursor.getString(4) + " " +
                cursor.getString(5) + " " +
                cursor.getString(6) + " " +
                cursor.getString(7) + " ");
    }
}

Un autre content provider

Nous allons maintenant utiliser le content provider du dictionnaire afin de nous familiariser avec les actions disponibles ( CRUD : Create Read, Update et Delete). Pour cela nous allons passer par plusieurs étapes :
  • Créer un AVD en API 22.
  • Ajouter des entrées au dictionnaire.
  • Utiliser le projet ContentProviderClient.
  • Demander la permission.
  • Interagir avec le content provider.

Créer un AVD en API 22

Le content provider du UserDictionary n'étant pas disponible à partir de l'API 23, nous allons créér une AVD en API 22.

Ajouter des entrées au dictionnaire

Afin d'avoir des données à lister, nous allons ajouter des entrées au dictionnaire personnel.

Utiliser le projet ContentProviderClient

Nous allons réutiliser le précédent projet, tout simplement.

Demander la permission

Ajoutons à notre AndroidManifest.xml la permission :

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

Interagir avec le content provider

Nous allons pouvoir utiliser le content provider UserDictionary.
Commençons par lister les entrées (LiveTemplate and_content_provider_user_dictionary_read) :

// A "projection" defines the columns that will be returned for each row
String[] projection = {
        UserDictionary.Words._ID,    // Contract class constant for the _ID column name
        UserDictionary.Words.WORD,   // Contract class constant for the word column name
        UserDictionary.Words.LOCALE  // Contract class constant for the locale column name
};

// Defines a string to contain the selection clause
String selectionClause = null;

// Initializes an array to contain selection arguments
String[] selectionArgs = null;

// Defines a string to order the result
String sortOrder = null;

// Does a query against the table and returns a Cursor object
Cursor mCursor = getContentResolver().query(
        UserDictionary.Words.CONTENT_URI,  // The content URI of the words table
        projection,                       // The columns to return for each row
        selectionClause,                  // Either null, or the word the user entered
        selectionArgs,                    // Either empty, or the string the user entered
        sortOrder);                       // The sort order for the returned rows

// Some providers return null if an error occurs, others throw an exception
if (null == mCursor) {
    /*
     * Insert code here to handle the error. Be sure not to use the cursor! You may want to
     * call android.util.Log.e() to log this error.
     *
     */
    if (BuildConfig.DEBUG) {
        Log.d(TAG, "onCreate CURSOR NULL");
    }
    // If the Cursor is empty, the provider found no matches
} else if (mCursor.getCount() < 1) {

    /*
     * Insert code here to notify the user that the search was unsuccessful. This isn't necessarily
     * an error. You may want to offer the user the option to insert a new row, or re-type the
     * search term.
     */
    if (BuildConfig.DEBUG) {
        Log.d(TAG, "onCreate CURSOR EMPTY");
    }

} else {
    // Insert code here to do something with the results
    try {
        while (mCursor.moveToNext()) {
            if (BuildConfig.DEBUG) {
                Log.d(TAG, "onCreate " +
                        mCursor.getLong(0) + " " +
                        mCursor.getString(1) + " " +
                        mCursor.getString(2) + " "
                );
            }
        }
    } finally {
        mCursor.close();
    }
}

Écrire dans le Content Provider

Maintenant écrivons dans le content provider :

// Defines a new Uri object that receives the result of the insertion
Uri newUri;

// Defines an object to contain the new values to insert
ContentValues newValues = new ContentValues();

/*
* Sets the values of each column and inserts the word. The arguments to the "put"
* method are "column name" and "value"
*/
newValues.put(UserDictionary.Words.APP_ID, "example.user");
newValues.put(UserDictionary.Words.LOCALE, "en_US");
newValues.put(UserDictionary.Words.WORD, "insert");
newValues.put(UserDictionary.Words.FREQUENCY, "100");

newUri = getContentResolver().insert(
UserDictionary.Words.CONTENT_URI,   // the user dictionary content URI
newValues                          // the values to insert
);
if (BuildConfig.DEBUG) {
    Log.d(TAG, "onCreate " + newUri);
}

Mise à jour

La mise à jour des informations est similaire :

// Defines an object to contain the updated values
ContentValues updateValues = new ContentValues();

// Defines selection criteria for the rows you want to update
String selectionClause = UserDictionary.Words.LOCALE +  " LIKE ?";
String[] selectionArgs = {"en_%"};

// Defines a variable to contain the number of updated rows
int rowsUpdated = 0;

/*
* Sets the updated value and updates the selected words.
*/
updateValues.putNull(UserDictionary.Words.LOCALE);

rowsUpdated = getContentResolver().update(
    UserDictionary.Words.CONTENT_URI,   // the user dictionary content URI
    updateValues,                      // the columns to update
    selectionClause,                   // the column to select on
    selectionArgs                      // the value to compare to
);
Quand nous regardons dans le dictionnaire, nous voyons que maintenant nos mots sont disponibles pour toutes les langues :

Effacer des données

Nous allons maintenant supprimer des données :

// Defines selection criteria for the rows you want to delete
String selectionClause = UserDictionary.Words.WORD + " LIKE ?";
String[] selectionArgs = {"%insert%"};

// Defines a variable to contain the number of rows deleted
int rowsDeleted = 0;

// Deletes the words that match the selection criteria
rowsDeleted = getContentResolver().delete(
    UserDictionary.Words.CONTENT_URI,   // the user dictionary content URI
    selectionClause,                   // the column to select on
    selectionArgs                      // the value to compare to
);

Autres contents providers

De nombreux contents providers sont disponibles tels que :

Création d'un Content Provider

Les étapes

Nous allons maintenant créer un content provider. Pour cela nous allons suivre plusieurs étapes :
  • Créer un projet ContentProvider.
  • Implémenter rapidement la méthode query.
  • Tester dans le projet ContentProviderClient la bonne réaction.
  • Implémenter une base de donnée pour le Content Provider.

Implémentation rapide

La méthode query doit retourner un Cursor, implémentons rapidement la création d'un Cursor contenant des Users.

@Override
public Cursor query(Uri uri, String[] projection, String selection,
                    String[] selectionArgs, String sortOrder) {

    MatrixCursor cursor = new MatrixCursor(new String[]{"name", "firstname"});
    cursor.newRow()
            .add("name", "SALAUN")
            .add("firstname", "Tristan");

    cursor.newRow()
            .add("name", "SNOW")
            .add("firstname", "John");
    return cursor;
    }

Test dans ContentProviderClient

Ouvrons le projet ContentProviderClient, et ajoutons le code pour interroger notre nouveau ContentProvider :

Cursor cursor = getContentResolver().query(Uri.parse("content://com.exemple.contentprovider.provider"), null, null, null, null);
if(cursor.moveToFirst()) {
    StringBuilder strBuild=new StringBuilder();
    while (!cursor.isAfterLast()) {
        strBuild.append("\n"+cursor.getString(0)+ "-"+ cursor.getString(1));
        cursor.moveToNext();
    }
    if(BuildConfig.DEBUG){
        Log.d(TAG, "onCreate " + strBuild);
    }
}

Base de donnée avec Room

Implémentons maintenant une vrai base de donnée en utilisant Room.
Pour cela je vais utiliser le Live Template and_db_room_00_documentation et écrire les classes pour gérer ma base de donnée. La base de notre Content Provider sera l'entity suivante :

public class UserEntity {
    public String name;
    public String firstname;
}

Classe de contrat

Afin de rendre notre content provider plus accessible à des développeurs tiers, nous allons écrire une classe de contrat définissant toutes les informations nécessaires pour accéder à notre content provider :

import android.net.Uri;
import android.provider.BaseColumns;

public final class UserContract {
    // The Authority
    public static final String AUTHORITY = "com.exemple.contentprovider.provider";

    // The path to the data… and explain
    public static final String PATH_TO_DATA = "users"; //Vous pouvez déclarer plusieurs paths (les paths utilisent les /)


    public interface MyDatas extends BaseColumns {

        // The URI and explain, with example if you want
        public static final Uri CONTENT_URI = Uri.parse("content://" + UserContract.AUTHORITY + "/" + UserContract.PATH_TO_DATA);

        // My Column ID and the associated explanation for end-users
        public static final String KEY_COL_ID = _ID; // Mandatory

        // My Column Name and the associated explanation for end-users
        public static final String KEY_COL_NAME = "name";

        // My Column First Name and the associated explanation for end-users
        public static final String KEY_COL_FIRSTNAME = "firstName";

        // The index of the column ID
        public static final int ID_COLUMN = 1;
        // The index of the column NAME
        public static final int NAME_COLUMN = 2;
        // The index of the column FIRST NAME
        public static final int FIRSTNAME_COLUMN = 3;
    }
}

Intégration au content provider

Nous allons intégrer la base de donnée à notre content provider.
Implémentation de la méthode afin de récupérer une instance de la base de donnée :

public boolean onCreate() {
    Context context = getContext();
    userDao = AppDatabase.getDatabase(context).userDao();
    if (userDao != null) {
        return true;
    }
    return false;
}

Query

Dans une première étape nous retournerons l'entièreté de la base de donnée avec le code suivant :

public Cursor query(Uri uri, String[] projection, String selection,
                    String[] selectionArgs, String sortOrder) {
    return userDao.getCursorAll();
}

Insert

Une implémentation possible serait :

 public Uri insert(Uri uri, ContentValues values) {
    UserEntity currentUserEntity = new UserEntity();
    if (values.containsKey(UserContract.MyDatas.KEY_COL_FIRSTNAME)) {
        currentUserEntity.firstname = values.getAsString(UserContract.MyDatas.KEY_COL_FIRSTNAME);
    }
    if (values.containsKey(UserContract.MyDatas.KEY_COL_NAME)) {
        currentUserEntity.name = values.getAsString(UserContract.MyDatas.KEY_COL_NAME);
    }
    long rowID = userDao.insert(currentUserEntity);
    if (rowID > 0) {
        Uri _uri = ContentUris.withAppendedId(UserContract.MyDatas.CONTENT_URI, rowID);
        getContext().getContentResolver().notifyChange(_uri, null);
        return _uri;
    }
    throw new SQLiteException("Failed to add a record into " + uri);
}

Les services

Cycle de vie des services.

Intent service.

Intent service.

Service bindé.

Arrière-plan et premier plan

Le choix

Pour choisir le bon mode pour lancer une tâche de fond, suivons les indications de ce schéma :

Lier services et activités.

Nous allons utiliser EventBus pour faire communiquer nos services avec notre Activité.
Dans un premier temps commençons par un tutoriel assez simple, mais regroupant tous les concepts, ici.
Nous trouverons un peu plus de détails sur EventBus ici.

Utiliser des threads depuis un service.

Commençons par utiliser le code suivant dans notre MainActivity :

for (int i = 0; i < 10; i++) {
    if (BuildConfig.DEBUG) {
        Log.d(TAG, "run background " + i);
    }
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}
Que remarquons nous ?

Plaçons maintenant le code dans notre service, que remarquons nous ?

Countdown simple

Utilisons EventBus, et un service intent pour lancer notre countdown avec l'interface graphique suivante :

Définir des alarmes.

Nous pouvons aussi déclencher des actions à un temps (heure/jour) donné.
Soit en utilisqnt un Intent :
Un exemple ici.
Nous pouvons aussi utiliser le Live Template : and_intent_set_task.
Modifions l'exemple pour afficher une notification, qui sera plus facile à visualiser. En utilisant le code vu précédemment.

Soit en utilisant du code :
La référence se trouve ici et nous trouverons un autre exemple ici.

Travaux pratiques

Réalisons maintenant une application qui va nous permettre de gérer un décompte avec les fonctionnalités suivantes :
  • Initialiser la valeur.
  • Lancer.
  • Arrêter.
  • Remettre à zéro.

Countdown évolué

Utilisons maintenant un service bindé pour pouvoir interagir avec notre service via l'interface suivante :

Tester une application Android

Présentation des outils

Il existe plusieurs outils adaptés aux différents types de tests. Les différents types de tests :
  • Test monkey.
  • monkeyrunner.
  • Test unitaire.
  • Test d'intégration.
  • Test fonctionnels de bout en bout.

Tests monkey

Un des test les plus simple à mettre en oeuvre et qui remonte souvent des bugs est le test du singe (monkey). On le lance de la manière suivante :

adb shell monkey -p your.package.name -v 500

monkeyrunner

Nous pouvons écrire des tests simples, par exemple pour automatiser la génération de captures d'écrans avec un script monkeyrunner qui va installer l'application, réaliser des actions (tout en effectuant des captures d'écran), pour enfin effacer l'application testée.
  • La référence ici.
  • Astuces pour lancer monkeyrunner ici.
Voyons tout cela en détail dans le script screenshoot_app.py :

# -*- coding: utf-8 -*-

# https://developer.android.com/studio/test/monkeyrunner
# https://medium.com/@soonsantos/guide-to-run-monkeyrunner-e9363f36bca4

# Imports the monkeyrunner modules used by this program
from com.android.monkeyrunner import MonkeyRunner, MonkeyDevice

# Connects to the current device, returning a MonkeyDevice object
device = MonkeyRunner.waitForConnection()

MonkeyRunner.alert(u"\n\nMonkeyRunnerScript\nMerci de vérifier que :\n\
        \t - Le téléphone est bien installé.\n\
        \n\n\n\n\n\
        Appuyez sur 'Continuer' POUR REPENDRE L'EXECUTION...","MonkeyRunner","Continuer");

print("Start")

# Presses the Home button
device.press('KEYCODE_HOME',MonkeyDevice.DOWN_AND_UP)

# sets a variable with the package's internal name
package = 'fr.salaun.tristan.todelete'

# sets a variable with the name of an Activity in the package
activity = 'fr.salaun.tristan.todelete.MainActivity'


apk_path = device.shell('pm path ' + package)
if apk_path.startswith('package:'):
    print "XXXYY already installed."
else:
    print "XXXYY app is not installed, installing APKs..."
    device.installPackage('D:/path to apk/yourapp.apk')

# sets the name of the component to start
runComponent = package + '/' + activity

# Runs the component
device.startActivity(component=runComponent)

# Wait a bit
MonkeyRunner.sleep(1)

# Takes a screenshot
print("Take screenshoot of screen")
result = device.takeSnapshot()

# Writes the screenshot to a file
result.writeToFile('screenshoot.png','png')


# ==================================================
# Go out of the application
device.press("KEYCODE_BACK", MonkeyDevice.DOWN_AND_UP)

Tests unitaires

Ils permettent de tester les méthodes de chaque classe, séparément.

Fonctions spéciales

  • setup appelée avant chaque test
  • tearDown appelée après chaque test

Assertions

  • assertEquals(a,b)
  • assertTrue(a) et assertFalse(a)
  • assertSame(a,b) et assertNotSame(a,b)
  • assertNull(a) et assertNotNull(a)
  • fail(message)

Bonnes pratiques

  • Écrire les test en même temps que le code.
  • Tester uniquement ce qui peut vraiment provoquer des erreurs.
  • Exécuter ses tests aussi souvent que possible, idéalement après chaque changement de code.
  • Écrire un test pour tout bogue signalé (même s’il est corrigé).
  • Ne pas tester plusieurs méthodes dans un même test : JUnit s’arrête à la première erreur.
  • Attention, les méthodes privées ne peuvent pas être testées !

Simulation d'interactions utilisateur avec Espresso.

Nous allons voir plus en détails la mise en place des tests.

AndroidJUnit4 déprécié

Pour corriger le message :

Ajoutons la librairie :

androidTestImplementation 'androidx.test.ext:junit:1.1.1'
Et modifions l'import correspondant.

Test Content provider

Regardons plus en détail le projet ContentProvider et mettons en place des tests relatifs à notre base de donnée.

uiautomatorviewer

Afin de nous aider à rédiger un test, nous pouvons utiliser uiautomatorviewer.bat (dans C:\Users\USER_NAME\AppData\Local\Android\Sdk\tools\bin pour identifier les id des éléments composants l'interface graphique des applications tierces.

Test Suite

Regardons maintenant plus en détail le projet TestSuite nous mettrons en place des tests sur notre application d'envoi de SMS.

Tests enregistrés

Nous pouvons mettre en place des tests de manière graphique sans avoir besoin d'écrire de code.

Utilisation de Cloud Test Lab.

Nous allons suivre le tutoriel ici

La référence est disponible ici : https://firebase.google.com/docs/test-lab.

Send SMS

Présentation

Revoyons certains principes vu dans la formation, nous allons réaliser une application qui permet d'envoyer des SMS, et informer l'utilisateur de l'état d'avancée des envois/réceptions. Nous allons encore une fois grandement nous aider des Live Templates mis à disposition.

GUI

Mettons en place au minimum, l'interface suivante :

Contact picker

Implémentons la sélection des contacts, en deux étapes :
  • L'implémentation du click pour sélectionner le contact.
  • La récupération du contact lors du retour de la sélection.
Implémentons le callback pour le click du bouton, et utilisons le Live Template : and_intent_select_contact pour implémenter la méthode.
Suivons les instructions.

Send SMS

Implémentons l'envoi des SMS, en suivant les étapes suivantes :
  • Récupération des références vers les EditText.
  • Implémentation du click pour envoyer le SMS.
  • Ajoutons la permission pour envoyer les SMS.
  • Implémentation des classes de BroadcastReceiver.

Récupération des EditText

Déclarons nos deux variables d'instances de classe :

private EditText editTextNumber, editTextText;
Récupérons leur valeur :

editTextNumber = findViewById(R.id.activity_main_editText_number);
editTextText = findViewById(R.id.activity_main_editText_message);

Implémentation du code

Implémentons le code pour envoyer les SMS : utilisons, hors d'une méthode, le Live Template : and_send_sms_01_method.
Suivons les instructions.
Implémentons le callback pour le click du bouton avec :

sendSMS(getApplicationContext(), editTextNumber.getText().toString(), editTextText.getText().toString());

Permissions

Utilisons le Live Template and_permission_single.
Déplaçons le code situé dans le click du bouton, vers la méthode actionToBeCalled(), et remplaçons le par l'appel à la méthode actionToBeCalled().
Testons notre code.

Correction des erreurs

Si nous avons l'erreur :

java.lang.SecurityException: Sending SMS message: uid XXXXX does not have android.permission.SEND_SMS.
Nous avons :
  • Soit oublié d'ajouter la permission dans le Manifest.xml.
  • Soit oublié de demander la permission dynamiquement (Android 6.0+).

Allons plus loin

Améliorons l'UX de notre application :
  • Passons au Material design sur les EditText et les Buttons.
  • Effectuons des tests sur les valeurs des champs.
  • Mémorisons les dernières valeurs utilisées.
  • Mettons en place un EventBus pour communiquer des BroadcastReceivers vers la MainActivity.

Annexes

Raccourcis claviers utiles

Touches Action
Shift deux fois rapidement. Ouvrir la recherche.
F2 Aller jusqu'à la prochaine erreur.
Ctrl + Y Effacer une ligne.
Shift + F6 Renommer.
Shift + Ctrl + Flèche haut/bas Déplacer une ligne de code.
Ctrl + Alt + L Indenter le code.
Alt + F7 Find usage.
Ctrl + Alt + O Organiser les imports.
Ctrl (maintenu) + click souris Ouvrir la source cliquée.
Ctrl + D Dupliquer la ligne.

Le mode démo

Sur le téléphone, allons dans le menu développeur, Mode de démonstration de l'interface du système et activons le mode démonstration et affichons le :

Quels changements observons nous ?

LivesTemplates

Nous avons à notre disposition plusieurs LiveTemplates, tels que :
  • and_tts : pour activer une synthèse vocale.
  • and_intent_voice_recognition : pour activer une reconnaissance vocale.

TODO / FIXME

En commentaires, les mots clés TODO et FIXME sont interprétés par l'éditeur :

Universal ADB Drivers

Un outil indispensable sous Windows pour installer les drivers manquants pour que le système reconnaisse son téléphone/tablette :
Universal ADB Drivers

Les gestures

Nous pouvons mettre en place la reconnaissance de gesture (motifs) définies par le développeur.

Le swipe

Nous pouvons aussi réagir à des mouvements simples : le swipe (haut, bas, droite et gauche).

Références utiles

Jetpack Compose

  • Jetpack Compose
  • Jetpack Compose tutorial
  • La passerelle Web et JS

    Nous pouvons mettre en place une passerelle JS pour appeler du code natif Android depuis une page Web affichée dans une webview :

    Contact Picker

    Exemple de sélection d'un contact dans notre répertoire téléphonique.
    Dans le clickListener d'un bouton, nous allons utiliser le LiveTemplate suivant : and_intent_select_contact, et suivre les instructions.

    Scripts batch/bash

    Voyons ensemble sur les prochaines diapositives des scripts qui vont nous aider à développer/tester/installer plus rapidement nos applications.

    clear.bat, pour effacer les données d'une application :
    
    echo Waiting for device on USB
    adb wait-for-usb-device
    echo Device found
    
    echo Clear APP Tristan
    adb shell pm clear fr.salaun.tristan.android.myapplication
    

    delete.bat, pour effacer une application :
    
    echo Waiting for device on USB
    adb wait-for-usb-device
    echo Device found
    
    echo "Delete APP Tristan"
    adb uninstall fr.salaun.tristan.android.myapplication
    

    Scripts batch/bash

    install.sh, pour installer plusieurs applications d'un répertoire :
    
    #!/bin/bash
    
    # example : ./install_all_apk.sh /c/apk/
    
    echo Waiting for device on USB
    adb wait-for-usb-device
    echo Device found
    
    echo "The path to use : $1"
    
    for filename in $1/*.apk; do
       echo "installing $filename"
       adb install -r -d -g $filename
    done
    

    Scripts batch/bash

    install_apk_all_devices.bat, pour installer une applications sur plusieurs devices :
    
    @echo off
    
    SETLOCAL ENABLEDELAYEDEXPANSION
    :: INSTALL ON ALL ATTACHED DEVICES ::
    FOR /F "tokens=1,2 skip=1" %%A IN ('adb devices') DO (
    	start "Sub" install_apk_all_devices_installer.bat %%A
    )
    ENDLOCAL
    
    :EOF
    

    Scripts batch/bash

    install_apk_all_devices_installer.bat, qui est appelé par le script précédent :
    
    @echo off
    SET ARGUMENTS=%~1
    
    if "%ARGUMENTS%" == "" (
        GOTO EOF
    )
    
    adb -s %ARGUMENTS% uninstall fr.salaun.tristan.android.myapplication
    
    echo Waking up the device %ARGUMENTS%.
    adb -s %ARGUMENTS% shell input keyevent KEYCODE_WAKEUP
    
    REM Sleep for 2 seconds
    echo Wait a bit
    ping -n 5 127.0.0.1>nul
    
    echo Unlock the screen
    adb -s %ARGUMENTS% shell input keyevent 82
    
    echo Install the application.
    adb -s %ARGUMENTS% install -r -t D:\apk\debug\fr.salaun.tristan.android.myapplication-debug.apk
    
    if errorlevel 1 goto performdeletebefore
    echo Install Success!
    goto launch
    :performdeletebefore
    echo Something bad happened during install.
    echo Try to uninstall first.
    adb -s %ARGUMENTS% uninstall fr.salaun.tristan.android.myapplication
    adb -s %ARGUMENTS% install -r -t D:\apk\debug\fr.salaun.tristan.android.myapplication-debug.apk
    
    if errorlevel 1 goto ERROR
    
    :launch
    echo Launch the application.
    adb -s %ARGUMENTS% shell am start -n fr.salaun.tristan.android.myapplication/fr.salaun.tristan.android.myapplication.MainActivity
    
    :EOF
    exit
    
    :ERROR
    echo "Exit with error"
    

    Utilisation du JSON avec GSON.

    Créons le projet suivant : com.example.jsonmyapplication.
    Ajoutons la dépendance dans le build.graddle (app) :
    
    implementation 'com.google.code.gson:gson:2.8.6'
    
    Puis créons les classes suivantes :
    
    data class Book(var id: String, var name: String, var author: String, var genre: String, var numpages: Int, var releaseDate: String, var cover: String)
    
    
    data class BookList (val bookList: List<Book>)
    
    Déclarons une liste de livres :
    
    var list = arrayListOf(
    Book(
        id = "01fsEF", name = "1984",
        author = "George Orwell",
        genre = "Fiction dystopique",
        numpages = 376,
        releaseDate = "1949",
        cover = "1984.png"
    ),
    Book(
        id = "3H1J0n",
        name = "Le Meilleur des mondes",
        author = "Aldous Huxley",
        genre = "Science-fiction",
        numpages = 285,
        releaseDate = "1932",
        cover = "meilleur-des-mondes.jpg"
    ),
    Book(
        id = "MbtsI7",
        name = "Malevil",
        author = "Robert Merle",
        genre = "Littératures de l'imaginaire",
        numpages = 541,
        releaseDate = "1972",
        cover = "malevil.jpg"
    )
    )
    
    var bookList = BookList (list)
    

    Utilisation du JSON avec GSON (suite).

    Définissons une méthode utilitaire pour formater le JSON :
    
    fun jsonToPrettyFormat(jsonString: String?): String? {
        val json = JsonParser.parseString(jsonString).asJsonObject
        val gson = GsonBuilder()
            .serializeNulls()
            .disableHtmlEscaping()
            .setPrettyPrinting()
            .create()
        return gson.toJson(json)
    }
    

    Utilisation du JSON avec GSON (suite et fin).

    Mettons en place la "Serialization" :
    
    // Serialization
    val gson = Gson()
    val listType: Type = object : TypeToken<BookList?>() {}.type
    val jsonResult: String = gson.toJson(bookList, listType)
    Log.d(TAG, "onCreate jsonResult = $jsonResult")
    
    Log.d(TAG, "onCreate jsonResult = ${jsonToPrettyFormat(jsonResult)}")
    
    Et enfin testons la "Déserialization" :
    
    // Deserialization
    val bookList2: BookList = Gson().fromJson(jsonResult, listType)
    Log.d(TAG, "onCreate bookList2 = ${bookList2}")
    

    Utiliser le presse-papier

    Créons un projet simple (en Java ou Kotlin), donnons un id au TextView déjà présent, et dans le code de l'Activity principale :
    En Java :
    
    TextView tvDemo = (TextView) findViewById(R.id.textview);
    tvDemo.setOnLongClickListener(new View.OnLongClickListener() {
    	@Override
    	public boolean onLongClick(View v) {
    		copyContentToClipboard("content", ((TextView)v).getText().toString());
    		return true;
    	}
    });
    
    En Kotlin :
    
    findViewById<TextView>(R.id.textview).setOnLongClickListener(View.OnLongClickListener {
        copyContentToClipboard("label", (it as TextView).text.toString())
        return@OnLongClickListener true
    })
    
    Utilisons le Live Template and_copy_to_clipboard pour écrire la méthode : copyContentToClipboard.

    App streaming

    Proposons maintenant à l'utilisateur de tester notre application, sans avoir besoin de l'instaler, pour cela nous allons mettre en place le streaming d'application :
    TODO.

    RenderScript

    MVVM

    Live data, ViewModel, Retrofit Android Architecture Component

    RecyclerView

    Gestion du click en Kotlin.

    Sources

    handleUncaughtException

    Timber

    Décompiler un APK

    Récupération de l'APK en utilisant par exemple : ES Explorateur de Fichiers ou le backup android.

    Koin

    Injection de dépendances faciles :

    run-as

    Récupération de fichiers dans une application débuggable :
    Le plus simple étant d'utiliser le View / Tool Windows / Device File Explorer.
    Un seul fichier 
    
    adb exec-out run-as debuggable.app.package.name cat databases/file > file
    
    Plusieurs fichiers 
    
    adb exec-out run-as debuggable.app.package.name tar c databases/ > databases.tar
    adb exec-out run-as debuggable.app.package.name tar c shared_prefs/ > shared_prefs.tar
    
    A tester :
    
    > adb shell
    shell $ run-as com.example.package
    shell $ chmod 666 databases/file
    shell $ exit                                               ## exit out of 'run-as'
    shell $ cp /data/data/package.name/databases/file /sdcard/
    shell $ run-as com.example.package
    shell $ chmod 600 databases/file
    > adb pull /sdcard/file .
    

    Backup

    Backup complet :
    
    adb backup -apk -shared -all -f <filepath>/backup.ab
    
    Restauration :
    
    adb restore <filepath>/backup.ab
    
    Backup d'une seule application (avec APK -apk) :
    
    adb backup -f "“"D:\myfolder\myapp.ab" -apk <package name>
    
    Multiples exemple.

    Pour extraire facilement les fichiers d'un backup, utiliser : adbextractor.
    
    java -jar "C:\ADB\android-backup-tookit\android-backup-extractor\android-backup-extractor-20180203-bin\abe.jar" unpack c:\adb\backup2.ab backup-extracted.tar  (You’ll be asked to enter the password)
    

    Tools pour Android Studio

    Il est possible de définir des attributs qui seront uniquement utilisés dans Android Studio. Pour cela nous allons non plus utiliser android:... mais tools:... :
    
    
    Toute la documentation.

    TODO Attribute value Description of placeholder data @tools:sample/full_names Full names that are randomly generated from the combination of @tools:sample/first_names and @tools:sample/last_names. @tools:sample/first_names Common first names. @tools:sample/last_names Common last names. @tools:sample/cities Names of cities from across the world. @tools:sample/us_zipcodes Randomly generated US zipcodes. @tools:sample/us_phones Randomly generated phone numbers with the following format: (800) 555-xxxx. @tools:sample/lorem Placeholder text that is derived from Latin. @tools:sample/date/day_of_week Randomized dates and times for the specified format. @tools:sample/date/ddmmyy @tools:sample/date/mmddyy @tools:sample/date/hhmm @tools:sample/date/hhmmss @tools:sample/avatars Vector drawables that you can use as profile avatars. @tools:sample/backgrounds/scenic Images that you can use as backgrounds.