解决Xposed拦截并修改:获取手机已安装应用的App包名和App标签问题
阅读原文时间:2021年04月20日阅读:1

首先,我们先明白一个问题,如何去遍历获取手机已安装应用的App相应的信息。

大多数情况下,我们使用PackageManager类提供的getInstalledPackages()接口来获取手机已安装应用信息。

例如博主这里的代码为:

 PackageManager packageManager=getPackageManager();

        List<PackageInfo> packageInfos=packageManager.getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES);

getInstalledPackages()源码分析:

/**
     * Return a List of all packages that are installed on the device.
     *
     * @param flags Additional option flags to modify the data returned.
     * @return A List of PackageInfo objects, one for each installed package,
     *         containing information about the package. In the unlikely case
     *         there are no installed packages, an empty list is returned. If
     *         flag {@code MATCH_UNINSTALLED_PACKAGES} is set, the package
     *         information is retrieved from the list of uninstalled
     *         applications (which includes installed applications as well as
     *         applications with data directory i.e. applications which had been
     *         deleted with {@code DONT_DELETE_DATA} flag set).
     */
    public abstract List<PackageInfo> getInstalledPackages(@PackageInfoFlags int flags);

首先我们明确一下,PackageManager是一个抽象类,它内部的getInstalledPackages接口也是一个抽象方法,返回的是一个PackageInfo类的List集合,PackageInfo类即为App信息的实体类。

下面我们开始探讨如何去拦截getInstalledPackages()接口。

我们可以看到getInstalledPackages()是一个抽象方法,所以肯定不能直接去Hook它,只能去Hook它的实现方法,即找到PackageManager抽象类的具体实现类。我们继续往下查找代码,看看是谁实现了PackageManager抽象类。

首当其冲很容易就会找到MockPackageManager类,我们发现它确实继承了PackageManager抽象类,但是它并不是真正的实现类。看一下它内部实现的getInstalledPackages接口:

@Override
    public List<PackageInfo> getInstalledPackages(int flags) {
        throw new UnsupportedOperationException();
    }

仅仅是抛出了一个UnsupportedOperationException异常!

我们找到这个类的具体介绍:

*
 * A mock {@link android.content.pm.PackageManager} class.  All methods are non-functional and throw
 * {@link java.lang.UnsupportedOperationException}. Override it to provide the operations that you
 * need.
 *
 * @deprecated Use a mocking framework like <a href="https://github.com/mockito/mockito">Mockito</a>.
 * New tests should be written using the
 * <a href="{@docRoot}tools/testing-support-library/index.html">Android Testing Support Library</a>.
 */

意思为:MockPackageManager类是PackageManager的模拟类,重写它以提供您所需要的操作。所以它并不是真正的实现类。

那么真正实现PackageManager抽象类的到底是谁??

答案是:ApplicationPackageManager类。

ApplicationPackageManager类中具体实现了PackageManager抽象类的一系列接口,getInstalledPackages接口也是在ApplicationPackageManager类中实现的。

这个时候你可能要抓狂了,因为你发现在编辑器中写代码根本找不到这个ApplicationPackageManager类!

博主一开始也是对这个非常抓狂,一直找ApplicationPackageManager类的路径位置,终于让我给找到了,它位于AndroidSDK安装路径下。(ps:找到真的很不容易,平时开发很少关注SDK这块---TAT----)

具体博主的路径为:

 找到了就很好办了,打开看看它的源码实现:

/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.app;

import android.annotation.DrawableRes;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.StringRes;
import android.annotation.XmlRes;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.IntentSender;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.ChangedPackages;
import android.content.pm.ComponentInfo;
import android.content.pm.FeatureInfo;
import android.content.pm.IOnPermissionsChangeListener;
import android.content.pm.IPackageDataObserver;
import android.content.pm.IPackageDeleteObserver;
import android.content.pm.IPackageInstallObserver;
import android.content.pm.IPackageManager;
import android.content.pm.IPackageMoveObserver;
import android.content.pm.IPackageStatsObserver;
import android.content.pm.InstantAppInfo;
import android.content.pm.InstrumentationInfo;
import android.content.pm.IntentFilterVerificationInfo;
import android.content.pm.KeySet;
import android.content.pm.PackageInfo;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageItemInfo;
import android.content.pm.PackageManager;
import android.content.pm.ParceledListSlice;
import android.content.pm.PermissionGroupInfo;
import android.content.pm.PermissionInfo;
import android.content.pm.ProviderInfo;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.content.pm.SharedLibraryInfo;
import android.content.pm.VerifierDeviceIdentity;
import android.content.pm.VersionedPackage;
import android.content.res.Resources;
import android.content.res.XmlResourceParser;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.Process;
import android.os.RemoteException;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.UserManager;
import android.os.storage.StorageManager;
import android.os.storage.VolumeInfo;
import android.provider.Settings;
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
import android.system.StructStat;
import android.util.ArrayMap;
import android.util.IconDrawableFactory;
import android.util.LauncherIcons;
import android.util.Log;
import android.view.Display;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.SomeArgs;
import com.android.internal.util.Preconditions;
import com.android.internal.util.UserIcons;

