Android: List External Storage Files

This article explains how to list files from the external storage (SD Card) in Android. Though you can list files recursively using a simple method, the new Runtime Permission Model introduced in Android 6 makes it a little difficult. Let's dive into the code and see how we can list all the files recursively.

Android: List External Storage Files

As I mentioned earlier, I am using Kotlin for Android development since it is the future of Android. If you are using Java, just copy and paste the code into your class method by method. The Android Studio will translate the method into Java for you.
Step 1:
Create a new Android project with an empty activity.

Android: List External Storage Files

Project Name: List SD Card Files
Package name: com.javahelps.listsdcardfiles
Language: Kotlin

Step 2:
Open the manifests file and add the following permission to read the SD card.
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
After adding the permission, your manifest should look like this:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.javahelps.listsdcardfiles">

    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

    <application
            android:allowBackup="true"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:roundIcon="@mipmap/ic_launcher_round"
            android:supportsRtl="true"
            android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>

</manifest>
According to the Android Developer Guide android.permission.READ_EXTERNAL_STORAGE is a dangerous permission.

Step 3:
Add a TextView and a Button to the activity_main.xml as shown below. The button calls an on-click listener function named list which is to be defined in the next step.
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

    <Button
            android:text="List"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            android:id="@+id/btnList"
            android:onClick="list"/>


    <ScrollView
            android:layout_width="match_parent"
            android:layout_height="0dp"
            app:layout_constraintBottom_toTopOf="@id/btnList"
            app:layout_constraintTop_toTopOf="parent">

        <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical">

            <TextView
                    android:text=""
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    app:layout_constraintBottom_toTopOf="@id/btnList"
                    app:layout_constraintTop_toTopOf="parent"
                    android:id="@+id/txtFiles"/>
        </LinearLayout>
    </ScrollView>

</android.support.constraint.ConstraintLayout>
As you can see, the TextView is wrapped in a ScrollView to handle a large number of files.

Step 4:
Open the MainActivity.kt and add a listFiles and a listExternalStorage function as shown below:
package com.javahelps.listsdcardfiles

import android.os.Bundle
import android.os.Environment
import android.support.v7.app.AppCompatActivity
import android.widget.Toast
import kotlinx.android.synthetic.main.activity_main.*
import java.io.File


class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    private fun listExternalStorage() {
        val state = Environment.getExternalStorageState()

        if (Environment.MEDIA_MOUNTED == state || Environment.MEDIA_MOUNTED_READ_ONLY == state) {
            listFiles(Environment.getExternalStorageDirectory())
            Toast.makeText(this, "Successfully listed all the files!", Toast.LENGTH_SHORT)
                .show()
        }
    }

    /**
     * Recursively list files from a given directory.
     */
    private fun listFiles(directory: File) {
        val files = directory.listFiles()
        if (files != null) {
            for (file in files) {
                if (file != null) {
                    if (file.isDirectory) {
                        listFiles(file)
                    } else {
                        txtFiles.append(file.absolutePath + "\n")
                    }
                }
            }
        }
    }
}
The listFiles receives a directory and recursively list files in that directory. If the newly found File is a directory, it's sub-files are listed recursively. If it is an actual file, the absolute path of that file is appended to the txtFiles TextView.

Step 5:
Add the requestCode variable, the list function and override the onRequestPermissionsResult function as shown below:
package com.javahelps.listsdcardfiles

import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.support.v7.app.AppCompatActivity
import android.view.View
import android.widget.Toast
import kotlinx.android.synthetic.main.activity_main.*
import java.io.File


class MainActivity : AppCompatActivity() {

    private val requestCode = 100

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    fun list(view: View) {

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            requestPermissions(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), requestCode)
        } else {
            listExternalStorage()
        }
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
        if (requestCode == this.requestCode) {
            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                // Permission is granted
                listExternalStorage()
            } else {
                Toast.makeText(this, "Until you grant the permission, I cannot list the files", Toast.LENGTH_SHORT)
                    .show()
            }
        }
    }

    private fun listExternalStorage() {
        val state = Environment.getExternalStorageState()

        if (Environment.MEDIA_MOUNTED == state || Environment.MEDIA_MOUNTED_READ_ONLY == state) {
            listFiles(Environment.getExternalStorageDirectory())
            Toast.makeText(this, "Successfully listed all the files!", Toast.LENGTH_SHORT)
                .show()
        }
    }

    /**
     * Recursively list files from a given directory.
     */
    private fun listFiles(directory: File) {
        val files = directory.listFiles()
        if (files != null) {
            for (file in files) {
                if (file != null) {
                    if (file.isDirectory) {
                        listFiles(file)
                    } else {
                        txtFiles.append(file.absolutePath + "\n")
                    }
                }
            }
        }
    }
}
The list function checks if the Android version is 6.0 or later and if so, it checks if the READ_EXTERNAL_STORAGE permission is granted. If it is already granted, this function simply calls the listExternalStorage function. If not, it requests the permission. The onRequestPermissionsResult function will be called when the user has responded to the permission request. In that method, we check if the user has granted the permission and call the listExternalStorage function if the permission is granted. As you can see, listing files from SD card is implemented in Step 4 but it requires the runtime permission check after Android 6.

