В начале было приложение. И было это приложение для цифровой подписи. Работало оно просто: подписываешь документы на смартфоне, не думая о каких-либо дополнительных токенах. Все упаковано в телефон — и сертификаты, и библиотеки. Процесс знакомый: авторизовался, загрузил файл, подписал.

Но вот однажды пользователи стали жаловаться, что не могут найти ранее подписанные документы. Бывало, пользователь подписал файл, долго к нему не возвращался, а спустя время уже не смог его отыскать. Переустановил приложение или авторизовался с другого устройства, и все файлы как в воду канули. 

Меня зовут Георгий Левченко, я Android-разработчик в ПСБ. Начал я искать источник бед пользовательских. Оказалось, виной всему было то, что все файлы хранились в кэш-директории приложения. Понял я: чтобы файлы не пропадали, им нужен менеджмент для файлов. Отыскался у Android для таких целей готовый инструмент — Storage Access Framework (SAF), он служит для доступа к файлам через стандартный интерфейс без управления разрешениями вручную. Поведаю под катом, как я решал с его помощью эту задачу. 

Почему именно Storage Access Framework?

Припомним, как менялась история взаимодействия с хранилищем в царстве Android разных версий.

  • Android 1.0 — начиная с этой версии (и до API 23 в версии 6.0) доступ к хранилищу был неограниченным. Достаточно было указать READ_EXTERNAL_STORAGE разрешение — и, установив приложение, можно было делать все, что заблагорассудится;

  • Android 6.0 — появление модели Runtime Permission;

  • Android 4.4 — здесь уже появилась первая версия Storage Access Framework, которая позволяла выбирать файлы без запроса разрешений. Однако с последующими версиями Android работа разработчиков с SAF все больше усложнялась;

  • Android 11 — в этой версии реализовали хранилище Scoped Storage. Из-за ряда особенностей Scoped Storage нам не подошел. 

А в целом мы хотели, чтобы пользователь мог выбирать документ без лишних препятствий. Лучше всего подходило решение Storage Access Framework. На нем я и остановился.

Как хранилище Android файлы по папкам раскладывает?

Здесь можно выделить три составные части:

  • документ-провайдер — он дает возможность доступа к файлам;

  • клиентское приложение, которому требуется доступ;

  • picker — с его помощью пользователь может выбирать нужные файлы из хранилища.

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

При этом у фреймворка есть плюсы, которые и делают его наиболее подходящим решением для такой задачи, какая была у меня: 

  • не ограничивает доступ к файловой системе напрямую;

  • работает поверх существующей системы разрешений;

  • требует только явного действия пользователя для предоставления доступа;

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

Какие требования на плечи взвалили?

У нас были требования к процессу работы с файлами для пользователя.

Нужно было реализовать подобие работы с папкой загрузок на ПК: когда пользователь выделил определенную область для файлов и они там хранятся независимо от того, установлено ли еще приложение или его удалили.

Еще важна была возможность сохранять все в файле на SD-карту смартфона. Чтобы при переносе приложения на новое устройство можно было указать, что теперь на этом устройстве, после перепривязки аккаунта, файл следует забирать с SD-карты, а дальше остальные данные уже автоматически бы связывались.

С учетом этих и других требований я составил такой план:

  • запросить доступ в определенную папку; 

  • сохранить полученный путь;

  • проверять его доступность перед работой;

  • по полученному пути работать с файлами.

То есть в picker нужно выбрать папку, сохранить ее — и так получить путь, куда потом будут сохраняться файлы. В дальнейшем приложение при работе с подписью будет проверять эти пути — смотреть, что находится в папке, и управлять файлами.

Что касается запроса доступа, то здесь можно выбрать три типа:

Создать новый файл ACTION_CREATE_DOCUMENT

Открыть документ или файл ACTION_OPEN_DOCUMENT

Предоставить доступ к содержимому каталога ACTION_OPEN_DOCUMENT_TREE (доступно в Android 5.0 уровня API 21 и выше).

Чтобы было наглядно, я покажу пример, как получать такие запросы доступа:

По сути все происходит через обычный activity launcher. И здесь же можно встретить еще один камень преткновения, подсвечу этот момент — вдруг с ним кто-то еще столкнется.

Обратите внимание: все полученные URI, с которыми мы будем получать данные файла, по умолчанию не persistable. То есть они будут доступны не очень долго. Эту проблему можно решить установкой persistable-флага, но злоупотреблять им тоже не стоит, а лучше через какое-то время его релизить, так как он влияет на производительность и/или на объем занимаемой памяти.

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

API очень простой: всего лишь получить файл, данные о нем, его объем, метаинформацию и так далее.

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

В этот фрагменте видно, как я кэшировал файлы перед подписью.

Здесь тоже важный момент: я отображаю эти файлы по URI — они предоставляются без MimeType, который отвечает за тип файлов (аудио, видео и так далее). Еще надо постараться не забывать указывать fileExtension. API здесь упрощенный, но не без подводных камней.

Где еще споткнуться можно?

Еще можно набрести на ошибку взаимодействия с External Storage. Скажу честно: я так до конца и не выяснил ее причину. Возможно, дело в реализации вендоров или в Google. Во всяком случае на момент разработки разговоров об этой ошибке на форумах и в Google было множество. И Google, несмотря на заявки, ее игнорировал.

Возникала эта ошибка External Storage нестабильно и в основном на смартфонах Samsung и еще нескольких китайских брендов холдинга BBK (vivo, Realme, OnePlus). Причем exception вылетал на com.android.externalstorage.

Дело было, скорее всего, в том, что у пользователей внешнее хранилище могло быть отключено, и тогда при получении URI и обращении по нему возникала ошибка. Из-за этого мог случиться непредвиденный краш.

Решить эту проблему можно легко: просто обернуть и сделать дополнительный алерт:

Вдобавок на форуме мне подсказали, что нужно делать переход в глубинный permission из настроек. Поскольку найти его непросто, посоветую сначала показывать пользователю alert: мол, такая-то ситуация, — а затем уже делать переход по этому пермишену. Вот как это может быть реализовано:

Что в сухом остатке…

Что же, API у Storage Access Framework довольно простой и бесхитростный. Не нужно выполнять дополнительные запросы в память — просто получаем URI и с ним работаем. Пользователи, которые слабо разбираются в работе смартфона, не будут пугаться доступов в память и записи в нее, а сейчас это особенно насущно, потому что люди волнуются из-за громких историй в СМИ.

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

А вам доводилось работать с SAF? Какие трудности из-за плеча глядели, какими решениями их победить удалось? :) Прошу делиться в комментариях!