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

背景

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

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

分析思路

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

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

界面内容如下:


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", 并且加入权限 <uses-permission android:name="android.permission.MANAGE_NETWORK_POLICY" />, 否则接口调用会失败

反射调用方式如下:

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)才能从底层驱动限制网络访问.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,392评论 25 707
  • 持续分享294天,2018年5月3日,张红。 今天作为咨询师进行约练。其实南方这个问题也是我当下的问题,所以刚开始...
    啊呦a7_94阅读 184评论 0 0
  • 2700年前,世界上几个古老的文明国家,都呈现出了极为灿烂的文化,各国杰出的学者、思想家纷纷涌现。在古希腊...
    flh1972831阅读 704评论 0 1
  • 今早接了学生时代一个关系甚好朋友的电话,电话里她说终于和自己喜欢N久的男生分手了,我有些诧异,既然那么喜欢对方为什...
    陌夏微凉阅读 576评论 0 0
  • 1 其实我们都一样,同活在一个世界,生活在同一个地球,每天早晨起来,看到的也是同一个太阳。每天日升日落、云...
    云天雨阅读 202评论 0 0