Running this code will show you the application with a button to list files. However, it is not recommended to traverse the SD card in the main thread because it may take a long time if the SD card contains a large number of files.


Step 6:
Add the rxjava dependency to the build.gradle (Module: app).
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
implementation 'io.reactivex.rxjava2:rxjava:2.x.x'

After adding these two dependencies your build.gradle should look like this:
apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "com.javahelps.listsdcardfiles"
        minSdkVersion 15
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
    implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
    implementation 'io.reactivex.rxjava2:rxjava:2.x.x'
}

After saving the changes, synchronize the project to download the dependencies.

Step 7:
Modify the MainActivity.kt as shown below:
package com.javahelps.listsdcardfiles

import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.support.v7.app.AppCompatActivity
import android.util.Log
import android.view.View
import android.widget.Toast
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import kotlinx.android.synthetic.main.activity_main.*
import org.reactivestreams.Publisher
import org.reactivestreams.Subscriber
import java.io.File


class MainActivity : AppCompatActivity() {

    private val requestCode = 100
    private var disposable: Disposable? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    fun list(view: View) {

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            requestPermissions(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), requestCode)
        } else {
            listExternalStorage()
        }
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
        if (requestCode == this.requestCode) {
            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                // Permission is granted
                listExternalStorage()
            } else {
                Toast.makeText(this, "Until you grant the permission, I cannot list the files", Toast.LENGTH_SHORT)
                    .show()
            }
        }
    }

    override fun onPause() {
        super.onPause()
        this.disposable?.dispose()
    }

    private fun listExternalStorage() {
        val state = Environment.getExternalStorageState()

        if (Environment.MEDIA_MOUNTED == state || Environment.MEDIA_MOUNTED_READ_ONLY == state) {

            this.disposable = Observable.fromPublisher(FileLister(Environment.getExternalStorageDirectory()))
                .subscribeOn(Schedulers.newThread())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe({
                    txtFiles.append(it + "\n")
                }, {
                    Log.e("MainActivity", "Error in listing files from the SD card", it)
                }, {
                    Toast.makeText(this, "Successfully listed all the files!", Toast.LENGTH_SHORT)
                        .show()
                    this.disposable?.dispose()
                    this.disposable = null
                })
        }
    }

    private class FileLister(val directory: File) : Publisher<String> {

        private lateinit var subscriber: Subscriber<in String>

        override fun subscribe(s: Subscriber<in String>?) {
            if (s == null) {
                return
            }
            this.subscriber = s
            this.listFiles(this.directory)
            this.subscriber.onComplete()
        }

        /**
         * Recursively list files from a given directory.
         */
        private fun listFiles(directory: File) {
            val files = directory.listFiles()
            if (files != null) {
                for (file in files) {
                    if (file != null) {
                        if (file.isDirectory) {
                            listFiles(file)
                        } else {
                            subscriber.onNext(file.absolutePath)
                        }
                    }
                }
            }
        }

    }

}
Notice that there is a disposable instance variable which is used to dispose the RxJava resources in the onPause method and in the onComplete (the third block in the subscribe method call) lambda expression. Now there is a FileLister publisher which receives the parent directory to traverse and publish the files to the subscriber. Once the traversal is completed (after the recursive method call), the publisher calls the onComplete method to let the subscriber know that the process is completed.

The listExternalStorage function executes the publisher in a separate thread and appends the result to TextView in the main thread.

Though we can list all the files from the external storage, it is not recommended to do so unless otherwise there is a valid reason behind it. You can find the source code of this project at the GitHub repository.




If you have any questions, feel free to comment below.
Previous
Next Post »

Contact Form

Name

Email *

Message *