Composants fondamentaux d'Android

Manifest

  • Description XML des composants d'une application
  • Fichier AndroidManifest.xml
  • L'espace de nom Android déclare des balises et attributs à utiliser pour construire un manifeste
  • La majorité des attributs ont le préfixe android:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="edu.android.helloworld">

    <uses-sdk android:minSdkVersion="19" android:targetSdkVersion="21" />
    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme">
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
  • <manifest> : noeud principal
    • package : nom du package de l'application
    • android:versionCode : code de la version de l'application
    • android:versionName : nom de la version de l'application
  • <uses-sdk> : compatibilité de l'application
    • android:minSdkVersion : version minimum de l'API
    • android:targetSdkVersion : version recommandée de l'API
  • <uses-feature> : fonctionnalité utilisée
    • android:name : nom de la fonctionnalité
    • android:required : indique si la fonctionnalité est obligatoire
  • <uses-permissions> : permission d'exécution
    • android:name : nom de la permission demandée
    • android:maxSdkVersion : version maximum pour la demande

Activité

  • Composant principal d'une application gérant un écran (ou plusieurs mais c'est déconseillé)
  • Instance de la classe Activity
  • Son cycle de vie est géré par des méthodes appelées par le système

Cycle de vie

  • onCreate()
        Appelée quand l'activité est créée
  • onStart()
        Appelée quand l'activité est visible
  • onResume()
        Appelée quand l'activité passe au premier plan
  • onPause()
        Appelée quand l'activité n'est plus au premier
  • onStop()
        Appelée quand l'activité va être arrêtée
  • onDestroy()
        Appelée avant l'arrêt de l'activité
<!-- AndroidManifest.xml -->
<activity android:name=".MainActivity" android:label="@string/app_name">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>


// MainActivity.java
public class MainActivity extends Activity implements SurfaceHolder.Callback {
    private Camera mCamera = null;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState); // Always call the superclass method first
        setContentView(R.layout.main_activity); // Set the user interface layout for this Activity
        SurfaceView surface = (SurfaceView) findViewById(R.id.menu_settings);
        SurfaceHolder holder = surface.getHolder();
        holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
        holder.addCallback(this);
    }
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        initializeCamera();
    }
    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        if (mCamera != null) { mCamera.stopPreview(); }
    }
    @Override
    public void onPause() {
        super.onPause(); // Always call the superclass method first
        // Release the Camera because we don't need it when paused and other activities might need to use it.
        if (mCamera != null) { mCamera.release(); mCamera = null; }
    }
    @Override
    public void onResume() {
        super.onResume(); // Always call the superclass method first
        // Get the Camera instance as the activity achieves full user focus
        if (mCamera == null) { initializeCamera(); }
    }
    private void initializeCamera() {
        try { mCamera.setPreviewDisplay(holder); mCamera.startPreview();
        } catch (IOException e) { e.printStackTrace(); }
    }
}

Restauration

  • La méthode onCreate() prend en paramètre une instance Bundle
  • Bundle est une table clé/valeur utilisée pour conserver des informations après destruction
  • La méthode onSaveInstanceState() est déclenchée lorsqu'il y a des chances que l'activité soit tuée
    private final static 
        String MYKEY = "my.key";
    
    @Override
    protected void onSaveInstanceState 
        (Bundle bundle) {
    
      super.onSaveInstanceState(bundle);
      bundle.putInt(MYKEY, 10);
    
      /*
       * On pourra le récupérer 
       * plus tard avec
       * int resultat = 
       *   bundle.getInt(MYKEY);
       */
    
    }

Types d'activités

Service

  • Composant d'une application s'exécutant en tâche de fond
  • Instance de la classe Service
  • Un service peut être lancé manuellement...
  • ...ou lorsqu'une activité tente d'y accéder
  • Il peut être local ou accepter les connexions d'autres applications
