image/svg+xml $ $ ing$ ing$ ces$ ces$ Res Res ea ea Res->ea ou ou Res->ou r r ea->r ch ch ea->ch r->ces$ r->ch ch->$ ch->ing$ T T T->ea ou->r

Intent

Communication par Intent

Principales utilisation d'un Intent depuis une activité, un service ou un BroadcastReceiver (composants héritant de Context) :

Structure d'un Intent

Actions d'Intent pour activités

Quelques constantes d'actions usuelles définies dans la classe Intent :

ACTION_MAIN  action principale
ACTION_VIEW  visualisation de données
ACTION_ATTACH_DATA  attachement de données sur une URI
ACTION_EDIT  éditer des données
ACTION_PICK  choisir sur un répertoire de données
ACTION_CHOOSER  force l'apparition d'un menu pour choisir l'application à utiliser ; EXTRA_INTENT contient l'Intent originel, EXTRA_TITLE le titre du menu
ACTION_GET_CONTENT  obtenir un contenu selon son type MIME
ACTION_SEND  envoyer un contenu spécifié par EXTRA_TEXT ou EXTRA_STREAM à un destinataire non spécifié
ACTION_SENDTO  envoyer un contenu à un destinataire spécifié dans l'URI
ACTION_INSERT  insère un élément vierge dans le répertoire spécifié par l'URI
ACTION_DELETE  supprime l'élément désigné par l'URI
ACTION_PICK_ACTIVITY  propose un menu de sélection d'activité selon l'EXTRA_INTENT fourni ; retourne la classe choisie mais ne la lance pas
ACTION_SEARCH, ACTION_WEBSEARCH  réalise une recherche (chaîne à rechercher avec clé SearchManager.QUERY)

...

Possibilité de retrouver toutes les activités pouvant traiter un Intent implicite avec getPackageManager().queryIntentActivities(intent).
Exemple de méthode transformant un Intent implicite en une Map<String, Intent> avec tous les Intent explicitant les activités :

// Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License

package fr.upem.coursand.filechooser

import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager

/**
 * This extension method for an implicit Intent find all the activities that are compatible
 * with the given Intent and create a map for all these activities with the Intent to launch them.
 *
 * Typical use to obtain all the activities for ACTION_SEND compatible with image/jpeg
 * val intentMap = Intent(android.content.Intent.ACTION_SEND).setType("image/jpeg").resolveIntent()
 */
fun Intent.resolveIntent(context: Context): Map<String, Intent> {
    // get a List<ResolveInfo>
    val resolveInfos = context.packageManager.queryIntentActivities(this, PackageManager.MATCH_ALL)
    return resolveInfos.map {
        val intent = this.clone() as Intent
        // set the class name for the Intent
        intent.setClassName(it.activityInfo.packageName, it.activityInfo.name)
        // return the label of the activity linked to the Intent to launch it
        it.activityInfo.name to intent
    }.sortedBy { it.first }.toMap()
}

Quelques drapeaux d'Intent utilisables avec setFlags(int)

Démarrage d'activité (et communication inter-activité)

Principe

Exemple : activité de login

Une activité a besoin de connaître l'identifiant et le mot de passe de l'utilisateur pour un service web.

Création de l'activité de connexion appelante

L'activité appelante ConnectionActivity peut communiquer le nom du service de connexion à LoginActivity. LoginActivity est chargée de récupérer les identifiants de l'utilisateur (et peut récupérer des identifiants déjà mémorisés dans les préférences).

En Java, on utilise startActivityForResult :

// Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License

package fr.upem.coursand.login;

import android.content.Intent;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.Button;
import android.widget.TextView;

import fr.upem.coursand.R;

/** A fake connection activity that uses the {@link fr.upem.coursand.login.LoginActivity}
 * to retrieve login information and display them.
 * This is an example using startActivityForResult.
 */
public class ConnectionActivity extends AppCompatActivity {

