Unzip a file using Scoped Storage in Android - android

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);
}
}
}
});

Related

I Use Storage Access Framework to browse a file link f.cfg, when you select the file, the app need to load another file like f.dat automatically, how?

In Android Studio:
This app must use COMTRADE files. The COMTRADE files are *.cfg *.dat and *.rio files. For example, (user.cfg, user.dat and user.rio). The main file is *.cfg file, so the user must browse by SAF to find *.cfg files, when the user select this file, the app must load other files ( *.dat and *.rio) automatically.
I can not use <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/> for android level 30
So I have to use Storage Access Framework, By below code the user browse *.cfg files and select a file like user.cfg
if(SDK_INT >= Build.VERSION_CODES.R)
{
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
Act.startActivityForResult(intent, 111);
}
else
{...}
Then SAF return URI of that user.cfg file, and the app does not have any URI of other files to load.
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
super.onActivityResult(requestCode, resultCode, data);
if(requestCode == 111)
{
if (resultCode == Activity.RESULT_OK)
{
if (data != null)
{
try
{
Uri uri = null;
uri = data.getData();
InputStreamReader inputStreamReader = new InputStreamReader(getContentResolver().openInputStream(uri));
BufferedReader br = new BufferedReader(inputStreamReader);
// I want URI of user.dat how?
InputStreamReader inputStreamReader1 = new InputStreamReader(getContentResolver().openInputStream(uri1));
BufferedReader br1 = new BufferedReader(inputStreamReader1);
....
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
}
}
}
Please help me how can I do this? Thanks.
Please help me how can I do this?
You cannot do this, at least by means of using ACTION_OPEN_DOCUMENT, unless you ask the user to open each of your three documents in turn. Just because the user selected a .cfg file does not mean that you have any rights to any other files, including those adjacent to it.
You could try using ACTION_OPEN_DOCUMENT_TREE and let the user choose the document tree that contains your desired files. From there, you can:
Use DocumentFile.fromTreeUri() to get a DocumentFile representing the tree
Call listFiles() on that DocumentFile to get the direct contents of that tree, in the form of a list of DocumentFile objects
Call getName() on each of those to get their display names, then see which names have your desired file extensions and matching base names (foo.cfg and foo.rio and foo.dat)
For those that you want to use, call getUri() on the DocumentFile to get a Uri to use with ContentResolver and openInputStream() to read in the content

How get permission for specific folder

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.

How do some apps reach the contents of ".../Android/..." sub-folders on Android 11 without root?

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();
}
}
}

Is there any way of writing files to SDCard with flutter File.writeAsStringSync

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.

Storage Access Framework Getting Correct Uri Path Delete/Edit/Get File

TL:DR; I explained how to use create folders and subfolders using DocumentFile and how to delete file created using this class. Uri returned from onActvityResult() and documentFile.getUri.toString() are not same. My question is how to get a valid Uri to manipulate folders and files without using SAF UI, if possible not without using hack.
Let me share what i've learned so far and ask my questions.
If you want get Uri of folder and work on it, you should use Intent with ACTION_OPEN_DOCUMENT_TREE to get an Uri to access folders and set W/R permission for that uri.
Persistible permission granted onActivityResult with:
final int takeFlags = data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
// Check for the freshest data.
getContentResolver().takePersistableUriPermission(treeUri, takeFlags);
If you select the main folder of device:
Uri treeUri = data.getData();
treeUri.toString()
Returns: content://com.android.externalstorage.documents/tree/primary:
File mediaStorageDir = new File(Environment.getExternalStorageDirectory(), "");
Returns: storage/emulated/0
new File(treeUri.toString()).getAbsolutePath();
Returns: content:/com.android.externalstorage.documents/tree/primary:
If you use the DocumentFile class for getting path of the main folder you get
DocumentFile saveDir = null;
saveDir = DocumentFile.fromFile(Environment.getExternalStorageDirectory());
String uriString = saveDir.getUri().toString();
Returns: file:///storage/emulated/0
My first question is how can get the Uri with content using DocumentFile class.
I'm building a photography app and as default i'd like to set an initial folder for images using DocumentFile class.
#TargetApi(19)
protected DocumentFile getSaveDirMainMemory() {
DocumentFile saveDir = null;
saveDir = DocumentFile.fromFile(Environment.getExternalStorageDirectory());
// saveDir =
// DocumentFile.fromFile(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM));
// saveDir =
// DocumentFile.fromFile(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES));
DocumentFile newDir = null;
/*
* Check or create Main Folder
*/
// Check if main folder exist
newDir = saveDir.findFile(DIR_MAIN);
// Folder does not exist, create it
if (newDir == null || !newDir.exists()) {
newDir = saveDir.createDirectory(DIR_MAIN);
}
/*
* Check or create Sub-Folder
*/
DocumentFile newSubDir = null;
// Check if sub-folder exist
newSubDir = newDir.findFile(DIR_SUB);
// Folder does not exist, create it
if (newSubDir == null || !newSubDir.exists()) {
newSubDir = newDir.createDirectory(DIR_SUB);
}
if (newSubDir != null && newSubDir.exists()) {
return newSubDir;
} else if (newDir != null && newDir.exists()) {
return newDir;
} else {
return saveDir;
}
}
This method creates DIR_MAIN/DIR_SUB inside main memory of the device or PICTURES or DCIM folder depending on choice. Using this default folder i save images to this created sub folder.
I get newSubDir.getUri().toString(): file:///storage/emulated/0/MainFolder/SubFolder I named DIR_MAIN MainFolder, DIR_SUB: SubFolder to test.
To access or delete images i use this path and image name i created as
DocumentFile imageToDeletePath = DocumentFile.fromFile(new File(lastSavedImagePath));
DocumentFile imageToDelete = imageToDeletePath.findFile(lastSavedImageName);
imageDelete returns null because Uri is not in correct format.
If i open SAF ui and get UI onActivityResult and save it as string i use this method to get a directory and check Uri permissions
#TargetApi(19)
protected DocumentFile getSaveDirNew(String uriString) {
DocumentFile saveDir = null;
boolean canWrite = isUriWritePermission(uriString);
if (canWrite) {
try {
saveDir = DocumentFile.fromTreeUri(MainActivity.this, Uri.parse(uriString));
} catch (Exception e) {
saveDir = null;
}
}
return saveDir;
}
Check if Uri from string has write permission, it may not have if you don't take or remove persistable permissions.
private boolean isUriWritePermission(String uriString) {
boolean canWrite = false;
List<UriPermission> perms = getContentResolver().getPersistedUriPermissions();
for (UriPermission p : perms) {
if (p.getUri().toString().equals(uriString) && p.isWritePermission()) {
Toast.makeText(this, "canWrite() can write URI:: " + p.getUri().toString(), Toast.LENGTH_LONG).show();
canWrite = true;
break;
}
}
return canWrite;
}
After saving image with valid uri and using
DocumentFile imageToDeletePath = DocumentFile.fromTreeUri(this, Uri.parse(lastSavedImagePath));
DocumentFile imageToDelete = imageToDeletePath.findFile(lastSavedImageName);
Uri.fromFile() and DocumentFile.fromTreeUri() create uris from two different worlds.
While they currently look very similar, this is just coincidence and could change with any future Android release.
There is no "non-hacky" way to convert from one to the other. If you want a dirty solution, you can go for reflection (view the source code of DocumentFile.fromTreeUri and possibly use the Storage class on newer Android versions.
Also see:
Android - Storage Access Framework - Uri into local file

Categories

Resources