Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 541
Robert Ruxandrescu Avatar asked Jan 27 '23 06:01

Robert Ruxandrescu


1 Answers

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 (and ACTION_OPEN_DOCUMENT) 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.

like image 143
CommonsWare Avatar answered Jan 28 '23 20:01

CommonsWare