public class LocalService extends Service {
    private NotificationManager mNM;
    // Unique Identification Number for the Notification. We use it on Notification start, and to cancel it.
    private int NOTIFICATION = R.string.local_service_started;
    /** Class for clients to access because we know this service runs in the same process as its clients, we don't need to deal with IPC. */
    public class LocalBinder extends Binder {
        LocalService getService() {
            return LocalService.this;
        }
    }
    @Override
    public void onCreate() {
        mNM = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
        // Display a notification about us starting.  We put an icon in the status bar.
        showNotification();
    }
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.i("LocalService", "Received start id " + startId + ": " + intent);
        // We want this service to continue running until it is explicitly stopped, so return sticky.
        return START_STICKY;
    }
    @Override
    public void onDestroy() {
        // Cancel the persistent notification.
        mNM.cancel(NOTIFICATION);
        // Tell the user we stopped.
        Toast.makeText(this, R.string.local_service_stopped, Toast.LENGTH_SHORT).show();
    }
    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }
    // This is the object that receives interactions from clients. See RemoteService for a more complete example.
    private final IBinder mBinder = new LocalBinder();
    /** Show a notification while this service is running. */
    private void showNotification() {
        // In this sample, we'll use the same text for the ticker and the expanded notification
        CharSequence text = getText(R.string.local_service_started);
        // Set the icon, scrolling text and timestamp
        Notification notification = new Notification(R.drawable.stat_sample, text,
                System.currentTimeMillis());
        // The PendingIntent to launch our activity if the user selects this notification
        PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
                new Intent(this, LocalServiceActivities.Controller.class), 0);
        // Set the info for the views that show in the notification panel.
        notification.setLatestEventInfo(this, getText(R.string.local_service_label),
                       text, contentIntent);
        // Send the notification.
        mNM.notify(NOTIFICATION, notification);
    }
}
    private LocalService mBoundService;
    private ServiceConnection mConnection = new ServiceConnection() {
        public void onServiceConnected(ComponentName className, IBinder service) {
            // This is called when the connection with the service has been
            // established, giving us the service object we can use to
            // interact with the service.  Because we have bound to a explicit
            // service that we know is running in our own process, we can
            // cast its IBinder to a concrete class and directly access it.
            mBoundService = ((LocalService.LocalBinder)service).getService();
    
            // Tell the user about this for our demo.
            Toast.makeText(Binding.this, R.string.local_service_connected,
                    Toast.LENGTH_SHORT).show();
        }
        public void onServiceDisconnected(ComponentName className) {
            // This is called when the connection with the service has been
            // unexpectedly disconnected -- that is, its process crashed.
            // Because it is running in our same process, we should never
            // see this happen.
            mBoundService = null;
            Toast.makeText(Binding.this, R.string.local_service_disconnected,
                    Toast.LENGTH_SHORT).show();
        }
    };
    
    void doBindService() {
        // Establish a connection with the service.  We use an explicit
        // class name because we want a specific service implementation that
        // we know will be running in our own process (and thus won't be
        // supporting component replacement by other applications).
        bindService(new Intent(Binding.this, 
                LocalService.class), mConnection, Context.BIND_AUTO_CREATE);
        mIsBound = true;
    }
    void doUnbindService() {
        if (mIsBound) {
            // Detach our existing connection.
            unbindService(mConnection);
            mIsBound = false;
        }
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        doUnbindService();
    }

Intention

  • Message traduisant une requête d'action à faire réaliser par un composant
  • Facilite la communication
    entre composants
  • Classe Intent
  • Cas d'utilisations
    • Démarrer une activité
    • Démarrer un service
    • Diffuser un événement
  • Types d'intentions
    • Explicite : le nom du composant destinataire est défini
    • Implicite : le composant destinataire est défini par les actions demandées dans la requête

Illustration of how an implicit intent is delivered through the system to start another activity: [1] Activity A creates an Intent with an action description and passes it to startActivity(). [2] The Android System searches all apps for an intent filter that matches the intent. When a match is found, [3] the system starts the matching activity (Activity B) by invoking its onCreate() method and passing it the Intent

Construction

  • Une intention contient des informations concernant le composant qui doit l'a traiter
  • Le champ Component rend l'intention explicite en renseignant le composant destinataire
  • Sinon il s'agit d'une intention implicite et il faut renseigner d'autres champs pour déterminer les destinataires
    • Action : ce que le destinataire doit faire
    • Data : les données utilisées pour réaliser l'action
    • Type : indique le type de données incluses
    • Category : détermine le type de destinataire
    • Extra : informations supplémentaires
    • Flags : pour modifier le comportement de l'intention

Source : OpenClassroom

// On déclare une constante dans la classe FirstClass
public final static String NOMS = "sdz.chapitreTrois.intent.examples.NOMS";

// Autre part dans le code
Intent i = new Intent();
String[] noms = new String[] {"Dupont", "Dupond"};
i.putExtra(FirstClass.NOMS, noms);
		
