如何在不使用文件或文件路径的情况下获取文件系统(不仅仅是已安装的)中 APK 文件的信息?

Posted

技术标签:

【中文标题】如何在不使用文件或文件路径的情况下获取文件系统(不仅仅是已安装的)中 APK 文件的信息?【英文标题】:How to get information of an APK file in the file system (not just installed ones) without using File or file-path? 【发布时间】:2019-10-12 00:45:49 【问题描述】:

背景

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

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

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

问题

其中一个是 packageManager.getPackageArchiveInfo ,它给定一个文件路径,返回 PackageInfo ,我可以得到各种信息:

    name(在当前配置上),又名“标签”,使用 packageInfo.applicationInfo.loadLabel(packageManager)。这基于设备的当前配置(区域设置等...) 包名,使用packageInfo.packageName 版本代码,使用packageInfo.versionCodepackageInfo.longVersionCode。 版本号,使用packageInfo.versionName 应用图标,使用多种方式,基于当前配置(密度等...):

一个。 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 文件的基本细节。

我希望 Google 会为此提供一个好的替代方案(请求 here 和 here ),因为目前我找不到任何好的解决方案。

我尝试过的

使用我从 SAF 获得的 Uri 并从中获得 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 框架找到任何替代方案,但我找不到任何替代方案。不仅如此,我发现的所有库也不提供这样的东西。

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

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

    我发现它有时无法解析框架可以正常解析的 APK 文件 (here)。也许很快就会修复。

    我尝试仅提取 APK 文件的基本解析,它成功了,但它的速度更差 (here)。从其中一个类(称为AbstractApkFile)中获取代码。所以从文件中,我得到了不应该占用太多内存的清单文件,我使用库单独解析它。这里:

    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。除非有办法优化库的代码...

问题

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

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

注意:当然,我可以将 InputStream 复制到一个文件中然后使用它,但这非常低效,因为我必须为找到的每个文件都这样做,这样做会浪费空间和时间,因为文件已经存在。


编辑:在找到解决方法 (here) 以通过 getPackageArchiveInfo ("/proc/self/fd/" + fileDescriptor.fd) 获取有关 APK 的非常基本信息后,我仍然找不到任何方法来获取 app-label 和 应用程序图标。请,如果有人知道如何单独使用 SAW(没有存储权限),请告诉我。

我已经为此设置了一个新的赏金,希望有人也能找到一些解决方法。


由于我发现了一个新发现,我将提供新的赏金:一个名为“Solid Explorer”的应用针对 API 29,但使用 SAF 它仍然可以显示 APK 信息,包括应用名称和图标。

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

尝试了一个名为“Addons detector”的应用程序,我找不到该应用程序为此目的使用的任何特殊库,这意味着可以使用普通框架来完成它,而无需非常特殊的技巧。

编辑:关于“Solid Explorer”,似乎他们只是使用“requestLegacyExternalStorage”的特殊标志,所以他们不单独使用SAF,而是使用普通框架。

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


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

    限定符是a bit wrong,您需要to find,这是最适合您的情况。 对于自适应图标,使用 can get a PNG 而不是 VectorDrawable。 对于 VectorDrawable,它得到 just the byte-array。不知道如何将其转换为真正的 VectorDrawable。

【问题讨论】:

