I have implemented a method for users to save/export files created by my app to any location using the Storage Access Framework. The architecture is:
Use Intent.ACTION_OPEN_DOCUMENT_TREE to have the user select a directory
Use DocumentsContract to create a file in that directory
Write the data out to this file
The file is a custom file extension containing custom data, so not a standard file type or mime type.
I am using a Samsung Galaxy S7 with an SD card inserted. So it does not support Adoptable Storage. For testing/developing, I am using the My Files system app to open files and a Windows PC with MTP to pull files from the device.
This architecture/code works very well in the following circumstances:
Any internal storage location
Secondary external storage, unencrypted (removable SD card)
If the SD card is encrypted, I start running into strange issues.
Issue 1: "My Files" system app sees the file as having size 0 bytes and will not open it.
Issue 2: Windows PC also sees the file as having size 0 bytes and will not copy it to my PC over MTP.
Strangely, ES File Explorer can see the file and open it. DropBox can see the file and upload it. Issue 1 and 2 above are not present if the SD card is NOT encrypted.
So what is going on here? I have tried a lot of things to troubleshoot. My theory is that there's something going on with handling MIME types and Content URIs that behaves differently with encryption on or off - specifically concerning My Files and MTP. Why would ES File Explorer have no issues seeing this file and opening it?
Here are relevant code snippets:
private void selectStoragePathExtended() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
startActivityForResult(intent, REQUEST_CODE_OPEN_DIRECTORY);
}
#Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CODE_OPEN_DIRECTORY) {
if (resultCode == Activity.RESULT_OK) {
mLocalStorageUri = DocumentsContract.buildDocumentUriUsingTree(data.getData(), DocumentsContract.getTreeDocumentId(data.getData()));
}
}
}
// This will get called with mLocalStorageUri and a File stored in the app's "ExternalFilesDir"
public static void copyFile(ContentResolver cr, File sourceFile, Uri destFolderUri) throws IOException {
if (!sourceFile.exists()) {
return;
}
FileChannel source = null;
FileChannel destination = null;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
try {
source = new FileInputStream(sourceFile).getChannel();
Uri destUri = DocumentsContract.createDocument(cr, destFolderUri, "*/*", sourceFile.getName());
ParcelFileDescriptor pfd = cr.openFileDescriptor(destUri, "w");
destination = new FileOutputStream(pfd.getFileDescriptor()).getChannel();
if (source.size() > FILE_COPY_MAX_BLOCK) {
// Transfer file in 128MB blocks
long position = 0;
while (destination.transferFrom(source, position, FILE_COPY_MAX_BLOCK) > 0) {
position += FILE_COPY_MAX_BLOCK;
}
} else {
long bytesCopied = destination.transferFrom(source, 0, source.size());
if (bytesCopied != source.size()) {
String errorMsg = String.format("Error: only %d out of %d bytes copied", bytesCopied, source.size());
throw new IOException(errorMsg);
}
}
destination.close();
pfd.close();
}
finally {
if(source != null) {
source.close();
}
if(destination != null) {
destination.close();
}
}
}
}
Yes I am using a non-standard file copy method with FileChannel, but I have tested with basic FileOutputStreams and other methods with same results. Also, remember, behavior is normal and working with SD card encryption off. And it actually does work with encryption on, but neither My Files nor Windows MTP can read the file. Only 3rd party tools like ES File Explorer and DropBox.
Any ideas? Gotta be a mime type/URI issue?
Related
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.
I am working on an app where I want to be able to export and import some data from the app, on a .txt file.
The minimum API of the app is 21.
The export part works well, but I am having trouble with the import part.
I open the file explorer :
butImportPatient.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_OPEN_DOCUMENT);
intent.setType("*/*");
startActivityForResult(intent, IMPORTPATIENT_ACTIVITY_REQUEST_CODE);
}
});
This looks like it is working.
But my onActivityResult doesn't work, I didn't find how I can get the file from the Uri.
For now, here is my code :
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == IMPORTPATIENT_ACTIVITY_REQUEST_CODE && resultCode == RESULT_OK) {
File file = new File(data.getData().getPath()) ;
String path = file.getAbsolutePath() ;
StringBuilder text = new StringBuilder();
try {
BufferedReader br = new BufferedReader(new FileReader(path));
String line;
while ((line = br.readLine()) != null) {
text.append(line);
text.append("\n");
}
br.close();
} catch (IOException e) {
e.printStackTrace();
}
AlertDialog.Builder builder = new AlertDialog.Builder(this) ;
builder.setMessage(path)
.show() ;
}
}
It is a mix of multiple posts I saw here, but none seems to work.
I get this path :
/document/home:List.txt
It creates FileNotFoundException. How can I get the real path of the file ?
I didn't find how I can get the file from the Uri.
There is no file. ACTION_OPEN_DOCUMENT and ACTION_GET_CONTENT do not open a file. They open a document. That document might be a file. It might not.
That Uri might point to:
A local file on external storage
A local file on internal storage for the other app
A local file on removable storage
A local file that is encrypted and needs to be decrypted on the fly
A stream of bytes held in a BLOB column in a database
A piece of content that needs to be downloaded by the other app first
...and so on
How can I get the real path of the file ?
You don't.
If you wish to only accept files, integrate a file chooser library instead of using ACTION_OPEN_DOCUMENT or
ACTION_GET_CONTENT. Just bear in mind that filesystem access to external storage is limited on Android 10+.
If you use ACTION_GET_CONTENT, and the scheme of the Uri that you get is file, then getPath() will be a filesystem path.
Otherwise, you need to understand that you have no idea where the document is coming from, and stop thinking in terms of "real path of the file". Use ContentResolver and openInputStream() to make a copy of the content to some file that you control, then work with that file.
I am working on an app where I want to be able to export and import some data from the app, on a .txt file.
The minimum API of the app is 21.
The export part works well, but I am having trouble with the import part.
I open the file explorer :
butImportPatient.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_OPEN_DOCUMENT);
intent.setType("*/*");
startActivityForResult(intent, IMPORTPATIENT_ACTIVITY_REQUEST_CODE);
}
});
This looks like it is working.
But my onActivityResult doesn't work, I didn't find how I can get the file from the Uri.
For now, here is my code :
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == IMPORTPATIENT_ACTIVITY_REQUEST_CODE && resultCode == RESULT_OK) {
File file = new File(data.getData().getPath()) ;
String path = file.getAbsolutePath() ;
StringBuilder text = new StringBuilder();
try {
BufferedReader br = new BufferedReader(new FileReader(path));
String line;
while ((line = br.readLine()) != null) {
text.append(line);
text.append("\n");
}
br.close();
} catch (IOException e) {
e.printStackTrace();
}
AlertDialog.Builder builder = new AlertDialog.Builder(this) ;
builder.setMessage(path)
.show() ;
}
}
It is a mix of multiple posts I saw here, but none seems to work.
I get this path :
/document/home:List.txt
It creates FileNotFoundException. How can I get the real path of the file ?
I didn't find how I can get the file from the Uri.
There is no file. ACTION_OPEN_DOCUMENT and ACTION_GET_CONTENT do not open a file. They open a document. That document might be a file. It might not.
That Uri might point to:
A local file on external storage
A local file on internal storage for the other app
A local file on removable storage
A local file that is encrypted and needs to be decrypted on the fly
A stream of bytes held in a BLOB column in a database
A piece of content that needs to be downloaded by the other app first
...and so on
How can I get the real path of the file ?
You don't.
If you wish to only accept files, integrate a file chooser library instead of using ACTION_OPEN_DOCUMENT or
ACTION_GET_CONTENT. Just bear in mind that filesystem access to external storage is limited on Android 10+.
If you use ACTION_GET_CONTENT, and the scheme of the Uri that you get is file, then getPath() will be a filesystem path.
Otherwise, you need to understand that you have no idea where the document is coming from, and stop thinking in terms of "real path of the file". Use ContentResolver and openInputStream() to make a copy of the content to some file that you control, then work with that file.
I am working on an app where I want to be able to export and import some data from the app, on a .txt file.
The minimum API of the app is 21.
The export part works well, but I am having trouble with the import part.
I open the file explorer :
butImportPatient.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_OPEN_DOCUMENT);
intent.setType("*/*");
startActivityForResult(intent, IMPORTPATIENT_ACTIVITY_REQUEST_CODE);
}
});
This looks like it is working.
But my onActivityResult doesn't work, I didn't find how I can get the file from the Uri.
For now, here is my code :
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == IMPORTPATIENT_ACTIVITY_REQUEST_CODE && resultCode == RESULT_OK) {
File file = new File(data.getData().getPath()) ;
String path = file.getAbsolutePath() ;
StringBuilder text = new StringBuilder();
try {
BufferedReader br = new BufferedReader(new FileReader(path));
String line;
while ((line = br.readLine()) != null) {
text.append(line);
text.append("\n");
}
br.close();
} catch (IOException e) {
e.printStackTrace();
}
AlertDialog.Builder builder = new AlertDialog.Builder(this) ;
builder.setMessage(path)
.show() ;
}
}
It is a mix of multiple posts I saw here, but none seems to work.
I get this path :
/document/home:List.txt
It creates FileNotFoundException. How can I get the real path of the file ?
I didn't find how I can get the file from the Uri.
There is no file. ACTION_OPEN_DOCUMENT and ACTION_GET_CONTENT do not open a file. They open a document. That document might be a file. It might not.
That Uri might point to:
A local file on external storage
A local file on internal storage for the other app
A local file on removable storage
A local file that is encrypted and needs to be decrypted on the fly
A stream of bytes held in a BLOB column in a database
A piece of content that needs to be downloaded by the other app first
...and so on
How can I get the real path of the file ?
You don't.
If you wish to only accept files, integrate a file chooser library instead of using ACTION_OPEN_DOCUMENT or
ACTION_GET_CONTENT. Just bear in mind that filesystem access to external storage is limited on Android 10+.
If you use ACTION_GET_CONTENT, and the scheme of the Uri that you get is file, then getPath() will be a filesystem path.
Otherwise, you need to understand that you have no idea where the document is coming from, and stop thinking in terms of "real path of the file". Use ContentResolver and openInputStream() to make a copy of the content to some file that you control, then work with that file.
I've been searching for many topics about android file writing, yet most of them wanted to write files to android internal storage. Others who wanted to write files on external SD card didn't success at all. My case is quite similar but I think that writing files to external USB is a totally different case.
I am using Samsung galaxy Note II running stock TouchWiz 4.4.2 [not rooted]. My phone supports micro-USB-OTG and I can mount my USB as rwxrwx--x without rooting. The complete path of my USB is /storage/UsbDriveA.
I've tried to use Environment.getExternalStorageDirectory() to get the path or use the path (mentioned above) directly but neither of them succeed. The first one returns internal storage path and the second one returns an error with "permission denied". I have already put the
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
in Android Manifest so I wondered why my code didn't work.
Moreover, I can write anything to my USB using Root Browser (use it without root) and Simple Browser thus I believe that there's a way to do that.
Here's my code:
File file = new File(path.getAbsolutePath(), "test.txt");
// File file = new File("/storage/extSdCard","test.txt");
err = false;
try {
FileOutputStream f = new FileOutputStream(file);
PrintWriter pw = new PrintWriter(f);
pw.print(get);
pw.flush();
pw.close();
f.close();
}catch (Exception e) {
e.printStackTrace();
Toast.makeText(MainActivity.this, "writing error",Toast.LENGTH_LONG).show();
err = true;
}
Log.i("File Path:", file.getPath());
From android 4.4, you can use Storage Access Framework to access to removable media (see https://commonsware.com/blog/2014/04/09/storage-situation-removable-storage.html).
For example, I tried with success to copy a pdf file from local memory to removable memory connected by OTG adapter. The only limitation: the user has to choose a destination folder.
1) call Intent.ACTION_CREATE_DOCUMENT:
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.setType("application/pdf");
intent.putExtra(Intent.EXTRA_TITLE, file.getName());
startActivityForResult(intent, REQUEST_CODE);
2) intercept the return intent
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent data){
if(requestCode == REQUEST_CODE) {
if (resultCode != RESULT_OK) return;
copyFile(fileToCopy, data.getData());
}
}
3) use the ContentResolver to open the outputStream and use it to copy the file
private void copyFile(File src, Uri destUri) {
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try {
bis = new BufferedInputStream(new FileInputStream(src));
bos = new BufferedOutputStream(getContentResolver().openOutputStream(destUri));
byte[] buf = new byte[1024];
bis.read(buf);
do {
bos.write(buf);
} while(bis.read(buf) != -1);
} catch (NullPointerException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (bis != null) bis.close();
if (bos != null) bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
From https://source.android.com/devices/storage/
Starting in Android 4.4, ...
The WRITE_EXTERNAL_STORAGE permission must only grant write access to
the primary external storage on a device. Apps must not be allowed to
write to secondary external storage devices, except in their
package-specific directories as allowed by synthesized permissions.
Restricting writes in this way ensures the system can clean up files
when applications are uninstalled.
So, starting from Android 4.4 in devices with multiple external storages you will be able to write only on the primary external storage. Take into account that External Storage does not mean only "real external" devices. It is defined as follows (from the External Storage reference)
External storage can be provided by physical media (such as an SD
card), or by exposing a portion of internal storage through an
emulation layer.
Anyway there is a workaround to write to secondary external storage using the media content provider. Take a look at http://forum.xda-developers.com/showthread.php?t=2634840
I have used it on a project of mine, but as the author says, it's far from the ideal solution, and it is not guaranteed to work on coming Android versions, so you must not let all your app to rely on this workaround.