美文网首页Android开发爱上AndroidAndroid
Android系统添加流量控制开关(NetworkPolicyM

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

作者: 幽客 | 来源:发表于2018-08-04 11:45 被阅读26次

    背景

    最近产品那边有个需求是需要有个系统接口, 用来控制第三方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 中的一些逻辑, 这部分还有很多流程和控制逻辑, 我没有深入研究, 有兴趣的可以看看.

    解决问题

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

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

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

    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是Android中用来控制网络访问策略的管理类, 可通过APP 的 Uid 来设置相关策略, 目前系统中只实现了 POLICY_REJECT_METERED_BACKGROUND的功能,即限制后台应用数据访问, 我们可以在此基础上实现更多功能, NetworkPolicyManager只是用来管理策略, 相关策略会被存储到/data/system/netpolicy.xml文件中, 实际限制网络访问的是 ConnectivityService, 当NetworkPolicyManager中策略更改后, 会通知注册了回调函数的ConnectivityService, 最终 ConnectivityService 根据APP的 uid 和当前策略, 限制单个App的网络访问.

    相关文章

      网友评论

        本文标题:Android系统添加流量控制开关(NetworkPolicyM

        本文链接:https://www.haomeiwen.com/subject/mfluvftx.html