Android Kotlin: Getting a FileNotFoundException wi

2020-01-29 02:27发布

问题:

I'm working on an Android application where one of the features is to let the user choose a file to open (I'm wanting to open a plain text .txt file). I've worked on Android apps before with Java, but for this one, I'm using Kotlin, and it's my first time using Kotlin.

I currently have the app display a file chooser and let the user tap the file they want to open. Then I'm trying to use a File object to open the file and do a forEachLine loop. But for some reason, it's throwing a java.io.FileNotFoundException (No such file or directory) with the file chosen from the file picker. I'm not sure what's wrong, and if I have to do some conversion to convert the file path?

The code for my 'load' button:

val btn_load: Button = findViewById<Button>(R.id.btn_load_puzzle)
    btn_load.setOnClickListener {
        val intent = Intent()
            .setType("*/*")
            .setAction(Intent.ACTION_GET_CONTENT)

        startActivityForResult(Intent.createChooser(intent, "Select a file"), 111)
    }

My function to respond to the file selection:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)

    // Selected a file to load
    if ((requestCode == 111) && (resultCode == RESULT_OK)) {
        val selectedFilename = data?.data //The uri with the location of the file
        if (selectedFilename != null) {
            val filenameURIStr = selectedFilename.toString()
            if (filenameURIStr.endsWith(".txt", true)) {
                val msg = "Chosen file: " + filenameURIStr
                val toast = Toast.makeText(applicationContext, msg, Toast.LENGTH_SHORT)
                toast.show()
                File(selectedFilename.getPath()).forEachLine {
                    val toast = Toast.makeText(applicationContext, it, Toast.LENGTH_SHORT)
                    toast.show()
                }
            }
            else {
                val msg = "The chosen file is not a .txt file!"
                val toast = Toast.makeText(applicationContext, msg, Toast.LENGTH_LONG)
                toast.show()
            }
        }
        else {
            val msg = "Null filename data received!"
            val toast = Toast.makeText(applicationContext, msg, Toast.LENGTH_LONG)
            toast.show()
        }
    }
}

The FileNotFound exception is thrown on the line where it creates the File object to do the forEachLine loop:

java.lang.RuntimeException: Failure delivering result ResultInfo{who=null, request=111, result=-1, data=Intent { dat=content://com.android.externalstorage.documents/document/0000-0000:Sudoku puzzles/hard001.txt flg=0x1 }} to activity {com.example.sudokusolver/com.example.sudokusolver.MainActivity}: java.io.FileNotFoundException: /document/0000-0000:Sudoku puzzles/hard001.txt (No such file or directory)

回答1:

You did not receive a file path, you received a Uri. You have to use Uri based APIs such as ContentResolver.openInputStream() to access the contents at that Uri as Android does not grant your app direct File access to the underlying file (it could also be streamed from Google Drive or downloaded directly from the internet without your app being aware that this is happening):

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)

    // Selected a file to load
    if ((requestCode == 111) && (resultCode == RESULT_OK)) {
        val selectedFilename = data?.data //The uri with the location of the file
        if (selectedFilename != null) {
            contentResolver.openInputStream(selectedFilename)?.bufferedReader()?.forEachLine {
                val toast = Toast.makeText(applicationContext, it, Toast.LENGTH_SHORT)
                toast.show()
            }
        } else {
            val msg = "Null filename data received!"
            val toast = Toast.makeText(applicationContext, msg, Toast.LENGTH_LONG)
            toast.show()
        }
    }
}

Here we can assume we get contents of the proper format by passing in the proper mime type to our request (as there is no requirement that a text file end in exactly the .txt extension as part of its path):

val intent = Intent()
    .setType("text/*")
    .setAction(Intent.ACTION_GET_CONTENT)

startActivityForResult(Intent.createChooser(intent, "Select a file"), 111)

Which will automatically make any non text file unable to be selected.



回答2:

You cannot open a Java File on a ÙRI converted to a String, the "path" section of the URI has no relation to a physical file location.

Use a contentResolver to get a Java FileDescriptor to open the file with.

val parcelFileDescriptor: ParcelFileDescriptor =
            contentResolver.openFileDescriptor(uri, "r")
    val fileDescriptor: FileDescriptor = parcelFileDescriptor.fileDescriptor

This method is compatible with Android 10 where file paths for non App private directories are not usuable.

https://developer.android.com/training/data-storage/shared/documents-files