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.



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.


CONTENTS:


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.

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.


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!

I use the DeleteDocument block, and supply the variable pickedFile for the uriString. Just ensure you have copied to the ASD first ;)

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


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.

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

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.

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



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

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


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:

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.


Is SAF the panacea to file restrictions I/we have been looking for? I will leave it up to you to decide, but for me, now I believe I have got my head around how SAF works, it is a great "one stop" alternative to the other methods available...