Не работают разрешения на запись - хранилище с ограниченным объемом Android SDK 30 (также известное как Android 11)

Кто-нибудь еще считает, что хранилище с ограниченным объемом памяти практически невозможно приступить к работе? ржу не могу.

Я пытался понять, как разрешить пользователю давать моему приложению права на запись в текстовый файл вне папки приложения. (Скажем, разрешить пользователю редактировать текст файла в своей папке «Документы»). У меня есть 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")
        }
    }
}

person Kamala Phoenix    schedule 03.10.2020    source источник
comment
Откуда взялся uri? Обратите внимание, что MANAGE_EXTERNAL_STORAGE для Android 11+ (не Android 10).   -  person CommonsWare    schedule 04.10.2020
comment
Полезно знать @CommonsWare! Fwiw, я запускаю код на моем телефоне с SDK 30. Просто обновил вопрос, чтобы добавить код для получения URI. Спасибо за вашу помощь!   -  person Kamala Phoenix    schedule 04.10.2020
comment
Прочтите это: stackoverflow.com/a/66366102/9917404   -  person Thoriya Prahalad    schedule 25.02.2021
comment
Пожалуйста, посмотрите этот способ по этой ссылке, это может быть полезно. stackoverflow.com/a/67140033/12272687   -  person Mori    schedule 17.04.2021


Ответы (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)
    }
}
person Andrey Alexandrov    schedule 12.12.2020

Что касается вашего кода:

  • Ни одно из перечисленных вами разрешений не имеет ничего общего с ACTION_OPEN_DOCUMENT
  • Ни один из флагов на вашем Intent не принадлежит ему

Однако ваша настоящая проблема заключается в том, что вы, кажется, выбираете носитель, например, из категории «Аудио». ACTION_OPEN_DOCUMENT гарантирует, что мы можем читать из содержимого, идентифицированного Uri, но не гарантирует доступное для записи место. К сожалению, MediaProvider блокирует весь доступ на запись, вызывая исключение, сообщение которого вы процитировали.

Цитируя себя из проблемы, которую я подал в прошлом году:

Проблема в том, что у нас нет способа указать ACTION_OPEN_DOCUMENT Intent, который мы собираемся писать, и поэтому хотим ограничить пользователя доступными для записи местоположениями. Учитывая, что Android Q / R уделяет особое внимание переходу на Storage Access Framework, такая функция необходима. В противном случае все, что мы можем сделать, это определить, что у нас нет доступа на запись (например, DocumentFile и canWrite()), а затем сказать пользователю, извините, я не могу писать туда, что приводит к плохому взаимодействию с пользователем.

Я написал немного больше об этой проблеме в это сообщение в блоге.

Итак, используйте DocumentFile и canWrite(), чтобы узнать, разрешено ли вам писать в место, обозначенное Uri, и попросите пользователя выбрать другой документ.

