Interrogation du contenu de manière asynchrone avec un chargeur de curseur

Les opérations de base de données peuvent prendre beaucoup de temps, il est donc particulièrement important qu'aucune requête ou transaction de base de données ou de fournisseur de contenu ne soit effectuée sur le thread d'application principal. Pour simplifier le processus de gestion des curseurs, de synchroniser correctement avec le fil de l'interface utilisateur et de garantir que toutes les requêtes se déroulent sur un fil de fond, Android fournit la classe Loader. Loaders et Loader Manager permettent de simplifier le chargement de données en arrière-plan asynchrone. Les chargeurs créent un fil d’arrière-plan dans lequel vos requêtes et transactions de base de données sont effectuées, avant la synchronisation avec le fil d’interface utilisateur et le retour des données traitées via des gestionnaires de rappel. Loader Manager comprend une mise en cache simple, garantissant que les chargeurs ne sont pas interrompus par l'activité redémarrée en raison de modifications de la configuration du périphérique, et qu'ils soient informés des événements de cycle de vie d'activité et de fragmentation. Cela garantit que les chargeurs sont supprimés lorsque l'activité ou le fragment parent est définitivement détruit. La classe AsyncTaskLoader peut être étendue pour charger tout type de données à partir de n'importe quelle source de données. La classe CursorLoader est particulièrement intéressante. Cursor Loader est spécialement conçu pour prendre en charge les requêtes asynchrones sur les fournisseurs de contenu, renvoyer un curseur de résultat et les notifications de toute mise à jour du fournisseur sous-jacent.

REMARQUE