    public static final String SERVICE_NAME = "coursandService";
    public static final int REQUEST_ID = 1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_connection);
        Button supplyLoginButton = findViewById(R.id.supplyLoginButton);
        supplyLoginButton.setOnClickListener( v -> {
            Intent i = new Intent(this, LoginActivity.class);
            i.putExtra("service", SERVICE_NAME);
            startActivityForResult(i, REQUEST_ID);
        });
    }

    /** Called back when we have the result of {@link fr.upem.coursand.login.LoginActivity} */
    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        TextView v = findViewById(R.id.loginInfoView);
        if (requestCode == REQUEST_ID && resultCode == RESULT_OK) {
            String text = "Fetched login data: \n" +
                    "Username: " + data.getStringExtra("username") + "\n" +
                    "Password: " + data.getStringExtra("password") + "\n";
            v.setText(text);
        } else if (requestCode == REQUEST_ID && resultCode == RESULT_CANCELED) {
            v.setText("cancelled");
        }
    }
}

Avec Kotlin, startActivityForResult est déprécié, on utilise registerForActivityResult qu'on initialise avec un ActivityResultContract :

// Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License
package fr.upem.coursand.login

import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.Button
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.result.registerForActivityResult
import androidx.appcompat.app.AppCompatActivity
import fr.upem.coursand.R

/** A fake connection activity that uses the [KotlinLoginActivity]
 * to retrieve login information and display them.
 * This is an example using startActivityForResult.
 */
class KotlinConnectionActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_connection)
        val supplyLoginButton = findViewById<Button>(R.id.supplyLoginButton)
        supplyLoginButton.setOnClickListener { v: View? ->
            val i = Intent(this, LoginActivity::class.java)
            i.putExtra("service", SERVICE_NAME)
            getResult.launch(i)
        }
    }

    private val getResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
        val v = findViewById<TextView>(R.id.loginInfoView)
        if (it.resultCode == RESULT_OK) {
            it.data?.let { data ->
                val text = """
                Fetched login data: 
                Username: ${data.getStringExtra("username")}
                Password: ${data.getStringExtra("password")}
                
                """.trimIndent()
                v.text = text
            }
        } else if (it.resultCode == RESULT_CANCELED) {
            v.text = "cancelled"
        }
    }


    companion object {
        const val SERVICE_NAME = "coursandService"
    }
}

Création de l'activité de récupération des identifiants (activité appelée)

L'activité appelée se charge de demander à l'utilisateur ses identifiants et les envoie comme résultat à l'activité appelante. Si les identifiants sont présents dans les préférences, ils sont préremplis.

// Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License

package fr.upem.coursand.login;

import android.content.Intent;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

import fr.upem.coursand.R;

/** An activity that asks the user to supply her login information.
 * The username and password are then sent back to the calling activity.
 * It illustrates bidirectional communication between activities.
 */
public class LoginActivity extends AppCompatActivity {

    private SharedPreferences prefs;
    private String service; // the service for which we login
    private EditText usernameView;
    private EditText passwordView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);

        usernameView = findViewById(R.id.usernameView);
        passwordView = findViewById(R.id.passwordView);

        // read the calling intent
        service = getIntent().getStringExtra("service");
        TextView serviceView = findViewById(R.id.serviceView);
        serviceView.setText(service); // display the service name

        // maybe we have already saved into the preferences of the activity the login info for this service
        // in this case we prefill the input fields with the stored data
        prefs = PreferenceManager.getDefaultSharedPreferences(this);
        String username0 = prefs.getString(service + ".username", null);
        String password0 = prefs.getString(service + ".password", null);
        if (username0 != null) usernameView.setText(username0);
        if (password0 != null) passwordView.setText(password0);

        // set the login info
        findViewById(R.id.loginButton).setOnClickListener( v -> {
            String username = usernameView.getText().toString();
            String password = passwordView.getText().toString();
            if (username.isEmpty() || password.isEmpty()) {
                Toast.makeText(this, "Missing information", Toast.LENGTH_LONG).show();
            } else {
                CheckBox rememberCheckBox = findViewById(R.id.rememberCheckBox);
                SharedPreferences.Editor editor = prefs.edit(); // open a transaction for the preferences
                if (rememberCheckBox.isChecked()) {
                    // we store the login info into the prefs
                    editor.putString(service + ".username", username);
                    editor.putString(service + ".password", password);
                    editor.putLong(service + ".timestamp", System.currentTimeMillis());
                } else {
                    // we must forget the information already (or not) stored
                    editor.remove(service + ".username");
                    editor.remove(service + ".password");
                    editor.remove(service + ".timestamp");
                }
                // finally we commit the prefs transaction
                editor.commit();

                // create a result intent and send it back to the calling activity
                Intent resultIntent = new Intent();
                resultIntent.putExtra("service", service);
                resultIntent.putExtra("username", username);
                resultIntent.putExtra("password", password);
                setResult(RESULT_OK, resultIntent);

                // the job is done, we quit the activity
                finish();
            }
        });
    }


}

