Developing an Android app in Flutter targetting Android SDK 30+.
I want to read and write data (xml files) to something like:
/storage/emulated/0/CustomDirectory/example.xml
Reading around I guess I'm supposed to use Intent.ACTION_OPEN_DOCUMENT_TREE so I wrote a MethodChannel which allows me to open the SelectDialog just fine. (I've trimmed all the try-catch and error handling for brevity)
private fun selectDirectory() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
startActivityForResult(intent, 100)
}
#RequiresApi(Build.VERSION_CODES.Q)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
val uri = data.data!!
contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
return uri.toString())
}
I can call this from Flutter, it opens the "Select Directory" dialog, and I can choose my CustomDirectory, which then returns me a Content URI of:
content://com.android.externalstorage.documents/tree/primary%3ACustomDirectory
How do I convert that into a Flutter Directory?
In Flutter, I can call Directory.fromUri(...) but that just throws
Unsupported operation: Cannot extract a file path from a content URI
So I'm a little unsure of where to go from here, do I need to change the flags of my Intent or am I doing something very wrong somewhere?
This is going to be a long answer and a lot of the code is specific to my use case so if someone wants to reuse it, you might need to tweak things.
Basically with the changes in Android 30+ I wasn't able to get permissions to write to a directory on the user's phone that wasn't my apps own directory without requesting the dreaded manage_external_storage.
I solved this by doing this with native Kotlin then calling those methods via an interface in Dart.
First starting with the Kotlin code
class MainActivity : FlutterActivity() {
private val CHANNEL = "package/Main"
private var pendingResult: MethodChannel.Result? = null
private var methodCall: MethodCall? = null
override fun configureFlutterEngine(#NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
CHANNEL
).setMethodCallHandler { call, result ->
val handlers = mapOf(
"getSavedRoot" to ::getSavedRoot,
"selectDirectory" to ::copyDirectoryToCache,
"createDirectory" to ::createDirectory,
"writeFile" to ::writeFile,
)
if (call.method in handlers) {
handlers[call.method]!!.invoke(call, result)
} else {
result.notImplemented()
}
}
}
This sets up our MainActivity to listen for methods named in the setMethodCallHandler method.
There are plenty of examples you can find for how to implement basic IO functions in Kotlin so I won't post them all here, but an example of how to open a set a content root and handle the result:
class MainActivity : FlutterActivity() {
//...
#RequiresApi(Build.VERSION_CODES.LOLLIPOP)
private fun selectContentRoot(call: MethodCall, result: MethodChannel.Result) {
pendingResult = result
try {
val browseIntent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
startActivityForResult(browseIntent, 100)
} catch (e: Throwable) {
Log.e("selectDirectory", " error", e)
}
}
#RequiresApi(Build.VERSION_CODES.LOLLIPOP)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == 100 && resultCode == RESULT_OK) {
val uri: Uri = data?.data!!
contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
return pendingResult!!.success(uri.toString())
}
return
}
//..
Now to invoke that code in Dart I created an interface named AndroidInterface and implemented
class AndroidInterface {
final _platform = const MethodChannel('package/Main');
final _errors = {
'no persist document tree': FileOperationError.noSavedPersistRoot,
'pending': FileOperationError.pending,
'access error': FileOperationError.accessError,
'exists': FileOperationError.alreadyExists,
'creation failed': FileOperationError.creationFailed,
'canceled': FileOperationError.canceled,
};
String? _root;
// invoke a method with given arguments
Future<FileOperationResult<String>> _invoke(
String method, {
bool returnVoid = false,
String? root,
String? directory,
String? subdir,
String? name,
Uint8List? bytes,
bool? overwrite,
}) async {
try {
final result = await _platform.invokeMethod<String>(method, {
'root': root,
'directory': directory,
'subdir': subdir,
'name': name,
'bytes': bytes,
'overwrite': overwrite,
});
if (result != null || returnVoid) {
final fileOperationResult = FileOperationResult(result: result);
fileOperationResult.result = result;
return fileOperationResult;
}
return FileOperationResult(error: FileOperationError.unknown);
} on PlatformException catch (e) {
final error = _errors[e.code] ?? FileOperationError.unknown;
return FileOperationResult(
error: error,
result: e.code,
message: e.message,
);
}
}
Future<FileOperationResult<String>> selectContentRoot() async {
final result = await _invoke('selectContentRoot');
// release currently selected directory if new directory selected successfully
if (result.error == FileOperationError.success) {
if (_root != null) {
await _invoke('releaseDirectory', root: _root, returnVoid: true);
}
_root = result.result;
}
return result;
}
//...
Which basically sends the requests via _platform.invokeMethod passing the name of the method, and the arguments to send.
Using a factory pattern you can implement this interface device running 30+ and use standard stuff for Apple and devices running 29 and below.
Something like:
abstract class IOInterface {
//...
/// Select a subdirectory of the root directory
Future<void> selectDirectory(String? message, String? buttonText);
}
And a factory to decide what interface to use
class IOFactory {
static IOInterface? _interface;
static IOInterface? get instance => _interface;
IOFactory._create();
static Future<IOFactory> create() async {
final component = IOFactory._create();
if (Platform.isAndroid) {
final androidInfo = await DeviceInfoPlugin().androidInfo;
final sdkInt = androidInfo.version.sdkInt;
_interface = sdkInt > 29 ? AndroidSDKThirty() : AndroidSDKTwentyNine();
}
if (Platform.isIOS) {
_interface = AppleAll();
}
return component;
}
}
Finally, the implementation for 30+ could look like
class AndroidSDKThirty implements IOInterface {
final AndroidInterface _androidInterface = AndroidInterface();
#override
Future<void> selectDirectory(String? message, String? buttonText) async {
final contentRoot = await _androidInterface.getContentRoot();
//...
}
Hopefully, this is enough to get you started and pointed in the right direction.
Related
Extension functions that returns save Bitmap to device storage and returns result using sealed and class that represent its state.
sealed class RequestStatus<T> {
data class Loading<T>(val data: T? = null) : RequestStatus<T>()
data class Success<T>(val data: T) : RequestStatus<T>()
data class Failed<T>(val error: Exception, val data: T? = null) : RequestStatus<T>() {
val message = nullableErrorMsg(error.localizedMessage)
}
}
fun View.createAndStoreScreenshot(
appName: String,
#ColorRes backgroundColor: Int
): Flow<RequestStatus<Uri>> = flow {
emit(RequestStatus.Loading())
... Some code
try {
FileOutputStream(imageFile).use { stream ->
// Create bitmap screen capture
val bitmap = Bitmap.createBitmap(createBitmapFromView(backgroundColor))
// This method may take several seconds to complete, so it should only be called from a worker thread.
// Read https://developer.android.com/reference/android/graphics/Bitmap#compress(android.graphics.Bitmap.CompressFormat,%20int,%20java.io.OutputStream)
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
val uriForFile = FileProvider.getUriForFile(
context.applicationContext,
"${context.applicationContext.packageName}.provider",
imageFile
)
emit(RequestStatus.Success(uriForFile))
}
}
catch (e: Exception) {
emit(RequestStatus.Failed(e))
}
}
Usage
viewLifecycleOwner.lifecycleScope.launch {
withContext(Dispatchers.IO) {
view.createAndStoreScreenshot(
getString(R.string.app_name),
R.color.colorWhite_PrimaryDark
).onEach { req ->
when (req) {
is RequestStatus.Loading -> {
...
}
}.collect()
}
}
FileOutputStream is complaining as it can block UI thread, but you cannot switch context inside flow builder so we move on using flowOn(Dispatchers.IO) and remove the withContext(Dispatchers.IO) from the caller. Now we the Android Studio is complaining Not enough information to infer type variable T when emitting Loading and Failed state but explicitly providing the data type also marked as redundant. What is the problem here?
It should be Uri? not Uri for Loading and Failed state.
The question is specifically for apps targeting API 31 and above.
I have referred to a lot of similar StackOverflow questions, Official Docs, etc.
There are some limitations to API 31 as mentioned here - Docs.
Usecase
To write a JSON file to the device's external storage so that the data is not lost even if the app is uninstalled.
How to achieve this for apps, targeting API 31 and above?
My current code saves the file to app-specific storage directory obtained using context.getExternalFilesDir(null).
The problem is that the file is not retained when the app is uninstalled.
Any blogs or codelabs explaining the exact scenario are also fine.
Note
Aware that the user can see, modify or delete the file from other apps when we store it in the external storage directory - That is fine.
Device root directory, Documents, or any other location is fine, given that all devices can access the file in the same way.
Your app can store as much files in public Documents or Download directory and sub directories as it wants without any user interaction. All with classic file means.
At reinstall you use ACTION_OPEN_DOCUMENT_TREE to let the user choose the used subfolder.
Thanks to #CommonsWare's comments on my question, I got how to implement it.
My use-case is to create, read and write a JSON file.
I am using Jetpack Compose, so the code shared is for Compose.
Composable code,
val JSON_MIMETYPE = "application/json"
val createDocument = rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument(
mimeType = JSON_MIMETYPE,
),
) { uri ->
uri?.let {
viewModel.backupDataToDocument(
uri = it,
)
}
}
val openDocument = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument(),
) { uri ->
uri?.let {
viewModel.restoreDataFromDocument(
uri = it,
)
}
}
ViewModel code,
fun backupDataToDocument(
uri: Uri,
) {
viewModelScope.launch(
context = Dispatchers.IO,
) {
// Create a "databaseBackupData" custom modal class to write data to the JSON file.
jsonUtil.writeDatabaseBackupDataToFile(
uri = uri,
databaseBackupData = it,
)
}
}
fun restoreDataFromDocument(
uri: Uri,
) {
viewModelScope.launch(
context = Dispatchers.IO,
) {
val databaseBackupData = jsonUtil.readDatabaseBackupDataFromFile(
uri = uri,
)
// Use the fetched data as required
}
}
JsonUtil
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
private val databaseBackupDataJsonAdapter: JsonAdapter<DatabaseBackupData> = moshi.adapter(DatabaseBackupData::class.java)
class JsonUtil #Inject constructor(
#ApplicationContext private val context: Context,
) {
fun readDatabaseBackupDataFromFile(
uri: Uri,
): DatabaseBackupData? {
val contentResolver = context.contentResolver
val stringBuilder = StringBuilder()
contentResolver.openInputStream(uri)?.use { inputStream ->
BufferedReader(InputStreamReader(inputStream)).use { bufferedReader ->
var line: String? = bufferedReader.readLine()
while (line != null) {
stringBuilder.append(line)
line = bufferedReader.readLine()
}
}
}
return databaseBackupDataJsonAdapter.fromJson(stringBuilder.toString())
}
fun writeDatabaseBackupDataToFile(
uri: Uri,
databaseBackupData: DatabaseBackupData,
) {
val jsonString = databaseBackupDataJsonAdapter.toJson(databaseBackupData)
writeJsonToFile(
uri = uri,
jsonString = jsonString,
)
}
private fun writeJsonToFile(
uri: Uri,
jsonString: String,
) {
val contentResolver = context.contentResolver
try {
contentResolver.openFileDescriptor(uri, "w")?.use {
FileOutputStream(it.fileDescriptor).use { fileOutputStream ->
fileOutputStream.write(jsonString.toByteArray())
}
}
} catch (fileNotFoundException: FileNotFoundException) {
fileNotFoundException.printStackTrace()
} catch (ioException: IOException) {
ioException.printStackTrace()
}
}
}
First you ask is there a way to skip user manual selection, the answer is yes and no, it's yes if you get MANAGE_EXTERNAL_STORAGE permission from the user but as it is a sensitive and important permission google will reject your app to be published on google play if it is not a file-manager or antivirus or other app that really needs this permission. read this page for more info
and the answer is no if you don't use MANAGE_EXTERNAL_STORAGE permission.
You should use Storage Access Framework, It doesn't need any permission, I will tell you how to implement it:
Imagine you have saved JSON to a text file, here is the steps to restore data from it:
1- We need a method that displays a dialog to the user, so he/she can choose saved file:
private void openFile() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("text/*");
someActivityResultLauncher.launch(intent);
}
2- We should implement onActivityResult to get the uri of the file that user has selected, so add someActivityResultLauncher to activity class:
ActivityResultLauncher<Intent> someActivityResultLauncher;
3- Add following methods to Activity class:
private void registerActivityResult()
{
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();
Uri uri = data.getData();
// Perform operations on the document using its URI.
try {
String txt = readTextFromUri(Setting_Activity.this, uri);
dorestore(txt);
}catch (Exception e){}
}
}
});
}
4- Call the above method in onCreate() method of activity:
#Override
protected void onCreate(Bundle savedInstanceState) {
//......
registerActivityResult();
//.....
}
5- Implement the readTextFromUri() method we used in step 3:
public static String readTextFromUri(Context context, Uri uri) throws IOException {
StringBuilder stringBuilder = new StringBuilder();
try (InputStream inputStream =
context.getContentResolver().openInputStream(uri);
BufferedReader reader = new BufferedReader(
new InputStreamReader(Objects.requireNonNull(inputStream)))) {
String line;
while ((line = reader.readLine()) != null) {
stringBuilder.append(line);
}
}
return stringBuilder.toString();
}
That's all, just implement the dorestore() method we used in step 3 to restore your json data.
in mycase it looke something like this:
private void dorestore(String data)
{
ArrayList<dbObject_user> messages = new ArrayList<>();
try {
JSONArray jsonArray = new JSONArray(data);
//....
//parsing json and saving it to app db....
//....
}
catch (Exception e)
{}
}
just call the openFile() in step 1. That's all.
I am using Storage Access Framework for Image Picker in my app. Below is the code
val types = arrayOf("image/png", "image/jpeg", "image/jpg")
val intent = Intents.createDocumentIntent(types, true)
if (canDeviceHandle(intent)) caller.startActivityForResult(intent, OPEN_GALLERY)
Here is the intent for creating document
fun createDocumentIntent(types: Array<String>, allowedMultiple: Boolean): Intent {
return Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = if (!types.isNullOrEmpty()) {
putExtra(Intent.EXTRA_MIME_TYPES, types)
types[0]
} else "*/*"
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowedMultiple)
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
}
This is in OnActivityResult
private fun handleGalleryActivityResult(data: Intent?, callbacks: FilePicker.Callbacks) {
if (data == null) return
val files = mutableListOf<Uri>()
when {
data.clipData != null -> {
val clipData = data.clipData ?: return
(0 until clipData.itemCount).forEach { files.add(clipData.getItemAt(it).uri) }
}
data.data != null -> {
files.add(data.data!!)
}
else -> return
}
files.forEach {
val flags = data.flags and Intent.FLAG_GRANT_READ_URI_PERMISSION
activity.contentResolver.takePersistableUriPermission(it, flags)
}
callbacks.onFilesPicked(files)
}
I am getting crash in line
activity.contentResolver.takePersistableUriPermission(it, flags)
in onActivityResult.
I read many solutions regarding this crash like adding persistable (FLAG_GRANT_PERSISTABLE_URI_PERMISSION) flag or adding takePersistableUriPermission but I have already have this but still I am getting this crash . I couldn't find any solution till now and my app users are facing this issue also on my phone I am not able to reproduce it myself.
Also on side note: I am using target version -> 11
Replace:
val flags = data.flags and Intent.FLAG_GRANT_READ_URI_PERMISSION
with:
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
The only values that you pass to takePersistableUriPermission() are FLAG_GRANT_READ_URI_PERMISSION and FLAG_GRANT_WRITE_URI_PERMISSION, and you have no idea what data.flags has in it.
Context: Android 10, API 29.
I print a PDF file generated from a WebView, but now I'd like to save it to a file. So I tried the Intent.ACTION_CREATE_DOCUMENT to pick the file and save it via the printAdapter's onWrite method.
The problem is that the file is always empty - 0 bytes - and no errors are raised. It justs calls onWriteFailed, but with an empty error message.
choosenFileUri has a value like content://com.android.providers.downloads.documents/document/37
The method I use to start the intent to pick a new file. Note that the result of this activity is a Uri:
fun startIntentToCreatePdfFile(fragment: Fragment, filename : String) {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/pdf"
putExtra(Intent.EXTRA_TITLE, filename)
}
fragment.startActivityForResult(intent, IntentCreatePdfDocument)
}
The method I use to "print" the PDF to a file. The fileUri comes from the Intent.ACTION_CREATE_DOCUMENT:
fun printPdfToFile(
context: Context,
webView: WebView,
fileUri: Uri
) {
(context.getSystemService(Context.PRINT_SERVICE) as? PrintManager)?.let {
val jobName = "Print PDF to save it"
val printAdapter = webView.createPrintDocumentAdapter(jobName)
val printAttributes = PrintAttributes.Builder()
.setMediaSize(PrintAttributes.MediaSize.ISO_A4)
.setResolution(PrintAttributes.Resolution("pdf", "pdf", 600, 600))
.setMinMargins(PrintAttributes.Margins.NO_MARGINS).build()
printAdapter.onLayout(null, printAttributes, null, object : LayoutResultCallback() {
override fun onLayoutFinished(info: PrintDocumentInfo, changed: Boolean) {
context.contentResolver.openFileDescriptor(fileUri, "w")?.use {
printAdapter.onWrite(
arrayOf(PageRange.ALL_PAGES),
it,
CancellationSignal(),
object : WriteResultCallback() {
})
}
}
}, null)
}
}
What I do pick file onActivityResult:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode != Activity.RESULT_OK) {
return null
}
if (requestCode != IntentCreatePdfDocument) {
throw Exception("RequestCode not implemented: $requestCode")
}
val choosenFileUri = data?.data
// If it is null, nothing to do
if (choosenFileUri == null) {
return
}
try {
HtmlHelpers.savePdfFromHtml(
requireContext(),
"html document to be represented in the WebView",
choosenFileUri)
} catch (exception: Exception) {
_logger.error(exception)
Helpers.showError(requireActivity(), getString(R.string.generic_error))
}
dismiss()
}
...where HtmlHelpers.savePdfFromHtml is:
fun savePdfFromHtml(
context: Context,
htmlContent: String,
fileUri: Uri
) {
generatePdfFromHtml(
context,
htmlContent
) { webView ->
PrintHelpers.printPdfToFile(
context,
webView,
fileUri)
}
}
...and generatePdfFromHtml is:
private fun generatePdfFromHtml(
context: Context,
htmlContent: String,
onPdfCreated: (webView: WebView) -> Unit
) {
val webView = WebView(context)
webView.settings.javaScriptEnabled = true
webView.webViewClient = object : WebViewClient() {
override fun onPageFinished(webView: WebView, url: String) {
onPdfCreated(webView)
}
}
webView.loadDataWithBaseURL(
null,
htmlContent,
"text/html; charset=utf-8",
"UTF-8",
null);
}
I checked all the other answer about this topic, but everyone creates manually the ParcelFileDescriptor instead of it in the onWrite method. Everyone does something like this:
fun getOutputFile(path: File, fileName: String): ParcelFileDescriptor? {
val file = File(path, fileName)
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE)
}
But I cannot do this since I have only the Uri.
Edit: as suggested by #blackapps, I tried to open the output stream after I got the FileDescriptor, but I still got the same result:
context.contentResolver.openFileDescriptor(fileUri, "w")?.use {
val fileDescriptor = it
FileOutputStream(it.fileDescriptor).use {
printAdapter.onWrite(
arrayOf(PageRange.ALL_PAGES),
fileDescriptor,
CancellationSignal(),
object : WriteResultCallback() {
})
}
}
For generating the PDF file from the HTML content I've used this Library
I'm storing the pdf file inside the download folder of shared storage(External). Use the below method to retrieve the location.
//fileName is the name of the file that you want to save.
fun getSavedFile(context: Context, fileName: String): File {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
return File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)!!,
"$fileName.pdf"
)
}
return File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
"$fileName.pdf"
)
}
Then use the library inbuilt method to generate the PDF from the HTML content loading inside the WebView
//webView is the ID of WebView where we are loading the html content
// fileName : Pass whatever name you want.
PDFUtil.generatePDFFromWebView(pdfDownloadUtil.getSavedFile(getApplicationContext(), fileName), webView, new PDFPrint.OnPDFPrintListener() {
#Override
public void onSuccess(File file) {
savedPdfFile = file;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createFile(Uri.fromFile(file));
}
}
#Override
public void onError(Exception exception) {
}
});
}
Now, We need to fire the intent after getting the pdf of passed html content.
private void createFile(Uri pickerInitialUri) {
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("application/pdf");
intent.putExtra(Intent.EXTRA_TITLE, fileName);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri);
}
createFileResult.launch(intent);
}
ActivityResultLauncher<Intent> createFileResult = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == AppCompatActivity.RESULT_OK) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
pdfDownloadUtil.writeFileContent(this, savedPdfFile, result.getData().getData(), () -> {
showSnackToOpenPdf();
return null;
});
}
}
}
);
In oreo or above device, We are using the above method i.e writeFileContent
#RequiresApi(Build.VERSION_CODES.O)
fun writeFileContent(
context: #NotNull Context,
savedPdfFile: #NotNull File,
uri: #NotNull Uri,
listener: () -> Unit
) {
try {
val file = uri.let { context.contentResolver.openFileDescriptor(it, "w") }
file?.let {
val fileOutputStream = FileOutputStream(it.fileDescriptor)
fileOutputStream.write(Files.readAllBytes(savedPdfFile.toPath()))
fileOutputStream.close()
it.close()
}
listener()
} catch (e: FileNotFoundException) {
//print logs
e.printStackTrace()
} catch (e: IOException) {
//print logs
e.printStackTrace()
}
}
Note: If the number of pages is large i.e more than 200 pages. Then it won't work as internally it's cache the pages in the WebView and then load it. So alternative way is to get the link to the PDF file from the API.
I checked all the other answer about this topic, but everyone creates manually the ParcelFileDescriptor instead of it in the onWrite method. Everyone does something like this:
fun getOutputFile(path: File, fileName: String): ParcelFileDescriptor? {
val file = File(path, fileName)
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE)
}
But I cannot do this since I have only the Uri.
-> Uri contains the getPath() method. You can use this to create a file object,
val file = File(uri.path)
and use in ParcelFileDescriptor. Null-check the getPath() method.
I am currently trying to save a text file inside a fragment, but I can't get it to work:
Here is the method called when the user clicks the save button
private fun saveText(){
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
putExtra(Intent.EXTRA_TITLE, "text.txt")
}
startActivityForResult(intent, 1)
}
Here is the onActivityResult method:
override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
if (requestCode == 1 && resultCode == Activity.RESULT_OK) {
try {
val path = resultData?.data?.path
Log.wtf("Path", filePath)
val writer: Writer = BufferedWriter(FileWriter(path))
writer.write("Example Text")
writer.close()
} catch (e: Exception){
e.printStackTrace()
}
}
}
I also have the permissions set in the manifest and the file itself is created, but nothing is written. Perhaps I'm writing to the file wrong?
The error thrown is FileNotFoundException, because its trying to use a file from /document when I'm selecting one from /downloads
Suggested solution which unfortunately doesn't work:
resultData?.data?.let {
requireActivity().contentResolver.openOutputStream(it).use { stream ->
stream!!.bufferedWriter().write("Example Text")
}
}
A Uri is not a file.
Replace:
val path = resultData?.data?.path
Log.wtf("Path", filePath)
val writer: Writer = BufferedWriter(FileWriter(path))
writer.write("Example Text")
writer.close()
with:
resultData?.data?.let { contentResolver.openOutputStream(it).use { stream ->
stream.writer().write("Example Text")
}
}