Pour conserver un code concis et encapsulé, tous les exemples d'ici ne montrent pas explicitement l'utilisation d'un Cursor Loader lors de la création d'une requête de fournisseur de contenu (ce qui est mauvais et nous le mettons mal à l'aise). Pour vos applications, il est important de toujours utiliser un chargeur de curseur, ou une autre technique de threading d'arrière-plan, lors de l'exécution de requêtes ou de transactions sur des fournisseurs de contenu ou des bases de données. Le chargeur de curseur gère toutes les tâches de gestion requises pour utiliser un curseur, y compris la gestion du cycle de vie du curseur pour s'assurer que les curseurs sont fermés à la fin de l'activité. Requête du fournisseur de contenu - ce qui est mauvais et nous nous en sentons mal. Pour vos applications, il est important de toujours utiliser un chargeur de curseur, ou une autre technique de threading d'arrière-plan, lors de l'exécution de requêtes ou de transactions sur des fournisseurs de contenu ou des bases de données.

Les chargeurs de curseurs observent également les changements dans la requête sous-jacente, vous n’avez donc pas besoin de mettre en œuvre vos propres observateurs de contenu.


Implémentation de rappels de chargeur de curseur

Pour utiliser un chargeur de curseur, créez une nouvelle implémentation LoaderManager.LoaderCallbacks. Les rappels du chargeur sont implémentés à l'aide de génériques. Vous devez donc spécifier le type explicite en cours de chargement, dans ce cas, les curseurs, lors de l'implémentation du vôtre

 

  1. LoaderManager.LoaderCallbacks<Cursor>
  2. loaderCallback = new LoaderManager.LoaderCallbacks<Cursor>() {
  3. @Override public Loader<Cursor>
  4. onCreateLoader(int id, Bundle args) {
  5. return null; }
  6.  
  7. @Override
  8. public void onLoadFinished(Loader<Cursor> loader, Cursor data) {}
  9.  
  10. @Override
  11. public void onLoaderReset(Loader<Cursor> loader) {}
  12. } ;

 Si vous n'avez besoin que d'une seule implémentation Loader au sein de votre fragment ou de votre activité, vous le faites généralement en demandant à ce composant d'implémenter l'interface :

  1. public class MyActivity extends AppCompatActivity
  2. implements LoaderManager.LoaderCallbacks<Cursor>

 

The Loader Callbacks consist of three handlers:

onCreateLoader : appelé lorsque le chargeur est initialisé, ce gestionnaire doit créer et renvoyer un nouvel objet Cursor Loader. Les arguments du constructeur Cursor Loader reflètent ceux nécessaires à l'exécution d'une requête à l'aide du résolveur de contenu. En conséquence, lorsque ce gestionnaire est exécuté, les paramètres de requête que vous spécifiez seront utilisés pour effectuer une requête à l'aide du résolveur de contenu. Notez qu’un signal d’annulation n’est pas requis (ni pris en charge). Au lieu de cela, Cursor Loader crée son propre objet Signal d'annulation, qui peut être déclenché en appelant cancelLoad.

onLoadFinished : lorsque le gestionnaire de chargeur a terminé la requête asynchrone, le gestionnaire onLoadFinished est appelé, le résultat Cursor étant transmis en tant que paramètre. Utilisez ce curseur pour mettre à jour des adaptateurs et d'autres éléments de l'interface utilisateur.

onLoaderReset : lorsque Loader Manager réinitialise votre Cursor Loader, onLoaderReset est appelé. Dans ce gestionnaire, vous devez libérer toutes les références aux données renvoyées par la requête et réinitialiser l'interface utilisateur en conséquence. Le curseur sera fermé par le gestionnaire du chargeur, vous ne devez donc pas essayer de le fermer. Le Listing 10-13 montre une implémentation squelette des rappels de chargeur de curseur.  

LISTING 10-13 : Mise en œuvre de rappels de chargeur

 

  1. public Loader<Cursor> onCreateLoader(int id, Bundle args) {
  2. // Construct the new query in the form of a Cursor Loader. Use the id
  3. // parameter to construct and return different loaders.
  4. String[] projection = null;
  5. String where = null;
  6. String[] whereArgs = null;
  7. String sortOrder = null;
  8. // Query URI
  9. Uri queryUri = MyHoardContentProvider.CONTENT_URI;
  10. // Create the new Cursor loader.
  11. return new CursorLoader(this, queryUri, projection, where, whereArgs, sortOrder); }
  12. public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
  13. // You are now on the UI thread, update your UI with the loaded data.
  14. // Returns cached data automatically if initLoader is called after
  15. // a configuration change.
  16. }
  17.  
  18. public void onLoaderReset(Loader<Cursor> loader) {
  19. // Handle any cleanup necessary when Loader (or its parent)
  20. // is completely destroyed, for example the application being
  21. // terminated. Note that the Cursor Loader will close the
  22. // underlying result Cursor so you don't have to.
  23. }


Initialisation, redémarrage et annulation d'un chargeur de curseur

Pour initialiser un nouveau chargeur, appelez la méthode initLoader du gestionnaire du chargeur en transmettant une référence à votre implémentation Loader Callback, un ensemble d’arguments facultatifs et un identificateur de chargeur. Ici, comme dans le reste du livre, nous utiliserons la version Support Library de Loader Manager pour assurer la compatibilité avec les versions antérieures. Notez également que dans ce fragment de code, l’activité englobante met en œuvre la Loader Callbacks :

  1. Bundle args = null;
  2. // Initialize Loader. "this" is the enclosing Activity that implements callbacks getSupportLoaderManager().initLoader(LOADER_ID, args, this);

 

 Cela se fait généralement dans la méthode onCreate de l'activité hôte (ou dans le gestionnaire onActivityCreated dans le cas de Fragments). Si un chargeur correspondant à l’identifiant utilisé n’existe pas déjà, il est créé dans le gestionnaire onCreateLoader du chargeur associé, comme décrit dans la section précédente. Dans la plupart des cas, c’est tout ce qui est nécessaire. Loader Manager gérera le cycle de vie des chargeurs que vous initialisez, des requêtes sous-jacentes et des curseurs résultants, y compris de toute modification apportée aux résultats de la requête. Si votre chargeur s’achève lors d’un changement de configuration de l’appareil, le résultat du curseur sera mis en file d’attente et vous le recevrez via onLoadFinished une fois que l’activité ou le fragment parent aura été recréé. Une fois un chargeur créé, vos résultats sont mis en cache lors des modifications de la configuration du périphérique. Les appels répétés à initLoader renverront immédiatement le dernier jeu de résultats via le gestionnaire onLoadFinished, sans que la méthode onStartLoading du chargeur ne soit appelée. Cela permet d'économiser beaucoup de temps et d'énergie sur la batterie en éliminant les lectures en double dans la base de données et le traitement associé. Si vous souhaitez supprimer le précédent chargeur et le recréer, utilisez la méthode restartLoader :

  1. getSupportLoaderManager().restartLoader(LOADER_ID, args, this);

