Как обращаться с SAF, когда я могу обрабатывать только файл или путь к файлу?

Фон

До Android Q, если мы хотели получить информацию об APK-файле, мы могли использовать WRITE_EXTERNAL_STORAGE и READ_EXTERNAL_STORAGE, чтобы получить доступ к хранилищу, а затем используйте PackageManager.getPackageArchiveInfo в пути к файлу.

Существуют аналогичные случаи, например использование класса ZipFile в сжатом файле и, возможно, бесчисленное множество API фреймворков и сторонних библиотек.

Проблема

Недавно Google объявил об огромном количестве ограничений на Android Q.

Одно из них называется Scoped Storage. разрушает разрешение на хранение, когда дело доходит до доступа ко всем файлам, которые есть на устройстве. Он позволяет либо обрабатывать медиафайлы, либо использовать очень ограниченную платформу доступа к хранилищу (SAF), который не позволяет приложениям получать доступ к файлам и использовать их с помощью File API и путей к файлам.

Когда вышла Android Q Beta 2, из-за нее сломалось множество приложений, в том числе и Google. Причина заключалась в том, что он был включен по умолчанию и затрагивал все приложения, независимо от того, нацелены они на Android Q или нет.

Причина в том, что многие приложения, SDK и сама платформа Android довольно часто используют File API. Во многих случаях они также не поддерживают решения, связанные с InputStream или SAF. Примером этого является именно тот пример анализа APK, о котором я писал (PackageManager.getPackageArchiveInfo).

Однако в Q beta 3 все немного изменилось, так что приложение, предназначенное для Q, будет иметь ограниченное хранилище, и есть флаг, чтобы отключить его и по-прежнему использовать обычные разрешения хранилища и File API, как обычно. К сожалению, этот флаг является временным (прочитайте здесь), так что это отсрочивает неизбежное .

Что я пробовал

Я пробовал и нашел следующие вещи:

  1. Использование разрешения на хранение действительно не позволило мне прочитать ни один файл, кроме медиафайла (я хотел найти файлы APK). Как будто файлов нет.

  2. Используя SAF, я смог найти APK-файл и найти его реальный путь с помощью обходного пути (ссылка здесь), я заметил, что File API может сказать мне, что файл действительно существует, но он не может получить его размер, и фреймворк не может использовать его путь с помощью getPackageArchiveInfo . Написал об этом здесь

  3. Я попытался сделать символическую ссылку на файл (ссылка здесь), а затем читать по символической ссылке. Это не помогло.

  4. В случае парсинга APK-файлов я пытался искать альтернативные решения. Я нашел 2 репозитория github, которые обрабатывают APK с помощью класса File (здесь и здесь), и тот, который вместо этого использует InputStream ( здесь). К сожалению, тот, который использует InputStream, очень старый, в нем отсутствуют различные функции (например, получение имени и значка приложения), и он не будет обновляться в ближайшее время. Кроме того, наличие библиотеки требует обслуживания, чтобы не отставать от будущих версий Android, иначе в будущем могут возникнуть проблемы или даже сбой.

Вопросы

  1. Как правило, есть ли способ по-прежнему использовать File API при использовании SAF? Я не говорю о корневых решениях или простом копировании файла куда-то еще. Я говорю о более твердом решении.

  2. В случае синтаксического анализа APK есть ли способ решить эту проблему, когда фреймворк предоставляет только путь к файлу в качестве параметра? Возможно, какой-нибудь обходной путь или способ использования InputStream?


