写入权限无法正常工作 - 作用域存储 Android SDK 30(也称为 Android 11)

3

还有其他人发现作用域存储几乎不可能使其正常工作吗?lol。

我一直在尝试了解如何允许用户授予我的应用程序写入权限,以编辑应用程序文件夹外的文本文件。(比方说允许用户编辑他们“文档”文件夹中文件的文本)。我已经设置好了MANAGE_EXTERNAL_STORAGE 权限,并可以确认应用程序具有该权限。但每次尝试时仍然会遇到问题。

val fileDescriptor = context.contentResolver.openFileDescriptor(uri, "rwt")?.fileDescriptor

我遇到了 Illegal Argument: Media is read-only 错误。

我的清单文件(Manifest)请求了以下三个权限:

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

我也尝试使用传统存储方式:

<application
    android:allowBackup="true"
    android:requestLegacyExternalStorage="true"

但仍然遇到了只读问题。

我错过了什么吗?

额外说明

我如何获取URI:

view?.selectFileButton?.setOnClickListener {
            val intent =
                Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
                    addCategory(Intent.CATEGORY_OPENABLE)
                    type = "*/*"
                    flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
                            Intent.FLAG_GRANT_WRITE_URI_PERMISSION
                }
            startActivityForResult(Intent.createChooser(intent, "Select a file"), 111)
        }

接着

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (requestCode == 111 && resultCode == AppCompatActivity.RESULT_OK && data != null) {
        val selectedFileUri = data.data;
        if (selectedFileUri != null) {
            viewModel.saveFilename(selectedFileUri.toString())
            val contentResolver = context!!.contentResolver
            val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or
                    Intent.FLAG_GRANT_WRITE_URI_PERMISSION
            contentResolver.takePersistableUriPermission(selectedFileUri, takeFlags)
            view?.fileName?.text = viewModel.filename
            //TODO("if we didn't get the permissions we needed, ask for permission or have the user select a different file")
        }
    }
}

uri 是从哪里来的?请注意,MANAGE_EXTERNAL_STORAGE 适用于 Android 11+(而不是 Android 10)。 - CommonsWare
很高兴知道@CommonsWare!顺便说一下,我正在运行代码以针对运行SDK 30的手机。刚刚更新了问题以添加获取URI的代码。感谢您的帮助! - Kamala Phoenix
阅读此内容:https://dev59.com/BVIG5IYBdhLWcg3w01A9#66366102 - Thoriya Prahalad
请查看此链接中的方法,可能会有用。https://dev59.com/BVIG5IYBdhLWcg3w01A9#67140033 - Mori
3个回答

3

您可以尝试下面的代码。它对我有效。

class MainActivity : AppCompatActivity() {

    private lateinit var theTextOfFile: TextView
    private lateinit var inputText: EditText
    private lateinit var saveBtn: Button
    private lateinit var readBtn: Button
    private lateinit var deleteBtn: Button

    private lateinit var someText: String
    private val filename = "theFile.txt"

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

        if (!isPermissionGranted()) {
            val permissions = arrayOf(WRITE_EXTERNAL_STORAGE)
            for (i in permissions.indices) {
                requestPermission(permissions[i], i)
            }
        }

        theTextOfFile = findViewById(R.id.theTextOfFile)
        inputText = findViewById(R.id.inputText)
        saveBtn = findViewById(R.id.saveBtn)
        readBtn = findViewById(R.id.readBtn)
        deleteBtn = findViewById(R.id.deleteBtn)

        saveBtn.setOnClickListener { savingFunction() }
        deleteBtn.setOnClickListener { deleteFunction() }
        readBtn.setOnClickListener {
            theTextOfFile.text = readFile()
        }

    }

    private fun readFile() : String{
        val rootPath = "/storage/emulated/0/Download/"
        val myFile = File(rootPath, filename)
        return if (myFile.exists()) {
            FileInputStream(myFile).bufferedReader().use { it.readText() }
        }
        else "no file"
    }

    private fun deleteFunction(){
        val rootPath = "/storage/emulated/0/Download/"
        val myFile = File(rootPath, filename)
        if (myFile.exists()) {
            myFile.delete()
        }
    }

    private fun savingFunction(){
        deleteFunction()
        someText = inputText.text.toString()
        val resolver = applicationContext.contentResolver
        val values = ContentValues()
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
            values.put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
            values.put(MediaStore.MediaColumns.MIME_TYPE, "text/plain")
            values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
            val uri = resolver.insert(MediaStore.Files.getContentUri("external"), values)
            uri?.let { it ->
                resolver.openOutputStream(it).use {
                    // Write file
                    it?.write(someText.toByteArray(Charset.defaultCharset()))
                    it?.close()
                }
            }
        } else {
            val rootPath = "/storage/emulated/0/Download/"
            val myFile = File(rootPath, filename)
            val outputStream: FileOutputStream
            try {
                if (myFile.createNewFile()) {
                    outputStream = FileOutputStream(myFile, true)
                    outputStream.write(someText.toByteArray())
                    outputStream.flush()
                    outputStream.close()
                }
            } catch (e: Exception) {
                e.printStackTrace()
            }

        }
    }

    private fun isPermissionGranted(): Boolean {
        val permissionCheck = ActivityCompat.checkSelfPermission(this, WRITE_EXTERNAL_STORAGE)
        return permissionCheck == PackageManager.PERMISSION_GRANTED
    }

    private fun requestPermission(permission: String, requestCode: Int) {
        ActivityCompat.requestPermissions(this, arrayOf(permission), requestCode)
    }
}

