7 votes

Comment mettre en œuvre un ContentProvider pour fournir des images à Gmail, Facebook, Evernote, etc.

Ma question précédente ( Est-il possible de partager une image sur Android via une URL de données ? ) est lié à cette question. J'ai compris comment partager une image de mon application vers une autre application sans avoir la permission d'écrire des fichiers sur le stockage externe. Cependant, j'obtiens toujours un certain nombre de comportements problématiques :

  1. Lorsque j'essaie de partager l'image à partir de mon téléphone (Android 2.2.2), des erreurs fatales se produisent dans les applications réceptrices, et l'image ne s'affiche pas du tout. (Cela pourrait-il être le résultat d'une opération dans mon application qui n'est pas prise en charge par Android 2.2.2 ? Ou cela aurait-il provoqué une erreur dans mon application plutôt que dans l'application cible) ?
  2. Quand j'essaie de partager l'image avec Evernote, tout fonctionne bien, mais parfois quelques secondes après l'enregistrement de la note, je reçois un message en bas de l'écran de mon application (de l'application Evernote) : "java.lang.SecurityException : Déni de permission : ouverture du fournisseur com.enigmadream.picturecode.PictureContentProvider à partir du ProcessRecord{413db6d0 1872:com.evernote/u0a10105} (pid=1872, uid=10105) qui n'est pas exporté à partir de l'uid 10104".
  3. Lorsque j'essaie de partager l'image sur Facebook, il y a un rectangle pour l'image, mais aucune image dedans.

Voici mon code ContentProvider. Il doit y avoir un moyen plus facile et/ou plus approprié d'implémenter un ContentProvider basé sur des fichiers (en particulier la fonction de requête). Je pense que la plupart des problèmes proviennent de l'implémentation de la requête. Ce qui est intéressant, c'est que ce fait fonctionnent très bien sur mon Nexus 7 lorsqu'on va sur GMail. Le nom d'affichage et la taille de la pièce jointe sont également pris en compte.

public class PictureContentProvider extends ContentProvider implements AutoAnimate {
    public static final Uri CONTENT_URI = Uri.parse("content://com.enigmadream.picturecode.snapshot/picture.png");
    private static String[] mimeTypes = {"image/png"};
    private Uri generatedUri;

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
       throw new RuntimeException("PictureContentProvider.delete not supported");
    }

    @Override
    public String getType(Uri uri) {
       return "image/png";
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
       throw new RuntimeException("PictureContentProvider.insert not supported");
    }

    @Override
    public boolean onCreate() {
       generatedUri = Uri.EMPTY;
       return true;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
          String[] selectionArgs, String sortOrder) {
       long fileSize = 0;
       MatrixCursor result = new MatrixCursor(projection);
       File tempFile;
       try {
          tempFile = generatePictureFile(uri);
          fileSize = tempFile.length();
       } catch (FileNotFoundException ex) {
          return result;
       }
       Object[] row = new Object[projection.length];
       for (int i = 0; i < projection.length; i++) {

          if (projection[i].compareToIgnoreCase(MediaStore.MediaColumns.DISPLAY_NAME) == 0) {
             row[i] = getContext().getString(R.string.snapshot_displaystring);
          } else if (projection[i].compareToIgnoreCase(MediaStore.MediaColumns.SIZE) == 0) {
             row[i] = fileSize;
          } else if (projection[i].compareToIgnoreCase(MediaStore.MediaColumns.DATA) == 0) {
             row[i] = tempFile;
          } else if (projection[i].compareToIgnoreCase(MediaStore.MediaColumns.MIME_TYPE)==0) {
             row[i] = "image/png";
          }
       }

       result.addRow(row);
       return result;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection,
          String[] selectionArgs) {
       throw new RuntimeException("PictureContentProvider.update not supported");
    }

    @Override
    public String[] getStreamTypes(Uri uri, String mimeTypeFilter) {
       return mimeTypes;
    }

    private File generatePictureFile(Uri uri) throws FileNotFoundException {
       if (generatedUri.compareTo(uri)==0)
          return new File(getContext().getFilesDir(), "picture.png");;
          Context context = getContext();
          String query = uri.getQuery();
          String[] queryParts = query.split("&");
          String pictureCode = "016OA";
          int resolution = 36;
          int frame = 0;
          int padding = 0;
          for (String param : queryParts) {
             if (param.length() < 2)
                continue;
             if (param.substring(0,2).compareToIgnoreCase("p=") == 0) {             
                pictureCode = param.substring(2);
             } else if (param.substring(0,2).compareToIgnoreCase("r=") == 0) {
                resolution = Integer.parseInt(param.substring(2));              
             } else if (param.substring(0, 2).compareToIgnoreCase("f=") == 0) {
                frame = Integer.parseInt(param.substring(2));
             } else if (param.substring(0, 2).compareToIgnoreCase("a=") == 0) {
                padding = Integer.parseInt(param.substring(2));
             }
          }
          Bitmap picture = RenderPictureCode(pictureCode, resolution, frame, padding);
          File tempFile = new File(context.getFilesDir(), "picture.png");       
          FileOutputStream stream;
          stream = new FileOutputStream(tempFile);
          picture.compress(CompressFormat.PNG, 90, stream);
          try {
             stream.flush();
             stream.close();
          } catch (IOException e) {
             e.printStackTrace();
             throw new Error(e);
          }
          picture.recycle();
          generatedUri = uri;
          return tempFile;
    }

    @Override
    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
       File tempFile = generatePictureFile(uri);
       return ParcelFileDescriptor.open(tempFile, ParcelFileDescriptor.MODE_READ_ONLY);
    }
...
}

