简体   繁体   中英

How to use Intent.ACTION_OPEN_DOCUMENT in Android Pie

I'm making a profile photo change function using Retrofit in android pie.

So I succeeded in uploading the photos taken with the camera to the server. But I don't know how to transfer the photos selected from the gallery to my server. (I'm fine with any kind of code in Java Kotlin.)

I will upload the video later.

I searched a lot on Google, but it was hard to get the information I wanted.

Well done in the google doc but i don't know how to do it. https://developer.android.com/guide/topics/providers/document-provider

The Google doc shows examples of using bitmap or inputstream or something.

Do I need a bitmap or inputstream to upload photos using Retrofit?

I actually need a valid uri.

public void performFileSearch() {
        Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        intent.setType("image/*");
        startActivityForResult(intent, PICTURES_DIR_ACCESS_REQUEST_CODE);
}


@Override
public void onActivityResult(int requestCode, int resultCode,Intent resultData) {
    if (requestCode == READ_REQUEST_CODE && resultCode ==Activity.RESULT_OK) {
        Uri uri = null;
        if (resultData != null) {
            uri = resultData.getData();
            Log.i(TAG, "Uri: " + uri.toString());
            showImage(uri);
        }
    }
}


public void Edit_Profile (String Image_Uri) {
    File file = new File(Image_Uri);
    RequestBody requestBody = RequestBody.create(file, MediaType.parse("image/*"));

    MultipartBody.Part body = MultipartBody.Part.createFormData("uploaded_file", Num+ID+".jpg", requestBody);
}

In fact, onActivityResult returns the following type of uri.

content://com.android.providers.media.documents/document/image%3A191474

So when I try to send it to my server using that uri I get a FileNotFoundException error.

This is a privacy restriction introduced in Android-Q. Direct access to shared/external storage devices is deprecated when an app targets API 29 and the path returned from getExternalStorageDirectory method is no longer directly accessible to apps. Use the app-specific directory to write & read files.

By default, apps targeting Android 10 and higher are given scoped access into external storage , or scoped storage. Such apps can see the following types of files within an external storage device without needing to request any storage-related user permissions:

Files in the app-specific directory, accessed using getExternalFilesDir(). Photos, videos, and audio clips that the app created from the media store.

Go through the documentation Open files using storage access framework

Coming to the context, One thing that you can do is that as CommonsWare suggested use InputStreamRequestBody . Otherwise, copy the selected file to your app sandbox folder IE, app-specific directory, then access the file from there without any permission. Just see the below implementation that works in Android-Q and later.

Perform file search

private void performFileSearch(String messageTitle) {
        Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
        intent.setType("application/*");
        String[] mimeTypes = new String[]{"application/x-binary,application/octet-stream"};
        if (mimeTypes.length > 0) {
            intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
        }

        if (intent.resolveActivity(getPackageManager()) != null) {
            startActivityForResult(Intent.createChooser(intent, messageTitle), OPEN_DIRECTORY_REQUEST_CODE);
        } else {
            Log.d("Unable to resolve Intent.ACTION_OPEN_DOCUMENT {}");
        }
    }

onActivityResult returned

@Override
public void onActivityResult(int requestCode, int resultCode, final Intent resultData) {
        // The ACTION_OPEN_DOCUMENT intent was sent with the request code OPEN_DIRECTORY_REQUEST_CODE.
        // If the request code seen here doesn't match, it's the response to some other intent,
        // and the below code shouldn't run at all.
        if (requestCode == OPEN_DIRECTORY_REQUEST_CODE) {
            if (resultCode == Activity.RESULT_OK) {
                // The document selected by the user won't be returned in the intent.
                // Instead, a URI to that document will be contained in the return intent
                // provided to this method as a parameter.  Pull that uri using "resultData.getData()"
                if (resultData != null && resultData.getData() != null) {
                    new CopyFileToAppDirTask().execute(resultData.getData());
                } else {
                    Log.d("File uri not found {}");
                }
            } else {
                Log.d("User cancelled file browsing {}");
            }
        }
    }

File writing to app specific path

public static final String FILE_BROWSER_CACHE_DIR = "CertCache";

@SuppressLint("StaticFieldLeak")
private class CopyFileToAppDirTask extends AsyncTask<Uri, Void, String> {
    private ProgressDialog mProgressDialog;

    private CopyFileToAppDirTask() {
        mProgressDialog = new ProgressDialog(YourActivity.this);
    }

    @Override
    protected void onPreExecute() {
        super.onPreExecute();
        mProgressDialog.setMessage("Please Wait..");
        mProgressDialog.show();
    }

    protected String doInBackground(Uri... uris) {
        try {
            return writeFileContent(uris[0]);
        } catch (IOException e) {
            Log.d("Failed to copy file {}" + e.getMessage());
            return null;
        }
    }

    protected void onPostExecute(String cachedFilePath) {
        mProgressDialog.dismiss();
          if (cachedFilePath != null) {
                Log.d("Cached file path {}" + cachedFilePath);
            } else {
               Log.d("Writing failed {}");
         }

    }
}

