Create/Copy File in Android Q using MediaStore
Asked Answered
A

3

28

I am trying to find method which can handle create and copy of any file except Media files (Picture/Video/Audio) to copy from one place to other in internal storage in Android Q. In this I have my file created in my app folder and I want those to move to Download Folder or some directory which I can create in Internal storage and then move their.

I searched and found modified below code but missing some thing to make it workable. Can some one help.

ContentResolver contentResolver = getContentResolver();

ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, "sam.txt");
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "text/plain");
contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS);

Uri uri = contentResolver.insert(MediaStore.Files.getContentUri("external"), contentValues);

try {
    InputStream inputStream = contentResolver.openInputStream(uri);
    OutputStream outputStream = new FileOutputStream(Environment.DIRECTORY_DOWNLOADS+"/");

    byte[] buffer = new byte[1024];

    int length;

    //copy the file content in bytes
    while ((length = inputStream.read(buffer)) > 0) {
            outputStream.write(buffer, 0, length);
        }
        inputStream.close();
        outputStream.close();
        } catch (Exception e) {
           e.printStackTrace();
        }

Is above complete code as I'm getting error 'Unknown URL'. What is missing? Please help.

Alagoas answered 28/12, 2019 at 13:2 Comment(11)
Any specific reason for relying on ContentResolver? you can read the byte data from the source location and write the byte content in new file on the desired location?Topknot
else, how can i get path of download folder as Environment.getExternalStoragePublicDirectory is deprecated in Android Q. if u can suggest any other option i m ok.Alagoas
Uri.fromFile(file) Replace by MediaStore.Files.getContentUri("external").Claudiaclaudian
The insert() returns you an uri. Use it to open an OutputStream.Claudiaclaudian
proandroiddev.com/working-with-scoped-storage-8a7e7cafea3Claudiaclaudian
@blackapps, getting error no such file. I m trying to copy file from app folder in Android/data/packagename/.. to Download Directory. Have i captured the input correctly? Where we telling code path of input file?Alagoas
Path of input file is irrelevant for the mediastore. You obtain an uri from the store. For the uri you open an output stream. And you also open a FileInputStream for the input file. Then you copy the stream.Claudiaclaudian
@Claudiaclaudian i have updated code as u guided, if path of input file is irrelevant, why i m getting NotFileFound exception. how to fix it.Alagoas
Uri uri = contentResolver.insert(MediaStore.Files.getContentUri("external"), contentValues); giving null uri.Alagoas
You should use the 'uri' for the OutputStream. I already told you twice before.Claudiaclaudian
if path of input file is irrelevant, It is only irrelevant for obtaining an output uri from the media store. Of course it is relevant the moment you open an input streram to do the copy.Claudiaclaudian
A
52

NOTE: If you reinstall the app, MediaStore will not recognize the previous-created file any more: Android 11 cannot retrieve files create with MediaStore after the app re-installs, using Intent to let user pick file is the only solution.


1. Create and Write File

createAndWriteButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        try {
            ContentValues values = new ContentValues();

            values.put(MediaStore.MediaColumns.DISPLAY_NAME, "menuCategory");       //file name                     
            values.put(MediaStore.MediaColumns.MIME_TYPE, "text/plain");        //file extension, will automatically add to file
            values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOCUMENTS + "/Kamen Rider Decade/");     //end "/" is not mandatory

            Uri uri = getContentResolver().insert(MediaStore.Files.getContentUri("external"), values);      //important!

            OutputStream outputStream = getContentResolver().openOutputStream(uri);

            outputStream.write("This is menu category data.".getBytes());

            outputStream.close();

            Toast.makeText(view.getContext(), "File created successfully", Toast.LENGTH_SHORT).show();
        } catch (IOException e) {
            Toast.makeText(view.getContext(), "Fail to create file", Toast.LENGTH_SHORT).show();
        }
    }
});

2. Find and Read File

findAndReadButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        Uri contentUri = MediaStore.Files.getContentUri("external");

        String selection = MediaStore.MediaColumns.RELATIVE_PATH + "=?";

        String[] selectionArgs = new String[]{Environment.DIRECTORY_DOCUMENTS + "/Kamen Rider Decade/"};

        Cursor cursor = getContentResolver().query(contentUri, null, selection, selectionArgs, null);

        Uri uri = null;

        if (cursor.getCount() == 0) {
            Toast.makeText(view.getContext(), "No file found in \"" + Environment.DIRECTORY_DOCUMENTS + "/Kamen Rider Decade/\"", Toast.LENGTH_LONG).show();
        } else {
            while (cursor.moveToNext()) {
                String fileName = cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME));

                if (fileName.equals("menuCategory.txt")) {
                    long id = cursor.getLong(cursor.getColumnIndex(MediaStore.MediaColumns._ID));

                    uri = ContentUris.withAppendedId(contentUri, id);

                    break;
                }
            }

            if (uri == null) {
                Toast.makeText(view.getContext(), "\"menuCategory.txt\" not found", Toast.LENGTH_SHORT).show();
            } else {
                try {
                    InputStream inputStream = getContentResolver().openInputStream(uri);

                    int size = inputStream.available();

                    byte[] bytes = new byte[size];

                    inputStream.read(bytes);

                    inputStream.close();

                    String jsonString = new String(bytes, StandardCharsets.UTF_8);

                    AlertDialog.Builder builder = new AlertDialog.Builder(view.getContext());

                    builder.setTitle("File Content");
                    builder.setMessage(jsonString);
                    builder.setPositiveButton("OK", null);

                    builder.create().show();
                } catch (IOException e) {
                    Toast.makeText(view.getContext(), "Fail to read file", Toast.LENGTH_SHORT).show();
                }
            }
        }
    }
});

3. Find and Overwrite File

findAndWriteButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        Uri contentUri = MediaStore.Files.getContentUri("external");

        String selection = MediaStore.MediaColumns.RELATIVE_PATH + "=?";

        String[] selectionArgs = new String[]{Environment.DIRECTORY_DOCUMENTS + "/Kamen Rider Decade/"};    //must include "/" in front and end

        Cursor cursor = getContentResolver().query(contentUri, null, selection, selectionArgs, null);

        Uri uri = null;

        if (cursor.getCount() == 0) {
            Toast.makeText(view.getContext(), "No file found in \"" + Environment.DIRECTORY_DOCUMENTS + "/Kamen Rider Decade/\"", Toast.LENGTH_LONG).show();
        } else {
            while (cursor.moveToNext()) {
                String fileName = cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME));

                if (fileName.equals("menuCategory.txt")) {                          //must include extension
                    long id = cursor.getLong(cursor.getColumnIndex(MediaStore.MediaColumns._ID));

                    uri = ContentUris.withAppendedId(contentUri, id);

                    break;
                }
            }

            if (uri == null) {
                Toast.makeText(view.getContext(), "\"menuCategory.txt\" not found", Toast.LENGTH_SHORT).show();
            } else {
                try {
                    OutputStream outputStream = getContentResolver().openOutputStream(uri, "rwt");      //overwrite mode, see below

                    outputStream.write("This is overwritten data。\n你就不要想起我。".getBytes());

                    outputStream.close();

                    Toast.makeText(view.getContext(), "File written successfully", Toast.LENGTH_SHORT).show();
                } catch (IOException e) {
                    Toast.makeText(view.getContext(), "Fail to write file", Toast.LENGTH_SHORT).show();
                }
            }
        }
    }
});

Demo: https://www.youtube.com/watch?v=idsUMiWjfnM

Hope this may help you.