Je l'ai également placé dans le fichier AndroidManifest.xml en tant que membre de la famille du fichier <activity> éléments :

    <provider 
        android:name="PictureContentProvider"
        android:authorities="com.enigmadream.picturecode.snapshot"
        android:grantUriPermissions="true"
        android:readPermission="com.enigmadream.picturecode.snapshot"
        tools:ignore="ExportedContentProvider">
        <grant-uri-permission android:path="/picture.png" />
    </provider>

Le code qui crée l'intention ressemble à ceci :

        resolution = mPicView.getWidth();
        if (mPicView.getHeight() > resolution)
            resolution = mPicView.getHeight();
        String paddingText = mPadding.getEditableText().toString();
        int padding;
        try {
            padding = Integer.parseInt(paddingText);
        } catch (NumberFormatException ex) {
            padding = 0;
        }
        Uri uri = Uri.parse(PictureContentProvider.CONTENT_URI 
            + "?p=" + Uri.encode(mPicView.getPictureCode()) + "&r=" + Integer.toString(resolution) 
            + "&f=" + Integer.toString(mPicView.getFrame()) + "&a=" + Integer.toString(padding));
        Intent share = new Intent(Intent.ACTION_SEND);
        share.setType("image/png");
        share.putExtra(Intent.EXTRA_STREAM, uri);
        share.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.share_subject_made));
        share.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        startActivity(Intent.createChooser(share, getString(R.id.menu_share)));

EDIT Voici les deux premières lignes de la trace de la pile lorsque l'erreur se produit sur mon téléphone :

04-07 13:56:24.423 : E/DatabaseUtils(19431) : java.lang.SecurityException : Déni de permission : lecture uri de com.enigmadream.picturecode.PictureContentProvider content://com.enigmadream.picturecode.snapshot/picture.png?p=01v131&r=36&f=0&a=0 à partir de pid=19025, uid=10062 exige com.enigmadream.picturecode.snapshot

04-07 13:56:24.423 : E/DatabaseUtils(19431) : at Android.content.ContentProvider$Transport.enforceReadPermission(ContentProvider.java:271)

2voto

jcasner Points 335

J'ai eu des problèmes avec le partage vers certains clients de messagerie et d'email à cause de la query méthode. Certaines applications destinataires envoient en null pour le projection paramètre. Lorsque cela se produit, votre code lance un NullPointerException . Le NPE est facile à résoudre. Cependant, les applications qui envoient null J'ai encore besoin de quelques informations en retour. Je ne peux toujours pas partager avec Facebook, mais je peux le faire avec toutes les autres applications que j'ai testées :

EDIT Je n'arrive pas non plus à le faire fonctionner avec Google Hangout. Avec cela, au moins, j'obtiens un toast indiquant You can't send this file on Hangouts. Try using a picture . Voir aussi cette question : L'image a été supprimée après avoir été envoyée via Google hangout . Je suppose que c'est parce que j'utilise du contenu privé et que Hangouts ne peut pas/ne veut pas l'accepter pour une raison quelconque.

