背景
最近产品那边有个需求是需要有个系统接口, 用来控制第三方APP的流量访问权限, 即你可以单独关闭某一个APP的流量访问权限(WIFI下不影响), 本篇文章就是记录我解决这个问题的流程, 主要说明如何在自己对相关模块不熟悉的情况下, 分析并解决问题.
注: 本文中源码均为高通平台, Android 7.1代码
分析思路
首先如果系统没有这方面的功能或者接口的话, 光靠自己去实现难度有点大 , 因为你得对整个网络访问流程很熟悉., 我自己是没有这个模块的开发经验的, 所以只能先看看系统中有没有类似的功能. 很庆幸的是, 刚好有个类似的功能, 在Android原生设置界面里面, 有个 应用数据流量 界面, 打开方式如下:
设置 -> 应用程序 -> 应用程序信息(点击任何一个app) -> 数据使用
界面内容如下:
可以看到, 对于每个应用, 都有 允许在后台使用移动数据流量 的开关选项, 这个只能控制后台应用的数据访问权限, 既然能控制后台应用, 前台应用自然不是问题, 看到这里基本就不慌了, 找到关键点了, 接下来就是根据这个信息阅读源码, 查看流程了.
后台数据访问控制流程
首先得把控制后台数据访问流程弄清楚, 才知道怎么添加前台数据控制接口.
应用数据流量这个界面, 对应的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是否能使用流量, 我们只需控制 isForeground
和 isBlacklisted
这两个布尔变量的值, 这样后面的逻辑你可以不用修改, 就能完成控制流量访问权限了, 当 isForeground = false
和 isBlacklisted = true
, 策略就会变为 RULE_REJECT_METERED
, 并且会调用 setMeteredNetworkBlacklist(uid, true);
这样就没法访问网络了.
具体实现方法有多种, 可以根据需求来进行定制, 最简单能想到的就有两种方法:
- 增加额外函数, 自己修改逻辑控制流程
- 增加策略组, 比如增加一个 RULE_REQUEST_DISABLE_MOBILE_TRAFFIC, 根据此策略来控制相关逻辑达到控制流量访问.
我自己的做法是直接在APP调用 mPolicyManager.setUidPolicy(RULE_REJECT_METERED)
, 然后在 updateRulesForDataUsageRestrictionsUL(int uid, boolean uidDeleted)
中, 判断如果uidPolicy
是RULE_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中是没有此类的, 因此调用主要分两种方式:
- 调用APP是通过Android源码方式编译, 则直接调用相关接口即可
- 调用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)才能从底层驱动限制网络访问.