当前位置: 首页 > 工具软件 > OWNER > 使用案例 >

一键设置 DeviceAdmin/ProfileOwner/DeviceOwner 应用

符学
2023-12-01

一键设置 DeviceAdmin/ProfileOwner/DeviceOwner 应用


概述

Android提供了三种设备管理方案,Device Administration(设备管理员), ProfileOwner(配置文件所有者)和 DeviceOwner(设备所有者)。这三种管理方案对应三种等级的管理权限,相对的,等级越高所拥有的管理权限越高,面临的风险也对大,所以,要将一个应用设置成为这些管理设备,也需要不同的权限等级。

设置 Device Administration

要设置一个DeviceAdmin 所需要权限相对来说是最小的,Android系统提供了一种方案:

Intent intent = new Intent(
	DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN);
intent.putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN,
	mComponentName);
intent.putExtra(DevicePolicyManager.EXTRA_ADD_EXPLANATION, "提示文字");
startActivityForResult(intent, 1);

该方法将激活DeviceAdmin的动作委托给系统Settings,有界面提示用户是否激活/停用/卸载一个设备管理应用。这种方案其实是一种动态权限,由用户决定是否启用设备管理。在特殊行业中,有些操作不应该让用户决定,由管理平台在后台一键设置。
如果一个应用的进程所属system,那么就和系统Settings具有相同的权限,我们可以在系统中添加一个这样的程序,用来一键设置自己的DeviceAdmin程序。

/**
 * 设置DeviceAdmin
 * packageName: 应用程序包名
 * policyReceiver: 继承了DeviceAdminReceiver的类名
 */
public static void setActiveAdmin(String packageName, String policyReceiver) {
    // 1. 将包名和类名转换为ComponentName
    ComponentName component = new ComponentName(packageName, policyReceiver);
    // 2. 通过ComponentName获取ActivityInfo,如果为空,说明参数传递有误,系统中根本不存在这个应用,直接返回
    ActivityInfo ai = null;
    try {
        ai = iPackageManager.getReceiverInfo(component,
                PackageManager.GET_META_DATA |
                PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS |
                PackageManager.MATCH_DIRECT_BOOT_UNAWARE |
                PackageManager.MATCH_DIRECT_BOOT_AWARE, getMyUserId());
    } catch (RemoteException e) {
        debug("Unable to load component: " + component);
    }
    // 3. 调用DevicePolicyManager的现有方法setActiveAdmin(系统级应用有权限)
    mDevicePolicyManager.setActiveAdmin(component, true /*refreshing*/, mUserId);
}
/**
 * 通过包名判断一个程序是否为激活的DeviceAdmin
 * packageName: 应用程序包名
 * return
 *      true: 是
 *      false: 不是
 */
public static boolean packageHasActiveAdmins(String packageName) {
    // packageHasActiveAdmins方法标记为@hide,只有系统应用可以调用
    DevicePolicyManager devicePolicyManager = (DevicePolicyManager) getSystemService(Context.DEVICE_POLICY_SERVICE);
    return devicePolicyManager.packageHasActiveAdmins(packageName);
}

了解更多DeviceAdmin的权限以及API的使用详情,请转到往期文章Android Device Administration 应用的能力

设置 ProfileOwner

之前介绍过,ProfileOwner 在国内可能不适用(Android DevicePolicyManager 设备管理中已做出说明),所以设置一个ProfileOwner程序有几条途径不做深入讲解,这里只给出一个方案:
属于system进程的系统应用,有设置ProfileOwner程序的能力,可以在系统中实现一个系统应用,暴露一个一键设置ProfileOwner的接口:

/**
 * 设置ProfileOwner
 * packageName: 应用程序包名
 * policyReceiver: 继承了DeviceAdminReceiver的类
 * deviceUserName: 为ProfileOwner设置一个名字,如不设置传null
 * return
 *      true:  设置成功
 *      false: 设置失败
 * (限于篇幅这里只给出了关键代码,一部分逻辑、变量代码省略)
 */
