Как получить информацию об APK файле в файловой системе (а не только в установленных) без использования File или file-path?

Фон

Мое приложение (здесь) может выполнять поиск файлов APK во всей файловой системе (а не только в установленных приложениях), отображая информацию о каждом из них, позволяя удалять, совместно использовать, устанавливать...

В рамках функции хранения с ограниченным объемом в Android Q компания Google объявила, что SAF (структура доступа к хранилищу) заменит обычные разрешения для хранилища. Это означает, что даже если вы попытаетесь использовать разрешения для хранилища, он предоставит доступ только к определенным типам файлов для файла и пути к файлу, которые будут использоваться или полностью помещаться в "песочницу" (о здесь).

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

Проблема

Одним из них является packageManager.getPackageArchiveInfo, который с учетом пути к файлу возвращает PackageInfo, о котором я могу получить различную информацию о:

  1. name (в текущей конфигурации), AKA "label", используя packageInfo.applicationInfo.loadLabel(packageManager). Это зависит от текущей конфигурации устройства (локаль и т.д.).
  2. имя пакета, используя packageInfo.packageName
  3. код версии, используя packageInfo.versionCode или packageInfo.longVersionCode.
  4. номер версии, используя packageInfo.versionName
  5. Значок приложения, используя различные способы, в зависимости от текущей конфигурации (плотность и т.д.):

а. BitmapFactory.decodeResource(packageManager.getResourcesForApplication(applicationInfo),packageInfo.applicationInfo.icon, bitmapOptions)

б. если установлено, AppCompatResources.getDrawable(createPackageContext(packageInfo.packageName, 0), packageInfo.applicationInfo.icon )

с. ResourcesCompat.getDrawable(packageManager.getResourcesForApplication(applicationInfo), packageInfo.applicationInfo.icon, null)

Есть еще много того, что он возвращает вам, и многое, что не является обязательным, но я думаю, что это основные детали об APK файлах.

Я надеюсь, что Google предоставит хорошую альтернативу для этого (запрошено здесь и здесь), потому что в настоящее время я не могу найти для этого хорошего решения.

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

Довольно просто использовать Uri, полученный от SAF, и получить из него InputStream:

@TargetApi(Build.VERSION_CODES.O)
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    setSupportActionBar(toolbar)
    packageInstaller = packageManager.packageInstaller

    val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
    intent.addCategory(Intent.CATEGORY_OPENABLE)
    intent.type = "application/vnd.android.package-archive"
    startActivityForResult(intent, 1)
}

