diff --git a/core/java/android/net/NetworkStats.java b/core/java/android/net/NetworkStats.java index 844d05564b2cb..fb7a4f8ad216b 100644 --- a/core/java/android/net/NetworkStats.java +++ b/core/java/android/net/NetworkStats.java @@ -111,6 +111,14 @@ public class NetworkStats implements Parcelable { && operations == 0; } + public void add(Entry another) { + this.rxBytes += another.rxBytes; + this.rxPackets += another.rxPackets; + this.txBytes += another.txBytes; + this.txPackets += another.txPackets; + this.operations += another.operations; + } + @Override public String toString() { final StringBuilder builder = new StringBuilder(); diff --git a/core/java/android/net/NetworkStatsHistory.java b/core/java/android/net/NetworkStatsHistory.java index 0003c6e3d6ae0..a37c26f90d73e 100644 --- a/core/java/android/net/NetworkStatsHistory.java +++ b/core/java/android/net/NetworkStatsHistory.java @@ -342,11 +342,23 @@ public class NetworkStatsHistory implements Parcelable { * for combining together stats for external reporting. */ public void recordEntireHistory(NetworkStatsHistory input) { + recordHistory(input, Long.MIN_VALUE, Long.MAX_VALUE); + } + + /** + * Record given {@link NetworkStatsHistory} into this history, copying only + * buckets that atomically occur in the inclusive time range. Doesn't + * interpolate across partial buckets. + */ + public void recordHistory(NetworkStatsHistory input, long start, long end) { final NetworkStats.Entry entry = new NetworkStats.Entry( IFACE_ALL, UID_ALL, SET_DEFAULT, TAG_NONE, 0L, 0L, 0L, 0L, 0L); for (int i = 0; i < input.bucketCount; i++) { - final long start = input.bucketStart[i]; - final long end = start + input.bucketDuration; + final long bucketStart = input.bucketStart[i]; + final long bucketEnd = bucketStart + input.bucketDuration; + + // skip when bucket is outside requested range + if (bucketStart < start || bucketEnd > end) continue; entry.rxBytes = getLong(input.rxBytes, i, 0L); entry.rxPackets = getLong(input.rxPackets, i, 0L); @@ -354,7 +366,7 @@ public class NetworkStatsHistory implements Parcelable { entry.txPackets = getLong(input.txPackets, i, 0L); entry.operations = getLong(input.operations, i, 0L); - recordData(start, end, entry); + recordData(bucketStart, bucketEnd, entry); } } diff --git a/core/java/android/net/NetworkTemplate.java b/core/java/android/net/NetworkTemplate.java index 39a4d7bb0d8f4..d8e53d523f074 100644 --- a/core/java/android/net/NetworkTemplate.java +++ b/core/java/android/net/NetworkTemplate.java @@ -61,6 +61,13 @@ public class NetworkTemplate implements Parcelable { com.android.internal.R.array.config_data_usage_network_types); } + private static boolean sForceAllNetworkTypes = false; + + // @VisibleForTesting + public static void forceAllNetworkTypes() { + sForceAllNetworkTypes = true; + } + /** * Template to match {@link ConnectivityManager#TYPE_MOBILE} networks with * the given IMSI. @@ -225,7 +232,7 @@ public class NetworkTemplate implements Parcelable { // TODO: consider matching against WiMAX subscriber identity return true; } else { - return (contains(DATA_USAGE_NETWORK_TYPES, ident.mType) + return ((sForceAllNetworkTypes || contains(DATA_USAGE_NETWORK_TYPES, ident.mType)) && Objects.equal(mSubscriberId, ident.mSubscriberId)); } } @@ -291,7 +298,7 @@ public class NetworkTemplate implements Parcelable { if (ident.mType == TYPE_WIMAX) { return true; } else { - return contains(DATA_USAGE_NETWORK_TYPES, ident.mType); + return sForceAllNetworkTypes || contains(DATA_USAGE_NETWORK_TYPES, ident.mType); } } diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index ea3cab44477c7..a0641d5db1b61 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -4210,6 +4210,8 @@ public final class Settings { public static final String NETSTATS_GLOBAL_ALERT_BYTES = "netstats_global_alert_bytes"; /** {@hide} */ public static final String NETSTATS_SAMPLE_ENABLED = "netstats_sample_enabled"; + /** {@hide} */ + public static final String NETSTATS_REPORT_XT_OVER_DEV = "netstats_report_xt_over_dev"; /** {@hide} */ public static final String NETSTATS_DEV_BUCKET_DURATION = "netstats_dev_bucket_duration"; diff --git a/services/java/com/android/server/net/NetworkStatsCollection.java b/services/java/com/android/server/net/NetworkStatsCollection.java index c2e475a5b4ead..9ddf011069147 100644 --- a/services/java/com/android/server/net/NetworkStatsCollection.java +++ b/services/java/com/android/server/net/NetworkStatsCollection.java @@ -71,7 +71,7 @@ public class NetworkStatsCollection implements FileRotator.Reader { private HashMap mStats = Maps.newHashMap(); - private long mBucketDuration; + private final long mBucketDuration; private long mStartMillis; private long mEndMillis; @@ -95,6 +95,18 @@ public class NetworkStatsCollection implements FileRotator.Reader { return mStartMillis; } + /** + * Return first atomic bucket in this collection, which is more conservative + * than {@link #mStartMillis}. + */ + public long getFirstAtomicBucketMillis() { + if (mStartMillis == Long.MAX_VALUE) { + return Long.MAX_VALUE; + } else { + return mStartMillis + mBucketDuration; + } + } + public long getEndMillis() { return mEndMillis; } @@ -121,6 +133,15 @@ public class NetworkStatsCollection implements FileRotator.Reader { */ public NetworkStatsHistory getHistory( NetworkTemplate template, int uid, int set, int tag, int fields) { + return getHistory(template, uid, set, tag, fields, Long.MIN_VALUE, Long.MAX_VALUE); + } + + /** + * Combine all {@link NetworkStatsHistory} in this collection which match + * the requested parameters. + */ + public NetworkStatsHistory getHistory( + NetworkTemplate template, int uid, int set, int tag, int fields, long start, long end) { final NetworkStatsHistory combined = new NetworkStatsHistory( mBucketDuration, estimateBuckets(), fields); for (Map.Entry entry : mStats.entrySet()) { @@ -128,7 +149,7 @@ public class NetworkStatsCollection implements FileRotator.Reader { final boolean setMatches = set == SET_ALL || key.set == set; if (key.uid == uid && setMatches && key.tag == tag && templateMatches(template, key.ident)) { - combined.recordEntireHistory(entry.getValue()); + combined.recordHistory(entry.getValue(), start, end); } } return combined; @@ -145,6 +166,9 @@ public class NetworkStatsCollection implements FileRotator.Reader { final NetworkStats.Entry entry = new NetworkStats.Entry(); NetworkStatsHistory.Entry historyEntry = null; + // shortcut when we know stats will be empty + if (start == end) return stats; + for (Map.Entry mapEntry : mStats.entrySet()) { final Key key = mapEntry.getKey(); if (templateMatches(template, key.ident)) { @@ -175,8 +199,9 @@ public class NetworkStatsCollection implements FileRotator.Reader { */ public void recordData(NetworkIdentitySet ident, int uid, int set, int tag, long start, long end, NetworkStats.Entry entry) { - noteRecordedHistory(start, end, entry.rxBytes + entry.txBytes); - findOrCreateHistory(ident, uid, set, tag).recordData(start, end, entry); + final NetworkStatsHistory history = findOrCreateHistory(ident, uid, set, tag); + history.recordData(start, end, entry); + noteRecordedHistory(history.getStart(), history.getEnd(), entry.rxBytes + entry.txBytes); } /** diff --git a/services/java/com/android/server/net/NetworkStatsService.java b/services/java/com/android/server/net/NetworkStatsService.java index 0e93b0affc6b6..ba122ecdf2317 100644 --- a/services/java/com/android/server/net/NetworkStatsService.java +++ b/services/java/com/android/server/net/NetworkStatsService.java @@ -44,6 +44,7 @@ import static android.provider.Settings.Secure.NETSTATS_DEV_PERSIST_BYTES; import static android.provider.Settings.Secure.NETSTATS_DEV_ROTATE_AGE; import static android.provider.Settings.Secure.NETSTATS_GLOBAL_ALERT_BYTES; import static android.provider.Settings.Secure.NETSTATS_POLL_INTERVAL; +import static android.provider.Settings.Secure.NETSTATS_REPORT_XT_OVER_DEV; import static android.provider.Settings.Secure.NETSTATS_SAMPLE_ENABLED; import static android.provider.Settings.Secure.NETSTATS_TIME_CACHE_MAX_AGE; import static android.provider.Settings.Secure.NETSTATS_UID_BUCKET_DURATION; @@ -177,6 +178,7 @@ public class NetworkStatsService extends INetworkStatsService.Stub { public long getPollInterval(); public long getTimeCacheMaxAge(); public boolean getSampleEnabled(); + public boolean getReportXtOverDev(); public static class Config { public final long bucketDuration; @@ -221,6 +223,8 @@ public class NetworkStatsService extends INetworkStatsService.Stub { /** Cached {@link #mDevRecorder} stats. */ private NetworkStatsCollection mDevStatsCached; + /** Cached {@link #mXtRecorder} stats. */ + private NetworkStatsCollection mXtStatsCached; /** Current counter sets for each UID. */ private SparseIntArray mActiveUidCounterSet = new SparseIntArray(); @@ -295,6 +299,7 @@ public class NetworkStatsService extends INetworkStatsService.Stub { // read historical network stats from disk, since policy service // might need them right away. mDevStatsCached = mDevRecorder.getOrLoadCompleteLocked(); + mXtStatsCached = mXtRecorder.getOrLoadCompleteLocked(); // bootstrap initial stats to prevent double-counting later bootstrapStatsLocked(); @@ -371,6 +376,7 @@ public class NetworkStatsService extends INetworkStatsService.Stub { mUidTagRecorder = null; mDevStatsCached = null; + mXtStatsCached = null; mSystemReady = false; } @@ -469,12 +475,12 @@ public class NetworkStatsService extends INetworkStatsService.Stub { @Override public NetworkStats getSummaryForNetwork( NetworkTemplate template, long start, long end) { - return mDevStatsCached.getSummary(template, start, end); + return internalGetSummaryForNetwork(template, start, end); } @Override public NetworkStatsHistory getHistoryForNetwork(NetworkTemplate template, int fields) { - return mDevStatsCached.getHistory(template, UID_ALL, SET_ALL, TAG_NONE, fields); + return internalGetHistoryForNetwork(template, fields); } @Override @@ -507,11 +513,56 @@ public class NetworkStatsService extends INetworkStatsService.Stub { }; } + /** + * Return network summary, splicing between {@link #mDevStatsCached} + * and {@link #mXtStatsCached} when appropriate. + */ + private NetworkStats internalGetSummaryForNetwork( + NetworkTemplate template, long start, long end) { + if (!mSettings.getReportXtOverDev()) { + // shortcut when XT reporting disabled + return mDevStatsCached.getSummary(template, start, end); + } + + // splice stats between DEV and XT, switching over from DEV to XT at + // first atomic bucket. + final long firstAtomicBucket = mXtStatsCached.getFirstAtomicBucketMillis(); + final NetworkStats dev = mDevStatsCached.getSummary( + template, Math.min(start, firstAtomicBucket), Math.min(end, firstAtomicBucket)); + final NetworkStats xt = mXtStatsCached.getSummary( + template, Math.max(start, firstAtomicBucket), Math.max(end, firstAtomicBucket)); + + xt.combineAllValues(dev); + return xt; + } + + /** + * Return network history, splicing between {@link #mDevStatsCached} + * and {@link #mXtStatsCached} when appropriate. + */ + private NetworkStatsHistory internalGetHistoryForNetwork(NetworkTemplate template, int fields) { + if (!mSettings.getReportXtOverDev()) { + // shortcut when XT reporting disabled + return mDevStatsCached.getHistory(template, UID_ALL, SET_ALL, TAG_NONE, fields); + } + + // splice stats between DEV and XT, switching over from DEV to XT at + // first atomic bucket. + final long firstAtomicBucket = mXtStatsCached.getFirstAtomicBucketMillis(); + final NetworkStatsHistory dev = mDevStatsCached.getHistory( + template, UID_ALL, SET_ALL, TAG_NONE, fields, Long.MIN_VALUE, firstAtomicBucket); + final NetworkStatsHistory xt = mXtStatsCached.getHistory( + template, UID_ALL, SET_ALL, TAG_NONE, fields, firstAtomicBucket, Long.MAX_VALUE); + + xt.recordEntireHistory(dev); + return xt; + } + @Override public long getNetworkTotalBytes(NetworkTemplate template, long start, long end) { mContext.enforceCallingOrSelfPermission(READ_NETWORK_USAGE_HISTORY, TAG); assertBandwidthControlEnabled(); - return mDevStatsCached.getSummary(template, start, end).getTotalBytes(); + return internalGetSummaryForNetwork(template, start, end).getTotalBytes(); } @Override @@ -1190,6 +1241,10 @@ public class NetworkStatsService extends INetworkStatsService.Stub { return getSecureBoolean(NETSTATS_SAMPLE_ENABLED, true); } @Override + public boolean getReportXtOverDev() { + return getSecureBoolean(NETSTATS_REPORT_XT_OVER_DEV, true); + } + @Override public Config getDevConfig() { return new Config(getSecureLong(NETSTATS_DEV_BUCKET_DURATION, HOUR_IN_MILLIS), getSecureLong(NETSTATS_DEV_ROTATE_AGE, 15 * DAY_IN_MILLIS), diff --git a/services/tests/servicestests/src/com/android/server/NetworkStatsServiceTest.java b/services/tests/servicestests/src/com/android/server/NetworkStatsServiceTest.java index 332d1980167f7..cdc4d784cf01c 100644 --- a/services/tests/servicestests/src/com/android/server/NetworkStatsServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/NetworkStatsServiceTest.java @@ -777,10 +777,80 @@ public class NetworkStatsServiceTest extends AndroidTestCase { } + public void testReportXtOverDev() throws Exception { + // bring mobile network online + expectCurrentTime(); + expectDefaultSettings(); + expectNetworkState(buildMobile3gState(IMSI_1)); + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + expectNetworkStatsPoll(); + + replay(); + mServiceContext.sendBroadcast(new Intent(CONNECTIVITY_ACTION_IMMEDIATE)); + verifyAndReset(); + + // create some traffic, but only for DEV, and across 1.5 buckets + incrementCurrentTime(90 * MINUTE_IN_MILLIS); + expectCurrentTime(); + expectDefaultSettings(); + expectNetworkStatsSummaryDev(new NetworkStats(getElapsedRealtime(), 1) + .addIfaceValues(TEST_IFACE, 6000L, 60L, 3000L, 30L)); + expectNetworkStatsSummaryXt(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + expectNetworkStatsPoll(); + + replay(); + mServiceContext.sendBroadcast(new Intent(ACTION_NETWORK_STATS_POLL)); + + // verify service recorded history: + // 4000(dev) + 2000(dev) + assertNetworkTotal(sTemplateImsi1, 6000L, 60L, 3000L, 30L, 0); + verifyAndReset(); + + // create traffic on both DEV and XT, across two buckets + incrementCurrentTime(2 * HOUR_IN_MILLIS); + expectCurrentTime(); + expectDefaultSettings(); + expectNetworkStatsSummaryDev(new NetworkStats(getElapsedRealtime(), 1) + .addIfaceValues(TEST_IFACE, 6004L, 64L, 3004L, 34L)); + expectNetworkStatsSummaryXt(new NetworkStats(getElapsedRealtime(), 1) + .addIfaceValues(TEST_IFACE, 10240L, 0L, 0L, 0L)); + expectNetworkStatsUidDetail(buildEmptyStats()); + expectNetworkStatsPoll(); + + replay(); + mServiceContext.sendBroadcast(new Intent(ACTION_NETWORK_STATS_POLL)); + + // verify that we switching reporting at the first atomic XT bucket, + // which should give us: + // 4000(dev) + 2000(dev) + 1(dev) + 5120(xt) + 2560(xt) + assertNetworkTotal(sTemplateImsi1, 13681L, 61L, 3001L, 31L, 0); + + // also test pure-DEV and pure-XT ranges + assertNetworkTotal(sTemplateImsi1, startTimeMillis(), + startTimeMillis() + 2 * HOUR_IN_MILLIS, 6001L, 61L, 3001L, 31L, 0); + assertNetworkTotal(sTemplateImsi1, startTimeMillis() + 2 * HOUR_IN_MILLIS, + startTimeMillis() + 4 * HOUR_IN_MILLIS, 7680L, 0L, 0L, 0L, 0); + + verifyAndReset(); + } + private void assertNetworkTotal(NetworkTemplate template, long rxBytes, long rxPackets, long txBytes, long txPackets, int operations) throws Exception { + assertNetworkTotal(template, Long.MIN_VALUE, Long.MAX_VALUE, rxBytes, rxPackets, txBytes, + txPackets, operations); + } + + private void assertNetworkTotal(NetworkTemplate template, long start, long end, long rxBytes, + long rxPackets, long txBytes, long txPackets, int operations) throws Exception { + // verify history API final NetworkStatsHistory history = mSession.getHistoryForNetwork(template, FIELD_ALL); - assertValues(history, Long.MIN_VALUE, Long.MAX_VALUE, rxBytes, rxPackets, txBytes, + assertValues(history, start, end, rxBytes, rxPackets, txBytes, txPackets, operations); + + // verify summary API + final NetworkStats stats = mSession.getSummaryForNetwork(template, start, end); + assertValues(stats, IFACE_ALL, UID_ALL, SET_DEFAULT, TAG_NONE, rxBytes, rxPackets, txBytes, txPackets, operations); } @@ -791,10 +861,17 @@ public class NetworkStatsServiceTest extends AndroidTestCase { private void assertUidTotal(NetworkTemplate template, int uid, int set, long rxBytes, long rxPackets, long txBytes, long txPackets, int operations) throws Exception { + // verify history API final NetworkStatsHistory history = mSession.getHistoryForUid( template, uid, set, TAG_NONE, FIELD_ALL); assertValues(history, Long.MIN_VALUE, Long.MAX_VALUE, rxBytes, rxPackets, txBytes, txPackets, operations); + + // verify summary API + final NetworkStats stats = mSession.getSummaryForAllUid( + template, Long.MIN_VALUE, Long.MAX_VALUE, false); + assertValues(stats, IFACE_ALL, uid, set, TAG_NONE, rxBytes, rxPackets, txBytes, txPackets, + operations); } private void expectSystemReady() throws Exception { @@ -819,7 +896,15 @@ public class NetworkStatsServiceTest extends AndroidTestCase { } private void expectNetworkStatsSummary(NetworkStats summary) throws Exception { + expectNetworkStatsSummaryDev(summary); + expectNetworkStatsSummaryXt(summary); + } + + private void expectNetworkStatsSummaryDev(NetworkStats summary) throws Exception { expect(mNetManager.getNetworkStatsSummaryDev()).andReturn(summary).atLeastOnce(); + } + + private void expectNetworkStatsSummaryXt(NetworkStats summary) throws Exception { expect(mNetManager.getNetworkStatsSummaryXt()).andReturn(summary).atLeastOnce(); } @@ -847,6 +932,7 @@ public class NetworkStatsServiceTest extends AndroidTestCase { expect(mSettings.getPollInterval()).andReturn(HOUR_IN_MILLIS).anyTimes(); expect(mSettings.getTimeCacheMaxAge()).andReturn(DAY_IN_MILLIS).anyTimes(); expect(mSettings.getSampleEnabled()).andReturn(true).anyTimes(); + expect(mSettings.getReportXtOverDev()).andReturn(true).anyTimes(); final Config config = new Config(bucketDuration, deleteAge, deleteAge); expect(mSettings.getDevConfig()).andReturn(config).anyTimes(); @@ -885,8 +971,20 @@ public class NetworkStatsServiceTest extends AndroidTestCase { private static void assertValues(NetworkStats stats, String iface, int uid, int set, int tag, long rxBytes, long rxPackets, long txBytes, long txPackets, int operations) { - final int i = stats.findIndex(iface, uid, set, tag); - final NetworkStats.Entry entry = stats.getValues(i, null); + final NetworkStats.Entry entry = new NetworkStats.Entry(); + if (set == SET_DEFAULT || set == SET_ALL) { + final int i = stats.findIndex(iface, uid, SET_DEFAULT, tag); + if (i != -1) { + entry.add(stats.getValues(i, null)); + } + } + if (set == SET_FOREGROUND || set == SET_ALL) { + final int i = stats.findIndex(iface, uid, SET_FOREGROUND, tag); + if (i != -1) { + entry.add(stats.getValues(i, null)); + } + } + assertEquals("unexpected rxBytes", rxBytes, entry.rxBytes); assertEquals("unexpected rxPackets", rxPackets, entry.rxPackets); assertEquals("unexpected txBytes", txBytes, entry.txBytes); diff --git a/services/tests/servicestests/src/com/android/server/net/NetworkStatsCollectionTest.java b/services/tests/servicestests/src/com/android/server/net/NetworkStatsCollectionTest.java index 863482156e87e..1a6c289316887 100644 --- a/services/tests/servicestests/src/com/android/server/net/NetworkStatsCollectionTest.java +++ b/services/tests/servicestests/src/com/android/server/net/NetworkStatsCollectionTest.java @@ -16,7 +16,11 @@ package com.android.server.net; +import static android.net.NetworkStats.SET_DEFAULT; +import static android.net.NetworkStats.TAG_NONE; +import static android.net.NetworkStats.UID_ALL; import static android.net.NetworkTemplate.buildTemplateMobileAll; +import static android.text.format.DateUtils.HOUR_IN_MILLIS; import static android.text.format.DateUtils.MINUTE_IN_MILLIS; import android.content.res.Resources; @@ -47,6 +51,14 @@ public class NetworkStatsCollectionTest extends AndroidTestCase { private static final String TEST_FILE = "test.bin"; private static final String TEST_IMSI = "310260000000000"; + @Override + public void setUp() throws Exception { + super.setUp(); + + // ignore any device overlay while testing + NetworkTemplate.forceAllNetworkTypes(); + } + public void testReadLegacyNetwork() throws Exception { final File testFile = new File(getContext().getFilesDir(), TEST_FILE); stageFile(R.raw.netstats_v1, testFile); @@ -125,6 +137,20 @@ public class NetworkStatsCollectionTest extends AndroidTestCase { 77017831L, 100995L, 35436758L, 92344L); } + public void testStartEndAtomicBuckets() throws Exception { + final NetworkStatsCollection collection = new NetworkStatsCollection(HOUR_IN_MILLIS); + + // record empty data straddling between buckets + final NetworkStats.Entry entry = new NetworkStats.Entry(); + entry.rxBytes = 32; + collection.recordData(null, UID_ALL, SET_DEFAULT, TAG_NONE, 30 * MINUTE_IN_MILLIS, + 90 * MINUTE_IN_MILLIS, entry); + + // assert that we report boundary in atomic buckets + assertEquals(0, collection.getStartMillis()); + assertEquals(2 * HOUR_IN_MILLIS, collection.getEndMillis()); + } + /** * Copy a {@link Resources#openRawResource(int)} into {@link File} for * testing purposes.