【问题标题】:Write permissions not working - scoped storage Android SDK 30 (aka Android 11)写入权限不起作用 - 范围存储 Android SDK 30(又名 Android 11)
【发布时间】:2020-12-13 09:57:47
【问题描述】:

还有其他人发现范围存储几乎不可能开始工作吗?哈哈。

我一直在尝试了解如何允许用户向我的应用授予对应用文件夹之外的文本文件的写入权限。 (假设允许用户在其 Documents 文件夹中编辑文件的文本)。我已经设置了MANAGE_EXTERNAL_STORAGE 权限,并且可以确认该应用程序具有该权限。但还是每次尝试

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

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

我的清单请求这三个权限:

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

【问题讨论】:

标签: android kotlin android-10.0 android-11 scoped-storage


【解决方案1】:

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

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

【讨论】:

    【解决方案2】:

    就您的代码而言:

    • 您列出的所有权限都与ACTION_OPEN_DOCUMENT 无关
    • Intent 上的标志都不属于那里

    不过,您真正的问题是您似乎在选择媒体,例如从音频类别中选择媒体。 ACTION_OPEN_DOCUMENT 保证我们可以从Uri 标识的内容中读取,但不保证可写位置。不幸的是,MediaProvider 阻止了所有写访问,抛出了您引用的消息的异常。

    the issue that I filed last year引用我自己的话:

    问题是我们无法在ACTION_OPEN_DOCUMENT Intent 上指定我们打算写入的内容,因此希望将用户限制在可写位置。鉴于 Android Q/R 更加强调我们迁移到存储访问框架,因此需要这种功能。否则,我们只能检测到我们没有写权限(例如DocumentFilecanWrite()),然后告诉用户“对不起,我不能在那里写”,这会导致糟糕的用户体验。

    我在this blog post 中写了更多关于这个问题的内容。

    因此,使用DocumentFilecanWrite() 来查看是否允许您写入Uri 标识的位置,并要求用户选择不同的文档。

    【讨论】:

    • 但是有没有办法让用户授予我们写入文件的权限?为了清楚起见,我正在制作一个文档文本编辑器,我希望用户能够选择他们想要打开和编辑的任何文件。难道不应该有办法让用户授予我们写入用户想要的任何文件的权限吗?
    • @KamalaPhoenix:“但有没有办法让用户授予我们写入文件的权限?” -- 不是通过ACTION_OPEN_DOCUMENT。 “我正在制作文档文本编辑器”——这不是媒体。我只能在选择媒体(例如,音频中的音乐曲目)时遇到此异常。唯一的例外是“媒体是只读的”。如果您的用户从存储访问框架 UI 中更传统的位置(例如,“内部存储”)中进行选择,则不应出现此异常,并且应该能够写入请求的位置。
    • 很奇怪。从“Internal Storage > Documents”文件夹打开 Markdown 文件 (.md) 时出现此错误。
    • @KamalaPhoenix:这很奇怪也很可怕。这是什么设备?您能否提供实际返回的Uri
    • 这是运行 Android 11 的 Google Pixel 3。我在 onActivityResult 中获得的 URI 是:content://com.android.providers.media.documents/document/document%3A22678。值得注意的是,当我使用模拟器或运行 sdk 28 的其他手机时,一切正常。但是在 sdk 30 中使用 android:requestLegacyExternalStorage="true" 并不能解决这些错误。
    【解决方案3】:

    在 Android 11 上并使用 API 30 模拟器进行测试时,我发现了类似的公共文件夹

    Download, Documents, DCIM, Alarms, Pictures and such 
    

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

    仅限于应用自己的文件。

    此外,我发现一个应用程序以这种方式创建的文件可以由另一个应用程序使用 SAF 写入。

    【讨论】:

    • 您能否阐明“使用经典文件系统路径可写”的含义?这是否包括下载或通过 USB 传输到设备上的文件?
    • 那么您是如何以旧方式读写文件的?不使用 uris 或内容提供商或 saf 或 mediastore?但是使用 File 和 FileInput/OutputStream。
    • 啊。我永远无法弄清楚如何让用户选择文件并从中获取文件。文件选择器似乎总是给你一个 Uri。听起来我需要寻找一种较旧的方式来选择文件。
    • 没有。一点也不。不要回到过去。所有这些都可以使用 uris 来完成。如果您使用 ACTION_OPEN_DOCUMENT 获得 uri,那么它是可写的。如果不检查 inActivityResult 中获得的标志。您尝试进行读写,但您可以先检查您是否获得了读写权限。进一步从您的意图中删除标志。你不能授予任何东西。相反,如果在 onActivityResult 中授予您任何东西,您应该很高兴。
    • @blackapps:我无法重现您对Documents/ 的发现。我没有尝试过其他位置。当我尝试写入用户通过 USB 电缆传输到设备上的文档时,我在 Pixel 4 上遇到了与 Kamala 相同的异常。我想 USB 电缆选项可以使文件可由 nothing 写入,但这似乎很奇怪。更糟糕的是,我们用于确定我们是否具有写访问权限的标准选项都声称我们有,尽管事实上它失败了。
    猜你喜欢
    • 1970-01-01
    • 2021-11-21
    • 1970-01-01
    • 1970-01-01
    • 2021-04-14
    • 2021-06-03
    • 1970-01-01
    • 1970-01-01
    • 2019-12-18
    相关资源
    最近更新 更多