override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
    super.onActivityResult(requestCode, resultCode, resultData)
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && requestCode == 1 && resultCode == Activity.RESULT_OK && resultData != null) {
        val uri = resultData.data
        val isDocumentUri = DocumentFile.isDocumentUri(this, uri)
        if (!isDocumentUri)
           return
        val documentFile = DocumentFile.fromSingleUri(this, uri)
        val inputStream = contentResolver.openInputStream(uri)
        //TODO do something with what you got above, to parse it as APK file

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

Я пытался найти какую-либо альтернативу, используя платформу Android, но не смог ее найти. Не только это, но и все библиотеки, которые я нашел, тоже не предлагают такой возможности.

ОБНОВЛЕНИЕ: обнаружил, что одна из библиотек, которые я просматривал (здесь) - вроде как имеет возможность анализировать файл APK (включая его ресурсы), используя только поток, но:

  1. В исходном коде используется путь к файлу (класс ApkFile), и он требует примерно в 10 раз больше, чем при обычном анализе с использованием платформы Android. Причина, вероятно, в том, что он разбирает все возможное или близкое к нему. Другой способ (класс ByteArrayApkFile) для анализа - использование байтового массива, который включает в себя весь контент APK. Очень расточительно читать весь файл, если вам нужна только небольшая его часть. Кроме того, для этого может потребоваться много памяти, и, как я уже проверял, на самом деле он может достичь OOM, потому что мне нужно поместить весь контент APK в байтовый массив.

  2. Я обнаружил, что иногда не удается проанализировать APK файлы, которые фреймворк может нормально анализировать (здесь). Возможно, это скоро будет исправлено.

  3. Я попытался извлечь только базовый анализ файла APK, и это сработало, но это еще хуже с точки зрения скорости (здесь). Взял код у одного из классов (он называется AbstractApkFile). Итак, из файла я получаю только файл манифеста, который не должен занимать много памяти, и анализирую его один, используя библиотеку. Здесь:

    AsyncTask.execute {
        val packageInfo = packageManager.getPackageInfo(packageName, 0)
        val apkFilePath = packageInfo.applicationInfo.publicSourceDir
        // I'm using the path only because it easier this way, but in reality I will have a Uri or inputStream as the input, which is why I use FileInputStream to mimic it.
        val zipInputStream = ZipInputStream(FileInputStream(apkFilePath))
        while (true) {
            val zipEntry = zipInputStream.nextEntry ?: break
            if (zipEntry.name.contains("AndroidManifest.xml")) {
                Log.d("AppLog", "zipEntry:$zipEntry ${zipEntry.size}")
                val bytes = zipInputStream.readBytes()
                val xmlTranslator = XmlTranslator()
                val resourceTable = ResourceTable()
                val locale = Locale.getDefault()
                val apkTranslator = ApkMetaTranslator(resourceTable, locale)
                val xmlStreamer = CompositeXmlStreamer(xmlTranslator, apkTranslator)
                val buffer = ByteBuffer.wrap(bytes)
                val binaryXmlParser = BinaryXmlParser(buffer, resourceTable)
                binaryXmlParser.locale = locale
                binaryXmlParser.xmlStreamer = xmlStreamer
                binaryXmlParser.parse()
                val apkMeta = apkTranslator.getApkMeta();
                Log.d("AppLog", "apkMeta:$apkMeta")
                break
            }
        }
    }
    

Итак, на данный момент это не очень хорошее решение, поскольку оно медленное и потому, что для получения имени приложения и значка требуется предоставить все данные APK, что может привести к OOM. Это, если возможно, есть способ оптимизировать код библиотеки...

Вопросы

  1. Как я могу получить информацию об APK (по крайней мере, о чем я упоминал в списке) из InputStream файла APK?

  2. Если нет альтернативы нормальному фреймворку, где я могу найти такую вещь, которая позволит это? Есть ли какая-нибудь популярная библиотека, которая предлагает его для Android?

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


ОБНОВЛЕНИЕ: после нахождения обходного пути (здесь), чтобы получить основную информацию об APK через getPackageArchiveInfo (на "/proc/self/fd/" + fileDescriptor.fd), я все еще не могу найти способ получить ярлык приложения и значок приложения. Пожалуйста, если кто-нибудь знает, как получить те с SAW (без разрешения на хранение), дайте мне знать.

Я назначил новую награду за это, надеясь, что кто-то найдет и обходной путь для этого.


Я назначаю новую награду из-за нового открытия, которое я обнаружил: приложение под названием "Solid Explorer" предназначено для API 29, и все же, используя SAF, оно все равно может отображать информацию об APK, включая имя и значок приложения..

Несмотря на то, что в начале, когда он впервые нацелен на API 29, он не отображал никакой информации о файлах APK, включая значок и имя приложения.

Опробовав приложение под названием "Детектор аддонов", я не смог найти какой-либо специальной библиотеки, которую это приложение использует для этой цели, а это значит, что можно было бы сделать это, используя обычную среду, без очень специальных уловок..

ОБНОВЛЕНИЕ: о "Solid Explorer", кажется, что они просто используют специальный флаг "requestLegacyExternalStorage", поэтому они используют не только SAF, но вместо этого нормальный каркас.

Поэтому, пожалуйста, если кто-нибудь знает, как получить имя приложения и значок приложения, используя только SAF (и может показать его в рабочем примере), пожалуйста, дайте мне знать.

Ответы

Ответ 1

Хорошо, я думаю, что нашел способ с помощью фреймворка Android (кто-то на Reddit дал мне это решение), чтобы использовать путь к файлу и использовать его, но это не совсем идеально. Некоторые заметки:

  1. Не так прямо, как раньше.
  2. Хорошо, что также возможно обрабатывать даже файлы, находящиеся вне хранилища устройства.
  3. Это выглядит как обходной путь, и я не уверен, как долго это будет работать.
  4. По какой-то причине я не могу загрузить ярлык приложения (вместо него всегда возвращается только имя пакета), и то же самое относится и к значку приложения (всегда пустому).

Короче говоря, решение использует это:

val fileDescriptor = contentResolver.openFileDescriptor(uri, "r") ?: return
val packageArchiveInfo = packageManager.getPackageArchiveInfo("/proc/self/fd/" + fileDescriptor.fd, 0)

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

Вот пример приложения (также доступно здесь):

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        startActivityForResult(
                Intent(Intent.ACTION_OPEN_DOCUMENT).addCategory(Intent.CATEGORY_OPENABLE)
                        .setType("application/vnd.android.package-archive"), 1
        )
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        try {
            val uri = data?.data ?: return
            val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
            contentResolver.takePersistableUriPermission(uri, takeFlags)
            val isDocumentUri = DocumentFile.isDocumentUri(this, uri)
            if (!isDocumentUri)
                return
            val documentFile = DocumentFile.fromSingleUri(this, uri) ?: return
            val fileDescriptor = contentResolver.openFileDescriptor(uri, "r") ?: return
            val packageArchiveInfo = packageManager.getPackageArchiveInfo("/proc/self/fd/" + fileDescriptor.fd, 0)
            Log.d("AppLog", "got APK info?${packageArchiveInfo != null}")
            if (packageArchiveInfo != null) {
                val appLabel = loadAppLabel(packageArchiveInfo.applicationInfo, packageManager)
                Log.d("AppLog", "appLabel:$appLabel")
            }
        } catch (e: Exception) {
            e.printStackTrace()
            Log.e("AppLog", "failed to get app info: $e")
        }
    }

    fun loadAppLabel(applicationInfo: ApplicationInfo, packageManager: PackageManager): String =
            try {
                applicationInfo.loadLabel(packageManager).toString()
            } catch (e: java.lang.Exception) {
                ""
            }
    }
}