import dalvik.system.VMRuntime;

import libcore.util.EmptyArray;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/** @hide */
public class ApplicationPackageManager extends PackageManager {
    private static final String TAG = "ApplicationPackageManager";
    private final static boolean DEBUG_ICONS = false;

    private static final int DEFAULT_EPHEMERAL_COOKIE_MAX_SIZE_BYTES = 16384; // 16KB

    // Default flags to use with PackageManager when no flags are given.
    private final static int sDefaultFlags = PackageManager.GET_SHARED_LIBRARY_FILES;

    private final Object mLock = new Object();

    @GuardedBy("mLock")
    private UserManager mUserManager;
    @GuardedBy("mLock")
    private PackageInstaller mInstaller;

    @GuardedBy("mDelegates")
    private final ArrayList<MoveCallbackDelegate> mDelegates = new ArrayList<>();

    @GuardedBy("mLock")
    private String mPermissionsControllerPackageName;

    .......

    //代码过长,有两千多行,这里贴出具体实现代码

    @SuppressWarnings("unchecked")
    @Override
    public List<PackageInfo> getInstalledPackages(int flags) {
        return getInstalledPackagesAsUser(flags, mContext.getUserId());
    }

    /** @hide */
    @Override
    @SuppressWarnings("unchecked")
    public List<PackageInfo> getInstalledPackagesAsUser(int flags, int userId) {
        try {
            ParceledListSlice<PackageInfo> parceledList =
                    mPM.getInstalledPackages(flags, userId);
            if (parceledList == null) {
                return Collections.emptyList();
            }
            return parceledList.getList();
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }


    ........
}

从源码中我们可以很清楚的看到,首先我们可以看到ApplicationPackageManager类确实是继承了PackageManager抽象类,其中关键的getInstalledPackages接口实现可以看到,原来它是返回了getInstalledPackagesAsUser()方法调用,getInstalledPackagesAsUser()才是具体的实现,接着看getInstalledPackagesAsUser()方法,关键代码为:

ParceledListSlice<PackageInfo> parceledList =
                    mPM.getInstalledPackages(flags, userId);

原来真正调用的是 IPackageManager类的getInstalledPackages()方法,参数flags即为我们在getInstalledPackages接口传入的值。userId为Context类中的userId变量。

更加深入的源码分析就到这里,有兴趣的读者可以自行查看。在这里我们的目的已经达到了,即找到PackageManager抽象类的具体实现类,下面我们开始探讨如何去拦截它的getInstalledPackages接口。

Hook->getInstalledPackages接口

上面我们已经分析到,getInstalledPackages接口本身是一个抽象方法,在ApplicationPackageManager类中进行了该抽象方法的具体实现,所以我们Hook的目标类是ApplicationPackageManager,目标方法是getInstalledPackages(Int)。

实现代码为:

XposedHelpers.findAndHookMethod("android.app.ApplicationPackageManager", loadPackageParam.classLoader, "getInstalledPackages", int.class, new XC_MethodHook() {
                @Override
                protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                   //Hook后的具体逻辑操作

                }
            });

在这里还有一个疑问,通常我们拦截到该方法后,需要返回我们自己的处理信息,我们了解到getInstalledPackages(Int)方法返回的是一个PackageInfo类的List集合,具体做修改的话难以下手,比如修改其中的某一个item信息,或者修改的是整条list,这该怎么办?具体的处理逻辑为:遍历原先的List集合,找到我们要修改的某条item,然后赋值修改该item的具体信息。

代码如下:

XposedHelpers.findAndHookMethod("android.app.ApplicationPackageManager", loadPackageParam.classLoader, "getInstalledPackages", int.class, new XC_MethodHook() {
                @Override
                protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                    //自定义一个新的List用来接收原来的返回信息
                    List<PackageInfo> packageInfos=(List) param.getResult();

                    //开始遍历查找
                    for (PackageInfo packageInfo:packageInfos){
                        //匹配信息为你想要修改的某个App标签识别码
                        if (packageInfo.applicationInfo.labelRes==2131230747){
                            //找到了,修改该App的应用包名
                            packageInfo.packageName="com.test.testing";
                        }

                    }

                    //把修改后的List当作结果返回去
                    param.setResult(packageInfos);
                }
            });

这里具体再说一下出现的标签识别码labelRes

labelRes位于PackageItemInfo类中,官方描述为:

 /**
     * A string resource identifier (in the package's resources) of this
     * component's label.  From the "label" attribute or, if not set, 0.
     */

