Skip to content

SayantanRC/FileX

Repository files navigation

FileX

JitPack
Build instructions

Quick links

Philosophy

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:

  1. 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.
  2. 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.
  3. Known syntax is used. You will find methods like mkdirs(), delete(), canonicalPath just like old Java File had.

How paths are interpreted?

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

Internal classification (based on isTraditional)

Classification

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.

Getting started

You can import the library in your project in any of the below ways.

Get the library from jitpack.io

  1. In top-level build.gradle file, in allprojects section, add jitpack as shown below.
allprojects {
    repositories {
        google()
        jcenter()
        ...
        maven { url 'https://jitpack.io' }
    }
}
  1. 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-7'
}

Perform a gradle sync. Now you can use the library in the project.

Use the AAR file from this repository.

  1. Get the latest released AAR file from the Releases page.
  2. In your app module directory of the project, there should exist a directory named libs. If not, create it.
  3. Place the downloaded AAR file inside the libs directory.
  4. In the "app" level build.gradle file, add the following lines under dependencies.
dependencies {
    ...
    implementation fileTree(dir: 'libs', include: ['*.aar'])
    implementation files('libs/FileX-release.aar')
}

Perform a gradle sync to use the library.

Initialization

Initialize 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.

Manifest

<application
        ...
        android:name=".AppInstance"
        ...
        >
        ...
        
</application>

Application class

class AppInstance: Application() {
    override fun onCreate() {
        ...
        FileXInit(this, false)
    }
}

Create FileX objects to work with

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.

Init methods

These are public methods available from FileXInit class.

Check for file read-write permissions.

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.

Usage

val isPermissionGranted = FileXInit.isUserPermissionGranted()
if (isPermissionGranted){
    // ... create some files
}

Request for read-write access.

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.

Arguments:

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 is Activity.RESULT_OK else usually Activity.RESULT_CANCELED.
  • data: Intent with some information.
    • For FileXT
      data.getStringArrayExtra("permissions") = Array is requested permissions. Equal to array consisting Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE
      data.getStringArrayExtra("grantResults") = Array is granted permissions. If granted, should be equal to array of PackageManager.PERMISSION_GRANTED, PackageManager.PERMISSION_GRANTED
    • For FileX11
      data.data = Uri of the selected root directory.

Usage

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
}

Refresh storage volumes

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()

Public attributes for FileX

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 file 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 file 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.

Public methods for FileX

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")
dir.listEverything()?.let { everything ->
for (i in everything.first.indices) {
// loop over all the indices
// NOTE: Actual size of directories will be incorrect.
Log.d("DEBUG_TAG",
"Found content with name "${everything.first[i]}"" +
"This file is a directory: ${everything.second[i]}." +
"Size (in bytes) of this file: ${everything.third[i]}." +
"Last modified value: ${everything.fourth[i]}."
)
}
}
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")
dir.listEverythingInQuad()?.forEach {
// Each item represents 1 file or directory
Log.d("DEBUG_TAG",
"Found content with name: "${it.first}"." +
"This file is a directory: ${it.second}." +
"Size (in bytes) of this file: ${it.third}." +
"Last modified value: ${it.fourth}."
)
}

Easy writing to files.

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.")
  }
})