person android developer    schedule 18.05.2019    source источник
comment
Как правило, есть ли способ по-прежнему использовать File API при использовании SAF? -- нет, потому что нет требования, чтобы Uri указывал на обычный файл в файловой системе, не говоря уже о том, к которому у вас есть доступ. Например, DocumentsProvider может потребоваться расшифровать файл на лету, как показано в этот образец, на который вы указали 4,5 года назад. Все, что вы можете сделать, это скопировать содержимое в файл, которым вы управляете.   -  person CommonsWare    schedule 18.05.2019
comment
@CommonsWare Я не помню первую ссылку, которую вы предоставили, и не знаю, о чем она, но вторую ссылку я помню. Дело в том, что речь шла о SD-карте, а теперь это распространяется и на обычное хранилище. Но даже для SD-карты все еще можно было читать с нее с помощью File API (используя обходные пути, которые я упомянул), и я мог нормально анализировать файлы APK. Я не знаю, сработает ли это для зашифрованного хранилища, но если нет, по крайней мере, я мог бы попробовать. Нет ли какого-нибудь обходного пути, который вы можете придумать? Копировать файлы для этого плохо...   -  person android developer    schedule 18.05.2019
comment
Я не помню первую ссылку, которую вы предоставили, и не знаю, о чем она — вы ссылались на нее из своего 4,5-летнего вопроса. Это пример DocumentsProvider. Любой независимо написанный DocumentsProvider нарушит ваш метод getFullPathFromTreeUri(), так как вы слепо предполагаете, что Uri принадлежит конкретному DocumentsProvider (тот, который в идеале не будет поддерживать ваш хак и, возможно, перестанет поддерживать его в какой-то будущей версии Android).   -  person CommonsWare    schedule 18.05.2019
comment
Нет ли какого-нибудь обходного пути, который вы можете придумать? -- по определению не может быть надежного обходного пути. Не требуется, чтобы Uri указывал на файл в файловой системе, не говоря уже о файле, к которому вы можете получить прямой доступ любым способом. Пользователь может использовать SAF, чтобы выбрать местоположение на Google Диске, образец Vault, на который я ссылаюсь, сетевой ресурс SMB/CIFS или любой другой DocumentsProvider, к которому у него есть доступ. Uri может указывать на расположение на сервере, или в столбце BLOB базы данных, или на что-то еще, что захочет DocumentsProvider автор.   -  person CommonsWare    schedule 18.05.2019
comment
@CommonsWare У меня есть Google Диск, и когда я использую средство выбора для выбора папки, оно мне не предлагалось. Почему? А что, если я предположу, что пользователь выбрал реальный путь из реальной файловой системы? Будет ли это работать? Проблема здесь не в предположении (поскольку в моем тесте я выбираю путь из файловой системы), а в самом использовании. API, который у меня есть, нуждается в пути к файлу или File API. Действительно ли Google собирается сломать так много приложений? Что я могу сделать для сканирования файлов APK во всей файловой системе?   -  person android developer    schedule 18.05.2019
comment
Почему? -- Я не знаю. Я лично не пользуюсь Google Диском, но это выдающийся пример независимого DocumentsProvider. Будет ли это работать? -- Я не знаю, что это такое. Действительно ли Google собирается сломать так много приложений? -- Google сломал их пять лет назад. Вместо того, чтобы работать над альтернативными решениями ваших проблем (например, над библиотекой разбора APK, которая не зависит от файлов), вы и другие прибегли к хакам. Если бы вы использовали эти хаки как краткосрочную временную лазейку и приложили усилия к устранению необходимости в них, сегодня вы были бы в лучшей форме.   -  person CommonsWare    schedule 18.05.2019
comment
Что я могу сделать для сканирования файлов APK во всей файловой системе? -- Я не думаю, что есть вариант для этого. Для этого должен быть вариант. Вы громко жаловались на недостатки SAF, и я с некоторыми согласен. Во-первых, должен быть список всех APK-файлов (или PDF-файлов для более традиционного сценария), и я не думаю, что на данный момент для этого есть возможность.   -  person CommonsWare    schedule 18.05.2019
comment
Ну, Google сказал, что можно использовать разрешение на хранение, и что SD-карта все еще может быть прочитана с его помощью, поэтому все должно было остаться. Кроме того, некоторые функции не совсем хаки, а больше похожи на обходные пути, но ладно... О том, будет ли это все еще работать? Я имею в виду правильное получение содержимого файла из SAF с помощью других средств. До сих пор это работало нормально с использованием File API, но теперь кажется, что даже невозможно получить доступ к файлу с помощью File API. SAF API не хватает во многих отношениях. У него есть способ переместить файл только на API 24 (moveDocument). До этого вам приходилось копировать и удалять. И это только один пример.   -  person android developer    schedule 18.05.2019
comment
Приложения не могут просто приспособиться к этим изменениям. Многие библиотеки (особенно на C/C++) и даже фреймворк не готовы для SAF. Не всегда у нас есть функция с параметром InputStream. Иногда все, что у нас есть, это файл или путь к файлу. И современная ОС должна иметь Files API для доступа ко всем файлам как они есть, а File API — это очень известный стандарт, который не должен устареть без достаточной замены, имеющей в виду обратную совместимость. Не все файлы в файловой системе являются медиафайлами. Приложения должны иметь доступ ко всем файлам, если пользователь разрешает это.   -  person android developer    schedule 18.05.2019
comment
И, вероятно, так и останется, имея библиотеки, которые не могут справиться с тем, что может предложить SAF, что делает копирование файлов единственным обходным путем для их использования, что является огромным недостатком. Даже в документации говорится, что использование SAF неэффективно. : представление документа, поддерживаемого либо DocumentsProvider, либо необработанным файлом на диске. Это служебный класс, предназначенный для эмуляции традиционного интерфейса File. Он предлагает упрощенный вид дерева документов, но имеет значительные накладные расходы. Для оптимальной производительности и более богатого набора функций используйте методы и константы DocumentsContract напрямую.   -  person android developer    schedule 18.05.2019


