如何在不使用 File 或 file-path 的情况下获取文件系统(不仅仅是已安装的)中 APK 文件的信息? [英] How to get information of an APK file in the file system (not just installed ones) without using File or file-path?

查看:19
本文介绍了如何在不使用 File 或 file-path 的情况下获取文件系统(不仅仅是已安装的)中 APK 文件的信息?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我的应用(此处) 可以在整个文件系统(不仅仅是已安装的应用程序)中搜索 APK 文件,显示每个文件的信息,允许删除、共享、安装...

My app (here) can search for APK files throughout the file system (not just of installed apps), showing information about each, allowing to delete, share, install...

作为 Android Q 范围内存储功能的一部分,谷歌宣布 SAF(存储访问框架)将取代正常的存储权限.这意味着即使您尝试使用存储权限,它也只会授予访问特定类型的文件的权限,以便使用或完全沙盒化的 File 和 file-path(写于 此处).

As part of the scoped-storage feature on Android Q, Google announced that SAF (storage access framework) will replace the normal storage permissions. This means that even if you will try to use storage permissions, it will only grant to access to specific types of files for File and file-path to be used or completely be sandboxed (written about here).

这意味着很多框架将需要依赖 SAF 而不是 File 和 file-path.

This means that a lot of frameworks will need to rely on SAF instead of File and file-path.

其中之一是 packageManager.getPackageArchiveInfo ,给出文件路径,返回 PackageInfo ,我可以获得有关的各种信息:

One of them is packageManager.getPackageArchiveInfo , which given a file path, returns PackageInfo , which I can get various information about:

  1. name(在当前配置上),又名标签",使用 packageInfo.applicationInfo.loadLabel(packageManager) .这基于设备的当前配置(语言环境等...)
  2. 包名,使用packageInfo.packageName
  3. version code ,使用 packageInfo.versionCodepackageInfo.longVersionCode .
  4. 版本号,使用packageInfo.versionName
  5. 应用图标,使用各种方式,基于当前配置(密度等...):
  1. name (on the current configuration) , AKA "label", using packageInfo.applicationInfo.loadLabel(packageManager) . This is based on the current configuration of the device (locale, etc...)
  2. package name , using packageInfo.packageName
  3. version code , using packageInfo.versionCode or packageInfo.longVersionCode .
  4. version number , using packageInfo.versionName
  5. app icon, using various ways, based on the current configuration (density etc... ) :

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

B.如果已安装,AppCompatResources.getDrawable(createPackageContext(packageInfo.packageName, 0), packageInfo.applicationInfo.icon )

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

它返回给你的还有很多,还有很多是可选的,但我认为这些是关于 APK 文件的基本细节.

There are a lot more that it returns you and a lot that are optional, but I think those are the basic details about APK files.

我希望谷歌为此提供一个很好的替代方案(请求此处此处 ),因为目前我找不到任何好的解决方案.

I hope Google will provide a good alternative for this (requested here and here ), because currently I can't find any good solution for it.

使用从 SAF 获得的 Uri 并从中获得 InputStream 非常容易:

It's quite easy to use the Uri that I get from SAF and have an InputStream from it :

@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

但是现在你被卡住了,因为我见过的所有框架都需要一个文件或文件路径.

But now you are stuck because all the framework I've seen needs a File or file-path.

我尝试使用 Android 框架找到任何替代方案,但找不到任何替代方案.不仅如此,我发现的所有图书馆也没有提供这样的东西.

I've tried to find any kind of alternative using the Android framework but I couldn't find any. Not only that, but all libraries I've found don't offer such a thing either.

发现我看过的库之一(here) - 可以选择仅使用流来解析 APK 文件(包括其资源),但是:

found out that one of the libraries I've looked at (here) - kinda has the option to parse APK file (including its resources) using just a stream, but :

  1. 原始代码使用文件路径(类为ApkFile),比使用Android框架正常解析需要大约x10倍.原因可能是它解析了所有可能的东西,或者接近它.另一种解析方法(类是 ByteArrayApkFile )是使用包含整个 APK 内容的字节数组.如果您只需要其中的一小部分,则读取整个文件非常浪费.此外,这种方式可能会占用大量内存,而且正如我所测试的,它确实可以达到 OOM,因为我必须将整个 APK 内容放入一个字节数组中.

  1. The original code uses a file path (class is ApkFile), and it takes about x10 times more than normal parsing using the Android framework. The reason is probably that it parses everything possible, or close to it. Another way (class is ByteArrayApkFile ) to parse is by using a byte-array that includes the entire APK content. Very wasteful to read the entire file if you need just a small part of it. Plus it might take a lot of memory this way, and as I've tested, indeed it can reach OOM because I have to put the entire APK content into a byte array.

我发现它有时无法解析框架可以很好解析的 APK 文件(此处).也许很快就会修复.

I've found out it sometimes fails to parse APK files that the framework can parse fine (here). Maybe it will soon be fixed.

我试图只提取 APK 文件的基本解析,它奏效了,但在速度方面更糟(此处).从其中一个类(称为 AbstractApkFile)中获取代码.所以在文件之外,我只得到了不应该占用太多内存的清单文件,我使用库单独解析它.这里:

