Android系統添加流量控制開關(NetworkPolicyManager)

背景

最近產品那邊有個需求是需要有個系統接口, 用來控制第三方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 中的一些邏輯, 這部分還有很多流程和控制邏輯, 我沒有深入研究, 有興趣的可以看看.

解決問題

通過上面流程, 我們已經知道如何限制前臺應用的流量訪問了, 即修改 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的網絡訪問.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章