Cela n'est généralement nécessaire que lorsque vos paramètres de requête changent, tels que les requêtes de recherche, l'ordre de tri ou les paramètres de filtrage. Si vous souhaitez annuler un Cursor Loader pendant son exécution, vous pouvez appeler sa méthode cancelLoad.

 

  1. getSupportLoaderManager().getLoader(LOADER_ID).cancelLoad();

Le déclenchera un signal d'annulation interne dans le chargeur de curseur, qui sera à son tour transmis au fournisseur de contenu associé.

 
Ajouter, supprimer et mettre à jour du contenu

Pour effectuer des transactions sur les fournisseurs de contenu, utilisez les méthodes d'insertion, de suppression et de mise à jour sur le résolveur de contenu. Comme les requêtes, les transactions du fournisseur de contenu doivent être explicitement déplacées vers un thread de travail en arrière-plan pour éviter de bloquer le thread d'interface utilisateur avec des opérations pouvant prendre beaucoup de temps.

 
Insérer du contenu

Le résolveur de contenu propose deux méthodes pour insérer de nouveaux enregistrements dans un fournisseur de contenu: insert et bulkInsert. Les deux méthodes acceptent l’URI du fournisseur de contenu dans lequel vous insérez; la méthode insert utilise un seul nouvel objet ContentValues ​​et la méthode bulkInsert en extrait un tableau. La méthode insert renvoie un URI à l'enregistrement nouvellement ajouté, tandis que la méthode bulkInsert renvoie le nombre de lignes ajoutées avec succès. Le Listing 10-14 montre comment utiliser la méthode insert pour ajouter de nouvelles lignes à un fournisseur de contenu.

LISTING 10-14 : Insertion de nouvelles lignes dans un fournisseur de contenu

 

  1. // Create a new row of values to insert.
  2. ContentValues newValues = new ContentValues();
  3. // Assign values for each row.
  4. newValues.put(HoardDB.HoardContract.KEY_GOLD_HOARD_NAME_COLUMN,newHoardName);
  5. newValues.put(HoardDB.HoardContract.KEY_GOLD_HOARDED_COLUMN,newHoardValue);
  6. newValues.put(HoardDB.HoardContract.KEY_GOLD_HOARD_ACCESSIBLE_COLUMN, newHoardAccessible);
  7. // Get the Content Resolver
  8. ContentResolver cr = getContentResolver();
  9. // Insert the row into your table
  10. Uri newRowUri = cr.insert(MyHoardContentProvider.CONTENT_URI, newValues);

 
Suppression de contenu

Pour supprimer un seul enregistrement, appelez delete sur le résolveur de contenu en transmettant l'URI de la ligne à supprimer. Vous pouvez également spécifier une clause where pour supprimer plusieurs lignes. Les appels à supprimer renverront le nombre de lignes supprimées. Le Listing 10-15 montre comment supprimer un nombre de lignes correspondant à une condition donnée.

LISTING 10-15: Deleting rows from a Content Provider

 

  1. // Specify a where clause that determines which row(s) to delete.
  2. // Specify where arguments as necessary.
  3. String where = HoardDB.HoardContract.KEY_GOLD_HOARDED_COLUMN + "=?";
  4. String[] whereArgs = {"0"};
  5. // Get the Content Resolver.
  6. ContentResolver cr = getContentResolver();
  7. // Delete the matching rows int deletedRowCount = cr.delete(MyHoardContentProvider.CONTENT_URI, where, whereArgs);