Ответы (2)


Как обращаться с SAF, когда я могу обрабатывать только файл или путь к файлу? Это возможно, даже если вы можете отправить только объект Java File или строку пути к библиотечной функции, которую вы не можете изменить:

Во-первых, получите Uri для файла, который вам нужно обработать (в форме строки это будет выглядеть как «content://...»), затем:

    try {
        ParcelFileDescriptor parcelFileDescriptor =
                getContentResolver().openFileDescriptor(uri, "r"); // may get FileNotFoundException here
        // Obtain file descriptor:
        int fd = parcelFileDescriptor.getFd(); // or detachFd() if we want to close file in native code
        String linkFileName = "/proc/self/fd/" + fd;
        // Call library function with path/file string:
        someFunc(/*file name*/ linkFileName);
        // or with File parameter
        otherFunc(new File(linkFileName));
        // Finally, if you did not call detachFd() to obtain the file descriptor, call:
        parcelFileDescriptor.close();
        // Otherwise your library function should close file/stream...
    } catch (FileNotFoundException fnf) {
        fnf.printStackTrace(); // or whatever
    }          
person gregko    schedule 12.10.2019
comment
К сожалению, я тоже нашел это решение и написал его где-то еще, и хотя оно кажется рабочим, оно не всегда работает. Например, вы можете использовать getPackageArchiveInfo для файла, но когда вы попытаетесь получить его значок или метку, вы потерпите неудачу. Пожалуйста, поправьте меня, если я ошибаюсь в этом, и если у вас есть обходной путь или хотя бы объяснение, дайте мне знать и об этом. Я проголосовал за ваш ответ только потому, что вы пытаетесь помочь, и у вас все получилось. - person android developer; 12.10.2019
comment
Под меткой и значком вы имеете в виду метку и значок пакета приложения? Разве вы не можете получить их из PackageManager, например. getApplicationIcon (String packageName) и аналогичные для метки? Там не нужен файл, просто имя пакета... - person gregko; 13.10.2019
comment
Я только что попробовал эти вызовы - pm.getApplicationLabel() и pm.getApplicationIcon() на Android 10, доступ к устаревшему хранилищу не включен, работает нормально... - person gregko; 13.10.2019
comment
getApplicationIcon возвращает мне значок по умолчанию, поэтому я его не использовал, а использование ResourcesCompat.getDrawable вызывает исключение. Что касается имени приложения, я всегда получаю что-то странное: имя пакета с какой-то строкой, но никогда не имя приложения. Протестировано на APK F-Droid. Если вам каким-то образом удалось заставить это работать, поделитесь, пожалуйста, своим кодом для этого, возможно, поместите здесь полный образец: stackoverflow.com/q /56309165/878126 . Я даже назначу новую награду. Как я уже писал, я действительно нашел решение, которое вы написали (кто-то еще написал мне это где-то), но app-icon и app-label никогда не работают. - person android developer; 13.10.2019
comment
Извините, у меня нет решений, кроме того, что я написал до сих пор, управление пакетами не является моей областью интересов... - person gregko; 14.10.2019
comment
Но ты сказал, что это работает для тебя. Это работает или нет? - person android developer; 14.10.2019
comment
У меня отлично сработало, по крайней мере, в двух приложениях, которые я пробовал, это вызов packageManager.getApplicationIcon(packageName) и packageManager.getApplicationLabel(appInfo). - person gregko; 15.10.2019
comment
Если вы действительно хотите использовать packageManager.getPackageArchiveInfo (String archiveFilePath, флаги int); Я действительно не понимаю, как это сделать, если только вы не скопировали APK-файл куда-нибудь на видимое пользователем внутреннее хранилище или SD-карту и не запросили у пользователя разрешение на его чтение с помощью Scoped Storage ... Но даже в более ранних версиях Android могли бы вы действительно получить доступ к файлам APK в каталоге /data/app без рута? - person gregko; 15.10.2019
comment
Файлы APK могут существовать в хранилище, которое видит пользователь. Пользователь может копировать файлы APK через USB или скачивать их. Вот почему я использую getPackageArchiveInfo, так как только он может получить информацию о них. До Android Q я просто использовал разрешение на хранение, и все работало нормально. Что касается /data/app, конечно, содержимое APK-файлов этой папки общедоступно для всех приложений (без рута), но я спрашиваю не об этом. Я просто воспринимаю это как простой способ проверить, работает ли он. Не у всех есть APK-файл в папке загрузок, но у всех есть APK-файлы установленных приложений. - person android developer; 15.10.2019
comment
Итак, я вызвал packageManager.getPackageArchiveInfo(linkFileName, 0); на Android 10, без доступа к устаревшему хранилищу, в файле APK, скопированном в каталог загрузки. Я получил объект PackageInfo в порядке. Что именно вам нужно сделать с этим PackageInfo, который не работает? Я мог бы попробовать ваш точный код, чтобы посмотреть, что произойдет. - person gregko; 15.10.2019
comment
Уже сказано: иконка и название приложения. Для иконки я как-то получаю либо исключение, либо дефолтную. Для имени приложения (метки) я как-то получаю странную комбинацию имени пакета и чего-то еще. Если у вас есть решение, напишите его в другой теме. Здесь вы уже ответили, и хотя в какой-то момент я получил это решение, было бы несправедливо не принять ваш ответ здесь, потому что я сам на него не отвечал. Итак, вы получаете V сейчас. :) - person android developer; 15.10.2019
comment
Я вижу, что есть разница, если вы делаете getPackageArchiveInfo() установленного приложения с его именем пакета, и если вы получаете ту же информацию, вызывая getPackageArchiveInfo(), указав имя файла в файле APK. Затем вызов packageManager.getApplicationLabel(appInfo) для обоих типов объекта applicationInfo на Android 10 дает разные результаты. Вероятно, вызов API getApplicationLabel(appInfo) не может открыть файл APK без разрешения. Однако вы можете открыть APK как ZIP-файл и прочитать из него значения (например, ресурс значка, имя приложения из строковых таблиц и т. д.). - person gregko; 15.10.2019
comment
Конечно, есть разница. Есть полный доступ к установленному приложению, используя обычный File API. А снаружи везде используется ужасный SAF с обходными путями... :( - person android developer; 15.10.2019

Публикую еще один ответ, чтобы было больше места и позвольте мне вставить фрагменты кода. Учитывая дескриптор файла, как объяснялось в моем предыдущем ответе, я попытался использовать пакет net.dongliu:apk-parser, упомянутый @androiddeveloper в исходном вопросе, следующим образом (Lt.d — это мое сокращение для использования Log.d(SOME_TAG, string. ..))

            String linkFileName = "/proc/self/fd/" + fd;
            try (ApkFile apkFile = new ApkFile(new File(linkFileName))) {
                ApkMeta apkMeta = apkFile.getApkMeta();
                Lt.d("ApkFile Label: ", apkMeta.getLabel());
                Lt.d("ApkFile pkg name: ", apkMeta.getPackageName());
                Lt.d("ApkFile version code: ", apkMeta.getVersionCode());
                String iconStr = apkMeta.getIcon();
                Lt.d("ApkFile icon str: ", iconStr);
                for (UseFeature feature : apkMeta.getUsesFeatures()) {
                    Lt.d(feature.getName());
                }
            }
            catch (Exception ex) {
                Lt.e("Exception in ApkFile code: ", ex);
                ex.printStackTrace();
            }
        }