@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
        String sortOrder) {
    if (projection == null) {
        projection = new String[] {
                MediaStore.MediaColumns.DISPLAY_NAME,
                MediaStore.MediaColumns.SIZE,
                MediaStore.MediaColumns._ID,
                MediaStore.MediaColumns.MIME_TYPE
        };
    }

    final long time = System.currentTimeMillis();
    MatrixCursor result = new MatrixCursor(projection);
    final File tempFile = generatePictureFile(uri);

    Object[] row = new Object[projection.length];
    for (int i = 0; i < projection.length; i++) {

       if (projection[i].compareToIgnoreCase(MediaStore.MediaColumns.DISPLAY_NAME) == 0) {
          row[i] = uri.getLastPathSegment();
       } else if (projection[i].compareToIgnoreCase(MediaStore.MediaColumns.SIZE) == 0) {
          row[i] = tempFile.length();
       } else if (projection[i].compareToIgnoreCase(MediaStore.MediaColumns.DATA) == 0) {
          row[i] = tempFile;
       } else if (projection[i].compareToIgnoreCase(MediaStore.MediaColumns.MIME_TYPE)==0) {
          row[i] = _mimeType;
       } else if (projection[i].compareToIgnoreCase(MediaStore.MediaColumns.DATE_ADDED)==0 ||
               projection[i].compareToIgnoreCase(MediaStore.MediaColumns.DATE_MODIFIED)==0 ||
               projection[i].compareToIgnoreCase("datetaken")==0) {
           row[i] = time;
       } else if (projection[i].compareToIgnoreCase(MediaStore.MediaColumns._ID)==0) {
           row[i] = 0;
       } else if (projection[i].compareToIgnoreCase("orientation")==0) {
           row[i] = "vertical";
       }
    }

    result.addRow(row);
    return result;
}

1voto

CommonsWare Points 402670

Je ne connais pas les autorisations dont j'ai besoin ou non, ni comment les désactiver.

Essayez de le remplacer :

<provider 
    android:name="PictureContentProvider"
    android:authorities="com.enigmadream.picturecode.snapshot"
    android:grantUriPermissions="true"
    android:readPermission="com.enigmadream.picturecode.snapshot"
    tools:ignore="ExportedContentProvider">
    <grant-uri-permission android:path="/picture.png" />
</provider>

avec :

<provider 
    android:name="PictureContentProvider"
    android:authorities="com.enigmadream.picturecode.snapshot"
    android:exported="true"
    tools:ignore="ExportedContentProvider">
</provider>

Vous auriez vraiment dû android:exported="true" dans le premier aussi, mais le changement de permissions auquel je faisais référence représente la suppression de android:readPermission y <grant-uri-permissions >.

Ensuite, débarrassez-vous de addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); dans votre code Java.

Cela met en place votre ContentProvider pour être lisible dans le monde entier. À long terme, ce n'est peut-être pas la bonne solution. À court terme, cela vous aidera à déterminer si votre problème avec Android 2.2.2 est dû au fait que FLAG_GRANT_READ_URI_PERMISSION n'est pas honorée.

0voto

varren Points 4621

J'ai eu le même problème et après beaucoup de recherche et de débogage, voici mes 5 centimes : (sur mes appareils Android 2.3.3 et 4.2)

  1. L'application Facebook n'utilisera pas openFile(Uri uri, String mode) Nous devons donc lui donner un véritable URI (dans projection[i] == MediaStore.MediaColumns.DATA) avec droits de lecture .
  2. Vous devez donner à Facebook toutes les projections qu'il vous demande, sinon FB-app ne fera que répéter. query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)

Il s'agit probablement d'un piratage, et cela ne fonctionnerait pas sur certaines données sensibles, mais vous pouvez faire file.setReadable(true, false); sur le fichier interne quelque part dans votre code et facebook sera en mesure de voir et d'envoyer l'image. Pas besoin des autorisations de stockage externe d'Android. Il n'y a probablement pas besoin de fournisseur de contenu si l'img est lisible. On peut simplement envoyer le lien dans l'intention. Mais j'ai eu un comportement étrange de la prévisualisation de Google+ sans le fournisseur, alors j'ai décidé de le garder. Je n'ai pas beaucoup d'appareils à tester et je n'utilise que Twitter, FB, Google+, Skype et Gmail. Cela fonctionne bien pour l'instant.

Voici Démonstration GitHub :

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X