Reputation: 365
I am writing a new Application on Android 11 (SDK Version 30) and I simply cannot find an example on how to save a file to the external storage.
I read their documentation and now know that they basicly ignore Manifest Permissions (READ_EXTERNAL_STORAGE
and WRITE_EXTERNAL_STORAGE
). They also ignore the android:requestLegacyExternalStorage="true"
in the manifest.xml application tag.
In their documentation https://developer.android.com/about/versions/11/privacy/storage they write you need to enable the DEFAULT_SCOPED_STORAGE
and FORCE_ENABLE_SCOPED_STORAGE
flags to enable scoped storage in your app.
Where do I have to enable those?
And when I've done that how and when do I get the actual permission to write to the external storage? Can someone provide working code?
I want to save .gif, .png and .mp3 files. So I don't want to write to the gallery.
Thanks in advance.
Upvotes: 33
Views: 60212
Reputation: 697
I know this question is a bit old but here is a working solution to organize your files anywhere except the root phone directory
First In Your build.gradle file, implement the SAF framework's DocumentFile class:
implementation 'androidx.documentfile:documentfile:1.0.1'
Next Call this method which request permissions for the SAF to operate (You will only need to do this once on user install):
private void requestDocumentTreePermissions() {
// Choose a directory using the system's file picker.
new AlertDialog.Builder(this)
.setMessage("*Please Select A Folder For The App To Organize The Videos*")
.setPositiveButton("Ok", new DialogInterface.OnClickListener() {
@RequiresApi(api = Build.VERSION_CODES.Q)
@Override
public void onClick(DialogInterface dialog, int which) {
StorageManager sm = (StorageManager) getSystemService(Context.STORAGE_SERVICE);
Intent intent = sm.getPrimaryStorageVolume().createOpenDocumentTreeIntent();
String startDir = "Documents";
Uri uri = intent.getParcelableExtra("android.provider.extra.INITIAL_URI");
String scheme = uri.toString();
scheme = scheme.replace("/root/", "/document/");
scheme += "%3A" + startDir;
uri = Uri.parse(scheme);
Uri rootUri = DocumentsContract.buildDocumentUri(
EXTERNAL_STORAGE_PROVIDER_AUTHORITY,
uri.toString()
);
Uri treeUri = DocumentsContract.buildTreeDocumentUri(
EXTERNAL_STORAGE_PROVIDER_AUTHORITY,
uri.toString()
);
uri = Uri.parse(scheme);
Uri treeUri2 = DocumentsContract.buildTreeDocumentUri(
EXTERNAL_STORAGE_PROVIDER_AUTHORITY,
uri.toString()
);
List<Uri> uriTreeList = new ArrayList<>();
uriTreeList.add(treeUri);
uriTreeList.add(treeUri2);
getPrimaryVolume().createOpenDocumentTreeIntent()
.putExtra(EXTRA_INITIAL_URI, rootUri);
Intent intent2 = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
// Optionally, specify a URI for the directory that should be opened in
// the system file picker when it loads.
intent2.addFlags(
Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION
| Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
| Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
intent2.putExtra(EXTRA_INITIAL_URI, rootUri);
startActivityForResult(intent2, 99);
}
})
.setCancelable(false)
.show();
}
Next Store some Persistant Permissions:
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 99 && resultCode == RESULT_OK) {
//get back the document tree URI (in this case we expect the documents root directory)
Uri uri = data.getData();
//now we grant permanent persistant permissions to our contentResolver and we are free to open up sub directory Uris as we please until the app is uninstalled
getSharedPreferences().edit().putString(ACCESS_FOLDER, uri.toString()).apply();
final int takeFlags = (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
getApplicationContext().getContentResolver().takePersistableUriPermission(uri, takeFlags);
//simply recreate the activity although you could call some function at this point
recreate();
}
}
You can call the documentFile's rename method on the correct file
DocumentFile df = DocumentFile.fromTreeUri(MainActivity.this, uri);
df = df.findFile("CurrentName")
df.renameTo("NewName");
You Can also open InputStreams and OutputStreams using your content resolver because of the persistant URI permissions granted to your content resolver for that DocumentFile using the following snippet:
getContentResolver().openInputStream(df.getUri());
getContentResolver().openOutputStream(df.getUri());
InputStreams are for reading and OutputStreams are for saving
You can list files using
df.listFiles();
Or You can list out files using:
public static DocumentFile findFileInDirectoryMatchingName(Context mContext, Uri mUri, String name) {
final ContentResolver resolver = mContext.getContentResolver();
final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(mUri,
DocumentsContract.getDocumentId(mUri));
Cursor c = null;
try {
c = resolver.query(childrenUri, new String[]{
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
DocumentsContract.Document.COLUMN_MIME_TYPE,
DocumentsContract.Document.COLUMN_LAST_MODIFIED
}, DocumentsContract.Document.COLUMN_DISPLAY_NAME + " LIKE '?%'", new String[]{name}, null);
c.moveToFirst();
while (!c.isAfterLast()) {
final String filename = c.getString(1);
final String mimeType = c.getString(2);
final Long lastModified = c.getLong(3);
if (filename.contains(name)) {
final String documentId = c.getString(0);
final Uri documentUri = DocumentsContract.buildDocumentUriUsingTree(mUri,
documentId);
return DocumentFile.fromTreeUri(mContext, documentUri);
}
c.moveToNext();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (c != null) {
c.close();
}
}
return null;
}
Which will run faster than the df.listFiles() method
Src (This is my own implementation but here is the original SF question) Renaming Video / Image While Targeting Android 11 (Api 30)
Upvotes: 1
Reputation: 1831
Corresponding To All Api, included Api 30, Android 11 :
public static File commonDocumentDirPath(String FolderName)
{
File dir = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
{
dir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS) + "/" + FolderName);
}
else
{
dir = new File(Environment.getExternalStorageDirectory() + "/" + FolderName);
}
// Make sure the path directory exists.
if (!dir.exists())
{
// Make it, if it doesn't exit
boolean success = dir.mkdirs();
if (!success)
{
dir = null;
}
}
return dir;
}
Now, use this commonDocumentDirPath
for saving file.
A side note from comments, getExternalStoragePublicDirectory
with certain scopes are now working with Api 30, Android 11. Cheers! Thanks to CommonsWare hints.
Upvotes: 33
Reputation: 768
I'm using this method and it really worked for me I hope I can help you. Feel free to ask me if something is not clear to you
Bitmap imageBitmap;
OutputStream outputStream ;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
{
ContentResolver resolver = context.getContentResolver();
ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME,"Image_"+".jpg");
contentValues.put(MediaStore.MediaColumns.MIME_TYPE,"image/jpeg");
contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH,Environment.DIRECTORY_PICTURES + File.separator+"TestFolder");
Uri imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,contentValues);
try {
outputStream = resolver.openOutputStream(Objects.requireNonNull(imageUri) );
imageBitmap.compress(Bitmap.CompressFormat.JPEG,100,outputStream);
Objects.requireNonNull(outputStream);
Toast.makeText(context, "Image Saved", Toast.LENGTH_SHORT).show();
} catch (Exception e) {
Toast.makeText(context, "Image Not Not Saved: \n "+e, Toast.LENGTH_SHORT).show();
e.printStackTrace();
}
}
manifest file (Add Permission)
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
Upvotes: 4
Reputation: 389
private void createFile(String title) {
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("text/html");
intent.putExtra(Intent.EXTRA_TITLE, title);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, Uri.parse("/Documents"));
}
createInvoiceActivityResultLauncher.launch(intent);
}
private void createInvoice(Uri uri) {
try {
ParcelFileDescriptor pfd = getContentResolver().
openFileDescriptor(uri, "w");
if (pfd != null) {
FileOutputStream fileOutputStream = new FileOutputStream(pfd.getFileDescriptor());
fileOutputStream.write(invoice_html.getBytes());
fileOutputStream.close();
pfd.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
/////////////////////////////////////////////////////
// You can do the assignment inside onAttach or onCreate, i.e, before the activity is displayed
String invoice_html;
ActivityResultLauncher<Intent> createInvoiceActivityResultLauncher;
@Override
protected void onCreate(Bundle savedInstanceState) {
invoice_html = "<h1>Just for testing received...</h1>";
createInvoiceActivityResultLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == Activity.RESULT_OK) {
// There are no request codes
Uri uri = null;
if (result.getData() != null) {
uri = result.getData().getData();
createInvoice(uri);
// Perform operations on the document using its URI.
}
}
});
Upvotes: 7
Reputation: 9282
You can save files to the public directories on external storage.
Like Documents, Download, DCIM, Pictures and so on.
In the usual way like before version 10.
Upvotes: 10