I tried to extract just the basic parsing of the APK file, and it worked, but it's even worse in terms of speed (here). Took the code from one of the classes (called AbstractApkFile). So out of the file, I get just the manifest file which shouldn't take much memory, and I parse it alone using the library. Here:

AsyncTask.execute {
    val packageInfo = packageManager.getPackageInfo(packageName, 0)
    val apkFilePath = packageInfo.applicationInfo.publicSourceDir
    // I'm using the path only because it's 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.除非有一种方法可以优化库的代码...

So, for now, this is not a good solution, because of how slow it is, and because getting the app name and icon requires me to give the entire APK data, which could lead to OOM. That's unless maybe there is a way to optimize the library's code...

  1. 如何从 APK 文件的 InputStream 中获取 APK 信息(至少是我在列表中提到的内容)?

  1. How can I get an APK information (at least the things I've mentioned in the list) out of an InputStream of an APK file?

如果在正常框架上没有替代方案,我在哪里可以找到这样的东西?是否有任何流行的库为 Android 提供它?

If there is no alternative on the normal framework, where can I find such a thing that will allow it? Is there any popular library that offers it for Android?

注意:当然我可以将 InputStream 复制到一个文件然后使用它,但是这样效率很低,因为我必须为找到的每个文件都这样做,而且这样做浪费了空间和时间,因为文件已经存在.

Note: Of course I could copy the InputStream to a file and then use it, but this is very inefficient as I will have to do it for every file that I find, and I waste space and time in doing so because the files already exist.

找到解决方法后(此处)获取有关APK 通过 getPackageArchiveInfo (on "/proc/self/fd/" + fileDescriptor.fd) ,我仍然找不到任何方法来获取 app-label应用图标.如果有人知道如何单独使用 SAW(没有存储许可),请告诉我.

after finding the workaround (here) to get very basic information about the APK via getPackageArchiveInfo (on "/proc/self/fd/" + fileDescriptor.fd) , I still can't find any way to get app-label and app-icon. Please, if anyone knows how to get those with SAW alone (no storage permission), let me know.

我为此设置了新的悬赏,希望有人也能找到一些解决方法.

I've set a new bounty about this, hoping someone will find some workaround for this as well.

由于我发现了一个新发现,我准备了一个新的悬赏:一个名为Solid Explorer" 以 API 29 为目标,但使用 SAF 仍然可以显示 APK 信息,包括应用名称和图标.

I'm putting a new bounty because of a new discovery I've found: An app called "Solid Explorer" targets API 29, and yet using SAF it can still show APK information, including app name and icon.

即使在最初针对 API 29 时,它也没有显示有关 APK 文件的任何信息,包括图标和应用名称.

That's even though in the beginning when it first targeted API 29, it didn't show any information about APK files, including the icon and the app name.

试用名为<强>插件检测器",我找不到此应用用于此目的的任何特殊库,这意味着可以使用普通框架来实现它,而无需非常特殊的技巧.

Trying out an app called "Addons detector", I couldn't find any special library that this app uses for this purpose, which means it might be possible to do it using the normal framework, without very special tricks.

关于Solid Explorer",似乎他们只是使用了requestLegacyExternalStorage"的特殊标志,所以他们不单独使用SAF,而是使用普通框架.

about "Solid Explorer", seems that they just use the special flag of "requestLegacyExternalStorage", so they don't use SAF alone, but the normal framework instead.

所以,如果有人知道如何单独使用 SAF 获取应用程序名称和应用程序图标(并且可以在工作示例中显示),请告诉我.

So please, if anyone knows how to get app-name and app-icon using SAF alone (and can show it in a working sample), please let me know.

似乎APK-parser library 可以很好地获取应用程序名称,并且图标,但对于图标,它有一些问题:

seems that the APK-parser library can get the app name fine and the icons, but for icons it has a few issues:

  1. 限定词有点错误,你需要找到最适合您的情况.
  2. 对于自适应图标,它可以获得 PNG 而不是 VectorDrawable.
  3. 对于 VectorDrawable,它获得只是字节数组.不知道如何将其转换为真正的 VectorDrawable.
  1. the qualifiers are a bit wrong, and you need to find which is the best for your case.
  2. For adaptive icon it can get a PNG instead of VectorDrawable.
  3. For VectorDrawable, it gets just the byte-array. No idea how to convert it to a real VectorDrawable.

推荐答案

OK 我想我找到了一种使用 Android 框架的方法(reddit 上有人给了我这个解决方案),使用 file-path 并使用它,但它不是完美.一些注意事项:

OK I think I found a way using the Android framework (someone on reddit gave me this solution), to use file-path and use it, but it's not perfect at all. Some notes:

  1. 不像以前那么直接了.
  2. 好消息是,甚至可以处理设备存储之外的文件.
  3. 这看起来像是一种解决方法,但我不确定它能持续多久.
  4. 出于某种原因,我无法加载应用标签(它总是只返回包名称),应用图标也是如此(始终为空或默认图标).

简而言之,解决方案是使用这个:

The solution, in short, is using this:

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

我认为同样的方法可以用于所有需要文件路径的情况.

I think this same approach can be used for all cases that you need a file-path.

这是一个示例应用(也可以在此处) :

Here's a sample app (also available here) :

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

这篇关于如何在不使用 File 或 file-path 的情况下获取文件系统(不仅仅是已安装的)中 APK 文件的信息?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