person CommonsWare    schedule 03.10.2020
comment
Но разве нет способа заставить пользователя дать нам разрешение на запись в файл? Для ясности я делаю текстовый редактор документов и хочу, чтобы пользователь мог выбирать любой файл, который он хочет открыть и отредактировать. Разве не должно быть способа позволить пользователю дать нам разрешение на запись в любой файл, который хочет пользователь? - person Kamala Phoenix; 04.10.2020
comment
@KamalaPhoenix: Но разве нет способа заставить пользователя дать нам разрешение на запись в файл? - не через ACTION_OPEN_DOCUMENT. Я делаю текстовый редактор документов - это не медиа. Мне удавалось получить это исключение только при выборе носителя (например, музыкальной дорожки из Audio). Само исключение говорит, что носитель предназначен только для чтения. Если ваши пользователи выбирают более традиционные места в пользовательском интерфейсе Storage Access Framework (например, внутреннее хранилище), вы не должны получать это исключение и сможете писать в запрошенное место. - person CommonsWare; 04.10.2020
comment
Странный. Я получаю эту ошибку при открытии файла уценки (.md) из папки Внутреннее хранилище ›Документы. - person Kamala Phoenix; 04.10.2020
comment
@KamalaPhoenix: Это странно и страшно. Что это за устройство? Можете ли вы предоставить реальный Uri, который вы получаете обратно? - person CommonsWare; 04.10.2020
comment
Это Google Pixel 3 под управлением Android 11. URI, который я получаю в onActivityResult: content://com.android.providers.media.documents/document/document%3A22678. Также стоит отметить, что когда я использую эмулятор или другой телефон с sdk 28, все работает нормально. Но использование android:requestLegacyExternalStorage="true" в sdk 30 не помогает с этими ошибками. - person Kamala Phoenix; 04.10.2020
comment
@KamalaPhoenix: Но использование android: requestLegacyExternalStorage = true в sdk 30 не помогает с этими ошибками - это не должно. Это для прямого доступа к файловой системе, а не для Storage Access Framework. У меня есть Pixel 3 с 11 (хотя мне нужно посмотреть, будет ли он в финале - я использовал его для бета-версий). Попробую завтра провести эксперимент и посмотрю, что увижу. - person CommonsWare; 04.10.2020
comment
Спасибо! Я очень благодарен за помощь ???????? Значит, если у меня есть этот флаг, я могу использовать что-то еще для доступа к файлам старым способом? Или SAF неизбежен? Просто пытаюсь понять, как должен выглядеть переход с 28 на 30. - person Kamala Phoenix; 04.10.2020
comment
all we can do is detect that we do not have write access (e.g., DocumentFile and canWrite()). Возможно, при получении постоянного разрешения разрешение на запись уже не работает в data.getFlags () и, следовательно, не может быть использовано .. (Не проверено) - person blackapps; 04.10.2020
comment
@KamalaPhoenix: Хорошо, используя этот образец приложения, Я получаю те же базовые результаты, что и вы, для файлов, которые были созданы другими (например, при передаче файла пользователем через USB). Для документов, созданных приложением (например, ACTION_CREATE_DOCUMENT), доступ на запись SAF работает нормально. Я поэкспериментирую с обходным решением в ближайшие дни. Значит, если у меня есть этот флаг, я могу использовать что-нибудь еще для доступа к файлам старым способом? - до следующего года, да, когда вы поднимете targetSdkVersion до 30 или выше. - person CommonsWare; 05.10.2020
comment
@CommonsWare, спасибо !! Буквальная палочка-выручалочка. С нетерпением жду того, что вы найдете. - person Kamala Phoenix; 05.10.2020
comment
@KamalaPhoenix: Извините, но мой долгожданный обходной путь не удался. Для значений media Uri есть способ запросить у пользователя разрешение на запись. Я надеялся, что смогу найти способ получить соответствующий носитель Uri для документа Uri и запросить разрешение таким образом, но это не сработало. Я буду подавать различные отчеты об ошибках по этому поводу, так как есть несколько проблем, связанных с этой проблемой. Я также попробую использовать подход blackapps к использованию файлов, созданных исключительно приложениями, на случай, если это каким-то образом связано с проблемой передачи через USB. - person CommonsWare; 11.10.2020
comment
@CommonsWare спасибо! Я ценю помощь. Сообщите мне, что вы найдете. Я также поэкспериментирую, как скомпилировать то, что у меня есть, чтобы оно работало на Android 11. Markor и другие текстовые редакторы уценки по-прежнему работают в Android 11. Так что должен быть какой-то способ делать то, что я хочу, даже если это связано с как-то обходить SAF? Я не уверен. - person Kamala Phoenix; 14.10.2020

На Android 11 и тестировании с эмуляторами API 30 я обнаружил общедоступные папки, такие как

Download, Documents, DCIM, Alarms, Pictures and such 

возможность записи для моих приложений с использованием классических путей файловой системы.

Только для собственных файлов приложения.

Кроме того, я обнаружил, что файлы, созданные одним приложением таким образом, могут быть записаны другим приложением с использованием SAF.