// Encore autre part
String[] noms = i.getStringArrayExtra(FirstClass.NOMS);

Traitement

Filtres d'intentions

  • Utilisées pour indiquer les types d'intentions pouvant être traitées par l'application
  • Les filtres s'appliquent à des paramètres implicites de l'intention
  • Les filtres sont définis dans le manifeste
<application android:icon="@drawable/app_notes"
  android:label="@string/app_name" >
    <provider android:name="NotePadProvider"
      android:authorities="com.google.provider.NotePad" />
    <activity android:name="NotesList" android:label="@string/title_notes_list">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
        <intent-filter>
            <action android:name="android.intent.action.VIEW" />
            <action android:name="android.intent.action.EDIT" />
            <action android:name="android.intent.action.PICK" />
            <category android:name="android.intent.category.DEFAULT" />
            <data android:mimeType="vnd.android.cursor.dir/vnd.google.note" />
            </intent-filter>
        <intent-filter>
            <action android:name="android.intent.action.GET_CONTENT" />
            <category android:name="android.intent.category.DEFAULT" />
            <data android:mimeType="vnd.android.cursor.item/vnd.google.note" />
        </intent-filter>
    </activity>
    <!-- ... -->
</application>

Parcelable (1/2)

  • Il est possible d'associer un Bundle ou un Intent à une intention avec putExtras()
  • La table clé/valeur d'un Bundle permet de définir des données de types standards
  • L'implémentation de l'interface Parcelable permet à un objet d'être sérializable
  • Attention ! Il faut écrire les attributs dans le Parcel dans l'ordre de déclaration de la classe

Parcelable (2/2)

import android.os.Parcel;
import android.os.Parcelable;

public class Contact implements Parcelable{
  public static final Parcelable.Creator<Contact> CREATOR = new Parcelable.Creator<Contact>() {
    @Override
    public Contact createFromParcel(Parcel source) {
      return new Contact(source);
    }
    @Override
    public Contact[] newArray(int size) {
      return new Contact[size];
    }
  };

  private String mNom;
  private String mPrenom;
  private int mNumero;
  public Contact(String pNom, String pPrenom, int pNumero) {
    mNom = pNom;
    mPrenom = pPrenom;
    mNumero = pNumero;
  }
  public Contact(Parcel in) {
    mNom = in.readString();
    mPrenom = in.readString();
    mNumero = in.readInt();
  }
  @Override
  public int describeContents() {
    //On renvoie 0, car notre classe ne 
    // contient pas de paramètres spéciaux
    return 0;
  }
  @Override
  public void writeToParcel(Parcel dest, int flags) {
    // On ajoute les objets dans 
    // l'ordre dans lequel on les a déclarés
    dest.writeString(mNom);
    dest.writeString(mPrenom);
    dest.writeInt(mNumero);
  }
}

// Autre part dans le code
Intent i = new Intent();
Contact c = new Contact("Dupont", "Dupond", 06);
i.putExtra("sdz.chapitreTrois.intent.examples.CONTACT", c);
// Autre part dans le code
Contact c = i.getParcelableExtra("sdz.chapitreTrois.intent.examples.CONTACT");

Intention explicite sans retour

package sdz.chapitreTrois.intent.example;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MainActivity extends Activity {
  public final static String AGE 
    = "sdz.chapitreTrois.intent.example.AGE";
	
  private Button mPasserelle = null;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    
    mPasserelle = (Button) findViewById(R.id.passerelle);
    
    mPasserelle.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        // Le premier paramètre est le nom de l'activité actuelle
        // Le second est le nom de l'activité de destination
        Intent secondeActivite = 
            new Intent(MainActivity.this, IntentExample.class);
        
        // On rajoute un extra
        secondeActivite.putExtra(AGE, 31);

        // Puis on lance l'intent !
        startActivity(secondeActivite);
      }
    });
  }
}
package sdz.chapitreTrois.intent.example;

import android.app.Activity;
import android.os.Bundle;

public class IntentExample extends Activity {
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.layout_example);

    // On récupère l'intent qui a lancé cette activité
    Intent i = getIntent();

    // Puis on récupère l'âge donné dans l'autre activité, 
    // ou 0 si cet extra n'est pas dans l'intent
    int age = i.getIntExtra(MainActivity.AGE, 0);

    // S'il ne s'agit pas de l'âge par défaut
    if(age != 0)
      // Traiter l'âge
      age = 2;
  }
}

