jeudi 14 juin 2012

Utilisation des images Picasa de la galerie Android après un ActivityForResult sur HoneyComb et ICS

Si vous avez déjà développé une application qui utilise la galerie pour sélectionner des images sous Ice Cream Sandwich, vous vous êtes peut être demandé comment récupérer une image hébergée par Picasa retournée par le choix de l'utilisateur. Ceux qui ont essayé par la manière classique se sont surement retrouvé face à cette erreur:


Failed to find provider info for com.android.gallery3d.provider


Et pour cause, ceci est un bug de la galerie, le provider com.android.gallery3d.provider n'existe pas! c'est une erreur de la part de Google, car la galerie répond plutôt à com.google.android.gallery3d.provider. Voici comment j'ai procédé pour arriver à ce résultat:


Je vous épargne le XML, rien de plus classique ici.
Au niveau Java on appelle les applications susceptibles de nous renvoyer une image:


Intent takePictureIntent =  new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
startActivityForResult(takePictureIntent, actionCode);

Ensuite on réceptionne le résultat:

@Override
 protected void onActivityResult(int requestCode, int resultCode, Intent data)
 {
  switch (requestCode) {
   
   case ACTIVITY_SELECT_IMAGE:
   {
    if (resultCode == Activity.RESULT_OK)
    {
     Uri selectedImage = data.getData();
     final String[] columns = { MediaColumns.DATA, MediaColumns.DISPLAY_NAME };

     Cursor cursor = getContentResolver().query(selectedImage, columns, null, null, null);
     if (cursor != null)
     {
      cursor.moveToFirst();

      int columnIndex = cursor.getColumnIndex(MediaColumns.DATA);
      if (columnIndex != -1)
      {
       // regular processing for gallery files
       String fileName = cursor.getString(columnIndex);
       setPic(fileName);
      }
     } else
     {
      // Ce n'est pas une image stockée dans la gallerie
      {
       try
       {
        
        if (selectedImage.toString().startsWith("content://com.android.gallery3d.provider"))
        {
         // utiliser le provider com.google, pas
         // com.android provider.
         selectedImage = Uri.parse(selectedImage.toString().replace("com.android.gallery3d", "com.google.android.gallery3d"));
        }
        

        final InputStream inputStream = getContentResolver().openInputStream(selectedImage);
        // On lit l'image dans un InputStream

        // qu'on enregistre dans un FileOutputStream
        File f = createImageFile();
        OutputStream out = new FileOutputStream(f);

        int read = 0;
        byte[] bytes = new byte[1024];

        while ((read = inputStream.read(bytes)) != -1)
        {
         out.write(bytes, 0, read);
        }

        inputStream.close();
        out.flush();
        out.close();

        

        setPic(f.getAbsolutePath());
       }
       catch (IOException e)
       {
        Log.e("PhotoIntentActivity", "io", e);
       }
      }
     }
    }
    break;
   } // ACTIVITY_SELECT_IMAGE

  } // switch
 }

Et pour la forme voici la méthode setPic() qui me permet d'afficher l'image:

 private void setPic(String filePath)
 {

  mCurrentPhotoPath = filePath;
  // There isn't enough memory to open up more than a couple camera photos 
  // So pre-scale the target bitmap into which the file is decoded 

  // Get the size of the ImageView 
  int targetW = mImageView.getWidth();
  int targetH = mImageView.getHeight();

  // Get the size of the image 
  BitmapFactory.Options bmOptions = new BitmapFactory.Options();
  bmOptions.inJustDecodeBounds = true;
  BitmapFactory.decodeFile(filePath, bmOptions);
  int photoW = bmOptions.outWidth;
  int photoH = bmOptions.outHeight;



  // Figure out which way needs to be reduced less 
  int scaleFactor = 1;
  if ((targetW > 0) || (targetH > 0))
  {
   scaleFactor = Math.min(photoW / targetW, photoH / targetH);
  }

  // Set bitmap options to scale the image decode target 
  bmOptions.inJustDecodeBounds = false;
  bmOptions.inSampleSize = scaleFactor;
  bmOptions.inPurgeable = true;
  isFormReady = true;

  // Decode the JPEG file into a Bitmap 
  Bitmap bitmap = BitmapFactory.decodeFile(filePath, bmOptions);

  // Associate the Bitmap to the ImageView 
  mImageView.setImageBitmap(bitmap);
  mImageView.setVisibility(View.VISIBLE);
  mButtonSend.setEnabled(canSend());
 }

