Panacea
Is SAF the answer to Android File Restrictions ?
INTRO
Many would say no, use the required permissions for file access instead. For me, SAF (Storage Access Framework) can offer a more problem free approach to getting at files and folders in Shared Directories that are otherwise unavailable to your AppInventor app, and may well also help with the sharing or accessing files required or used by other apps in the Shared Directories. This guide aims to explore the basics of using SAF with Appinventor (keeping it as simple as possible), and how it can help to overcome the ever increasing file restrictions imposed by Android/Google - for our privacy and safety... You do not really need to use SAF on devices with Android 10 (API 30) or less, because there are fewer restrictions to file access, however, in my view, it makes sense to have a single unified approach.
SAF works for Android devices from 4.4 (API 19) onwards.
SAF returns a contenturi instead of a filepath. I will show how you can work with that. For success you need to work with and switch between the two.
Shared Directories ? These are in the root of your internal storage (/storage/emulated/0) e.g. Download, Documents, Pictures, DCIM, Music, Recordings.
The ASD (Application Specific Directory) is a file location within your app, and provides read/write access to any files stored there. it will be deleted if your app is uninstalled.
In all examples, the Screen1.Filescope and the File.Filescope are set to App, and Read Permission is checked for the file component.
My testing is carried out using companion app and compiled apps on Android 6 (API 23), Android 13 (API 33) - emulator, and Android 15 (API 35). Most of the screenshots come from Android 13 / Pixel 8 on my Genymotion Emulator.
In the main, I will be using the SAF extension by Sunny Gupta (credits where due) and the native File component. There are other little "Helpers" that I will also apply, either as procedures, components or extensions. I will generally be working with image and text files, given that these can be displayed with native components in AppInventor, but SAF (file management) can work with any file/mime type.
I provide a blank aia with SAF and File component pre-loaded: panBlank.aia. You can start from here and familiarise yourself with the SAF blocks available.
You should load up your Shared Directories with files of various types so that you can test the workings of the blocks.
CONTENTS:
Section 1 - Accessing a file in Shared Directories, Copying it to the ASD, Deleting the original file, Replacing the original file with the one in the ASD.
Section 2 - The Download directory, Created a Sub directory in Downloads, copying files to the sub Directory, Deleting files in Download.
Section 3 - Listing Files in Shared Directories
Section 4 - Working with Text/CSV files in Shared Directories
Section 5 - Permanency
Section 1
There are, in general two types of files to be found in the Shared Directories: media files, and non-media files. Also, there are files created/owned by your app, and files that come from elsewhere/other apps. My app is just not able to see, access or work with some of these files. This is where the power of SAF comes into play, because it is able to access all files.
I will dive in and show how to access a file in Shared Directories. If you have used the ImagePicker or FilePicker components in AppInventor, the initial workflow will be familiar to you.
Accessing a file in Shared Directories
In the blocks, you can see I use the OpenSingleDocument block and the InitialDir block (set to Documents). The type and extraMimeTypes are set to only display images. When the Uri is returned, The GotUri block is used to set the uriString to a label and a variable (for use later). I also return the filename of the contenturi selected, this is for later use, it gets lost after the original file is deleted. The uriString could have been set to an Image.Picture block to show the image. You can see the app images to the right, click the button, pick the image, return the uriString to a label.
Copying the Picked File to the ASD
I copy the picked file by using the CopyDocumentToFile block, building the directory path using the File component MakeFullPath block, and using a smalll procedure that removes the file:// prefix from the path. The Document Copied event returns a true response and the filepath. Now the file is in your ASD your app owns it!
Deleting the original file
Why ? If we intend to replace this file with the one the app now "owns", we need to delete it otherwise a new file will be created alongside the original.
I use the DeleteDocument block, and supply the variable pickedFile for the uriString. Just ensure you have copied to the ASD first ;)
Replace the original file with the one in the ASD
This is where things start to get interesting...although I can easily access a Document from a directory, if I want to write a Document to a directory, I first have to get permission to write to that directory, then I can copy the file from the ASD to a Document in the selected directory. Note: you can make the permission to a directory more permanent, I will cover this later...
I click on the btReplaceDoc, which opens the Documents folder. SAF offers a USE THIS FOLDER button. I press this.
I am then asked to allow access to files in the directory. I click allow.
I have to make some changes to the SAF GotUri block. The uriString returned from the Documents directory contains the word "tree", so if it does I can then call the CopyFileFrom ASD procedure. (If you look back to getting a document contenturi, you will see that it does not contain "tree", this only arises when working with Directories.
More interesting blocks in the CopyFileFrom ASD procedure. To create the correct targetParentUri, i use the uriString collected by accessing the directory Documents, then require a couple more SAF blocks to build the correct uri. The file is copied from the ASD to the Documents folder. If I had not deleted the original file, a second file with a suffix "(1)" would be created. You can see where i now use the filename variable.
I am a happy chap. I have been able to copy a file to ASD, using SAF, delete a file using SAF, then copy a file from the ASD to the Documents Directory, using SAF.
Here is the aia project for this section: pan1.aia
Section 2
The Download directory
Oddly, there is a directory restriction on the Download Directory. It is not possible to use OpenDocumentTree at Download. You can access all files in Download using OpenSingleDocument though. Anyhow, my approach to working around this is to create a Sub-Directory in Download, which is readable by SAF, then copy all the files in Download to the Sub Directory.
The behaviour when running the compiled app is similar for Android 13 & 15, It asks for file access permissions for both the File and SAF methods to create a sub directory. On Android 6, use the File method, the SAF method does not work because there are no restrictions on the Download directory in earlier Android versions (probably > 10). I also noticed that on Android 6, the provision of the initial directory in the blocks is not working, YMMV.
In fact, you do not need to do the sub directory thing for Download on the earlier Android versions at all, Download is available like the other root directories.
Created a Sub Directory in Downloads
It is not possible to use SAF to create a sub directory in the Download directory, because it cannot get permission. Therefore I use the File component to create the sub directory. (you can also use SAF, but you have to manually create it if you try to open the Document Tree to Download). I will show both methods.
with File Component:
To keep things obvious, my sub directoey is called sub. Thhis just happens without needing to go out to the file system and select anything.
with SAF
To use SAF, you first open the document tree at Downloads, if the sub directory sub does not exist. SAF will tell you that you cannot use that directory (Download) but offers you the opportunity to create a new folder inside Download. Press on CREATE NEW FOLDER, and SAF will provide a dialog to enter the sub directory name. You need to rely on the user to enter the same/correct name for the sub directory as you have in the blocks (this could be reworked to pick up whatever name a user gives it). SAF will then ask for me to give permission to access files, and then returns the contenturi of the sub directory
Copy files, 1 by 1, from Download to the sub directory
Using SAF, I have to copy the files one by one when they are in Download (you will see why I am doing this further down, when we start listing directories). The process, is straight forward. OpenSingle Document, select a file, and it gets copied - note we already have permission grnated to access the Download/sub directory.
Delete File in Download
Click the Delete Files In Download button to delete the file that has just been copied from the download directory.
It is possible to string together the copy and delete functions, but to delete only if the copy operation has been successful. i could use blocks like this:
Once again, I am a happy chap. I have been able, using SAF/File component, to create a sub directory in the Download directory, and copy all the files in Download to that sub directory, making this sub directory available to SAF for file listings.
Here is the aia project for this section: pan2.aia
Section 3
Listing Files in Shared Directories
The ability to list the contents of shared directories can greatly speed up the automation of some of the process covered so far. I am really only going to scratch the surface of what can be done, but what follows should provide sufficient information for the processes to be extended to meet other requirements.
There is a lot going on here so I made a video for this section.
I created a small list of all the root directories to mek it easier to select and show the file listings. After the directory is selected from the spinner, I open the document tree, select it and give permission for access. This then returns the directory contenturi.
I can then call theListFiles block with the uriString for the Directory. On return, I iterate over the list and create three lists from it: a listviewer list, which will display filenames, and images if present, a filenames list, and a list of all the contenturis for each file.
On selection in the listview I simply show the contenturi for the file, which hopefully demonstrates it is available for use (e.g. upload to ASD to make it owned by the app, copy somewhere, delete, view if viewable)
I can now get lists of files from any directory in Shared Directories, and have access to them, just as if I had opend them using OpenSingleDocument. Still a happy chap.
Here is the aia project for this section: pan3.aia
Section 4
Working with Text/CSV files in Shared Directories
SAF offers full CRUD (Create, Read, Update, Delete) for text/csv files, and works with files owned/created by the app or those from other apps/locations. This is a big plus, and i know that many developers are pulling in csv files to their device from other sources, and want to easily interact with those files.
The blocks I use below work through a CRUD with a single file. You should be able to see how you might create a list of text files to select from given the work done above...
Again, it seemed easier to make a video of the whole process. I create a csv file, with a mimetype for csv, in the Documents folder. You will see from the blocks that the SAF1.GotUri event block can very quickly start to get more complicated. i have to use a variable (action) to define the action to be taken when the contenturi is returned. You should also note that I have to get the display name of the file to be deleted before I delete it!
I am now able to CRUD any text file in the Shared Directories using SAF. Happy days...
Here is the aia project for this section: pan4.aia
Section 5
Permanency
The SAF extension offers blocks to set permanency on a directory. This means that I can use the contenturi/uriString, saved, for example, to tinydb, without needing to allow access permission each time I want to access that directory. The permanency will survive restarts of the app, and device reboots. The following blocks aim to demonstrate how this works. I will do it in stages to hopefully clarify things.
I will first look at where I am without "doing anything"
I have used this before up above. By Opening a Document Tree, clicking "Use This Folder", and then ALLOW access to files, I am able to carry out operations on this directory. But I have to do this everytime. Would make more sense to capture the uriString to the tinydb, so I have it and don't have to Open Document Tree and give permission each time?
Now, when we open the document tree, the uriString is saved to tinydb. Next time I open the app, the uriString will be available for me to use... or will it? Unfortunately not, because the "session permission" created by opening the Document Tree has not been set, I won't be able to directly save my text file using the uriString I have stored in the tinydb.
What I have to do is this:
Open Document Tree and Use Folder (this gives session permissions)
Save the uriString to tinydb
Then Take Persistable Permission for the uriString to Grant Read and Write
I can check persistable (not session) permissions are granted with the following blocks:
I pressed on Open Document Tree, and went through the routine to ALLOW access, then pressed on Take Permissions, then pressed on Are Permissions Granted, which showed true/true. I was then able to create a document.
Now, next time I start the app, I will have persistable permissions on the Documents directory, so I will be able to create my document, without having to "Open Document Tree"
You will see a Release Permissions button. I used this to remove the persistable permission while testing, but it may have its place in the process somewhere.
It is worth noting, that you will always have to press USE THIS FOLDER and ALLOW file access, whenever you "Open Document Tree", regardless of the persistable permissions state. (this can be a source of confusion...)
You should be able to see how persistable permissions can tie in with some of the previous sections and how they work
On Android 6, it was not necessary to Take Persistable Permissions, even though the grants were false, i was still able to create a document.
All I did this time, after opening the app, was press on Create Document, and it worked.
I now know how to set persistable permissions for a directory using SAF.
Here is the aia project for this section: pan5.aia
I have really only scratched the surface of what SAF can do, and how it can be used in AppInventor, if nothing else, consider this a worthwhile primer on how to get started with SAF.
I need to acknowledge and recognise all the good work that have been done in other areas of handling file restrictions on Android, SAF is just another way.
Credits again to Sunny, aka @vknow360, for the excellent SAF extension, and for advice and insight whilst I put this all together.