Intention explicite avec retour

public class MainActivity extends Activity {
  private Button mPasserelle = null;
  // L'identifiant de notre requête
  public final static int CHOOSE_BUTTON_REQUEST = 0;
  // L'identifiant de la chaîne de caractères 
  // qui contient le résultat de l'intent
  public final static String BUTTONS = 
    "sdz.chapitreTrois.intent.example.Boutons";

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    
    mPasserelle = (Button) findViewById(R.id.passerelle);
    
    mPasserelle.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        Intent secondeActivite = 
            new Intent(MainActivity.this, IntentExample.class);
        // On associe l'identifiant à notre intent
        startActivityForResult(secondeActivite, 
            CHOOSE_BUTTON_REQUEST);
      }
    });
  }
  
  @Override
  protected void onActivityResult(int requestCode, 
                                    int resultCode, Intent data) {
    // On vérifie tout d'abord à quel intent on fait 
    // référence ici à l'aide de notre identifiant
    if (requestCode == CHOOSE_BUTTON_REQUEST) {
      // On vérifie aussi que l'opération s'est bien déroulée
      if (resultCode == RESULT_OK) {
        // On affiche le bouton qui a été choisi
      	Toast.makeText(this, "Vous avez choisi le bouton " 
            + data.getStringExtra(BUTTONS), 
            Toast.LENGTH_SHORT).show();
      }
    }
  }
}
public class IntentExample extends Activity {
  private Button mButton1 = null;
  private Button mButton2 = null;
	
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.layout_example);
    
    mButton1 = (Button) findViewById(R.id.button1);
    mButton1.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        Intent result = new Intent();
        result.putExtra(MainActivity.BUTTONS, "1");
        setResult(RESULT_OK, result);
        finish();
      }
    });
    
    mButton2 = (Button) findViewById(R.id.button2);
    mButton2.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        Intent result = new Intent();
        result.putExtra(MainActivity.BUTTONS, "2");
        setResult(RESULT_OK, result);
        finish();
      }
    });
  }
}

Diffusion d'intention

<receiver android:name="CoucouReceiver">
  <intent-filter>
    <action 
      android:name="sdz.chapitreTrois.intent.action.coucou" />
  </intent-filter>
</receiver>


public class CoucouReceiver extends BroadcastReceiver {
  private static final String NOM_USER = 
    "sdz.chapitreTrois.intent.extra.NOM";

  // Déclenché dès qu'on reçoit un broadcast intent 
  // qui réponde aux filtres déclarés dans le Manifest
  @Override
  public void onReceive(Context context, Intent intent) {
    // On vérifie qu'il s'agit du bon intent
    if(intent.getAction().equals("ACTION_COUCOU")) {
      // On récupère le nom de l'utilisateur
      String nom = intent.getExtra(NOM_USER);
      Toast.makeText(context, "Coucou " + nom + " !", 
        Toast.LENGTH_LONG).show();
    }
  }
}

import android.app.Activity;
import android.content.IntentFilter;
import android.os.Bundle;

public class CoucouActivity extends Activity {
  private static final String COUCOU = 
    "sdz.chapitreTrois.intent.action.coucou";
  private IntentFilter filtre = null;
  private CoucouReceiver receiver = null;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    
    filtre = new IntentFilter(COUCOU);
    receiver = new CoucouReceiver();
  }
  
  @Override
  public void onResume() {
    super.onResume();
    registerReceiver(receiver, filtre);
  }

  /** Si vous déclarez votre receiver dans le onResume, 
  n'oubliez pas qu'il faut l'arrêter dans le onPause **/
  @Override
  public void onPause() {
    super.onPause();
    unregisterReceiver(receiver);
  }
}

Fournisseur de contenu

  • L'accès aux données est réalisé à travers un client ContentResolver
  • Un client fournit des méthodes CRUD pour gérer la persistance de données
// Queries the user dictionary and returns results
mCursor = getContentResolver().query(
    UserDictionary.Words.CONTENT_URI,   // The content URI of the words table
    mProjection,                        // The columns to return for each row
    mSelectionClause                    // Selection criteria
    mSelectionArgs,                     // Selection criteria
    mSortOrder);                        // The sort order for the returned rows