Il faut bien garder à l'esprit que la méthode donnée ici ne prend pas tous les cas de figure en compte, mais palie au bug de la galerie Android. Ceci n'est qu'un hack qui je l'espère sera provisoire. Si l'équipe d'Android change com.google.android.gallery3d.provider en com.android.gallery3d.provider alors cette méthode restera valide pour les futures versions, si c'est le contraire, alors il faudra mettre à jour vos appli avec de grosses verrues spécialement pour HoneyComb et IceScream Sandwich.

Le code montré ci-dessus est disponible dans les sources de mon appli: My Figure Collection

jeudi 12 avril 2012

Tutoriel Android Query: Chargement asynchrone d'images dans une listview

Ce post sera sûrement le premier d'une série de posts consacrés à une bibliothèque que je découvre, Android Query ou aQuery. Cette bibliothèque suit l'état d'esprit du framework jQuery, dans le sens elle permet au développeur de coder moins pour des taches courantes.

Voici un liste de ce qu'elle permet de faire à l'heure actuelle selon son auteur:

  • Moins de code
  • Appels AJAX
  • Chargement des images
  • Parcours des XML
  • le chainage des fonctions
  • l'attachement de listeners
  • l'authentification Facebook, Twitter, Google Reader, Picasa, Youtube, au contacts, emfin åa toutes les API Google ou même à votre propre service.
  • Ne plus se préoccuper de la fragmentation (accélération matérielle, etc...)
  • Ne plus se préoccuper de différents layout pour les différentes tailles d'écran
  • Etc...
Ça paraît prometteur sur le papier, et ça l'est, en tous cas pour ce que j'en ai testé. Pour vous en convaincre, voici une petite démo, qui est déjà en partie obsolète par rapport à la dernière version d'aQuery:



