52

背景

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

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

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

问题

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

  1. 名称(在当前配置上),又名“标签”,使用packageInfo.applicationInfo.loadLabel(packageManager). 这基于设备的当前配置(区域设置等...)
  2. 包名,使用packageInfo.packageName
  3. 版本代码,使用packageInfo.versionCodeor packageInfo.longVersionCode
  4. 版本号,使用packageInfo.versionName
  5. 应用程序图标,使用各种方式,基于当前配置(密度等...):

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

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

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

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

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

我试过的

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

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

  1. 原始代码使用文件路径(类为ApkFile),比使用 Android 框架正常解析要多出约 10 倍。原因可能是它解析了所有可能的或接近它的东西。另一种ByteArrayApkFile解析方式(class is )是使用包含整个 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'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。那是除非也许有办法优化库的代码......

问题

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

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

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


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

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


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

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

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

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

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


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

  1. 限定词有点错误,你需要找到最适合你的情况。
  2. 对于自适应图标,它可以获得 PNG而不是 VectorDrawable。
  3. 对于 VectorDrawable,它只获取byte-array。不知道如何将其转换为真正的 VectorDrawable。
4

2 回答 2

6

好的,我想我找到了一种使用 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) {
                ""
            }
    }
}
于 2019-08-09T06:54:28.883 回答
-4

使用下面的代码

/**
* 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;
    }
}
于 2019-06-21T07:24:33.463 回答