意思就是此包的字符串资源标识符(在包的资源中),主要作用是在查找App标签文本的时候作为标签文本的标识符,已确定查找到相应的App标签文本,我把它喊作是App标签识别码,意思更形象一些。

具体labelRes用到的地方就是我们接下来要将的部分,获取App标签文本信息。

博主获取App标签文本的代码是这样写的:

packageInfo.applicationInfo.loadLabel(getPackageManager()).toString()

其中packageInfo为PackageInfo类的对象。这里我们主要关注的是loadLabel()方法

loadLabel()方法位于PackageItemInfo类中,参数为一个PackageManager类的对象,这里并没有出现labelRes的影子,别急,我们接下来去看一下loadLabel()方法的源码:

/**
     * Retrieve the current textual label associated with this item.  This
     * will call back on the given PackageManager to load the label from
     * the application.
     *
     * @param pm A PackageManager from which the label can be loaded; usually
     * the PackageManager from which you originally retrieved this item.
     *
     * @return Returns a CharSequence containing the item's label.  If the
     * item does not have a label, its name is returned.
     */
    public CharSequence loadLabel(PackageManager pm) {
        if (nonLocalizedLabel != null) {
            return nonLocalizedLabel;
        }
        if (labelRes != 0) {
            CharSequence label = pm.getText(packageName, labelRes, getApplicationInfo());
            if (label != null) {
                return label.toString().trim();
            }
        }
        if (name != null) {
            return name;
        }
        return packageName;
    }

 我们先一步一步的看,首先是判断了一个变量nonLocalizedLabel 是否为空,不为空的话就直接返回该变量。那么nonLocalizedLabel变量又是什么呢?我们看一下它的官方描述:

 /**
     * The string provided in the AndroidManifest file, if any.  You
     * probably don't want to use this.  You probably want
     * {@link PackageManager#getApplicationLabel}
     */

意思为:AndroidManifest文件中提供的字符串(如果有的话)。也就是说如果App的核心配置文件里面写上了该App的标签,那么就可以直接返回该标签信息,不用执行下一步。很可惜的是大多数情况下nonLocalizedLabel变量为空,所以我们继续往下分析:

我们看到判断labelRes是否为0,这里主要是判断是否获取到了标识符labelRes,在获得labelRes值的时候,如果获取失败则返回0,成功就返回labelRes码,只有获取labelRes码成功才会进入获取App标签文本的方法getText().

毫无疑问,getText()方法的具体实现也是在ApplicationPackageManager类中,我们去看一下:

@Override
    public CharSequence getText(String packageName, @StringRes int resid,
                                ApplicationInfo appInfo) {
        ResourceName name = new ResourceName(packageName, resid);
        CharSequence text = getCachedString(name);
        if (text != null) {
            return text;
        }
        if (appInfo == null) {
            try {
                appInfo = getApplicationInfo(packageName, sDefaultFlags);
            } catch (NameNotFoundException e) {
                return null;
            }
        }
        try {
            Resources r = getResourcesForApplication(appInfo);
            text = r.getText(resid);
            putCachedString(name, text);
            return text;
        } catch (NameNotFoundException e) {
            Log.w("PackageManager", "Failure retrieving resources for "
                  + appInfo.packageName);
        } catch (RuntimeException e) {
            // If an exception was thrown, fall through to return
            // default icon.
            Log.w("PackageManager", "Failure retrieving text 0x"
                  + Integer.toHexString(resid) + " in package "
                  + packageName, e);
        }
        return null;
    }

 我们可以看到,具体的获取方法是getCachedString(),传入的参数是一个ResourceName 对象。我们的标识符labelRes最终传入了ResourceName类的构造函数里面。具体的源码分析不再讲解,这里我们主要的核心是如何去修改某个App相应的标签文本。

修改App相应的标签文本

两种Hook方式:

第一种:Hook目标类是PackageItemInfo类,目标方法是loadLabel()

XposedHelpers.findAndHookMethod(PackageItemInfo.class, "loadLabel", PackageManager.class, new XC_MethodHook() {
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {

                //匹配想要修改的App的标签
                if (param.getResult().equals("喜马拉雅FM")){
                    param.setResult("测试");
                }
            }
        });

第二种:Hook目标是ApplicationPackageManager类,目标方法是getText()

XposedHelpers.findAndHookMethod("android.app.ApplicationPackageManager",loadPackageParam.classLoader, methodName, String.class,int.class, ApplicationInfo.class,new XC_MethodHook() {
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {

                //匹配想要修改的App标签文本识别码
                if ((int)param.args[1]==2131230747){
                    param.setResult("测试");
                }

            }
        });

两种方式都可以修改某一条item的标签文本信息,具体的使用看业务逻辑。

本文到此结束,需要用到本文,请标明出处!