public static boolean setProfileOwner(String packageName, String policyReceiver, String deviceUserName) {
    // 1. 将包名和类名转换为ComponentName
    ComponentName component = new ComponentName(packageName, policyReceiver);
    // 2. 通过ComponentName获取ActivityInfo,如果为空,说明参数传递有误,系统中根本不存在这个应用,直接返回
    ActivityInfo ai = null;
    try {
        ai = iPackageManager.getReceiverInfo(component,
                PackageManager.GET_META_DATA |
                PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS |
                PackageManager.MATCH_DIRECT_BOOT_UNAWARE |
                PackageManager.MATCH_DIRECT_BOOT_AWARE, getMyUserId());
    } catch (RemoteException e) {
        debug("Unable to load component: " + component);
    }
    // 3. 调用DevicePolicyManager的现有方法setActiveAdmin(系统级应用有权限)
    mDevicePolicyManager.setActiveAdmin(component, true /*refreshing*/, mUserId);
    // 4. 设置setProfileOwner
    try {
        result = mDevicePolicyManager.setProfileOwner(component, deviceUserName, mUserId);
        debug("set package " + component + " as profile owner result: " + result);
    } catch (RemoteException | IllegalArgumentException | IllegalStateException e) {
        debug("Error: setProfileOwner failed to " + component.flattenToShortString() + " Error Info: " + e);
        // 如果设置ProfileOwner异常,则将设置的DeviceAdmin程序也一并取消
        try {
            mDevicePolicyManager.removeActiveAdmin(component, mUserId);
        } catch (RemoteException e2) {
            debug("Error: removeActiveAdmin failed to " + component.toString() + " Error Info: " + e2);
        }
    }
    if (result) {
        try {
            mDevicePolicyManager.setUserProvisioningState(DevicePolicyManager.STATE_USER_SETUP_FINALIZED, mUserId);
        } catch (RemoteException e) {
            debug("Error: setUserProvisioningState failed to " + component.toString() + " Error Info: " + e);
        }
        debug("Success: Active admin and profile owner set to " + component.toShortString() + " for user " + mUserId);
    }
}

当一个应用成为了ProfileOwner应用,它就拥有了所有的ProfileOwner的能力。了解更多ProfileOwner的权限以及API的使用详情,请转至往期文章Android ProfileOwner 应用的能力

设置 DeviceOwner

DeviceOwner可以使一个第三方应用程序拥有系统最高管理权限,面临的风险也是最大的。 要设置一个DeviceOwner程序, 需要很高的权限,系统提供两种方式:

adb shell

msm8953_64:/ # 
msm8953_64:/ # dpm set-device-owner --name Test com.action.deviceadmin/.DPMTestReceiver
-----------------------mName Test
mComponent: {com.action.deviceadmin/com.action.deviceadmin.DPMTestReceiver}, mName: Test, mUserId: 0
Success: Device owner set to package ComponentInfo{com.action.deviceadmin/com.action.deviceadmin.DPMTestReceiver}
Active admin set to component {com.action.deviceadmin/com.action.deviceadmin.DPMTestReceiver}
msm8953_64:/ # 

通过dpm命令设置一个DeviceOwner。

使用NFC方式

以下内容摘录自 https://mp.weixin.qq.com/s?__biz=MzAxMTE3MDkyMA==&mid=506926154&idx=1&sn=55ea2cfc894db74dcf296233a3e74a6f#rd 的文章内容。


用NFC传输的方式来使一个App成为DeviceOwner(设备所有者),我们需要两部手机。

首先,两台设备都要支持NFC并激活了NFC,并且激活了Android Beam功能(在设置里的NFC and payment里)。