Pour vous montrer la puissance de cette librairie, voici comment tout simplement je l'ai implémentée dans mon appli pour faire un chargement passif de mes image web dans une listview en utilisant un cache sur la SDCard.


  • Tout d'abord, récupérez le projet librairie sur Google Code : http://code.google.com/p/android-query/
  • Ajoutez la bibliothèque à votre projet:
     
  • OPTIONNEL: Au chargement de mon activité, j'ai préféré désigner moi-même un répertoire de cache sur ma carte SD plutôt que de garder le répertoire par défaut dans le dossier interne de l'appli:

    // Le dossier de cache sera créé dans le répertoire de stockage "externe" dans le dossier prévu à cet effet sous le nom du package de votre appli
    String dir = Environment.getExternalStorageDirectory().getPath() + "/Android/data/" + getPackageName() + "/files/images";
    
    final File directory = new File(dir);
    
    // On teste tout de même si le stockage externe est dispo
    if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()))
    {
        // Fichier qui va indiquer au système que notre dossier ne contient rien qui intéresse la galerie 
        File nomedia = new File(dir + "/.nomedia");
        if (!nomedia.exists())
        {
            try
            {
                directory.mkdirs();
                nomedia.createNewFile();
            }
            catch (IOException e)
            {
                e.printStackTrace();
            }
        }
        AQUtility.setCacheDir(directory);
    }
    
    
    
  • Au chargement de ma ListActivity (ou de mon ListFragment en l'occurence), j'initialise une instance de aQuery:

    listAq = new AQuery(this.getActivity());

  • Dans mon adapter, je charge l'image à partir de son URL web. Si elle est déjà en cache je l'affiche, sinon je la télécharge:


    public View getView(int position, View convertView, ViewGroup parent)
    {
        View v = super.getView(position, convertView, parent);
    
        //la fonction recycle permet de ne pas dupliquer l'objet aQuery de nombreuses fois lors du parcours de la liste
        AQuery aq = listAq.recycle(v);
    
        
        final String url = "http://une.url.vers/mon.image.jpg";
        Bitmap placeholder = aq.getCachedImage(url);
    
        //Si la liste est en train de défiler rapidement on affiche l'image en mémoire ou rien (ou un placeholder, mais dans mon cas le placeholder est l'image en cache):
        if(aq.shouldDelay(position, v, parent, url))
        {
         
            aq.id(R.id.ImageView_icon).image(placeholder,1f);
     
        //Sinon on télécharge l'image     
        }else
        {
            //Si l'image était en cache en la charge
            if(placeholder!=null)
            {
                //la fonction progress gère le chargement de l'image. La vue R.id.progressBar1 reste affichée jusqu'au chargement
                aq.id(R.id.ImageView_icon).progress(R.id.progressBar1).image(placeholder, 1f);
    
            //Si non on la télécharge, toujours avec une vue de chargement
            }else
            {
                //placeholder2 remplacera l'image en cas d'échec du téléchargement
                Bitmap placeholder2 = aq.getCachedImage(R.id.placeholder);
                aq.id(R.id.ImageView_icon).progress(R.id.progressBar1).image(url, true, true, 0, 0, placeholder2, 0, 1f);
            }
        }
    
        return v;
    }
    

Voilà, vous avez un chargement asynchrone de vos images sans bug et dans un cache. Ma façon de l'implémenter est assez verbeuse, mais si vous avez des besoins moins poussés que moi quelques lignes suffisent. Lorsque je testerai d'autres fonctionnalités de aQuery je posterai mes retours. Je ne peux que vous conseiller de lire la doc de la librairie, qui est très bien fournie.

mercredi 11 avril 2012

Menu radial animé sur Android avec Satellite Menu

Au hasard d'une recherche sur des librairies utiles, je suis tombé sur un projet bien sympathique nommé Satellite Menu.

Ce projet s'inspire d'une appli qui a marqué les esprits par son menu radial très original, Path.
Le principe est très simple: un bouton circulaire en bas à gauche de l'écran. Lorsque l'on clique dessus, d'autres bouton viennent graviter autour de celui-ci pour accéder aux fonctionnalités de l'appli.




Voilà dans le principe.
Dans la pratique on va procéder comme suit:

  • Pour utiliser le fameux menu, d'abord déclarez le bouton principal dans votre layout:
     
    <framelayout 
        android:layout_height="fill_parent" 
        android:layout_width="fill_parent" 
        android:orientation="vertical" 
        xmlns:android="http://schemas.android.com/apk/res/android" 
        xmlns:sat="http://schemas.android.com/apk/res/android.view.ext">
    
        <android.view.ext.satellitemenu 
            android:id="@+id/menu" 
            android:layout_gravity="bottom|left" 
            android:layout_height="wrap_content" 
            android:layout_margin="8dp" 
            android:layout_width="wrap_content" 
            sat:closeonclick="true"  
            sat:expandduration="500" 
            sat:mainimage="@drawable/ic_launcher" 
            sat:satellitedistance="170dp" 
            sat:totalspacingdegree="90">    
        </android.view.ext.satellitemenu>
    
    </framelayout>

     Vous noterez le préfixe sat, qui permet de définir les propriété propres au menu circulaire. (vous pouvez jouer avec ces options, je pense qu'elles sont assez explicites).
  • Dans votre activité, déclarez vos boutons comme de façon similaire à un menu classique:

    SatelliteMenu menu = (SatelliteMenu) findViewById(R.id.menu);
            
    List items = new ArrayList();
    items.add(new SatelliteMenuItem(4, R.drawable.ic_1));
    items.add(new SatelliteMenuItem(4, R.drawable.ic_3));
    items.add(new SatelliteMenuItem(4, R.drawable.ic_4));
    items.add(new SatelliteMenuItem(3, R.drawable.ic_5));
    items.add(new SatelliteMenuItem(2, R.drawable.ic_6));
    items.add(new SatelliteMenuItem(1, R.drawable.ic_2));
    
    menu.addItems(items);        
            
    menu.setOnItemClickedListener(new SateliteClickedListener() {
       
     public void eventOccured(int id) {
      Log.i("sat", "Clicked on " + id);
     }
    });
    
Voilà, vous savez désormais comment créer un beau menu radial original pour votre appli Android.

vendredi 6 avril 2012

Vérifier une adresse e-mail en Java

Petit article rapide aujourd'hui, comme l'indique le titre, comment vérifier la validité d'une adresse e-mail sous Android ou en Java.

Il y a plusieurs méthodes, la première étant de se hisser sur les épaules des spécialistes du web, Apache, et leur classe de validation EmailValidator.
Un simple appel au singleton et le tour est joué:

if(EmailValidator.getInstance().isValid("test.mail@gmail.com"))
{
    Log.d(TAG,"Cette adresse est valide");
} 

Une autre méthode consiste à utiliser une expression régulière. Voici une méthode java avec celle utilisée par la messagerie K-9:


 
public final static Pattern EMAIL_ADDRESS_PATTERN = Pattern.compile(
           "[a-zA-Z0-9\\+\\.\\_\\%\\-\\+]{1,256}" +
           "\\@" +
           "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}" +
           "(" +
           "\\." +
           "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25}" +
           ")+"
       );

public static boolean isValiEmail(String email)
{
    return EMAIL_ADDRESS_PATTERN.matcher(email).matches();
}




Maintenant à vous de choisir, il existe d'autres expressions régulière et d'autres méthodes, et je suis prèt à en débattre avec vous :)

samedi 24 mars 2012

Déverouillage ICS désactivé par admin., règles chiffrement ou stockage des identifiants

Si comme moi il vous est arrivé cette envie de désactiver le Face Lock ou le PIN de votre téléphone ICS et que vous vous retrouvez avec certaines options de dévérouillage grisées (Aucun, Faire glisser...) avec ce beau message "disabled by administrator encryption policy or credential storage" ("désactivé par admin., règles chiffrement ou stockage des identifiants"), pas de panique.

 Cela provient probablement du fait que vous avez un compte Exchange sur votre mobile, ou comme dans mon cas, configuré un VPN. Si vous n'avez plus besoin de l'un où de l'autre, il vous suffit d'effacer vos certificats et tout devrait revenir dans l'ordre:

mardi 21 février 2012

Sauvegarder l'état des variables d'une activité Android lors de la rotation

Si vous développez des appli Android, il vous est sûrement déjà arrivé de devoir remplir passer des données d'une activité à une autre, et de vous apercevoir que lorsque vous tourniez votre téléphone les données du bundle étaient perdues.

Cela est une problématique classique du cycle de vie d'une activité sur Android. En effet, lors d'une rotation, votre activité est détruite, et elle se rappelle elle-même en quelque sorte. De ce faite, toutes les données qui avaient été stockées dans les variables automatiques sont initialisées à nouveau. Une façon de conserver ces données serait de passer les variables en statiques, et c'est une erreur que je considérerais de débutant, que j'ai faite à mes débuts. Sur le principe cela fonctionne, mais le code n'est pas propre.

Le meilleure pratique est, je pense, de considérer la surcharge de la méthode onSaveInstanceState(Bundle icicle) de la classe Activity. Prenons une activité classique:

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


public class testActivity extends Activity
{
 
 String test;
 
 @Override
 protected void onCreate(Bundle savedInstanceState)
 {
  super.onCreate(savedInstanceState);
  test = getIntent().getExtras().getString("test", "test perdu");
  Log.d("test bundle", test);
 }

}

Si vous avez lancé l'activité depuis une autre activité de cette façon:

Intent i = new Intent(this, testActivity.class);
i.putExtra("test", "test ok");
startActivity(i);

Votre logcat va afficher test ok.
Si vous faites une rotation de l'écran, votre logcat affiche test perdu. C'est là qu'intervient onSaveInstanceState(Bundle icicle). Celui-ci va être appelé avant la destruction de l'activité. Nous allons donc l'implémenter ainsi:

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

public class testActivity extends Activity
{
 String test;

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

  test = getIntent().getExtras().getString("test");
  if (test == null) savedInstanceState.getString("test", "test non défini");

  Log.d("test bundle", test);
 }

 @Override
 protected void onSaveInstanceState(Bundle outState)
 {
  outState.putString("test", test);
  super.onSaveInstanceState(outState);
 }

}