Mise à jour du contenu

Vous pouvez mettre à jour des lignes à l’aide de la méthode de mise à jour du résolveur de contenu. La méthode update utilise l'URI du fournisseur de contenu cible, un objet ContentValues ​​qui mappe les noms de colonne sur les valeurs mises à jour et une clause where qui indique les lignes à mettre à jour. Lorsque la mise à jour est exécutée, chaque ligne correspondant à la clause where est mise à jour à l'aide des valeurs de contenu spécifiées et le nombre de mises à jour réussies est renvoyé.Vous pouvez également choisir de mettre à jour une ligne spécifique en spécifiant son URI unique, comme indiqué dans le Listing 10-16.

LISTING 10-16 : Mise à jour d'un enregistrement dans un fournisseur de contenu

  1. // Create a URI addressing a specific row.
  2. Uri rowURI = ContentUris.withAppendedId(MyHoardContentProvider.CONTENT_URI,hoardId);
  3. // Create the updated row content, assigning values for each row.
  4. ContentValues updatedValues = new ContentValues();
  5. updatedValues.put(HoardDB.HoardContract.KEY_GOLD_HOARDED_COLUMN,newHoardValue);
  6. // [ ... Repeat for each column to update ... ]
  7. // If we specify a specific row, no selection clause is required.
  8. String where = null;
  9. String[] whereArgs = null;
  10. // Get the Content Resolver.
  11. ContentResolver cr = getContentResolver();
  12. // Update the specified row.
  13. int updatedRowCount = cr.update(rowURI, updatedValues, where, whereArgs);

 

Accéder aux fichiers stockés dans les fournisseurs de contenu

Dans la section précédente, «Stockage de fichiers dans un fournisseur de contenu», nous avons décrit la procédure de stockage des fichiers dans les fournisseurs de contenu. Pour accéder à un fichier stocké dans ou pour insérer un nouveau fichier dans un fournisseur de contenu, utilisez les méthodes openOutputStream ou openInputStream du résolveur de contenu. Transmettez l'URI à la ligne du fournisseur de contenu contenant le fichier souhaité. Le fournisseur de contenu utilisera son implémentation openFile pour interpréter votre demande et renverra un flux d'entrée ou de sortie dans le fichier demandé, comme indiqué dans le listing 10-17.

LISTING 10-17 : Lecture et écriture de fichiers depuis et vers un fournisseur de contenu

 

  1. public void addNewHoardWithImage(int rowId, Bitmap hoardImage) {
  2. // Create a URI addressing a specific row.
  3. Uri rowURI = ContentUris.withAppendedId(MyHoardContentProvider.CONTENT_URI, rowId);
  4. // Get the Content Resolver
  5. ContentResolver cr = getContentResolver();
  6. try {
  7. // Open an output stream using the row's URI.
  8. OutputStream outStream = cr.openOutputStream(rowURI);
  9. // Compress your bitmap and save it into your provider. hoardImage.compress(Bitmap.CompressFormat.JPEG, 80, outStream);
  10. }
  11. Log.d(TAG, "No file found for this record.");
  12. }
  13. }
  14.  
  15. public Bitmap getHoardImage(long rowId) {
  16. Uri myRowUri = ContentUris.withAppendedId(MyHoardContentProvider.CONTENT_URI, rowId);
  17. try {
  18. // Open an input stream using the new row's URI.
  19. InputStream inStream = getContentResolver().openInputStream(myRowUri);
  20. // Make a copy of the Bitmap.
  21. Bitmap bitmap = BitmapFactory.decodeStream(inStream);
  22. return bitmap;
  23. }
  24. Log.d(TAG, "No file found for this record.");
  25. }
  26. return null; }


Accès aux fournisseurs de contenu soumis à des autorisations restreintes