Ouverture d'activité depuis une URL

BroadcastReceiver

Principe de fonctionnement d'un BroadcastReceiver

Événements broadcast du système

Listés comme constantes dans Intent

Exemple 1 : Une gauge à batterie recevant ACTION_BATTERY_CHANGED

// Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License

package fr.upem.coursand.battery;

import android.app.ActionBar;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.BatteryManager;
import android.os.Bundle;
import android.widget.TextView;

import java.util.Locale;

import fr.upem.coursand.R;

/**
 * This class presents the use of a BroadcastReceiver to receive a sticky broadcast
 * in order to display the status of the device battery.
 */
public class BatteryActivity extends Activity 
{
	private final BroadcastReceiver batteryReceiver =
			new BroadcastReceiver()
	{
		@Override
		public void onReceive(Context context, Intent intent) 
		{
			// Fetch battery state data in the intent
			updateStatus(String.format(Locale.getDefault(),
				"Charge ratio: %f\nBattery technology: %s\nBattery temperature: %s\nBattery voltage: %d\n",
				(float)intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) / intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1), // charge ratio
				intent.getStringExtra(BatteryManager.EXTRA_TECHNOLOGY),
				intent.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, -1),
				intent.getIntExtra(BatteryManager.EXTRA_VOLTAGE, -1)),
				intent.getIntExtra(BatteryManager.EXTRA_ICON_SMALL, -1));
		}
	};
	
	@Override
	protected void onCreate(Bundle savedInstanceState)
	{
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_battery);
	}
	
	private void updateStatus(String text, int logo)
	{
		TextView view = findViewById(R.id.mainBatteryTextView);
		view.setText(text);
		ActionBar ab = getActionBar();
		if (ab != null)
			ab.setLogo(logo); // Set an icon for the battery state
	}
	
	@Override
	protected void onStart()
	{
		super.onStart();
		registerReceiver(batteryReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
	}
	
	@Override
	protected void onStop()
	{
		super.onStop();
		unregisterReceiver(batteryReceiver);
	}
}

Jauge à batterie

Types d'événements broadcast

Broadcast normal/ordonné

Broadcast non collant/collant

Envoi d'un événement broadcast

Broadcast local

Déclaration d'un BroadcastReceiver dans le manifeste

<receiver android:enabled=["true" | "false"] ←  Instantiation automatique par le système
          android:exported=["true" | "false"] ← Accessibilité depuis les autres applications
          android:icon="drawable resource"
          android:label="string resource"
          android:name="string" ← Nom de la classe
          android:permission="string" ← Permission nécessaire pour envoyer un broadcast
          android:process="string" >
    . . .
</receiver>

Filtre d'Intent

Quelques exemples de filtre

Pour qu'une activité soit affichée dans le launcher d'applications :

<intent-filter>
    <action android:name="android.intent.action.MAIN" />
    <category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