Arsenide answered 13/7, 2020 at 15:27 Comment(20)
it will store in external storage or internal storage?Cleanlimbed
@Cleanlimbed external, you can check out my demo: youtube.com/watch?v=idsUMiWjfnMArsenide
@SamChen how to insert files to lower APIs? Since RELATIVE_PATH is a new constant, I cant use that... :(Launcelot
@Sourav Kannantha B For me I just abandon them : ). For old devices you should use the old methods, just search other posts like "Android Create Save File in External Storage".Arsenide
@SamChen Thanks.. I know the old method.. But will it be compatible? If a user upgrades from Android9 to 10, then files saved using old method will be available/detectable? in mediastore queryLauncelot
@Sourav Kannantha B I don't know, I've never tried. You can create a post.Arsenide
This has been incredibly helpful for some upcoming work on Android TV. Thanks!Bering
how to save image in DCIM folder of external SDCARD?Lead
@Milan Tejani Say your app called Sam, then you just set the RELATIVE_PATH to Environment.DIRECTORY_DCIM + "/Sam/".Arsenide
I have created a folder named "SUS" inside download folder and saving files in it using Media Store API. I am able to access to all the saved files but if I am copying some files from PC into SUS folder then it is not listing because it is not added using MediaStore API. How to access the copied files here? Any suggestionsPissed
@vijaya zararia Use Intent with ACTION_GET_CONTENT, this is the only way.Arsenide
Will this help to list down all the files under SUS folder?Pissed
@vijaya zararia Yes.Arsenide
@SamChen I want to list all the files of SUS folder in my recyclerview. I had created this SUS folder using Mediastore api. There are couple of files also created using mediastore api. Some files are copied from my PC in the same SUS folder. I want to show all this files in the recyclerview copied one and generated from app. Can you share code snippet ? It would be a great help.Pissed
@vijaya zararia Why do you keep asking the same question? I've given you the answer already.Arsenide
@SamChen how to do the same for an android version less than Q. Also how to request write permission as adding in manifest doesn't seems to work.Miserere
@Kundan Jha Please read the 4th comment.Arsenide
how to view pdf file after download it in android 11? using intentKcal
I'm getting this error : java.lang.IllegalArgumentException: Uri lacks 'file' scheme: content://media/external/downloads/77952.Daman
@YasirAli I tried this startActivity(Intent(DownloadManager.ACTION_VIEW_DOWNLOADS))Daman
D
3

As you mentioned Environment.getExternalStoragePublicDirectory is marked deprecated. So there is no regular way to get the path to Downloads directory to save your file there. Alternatively you can use ACTION_CREATE_DOCUMENT to show path picker and then use returned uri to write file to selected location.

This is how to show picker:

// Request code for creating a document.

const val CREATE_FILE = 1

private fun createFile(pickerInitialUri: Uri) {
    val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
        addCategory(Intent.CATEGORY_OPENABLE)
        type = "text/plain"
        putExtra(Intent.EXTRA_TITLE, "sam.txt")

        // Optionally, specify a URI for the directory that should be opened in
        // the system file picker before your app creates the document.
        putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri)
    }
    startActivityForResult(intent, CREATE_FILE)
}

And this is how to get selected uri and write file:

override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
    if (requestCode == CREATE_FILE && resultCode == Activity.RESULT_OK) {
        // The result data contains a URI for the document or directory that
        // the user selected.
        resultData?.data?.also { outputUri ->
            // Perform operations on the document using its URI.
            FileInputStream(inputFile).use { inputStream ->
                context.contentResolver.openFileDescriptor(outputUri, "w")?.use {
                    FileOutputStream(it.fileDescriptor).use { outputStream ->
                        FileUtils.copy(inputStream, outputStream)
                    }
                }
            }
        }
    }
}

More information can be found here.

EDIT:

To pick a directory to persist files ACTION_OPEN_DOCUMENT_TREE can be used. Then use takePersistableUriPermission method to take granted persistable permission to be able to use it after device restart. And then use DocumentFile to execute file operations.

Open directory request:

private static final int OPEN_DIRECTORY_REQUEST_CODE = 1;

void openDirectory() {
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
    intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
    startActivityForResult(intent, OPEN_DIRECTORY_REQUEST_CODE);
}

