这一段时间,又被调回来做清理应用了。之前,也做过一段清理。这次又回来做,争取要做得好些。言归正传,清理应用中在获取无效的 apk 文件后,需要展示 apk 的信息。其他信息都容易获取,只有 apk 的应用名称的获取,费了一些工夫。
这里通过查询 MediaStore.Files.getContentUri("external")
的方法来获取,
Uri uri = MediaStore.Files.getContentUri("external");
String selection = "(" + MediaStore.Files.FileColumns.DATA + " LIKE '%.apk'" + ") and " + MediaStore.Files.FileColumns.SIZE + " >1 ";
Cursor cursor = getContentResolver().query(uri, null, selection, null, null);
if (cursor != null) {
try {
cursor.moveToFirst();
while (!cursor.isAfterLast()) {
int id = cursor.getInt(cursor.getColumnIndex(MediaStore.Files.FileColumns._ID));
String data = cursor.getString(cursor.getColumnIndex(MediaStore.Files.FileColumns.DATA));
long size = cursor.getLong(cursor.getColumnIndex(MediaStore.Files.FileColumns.SIZE));
String displayName = cursor.getString(cursor.getColumnIndex(MediaStore.Files.FileColumns.DISPLAY_NAME));
String title = cursor.getString(cursor.getColumnIndex(MediaStore.Files.FileColumns.TITLE));
long dateAdded = cursor.getLong(cursor.getColumnIndex(MediaStore.Files.FileColumns.DATE_ADDED));
long dateModified = cursor.getLong(cursor.getColumnIndex(MediaStore.Files.FileColumns.DATE_MODIFIED));
String mimeType = cursor.getString(cursor.getColumnIndex(MediaStore.Files.FileColumns.MEDIA_TYPE));
Log.d("ApkActivity", "id=" + id + ", data=" + data + ", size=" + size + ", displayName="
+ displayName + ", title=" + title + ", dateAdded=" + dateAdded + ", dateModified=" + dateModified
+ ", mimeType=" + mimeType);
cursor.moveToNext();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
cursor.close();
}
}
运行程序,查看日志:
D/ApkActivity: id=25949, data=/storage/emulated/0/skyfake.apk, size=16467801, displayName=null, title=skyfake, dateAdded=1541168381, dateModified=1541166406, mimeType=0
D/ApkActivity: id=25950, data=/storage/emulated/0/yingyongbao.apk, size=8746837, displayName=null, title=yingyongbao, dateAdded=1541168381, dateModified=1541166455, mimeType=0
D/ApkActivity: id=25974, data=/storage/emulated/0/kuanappstore.apk, size=11948541, displayName=null, title=kuanappstore, dateAdded=1541168381, dateModified=1541166476, mimeType=0
可以看到,获取到了 apk 的存储路径,文件名称,文件大小等信息。
PackageInfo packageInfo = getPackageManager().getPackageArchiveInfo(data, PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES);
if (packageInfo != null) {
String packageName = packageInfo.packageName;
int versionCode = packageInfo.versionCode;
String versionName = packageInfo.versionName;
String label = packageInfo.applicationInfo.loadLabel(getPackageManager()).toString();
Log.d("ApkActivity", "packageName=" + packageName + ", versionCode=" + versionCode + ", versionName=" + versionName + ", labe=" + label);
} else {
Log.d("ApkActivity", "apk broken");
}
打印日志如下:
D/ApkActivity: apk broken
D/ApkActivity: packageName=com.tencent.android.qqdownloader, versionCode=7262130, versionName=7.2.6, labe=com.qq.AppService.RealAstApp
D/ApkActivity: packageName=com.coolapk.market, versionCode=1805241, versionName=8.3, labe=com.coolapk.market.CoolMarketApplication
可以看到,获取的包名,版本信息是正确的,但是获取的应用名称信息 label 竟然是包名,这是不正确的。那么,怎么才能通过一个 apk 的路径获取到它的应用名称呢?
思考一下:apk 分为三类,受损的apk,已安装的 apk 和未安装的 apk。我们知道,对于已安装的 apk,获取它的信息是可行的。重点是一个 apk 还未安装,怎样获取到它的应用名称呢?
请教了星球里的同学,可以查看一下系统的安装器 PackageInstaller
里是怎么实现的。看一下,安装器的界面:
查看一下源码,不难找到系统是通过 PackageUtil.java
这个类来获取 apk 的应用名称和应用图标的。来看一下具体的方法:
/**
* Utility method to load application label
*
* @param pContext context of package that can load the resources
* @param appInfo ApplicationInfo object of package whose resources are to be loaded
* @param snippetId view id of app snippet view
*/
public static AppSnippet getAppSnippet(
Activity pContext, ApplicationInfo appInfo, File sourceFile) {
final String archiveFilePath = sourceFile.getAbsolutePath();
Resources pRes = pContext.getResources();
AssetManager assmgr = new AssetManager();
assmgr.addAssetPath(archiveFilePath);
Resources res = new Resources(assmgr, pRes.getDisplayMetrics(), pRes.getConfiguration());
CharSequence label = null;
// Try to load the label from the package's resources. If an app has not explicitly
// specified any label, just use the package name.
if (appInfo.labelRes != 0) {
try {
label = res.getText(appInfo.labelRes);
} catch (Resources.NotFoundException e) {
}
}
if (label == null) {
label = (appInfo.nonLocalizedLabel != null) ?
appInfo.nonLocalizedLabel : appInfo.packageName;
}
Drawable icon = null;
// Try to load the icon from the package's resources. If an app has not explicitly
// specified any resource, just use the default icon for now.
if (appInfo.icon != 0) {
try {
icon = res.getDrawable(appInfo.icon);
} catch (Resources.NotFoundException e) {
}
}
if (icon == null) {
icon = pContext.getPackageManager().getDefaultActivityIcon();
}
return new PackageUtil.AppSnippet(label, icon);
}
可以看到,系统先创建一个 AssetManager
实例,然后调用 addAssetPath
方法把 apk 的路径添加进去,最后再创建一个 Resources
对象。这样就可以获取到 apk 的 应用名称和应用图标了。
但是,由于 addAssetPath
是一个 hide 的方法。我们需要通过反射来调用这个方法。修改后的类如下:
public class PackageUtil {
public static class AppSnippet {
public CharSequence label;
public Drawable icon;
public AppSnippet(CharSequence label, Drawable icon) {
this.label = label;
this.icon = icon;
}
@Override
public String toString() {
return "AppSnippet{" +
"label=" + label +
", icon=" + icon +
'}';
}
}
public static AppSnippet getAppSnippet(
Context pContext, ApplicationInfo appInfo, File sourceFile) {
final String archiveFilePath = sourceFile.getAbsolutePath();
Resources pRes = pContext.getResources();
AssetManager assetManager = null;
try {
assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, archiveFilePath);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
Resources res = new Resources(assetManager, pRes.getDisplayMetrics(), pRes.getConfiguration());
CharSequence label = null;
// Try to load the label from the package's resources. If an app has not explicitly
// specified any label, just use the package name.
if (appInfo.labelRes != 0) {
try {
label = res.getText(appInfo.labelRes);
} catch (Resources.NotFoundException e) {
}
}
if (label == null) {
label = (appInfo.nonLocalizedLabel != null) ?
appInfo.nonLocalizedLabel : appInfo.packageName;
}
Drawable icon = null;
// Try to load the icon from the package's resources. If an app has not explicitly
// specified any resource, just use the default icon for now.
if (appInfo.icon != 0) {
try {
icon = res.getDrawable(appInfo.icon);
} catch (Resources.NotFoundException e) {
}
}
if (icon == null) {
icon = pContext.getPackageManager().getDefaultActivityIcon();
}
return new AppSnippet(label, icon);
}
}
将获取 label 的代码修改为:
// String label = packageInfo.applicationInfo.loadLabel(getPackageManager()).toString();
String label = PackageUtil.getAppSnippet(ApkActivity.this, packageInfo.applicationInfo, new File(data)).label.toString();
运行程序,打印日志:
D/ApkActivity: packageName=com.tencent.android.qqdownloader, versionCode=7262130, versionName=7.2.6, labe=应用宝
D/ApkActivity: packageName=com.coolapk.market, versionCode=1805241, versionName=8.3, labe=酷安
可以看到,已经可以正确获取到 apk 的应用名称了。
从这里,不仅学到了一个知识点,而且学到了要学会去查看系统源码。
Android插件化探索(二)资源加载
Android 6.0 系统源码