Ответ 2

Как вы упомянули, getPackageArchiveInfo(String archiveFilePath, int flags) извлекает PackageInfo с использованием archiveFilePath но вам нужен другой подход.

Решение (Этот подход был отклонен)

Вы можете получить все packagesNames getPackageInfo(String packageName, int flags) установленные на устройстве, используя следующее намерение, а затем с помощью метода getPackageInfo(String packageName, int flags) легко получить доступ к его информации (это как-то ошибка безопасности и конфиденциальности!)

Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);
mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);
List<ResolveInfo> pkgAppsList=context.getPackageManager().queryIntentActivities(mainIntent,0);
for(int i = 0 ; i< pkgAppsList.size() ;i++)
{
  PackageInfo info = context.getPackageManager().getPackageInfo(pkgAppsList.get(i).packageName,1);
  // TODO
  // do what you need for each package .
}

Ответ 3

Используйте код ниже

/**
* Get the apk path of this application.
*
* @param context any context (e.g. an Activity or a Service)
* @return full apk file path, or null if an exception happened (it should not happen)
*/
public static String getApkName(Context context) {
    String packageName = context.getPackageName();
    PackageManager pm = context.getPackageManager();
    try {
        ApplicationInfo ai = pm.getApplicationInfo(packageName, 0);
        String apk = ai.publicSourceDir;
        return apk;
    } catch (Throwable x) {
        return null;
    }
}