"如何从 APK 文件的 InputStream 中获取 APK 信息?" -- 将字节复制到您控制的文件中,然后在该文件上使用getPackageArchiveInfo()。是的,我知道,这很慢且占用空间,但它绝对是您最简单、最可靠的选择。否则,您需要翻阅 AOSP 代码,找到实现 getPackageArchiveInfo() 的东西,然后尝试创建自己的克隆。如果有人已经这样做了,我会感到相当惊讶。 正如我所写,问题是不使用文件或文件路径。您的解决方案就是这样使用的。至于做你写的人,实际上有一个,但它不适用于Android,我认为它没有InputStream作为参数,不确定它是否支持获取图标。我认为它在这里:github.com/lxbzmy/android-apk-parser 你检查过这个吗? ***.com/questions/20067508/… 我认为您可以从 URI 获取路径,并继续您的旧逻辑。 @androiddeveloper @NaitikSoni 我已经尝试过这个技巧。正如我所说,我不能再使用存储权限,因为它计划在未来消失,取而代之的是只允许访问非常特定类型的文件(媒体文件)或完全沙盒的权限。因此,正如我所写,正因为如此,我需要一个单独使用 SAF 的解决方案。没有其他权限,尤其是计划取消的存储权限。 @ManojPerumarath 遗憾的是,我已经对其进行了测试:即使您找到了路径,并且可以通过 SAF 访问它,您也无法通过 File API 或 file- 访问它小路。我在这里报道过这个:issuetracker.google.com/issues/132481545。还要求能够在这里这样做:issuetracker.google.com/issues/134473341。可悲的是,谷歌似乎坚持到了最后,破坏了这么多的应用程序和库,而且它甚至都表现不佳:issuetracker.google.com/issues/130261278。 :( 【参考方案1】:

好的,我想我找到了一种使用 Android 框架的方法(reddit 上有人给了我这个解决方案),使用文件路径并使用它,但它一点也不完美。一些注意事项:

    不像以前那样直接。 好消息是它甚至可以处理设备存储之外的文件。 这似乎是一种解决方法,但我不确定它可以工作多长时间。 由于某种原因,我无法加载应用标签(它始终只返回包名称),应用图标也是如此(始终为空或默认图标)。

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

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

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

这是一个示例应用程序(也可以使用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) 
                ""
            
    

【讨论】:

在 Android 29+ 上通过 /proc/self/fd/… 符号链接读取文件可能会失败。 OTOH,requestLegacyExternalStorage 标志是使用基于文件路径的 API 的合法方式。 requestLegacyExternalStorage 在以 API 30 为目标时是不可能的。遗憾的是,我使用的当前解决方案似乎已存档:github.com/hsiafan/apk-parser。我已经做了一个工作分支,但我不是这个主题的专业人士(解析 APK 文件):github.com/AndroidDeveloperLB/apk-parser。该解决方案允许您甚至使用 InputStream 解析 APK 文件 查看developer.android.com/about/versions/11/privacy/… 为了帮助您的应用更顺畅地与第三方媒体库一起工作,Android 11 允许您使用除 MediaStore API 之外的 API 来使用直接访问共享存储中的媒体文件文件路径。这些 API 包括以下内容: – 文件 API。 – 原生库,例如 fopen()。 @AlexCohn 这不是媒体文件。它是 APK 文件,在某些情况下甚至可能在 ZIP 文件中。遗憾的是,当前的 Android API 甚至无法解析拆分的 APK 文件。这就是使用第三方库的更多理由。可悲的是,再一次,这是我发现的唯一一个很好的图书馆,它被存档了。 你是说在 Android 11 上这种对文件 API 访问的反转仅限于 mp4、mp3 等文件?他们是按文件名还是按内容检查?【参考方案2】:

使用下面的代码

/**
* 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;
    

【讨论】:

再一次,就像我之前写给另一个答案(这里:***.com/a/56333577/878126),在 cmets ,在标题和问题本身中:我不是要求获取已安装的信息应用 。我正在询问有关获取文件系统上任何 APK 文件的信息。您没有“packageName”作为输入。您甚至没有文件路径作为输入。您可以使用 SAF API,它为您提供 DocumentFile、Uri 或 InputStream。 在这里,我进一步更新了问题,以确保人们理解。

以上是关于如何在不使用文件或文件路径的情况下获取文件系统(不仅仅是已安装的)中 APK 文件的信息?的主要内容,如果未能解决你的问题,请参考以下文章

在不使用 windows 文件缓存的情况下复制文件

在不加载 PHAsset us 照片库的情况下获取所有图像本地标识符/文件路径

在不安装 apk 的情况下获取 Android .apk 文件 VersionName 或 VersionCode

Java中有没有办法在不尝试创建文件的情况下确定路径是不是有效?

如何在不引用文件的情况下从图像中获取字节数据

如何在不打开CAD文件的情况下,获取该文件中起作用的标注样式中的全局比例和图形范围。