前言

这一段时间,又被调回来做清理应用了。之前,也做过一段清理。这次又回来做,争取要做得好些。言归正传,清理应用中在获取无效的 apk 文件后,需要展示 apk 的信息。其他信息都容易获取,只有 apk 的应用名称的获取,费了一些工夫。

正文

1. 获取到 sd 卡上所有的 .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/skyvpnfake.apk, size=16467801, displayName=null, title=skyvpnfake, 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 的存储路径,文件名称,文件大小等信息。

2. 根据 apk 路径获取 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 里是怎么实现的。看一下,安装器的界面:

android 获取已安装的apk 获取apk信息_sd卡


查看一下源码,不难找到系统是通过 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 的应用名称了。

结语

从这里,不仅学到了一个知识点,而且学到了要学会去查看系统源码。