De nombreux fournisseurs de contenu nécessitent des autorisations spécifiques avant de pouvoir lire et écrire. Par exemple, les fournisseurs de contenu natifs qui incluent des informations sensibles, telles que les coordonnées et les journaux d'appels, disposent d'un accès en lecture et en écriture protégé par des autorisations. Les fournisseurs de contenu natif sont décrits plus en détail dans la section «Utilisation de fournisseurs de contenu Android natifs».Pour interroger ou modifier un fournisseur de contenu pour lequel une autorisation est requise, vous devez déclarer les autorisations use-autorisations correspondantes dans votre manifeste pour les lire et / ou y écrire, respectivement:

  1. < uses-permission android:name="android.permission.READ_CONTACTS"/ >
  2. < uses-permission android:name="android.permission.WRITE_CALL_LOG"/ >

Les autorisations de manifeste sont accordées par l'utilisateur dans le cadre du flux d'installation normal; Cependant, Android 6.0 Marshmallow (API de niveau 23) a introduit une exigence supplémentaire en matière d'autorisations dangereuses, notamment celles qui protègent l'accès à des informations potentiellement sensibles. Les autorisations dangereuses nécessitent une approbation explicite de l'utilisateur lors de leur premier accès dans l'application, par le biais de demandes d'autorisation d'exécution. Chaque fois que vous tentez d'accéder à un fournisseur de contenu protégé par une autorisation dangereuse, vous devez utiliser la méthode ActivityCompat.checkSelfPermission, en transmettant la constante d'autorisation appropriée pour déterminer si vous avez accès au fournisseur. Il renverra PERMISSION_GRANTED si l'autorisation de l'utilisateur est accordée ou PERMISSION_DENIED si l'utilisateur a refusé ou n'a pas encore accordé l'accès :

  1. int permission = ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS);
  2. if (permission==PERMISSION_GRANTED) {
  3. // Access the Content Provider
  4. } else {
  5. // Request the permission or
  6. // display a dialog showing why the function is unavailable.
  7. }

 

Si vous ne disposez pas de l'autorisation, vous pouvez utiliser la méthode shouldShowRequestPermissionRationale de la classe ActivityCompat pour déterminer si c'est la première fois que cette application présente à l'utilisateur une demande pour cette autorisation, indiquée avec un résultat faux, ou s'il a déjà refusé. une requête. Dans ce dernier cas, vous pouvez envisager de fournir un contexte supplémentaire décrivant pourquoi vous avez besoin de l'autorisation demandée, avant de leur présenter à nouveau le dialogue de demande d'autorisation: 

 

  1. if (ActivityCompat.shouldShowRequestPermissionRationale(
  2. this, Manifest.permission.READ_CALL_LOG)) {
  3. // TODO: Display additional rationale for the requested permission.
  4. }

Pour afficher la boîte de dialogue demande d’autorisation du système d’exécution, appelez la méthode ActivityCompat.requestPermission, en spécifiant les autorisations requises :

  1. ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.READ_CONTACTS},CONTACTS_PERMISSION_REQUEST);

 Cette fonction s’exécute de manière asynchrone et affiche une boîte de dialogue Android standard qui ne peut pas être personnalisée. Vous pouvez recevoir un rappel lorsque l'utilisateur a accepté ou refusé votre demande d'exécution, en remplaçant le gestionnaire onRequestPermissionsResult:

 

  1. @Override
  2. public void onRequestPermissionsResult(int requestCode,@NonNull String[] permissions,@NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults);
  3. // TODO React to granted / denied permissions.
  4. }

 Il est courant d’écouter ce rappel et d’autoriser l’exécution d’une fonctionnalité précédemment protégée par votre vérification d’autorisation. Le résultat, pour l'utilisateur, sera une boîte de dialogue d'autorisation interstitielle affichée avant la fin de l'action demandée. Ceci est généralement préférable pour qu'ils aient à recommencer l'action; Cependant, veillez à ne pas créer une boucle sans fin de request-denial-request.

 

Utilisation De Fournisseurs De Contenu Android Natif

Android expose plusieurs fournisseurs de contenu natifs auxquels vous pouvez accéder directement à l'aide des techniques décrites précédemment dans ce chapitre. Le package android.provider inclut de nombreux fournisseurs de contenu utiles, notamment :

