如何在不使用文件或文件路径的情况下获取文件系统(不仅仅是已安装的)中 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.versionCode
或packageInfo.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 文件的信息?的主要内容,如果未能解决你的问题,请参考以下文章
在不加载 PHAsset us 照片库的情况下获取所有图像本地标识符/文件路径
在不安装 apk 的情况下获取 Android .apk 文件 VersionName 或 VersionCode