Quick links
- Philosophy
- Internal classification
- Getting started
- Initialize the library
- Init methods
- Public attributes
- Public methods
- Easy writing to files
From Android 11 onward, it is mandatory to use DocumentsContract
or similar approach to write to shared storage, because of enforcement of Storage Access Framework. Our old and beloved Java File no longer works unless you are writing on private storage.
Hence, there are two different ways to write a file: 1. Use Java File to write to internal storage. 2. Use DocumentsContract to write to shared storage.
This means extra code to write. Also, DocumentsContract is not very friendly to work with, as it is completely a Uri based approach than the file path based approach we are generally aware of.
Hence FileX
was created. FileX tries to address these problems:
- It is mostly file path based. You as a user of the library do not have to think about Uris. They are handled in background.
- FileX also wraps around old Java File. You only need to mention one parameter
isTraditional
to use the Java File way, or the DocumentsContract way. - Known syntax is used. You will find methods like
mkdirs()
,delete()
,canonicalPath
just like old Java File had.
If you use the isTraditional
parameter as below:
FileX.new("my/example/path", isTraditional = true)
then it is similar to declaring:
File("my/example/path")
This can be used to access private storage of the app. This also lets you access shared storage on Android 10 and below.
However, for accessing shared storage on Android 11+, you cannot declare the isTraditional
parameter as true.
val f = FileX.new("my/path/on/shared/storage")
// ignoring the second parameter defaults to false.
You may call resetRoot()
on the object f
to open the Documents UI which will allow the user to select a root directory on the shared storage. Once a root directory is chosen by the user, the path mentioned by you will be relative to that root.
Assume in the above case, the user selects a directory as [Internal storage]/dir1/dir2
. Then f
here refers to [Internal storage]/dir1/dir2/my/path/on/shared/storage
.
This can also be seen by calling canonicalPath
on f
.
Log.d("Tag", f.canonicalPath)
// Output: /storage/emulated/0/dir1/dir2/my/path/on/shared/storage
Once a root is set, you can peacefully use methods like createNewFile()
to create the document, and other known methods for further operation and new file/document creation.
Please check the sections:
Check for file read write permissions
This picture shows how FileX internally classifies itself as two different types based on the isTraditional
argument. This is internal classification, and you as user do not have to worry.
However, based on this classification, some specific methods and attributes are available to specific types. Example createFileUsingPicker()
is a method available to FileX11
objects, i.e. if isTraditional
= false. But this method will throw an exception if used on FileXT
object. These exclusive methods are expanded in a following section.
You can import the library in your project in any of the below ways.
Get the library from jitpack.io
- In top-level
build.gradle
file, inallprojects
section, add jitpack as shown below.
allprojects { repositories { google() jcenter() ... maven { url 'https://jitpack.io' } } }
- In the "app" level
build.gradle
file, add the dependency.
dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" ... implementation 'com.github.SayantanRC:FileX:alpha-6' }
Perform a gradle sync. Now you can use the library in the project.
- Get the latest released AAR file from the Releases page.
- In your
app
module directory of the project, there should exist a directory namedlibs
. If not, create it. - Place the downloaded AAR file inside the
libs
directory. - In top-level
build.gradle
file, inallprojects
section, add the below lines.
allprojects { repositories { google() jcenter() ... flatDir { dirs 'libs' } } }
- In the "app" level
build.gradle
file, add the dependency.
dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" ... implementation(name:'FileX-release', ext:'aar') }
Perform a gradle sync to use the library.
In your MainActivity
class, in onCreate()
add the below line. This is only needed once in the entire app.
This has to be before any FileX
related operation or object creation is performed!!
FileXInit(this, false)
- The first argument is the context of the class.
FileXInit
will internally get the application context from this context. - The second argument is a global
isTraditional
attribute. All new FileX objects will take this value if not explicitly mentioned.
Alternately you can also initialise the FileXInit()
method from a subclass of the Application()
class if you have it in your app.
<application
...
android:name=".AppInstance"
...
>
...
</application>
class AppInstance: Application() { override fun onCreate() { ... FileXInit(this, false) } }
Working with FileX objects is similar to working with Java File objects.
val fx = FileX.new("my/path")
Here, the object fx
gets its isTraditional
parameter from the global parameter defined in FileXInit()
. If you wish to override it, you may declare as below:
val fx = FileX.new("my/path", true)
This creates a FileXT
object i.e. with isTraditional
= true even though the global value may be false.
These are public methods available from FileXInit
class.
fun isUserPermissionGranted(): Boolean
For FileXT, the above method checks if the Manifest.permission.READ_EXTERNAL_STORAGE
and Manifest.permission.WRITE_EXTERNAL_STORAGE
are granted by the system.
For FileX11, it checks if user has selected a root directory via the system ui and if the root exists now.
val isPermissionGranted = FileXInit.isUserPermissionGranted()
if (isPermissionGranted){
// ... create some files
}
fun requestUserPermission(reRequest: Boolean = false, onResult: ((resultCode: Int, data: Intent?) -> Unit)? = null)
For FileXT, this method requests Manifest.permission.READ_EXTERNAL_STORAGE
and Manifest.permission.WRITE_EXTERNAL_STORAGE
from the ActivityCompat.requestPermissions()
method.
For FileX11, this method starts the system ui to let the user select a global root directory. The uri from the selected root directory is internally stored.
All new FileX objects will consider this user selected directory as the root.
reRequest
: Only applicable for FileX11, defunct for FileXT. Default is "false". If "false" and global root is already selected by user, and exists now, then user is not asked again. If "true" user is prompted to select a new global root directory. Root of all previously created FileX objects will remain unchanged.
onResult: ((resultCode: Int, data: Intent?) -> Unit)
: Optional callback function called once permission is granted or denied.
resultCode
: If success, it isActivity.RESULT_OK
else usuallyActivity.RESULT_CANCELED
.data
: Intent with some information.- For FileXT
data.getStringArrayExtra("permissions")
= Array is requested permissions. Equal to array consistingManifest.permission.READ_EXTERNAL_STORAGE
,Manifest.permission.WRITE_EXTERNAL_STORAGE
data.getStringArrayExtra("grantResults")
= Array is granted permissions. If granted, should be equal to array ofPackageManager.PERMISSION_GRANTED
,PackageManager.PERMISSION_GRANTED
- For FileX11
data.data
= Uri of the selected root directory.
- For FileXT
FileXInit.requestUserPermission() { resultCode, data ->
// this will be executed once user grants read-write permission (or selects new global root).
// this block will also be executed if permission was already granted.
// if permission was not previously granted (or global root is null or deleted), user will be prompted,
// and this block will be executed once user takes action.
Log.d("DEBUG_TAG", "result code: $resultCode")
if (!FileXInit.isTraditional) {
Log.d("DEBUG_TAG", "root uri: ${data?.data}")
}
// create some files
}
fun refreshStorageVolumes()
Useful only for FileX11 and above Android M. Detects all attached storage volumes. Say a new USB OTG drive is attached, then this may be helpful. In most cases, manually calling this method is not required as it is done automatically by the library.
Usage: FileXInit.refreshStorageVolumes()
Attribute name | Return type ( ? - null return possible) |
Exclusively for | Description |
---|---|---|---|
uri | String? | FileX11 ( isTraditional =false) |
Returns Uri of the document. If used on FileX11 , returns the tree uri. If used on FileXT , returns Uri.fromFile() |
file | File? | FileXT ( isTraditional =true) |
Returns raw Java File. Maybe useful for FileXT . But usually not of much use for FileX11 as the returned File object cannot be read from or written to. |
path | String | - | Path of the document. Formatted with leading slash (/ ) and no trailing slash. |
canonicalPath | String | - | Canonical path of the object. For FileX11 returns complete path for any physical storage location (including SD cards) only from Android 11+. On lower versions, returns complete path for any location inside the Internal shared storage. |
absolutePath | String | - | Absolute path of the object. For FileX11 it is same as canonicalPath |
isDirectory | Boolean | - | Returns if the document referred to by the FileX object is directory or not. Returns false if document does not exist already. |
isFile | Boolean | - | Returns if the document is a file or not (like text, jpeg etc). Returns false if document does not exist. |
name | String | - | Name of the document. |
parent | String? | - | Path of the parent directory. This is not canonicalPath of the parent. Null if no parent. |
parentFile | FileX? | - | A FileX object pointing to the parent directory. Null if no parent. |
parentCanonical | String | - | canonicalPath of the parent directory. |
freeSpace | Long | - | Number of bytes of free space available in the storage location. |
usableSpace | Long | - | Number of bytes of usable space to write data. This usually takes care of permissions and other restrictions and more accurate than freeSpace |
totalSpace | Long | - | Number of bytes representing total storage of the medium. |
isHidden | Boolean | - | Checks if the document is hidden. For FileX11 checks if the name begins with a . |
extension | String | - | Extension of the document |
nameWithoutExtension | String | - | The name of the document without the extension part. |
storagePath | String? | FileX11 ( isTraditional =false) |
Returns the path of the document from the root of the storage. Returns null for FileXT Example 1: A document with user selected root = [Internal storage]/dir1/dir2 and having a path my/test_doc.txt .storagePath = /dir1/dir2/my/test_doc.txt Example 2: A document with user selected root = [SD card]/all_documents and having a path /thesis/doc.pdf .storagePath = /all_documents/thesis/doc.pdf |
volumePath | String? | FileX11 ( isTraditional =false) |
Returns the canonical path of the storage medium. Useful to find the mount point of SD cards and USB-OTG drives. This path, in most cases, is not readable or writable unless the user picks selects it from the system picker. Returns null for FileXT Example 1: A document with user selected root = [Internal storage]/dir1/dir2 and having a path my/test_doc.txt .volumePath = /storage/emulated/0 Example 2: A document with user selected root = [SD card]/all_documents and having a path /thesis/doc.pdf .volumePath = /storage/B840-4A40 (the location name is based on the UUID of the storage medium) |
rootPath | String? | FileX11 ( isTraditional =false) |
Returns the canonical path upto the root selected by the user from the system picker. Returns null for FileXT Example 1: In the above cases of the first example, rootPath = /storage/emulated/0/dir1/dir2 Example 2: In the above cases of the second example, rootPath = /storage/B840-4A40/all_documents |
parentUri | Uri? | FileX11 ( isTraditional =false) |
Returns the tree uri of the parent directory if present, else null. Returns null for FileXT |
isEmpty | Boolean | - | Applicable on directories. Returns true if the directory is empty. |
Method name | Return type ( ? - null return possible) |
Exclusively for | Description |
---|---|---|---|
refreshFile() | - | FileX11 ( isTraditional =false) |
Not required by FileXT If the document was not present during declaration of the FileX object, and the document is later created by any other app or this app from a background thread, then call refreshFile() on it to update the Uri pointing to the file.Do note that if your app is itself creating the document on the main thread, you need not call refreshFile() again.Example: val fx1 = FileX.new("aFile") val fx2 = FileX.new("/aFile") fx2.createNewFile() In this case you need not call refreshFile() on fx1 . However if any other app creates the document, or all the above operations are performed on background thread, then you will not be able to refer to fx1 unless it is refreshed.Even in the case of the file being created in a background thread, the Uri of the file does get updated after about 200 ms. But this is not very reliable, hence it is recommended to call refreshFile() . |
exists() | Boolean | - | Returns if the document exist. For FileX11 , internally calls refreshFile() before checking. |
length() | Long | - | Length of the file in bytes. |
lastModified() | Long | - | Value representing the time the file was last modified, measured in milliseconds since the epoch (00:00:00 GMT, January 1, 1970) |
canRead() | Boolean | - | Returns if the document can be read from. Usually always true for FileX11 . |
canWrite() | Boolean | - | Returns if the document can be written to. Usually always true for FileX11 . |
canExecute() | Boolean | FileXT ( isTraditional =true) |
Returns if the Java File pointed by a FileX object is executable. Always false for FileX11 . |
delete() | Boolean | - | Deletes a single document or empty directory. Does not delete a non-empty directory. Returns true if successful, else false. |
deleteRecursively() | Boolean | - | Deletes a directory and all documents and other directories inside it. Returns true if successful. |
deleteOnExit() | - | - | Requests that the file or directory denoted by this abstract pathname be deleted when the virtual machine terminates. All files on which this method is called will get deleted once System.exit() is called, similar to java.io.File.deleteOnExit() .RECOMMENDED: Although this works both on FileX11 and FileXT , but implementation for FileX11 is basically a patch work from the implementation from java.io.DeleteOnExitHook . It is highly recommended to surround the code by try-catch block. |
createNewFile() | Boolean | - | Creates a blank document referred to by the FileX object. Throws error if the whole directory path is not present. A safer alternative is a new variant of the method described below. |
createNewFile( makeDirectories:Boolean=false, overwriteIfExists:Boolean=false, optionalMimeType:String ) |
Boolean | - | Create a blank document. If makeDirectories = true (Default: false) -> Creates the whole directory tree before the document if not present.If overwriteIfExist = true (Default: false) -> Deletes the document if already present and creates a blank document.For FileX11 :optionalMimeType as string can be specified. Ignored for FileXT Returns true, if document creation is successful. |
createFileUsingPicker( optionalMimeType: String, afterJob: (resultCode: Int, data: Intent?) ) |
- | FileX11 ( isTraditional =false) |
Invoke the System file picker to create the file. Only applicable on FileX11 mime type can be spcified in optionalMimeType afterJob() - custom function can be passed to execute after document is created.resultCode = Activity.RESULT_OK if document is successfully created.data = Intent data returned by System after document creation. |
mkdirs() | Boolean | - | Make all directories specified by the path of the FileX object (including the last element of the path and other non-existing parent directories.). |
mkdir() | Boolean | - | Creates only the last element of the path as a directory. Parent directories must be already present. |
renameTo(dest: FileX) | Boolean | - | Move the current document to the path mentioned by the FileX parameter dest For FileX11 this only works in the actual sense of "moving" for Android 7+ (API 24) due to Android limitations.For lower Android versions, this copies the file / directory and then deletes from the old location. |
renameTo(newFileName: String) | Boolean | - | Rename the document in place. This is used to only change the name and cannot move the document. |
inputStream() | InputStream? | - | Returns an InputStream from a file to read the file. |
outputStream(append:Boolean=false) | OutputStream? | - | Returns an OutputStream to the document to write.Pass true to append to the end of the file. If false , deletes all contents of the file before writing. |
outputStream(mode:String) | OutputStream? | - | Returns an OutputStream to the document to write. This is mainly useful for FileX11 The mode argument can be"r" for read-only access,"w" for write-only access (erasing whatever data is currently in the file),"wa" for write-only access to append to any existing data,"rw" for read and write access on any existing data,and "rwt" for read and write access that truncates any existing file.For FileXT , please use outputStream(append:Boolean) as this method will not provide much advantage. If mode=wa , returns a FileOutputStream in append mode. Any other mode is "not append" mode. |
list() | Array-String? | - | Returns a String array of all the contents of a directory. |
list(filter: FileXFilter) | Array-String? | - | Returns the list filtering with a FileXFilter . This is similar to FileFilter in Java. |
list(filter: FileXNameFilter) | Array-String? | - | Returns the list filtering with a FileXNameFilter . This is similar to FilenameFilter in Java. |
listFiles() | Array-FileX? | - | Returns an array of FileX pointing to all the contents of a directory. |
listFiles(filter: FileXFilter) | Array-FileX? | - | Returns FileX elements array filtering with a FileXFilter . |
listFiles(filter: FileXNameFilter) | Array-FileX? | - | Returns FileX elements array filtering with a FileXNameFilter . |
copyTo( target:FileX, overwrite:Boolean=false, bufferSize:Int ) |
FileX | - | Copies a file and returns the target. Logic is completely copied from File.copyTo() of kotlin.io. |
copyRecursively( target:FileX, overwrite:Boolean=false, onError: (FileX, Exception) ) |
Boolean | - | Directory copy recursively, return true if success else false. Logic is completely copied from File.copyRecursively() of kotlin.io. |
listEverything() | 4 lists as a Quad [ List-String, List-Boolean, List-Long, List-Long ] |
- | This function returns a set of 4 lists. The lists contain information of all the containing files and directories. - 1st list: Contains names of objects [String] - 2nd list: Contains if the objects are file or directory [Boolean] - 3rd list: Sizes of the objects in bytes [Long] - 4th list: Last modified value of the object [Long] For a specific index, the elements of each list refer to properties of 1 file. For example, at (say) INDEX=5, 1st_list[5]="sample_doc.txt"; 2nd_list[5]=false; 3rd_list[5]=13345; 4th_list[5]=1619053629000 All these properties are for the same file "sample_doc.txt". Code example: val dir = FileX.new("a_directory") |
listEverythingInQuad() | List-Quad; Quad consisting of [ String, Boolean, Long, Long ] |
- | This function returns a single list, each element of the list being a Quad of 4 items. Each Quad item contain information of 1 file / directory. All the quads together denote all the contents of the directory. Each Quad element consists: - first: String : Name of an object (a file or directory).- second: Boolean : If the corresponding object is a directory or not.- third: Long : File size (in bytes, length() ) of the object. This is not accurate if the object is a directory (to be checked from the second )- fourth: Long : lastModified() value of the object.Code example: val dir = FileX.new("a_directory") |
You can easily write to a newly created file without having to deal with input or output streams. Check the below example:
// create a blank file
val fx = FileX.new(/my_dir/my_file.txt)
fx.createNewFile(makeDirectories = true)
// start writing to file
fx.startWriting(object : FileX.Writer() {
override fun writeLines() {
// write strings without line breaks.
writeString("a string. ")
writeString("another string in the same line.")
// write a new line. Similar to writeString() with a line break at the end.
writeLine("a new line.")
writeLine("3rd line.")
}
})