Receive picked directory and take persistable permission:

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == OPEN_DIRECTORY_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
        Uri directoryUri = data.getData();
        if (directoryUri == null)
            return;
        requireContext()
                .getContentResolver()
                .takePersistableUriPermission(directoryUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        // persist picked uri to be able to reuse it later
    } else
        super.onActivityResult(requestCode, resultCode, data);
}

And at last persist the file:

private void persistFile(@NonNull Uri directoryUri,
                         @NonNull File fileToPersist,
                         @NonNull String mimeType,
                         @NonNull String displayName) {
    DocumentFile dirFile = DocumentFile.fromSingleUri(requireContext(), directoryUri);
    if (dirFile != null) {
        DocumentFile file = dirFile.createFile(mimeType, displayName);
        if (file != null) {
            Uri outputUri = file.getUri();
            try (ParcelFileDescriptor fd = requireContext().getContentResolver().openFileDescriptor(outputUri, "w")) {
                if (fd != null) {
                    try (FileInputStream inputStream = new FileInputStream(fileToPersist)) {
                        try (FileOutputStream outputStream = new FileOutputStream(fd.getFileDescriptor())) {
                            FileUtils.copy(inputStream, outputStream);
                        }
                    }
                }
            } catch (Throwable th) {
                th.printStackTrace();
            }
        }
    }
}

Review this repo for an example of ACTION_CREATE_DOCUMENT usage.

Division answered 28/12, 2019 at 20:0 Comment(5)
Pls share code for onActivityResult in Java, Also 2 doubts: 1. My File is stored at Android/data/Files/sam.txt which i want to copy to Download folder or user selected folder using SAF now(as guided you), so where this path will be mentioned. 2. Can i save this uri for future so that user need not to ask everytime and code automatically copy file there.Alagoas
@Alagoas I would recommend you to think twice before moving or copying your file to external storage randomised path. In future version of Android, the app has to submit the usecase to google play console to enable the access of external storage. For more details, please look into the talk of scoped storage in Android dev summit 2019Topknot
@Alagoas 1. it is inputFile variable in my second snippet, it can be a member of activity or fragment. 2. made an edit in the postDivision
I would recommend you to think twice before moving or copying your file to external storage randomised path. Sorry but your remark to OP's problem is not to the point. OP tries to copy an existing file to the MediaStore which is ok. Using the MediaStore one can copy to Documents and Download directory on external storage without any permission. @Vaikundam RaghulClaudiaclaudian
@Division OP tries to copy an existing file to the MediaStore. ACTION_OPEN_DOCUMENT_TREE and DocumentFile have nothing to do with a solution.Claudiaclaudian
O
0

You can create a function that takes in a file and contentResolver and uses input and output streams to copy file.

// A class representing a file (You can use DocumentFile directly instead if you like)
data class LocalMedia(
    val name: String,
    val mimeType: String,
    val uri: Uri = Uri.EMPTY,
)

class MyViewModel: ViewModel() {

    private val downloadsFolder = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)

    @Suppress("BlockingMethodInNonBlockingContext")
    fun copyDocumentFile(
        contentResolver: ContentResolver,
        localMedia: LocalMedia,
    ) {
        viewModelScope.launch {
            withContext(Dispatchers.IO) {
                var inputStream: InputStream? = null
                var outputStream: OutputStream? = null
                try {
                    val outFile = DocumentFile.fromFile(downloadsFolder).createFile(localMedia.mimeType, localMedia.name)
                    if (outFile != null) {
                        inputStream = contentResolver.openInputStream(localMedia.uri)
                        outputStream = contentResolver.openOutputStream(outFile.uri)
                        if (inputStream != null && outputStream != null) {
                            outputStream.write(inputStream.readBytes())
                        }
                    }
                } catch (e: IOException) {
                    log("Failed to save file. $e")
                    _state.update { it.copy(error = e.message) }
                } finally {
                    inputStream?.close()
                    outputStream?.close()
                }
            }
        }
    }

}

Oneidaoneil answered 7/9, 2022 at 11:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.