1
关于你的代码:
- 你列出的权限与ACTION_OPEN_DOCUMENT没有任何关系 - 你的Intent上也不应该有这些标志
然而,你真正的问题是似乎在选择媒体,比如来自音频类别。ACTION_OPEN_DOCUMENT保证我们可以从由Uri标识的内容中读取,但它并不保证可写位置。不幸的是,MediaProvider阻止了所有写访问,抛出了你引用的异常消息。
引用我去年提交的问题
问题在于我们无法在 ACTION_OPEN_DOCUMENT Intent 上指定我们打算写入并因此希望限制用户只能写入位置。鉴于 Android Q/R 强调我们迁移到存储访问框架,这种功能是必需的。否则,我们只能检测到没有写入权限(例如,DocumentFilecanWrite()),然后告诉用户“抱歉,我不能在那里写入”,这会导致糟糕的用户体验。
我在this blog post中更详细地讨论了这个问题。
因此,请使用 DocumentFilecanWrite() 来查看是否允许您写入由 Uri 标识的位置,并要求用户选择不同的文档。

但是难道没有一种方法可以让用户授权我们写入文件吗?为了明确,我正在制作一个文档文本编辑器,我希望用户能够选择他们想要打开和编辑的任何文件。难道没有一种方法可以让用户授权我们写入用户想要的任何文件吗? - Kamala Phoenix
@KamalaPhoenix:“但是难道没有一种方法可以让用户给我们写入文件的权限吗?”--不是通过ACTION_OPEN_DOCUMENT。 “我正在制作一个文档文本编辑器”--那不是媒体。我只能在选择媒体(例如从音频中选择音乐曲目)时获得此异常。该异常明确表示“媒体是只读的”。如果您的用户从存储访问框架UI中的更传统的位置(例如“内部存储”)中进行选择,则不应出现此异常,并且您应该能够写入所请求的位置。 - CommonsWare
非常感谢!我真的很感激你的帮助。那么,如果我有那个标志,我可以使用其他方式访问旧的28种文件吗?还是SAF是不可避免的?只是想了解28到30的过渡应该是什么样子的。 - Kamala Phoenix
@KamalaPhoenix:对不起,但我的期望解决方法失败了。对于媒体Uri值,有一种方法可以向用户请求写入权限。我原本希望能找到一种方法来获取文档Uri的相应媒体Uri并以此方式请求权限,但这没有奏效。我将就此提交各种错误报告,因为在这个问题中涉及到几个问题。我还将尝试使用仅由应用程序创建的文件的blackapps方法,以防这是一种通过USB传输的问题。 - CommonsWare
@CommonsWare 谢谢!我很感激你的帮助。请告诉我你找到了什么。我也会进行一些实验,看看如何编译我所拥有的内容,以便在Android 11上运行。Markor和其他Markdown文本编辑器仍然可以在Android 11上工作。因此,即使涉及绕过SAF,也必须有某种方法来实现我想要的功能。我不确定。 - Kamala Phoenix
显示剩余7条评论

0
在 Android 11 上,使用 API 30 模拟器进行测试时,我发现了公共文件夹,例如:
Download, Documents, DCIM, Alarms, Pictures and such 

使用经典文件系统路径可写入我的应用程序。

仅限于应用程序自己的文件。

此外,我发现以这种方式创建的文件可以被使用SAF的不同应用程序写入。


你能澄清一下你所说的“可使用经典文件系统路径进行写入”是什么意思吗?这是否包括从设备上下载或通过 USB 传输的文件? - Kamala Phoenix
1
你是如何以旧的方式读写文件的呢?不使用URI、内容提供程序、SAF或MediaStore,而是使用File和FileInput/OutputStream。 - blackapps
不,完全不行。不要回到过去。所有都可以使用uri完成。如果您使用ACTION_OPEN_DOCUMENT获取uri,则它是可写的。如果没有,请检查inActivityResult中获得的标志。您尝试读和写,但首先可以检查是否获得了读写权限。进一步从意图中删除标志。你不能授权任何东西。相反,如果在onActivityResult中授予了任何权限,您应该感到高兴。 - blackapps
@blackapps:我无法重现您在“文档/”中的发现。我没有尝试其他位置。当我尝试通过USB电缆由用户传输到设备上的文档进行写入时,我在Pixel 4上遇到了与Kamala相同的异常情况。我想USB电缆选项可能会使文件变为不可写,但这似乎很奇怪。更糟糕的是,我们确定是否具有写访问权限的标准选项都声称我们有,尽管它失败了。 - CommonsWare
@CommonsWare,请尝试在我提到的公共文件夹中创建“新”的文件和目录。在这里运行所有API 30模拟器。 - blackapps
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接