Pour recevoir les intent implicites demandant à visualiser ou éditer un fichier JPEG :

<intent-filter android:label="@string/jpeg_editor">
    <action android:name="android.intent.action.VIEW" />
    <action android:name="android.intent.action.EDIT" />
    <data android:mimeType="image/jpeg" />
</intent-filter>

Intents implicites

Exemple : une activité calculant les empreintes cryptographiques MD5, SHA-1, SHA-2... d'un fichier

// Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License

package fr.upem.coursand.digester

import android.content.Intent
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import fr.upem.coursand.R
import kotlinx.android.synthetic.main.activity_digester.*

fun ByteArray.toHexString() = joinToString("") { "%02x".format(it) }

class DigesterActivity : AppCompatActivity() {

    var uri: Uri? = null
    private var digester: Digester? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_digester)
        uri = intent.data
        // usually the Uri is in the extra variable EXTRA_STREAM
        if (uri == null) uri = intent.getParcelableExtra(Intent.EXTRA_STREAM) as? Uri
    }

    override fun onStart() {
        super.onStart()
        val uri = this.uri
        if (uri != null) {
            // create an async task for digest computation
            digester = object : Digester() {

                override fun onPreExecute() {
                    uriView.text = uri.toString()
                    // display the metadata about the file
                    resultView.text = uri.getMetadata(this@DigesterActivity)?.map { "${it.key}=${it.value}" }?.joinToString("\n")
                }

                override fun onProgressUpdate(vararg values: Long?) {
                    // update the byte counter
                    statusView.text = "${values.last()} bytes processed"
                }

                override fun onPostExecute(result: Map<String, ByteArray>?) {
                    // display the result
                    if (result != null) {
                        val resultText = result.map { "${it.key}=${it.value.toHexString()}" }.joinToString("\n")
                        resultView.append(resultText)
                    } else
                        resultView.append("computation aborted")
                }
            }
            // start asynchronously the computation with the digester
            digester?.execute(contentResolver.openInputStream(uri))
        } else
            uriView.text = "This activity must be started with an Intent including a data URI"
    }

    // destroy the digest when the activity is destroyed
    // (we continue the computation if the activity is alive in the background)
    override fun onDestroy() {
        super.onDestroy()
        digester?.cancel(true)
    }
}

AlarmManager

Remarques :

Exemple : une activité qui planifie dans le futur le démarrage d'un service d'affichage de toast

// Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License

package fr.upem.coursand.toastservice

import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.SystemClock
import fr.upem.coursand.R
import kotlinx.android.synthetic.main.activity_future_toast.*

/**
 * Schedule a toast in the future
 * We use a  Intent that starts the ToastService in the future
 * This Intent will be launched using the AlarmManager
 */
class FutureToastActivity : AppCompatActivity() {

    private var currentPendingIntent: PendingIntent? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_future_toast)
        scheduleButton.setOnClickListener {
            // get the alarm manager
            val am = getSystemService(Context.ALARM_SERVICE) as AlarmManager

            // retrieve the data supplied in the fields
            val delay = delayView.text.toString().toInt() * 1000 // delay in millis
            val message = messageView.text.toString()

            // create the Intent to launch
            val intent = Intent(this, ToastService::class.java)
            intent.action = ToastService.TOAST_ACTION
            intent.putExtra("message", message)

            // wrap the Intent into a PendingIntent (delegated Intent that will be sent in the future)
            currentPendingIntent = PendingIntent.getService(this, 0, intent, 0)

            // schedule the intent with the AlarmManager
            am.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + delay, currentPendingIntent)
            cancelButton.isEnabled = true
        }
        cancelButton.setOnClickListener {
            currentPendingIntent?.let {
                val am = getSystemService(Context.ALARM_SERVICE) as AlarmManager
                // cancel the scheduled PendingIntent
                // note that the PendingIntent may have already been sent
                am.cancel(it)
                cancelButton.isEnabled = false
            }
        }
    }
}