Android ExoPlayer 2 track selection example

 A simple activity for choosing a video url to be played by the ExoPlayer 2.

class MainActivity : AppCompatActivity() {

    // Sample videos, more can be found in the media.exolist.json in the assets folder
    private val hls = "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8"
    private val dash = "http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=51AF5F39AB0CEC3E5497CD9C900EBFEAECCCB5C7.8506521BFC350652163895D4C26DEE124209AA9E&key=ik0"
    private val mp4 = "https://html5demos.com/assets/dizzy.mp4"

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

        btn1.setOnClickListener { et_url.setText(hls) }
        btn2.setOnClickListener { et_url.setText(dash) }
        btn3.setOnClickListener { et_url.setText(mp4) }

        btn_play.setOnClickListener {
            val preferExtensionDecoders = false
            val abrAlgorithm = PlayerActivity.ABR_ALGORITHM_DEFAULT
            val intent = Intent(this, PlayerActivity::class.java)

            val videoUrl = et_url.text.toString()
            intent.putExtra(PlayerActivity.PREFER_EXTENSION_DECODERS_EXTRA, preferExtensionDecoders)
            intent.putExtra(PlayerActivity.ABR_ALGORITHM_EXTRA, abrAlgorithm)
            intent.data = Uri.parse(videoUrl)
            intent.action = PlayerActivity.ACTION_VIEW

            // this video url doesn't end with a file extension, therefore it needs a extension override
            // for buildMediaSource(Uri uri, @Nullable String overrideExtension) in the PlayerActivity
            if (videoUrl == dash) {
                intent.putExtra(EXTENSION_EXTRA, ".mpd")
            }

            startActivity(intent)
        }
    }

}

In the PlayerActivity, after the DefaultTrackSelector is initialized by the ExoPlayer, it will be populated with track info of the provided video source. Here is a code snippet to print out the track info to logcat.

    logTracksButton.setOnClickListener( view -> {
      Log.d(TAG, "log tracks clicked");
      MappedTrackInfo mappedTrackInfo = Assertions.checkNotNull(trackSelector.getCurrentMappedTrackInfo());
      DefaultTrackSelector.Parameters parameters = trackSelector.getParameters();

      for (int rendererIndex = 0; rendererIndex < mappedTrackInfo.getRendererCount(); rendererIndex++) {
        if (TrackSelectionDialog.showTabForRenderer(mappedTrackInfo, rendererIndex)) {
          int trackType = mappedTrackInfo.getRendererType(rendererIndex);
          TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(rendererIndex);
          Boolean isRendererDisabled = parameters.getRendererDisabled(rendererIndex);
          DefaultTrackSelector.SelectionOverride selectionOverride = parameters.getSelectionOverride(rendererIndex, trackGroupArray);

          Log.d(TAG, "------------------------------------------------------Track item " + rendererIndex + "------------------------------------------------------");
          Log.d(TAG, "track type: " + trackTypeToName(trackType));
          Log.d(TAG, "track group array: " + new Gson().toJson(trackGroupArray));
          for (int groupIndex = 0; groupIndex < trackGroupArray.length; groupIndex++) {
            for (int trackIndex = 0; trackIndex <  trackGroupArray.get(groupIndex).length; trackIndex++) {
              String trackName = new DefaultTrackNameProvider(getResources()).getTrackName(trackGroupArray.get(groupIndex).getFormat(trackIndex));
              Boolean isTrackSupported = mappedTrackInfo.getTrackSupport(rendererIndex, groupIndex, trackIndex) == RendererCapabilities.FORMAT_HANDLED;
              Log.d(TAG, "track item " + groupIndex +": trackName: " + trackName + ", isTrackSupported: " + isTrackSupported);
            }
          }
          Log.d(TAG, "isRendererDisabled: " + isRendererDisabled);
          Log.d(TAG, "selectionOverride: " + new Gson().toJson(selectionOverride));
        }
      }

    });

This is what it will look like from the above code for printing the track info.

