I want to make a tool for , but I can't access its directory Com.tool.mobile in Android/data
Full Path: /Android/data/Com.tool.mobile
Tried Nothing (Because I am new I don't know much coding.
Q: How to access this specfic folder Com.tool.mobile of android/data
like this app:
Screenshot: Error - Special Permission is required
Screenshot: Dialog - Allow Tool to access files in data
App Link
okay so all of these people saying you cannot access app specific folders, are absolutely wrong, heres how u do it:
Step 1: get the permission of the folder you want,
public void openDirectory() {
String path = Environment.getExternalStorageDirectory() + "/Android/data/com.pubg.krmobile/whatever folder you want to access";
File file = new File(path);
String startDir, secondDir, finalDirPath;
if (file.exists()) {
startDir = "Android%2Fdata%2Fcom.pubg.krmobile%2Fthefilder%2Fsubfolder%2Fdeepersubfolder";
}
StorageManager sm = (StorageManager) getSystemService(Context.STORAGE_SERVICE);
Intent intent = sm.getPrimaryStorageVolume().createOpenDocumentTreeIntent();
Uri uri = intent.getParcelableExtra("android.provider.extra.INITIAL_URI");
String scheme = uri.toString();
Log.d("TAG", "INITIAL_URI scheme: " + scheme);
scheme = scheme.replace("/root/", "/document/");
finalDirPath = scheme + "%3A" + startDir;
uri = Uri.parse(finalDirPath);
intent.putExtra("android.provider.extra.INITIAL_URI", uri);
Log.d("TAG", "uri: " + uri.toString());
try {
startActivityForResult(intent, 6);
} catch (ActivityNotFoundException ignored) {
}}
step 2: write an onactivityresult to get the path
public void onActivityResult(int requestCode, int resultCode, #Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK) {
if (data != null) {
uri = data.getData();
if (uri.getPath().endsWith(".Statuses")) {
Log.d("TAG", "onActivityResult: " + uri.getPath());
final int takeFlags = data.getFlags()
& (Intent.FLAG_GRANT_READ_URI_PERMISSION);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
getContentResolver().takePersistableUriPermission(uri, takeFlags);
}
// these are my SharedPerfernce values for remembering the path
prefHelper.setIsScopePermissionGranted(true);
prefHelper.setSavedRoute(uri.toString());
// save any boolean in pref if user given the right path so we can use the path
// in future and avoid to ask permission more than one time
startActivity(new Intent(this, MainDashboardActivity.class));
finish();
} else {
// dialog when user gave wrong path
showWrongPathDialog();
}
}
}}
step 3: Get the files one by one from here
if (Build.VERSION.SDK_INT >= 29) {
// uri is the path which we've saved in our shared pref
DocumentFile fromTreeUri = DocumentFile.fromTreeUri(context, uri);
DocumentFile[] documentFiles = fromTreeUri.listFiles();
for (int i = 0; i < documentFiles.length; i++) {
documentFiles[i].getUri().toString() //uri of the document
}
}
}
and voila, you now have access to that folder using the path saved in shared prefrences
From Android 10 with Scoped Storage if you are targeting to SDK 30 you can't access to others app's files even if they are in external storage, at least until you request and got the approval from Google to do so, specifically you will need All Files Access Permission.
To know more see this video resource in YouTube from the official Android Developers channel.
You can temporary opt-out this restriction by setting your target SDK to 29 & using the requestLegacyExternalStorage flag to true in your app manifest, but you will need it to publish your app or app updates in the next November from this link about Google Play's target API level requirement:
Starting in November 2021, app updates will be required to target API
level 30 or above and adjust for behavioral changes in Android 11.
Existing apps that are not receiving updates are unaffected and can
continue to be downloaded from the Play Store. Wear OS apps must
continue to target API level 28 or higher.
Note: Before doing the request to get the approval from Google to get All Files Access Permission, see if your app really requires this permission, usually it was designed for apps that do files management or backups, and if your justifications is not good enough your request might be rejected.
Related
I'm creating an Android app which I need to unzip a file in Android/data folder, I'm already done with asking permissions to write at this folder using Scoped Storage with getContentResolver().takePersistableUriPermission() method. My question is given an Uri object representing a subfolder located in Android/data, how can I extract the zip contents in it?
I tried using default android Zip lib and Zip4j but I'm getting permission errors (I think that is related to the path).
EDIT: I don't want to unzip on the app specific data folder but in another app folder in Android/data. This is why I used Scoped Storage.
EDIT 2:
I used used this code to get access to write at Android/data:
StorageManager sm = (StorageManager) getApplicationContext().getSystemService(Context.STORAGE_SERVICE);
Intent intent = sm.getPrimaryStorageVolume().createOpenDocumentTreeIntent();
String startSubDir = "Android%2Fdata";
Uri uri = intent.getParcelableExtra("android.provider.extra.INITIAL_URI");
String scheme = uri.toString();
scheme = scheme.replace("/root/", "/document/");
scheme += "%3A" + startSubDir;
uri = Uri.parse(scheme);
intent.putExtra("android.provider.extra.INITIAL_URI", uri);
someActivityResultLauncher.launch(intent);
And onActivityResult() I requested for persistent permission:
ActivityResultLauncher<Intent> someActivityResultLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
new ActivityResultCallback<ActivityResult>() {
#Override
public void onActivityResult(ActivityResult result) {
if (result.getResultCode() == Activity.RESULT_OK) {
// There are no request codes
Intent data = result.getData();
if (data != null) {
getContentResolver().takePersistableUriPermission(data.getData(), Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
} else {
System.exit(0);
}
}
}
});
Background
There are various storage restrictions on Android 10 and 11, which also includes a new permission (MANAGE_EXTERNAL_STORAGE) to access all files (yet it doesn't allow access to really all files ) while the previous storage permission got reduced to grant access just to media files :
Apps can reach the "media" sub folder freely.
Apps can never reach "data" sub folder and especially the content.
For "obb" folder, if the app was allowed to install apps, it can reach it (to copy files to there). Otherwise it can't.
Using USB or root, you could still reach them, and as an end user you can reach them via the built-in file-manager app "Files".
The problem
I've noticed an app that somehow overcome this limitation (here) called "X-plore": Once you enter "Android/data" folder, it asks you to grant access to it (directly using SAF, somehow), and when you grant it, you can access everything in all folders of "Android" folder.
This means there might still be a way to reach it, but problem is that I couldn't make a sample that does the same, for some reason.
What I've found and tried
It seems this app targets API 29 (Android 10), and that it doesn't use the new permission yet, and that it has the flag requestLegacyExternalStorage. I don't know if the same trick they use will work when targeting API 30, but I can say that on my case, running on Pixel 4 with Android 11, it works fine.
So I tried to do the same:
I made a sample POC that targets Android API 29, has storage permissions (of all kinds) granted, including the legacy flag.
I tried to request access directly to "Android" folder (based on here), which sadly didn't work as it goes to some reason (kept going to DCIM folder, no idea why) :
val androidFolderDocumentFile = DocumentFile.fromFile(File(primaryVolume.directory!!, "Android"))
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED)
.putExtra(DocumentsContract.EXTRA_INITIAL_URI, androidFolderDocumentFile.uri)
startActivityForResult(intent, 1)
I tried various flags combinations.
When launching the app, when I reach the "Android" folder myself manually as this didn't work well, and I granted the access to this folder just like on the other app.
When getting the result, I try to fetch the files and folders in the path, but it fails to get them:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
Log.d("AppLog", "resultCode:$resultCode")
val uri = data?.data ?: return
if (!DocumentFile.isDocumentUri(this, uri))
return
grantUriPermission(packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
val fullPathFromTreeUri = FileUtilEx.getFullPathFromTreeUri(this, uri) // code for this here: https://stackoverflow.com/q/56657639/878126
val documentFile = DocumentFile.fromTreeUri(this, uri)
val listFiles: Array<DocumentFile> = documentFile!!.listFiles() // this returns just an array of a single folder ("media")
val androidFolder = File(fullPathFromTreeUri)
androidFolder.listFiles()?.forEach {
Log.d("AppLog", "${it.absoluteFile} children:${it.listFiles()?.joinToString()}") //this does find the folders, but can't reach their contents
}
Log.d("AppLog", "granted uri:$uri $fullPathFromTreeUri")
}
So using DocumentFile.fromTreeUri I could still get just "media" folder which is useless, and using the File class I could only see there are also "data" and "obb" folders, but still couldn't reach their contents...
So this didn't work well at all.
Later I've found out another app that uses this trick, called "MiXplorer". On this app, it failed to request "Android" folder directly (maybe it didn't even try), but it does grant you full access to it and its sub-folders once you allow it. And, it targets API 30, so this means it's not working just because you target API 29.
I've noticed (someone wrote me) that with some changes to the code, I could request access to each of the sub-folders separately (meaning a request for "data" and a new request for "obb"), but this is not what I see here, that apps do.
Meaning, to get to "Android" folder, I get use this Uri as a parameter for Intent.EXTRA_INITIAL_URI :
val androidUri=Uri.Builder().scheme("content").authority("com.android.externalstorage.documents")
.appendEncodedPath("tree").appendPath("primary:").appendPath("document").appendPath("primary:Android").build()
However, once you get an access to it, you won't be able to get the list of files from it, not via File, and not via SAF.
But, as I wrote, the weird thing is that if you try something similar, of getting to "Android/data" instead, you will be able to get its content:
val androidDataUri=Uri.Builder().scheme("content").authority("com.android.externalstorage.documents")
.appendEncodedPath("tree").appendPath("primary:").appendPath("document").appendPath("primary:Android/data").build()
The questions
How can I request an Intent directly to "Android" folder that will actually let me access to it, and let me get the sub-folders and their contents?
Is there another alternative for this? Maybe using adb and/or root, I could grant SAF access to this specific folder ?
Here is how it works in X-plore:
When on Build.VERSION.SDK_INT>=30,
[Internal storage]/Android/data is not accessible, java File.canRead() or File.canWrite() returns false, so we need to switch to alternative file system for files inside of this folder (and possibly also obb).
You already know how Storage access framework works, so I'll just give details about what needs to be done exactly.
You call ContentResolver.getPersistedUriPermissions() to find out if you already have saved permission for this folder. Initially you don't have it, so you ask user for permission:
To request access, use startActivityForResult with Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).putExtra(DocumentsContract.EXTRA_INITIAL_URI, DocumentsContract.buildDocumentUri("com.android.externalstorage.documents", "primary:Android"))
Here you set with EXTRA_INITIAL_URI that picker shall start directly on Android folder on primary storage, because we want access to Android folder. When your app will target API30, picker won't allow to choose root of storage, and also by getting permission to Android folder, you can work with both data and obb folders inside, with one permission request.
When user confirms by 2 clicks, in onActivityResult you'll get Uri in data which should be content://com.android.externalstorage.documents/tree/primary%3AAndroid. Make needed checks to verify that user confirmed correct folder. Then call contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION) to save permission, and you're ready.
So we're back to ContentResolver.getPersistedUriPermissions(), which contains list of granted permissions (there may be more of them), the one you've granted above looks like this: UriPermission {uri=content://com.android.externalstorage.documents/tree/primary%3AAndroid, modeFlags=3} (same Uri as you got in onActivityResult). Iterate the list from getPersistedUriPermissions to find uri of interest, if found work with it, otherwise ask user for grant.
Now you want to work with ContentResolver and DocumentsContract using this "tree" uri and your relative path to files inside of Android folder. Here is example to list data folder:
data/ is path relative to granted "tree" uri. Build final uri using either DocumentsContract.buildChildDocumentsUriUsingTree() (to list files) or DocumentsContract.buildDocumentUriUsingTree() (for working with individual files), example: DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, DocumentsContract.getTreeDocumentId(treeUri), DocumentsContract.getTreeDocumentId(treeUri)+"/data/"), you'll get uri=content://com.android.externalstorage.documents/tree/primary%3AAndroid/document/primary%3AAndroid%2Fdata%2F/children suitable for listing files in data folder. Now call ContentResolver.query(uri, ...) and process data in Cursor to get folder listing.
Similar way you work with other SAF functionality to read/write/rename/move/delete/create, which you probably already know, using ContentResolver or methods of DocumentsContract.
Some details:
it doesn't need android.permission.MANAGE_EXTERNAL_STORAGE
it works on target API 29 or 30
it works only on primary storage, not for external SD cards
for all files inside of data folder, you need to use SAF (java File won't work), just use file hierarchy relative to Android folder
in future Google may patch this hole in their "security" intentions, and this may not work after some security update
EDIT: sample code, based on Cheticamp Github sample. The sample shows the content (and file-count) of each of the sub-folders of "Android" folder:
class MainActivity : AppCompatActivity() {
private val handleIntentActivityResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode != Activity.RESULT_OK)
return#registerForActivityResult
val directoryUri = it.data?.data ?: return#registerForActivityResult
contentResolver.takePersistableUriPermission(
directoryUri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
if (checkIfGotAccess())
onGotAccess()
else
Log.d("AppLog", "you didn't grant permission to the correct folder")
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(findViewById(R.id.toolbar))
val openDirectoryButton = findViewById<FloatingActionButton>(R.id.fab_open_directory)
openDirectoryButton.setOnClickListener {
openDirectory()
}
}
private fun checkIfGotAccess(): Boolean {
return contentResolver.persistedUriPermissions.indexOfFirst { uriPermission ->
uriPermission.uri.equals(androidTreeUri) && uriPermission.isReadPermission && uriPermission.isWritePermission
} >= 0
}
private fun onGotAccess() {
Log.d("AppLog", "got access to Android folder. showing content of each folder:")
#Suppress("DEPRECATION")
File(Environment.getExternalStorageDirectory(), "Android").listFiles()?.forEach { androidSubFolder ->
val docId = "$ANDROID_DOCID/${androidSubFolder.name}"
val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(androidTreeUri, docId)
val contentResolver = this.contentResolver
Log.d("AppLog", "content of:${androidSubFolder.absolutePath} :")
contentResolver.query(childrenUri, null, null, null)
?.use { cursor ->
val filesCount = cursor.count
Log.d("AppLog", "filesCount:$filesCount")
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
val mimeIndex = cursor.getColumnIndex("mime_type")
while (cursor.moveToNext()) {
val displayName = cursor.getString(nameIndex)
val mimeType = cursor.getString(mimeIndex)
Log.d("AppLog", " $displayName isFolder?${mimeType == DocumentsContract.Document.MIME_TYPE_DIR}")
}
}
}
}
private fun openDirectory() {
if (checkIfGotAccess())
onGotAccess()
else {
val primaryStorageVolume = (getSystemService(STORAGE_SERVICE) as StorageManager).primaryStorageVolume
val intent =
primaryStorageVolume.createOpenDocumentTreeIntent().putExtra(EXTRA_INITIAL_URI, androidUri)
handleIntentActivityResult.launch(intent)
}
}
companion object {
private const val ANDROID_DOCID = "primary:Android"
private const val EXTERNAL_STORAGE_PROVIDER_AUTHORITY = "com.android.externalstorage.documents"
private val androidUri = DocumentsContract.buildDocumentUri(
EXTERNAL_STORAGE_PROVIDER_AUTHORITY, ANDROID_DOCID
)
private val androidTreeUri = DocumentsContract.buildTreeDocumentUri(
EXTERNAL_STORAGE_PROVIDER_AUTHORITY, ANDROID_DOCID
)
}
}
Well, I tried this code and it works on Android API 29, Samsung Galaxy 20FE:
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void triggerStorageAccessFramework() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
startActivityForResult(intent, REQUEST_CODE_STORAGE_ACCESS);
}
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CODE_STORAGE_ACCESS) {
Uri treeUri = null;
// Get Uri from Storage Access Framework.
treeUri = data.getData();
// Persist URI in shared preference so that you can use it later.
// Use your own framework here instead of PreferenceUtil.
MySharedPreferences.getInstance(null).setFileURI(treeUri);
// Persist access permissions.
final int takeFlags = data.getFlags()
& (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
getContentResolver().takePersistableUriPermission(treeUri, takeFlags);
createDir(DIR_PATH);
finish();
}
}
private void createDir(String path) {
Uri treeUri = MySharedPreferences.getInstance(null).getFileURI();
if (treeUri == null) {
return;
}
// start with root of SD card and then parse through document tree.
DocumentFile document = DocumentFile.fromTreeUri(getApplicationContext(), treeUri);
document.createDirectory(path);
}
I'm calling this from a button onClick:
Button btnLinkSd = findViewById(R.id.btnLinkSD);
btnLinkSd.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
triggerStorageAccessFramework();
}
});
In the UI, I'm pressing "show internal storage", I navigate to Android directory and press allow. After that, in debugging, if I try to list all files under android I'm getting a list of all directories in Data. If that's what you are looking for.
And finally, results in debug:
"Java Version Tested on Android 11"
This will copy file from assets folder to any directory inside android/data/xxx
#RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public class MainActivity extends AppCompatActivity
{
private final String EXTERNAL_STORAGE_PROVIDER_AUTHORITY = "com.android.externalstorage.documents";
private final String ANDROID_DOCID =
"primary:Android/data/xxxxFolderName";
Uri uri;
Uri treeUri;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button b=findViewById(R.id.ok);
uri = DocumentsContract.buildDocumentUri(
EXTERNAL_STORAGE_PROVIDER_AUTHORITY,
ANDROID_DOCID
);
treeUri = DocumentsContract.buildTreeDocumentUri(
EXTERNAL_STORAGE_PROVIDER_AUTHORITY,
ANDROID_DOCID
);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
openDirectory();
}
}
private Boolean checkIfGotAccess() {
List<UriPermission> permissionList = getContentResolver().getPersistedUriPermissions();
for (int i = 0; i < permissionList.size(); i++) {
UriPermission it = permissionList.get(i);
if (it.getUri().equals(treeUri) && it.isReadPermission())
return true;
}
return false;
}
#RequiresApi(api = Build.VERSION_CODES.Q)
private void openDirectory() {
if (checkIfGotAccess()) {
copyFile(treeUri);
//return;
}
Intent intent =
getPrimaryVolume().createOpenDocumentTreeIntent()
.putExtra(EXTRA_INITIAL_URI, uri);
ActivityResultLauncher<Intent> handleIntentActivityResult = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == Activity.RESULT_OK) {
if (result.getData() == null || result.getData().getData() == null)
return;
Uri directoryUri = result.getData().getData();
getContentResolver().takePersistableUriPermission(
directoryUri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
);
if (checkIfGotAccess())
copyFile(treeUri);
else
Log.d("AppLog", "you didn't grant permission to the correct folder");
}
});
handleIntentActivityResult.launch(intent);
}
#RequiresApi(api = Build.VERSION_CODES.N)
private StorageVolume getPrimaryVolume() {
StorageManager sm = (StorageManager) getSystemService(STORAGE_SERVICE);
return sm.getPrimaryStorageVolume();
}
private void copyFile(Uri treeUri) {
AssetManager assetManager = getAssets();
OutputStream out;
DocumentFile pickedDir = DocumentFile.fromTreeUri(this, treeUri);
String extension = "ini";
try {
InputStream inn = assetManager.open("xxxxfileName.ini");
assert pickedDir != null;
DocumentFile existing = pickedDir.findFile("xxxxfileName.ini");
if(existing!=null)
existing.delete();
DocumentFile newFile = pickedDir.createFile("*/" + extension, "EnjoyCJZC.ini");
assert newFile != null;
out = getContentResolver().openOutputStream(newFile.getUri());
byte[] buffer = new byte[1024];
int read;
while ((read = inn.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
inn.close();
out.flush();
out.close();
} catch (Exception fnfe1) {
fnfe1.printStackTrace();
}
}
}
I am trying to write to a file that is located in the SDCard, I found out that I need special permission for removable storage something that is not found in any known permission handler plugin for flutter (i tried simple_permission and permission_handler with no use).
I tried to acquire those permissions using the android side of things, so I wrote a simple function that would show the dialog and the user would allow the app to modify the content of the SDCard.
even after acquiring the rights to the SDCARD, I still get the same permissions denied error when trying to save files to the SDCard when using File.writeAsStringSync method.
I want to know if there is any known way/hack/workaround to save files in SDCards in flutter.
The android code i used is the same from this answer : https://stackoverflow.com/a/55024683/6641693
NOTE : I am targetting android 7 and beyond but not android 11.
I solved This, by ditching the dart file saving and using the android SAF.
First, what I did was try to get the sdCard modification permissions.
After that, I get to save the files I need.
here is the code I used to get the permissions ( aka the "allow this app to modify content on your sdCard" dialog )
public void takeCardUriPermission(String sdCardRootPath) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
File sdCard = new File(sdCardRootPath);
StorageManager storageManager = (StorageManager) getSystemService(Context.STORAGE_SERVICE);
StorageVolume storageVolume = storageManager.getStorageVolume(sdCard);
Intent intent = storageVolume.createAccessIntent(null);
try {
startActivityForResult(intent, 4010);
} catch (ActivityNotFoundException e) {
Log.e("TUNE-IN ANDROID", "takeCardUriPermission: "+e);
}
}
}
protected void onActivityResult(int requestCode, int resultCode, #Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 4010) {
Uri uri = data.getData();
grantUriPermission(getPackageName(), uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
Intent.FLAG_GRANT_READ_URI_PERMISSION);
final int takeFlags = data.getFlags() & (Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
Intent.FLAG_GRANT_READ_URI_PERMISSION);
getContentResolver().takePersistableUriPermission(uri, takeFlags);
methodChannel.invokeMethod("resolveWithSDCardUri",getUri().toString());
}
}
public Uri getUri() {
List<UriPermission> persistedUriPermissions = getContentResolver().getPersistedUriPermissions();
if (persistedUriPermissions.size() > 0) {
UriPermission uriPermission = persistedUriPermissions.get(0);
return uriPermission.getUri();
}
return null;
}
So in order to start the whole permissions acquiring process, you have to first call takeCardUriPermission and passing the URI of the sdCard path.
Note: on my FlutterActivity, i am able to get the sdCardPath directly using getExternalCacheDirs()[1].toString()
After calling takeCardUriPermission and once the allow button is pressed (or the decline) an activity result event will be called and the onActivtyResult method will be called. the requestCode check is useful when you have multiple events and you need to filter this one out.
The activity result code will give the app permissions to modify the files on the sdCard.
The getUri method is the one that we will be using afterwards when trying to save bytes to a file, it returns the URI of the SDCard that we selected (you can have multiple sdCards).
Saving Files
What I used to save a file is a straightforward method. First we need to get the URI of the sdCard and create a Documentfile out of it, then we go through the hierarchy of that directory (DocumentFile can reference files and directories) to find the needed file based on it's URI.
We do this search by splitting the file URI into parts and then navigating the hierarchy by testing if each part exists or not. Once we test all the parts we would have reached our file, if it exists, or we were stuck at the last directory we got to.
the resulting of this iteration is a DocumentFile that we can execute operations on and with.
the following is the full file saving code :
String filepath = (String) arguments.get("filepath");
final byte[] bytes = methodCall.argument("bytes");
try{
if(filepath==null || bytes==null)throw new Exception("Arguments Not found");
DocumentFile documentFile = DocumentFile.fromTreeUri(getApplicationContext(), getUri());
String[] parts = filepath.split("/");
for (int i = 0; i < parts.length; i++) {
DocumentFile nextfile = documentFile.findFile(parts[i]);
if(nextfile!=null){
documentFile=nextfile;
}
}
if(documentFile!=null && documentFile.isFile()){
OutputStream out = getContentResolver().openOutputStream(documentFile.getUri());
out.write(bytes);
out.close();
}else{
throw new Exception("File Not Found");
}
}catch (Exception e){
result.error("400",e.getMessage(),e);
return;
}
result.success(true);
Note: in my code, I am calling this under the MethodChannel's MethodCallHandler which will give me the argument I need: filePath which is the String URI of the file I want to write to and the bytes byte array representing the data I want to save. The same can be said for the result.success
The file writing code is simple: open the file, write the data and close the file.
Now that Google is deprecating the Google Drive Android API, I'm in the process of migrating my code to the Drive REST API.
I've got almost everything working, but I'm not able to replicate the functionality that creates a File Picker allowing the user the ability to choose which folder they want to save the file in their Google Drive. I am able to successfully copy a file to the user's root folder of their Google Drive, but I would prefer to create a picker that allows the user the ability to place it where they want it.
I have a file picker created to choose a file to download from Google Drive using the sample app that Google provided, but unfortunately they don't provide an example for folder choosing. Also, I was able to find documentation for creating a file picker for web applications, but not for Android apps.
I know that I can select the name of a folder to insert the file into, but how do I create a picker to allow the user to choose the folder (and name the file) using the REST API? This should be easy, and I'd like to avoid using third-party libraries.
Thank you!
This question is a bit old, so you may have figured it out already. But I just had the same problem, so perhaps also others are interested.
The issue here is that the Google Drive REST API uses IDs to identify files and folders. The API does not provide file or directory pickers, which is why you are using Android's built-in pickers. For instance, to allow the user to select a name and destination folder for a new file, you can use this intent:
Intent createDirectoryPickerIntent(String fileName, String mimeType) {
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType(mimeType);
intent.putExtra(Intent.EXTRA_TITLE, fileName);
return intent;
}
and create it in your activity:
public void openDirectoryPicker() {
if (mDriveServiceHelper != null) {
Intent pickerIntent = mDriveServiceHelper.createDirectoryPickerIntent("fileName", "text/plain");
startActivityForResult(pickerIntent, REQUEST_CODE_OPEN_DIR);
}
}
(Remember to delete the empty file in case writing the actual content in the second step fails.)
The file pickers identify files by URI and don't know anything about file IDs. Querying the files by name in order to get the id of the new file may fail as there may be duplicates in another or even the same folder. This is why Google recommends to use other means to access the files:
For clients that use the Android API's file picker (e.g. by calling
DriveClient#newOpenFileActivityIntentSender), we recommend using
Storage Access Framework (SAF) which accesses the Drive Android app's
content provider. The sample app demonstrates how to initiate the file
picker Intent and process the data it returns. (Google Drive Android API Deprecation Guide)
The sample app provides, as you stated, an example for reading a file in openFileUsingStorageAccessFramework(...). You can do the same for writing to the file you created with above directory picker. The URI is provided by the intent.
In your activity:
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent resultData) {
switch (requestCode) {
case REQUEST_CODE_SIGN_IN:
//...
}
break;
case REQUEST_CODE_OPEN_DIR:
if (resultCode == RESULT_OK && resultData != null) {
Uri uri = resultData.getData();
if (uri != null && mDriveServiceHelper != null) {
mDriveServiceHelper.writeToFileUsingStorageAccessFramework(this, getContentResolver(), uri, "some content")
.addOnSuccessListener(Void -> {
Log.v(TAG, "success");
})
.addOnFailureListener(exception -> {
Log.e(TAG, "Unable to write to file.", exception);
});
}
} else {
Log.v(TAG, "Unable to write to file.");
}
break;
}
super.onActivityResult(requestCode, resultCode, resultData);
}
and in the DriveServiceHelper very similar to the sample app:
Task<Void> writeToFileUsingStorageAccessFramework(Context context, ContentResolver contentResolver, Uri uri, String content) {
return Tasks.call(mExecutor, () -> {
// Retrieve the document's display name from its metadata.
String name;
try (Cursor cursor = contentResolver.query(uri, null, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
name = cursor.getString(nameIndex);
} else {
throw new IOException("Empty cursor returned for file.");
}
}
// Write content to the file, e.g.
try (OutputStream outputStream = contentResolver.openOutputStream(uri);) {
if (outputStream != null) {
outputStream.write(content.getBytes());
outputStream.close();
}
}
return null;
});
}
Background
On Android 4.4 (KitKat), Google has made access to the SD card quite restricted.
As of Android Lollipop (5.0), developers can use a new API that asks the user to confirm to allow access to specific folders, as written on the this Google-Groups post .
The problem
The post directs you to visit two websites:
https://android.googlesource.com/platform/development/+/android-5.0.0_r2/samples/Vault/src/com/example/android/vault/VaultProvider.java#258
This looks like an inner example (perhaps to be shown on the API demos later), but it's quite hard to understand what's going on.
http://developer.android.com/reference/android/support/v4/provider/DocumentFile.html
This is the official documentation of the new API, but it doesn't tell enough details about how to use it.
Here's what it tells you:
If you really do need full access to an entire subtree of documents,
start by launching ACTION_OPEN_DOCUMENT_TREE to let the user pick a
directory. Then pass the resulting getData() into fromTreeUri(Context,
Uri) to start working with the user selected tree.
As you navigate the tree of DocumentFile instances, you can always use
getUri() to obtain the Uri representing the underlying document for
that object, for use with openInputStream(Uri), etc.
To simplify your code on devices running KITKAT or earlier, you can
use fromFile(File) which emulates the behavior of a DocumentsProvider.
The questions
I have a few questions about the new API:
How do you really use it?
According to the post, the OS will remember that the app was given a permission to access the files/folders. How do you check if you can access the files/folders? Is there a function that returns me the list of files/folders that I can access?
How do you handle this problem on Kitkat? Is it a part of the support library?
Is there a settings screen on the OS that shows which apps have access to which files/folders?
What happens if an app is installed for multiple users on the same device?
Is there any other documentation/tutorial about this new API?
Can the permissions be revoked? If so, is there an intent that's being sent to the app?
Would asking for the permission work recursively on a selected folder?
Would using the permission also allow to give the user a chance of multiple selection by user's choice? Or does the app need to specifically tell the intent which files/folders to allow?
Is there a way on the emulator to try the new API ? I mean, it has SD-card partition, but it works as the primary external storage, so all access to it is already given (using a simple permission).
What happens when the user replaces the SD card with another one?
Lots of good questions, let's dig in. :)
How do you use it?
Here's a great tutorial for interacting with the Storage Access Framework in KitKat:
https://developer.android.com/guide/topics/providers/document-provider.html#client
Interacting with the new APIs in Lollipop is very similar. To prompt the user to pick a directory tree, you can launch an intent like this:
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
startActivityForResult(intent, 42);
Then in your onActivityResult(), you can pass the user-picked Uri to the new DocumentFile helper class. Here's a quick example that lists the files in the picked directory, and then creates a new file:
public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
if (resultCode == RESULT_OK) {
Uri treeUri = resultData.getData();
DocumentFile pickedDir = DocumentFile.fromTreeUri(this, treeUri);
// List all existing files inside picked directory
for (DocumentFile file : pickedDir.listFiles()) {
Log.d(TAG, "Found file " + file.getName() + " with size " + file.length());
}
// Create a new file and write into it
DocumentFile newFile = pickedDir.createFile("text/plain", "My Novel");
OutputStream out = getContentResolver().openOutputStream(newFile.getUri());
out.write("A long time ago...".getBytes());
out.close();
}
}
The Uri returned by DocumentFile.getUri() is flexible enough to use with may different platform APIs. For example, you could share it using Intent.setData() with Intent.FLAG_GRANT_READ_URI_PERMISSION.
If you want to access that Uri from native code, you can call ContentResolver.openFileDescriptor() and then use ParcelFileDescriptor.getFd() or detachFd() to obtain a traditional POSIX file descriptor integer.
How do you check if you can access the files/folders?
By default, the Uris returned through Storage Access Frameworks intents are not persisted across reboots. The platform "offers" the ability to persist the permission, but you still need to "take" the permission if you want it. In our example above, you'd call:
getContentResolver().takePersistableUriPermission(treeUri,
Intent.FLAG_GRANT_READ_URI_PERMISSION |
Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
You can always figure out what persisted grants your app has access to through the ContentResolver.getPersistedUriPermissions() API. If you no longer need access to a persisted Uri, you can release it with ContentResolver.releasePersistableUriPermission().
Is this available on KitKat?
No, we can't retroactively add new functionality to older versions of the platform.
Can I see what apps have access to files/folders?
There's currently no UI that shows this, but you can find the details in the "Granted Uri Permissions" section of adb shell dumpsys activity providers output.
What happens if an app is installed for multiple users on the same device?
Uri permission grants are isolated on a per-user basis, just like all other multi-user platform functionality. That is, the same app running under two different users has no overlaping or shared Uri permission grants.
Can the permissions be revoked?
The backing DocumentProvider can revoke permission at any time, such as when a cloud-based document is deleted. The most common way to discover these revoked permissions is when they disappear from ContentResolver.getPersistedUriPermissions() mentioned above.
Permissions are also revoked whenever app data is cleared for either app involved in the grant.
Would asking for the permission work recursively on a selected folder?
Yep, the ACTION_OPEN_DOCUMENT_TREE intent gives you recursive access to both existing and newly created files and directories.
Does this allow multiple selection?
Yep, multiple selection has been supported since KitKat, and you can allow it by setting EXTRA_ALLOW_MULTIPLE when starting your ACTION_OPEN_DOCUMENT intent. You can use Intent.setType() or EXTRA_MIME_TYPES to narrow the types of files that can be picked:
http://developer.android.com/reference/android/content/Intent.html#ACTION_OPEN_DOCUMENT
Is there a way on the emulator to try the new API?
Yep, the primary shared storage device should appear in the picker, even on the emulator. If your app only uses the Storage Access Framework for accessing shared storage, you no longer need the READ/WRITE_EXTERNAL_STORAGE permissions at all and can remove them or use the android:maxSdkVersion feature to only request them on older platform versions.
What happens when the user replaces the SD-card with another one?
When physical media is involved, the UUID (such as FAT serial number) of the underlying media is always burned into the returned Uri. The system uses this to connect you to the media that the user originally selected, even if the user swaps the media around between multiple slots.
If the user swaps in a second card, you'll need to prompt to gain access to the new card. Since the system remembers grants on a per-UUID basis, you'll continue to have previously-granted access to the original card if the user reinserts it later.
http://en.wikipedia.org/wiki/Volume_serial_number
In my Android project in Github, linked below, you can find working code that allows to write on extSdCard in Android 5. It assumes that the user gives access to the whole SD Card and then lets you write everywhere on this card. (If you want to have access only to single files, things get easier.)
Main Code snipplets
Triggering the Storage Access Framework:
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void triggerStorageAccessFramework() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
startActivityForResult(intent, REQUEST_CODE_STORAGE_ACCESS);
}
Handling the response from the Storage Access Framework:
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
#Override
public final void onActivityResult(final int requestCode, final int resultCode, final Intent resultData) {
if (requestCode == SettingsFragment.REQUEST_CODE_STORAGE_ACCESS) {
Uri treeUri = null;
if (resultCode == Activity.RESULT_OK) {
// Get Uri from Storage Access Framework.
treeUri = resultData.getData();
// Persist URI in shared preference so that you can use it later.
// Use your own framework here instead of PreferenceUtil.
PreferenceUtil.setSharedPreferenceUri(R.string.key_internal_uri_extsdcard, treeUri);
// Persist access permissions.
final int takeFlags = resultData.getFlags()
& (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
getActivity().getContentResolver().takePersistableUriPermission(treeUri, takeFlags);
}
}
}
Getting an outputStream for a file via the Storage Access Framework (making use of the stored URL, assuming that this is the URL of the root folder of the external SD card)
DocumentFile targetDocument = getDocumentFile(file, false);
OutputStream outStream = Application.getAppContext().
getContentResolver().openOutputStream(targetDocument.getUri());
This uses the following helper methods:
public static DocumentFile getDocumentFile(final File file, final boolean isDirectory) {
String baseFolder = getExtSdCardFolder(file);
if (baseFolder == null) {
return null;
}
String relativePath = null;
try {
String fullPath = file.getCanonicalPath();
relativePath = fullPath.substring(baseFolder.length() + 1);
}
catch (IOException e) {
return null;
}
Uri treeUri = PreferenceUtil.getSharedPreferenceUri(R.string.key_internal_uri_extsdcard);
if (treeUri == null) {
return null;
}
// start with root of SD card and then parse through document tree.
DocumentFile document = DocumentFile.fromTreeUri(Application.getAppContext(), treeUri);
String[] parts = relativePath.split("\\/");
for (int i = 0; i < parts.length; i++) {
DocumentFile nextDocument = document.findFile(parts[i]);
if (nextDocument == null) {
if ((i < parts.length - 1) || isDirectory) {
nextDocument = document.createDirectory(parts[i]);
}
else {
nextDocument = document.createFile("image", parts[i]);
}
}
document = nextDocument;
}
return document;
}
public static String getExtSdCardFolder(final File file) {
String[] extSdPaths = getExtSdCardPaths();
try {
for (int i = 0; i < extSdPaths.length; i++) {
if (file.getCanonicalPath().startsWith(extSdPaths[i])) {
return extSdPaths[i];
}
}
}
catch (IOException e) {
return null;
}
return null;
}
/**
* Get a list of external SD card paths. (Kitkat or higher.)
*
* #return A list of external SD card paths.
*/
#TargetApi(Build.VERSION_CODES.KITKAT)
private static String[] getExtSdCardPaths() {
List<String> paths = new ArrayList<>();
for (File file : Application.getAppContext().getExternalFilesDirs("external")) {
if (file != null && !file.equals(Application.getAppContext().getExternalFilesDir("external"))) {
int index = file.getAbsolutePath().lastIndexOf("/Android/data");
if (index < 0) {
Log.w(Application.TAG, "Unexpected external file dir: " + file.getAbsolutePath());
}
else {
String path = file.getAbsolutePath().substring(0, index);
try {
path = new File(path).getCanonicalPath();
}
catch (IOException e) {
// Keep non-canonical path.
}
paths.add(path);
}
}
}
return paths.toArray(new String[paths.size()]);
}
/**
* Retrieve the application context.
*
* #return The (statically stored) application context
*/
public static Context getAppContext() {
return Application.mApplication.getApplicationContext();
}
Reference to the full code
https://github.com/jeisfeld/Augendiagnose/blob/master/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/fragments/SettingsFragment.java#L521
and
https://github.com/jeisfeld/Augendiagnose/blob/master/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/util/imagefile/FileUtil.java
SimpleStorage helps you by simplifying Storage Access Framework across API levels. It works with scoped storage as well. For example:
val fileFromExternalStorage = DocumentFileCompat.fromSimplePath(context, basePath = "Downloads/MyMovie.mp4")
val fileFromSdCard = DocumentFileCompat.fromSimplePath(context, storageId = "9016-4EF8", basePath = "Downloads/MyMovie.mp4")
Granting SD card's URI permissions, picking files & folders are simpler with this library:
class MainActivity : AppCompatActivity() {
private lateinit var storageHelper: SimpleStorageHelper
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
storageHelper = SimpleStorageHelper(this, savedInstanceState)
storageHelper.onFolderSelected = { requestCode, folder ->
// do stuff
}
storageHelper.onFileSelected = { requestCode, file ->
// do stuff
}
btnRequestStorageAccess.setOnClickListener { storageHelper.requestStorageAccess() }
btnOpenFolderPicker.setOnClickListener { storageHelper.openFolderPicker() }
btnOpenFilePicker.setOnClickListener { storageHelper.openFilePicker() }
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
storageHelper.storage.onActivityResult(requestCode, resultCode, data)
}
override fun onSaveInstanceState(outState: Bundle) {
storageHelper.onSaveInstanceState(outState)
super.onSaveInstanceState(outState)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
storageHelper.onRestoreInstanceState(savedInstanceState)
}
}