query() argument SELECT parameter Notes
Uri FROM table_name Uri maps to the table in the provider named table_name.
projection col,col,col,... projection is an array of columns that should be included for each row retrieved.
selection WHERE col = value selection specifies the criteria for selecting rows.
selectionArgs (No exact equivalent. Selection arguments replace ? placeholders in the selection clause.)
sortOrder ORDER BY col,col,... sortOrder specifies the order in which rows appear in the returned Cursor.
// SELECT _ID, word, locale FROM words WHERE word = <userinput> ORDER BY word ASC;


/*
 * This defines a one-element String array to contain the selection argument.
 */
String[] mSelectionArgs = {""};

// Gets a word from the UI
mSearchString = mSearchWord.getText().toString();

// Remember to insert code here to check for invalid or malicious input.

// If the word is the empty string, gets everything
if (TextUtils.isEmpty(mSearchString)) {
    // Setting the selection clause to null will return all words
    mSelectionClause = null;
    mSelectionArgs[0] = "";
} else {
    // Constructs a selection clause that matches the word that the user entered.
    mSelectionClause = UserDictionary.Words.WORD + " = ?";
    // Moves the user's input string to the selection arguments.
    mSelectionArgs[0] = mSearchString;
}

// Does a query against the table and returns a Cursor object
mCursor = getContentResolver().query(
    UserDictionary.Words.CONTENT_URI,  // The content URI of the words table
    mProjection,                       // The columns to return for each row
    mSelectionClause                   // Either null, or the word the user entered
    mSelectionArgs,                    // Either empty, or the string the user entered
    mSortOrder);                       // 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 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.
     */
} else {
    // Insert code here to do something with the results
}
// Defines a list of columns to retrieve from the Cursor and load into an output row
String[] mWordListColumns =
{
    UserDictionary.Words.WORD,   // Contract class constant containing the word column name
    UserDictionary.Words.LOCALE  // Contract class constant containing the locale column name
};

// Defines a list of View IDs that will receive the Cursor columns for each row
int[] mWordListItems = { R.id.dictWord, R.id.locale};

// Creates a new SimpleCursorAdapter
mCursorAdapter = new SimpleCursorAdapter(
    getApplicationContext(),               // The application's Context object
    R.layout.wordlistrow,                  // A layout in XML for one row in the ListView
    mCursor,                               // The result from the query
    mWordListColumns,                      // A string array of column names in the cursor
    mWordListItems,                        // An integer array of view IDs in the row layout
    0);                                    // Flags (usually none are needed)

// Sets the adapter for the ListView
mWordList.setAdapter(mCursorAdapter);
public class ExampleProvider extends ContentProvider {
...
    // Creates a UriMatcher object.
    private static final UriMatcher sUriMatcher;
...
    /*
     * The calls to addURI() go here, for all of the content URI patterns that the provider
     * should recognize. For this snippet, only the calls for table 3 are shown.
     */
...
    /*
     * Sets the integer value for multiple rows in table 3 to 1. Notice that no wildcard is used in the path
     */
    sUriMatcher.addURI("com.example.app.provider", "table3", 1);

    /*
     * Sets the code for a single row to 2. In this case, the "#" wildcard is used. "content://com.example.app.provider/table3/3"
     * matches, but "content://com.example.app.provider/table3" doesn't.
     */
    sUriMatcher.addURI("com.example.app.provider", "table3/#", 2);
...
    // Implements ContentProvider.query()
    public Cursor query(
        Uri uri,
        String[] projection,
        String selection,
        String[] selectionArgs,
        String sortOrder) {
...
        /*
         * Choose the table to query and a sort order based on the code returned for the incoming
         * URI. Here, too, only the statements for table 3 are shown.
         */
        switch (sUriMatcher.match(uri)) {
            // If the incoming URI was for all of table3
            case 1:

                if (TextUtils.isEmpty(sortOrder)) sortOrder = "_ID ASC";
                break;

            // If the incoming URI was for a single row
            case 2:

                /*
                 * Because this URI was for a single row, the _ID value part is
                 * present. Get the last path segment from the URI; this is the _ID value.
                 * Then, append the value to the WHERE clause for the query
                 */
                selection = selection + "_ID = " uri.getLastPathSegment();
                break;
            default:
            ...
                // If the URI is not recognized, you should do some error handling here.
        }
        // call the code to actually do the query
    }
}

Composants fondamentaux d'Android

By Steven Enten

Composants fondamentaux d'Android

- Manifest - Activité - Service - Intention

  • 1,700