Navigateur : lit ou modifie l'historique des recherches du navigateur et du navigateur.

Calendrier - Crée de nouveaux événements et supprime, met à jour et lit les entrées de calendrier existantes. Cela inclut la modification de la liste des participants et la configuration des rappels. Log Journal des appels et numéros bloqués: le fournisseur du journal des appels stocke l'historique des appels, y compris les appels entrants et sortants, les appels manqués et les détails de l'appel, y compris les identifiants et les durées des appels. Numéros bloqués expose un tableau contenant des numéros bloqués et des adresses de messagerie. 

Contacts : récupère, modifie ou stocke les détails du contact. 

Store Media Store : fournit un accès centralisé et géré au multimédia sur votre appareil,y compris audio, vidéo et images. Vous pouvez stocker votre propre contenu multimédia dans le Media Store et le rendre disponible globalement, «Audio, vidéo et utilisation de l'appareil photo». Si possible, utilisez des fournisseurs de contenu natifs plutôt que de les dupliquer chaque fois que vous créez une application qui complète ou remplace les applications natives utilisant ces fournisseurs.

 
Accéder au journal des appels

Le journal des appels Android contient des informations sur les appels passés et reçus. L'accès au journal des appels est protégé par le READ_CALL_LOG manifeste uses-permission :

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

Pour les appareils Android exécutant Android 6.0 Marshmallow (API de niveau 23) et les versions ultérieures, vous devez également disposer de l'autorisation d'exécution correspondante :

  1. int permission = ActivityCompat.checkSelfPermission(this,Manifest.permission.READ_CALL_LOG);

Utilisez le résolveur de contenu pour interroger la table des appels du journal des appels à l'aide de sa constante statique CONTENT_URI: CallLog.Calls.CONTENT_URI.                                  

Le journal des appels est utilisé pour stocker tous les détails des appels entrants et sortants, tels que la date et l'heure des appels, leurs numéros de téléphone et leurs durées, ainsi que les valeurs mises en cache des détails de l'appelant tels que le nom, l'URI et les photos. Le Listing 10-18 montre comment interroger le journal des appels pour tous les appels sortants, en indiquant le nom, le numéro et la durée de chaque appel.

LISTING 10-18 : Accéder au fournisseur de contenu du journal des appels

  1. // Create a projection that limits the result Cursor
  2. // to the required columns.
  3. String[] projection = {
  4. CallLog.Calls.DURATION,
  5. CallLog.Calls.NUMBER,
  6. CallLog.Calls.CACHED_NAME,
  7. CallLog.Calls.TYPE
  8. } ;
  9. // Return only outgoing calls.
  10. String where = CallLog.Calls.TYPE + "=?";
  11. String[] whereArgs = {String.valueOf(CallLog.Calls.OUTGOING_TYPE)};
  12. // Get a Cursor over the Call Log Calls Provider.
  13. Cursor cursor = getContentResolver().query(CallLog.Calls.CONTENT_URI, projection, where, whereArgs, null);
  14. // Get the index of the columns.
  15. int durIdx = cursor.getColumnIndexOrThrow(CallLog.Calls.DURATION);
  16. int numberIdx = cursor.getColumnIndexOrThrow(CallLog.Calls.NUMBER);
  17. int nameIdx = cursor.getColumnIndexOrThrow(CallLog.Calls.CACHED_NAME);
  18. // Initialize the result set.
  19. String[] result = new String[cursor.getCount()];
  20. // Iterate over the result Cursor.
  21. while (cursor.moveToNext()) {
  22. String durStr = cursor.getString(durIdx);
  23. String numberStr = cursor.getString(numberIdx);
  24. String nameStr = cursor.getString(nameIdx);
  25. result[cursor.getPosition()] = numberStr + " for " + durStr + "sec" + ((null == nameStr) ? "" : " (" + nameStr + ")");
  26. Log.d(TAG, result[cursor.getPosition()]);
  27. }
  28. // Close the Cursor. cursor.close();