Он дает мне правильную метку приложения, для значка он дает мне только строку для каталога ресурсов (например, «res/drawable-mdpi-v4/fex.png»), поэтому снова необработанные функции чтения ZIP должны быть применены к прочитайте фактические биты значка. В частности, я тестировал APK ES File Explorer Pro (купил этот продукт и сохранил APK для собственной резервной копии, получил следующий результат:

 I/StorageTest: ApkFile Label: ES File Explorer Pro
 I/StorageTest: ApkFile pkg name: com.estrongs.android.pop.pro
 I/StorageTest: ApkFile version code: 1010
 I/StorageTest: ApkFile icon str: res/drawable-mdpi-v4/fex.png
 I/StorageTest: android.hardware.bluetooth
 I/StorageTest: android.hardware.touchscreen
 I/StorageTest: android.hardware.wifi
 I/StorageTest: android.software.leanback
 I/StorageTest: android.hardware.screen.portrait
person gregko    schedule 15.10.2019
comment
Что такое класс ApkFile? И почему бы не поместить ответ в ветку вопросов, на которую я указал: stackoverflow.com/q/56309165/878126? Удалось ли загрузить изображение? Помните, что это не всегда простой файл изображения (PNG/JPG). Это может быть VectorDrawable или AdaptiveIconDrawable, или даже AdaptiveIconDrawable из VectorDrawable. - person android developer; 15.10.2019
comment
Я узнал о пакете APK Parser (github.com/hsiafan/apk-parser) из вашего вопроса. , пункт 4 ссылки здесь и здесь... Парсер APK здесь не обязателен, он просто считывает информацию из файла APK, который у вас есть, например. в каталоге загрузки, например, из ZIP-архива. Что касается дополнительных тредов, то я просто теряюсь, если мне приходится раскидывать столько разных тредов. - person gregko; 16.10.2019
comment
Это не дополнительная нить. Это тот, который я специально спрашивал об анализе APK через SAF. Пожалуйста, пишите туда, а не сюда. Также, пожалуйста, напишите, как вы правильно получили рисунок, даже если это не обычный файл изображения (PNG/JPG/WEBP). - person android developer; 16.10.2019
comment
У меня нет времени копировать/вставлять ответы в другую тему. У вас уже есть все ответы. В случае с ES File Explorer APK, который я исследовал, мне сказали, что значок находится в res/drawable-mdpi-v4/fex.png. Это относительный путь внутри файла APK (ZIP), поэтому используйте любые функции/библиотеки для распаковки, чтобы извлечь этот файл PNG и создать из него рисунок... - person gregko; 16.10.2019
comment
Опять же, png-файл — это не всегда то, что лучше всего получить, не говоря уже о его плотности mdpi. Я не знаю, почему он выбрал его для вас, но я ясно вижу, что он имеет гораздо лучшую плотность внутри своего APK-файла. И есть приложения, которые не должны возвращать вам файл PNG. Попробуйте F-Droid, например. В этом случае вместо этого у него есть адаптивный рисунок. Как бы вы его загружали в этом случае? Вместо этого вы даже можете использовать VectorDrawable или даже их комбинацию. - person android developer; 16.10.2019
comment
Я уверен, что в Android SDK есть способы загрузить рисуемый (векторный или другой формат) из файла, zip-файла или чего-либо еще и нарисовать из него значок. Или, если вам нужен значок большего размера, функция доступа к ZIP позволяет вам перечислять каталоги и файлы внутри ZIP, поэтому ищите fex.png в других доступных для рисования каталогах. У меня действительно нет времени разрабатывать для вас этот код для рисования, мне он сейчас не нужен. Я отвечал здесь, чтобы лучше понять ограничения доступа к файлам и возможные обходные пути в Android 10 и выше. - person gregko; 16.10.2019
comment
Что ж, вопрос в другой ветке касается этого: ярлык и значок приложения, включая все особые случаи, чтобы он работал точно так же, как обычный доступ к файлам APK. - person android developer; 16.10.2019