How to implement a ContentProvider for providing image to Gmail, Facebook, Evernote, etc
Asked Answered
C

3

7

My previous question (Is it possible to share an image on Android via a data URL?) is related to this question. I have figured out how to share an image from my application to another application without having permission to write files to external storage. However, I do still get a number of problem behaviors:

  1. When I try to share the image from my phone (Android 2.2.2), fatal errors occur in the receiving applications, and they doesn't come up with the image at all. (Could this be a result of some operation in my App that isn't supported on Android 2.2.2? Or would that have caused an error in my app rather than the target app?)
  2. When I try to share the image to Evernote, everything works fine, but sometimes a few seconds after the note is saved, I get a message at the bottom of my app's screen (from the Evernote App): "java.lang.SecurityException: Permission Denial: opening provider com.enigmadream.picturecode.PictureContentProvider from ProcessRecord{413db6d0 1872:com.evernote/u0a10105} (pid=1872, uid=10105) that is not exported from uid 10104"
  3. When I try to share the picture to Facebook, there's a rectangle for the picture, but no picture in it.

Below is my ContentProvider code. There must be an easier and/or more proper way of implementing a file-based ContentProvider (especially the query function). I expect a lot of the problems come from the query implementation. The interesting thing is, this does work very nicely on my Nexus 7 when going to GMail. It picks up the correct display name and size for the attachment too.

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);
    }
...
}

I also have this in the AndroidManifest.xml file as a sibling of the <activity> elements:

    <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>

The code that creates the intent looks like this:

        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 Here are the first two lines of the stack trace when the error occurs on my phone:

04-07 13:56:24.423: E/DatabaseUtils(19431): java.lang.SecurityException: Permission Denial: reading com.enigmadream.picturecode.PictureContentProvider uri content://com.enigmadream.picturecode.snapshot/picture.png?p=01v131&r=36&f=0&a=0 from pid=19025, uid=10062 requires com.enigmadream.picturecode.snapshot

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

Citrate answered 7/4, 2013 at 18:14 Comment(7)
"When I try to share the image from my phone (Android 2.2.2), fatal errors occur in the receiving applications, and they doesn't come up with the image at all" -- there should be stack traces for these. As I have never seen a content:// Uri use query parameters, you might experiment with removing them. Go with a REST-style Uri syntax instead (content://com.enigmadream.picturecode.snapshot/p/.../r/.../f/.../a/.../picture.png). You might also temporarily disable the required permissions and see if that helps identify your issue.Hutner
I don't know how I could get a stack trace of the error if it's not occurring in my own application. Also, I know the query parameters are working in some circumstances because I get images generated according to the parameters. Do you think the treatment of query parameters in a content URI would be a difference between Android versions? Also, I just read up on the meaning of REST at en.wikipedia.org/wiki/Representational_state_transfer and didn't see anything about query strings (the word query doesn't even occur on that page). How is your example more "RESTful"?Citrate
Also, isn't there going to be a problem if I try to embed the parameters in the path in that the permissions will not apply to all the different paths any more?Citrate
"I don't know how I could get a stack trace of the error if it's not occurring in my own application" -- LogCat, the same place you see your own stack traces. "Do you think the treatment of query parameters in a content URI would be a difference between Android versions?" -- possibly. "How is your example more "RESTful"?" -- what I meant was a Uri sans query parameters. "isn't there going to be a problem if I try to embed the parameters in the path in that the permissions will not apply to all the different paths any more?" -- use pathPrefix in your manifest instead of path.Hutner
@Hutner Does the stack trace I added to my question give any indication of where I should focus my efforts?Citrate
It would suggest that you temporarily disable the required permissions and see what happens. For example, it's possible that the FLAG_GRANT_READ_URI_PERMISSION does not work with a Uri in an extra on Android 2.2.2 but does on Android 4.2.Hutner
Could you add a formal answer covering some basic level of information on the permissions involved in this sort of solution where we can continue this "discussion"? I'm not familiar with the permissions I am or am not requiring or how to disable them. It's possible my lack of understanding of permissions here is where I need to focus, and that understanding it could be the answer to my question. The only reason I have any permission code at all is because I copied it from examples.Citrate
W
2

I had issues with sharing to some email and messaging clients because of the query method. Some recipient apps send in null for the projection parameter. When that happens, your code throws a NullPointerException. The NPE is easy to solve. However, the apps that send null still require some information back. I still can't share to Facebook, but I can share to all other apps I've tested using:

EDIT I also cannot get it to work with Google Hangout. With that, at least, I get a toast indicating You can't send this file on Hangouts. Try using a picture. See also this question: Picture got deleted after send via Google hangout. I assume this is because I'm using private content and Hangouts can't / won't accept it for some reason.

@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;
}
Whoopee answered 10/3, 2014 at 20:55 Comment(0)
H
1

I'm not familiar with the permissions I am or am not requiring or how to disable them

Try replacing:

<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>

with:

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

You really should have android:exported="true" in the first one too, but the permissions change I was referring to represents the removal of android:readPermission and <grant-uri-permissions>.

Then, get rid of addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); in your Java code.

This sets up your ContentProvider to be world-readable. Long-term, that might not be the right answer. Short-term, it will help to determine if your Android 2.2.2 problem is because FLAG_GRANT_READ_URI_PERMISSION is not being honored.

Hutner answered 7/4, 2013 at 19:19 Comment(2)
That change does allow me to share the image via a GMail message on my Android 2.2 device. I can't think of any reason this application would need any degree of security. I have not needed to request any permissions whatsoever for it (that's why I was interested in continuing that) so it doesn't have much privilege on the device. It doesn't save any data except the temporary PNG based on the "code" entered in the UI (which is like source code, not a security code). Any reason not to leave it like this? Facebook still won't show, should I ask about that in a separate question or...?Citrate
@BlueMonkMN: "Any reason not to leave it like this?" -- I have no really good way to answer that. If you are comfortable with any app making any request of your provider at any time, go with it. "Facebook still won't show" -- if it is crashing, the stack trace probably differs from the "Permission Denial" one you cited above.Hutner
O
0

Had the same problem and after lots of research and debuging here is my 5 cents: (on my android 2.3.3 and 4.2 devices)

  1. Facebook app will not use openFile(Uri uri, String mode) so we have to give it real URI (in projection[i] == MediaStore.MediaColumns.DATA) with read rights.
  2. You have to give Facebook every projection it asks you, otherwise FB-app will just repeate query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)

It is probably a hack, and wouldn't work on some sensitive data, but you can make file.setReadable(true, false); on internal file somewhere in your code and facebook will be able to see and send the img. No need for android external storage permissions. Probably there is no need for content provider if img will be readable. Can just send link in Intent. But i had strange google+ preview bechavior without provider so decided to keep it. Don't have lots of devices to test and use only Twitter, FB , Google+ ,Skype, Gmail. Works fine for now

Here is GitHub Demo:

Orthorhombic answered 2/4, 2015 at 1:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.