private String writeFileContent(final Uri uri) throws IOException {
    InputStream selectedFileInputStream =
            getContentResolver().openInputStream(uri);
    if (selectedFileInputStream != null) {
        final File certCacheDir = new File(getExternalFilesDir(null), FILE_BROWSER_CACHE_DIR);
        boolean isCertCacheDirExists = certCacheDir.exists();
        if (!isCertCacheDirExists) {
            isCertCacheDirExists = certCacheDir.mkdirs();
        }
        if (isCertCacheDirExists) {
            String filePath = certCacheDir.getAbsolutePath() + "/" + getFileDisplayName(uri);
            OutputStream selectedFileOutPutStream = new FileOutputStream(filePath);
            byte[] buffer = new byte[1024];
            int length;
            while ((length = selectedFileInputStream.read(buffer)) > 0) {
                selectedFileOutPutStream.write(buffer, 0, length);
            }
            selectedFileOutPutStream.flush();
            selectedFileOutPutStream.close();
            return filePath;
        }
        selectedFileInputStream.close();
    }
    return null;
}

  // Returns file display name.
    @Nullable
    private String getFileDisplayName(final Uri uri) {
        String displayName = null;
        try (Cursor cursor = getContentResolver()
                .query(uri, null, null, null, null, null)) {
            if (cursor != null && cursor.moveToFirst()) {
                displayName = cursor.getString(
                        cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
                Log.i("Display Name {}" + displayName);

            }
        }

        return displayName;
    }

Here's a perhaps more general solution that allows the AsyncTask class to be separate, rather than embedded in an activity. It also returns a response to your activity when the task is complete, and uses a ProgressBar rather than the deprecated ProgressDialog.

The async task:

public class FileLoader extends AsyncTask<Uri, Void, String>
{
    private WeakReference<Context> contextRef;
    public AsyncResponse delegate = null;

    public interface AsyncResponse {
    void fileLoadFinish(String result);
    }
    FileLoader(Context ctx , AsyncResponse delegate) {
        contextRef = new WeakReference<>(ctx);
        this.delegate =  delegate;
    }

    @Override
    protected void onPreExecute() {
        super.onPreExecute();
    }

    protected String doInBackground(Uri... uris) {
        Context context = contextRef.get();
         ContentResolver contentResolver = context.getContentResolver();

        Uri uri = uris[0];
        try {
            String mimeType = contentResolver.getType(uri);
            Cursor returnCursor =
                contentResolver.query(uri, null, null, null, null);
            int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
            returnCursor.moveToFirst();
            String fileName = returnCursor.getString(nameIndex);
            InputStream inputStream =  contentResolver.openInputStream(uri);

            File downloadDir = 
              context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS);
            File f = new File(downloadDir +  "/" + fileName);

            FileOutputStream out = new FileOutputStream(f);
            IOUtils.copyStream(inputStream,out);
            returnCursor.close();
            return  f.getPath();
        }
        catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }

    protected void onPostExecute(String result) {
        delegate.fileLoadFinish(result);
        super.onPostExecute(result);
    }
}

and in your activity:

private static final int DIR_ACCESS_REQUEST_CODE = 13;
public void performFileSearch() {
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
    intent.addCategory(Intent.CATEGORY_OPENABLE);
    intent.setType("application/*");
    String[] mimeTypes = new String[]{"application/gpx+xml","application/vnd.google-earth.kmz"};
    intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
    if (intent.resolveActivity(getPackageManager()) != null) {
        startActivityForResult(Intent.createChooser(intent, "Choose KMZ or GPX file"), DIR_ACCESS_REQUEST_CODE);
    } else {
        Log.d("****File","Unable to resolve Intent.ACTION_OPEN_DOCUMENT");
    }
}


@Override
public void onActivityResult(int requestCode, int resultCode,Intent resultData)
{
    super.onActivityResult(requestCode, resultCode, resultData);
    if (requestCode == DIR_ACCESS_REQUEST_CODE && resultCode == Activity.RESULT_OK)
    {
        if (resultData != null)
        {
            Uri uri = resultData.getData();

            mProgressBar.setVisibility(View.VISIBLE);
            new FileLoader(this,
                new FileLoader.AsyncResponse(){
                    @Override
                    public void fileLoadFinish(String result){
                        processFile(new File(result));
                        mProgressBar.setVisibility(View.GONE);
                    }
                }).execute(uri);
         }
    }
}

My example attempts to find either.kmz files or.gpx files. The progress bar (if you need one, for long-running file operations) needs to be initialised (and hidden) in OnCreate():

mProgressBar = findViewById(R.id.progressbar);
mProgressBar.setVisibility(View.GONE);

My 'processFile()' method takes a while manipulating a map in the main activity so I have waited until it's done before hiding the ProgressBar.

I remain surprised that it takes such a lot of code to perform such a simple operation: to copy a file and make it available for use!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM