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:
- 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?)
- 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"
- 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)
content://
Uri
use query parameters, you might experiment with removing them. Go with a REST-styleUri
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. – HutnerUri
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?" -- usepathPrefix
in your manifest instead ofpath
. – HutnerFLAG_GRANT_READ_URI_PERMISSION
does not work with aUri
in an extra on Android 2.2.2 but does on Android 4.2. – Hutner