diff --git a/core/java/android/net/ITetheringStatsProvider.aidl b/core/java/android/net/ITetheringStatsProvider.aidl index 1aeabc1e62de2..da0bf4c47b94b 100644 --- a/core/java/android/net/ITetheringStatsProvider.aidl +++ b/core/java/android/net/ITetheringStatsProvider.aidl @@ -30,7 +30,10 @@ import android.net.NetworkStats; */ interface ITetheringStatsProvider { // Returns cumulative statistics for all tethering sessions since boot, on all upstreams. - NetworkStats getTetherStats(); + // @code {how} is one of the NetworkStats.STATS_PER_* constants. If {@code how} is + // {@code STATS_PER_IFACE}, the provider should not include any traffic that is already + // counted by kernel interface counters. + NetworkStats getTetherStats(int how); // Sets the interface quota for the specified upstream interface. This is defined as the number // of bytes, starting from zero and counting from now, after which data should stop being diff --git a/core/java/android/net/NetworkStats.java b/core/java/android/net/NetworkStats.java index 77ce65b0815a5..0780af602be2c 100644 --- a/core/java/android/net/NetworkStats.java +++ b/core/java/android/net/NetworkStats.java @@ -82,6 +82,11 @@ public class NetworkStats implements Parcelable { /** {@link #roaming} value where roaming data is accounted. */ public static final int ROAMING_YES = 1; + /** Denotes a request for stats at the interface level. */ + public static final int STATS_PER_IFACE = 0; + /** Denotes a request for stats at the interface and UID level. */ + public static final int STATS_PER_UID = 1; + // TODO: move fields to "mVariable" notation /** diff --git a/core/java/android/os/INetworkManagementService.aidl b/core/java/android/os/INetworkManagementService.aidl index 3de217494ac52..b63e3023565f3 100644 --- a/core/java/android/os/INetworkManagementService.aidl +++ b/core/java/android/os/INetworkManagementService.aidl @@ -219,6 +219,21 @@ interface INetworkManagementService */ void unregisterTetheringStatsProvider(ITetheringStatsProvider provider); + /** + * Reports that a tethering provider has reached a data limit. + * + * Currently triggers a global alert, which causes NetworkStatsService to poll counters and + * re-evaluate data usage. + * + * This does not take an interface name because: + * 1. The tethering offload stats provider cannot reliably determine the interface on which the + * limit was reached, because the HAL does not provide it. + * 2. Firing an interface-specific alert instead of a global alert isn't really useful since in + * all cases of interest, the system responds to both in the same way - it polls stats, and + * then notifies NetworkPolicyManagerService of the fact. + */ + void tetherLimitReached(ITetheringStatsProvider provider); + /** ** PPPD **/ @@ -266,7 +281,7 @@ interface INetworkManagementService /** * Return summary of network statistics all tethering interfaces. */ - NetworkStats getNetworkStatsTethering(); + NetworkStats getNetworkStatsTethering(int how); /** * Set quota for an interface. diff --git a/services/core/java/com/android/server/NetworkManagementService.java b/services/core/java/com/android/server/NetworkManagementService.java index e536e8d1eb0a4..f3e8b82199664 100644 --- a/services/core/java/com/android/server/NetworkManagementService.java +++ b/services/core/java/com/android/server/NetworkManagementService.java @@ -34,6 +34,7 @@ import static android.net.NetworkPolicyManager.FIREWALL_RULE_DENY; import static android.net.NetworkPolicyManager.FIREWALL_TYPE_BLACKLIST; import static android.net.NetworkPolicyManager.FIREWALL_TYPE_WHITELIST; import static android.net.NetworkStats.SET_DEFAULT; +import static android.net.NetworkStats.STATS_PER_UID; import static android.net.NetworkStats.TAG_ALL; import static android.net.NetworkStats.TAG_NONE; import static android.net.NetworkStats.UID_ALL; @@ -550,6 +551,18 @@ public class NetworkManagementService extends INetworkManagementService.Stub } } + @Override + public void tetherLimitReached(ITetheringStatsProvider provider) { + mContext.enforceCallingOrSelfPermission(NETWORK_STACK, TAG); + synchronized(mTetheringStatsProviders) { + if (!mTetheringStatsProviders.containsKey(provider)) { + return; + } + // No current code examines the interface parameter in a global alert. Just pass null. + notifyLimitReached(LIMIT_GLOBAL_ALERT, null); + } + } + // Sync the state of the given chain with the native daemon. private void syncFirewallChainLocked(int chain, String name) { SparseIntArray rules; @@ -1851,7 +1864,13 @@ public class NetworkManagementService extends INetworkManagementService.Stub private class NetdTetheringStatsProvider extends ITetheringStatsProvider.Stub { @Override - public NetworkStats getTetherStats() { + public NetworkStats getTetherStats(int how) { + // We only need to return per-UID stats. Per-device stats are already counted by + // interface counters. + if (how != STATS_PER_UID) { + return new NetworkStats(SystemClock.elapsedRealtime(), 0); + } + final NativeDaemonEvent[] events; try { events = mConnector.executeForList("bandwidth", "gettetherstats"); @@ -1894,14 +1913,14 @@ public class NetworkManagementService extends INetworkManagementService.Stub } @Override - public NetworkStats getNetworkStatsTethering() { + public NetworkStats getNetworkStatsTethering(int how) { mContext.enforceCallingOrSelfPermission(CONNECTIVITY_INTERNAL, TAG); final NetworkStats stats = new NetworkStats(SystemClock.elapsedRealtime(), 1); synchronized (mTetheringStatsProviders) { for (ITetheringStatsProvider provider: mTetheringStatsProviders.keySet()) { try { - stats.combineAllValues(provider.getTetherStats()); + stats.combineAllValues(provider.getTetherStats(how)); } catch (RemoteException e) { Log.e(TAG, "Problem reading tethering stats from " + mTetheringStatsProviders.get(provider) + ": " + e); diff --git a/services/core/java/com/android/server/connectivity/tethering/OffloadController.java b/services/core/java/com/android/server/connectivity/tethering/OffloadController.java index 55e290a4215e2..4393e3527e4d2 100644 --- a/services/core/java/com/android/server/connectivity/tethering/OffloadController.java +++ b/services/core/java/com/android/server/connectivity/tethering/OffloadController.java @@ -17,7 +17,9 @@ package com.android.server.connectivity.tethering; import static android.net.NetworkStats.SET_DEFAULT; +import static android.net.NetworkStats.STATS_PER_UID; import static android.net.NetworkStats.TAG_NONE; +import static android.net.NetworkStats.UID_ALL; import static android.net.TrafficStats.UID_TETHERING; import static android.provider.Settings.Global.TETHER_OFFLOAD_DISABLED; @@ -60,6 +62,8 @@ public class OffloadController { private final Handler mHandler; private final OffloadHardwareInterface mHwInterface; private final ContentResolver mContentResolver; + private final INetworkManagementService mNms; + private final ITetheringStatsProvider mStatsProvider; private final SharedLog mLog; private boolean mConfigInitialized; private boolean mControlInitialized; @@ -89,13 +93,14 @@ public class OffloadController { mHandler = h; mHwInterface = hwi; mContentResolver = contentResolver; + mNms = nms; + mStatsProvider = new OffloadTetheringStatsProvider(); mLog = log.forSubComponent(TAG); mExemptPrefixes = new HashSet<>(); mLastLocalPrefixStrs = new HashSet<>(); try { - nms.registerTetheringStatsProvider( - new OffloadTetheringStatsProvider(), getClass().getSimpleName()); + mNms.registerTetheringStatsProvider(mStatsProvider, getClass().getSimpleName()); } catch (RemoteException e) { mLog.e("Cannot register offload stats provider: " + e); } @@ -150,7 +155,26 @@ public class OffloadController { @Override public void onStoppedLimitReached() { mLog.log("onStoppedLimitReached"); - // Poll for statistics and notify NetworkStats + + // We cannot reliably determine on which interface the limit was reached, + // because the HAL interface does not specify it. We cannot just use the + // current upstream, because that might have changed since the time that + // the HAL queued the callback. + // TODO: rev the HAL so that it provides an interface name. + + // Fetch current stats, so that when our notification reaches + // NetworkStatsService and triggers a poll, we will respond with + // current data (which will be above the limit that was reached). + // Note that if we just changed upstream, this is unnecessary but harmless. + // The stats for the previous upstream were already updated on this thread + // just after the upstream was changed, so they are also up-to-date. + updateStatsForCurrentUpstream(); + + try { + mNms.tetherLimitReached(mStatsProvider); + } catch (RemoteException e) { + mLog.e("Cannot report data limit reached: " + e); + } } @Override @@ -180,16 +204,18 @@ public class OffloadController { private class OffloadTetheringStatsProvider extends ITetheringStatsProvider.Stub { @Override - public NetworkStats getTetherStats() { + public NetworkStats getTetherStats(int how) { NetworkStats stats = new NetworkStats(SystemClock.elapsedRealtime(), 0); // We can't just post to mHandler because we are mostly (but not always) called by // NetworkStatsService#performPollLocked, which is (currently) on the same thread as us. mHandler.runWithScissors(() -> { + // We have to report both per-interface and per-UID stats, because offloaded traffic + // is not seen by kernel interface counters. NetworkStats.Entry entry = new NetworkStats.Entry(); entry.set = SET_DEFAULT; entry.tag = TAG_NONE; - entry.uid = UID_TETHERING; + entry.uid = (how == STATS_PER_UID) ? UID_TETHERING : UID_ALL; updateStatsForCurrentUpstream(); diff --git a/services/core/java/com/android/server/net/NetworkStatsService.java b/services/core/java/com/android/server/net/NetworkStatsService.java index 8d46351e98458..3ad2dcad7d7fd 100644 --- a/services/core/java/com/android/server/net/NetworkStatsService.java +++ b/services/core/java/com/android/server/net/NetworkStatsService.java @@ -31,6 +31,8 @@ import static android.net.NetworkStats.IFACE_ALL; import static android.net.NetworkStats.SET_ALL; import static android.net.NetworkStats.SET_DEFAULT; import static android.net.NetworkStats.SET_FOREGROUND; +import static android.net.NetworkStats.STATS_PER_IFACE; +import static android.net.NetworkStats.STATS_PER_UID; import static android.net.NetworkStats.TAG_NONE; import static android.net.NetworkStats.UID_ALL; import static android.net.NetworkTemplate.buildTemplateMobileWildcard; @@ -1042,6 +1044,11 @@ public class NetworkStatsService extends INetworkStatsService.Stub { final NetworkStats xtSnapshot = getNetworkStatsXt(); final NetworkStats devSnapshot = mNetworkManager.getNetworkStatsSummaryDev(); + // Tethering snapshot for dev and xt stats. Counts per-interface data from tethering stats + // providers that isn't already counted by dev and XT stats. + final NetworkStats tetherSnapshot = getNetworkStatsTethering(STATS_PER_IFACE); + xtSnapshot.combineAllValues(tetherSnapshot); + devSnapshot.combineAllValues(tetherSnapshot); // For xt/dev, we pass a null VPN array because usage is aggregated by UID, so VPN traffic // can't be reattributed to responsible apps. @@ -1372,14 +1379,14 @@ public class NetworkStatsService extends INetworkStatsService.Stub { final NetworkStats uidSnapshot = mNetworkManager.getNetworkStatsUidDetail(UID_ALL); // fold tethering stats and operations into uid snapshot - final NetworkStats tetherSnapshot = getNetworkStatsTethering(); + final NetworkStats tetherSnapshot = getNetworkStatsTethering(STATS_PER_UID); uidSnapshot.combineAllValues(tetherSnapshot); final TelephonyManager telephonyManager = (TelephonyManager) mContext.getSystemService( Context.TELEPHONY_SERVICE); // fold video calling data usage stats into uid snapshot - final NetworkStats vtStats = telephonyManager.getVtDataUsage(true); + final NetworkStats vtStats = telephonyManager.getVtDataUsage(STATS_PER_UID); if (vtStats != null) { uidSnapshot.combineAllValues(vtStats); } @@ -1398,7 +1405,7 @@ public class NetworkStatsService extends INetworkStatsService.Stub { Context.TELEPHONY_SERVICE); // Merge video calling data usage into XT - final NetworkStats vtSnapshot = telephonyManager.getVtDataUsage(false); + final NetworkStats vtSnapshot = telephonyManager.getVtDataUsage(STATS_PER_IFACE); if (vtSnapshot != null) { xtSnapshot.combineAllValues(vtSnapshot); } @@ -1410,9 +1417,9 @@ public class NetworkStatsService extends INetworkStatsService.Stub { * Return snapshot of current tethering statistics. Will return empty * {@link NetworkStats} if any problems are encountered. */ - private NetworkStats getNetworkStatsTethering() throws RemoteException { + private NetworkStats getNetworkStatsTethering(int how) throws RemoteException { try { - return mNetworkManager.getNetworkStatsTethering(); + return mNetworkManager.getNetworkStatsTethering(how); } catch (IllegalStateException e) { Log.wtf(TAG, "problem reading network stats", e); return new NetworkStats(0L, 10); diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index e7722542fdcbb..ab679ff69d11c 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -6649,11 +6649,13 @@ public class TelephonyManager { * Get aggregated video call data usage since boot. * Permissions android.Manifest.permission.READ_NETWORK_USAGE_HISTORY is required. * - * @param perUidStats True if requesting data usage per uid, otherwise overall usage. + * @param how one of the NetworkStats.STATS_PER_* constants depending on whether the request is + * for data usage per uid or overall usage. * @return Snapshot of video call data usage * @hide */ - public NetworkStats getVtDataUsage(boolean perUidStats) { + public NetworkStats getVtDataUsage(int how) { + boolean perUidStats = (how == NetworkStats.STATS_PER_UID); try { ITelephony service = getITelephony(); if (service != null) { diff --git a/tests/net/java/com/android/server/connectivity/tethering/OffloadControllerTest.java b/tests/net/java/com/android/server/connectivity/tethering/OffloadControllerTest.java index d29a94b9e0660..360e5f6ead099 100644 --- a/tests/net/java/com/android/server/connectivity/tethering/OffloadControllerTest.java +++ b/tests/net/java/com/android/server/connectivity/tethering/OffloadControllerTest.java @@ -17,7 +17,10 @@ package com.android.server.connectivity.tethering; import static android.net.NetworkStats.SET_DEFAULT; +import static android.net.NetworkStats.STATS_PER_IFACE; +import static android.net.NetworkStats.STATS_PER_UID; import static android.net.NetworkStats.TAG_NONE; +import static android.net.NetworkStats.UID_ALL; import static android.net.TrafficStats.UID_TETHERING; import static android.provider.Settings.Global.TETHER_OFFLOAD_DISABLED; import static com.android.server.connectivity.tethering.OffloadHardwareInterface.ForwardedStats; @@ -85,6 +88,8 @@ public class OffloadControllerTest { ArgumentCaptor.forClass(ArrayList.class); private final ArgumentCaptor mTetherStatsProviderCaptor = ArgumentCaptor.forClass(ITetheringStatsProvider.Stub.class); + private final ArgumentCaptor mControlCallbackCaptor = + ArgumentCaptor.forClass(OffloadHardwareInterface.ControlCallback.class); private MockContentResolver mContentResolver; @Before public void setUp() { @@ -105,7 +110,7 @@ public class OffloadControllerTest { private void setupFunctioningHardwareInterface() { when(mHardware.initOffloadConfig()).thenReturn(true); - when(mHardware.initOffloadControl(any(OffloadHardwareInterface.ControlCallback.class))) + when(mHardware.initOffloadControl(mControlCallbackCaptor.capture())) .thenReturn(true); when(mHardware.getForwardedStats(any())).thenReturn(new ForwardedStats()); } @@ -375,7 +380,6 @@ public class OffloadControllerTest { assertEquals(stats.txBytes, entry.txBytes); assertEquals(SET_DEFAULT, entry.set); assertEquals(TAG_NONE, entry.tag); - assertEquals(UID_TETHERING, entry.uid); } @Test @@ -414,20 +418,33 @@ public class OffloadControllerTest { ethernetStats.txBytes = 100000; offload.setUpstreamLinkProperties(null); - NetworkStats stats = mTetherStatsProviderCaptor.getValue().getTetherStats(); + ITetheringStatsProvider provider = mTetherStatsProviderCaptor.getValue(); + NetworkStats stats = provider.getTetherStats(STATS_PER_IFACE); + NetworkStats perUidStats = provider.getTetherStats(STATS_PER_UID); + assertEquals(2, stats.size()); + assertEquals(2, perUidStats.size()); NetworkStats.Entry entry = null; + for (int i = 0; i < stats.size(); i++) { + assertEquals(UID_ALL, stats.getValues(i, entry).uid); + assertEquals(UID_TETHERING, perUidStats.getValues(i, entry).uid); + } + int ethernetPosition = ethernetIface.equals(stats.getValues(0, entry).iface) ? 0 : 1; int mobilePosition = 1 - ethernetPosition; entry = stats.getValues(mobilePosition, entry); assertNetworkStats(mobileIface, mobileStats, entry); + entry = perUidStats.getValues(mobilePosition, entry); + assertNetworkStats(mobileIface, mobileStats, entry); ethernetStats.rxBytes = 12345 + 100000; ethernetStats.txBytes = 54321 + 100000; entry = stats.getValues(ethernetPosition, entry); assertNetworkStats(ethernetIface, ethernetStats, entry); + entry = perUidStats.getValues(ethernetPosition, entry); + assertNetworkStats(ethernetIface, ethernetStats, entry); } @Test @@ -493,4 +510,17 @@ public class OffloadControllerTest { waitForIdle(); inOrder.verify(mHardware).stopOffloadControl(); } + + @Test + public void testDataLimitCallback() throws Exception { + setupFunctioningHardwareInterface(); + enableOffload(); + + final OffloadController offload = makeOffloadController(); + offload.start(); + + OffloadHardwareInterface.ControlCallback callback = mControlCallbackCaptor.getValue(); + callback.onStoppedLimitReached(); + verify(mNMService, times(1)).tetherLimitReached(mTetherStatsProviderCaptor.getValue()); + } } diff --git a/tests/net/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/net/java/com/android/server/net/NetworkStatsServiceTest.java index feb46d39b5633..fa997958ba6de 100644 --- a/tests/net/java/com/android/server/net/NetworkStatsServiceTest.java +++ b/tests/net/java/com/android/server/net/NetworkStatsServiceTest.java @@ -31,6 +31,8 @@ import static android.net.NetworkStats.ROAMING_YES; import static android.net.NetworkStats.SET_ALL; import static android.net.NetworkStats.SET_DEFAULT; import static android.net.NetworkStats.SET_FOREGROUND; +import static android.net.NetworkStats.STATS_PER_IFACE; +import static android.net.NetworkStats.STATS_PER_UID; import static android.net.NetworkStats.TAG_NONE; import static android.net.NetworkStats.UID_ALL; import static android.net.NetworkStatsHistory.FIELD_ALL; @@ -823,17 +825,24 @@ public class NetworkStatsServiceTest { incrementCurrentTime(HOUR_IN_MILLIS); expectCurrentTime(); expectDefaultSettings(); - expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1) - .addIfaceValues(TEST_IFACE, 2048L, 16L, 512L, 4L)); + // Traffic seen by kernel counters (includes software tethering). + final NetworkStats ifaceStats = new NetworkStats(getElapsedRealtime(), 1) + .addIfaceValues(TEST_IFACE, 1536L, 12L, 384L, 3L); + // Hardware tethering traffic, not seen by kernel counters. + final NetworkStats tetherStatsHardware = new NetworkStats(getElapsedRealtime(), 1) + .addIfaceValues(TEST_IFACE, 512L, 4L, 128L, 1L); + + // Traffic for UID_RED. final NetworkStats uidStats = new NetworkStats(getElapsedRealtime(), 1) .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 128L, 2L, 128L, 2L, 0L); - final String[] tetherIfacePairs = new String[] { TEST_IFACE, "wlan0" }; + // All tethering traffic, both hardware and software. final NetworkStats tetherStats = new NetworkStats(getElapsedRealtime(), 1) .addValues(TEST_IFACE, UID_TETHERING, SET_DEFAULT, TAG_NONE, 1920L, 14L, 384L, 2L, 0L); - expectNetworkStatsUidDetail(uidStats, tetherIfacePairs, tetherStats); + expectNetworkStatsSummary(ifaceStats, tetherStatsHardware); + expectNetworkStatsUidDetail(uidStats, tetherStats); forcePollAndWaitForIdle(); // verify service recorded history @@ -1013,10 +1022,16 @@ public class NetworkStatsServiceTest { } private void expectNetworkStatsSummary(NetworkStats summary) throws Exception { + expectNetworkStatsSummary(summary, new NetworkStats(0L, 0)); + } + + private void expectNetworkStatsSummary(NetworkStats summary, NetworkStats tetherStats) + throws Exception { when(mConnManager.getAllVpnInfo()).thenReturn(new VpnInfo[0]); - expectNetworkStatsSummaryDev(summary); - expectNetworkStatsSummaryXt(summary); + expectNetworkStatsTethering(STATS_PER_IFACE, tetherStats); + expectNetworkStatsSummaryDev(summary.clone()); + expectNetworkStatsSummaryXt(summary.clone()); } private void expectNetworkStatsSummaryDev(NetworkStats summary) throws Exception { @@ -1027,17 +1042,21 @@ public class NetworkStatsServiceTest { when(mNetManager.getNetworkStatsSummaryXt()).thenReturn(summary); } - private void expectNetworkStatsUidDetail(NetworkStats detail) throws Exception { - expectNetworkStatsUidDetail(detail, new String[0], new NetworkStats(0L, 0)); + private void expectNetworkStatsTethering(int how, NetworkStats stats) + throws Exception { + when(mNetManager.getNetworkStatsTethering(how)).thenReturn(stats); } - private void expectNetworkStatsUidDetail( - NetworkStats detail, String[] tetherIfacePairs, NetworkStats tetherStats) + private void expectNetworkStatsUidDetail(NetworkStats detail) throws Exception { + expectNetworkStatsUidDetail(detail, new NetworkStats(0L, 0)); + } + + private void expectNetworkStatsUidDetail(NetworkStats detail, NetworkStats tetherStats) throws Exception { when(mNetManager.getNetworkStatsUidDetail(UID_ALL)).thenReturn(detail); // also include tethering details, since they are folded into UID - when(mNetManager.getNetworkStatsTethering()).thenReturn(tetherStats); + when(mNetManager.getNetworkStatsTethering(STATS_PER_UID)).thenReturn(tetherStats); } private void expectDefaultSettings() throws Exception {