Create a file from a photo URI on Android
I have an Android app that needs to let the user select some pictures from the gallery and send these pictures to the backend (together with some other data).
To allow the user to select the pictures I have the following in my Fragment:
private void pickImages() {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("image/*");
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
startActivityForResult(intent, PICK_PHOTO_FOR_AVATAR);
}
I get the result of the selected photos by the user in here:
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == PICK_PHOTO_FOR_AVATAR && resultCode == Activity.RESULT_OK) {
if (data == null) {
//Display an error
Toast.makeText(getActivity(), "There was an error getting the pictures", Toast.LENGTH_LONG).show();
return;
}
ClipData clipData = data.getClipData();
String fileName = null, extension = null;
//if ClipData is null, then we have a regular file
if (clipData == null) {
//get the selected file uri
fileName = FileUtils.getPath(getActivity(), data.getData());
//obtain the extension of the file
int index = fileName.lastIndexOf('.');
if (index > 0) {
extension = fileName.substring(index + 1);
if (extension.equals("jpg") || extension.equals("png") || extension.equals("bmp") || extension.equals("jpeg"))
isAttachedFile = true;
}
}
ArrayList<Uri> photosUris = new ArrayList<>();
//for each image in the list of images, add it to the filesUris
if (clipData != null) for (int i = 0; i < clipData.getItemCount(); i++) {
ClipData.Item item = clipData.getItemAt(i);
Uri uri = item.getUri();
switch (i) {
case 0:
picture1Uri = uri;
break;
case 1:
picture2Uri = uri;
break;
}
photosUris.add(uri);
}
else if (isAttachedFile) {
Uri uri = Uri.parse(fileName);
picture1Uri = uri;
photosUris.add(uri);
}
uris = photosUris;
if (picture1Uri != null) {
image1.setVisibility(View.VISIBLE);
image1.setImageURI(picture1Uri);
}
if (picture2Uri != null) {
image2.setVisibility(View.VISIBLE);
image2.setImageURI(picture2Uri);
}
}
I then send the list of URIs to the Presenter, where I execute my MultiPart Retrofit call to the backend:
//obtain the file(s) information of the message, if any
if (uris != null && uris.size() > 0) {
for (int i = 0; i < uris.size(); i++) {
File file = null;
//this is the corect way to encode the pictures
String encodedPath = uris.get(i).getEncodedPath();
file = new File(encodedPath);
builder.addFormDataPart("photos[]", file.getName(), RequestBody.create(MediaType.parse("multipart/form-data"), file));
}
}
MultipartBody requestBody = builder.build();
//send the newly generated ticket
Call<GenerateNewTicketResponse> generateNewTicketCall = OperatorApplication.getApiClient().generateNewTicket(Constants.BEARER + accessToken, requestBody);
The problem is that this sometimes works, sometimes doesn't. Sometimes I get the error "java.io.FileNotFoundException", which throws me in the onFailure()
callback of the Retrofit call.
I found the following stackoverflow post Reading File from Uri gives java.io.FileNotFoundException: open failed: ENOENT but I'm not exactly sure how to implement the general suggestion in that response to my particular situation.
What would be the right way to get the right path towards the pictures selected by the user such that I can create files out of them and attach them in my MultiPart request?
Commonsware suggested to
Use a ContentResolver and openInputStream() to get an InputStream on the content pointed to by the Uri. Then, pass that to your decoding logic, such as BitmapFactory and its decodeStream() method.
, but I'm not sure exactly how to do that programmatically.
Any help would be appreciated.
Solution 1:
To allow the user to select the pictures I have the following in my Fragment:
This code is using ACTION_GET_CONTENT
. Particularly on Android 7.0+, generally that will return Uri
values with a content
scheme. Your code assumes that you are getting Uri
values with a file
scheme, where the path actually has meaning. Moreover, your code assumes that the user is picking files on the filesystem that you can access, and there is nothing that forces the user to do that. ACTION_GET_CONTENT
can be supported by apps where their content is:
- A local file on external storage
- A local file on internal storage for the other app
- A local file on removable storage
- A local file that is encrypted and needs to be decrypted on the fly
- A stream of bytes held in a
BLOB
column in a database - A piece of content on the Internet that needs to be downloaded by the other app first
- Content that is generated on the fly
- ...and so on
Instead of using RequestBody.create()
, use the InputStreamRequestBody
from this OkHttp issue comment. You provide the same media type as before, but instead of a File
(that you are incorrectly creating), you provide a ContentResolver
(from getContentResolver()
on a Context
) and the Uri
.
This blog post demonstrates how to use InputStreamRequestBody
(specifically a Kotlin port of the original) to upload content in this fashion. This blog post provides another look at the same problem and a similar solution.