PlayerActivity: log tracks clicked
PlayerActivity: ------------------------------------------------------Track item 0------------------------------------------------------
PlayerActivity: track type: TRACK_TYPE_VIDEO
PlayerActivity: track group array: {"hashCode":0,"length":1,"trackGroups":[{"formats":[{"accessibilityChannel":-1,"bitrate":263851,"channelCount":-1,"codecs":"avc1.4d400d","encoderDelay":0,"encoderPadding":0,"frameRate":-1.0,"hashCode":0,"height":234,"id":"0","initializationData":[[0,0,1,39,77,64,13,-87,24,52,63,-14,96,13,65,-128,65,-83,-73,-96,-48,38,94,-9,-64,64,0],[0,0,1,40,-34,9,-120]],"maxInputSize":-1,"metadata":{"entries":[{"variantInfos":[{"audioGroupId":"bipbop_audio","bitrate":263851,"subtitleGroupId":"subs"}]}]},"pcmEncoding":-1,"pixelWidthHeightRatio":1.0,"roleFlags":0,"rotationDegrees":0,"sampleMimeType":"video/avc","sampleRate":-1,"selectionFlags":0,"stereoMode":-1,"subsampleOffsetUs":9223372036854775807,"width":416},{"accessibilityChannel":-1,"bitrate":577610,"channelCount":-1,"codecs":"avc1.4d401e","encoderDelay":0,"encoderPadding":0,"frameRate":-1.0,"hashCode":0,"height":360,"id":"1","initializationData":[[0,0,1,39,77,64,13,-87,24,52,63,-14,96,13,65,-128,65,-83,-73,-96,-48,38,94,-9,-64,64,0],[0,0,1,40,-34,9,-120]],"maxInputSize":-1,"metadata":{"entries":[{"variantInfos":[{"audioGroupId":"bipbop_audio","bitrate":577610,"subtitleGroupId":"subs"}]}]},"pcmEncoding":-1,"pixelWidthHeightRatio":1.0,"roleFlags":0,"rotationDegrees":0,"sampleMimeType":"video/avc","sampleRate":-1,"selectionFlags":0,"stereoMode":-1,"subsampleOffsetUs":9223372036854775807,"width":640},{"accessibilityChannel":-1,"bitrate":915905,"channelCount":-1,"codecs":"avc1.4d401f","encoderDelay":0,"encoderPadding":0,"frameRate":-1.0,"hashCode":0,"height":540,"id":"2","initializationData":[[0,0,1,39,77,64,13,-87,24,52,63,-14,96,13,65,-128,65,-83,-73,-96,-48,38,94,-9,-64,64,0],[0,0,1,40,-34,9,-120]],"maxInputSize":-1,"metadata":{"entries":[{"variantInfos":[{"audioGroupId":"bipbop_audio","bitrate":915905,"subtitleGroupId":"subs"}]}]},"pcmEncoding":-1,"pixelWidthHeightRatio":1.0,"roleFlags":0,"rotationDegrees":0,"sampleMimeType":"video/avc","sampleRate":-1,"selectionFlags":0,"stereoMode":-1,"subsampleOffsetUs":9223372036854775807,"width":960},{"accessibilityChannel":-1,"bitrate":1030138,"channelCount":-1,"codecs":"avc1.4d401f","encoderDelay":0,"encoderPadding":0,"frameRate":-1.0,"hashCode":0,"height":720,"id":"3","initializationData":[[0,0,1,39,77,64,13,-87,24,52,63,-14,96,13,65,-128,65,-83,-73,-96,-48,38,94,-9,-64,64,0],[0,0,1,40,-34,9,-120]],"maxInputSize":-1,"metadata":{"entries":[{"variantInfos":[{"audioGroupId":"bipbop_audio","bitrate":1030138,"subtitleGroupId":"subs"}]}]},"pcmEncoding":-1,"pixelWidthHeightRatio":1.0,"roleFlags":0,"rotationDegrees":0,"sampleMimeType":"video/avc","sampleRate":-1,"selectionFlags":0,"stereoMode":-1,"subsampleOffsetUs":9223372036854775807,"width":1280},{"accessibilityChannel":-1,"bitrate":1924009,"channelCount":-1,"codecs":"avc1.4d401f","encoderDelay":0,"encoderPadding":0,"frameRate":-1.0,"hashCode":0,"height":1080,"id":"4","initializationData":[[0,0,1,39,77,64,13,-87,24,52,63,-14,96,13,65,-128,65,-83,-73,-96,-48,38,94,-9,-64,64,0],[0,0,1,40,-34,9,-120]],"maxInputSize":-1,"metadata":{"entries":[{"variantInfos":[{"audioGroupId":"bipbop_audio","bitrate":1924009,"subtitleGroupId":"subs"}]}]},"pcmEncoding":-1,"pixelWidthHeightRatio":1.0,"roleFlags":0,"rotationDegrees":0,"sampleMimeType":"video/avc","sampleRate":-1,"selectionFlags":0,"stereoMode":-1,"subsampleOffsetUs":9223372036854775807,"width":1920}],"hashCode":0,"length":5}]}
PlayerActivity: track item 0: trackName: 416 × 234, 0.26 Mbps, isTrackSupported: true
PlayerActivity: track item 0: trackName: 640 × 360, 0.58 Mbps, isTrackSupported: true
PlayerActivity: track item 0: trackName: 960 × 540, 0.92 Mbps, isTrackSupported: true
PlayerActivity: track item 0: trackName: 1280 × 720, 1.03 Mbps, isTrackSupported: true
PlayerActivity: track item 0: trackName: 1920 × 1080, 1.92 Mbps, isTrackSupported: true
PlayerActivity: isRendererDisabled: false
PlayerActivity: selectionOverride: null
PlayerActivity: ------------------------------------------------------Track item 1------------------------------------------------------
PlayerActivity: track type: TRACK_TYPE_AUDIO
PlayerActivity: track group array: {"hashCode":0,"length":2,"trackGroups":[{"formats":[{"accessibilityChannel":-1,"bitrate":-1,"channelCount":2,"codecs":"mp4a.40.2","encoderDelay":0,"encoderPadding":0,"frameRate":-1.0,"hashCode":0,"height":-1,"id":"bipbop_audio:BipBop Audio 1","initializationData":[[19,-112]],"label":"BipBop Audio 1","language":"en","maxInputSize":-1,"pcmEncoding":-1,"pixelWidthHeightRatio":1.0,"roleFlags":0,"rotationDegrees":0,"sampleMimeType":"audio/mp4a-latm","sampleRate":22050,"selectionFlags":5,"stereoMode":-1,"subsampleOffsetUs":9223372036854775807,"width":-1}],"hashCode":0,"length":1},{"formats":[{"accessibilityChannel":-1,"bitrate":-1,"channelCount":2,"codecs":"mp4a.40.2","encoderDelay":0,"encoderPadding":0,"frameRate":-1.0,"hashCode":0,"height":-1,"id":"bipbop_audio:BipBop Audio 2","initializationData":[[19,-112]],"label":"BipBop Audio 2","language":"en","maxInputSize":-1,"metadata":{"entries":[{"groupId":"bipbop_audio","name":"BipBop Audio 2","variantInfos":[]}]},"pcmEncoding":-1,"pixelWidthHeightRatio":1.0,"roleFlags":0,"rotationDegrees":0,"sampleMimeType":"audio/mp4a-latm","sampleRate":22050,"selectionFlags":0,"stereoMode":-1,"subsampleOffsetUs":9223372036854775807,"width":-1}],"hashCode":0,"length":1}]}
PlayerActivity: track item 0: trackName: English, Stereo, isTrackSupported: true
PlayerActivity: track item 1: trackName: English, Stereo, isTrackSupported: true
PlayerActivity: isRendererDisabled: false
PlayerActivity: selectionOverride: null
PlayerActivity: ------------------------------------------------------Track item 2------------------------------------------------------
PlayerActivity: track type: TRACK_TYPE_TEXT
PlayerActivity: track group array: {"hashCode":-1775022508,"length":9,"trackGroups":[{"formats":[{"accessibilityChannel":-1,"bitrate":-1,"channelCount":-1,"encoderDelay":0,"encoderPadding":0,"frameRate":-1.0,"hashCode":-2035873696,"height":-1,"id":"1/8219","initializationData":[],"maxInputSize":-1,"pcmEncoding":-1,"pixelWidthHeightRatio":1.0,"roleFlags":0,"rotationDegrees":0,"sampleMimeType":"application/cea-608","sampleRate":-1,"selectionFlags":0,"stereoMode":-1,"subsampleOffsetUs":9223372036854775807,"width":-1}],"hashCode":-2035873138,"length":1},{"formats":[{"accessibilityChannel":-1,"bitrate":-1,"channelCount":-1,"containerMimeType":"application/x-mpegURL","encoderDelay":0,"encoderPadding":0,"frameRate":-1.0,"hashCode":1531687113,"height":-1,"id":"subs:English","initializationData":[],"label":"English","language":"en","maxInputSize":-1,"metadata":{"entries":[{"groupId":"subs","name":"English","variantInfos":[]}]},"pcmEncoding":-1,"pixelWidthHeightRatio":1.0,"roleFlags":4096,"rotationDegrees":0,"sampleMimeType":"text/vtt","sampleRate":-1,"selectionFlags":5,"stereoMode":-1,"subsampleOffsetUs":9223372036854775807,"width":-1}],"hashCode":1531687671,"length":1},{"formats":[{"accessibilityChannel":-1,"bitrate":-1,"channelCount":-1,"containerMimeType":"application/x-mpegURL","encoderDelay":0,"encoderPadding":0,"frameRate":-1.0,"hashCode":-1996913626,"height":-1,"id":"subs:English (Forced)","initializationData":[],"label":"English (Forced)","language":"en","maxInputSize":-1,"metadata":{"entries":[{"groupId":"subs","name":"English (Forced)","variantInfos":[]}]},"pcmEncoding":-1,"pixelWidthHeightRatio":1.0,"roleFlags":0,"rotationDegrees":0,"sampleMimeType":"text/vtt","sampleRate":-1,"selectionFlags":2,"stereoMode":-1,"subsampleOffsetUs":9223372036854775807,"width":-1}],"hashCode":-1996913068,"length":1},{"formats":[{"accessibilityChannel":-1,"bitrate":-1,"channelCount":-1,"containerMimeType":"application/x-mpegURL","encoderDelay":0,"encoderPadding":0,"frameRate":-1.0,"hashCode":-119866970,"height":-1,"id":"subs:Français","initializationData":[],"label":"Français","language":"fr","maxInputSize":-1,"metadata":{"entries":[{"groupId":"subs","name":"Français","variantInfos":[]}]},"pcmEncoding":-1,"pixelWidthHeightRatio":1.0,"roleFlags":4096,"rotationDegrees":0,"sampleMimeType":"text/vtt","sampleRate":-1,"selectionFlags":4,"stereoMode":-1,"subsampleOffsetUs":9223372036854775807,"width":-1}],"hashCode":-119866412,"length":1},{"formats":[{"accessibilityChannel":-1,"bitrate":-1,"channelCount":-1,"containerMimeType":"application/x-mpegURL","encoderDelay":0,"encoderPadding":0,"frameRate":-1.0,"hashCode":19283972,"height":-1,"id":"subs:Français (Forced)","initializationData":[],"label":"Français (Forced)","language":"fr","maxInputSize":-1,"metadata":{"entries":[{"groupId":"subs","name":"Français (Forced)","variantInfos":[]}]},"pcmEncoding":-1,"pixelWidthHeightRatio":1.0,"roleFlags":0,"rotationDegrees":0,"sampleMimeType":"text/vtt","sampleRate":-1,"selectionFlags":2,"stereoMode":-1,"subsampleOffsetUs":9223372036854775807,"width":-1}],"hashCode":19284530,"length":1},{"formats":[{"accessibilityChannel":-1,"bitrate":-1,"channelCount":-1,"containerMimeType":"application/x-mpegURL","encoderDelay":0,"encoderPadding":0,"frameRate":-1.0,"hashCode":-1638324860,"height":-1,"id":"subs:Español","initializationData":[],"label":"Español","language":"es","maxInputSize":-1,"metadata":{"entries":[{"groupId":"subs","name":"Español","variantInfos":[]}]},"pcmEncoding":-1,"pixelWidthHeightRatio":1.0,"roleFlags":4096,"rotationDegrees":0,"sampleMimeType":"text/vtt","sampleRate":-1,"selectionFlags":4,"stereoMode":-1,"subsampleOffsetUs":9223372036854775807,"width":-1}],"hashCode":-1638324302,"length":1},{"formats":[{"accessibilityChannel":-1,"bitrate":-1,"channelCount":-1,"containerMimeType":"application/x-mpegURL","encoderDelay":0,"encoderPadding":0,"frameRate":-1.0,"hashCode":1239776482,"height":-1,"id":"subs:Español (Forced)","initializationData":[],"label":"Español (Forced)","language":"es","maxInputSize":-1,"metadata":
PlayerActivity: track item 0: trackName: Unknown, isTrackSupported: true
PlayerActivity: track item 1: trackName: English, isTrackSupported: true
PlayerActivity: track item 2: trackName: English, isTrackSupported: true
PlayerActivity: track item 3: trackName: French, isTrackSupported: true
PlayerActivity: track item 4: trackName: French, isTrackSupported: true
PlayerActivity: track item 5: trackName: Spanish, isTrackSupported: true
PlayerActivity: track item 6: trackName: Spanish, isTrackSupported: true
PlayerActivity: track item 7: trackName: Japanese, isTrackSupported: true
PlayerActivity: track item 8: trackName: Japanese, isTrackSupported: true
PlayerActivity: isRendererDisabled: false
PlayerActivity: selectionOverride: {"data":0,"groupIndex":0,"length":1,"reason":2,"tracks":[0]}

To select a track, it requires to override the parameters in the TrackSelector. Here is an example to pick the first track item in the text type, if the video source has text tracks, the following will make the video to display the text(closed caption).

    showTextCaptionsButton.setOnClickListener(view -> {
      Log.d(TAG, "showTextCaptionsButton clicked");

      MappedTrackInfo mappedTrackInfo = Assertions.checkNotNull(trackSelector.getCurrentMappedTrackInfo());
      DefaultTrackSelector.Parameters parameters = trackSelector.getParameters();
      DefaultTrackSelector.ParametersBuilder builder = parameters.buildUpon();
      for (int rendererIndex = 0; rendererIndex < mappedTrackInfo.getRendererCount(); rendererIndex++) {
        int trackType = mappedTrackInfo.getRendererType(rendererIndex);
        if (trackType == C.TRACK_TYPE_TEXT) {
          builder.clearSelectionOverrides(rendererIndex).setRendererDisabled(rendererIndex, false);
          //{"data":0,"groupIndex":1,"length":1,"reason":2,"tracks":[0]}
          int groupIndex = 0;
          int [] tracks = {0};
          int reason = 2;
          int data = 0;
          DefaultTrackSelector.SelectionOverride override = new DefaultTrackSelector.SelectionOverride(groupIndex, tracks, reason, data);

          builder.setSelectionOverride(rendererIndex, mappedTrackInfo.getTrackGroups(rendererIndex), override);
        }
      }

      trackSelector.setParameters(builder);

    });

To remove closed captions, it requires to clear the parameters in the track selector. Here is an example for removing the closed captions.

    removeTextCaptionsButton.setOnClickListener(view -> {
      Log.d(TAG, "removeTextCaptionsButton clicked");
      MappedTrackInfo mappedTrackInfo = Assertions.checkNotNull(trackSelector.getCurrentMappedTrackInfo());
      DefaultTrackSelector.Parameters parameters = trackSelector.getParameters();
      DefaultTrackSelector.ParametersBuilder builder = parameters.buildUpon();
      for (int rendererIndex = 0; rendererIndex < mappedTrackInfo.getRendererCount(); rendererIndex++) {
        int trackType = mappedTrackInfo.getRendererType(rendererIndex);
        if (trackType == C.TRACK_TYPE_TEXT) {
          builder.clearSelectionOverrides(rendererIndex).setRendererDisabled(rendererIndex, true);
        }
      }
      trackSelector.setParameters(builder);
    });

Android external storage

 These permissions need to be declared in the manifest file for external storage in Android. Starting Android 6.0 Marshmallow (API 23), after these permission are declared in the manifest file, they also need to be requested at run time and the user has to grant these in order for the app to access the external storage.

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

On Android 4.4 (API level 19) or higher, your app doesn’t need to request any storage-related permissions to access app-specific directories within external storage. The files stored in these directories are removed when your app is uninstalled.

On devices that run Android 9 (API level 28) or lower, any app can access app-specific files within external storage, provided that the other app has the appropriate storage permissions. To give users more control over their files and to limit file clutter, apps that target Android 10 (API level 29) and higher are given scoped access into external storage called scoped storage, by default. When scoped storage is enabled, apps cannot access the app-specific directories that belong to other apps.

In Android 10, adding android:requestLegacyExternalStorage="true" in the application tag in manifest file can ignore the scoped storage restriction and your apps can still access the external storage like before. With scoped storage enforcement in Android 11 and newer, this flag is not going to work anymore.

Android load a config file from external Documents directory

 1. Create a json file like this, config.json

{
  "environment": "dev"
}

2. Upload this config.json to the Android device

adb push config.json /sdcard/Documents/config.json

3. In Android Studio, click the Device Explorer on the right bottom panel, and check that this file is loaded.

4. Create a new Android Project, and add this permission in the Manifest file. As well as adding this flag to the application tag in the manifest file. android:requestLegacyExternalStorage="true"

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

5. Add these dependencies in the build gradle file.

implementation "androidx.preference:preference-ktx:1.1.0"
implementation 'com.google.code.gson:gson:2.8.6'

6. Create Config.kt

data class Config(val environment: String)

7. Add this id android:id="@+id/tv_result"to the TextView in activity_main.xml

8. Update the MainActivity file with the following code.

import android.Manifest
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import java.io.File
import java.io.FileInputStream
import java.io.IOException
import java.nio.charset.Charset
import android.content.pm.PackageManager
import android.os.Environment
import android.os.Environment.getExternalStoragePublicDirectory
import androidx.core.content.ContextCompat
import androidx.core.app.ActivityCompat
import com.google.gson.Gson
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    companion object {
        const val TAG = "MainActivity"

    }

    // /storage/emulated/0/Documents/test.txt or link /sdcard/Documents/config.json
    private var CONFIG_FILE_PATH = "${getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS)}/config.json"
    private val REQUEST_PERMISSIONS = 100
    private val PERMISSIONS_REQUIRED = arrayOf(
        Manifest.permission.READ_EXTERNAL_STORAGE
    )

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

        Log.d(TAG, "CONFIG_FILE_PATH: ${CONFIG_FILE_PATH}")

        if (checkPermission(PERMISSIONS_REQUIRED)) {
            showFileData()
        } else {
            ActivityCompat.requestPermissions(this, PERMISSIONS_REQUIRED, REQUEST_PERMISSIONS)
        }
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        Log.d(TAG, "requestCode: $requestCode")
        Log.d(TAG, "Permissions:" + permissions.contentToString())
        Log.d(TAG, "grantResults: " + grantResults.contentToString())

        if (requestCode == REQUEST_PERMISSIONS) {
            var hasGrantedPermissions = true
            for (i in grantResults.indices) {
                if (grantResults[i] != PackageManager.PERMISSION_GRANTED) {
                    hasGrantedPermissions = false
                    break
                }
            }

            if (hasGrantedPermissions) {
                showFileData()
            } else {
                finish()
            }

        } else {
            finish()
        }
    }

    private fun showFileData() {
        val targetFile = File(CONFIG_FILE_PATH)
        val targetFileContent = if (targetFile.exists()) {
            readFile(targetFile)
        } else {
            ""
        }

        val config = Gson().fromJson(targetFileContent, Config::class.java)

        val stringBuilder = StringBuilder()
        stringBuilder.append("\n")
        stringBuilder.append("file location: $CONFIG_FILE_PATH")
        stringBuilder.append("\n")
        stringBuilder.append("file content: $targetFileContent")
        stringBuilder.append("\n")
        stringBuilder.append("environment: ${config.environment}")

        Log.d("file_debug", stringBuilder.toString())
        tv_result.text = stringBuilder.toString()
    }

    private fun readFile(file: File) : String {
        var resultStr = ""
        try {
            val fileInputStream = FileInputStream(file)
            val size = fileInputStream.available()
            val buffer = ByteArray(size)
            fileInputStream.read(buffer)
            resultStr = String(buffer, Charset.forName("UTF-8"))
            fileInputStream.close()
        } catch (e: IOException) {
            e.printStackTrace()
        }
        return resultStr
    }

    private fun checkPermission(permissions: Array<String>): Boolean {
        for (permission in permissions) {
            if (ContextCompat.checkSelfPermission(applicationContext, permission) != PackageManager.PERMISSION_GRANTED) {
                return false
            }
        }
        return true
    }

}

9. Run the app.

10. When asked for file permission, click allow.

Note: This only works for Android 10 or lower, for Android 11, scoped storage is strictly enforced and apps can no longer freely access external directories without using Documents Provider API.

How to extract filename from Uri?

Now, we can extract filename with and without extension :) You will convert your bitmap to uri and get the real path of your file. Now w...