Comme on peut le voir ici, on place dans le bundle outState toutes les données que l'on veut sauvegarder. Nous avons légèrement modifier le traitement de la méthode onCreate(Bundle savedInstanceState) pour récupérer ces données que nous avons stockée dans outState. Donc on récupère, par exemple, les extras qui auraient du être passés par l'activité précédente. Si ces extras ont disparu à la rotation, on regarde si l'on ne peut pas les récupérer dans le savedInstanceState, qui est une copie du bundle outState précédent.

Bien sûr, cela ne fonctionne pas qu'avec les String, toutes les données stockables dans un bundle peuvent être sauvegardées dans outState de onSaveInstance et récupérée dans le savedInstance de onCreate après la rotation.

Vous pouvez désormais vous passer de ces vilaines variables statiques que l'on ne saurait voir et faire des rotations d'activité en conservant vos données :)

vendredi 10 février 2012

Liste d'arguments variable en objective-C et Java


Pour changer aujourd'hui un post non pas axé sur un language mais sur une façon de coder. Je vais vous expliquer comment créer une fonction varargs ou en français variadique, c'est à dire une fonction qui prend un nombre arbitraire d'opérandes.

Il vous est sûrement déjà arrivé de créer des fonctions que prennent des tableaux d'objets en paramètre. C'est très bien quand on ne sait pas quels éléments vont constituer le tableau, mais lorsque savez que vous coderez toujours en dur les paramètres de la fonction, je ne pense pas qu'il soit judicieux de créer et allouer un tableau. Voici comment je procède en Objective-C sur iPhone en Java sur Android.