第一台设备(Mobile A)是要在其上安装App,并使这个App成为Device Owner的。这个App可以是任意的一个App(我们的例子中是一个叫作Kiosk Mode Demo的App。

第二台设备(Mobile B)是要provision那台Mobile A的(使Mobile A上的App成为Device Owner),算是数据传输方/服务提供方。Mobile B上安装了我们的SetDeviceOwner这个App。

然后,在那个SetDeviceOwner的App里的源码中,比较关键的设置是下面几个:
EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME

对应要成为Device Owner的App的完整包名,例如:com.enmingx.test

EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_LOCATION

对应要成为Device Owner的App的下载URL,例如:http://www.dropbox.com/xxx

EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_CHECKSUM

对应要成为Device Owner的App的checksum(校验码),要计算校验码,可以用adb命令:

cat MY_APP.apk | openssl dgst -binary -sha1 | openssl base64 | tr ‘+/’ ‘-_’ | tr -d ‘=’

EXTRA_PROVISIONING_WIFI_SSID

对应用于下载要成为Device Owner的App的WiFi的名称

EXTRA_PROVISIONING_WIFI_SECURITY_TYPE

对应用于下载要成为Device Owner的App的安全类型,比如WPA或WPA2

最后,在那个SetDeviceOwner的App源码里,把这些数据都“打包”到一个NFC Bundle中,用NFC技术来传输到另一台手机。

你应该知道如何使用NFC来进行数据传输吧:

让两个手机足够接近,背靠背,然后会听到清脆的一声“叮”,显示"Touch to beam",然后你轻触作为传输方的那台设备的屏幕,就开始传输了。

为了成功使一台设备上的App成为Device Owner,这台设备必须从来没被配置过(当然更不能被Root过),也不能被设置过Device Owner或Profile Owner。如果已经配置过了,可以恢复出场设置。

开始操作:

  1. 对Mobile A恢复出厂设置(Factory Reset),一般在Settings(设置)里就可以选择(比较文明的方式);也可以用比较粗暴的方式,按键的方式(一般是同时按住 音量向上键+Home键+电源键 几秒,然后会出现选项,可以选择)。

  2. 当Mobile A的恢复出厂设置结束后,Mobile A会出现初始设置的界面。此时,Mobile A就是处于unprovisioned(还没被设置)的状态。

  3. 在Mobile B上,安装我们的SetDeviceOwner这个App,也就是要使其他设备的App成为Device Owner的,术语叫做“Device owner provisioning”。

  4. 在Mobile B上开启SetDeviceOwner这个App,点击“Generate checksum”按钮,会生成checksum(校验码)。

  5. Mobile A处于初始配置状态,两台设备的屏幕都是打开的。将两台设备(Mobile A和Mobile B)背靠背,足够近,直到听到清脆的“叮”的声响,然后在Mobile B上轻触屏幕,即开始从Mobile B向Mobile A进行NFC的数据传输。

  6. 当NFC传输完成后(一般瞬间就完成了),Mobile A上会显示配置Device Owner的界面,标题貌似是Set up your profile(记不清了…),点击Set up按钮之后会问你要不要Encrypt设备(对数据加密),点击“是”(OK),然后选择快速Encrypt还是对所有数据Encrypt(加密所有数据会很慢),一般都选Fast Encryption就好。然后开始对手机的数据加密,不要问为什么,就是必须要这步。

  7. 加密完成后,Mobile A会重启。然后,因为之前我们传输过去的数据里面指定了WiFi的SSID和密码,而且也指定了那个要成为Device Owner的App的下载链接(URL),因此,会显示让你配置选择WiFi,请选择你之前指定的那个WiFi,并连接。

  8. 一旦WiFi成功连接上Internet,就会开始下载指定的App。下载完成后会开始安装,然后会使这个App成为Device Owner。

  9. 如果你看到一个Toast跳出来说:Device Owner enabled,那么就OK了。恭喜,你的App已经成为了Mobile A的Device Owner了。


可以看到,按照官方的方法设置一个DeviceOwner程序,要么得拥有Shell这种级别的权限,要么就使用NFC传输的这种繁杂的方式。对于行业需求,NFC的方式显然不可行。
跟踪dpm的源码,设置DeviceOwner 的方法最终调用到DevicePolicyManagerService.java中:

@Override
public boolean setDeviceOwner(ComponentName admin, String ownerName, int userId) {
    if (!mHasFeature) {
        return false;
    }
    if (admin == null
            || !isPackageInstalledForUser(admin.getPackageName(), userId)) {
            throw new IllegalArgumentException("Invalid component " + admin
                + " for device owner");
    }
    final boolean hasIncompatibleAccountsOrNonAdb =
            hasIncompatibleAccountsOrNonAdbNoLock(userId, admin);
    synchronized (this) {
        enforceCanSetDeviceOwnerLocked(admin, userId, hasIncompatibleAccountsOrNonAdb);
        
        ...
        省略大量其它代码
        ...
    }
}

setDeviceOwner方法在正在设置DeviceOwenr之前会做很多权限检查,以保证安全性,只要调用方法enforceCanSetDeviceOwnerLockedcheckDeviceOwnerProvisioningPreConditionLocked

enforceCanSetDeviceOwnerLocked

private void enforceCanSetDeviceOwnerLocked(@Nullable ComponentName owner, int userId,
            boolean hasIncompatibleAccountsOrNonAdb) {
        if (!isAdb()) {
            enforceCanManageProfileAndDeviceOwners();
        }

        final int code = checkDeviceOwnerProvisioningPreConditionLocked(
                owner, userId, isAdb(), hasIncompatibleAccountsOrNonAdb);
        switch (code) {
            case CODE_OK:
                return;
            case CODE_HAS_DEVICE_OWNER:
                throw new IllegalStateException(
                        "Trying to set the device owner, but device owner is already set.");
            case CODE_USER_HAS_PROFILE_OWNER:
                throw new IllegalStateException("Trying to set the device owner, but the user "
                        + "already has a profile owner.");
            case CODE_USER_NOT_RUNNING:
                throw new IllegalStateException("User not running: " + userId);
            case CODE_NOT_SYSTEM_USER:
                throw new IllegalStateException("User is not system user");
            case CODE_USER_SETUP_COMPLETED:
                throw new IllegalStateException(
                        "Cannot set the device owner if the device is already set-up");
            case CODE_NONSYSTEM_USER_EXISTS:
                throw new IllegalStateException("Not allowed to set the device owner because there "
                        + "are already several users on the device");
            case CODE_ACCOUNTS_NOT_EMPTY:
                throw new IllegalStateException("Not allowed to set the device owner because there "
                        + "are already some accounts on the device");
            case CODE_HAS_PAIRED:
                throw new IllegalStateException("Not allowed to set the device owner because this "
                        + "device has already paired");
            default:
                throw new IllegalStateException("Unexpected @ProvisioningPreCondition " + code);
        }
    }

checkDeviceOwnerProvisioningPreConditionLocked

private int checkDeviceOwnerProvisioningPreConditionLocked(@Nullable ComponentName owner,
            int deviceOwnerUserId, boolean isAdb, boolean hasIncompatibleAccountsOrNonAdb) {
        if (mOwners.hasDeviceOwner()) {
            return CODE_HAS_DEVICE_OWNER;
        }
        if (mOwners.hasProfileOwner(deviceOwnerUserId)) {
            return CODE_USER_HAS_PROFILE_OWNER;
        }
        if (!mUserManager.isUserRunning(new UserHandle(deviceOwnerUserId))) {
            return CODE_USER_NOT_RUNNING;
        }
        if (mIsWatch && hasPaired(UserHandle.USER_SYSTEM)) {
            return CODE_HAS_PAIRED;
        }
        if (isAdb) {
            // if shell command runs after user setup completed check device status. Otherwise, OK.
            if (mIsWatch || hasUserSetupCompleted(UserHandle.USER_SYSTEM)) {
                if (!mInjector.userManagerIsSplitSystemUser()) {
                    if (mUserManager.getUserCount() > 1) {
                        return CODE_NONSYSTEM_USER_EXISTS;
                    }
                    if (hasIncompatibleAccountsOrNonAdb) {
                        return CODE_ACCOUNTS_NOT_EMPTY;
                    }
                } else {
                    // STOPSHIP Do proper check in split user mode
                }
            }
            return CODE_OK;
        } else {
            if (!mInjector.userManagerIsSplitSystemUser()) {
                // In non-split user mode, DO has to be user 0
                if (deviceOwnerUserId != UserHandle.USER_SYSTEM) {
                    return CODE_NOT_SYSTEM_USER;
                }
                // In non-split user mode, only provision DO before setup wizard completes
                if (hasUserSetupCompleted(UserHandle.USER_SYSTEM)) {
                    return CODE_USER_SETUP_COMPLETED;
                }
            } else {
                // STOPSHIP Do proper check in split user mode
            }
            return CODE_OK;
        }
    }

细看以上两个方法的权限检测,很多权限在检测之前会先判断 isAdb,如果是adb,很多权限都会跳过。让我们看看这个isAdb 是怎么来的。

private boolean isAdb() {
    final int callingUid = mInjector.binderGetCallingUid();

    return callingUid == Process.SHELL_UID || callingUid == Process.ROOT_UID;
    }

判断流程非常简单,就是获得调用方进程的Uid,如果是SHELL_UID 或者 ROOT_UID,那么就认为有设置DeviceOwner 的权限,跳过权限检查,直接设置。

**
 * Defines the root UID.
 * @hide
 */
public static final int ROOT_UID = 0;

/**
 * Defines the UID/GID under which system code runs.
 */
public static final int SYSTEM_UID = 1000;

/**
 * Defines the UID/GID under which the telephony code runs.
 */
public static final int PHONE_UID = 1001;

/**
 * Defines the UID/GID for the user shell.
 * @hide
 */
public static final int SHELL_UID = 2000;

以上是系统中定义的各大内部专属进程的UID值。属于system进程的应用的UID值为1000,没有权限设置DeviceOwner。而Android系统中设置DeviceOwner 的入口也只有这里。
要实现一键设置DeviceOwner程序的功能,只能修改DevicePolicyManagerService 的代码。我们可以制定一个方案,既然 SHELL 和 ROOT进程能够设置DeviceOwner,那么我们添加的系统进程也可以“冒充SHELL进程”, 只需要添加如下几行代码即可:

private boolean isAdb() {
    final int callingUid = mInjector.binderGetCallingUid();
    
    // 获取调用放的进程id -- pid
    final int callingPid = mInjector.binderGetCallingPid();
    
    return callingUid == Process.SHELL_UID || callingUid == Process.ROOT_UID || fixedProcess(callingPid);
}

fixedProcess(callingPid) 方法比较 我们自定义的系统进程的PID 和调用方的PID是否一致,如果一致则认为有权限设置DeviceOwner,也即是说,只有ROOT、SHEL 和 自定义系统进程 拥有设置DeviceOwner 的权限。应用进程的PID是系统生成的,不同的应用程序进程ID不可能重复,同一个应用被杀死后再开起来,进程ID也会不同,系统会以递增的形式分配进程ID。所以不用担心该ID被冒名顶替。fixedProcess方法会取得自定义系统进程事先保存好的PID。

添加了上述规避权限的流程,接下来只需要在自定义系统进程中添加一个设置DeviceOwner的接口即可:

/**
 * 设置DeviceOwner
 * packageName: 应用程序包名
 * policyReceiver: 继承了DeviceAdminReceiver的类
 * deviceUserName: 为DeviceOwner设置一个名字,如不设置传null
 * return
 *      true:  设置成功
 *      false: 设置失败
 * (限于篇幅这里只给出了关键代码,一部分逻辑、变量代码省略)
 */
public static boolean setDeviceOwner(String packageName, String policyReceiver, String deviceUserName) {
     // 1. 保存本应用的进程号,在设置DeviceOwner时的权限检测中取出,视为Shell、Root。
     saveMdmPid(android.os.Process.myPid());
     // 2. 将包名和类名转换为ComponentName
    ComponentName component = new ComponentName(packageName, policyReceiver);
    // 3. 通过ComponentName获取ActivityInfo,如果为空,说明参数传递有误,系统中根本不存在这个应用,直接返回
    ActivityInfo ai = null;
    try {
        ai = iPackageManager.getReceiverInfo(component,
                PackageManager.GET_META_DATA |
                PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS |
                PackageManager.MATCH_DIRECT_BOOT_UNAWARE |
                PackageManager.MATCH_DIRECT_BOOT_AWARE, getMyUserId());
    } catch (RemoteException e) {
        debug("Unable to load component: " + component);
    }
    // 4. 调用DevicePolicyManager的现有方法setActiveAdmin(需要系统级应用有权限)
    mDevicePolicyManager.setActiveAdmin(component, true /*refreshing*/, mUserId);
    // 5. 调用DevicePolicyManager的setDeviceOwner接口
    try {
        result = mDevicePolicyManager.setDeviceOwner(component, deviceUserName, mUserId);
        debug("set package " + component + " as device owner result: " + result);
    } catch (RemoteException | IllegalArgumentException | IllegalStateException e) {
        debug("Error: setDeviceOwner failed to " + component.flattenToShortString() + " Error Info: " + e);
        // Need to remove the admin that we just added.
        try {
            mDevicePolicyManager.removeActiveAdmin(component, mUserId);
        } catch (RemoteException e2) {
            debug("Error: removeActiveAdmin failed to " + component.toString() + " Error Info: " + e2);
        }
    }
    if (result) {
        try {
            mDevicePolicyManager.setUserProvisioningState(DevicePolicyManager.STATE_USER_SETUP_FINALIZED, mUserId);
        } catch (RemoteException e) {
            debug("Error: setUserProvisioningState failed to " + component.toString() + " Error Info: " + e);
        }
        debug("Success: Device owner set to package " + component);
        debug("Active admin set to component " + component.toShortString());
    }
}

如此,一键设置DeviceOwner 应用的接口就实现了。

对于一个应用程序来说,成为DeviceOwner应用即拥有了系统最高软件管理权限。了解更多DeviceOwner的权限以及API的使用详情,请回顾往期博客Android DeviceOwner 应用的能力


上一篇 Android DeviceOwner 应用的能力
下一篇 微信小程序入门级实战开发指南
 类似资料: