Android系统添加流量控制开关(NetworkPolicyManager)

背景

最近产品那边有个需求是需要有个系统接口, 用来控制第三方APP的流量访问权限, 即你可以单独关闭某一个APP的流量访问权限(WIFI下不影响), 本篇文章就是记录我解决这个问题的流程, 主要说明如何在自己对相关模块不熟悉的情况下, 分析并解决问题.

注: 本文中源码均为高通平台, Android 7.1代码

分析思路

首先如果系统没有这方面的功能或者接口的话, 光靠自己去实现难度有点大 , 因为你得对整个网络访问流程很熟悉., 我自己是没有这个模块的开发经验的, 所以只能先看看系统中有没有类似的功能. 很庆幸的是, 刚好有个类似的功能, 在Android原生设置界面里面, 有个 应用数据流量 界面, 打开方式如下:

设置 -> 应用程序 -> 应用程序信息(点击任何一个app) -> 数据使用

界面内容如下:


Android系统添加流量控制开关(NetworkPolicyManager)_第1张图片
Screenshot_20180803-170941.png

可以看到, 对于每个应用, 都有 允许在后台使用移动数据流量 的开关选项, 这个只能控制后台应用的数据访问权限, 既然能控制后台应用, 前台应用自然不是问题, 看到这里基本就不慌了, 找到关键点了, 接下来就是根据这个信息阅读源码, 查看流程了.

后台数据访问控制流程

首先得把控制后台数据访问流程弄清楚, 才知道怎么添加前台数据控制接口.
应用数据流量这个界面, 对应的Java代码路径为:

packages/apps/Settings/src/com/android/settings/datausage/AppDataUsage.java

这个类是一个Fragment, 原生设置基本都是Activity + PreferenceFragment 组合编写的.
对应的xml文件路径为: packages/apps/Settings/res/xml/app_data_usage.xml

首先找到 后台数据 状态更改后对应的逻辑控制代码:

@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
    if (com.android.settings.Utils.isMonkeyRunning()) {
        return false;
    }
    if (preference == mRestrictBackground) {
        mDataSaverBackend.setIsBlacklisted(mAppItem.key, mPackageName, !(Boolean) newValue);
        return true;
    } else if (preference == mUnrestrictedData) {
        mDataSaverBackend.setIsWhitelisted(mAppItem.key, mPackageName, (Boolean) newValue);
        return true;
    }
    return false;
}

可以看到调用了 mDataSaverBackend.setIsBlacklisted() 函数, 此代码文件路径如下:

packages/apps/Settings/src/com/android/settings/datausage/DataSaverBackend.java

对应函数代码如下:

public void setIsBlacklisted(int uid, String packageName, boolean blacklisted) {
    mPolicyManager.setUidPolicy(
            uid, blacklisted ? POLICY_REJECT_METERED_BACKGROUND : POLICY_NONE);
    if (blacklisted) {
        MetricsLogger.action(mContext, MetricsEvent.ACTION_DATA_SAVER_BLACKLIST, packageName);
    }
}

在这里我们看到了关键点 mPolicyManager, 即 NetworkPolicyManager, 这个就是Android系统用来控制网络访问策略的, 继续查看其 setUidPolicy() 函数:

frameworks/base/core/java/android/net/NetworkPolicyManager.java

public void setUidPolicy(int uid, int policy) {
    try {
        mService.setUidPolicy(uid, policy);
    } catch (RemoteException e) {
        throw e.rethrowFromSystemServer();
    }
}

可以看到 NetworkPolicyManager 只是一个代理类, 真正实现功能的是 NetworkPolicyManagerService 代码和路径如下:

frameworks/base/services/core/java/com/android/server/net/NetworkPolicyManagerService.java

public void setUidPolicy(int uid, int policy) {
    mContext.enforceCallingOrSelfPermission(MANAGE_NETWORK_POLICY, TAG);

    if (!UserHandle.isApp(uid)) {
        throw new IllegalArgumentException("cannot apply policy to UID " + uid);
    }
    synchronized (mUidRulesFirstLock) {
        final long token = Binder.clearCallingIdentity();
        try {
            final int oldPolicy = mUidPolicy.get(uid, POLICY_NONE);
            if (oldPolicy != policy) {
                setUidPolicyUncheckedUL(uid, oldPolicy, policy, true);
            }
        } finally {
            Binder.restoreCallingIdentity(token);
        }
    }
}

继续调用 setUidPolicyUncheckedUL(uid, oldPolicy, policy, true);

private void setUidPolicyUncheckedUL(int uid, int oldPolicy, int policy, boolean persist) {
    setUidPolicyUncheckedUL(uid, policy, persist);
    //部分代码省略....
}

调用 setUidPolicyUncheckedUL(uid, policy, persist);

private void setUidPolicyUncheckedUL(int uid, int policy, boolean persist) {
    mUidPolicy.put(uid, policy);

    // uid policy changed, recompute rules and persist policy.
    updateRulesForDataUsageRestrictionsUL(uid);
    if (persist) {
        synchronized (mNetworkPoliciesSecondLock) {
            writePolicyAL();
        }
    }
}

这里需要注意的是, 此处通过mUidPolicy.put(uid, policy); 将策略存到了SparseIntArray中, 同时 writePolicyAL() 函数会将你设置的UidPolicy写到xml文件中, 这样重启后相关策略也能正常生效, xml文件路径为 /data/system/netpolicy.xml, 这个函数具体内容就不说明了, 我们接着看最主要的函数 updateRulesForDataUsageRestrictionsUL(uid);

private void updateRulesForDataUsageRestrictionsUL(int uid) {
    updateRulesForDataUsageRestrictionsUL(uid, false);
}

直接调用 updateRulesForDataUsageRestrictionsUL(uid, false);

private void updateRulesForDataUsageRestrictionsUL(int uid, boolean uidDeleted) {
    // 部分代码省略...
   // 获取本次设置的策略
    final int uidPolicy = mUidPolicy.get(uid, POLICY_NONE);
   // 获取之前的策略
    final int oldUidRules = mUidRules.get(uid, RULE_NONE);
    // 是不是后台应用, 可以将此处逻辑做修改以达到控制前台流量访问
    final boolean isForeground = isUidForegroundOnRestrictBackgroundUL(uid);
    // 用于判断加入黑名单还是白名单的标志位
    final boolean isBlacklisted = (uidPolicy & POLICY_REJECT_METERED_BACKGROUND) != 0;
    final boolean isWhitelisted = mRestrictBackgroundWhitelistUids.get(uid);
    final int oldRule = oldUidRules & MASK_METERED_NETWORKS;
    int newRule = RULE_NONE;
    // 根据相关判断逻辑得到最终策略组, RULE_REJECT_METERED 表示限制流量访问
    // First step: define the new rule based on user restrictions and foreground state.
    if (isForeground) {
        if (isBlacklisted || (mRestrictBackground && !isWhitelisted)) {
            newRule = RULE_TEMPORARY_ALLOW_METERED;
        } else if (isWhitelisted) {
            newRule = RULE_ALLOW_METERED;
        }
    } else {
        if (isBlacklisted) {
            newRule = RULE_REJECT_METERED;
        } else if (mRestrictBackground && isWhitelisted) {
            newRule = RULE_ALLOW_METERED;
        }
    }
    // 更新策略组
    final int newUidRules = newRule | (oldUidRules & MASK_ALL_NETWORKS);
    // 部分代码省略...
    if (newUidRules == RULE_NONE) {
        mUidRules.delete(uid);
    } else {
        mUidRules.put(uid, newUidRules);
    }

    // 判断要加入白名单还是黑名单
    // Second step: apply bw changes based on change of state.
    if (newRule != oldRule) {
        if ((newRule & RULE_TEMPORARY_ALLOW_METERED) != 0) {
            // Temporarily whitelist foreground app, removing from blacklist if necessary
            // (since bw_penalty_box prevails over bw_happy_box).

            setMeteredNetworkWhitelist(uid, true);
            // TODO: if statement below is used to avoid an unnecessary call to netd / iptables,
            // but ideally it should be just:
            //    setMeteredNetworkBlacklist(uid, isBlacklisted);
            if (isBlacklisted) {
                setMeteredNetworkBlacklist(uid, false);
            }
        } else if ((oldRule & RULE_TEMPORARY_ALLOW_METERED) != 0) {
            // Remove temporary whitelist from app that is not on foreground anymore.

            // TODO: if statements below are used to avoid unnecessary calls to netd / iptables,
            // but ideally they should be just:
            //    setMeteredNetworkWhitelist(uid, isWhitelisted);
            //    setMeteredNetworkBlacklist(uid, isBlacklisted);
            if (!isWhitelisted) {
                setMeteredNetworkWhitelist(uid, false);
            }
            if (isBlacklisted) {
                setMeteredNetworkBlacklist(uid, true);
            }
        } else if ((newRule & RULE_REJECT_METERED) != 0
                || (oldRule & RULE_REJECT_METERED) != 0) {
            // Flip state because app was explicitly added or removed to blacklist.
            setMeteredNetworkBlacklist(uid, isBlacklisted);
            if ((oldRule & RULE_REJECT_METERED) != 0 && isWhitelisted) {
                // Since blacklist prevails over whitelist, we need to handle the special case
                // where app is whitelisted and blacklisted at the same time (although such
                // scenario should be blocked by the UI), then blacklist is removed.
                setMeteredNetworkWhitelist(uid, isWhitelisted);
            }
        } else if ((newRule & RULE_ALLOW_METERED) != 0
                || (oldRule & RULE_ALLOW_METERED) != 0) {
            // Flip state because app was explicitly added or removed to whitelist.
            setMeteredNetworkWhitelist(uid, isWhitelisted);
        } else {
            // All scenarios should have been covered above.
           // 部分代码省略...
        }
        // 发送策略更新消息, 最终注册了相关事件的类会收到消息
        // Dispatch changed rule to existing listeners.
        mHandler.obtainMessage(MSG_RULES_CHANGED, uid, newUidRules).sendToTarget();
    }
}

这个就是最主要的逻辑控制函数, 基本逻辑我在注释中间的简单描述了, 总的来说, 就是根据是不是前台应用,以及是否要加入黑名单这两个点来更新当前策略组, 其中 RULE_REJECT_METERED策略表示不允许访问流量.
相关策略更新后, 最终控制网络访问权限的是在 ConnectivityService.java中,并且策略更新后, 会影响到DownloadProvider 中的一些逻辑, 这部分还有很多流程和控制逻辑, 我没有深入研究, 有兴趣的可以看看.

除了设置 RULE_REJECT_METERED 这个状态外, 还需将App加入到黑名单中, 通过调用函数 setMeteredNetworkBlacklist(uid, true); 来实现, 如果要从黑名单中移除, 则调用 setMeteredNetworkBlacklist(uid, false); 即可.

解决问题

通过上面流程, 我们已经知道如何限制前台应用的流量访问了, 即修改 updateRulesForDataUsageRestrictionsUL(int uid, boolean uidDeleted)isForeground = isUidForegroundOnRestrictBackgroundUL(uid);的逻辑判断, 你可以直接将 isForeground = false, 然后编译系统, 刷机, 然后关掉某个App的 后台数据开关,这样这个应用就无法访问流量数据了, 可以通过这个方法确定我们的分析是否正确, 亲测有效.

要想完整控制某个APP是否能使用流量, 我们只需控制 isForegroundisBlacklisted这两个布尔变量的值, 这样后面的逻辑你可以不用修改, 就能完成控制流量访问权限了, 当 isForeground = falseisBlacklisted = true, 策略就会变为 RULE_REJECT_METERED, 并且会调用 setMeteredNetworkBlacklist(uid, true); 这样就没法访问网络了.

具体实现方法有多种, 可以根据需求来进行定制, 最简单能想到的就有两种方法:

  1. 增加额外函数, 自己修改逻辑控制流程
  2. 增加策略组, 比如增加一个 RULE_REQUEST_DISABLE_MOBILE_TRAFFIC, 根据此策略来控制相关逻辑达到控制流量访问.

我自己的做法是直接在APP调用 mPolicyManager.setUidPolicy(RULE_REJECT_METERED), 然后在 updateRulesForDataUsageRestrictionsUL(int uid, boolean uidDeleted) 中, 判断如果uidPolicyRULE_REJECT_METERED, 就重置规则和相关标志位, 这种方式修改很少, 但并不推荐, 修改如下:

@@ -3054,6 +3054,15 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
                 newRule = RULE_ALLOW_METERED;
             }
         }
+        if ((uidPolicy & RULE_REJECT_METERED) != 0) {
+            newRule = RULE_REJECT_METERED;
+            isBlacklisted = true;
+            isWhitelisted = false;
+        }
         final int newUidRules = newRule | (oldUidRules & MASK_ALL_NETWORKS);

         if (LOGV) {

提供接口

NetworkPolicyManager 是个隐藏类, 标准SDK中是没有此类的, 因此调用主要分两种方式:

  1. 调用APP是通过Android源码方式编译, 则直接调用相关接口即可
  2. 调用APP是通过IDE编译的, 可以通过反射方式调用

注意: 不管哪种方式, 都需要APP是系统APP, 即在AndroidManifest.xml中加入android:sharedUserId="android.uid.system", 并且加入权限 , 否则接口调用会失败

反射调用方式如下:

public class NetworkPolicy {

    // NetworkPolicyManager.RULE_REJECT_METERED = 1 << 2
    private static final int RULE_REJECT_METERED = 1 << 2;

    private Object mPolicyMgr;

    public NetworkPolicy(Context context) {
        try {
            mPolicyMgr = Class.forName("android.net.NetworkPolicyManager")
                    .getDeclaredMethod("from", Context.class).invoke(null, context);
        } catch (ClassNotFoundException | NoSuchMethodException |
                InvocationTargetException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    public void disableMobileTraffic(int uid) {
        try {
            mPolicyMgr.getClass().getDeclaredMethod("setUidPolicy", int.class, int.class)
                    .invoke(mPolicyMgr, uid, RULE_REJECT_METERED);
        } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
            e.printStackTrace();
        }
    }

    public void enableMobileTraffic(int uid) {
        try {
            mPolicyMgr.getClass().getDeclaredMethod("removeUidPolicy", int.class, int.class)
                    .invoke(mPolicyMgr, uid, RULE_REJECT_METERED);
        } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
            e.printStackTrace();
        }
    }

    public boolean isMobileTrafficDisabled(int uid) {
        try {
            Object policy = mPolicyMgr.getClass().getDeclaredMethod("getUidPolicy", int.class)
                    .invoke(mPolicyMgr, uid);
            if (((int) policy) == RULE_REJECT_METERED) {
                return true;
            }
        } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
            e.printStackTrace();
        }
        return false;
    }
}

总结

NetworkPolicyManager.java 是Android中用来控制网络访问策略的管理类, 可通过APP 的 Uid 来设置相关策略, 目前系统中只实现了 POLICY_REJECT_METERED_BACKGROUND的功能,即限制后台应用数据访问, 我们可以在此基础上实现更多功能, NetworkPolicyManager只是用来管理策略, 相关策略会被存储到/data/system/netpolicy.xml文件中, 实际控制网络状态的是 ConnectivityService , 限制网络访问是通过底层实现的. 当NetworkPolicyManager中策略更改后, 会通知注册了回调函数的ConnectivityService, 这时被限制网络的App查询的网络状态处于BLOCK, 同时调用setMeteredNetworkBlacklist(uid, true);后, 底层会限制App实际的网络请求, 最终达到限制App网络访问的功能.

2018/12/19 更新: 需调用setMeteredNetworkBlacklist(uid, true)才能从底层驱动限制网络访问.

你可能感兴趣的:(Android系统添加流量控制开关(NetworkPolicyManager))