Sur iPhone, tout d'abord, dans le .h, je déclare par exemple ceci:

- (void) maFonctionAvecUnParametre:(id)param1
        etUneListeVariable:(id)param2,...
        NS_REQUIRES_NIL_TERMINATION;

Vous remarquerez que le paramètre "etUneListeVariable" et constitué d'un paramètre, ici de type id car je n'ai pas voulu me limiter à un seul type, trois points et une macro système. Voici comment les interpréter:

  • le paramètre va définir tout simplement le type des arguments qui vont suivre
  • les trois petits points suivant une virgule annoncent que le nombre de paramètres est variable
  • le NS_REQUIRES_NIL_TERMINATION n'est pas indispensable, c'est une macro qui déclare au compilateur que la liste des paramètres doit se terminer par un "objet nul", soit nil en objective-C.
l'utilisation dans le .m se fait de cette façon:

-(void) maFonctionAvecUnParametre:(id)param1 etUneListeVariable:(id)param2,...
{
    va_list args;
    va_start(args, param2);

    id valeur = param2;

    //si le premier argument des varargs n'est pas nul
    if(valeur)
    {
        //on boucle sur les éléments la liste des paramètres variable en commençant par le premier, ici param2 
        do {
         //ici mes traitement sur l'objet valeur
        } while ((valeur = va_arg( args, id )));
    }
}
Exemple d'utilisation:

[monObjet maFonctionAvecUnParametre:toto etUneListeVariable:titi, tata, tutu, nil];

[monObjet2 maFonctionAvecUnParametre:toto etUneListeVariable:titi, tata, tutu, tyty, nil];


Voilà pour l'objective-C.
En ce qui concerne le Java et entre autres Android, rien de plus simple, trois petits points suffisent, le langage assimile la liste lui-même à un tableau:


private static void traiterMaListe(Object... params)
{
     for (Object param : params)
     {
          //traitement sur mon param
     }
}

Exemple:

monObjet.traiterMaListe(toto, tata, titi);
monObjet2.traiterMaListe(toto, tata, titi, tutu, tyty);

Pratique n'est-ce pas :)

Voilà pour mon domaine de compétence. Il faut savoir que la syntaxe d'objective-C est directement issue du C, et que d'autres langages comme le PHP par exemple implémentent les fonctions variadiques. Si vous voulez les implémenter dans un de ces langages, cherchez tout simplement varargs + votre langage sur Google, ou regardez sur Wikipedia :)

Twitter