person blackapps    schedule 04.10.2020
comment
Не могли бы вы пояснить, что вы подразумеваете под записью с использованием классических путей файловой системы? И включает ли это файлы, загруженные или перенесенные на устройство с USB-накопителя? - person Kamala Phoenix; 05.10.2020
comment
Ну как ты по старинке читал и записывал файлы? Без использования URI, контент-провайдеров, сейфа или медиа-магазина? Но используя File и FileInput / OutputStream. - person blackapps; 05.10.2020
comment
Ах. Я никогда не мог понять, как позволить пользователю выбрать файл и получить из него файл. Селектор файлов, кажется, всегда возвращает вам Uri. Похоже, мне нужно найти более старый способ выбора файлов. - person Kamala Phoenix; 06.10.2020
comment
Нет, совсем нет. Не возвращайся в прошлое. Все можно сделать с помощью уриов. Если вы получаете uri с помощью ACTION_OPEN_DOCUMENT, тогда он доступен для записи. Если нет, проверьте полученные флаги в inActivityResult. Вы пытаетесь читать и писать, но сначала можете проверить, есть ли у вас разрешение на чтение и запись. Далее удалите флаги из вашего намерения. Вы не можете ничего дать. Вместо этого вы должны быть рады, если вам что-то будет предоставлено в onActivityResult. - person blackapps; 06.10.2020
comment
@blackapps: я не могу воспроизвести ваши выводы для Documents/. В других местах я не пробовал. У меня такое же исключение на Pixel 4, что и у Камалы, когда я пытаюсь записать документ, переданный на устройство пользователем через USB-кабель. Я полагаю, что вариант с USB-кабелем может сделать файл доступным для записи ничем, но это кажется странным. Хуже того, наши стандартные варианты определения того, есть ли у нас доступ для записи, все утверждают, что мы это делаем, несмотря на то, что это не удается. - person CommonsWare; 11.10.2020
comment
@CommonsWare, попробуйте создать -новые- файлы и каталоги в общих папках, о которых я упоминал. Работает на всех эмуляторах api 30 здесь. - person blackapps; 11.10.2020
comment
@blackapps: Как же странно. Вчера и 10 минут назад я мог воспроизвести проблемы Камалы. Теперь, внезапно, я не могу. Я точно не знаю, что происходит. Я подозреваю, что это настоящая проблема, но я не знаю, как ее достоверно воспроизвести сейчас. :-( - person CommonsWare; 11.10.2020
comment
@CommonsWare, это так странно! Вы недавно обновляли телефон? Интересно, поймали ли они ошибку и решили ее со своей стороны? Можно еще на эмуляторе воспроизвести? Я могу попробовать на своем конце и посмотреть ... - person Kamala Phoenix; 25.10.2020
comment
@CommonsWare Итак, у меня была похожая ситуация, когда я больше не мог воспроизвести свои проблемы. А сегодня я сбросил настройки своего телефона и переустановил приложение, и у меня снова те же проблемы, что и раньше. Вы в конечном итоге сообщали об ошибках в Google? Могу ли я где-нибудь я сообщать об ошибках? - person Kamala Phoenix; 02.12.2020
comment
@KamalaPhoenix: Вы в конечном итоге сообщали об ошибках в Google? - Я так не думаю, потому что не думаю, что когда-нибудь смогу повторить эту проблему еще раз. Где я могу сообщать об ошибках? - Issueetracker.google.com является домом для такого рода ошибок. Это, вероятно, попадет в категорию Framework. Однако ключом к этим отчетам об ошибках является воспроизводимый тестовый пример. Без этого вряд ли вы далеко уйдете. Даже с воспроизводимым тестовым примером это борьба. - person CommonsWare; 02.12.2020
comment
@CommonsWare Уф. Да, это похоже на черную дыру. Ах! И теперь он вернулся к работе. Я замечаю проблему, когда кажется, что разрешения зависят от того, как я доберусь до выбранного файла. Если я просматриваю папку ярлыков «Документы» в приложении «Файл», то получаю ошибку с правами доступа. Если я перейду через внутреннее хранилище и перейду непосредственно к файлу, я не получу ошибки с разрешениями. Я правильно понял URI файла? Вот как я это делаю: github.com/madCode/dailylog/blob/master/app/src/main/java/com/ - person Kamala Phoenix; 02.12.2020
comment
@KamalaPhoenix: Ваши ссылки на filename немного пугающие, но в остальном это кажется разумным. Я видел некоторые различия в ответах SAF, основанные на отправной точке, как вы описываете, но не на этой конкретной. Можете ли вы убедиться, что ваш вопрос актуален, а также предоставить конкретную трассировку стека, которую вы видите? - person CommonsWare; 02.12.2020