Transition from DEV network stats to XT.

When XT stats are available, transition to prefer them over DEV,
since they aren't subject to hardware driver bugs.  Only switches at
the first atomic XT bucket, and adds a Settings.Secure flag to force
back to DEV if needed.  Includes tests to cover transition.

Fix tests where device overlay would change which network types
reflected data usage.  Test both history and summary APIs.  Fixed
collection timestamps to reflect full buckets.

Bug: 6504744
Change-Id: Idd7f3b2fdb064c36547c85c51c214fd938c59b7e
diff --git a/core/java/android/net/NetworkStats.java b/core/java/android/net/NetworkStats.java
index 844d055..fb7a4f8 100644
--- a/core/java/android/net/NetworkStats.java
+++ b/core/java/android/net/NetworkStats.java
@@ -111,6 +111,14 @@
                     && 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 0003c6e..a37c26f9 100644
--- a/core/java/android/net/NetworkStatsHistory.java
+++ b/core/java/android/net/NetworkStatsHistory.java
@@ -342,11 +342,23 @@
      * 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 @@
             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 39a4d7b..d8e53d5 100644
--- a/core/java/android/net/NetworkTemplate.java
+++ b/core/java/android/net/NetworkTemplate.java
@@ -61,6 +61,13 @@
                 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 @@
             // 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 @@
         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 ea3cab4..a0641d5 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -4210,6 +4210,8 @@
         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 c2e475a..9ddf011 100644
--- a/services/java/com/android/server/net/NetworkStatsCollection.java
+++ b/services/java/com/android/server/net/NetworkStatsCollection.java
@@ -71,7 +71,7 @@
 
     private HashMap<Key, NetworkStatsHistory> mStats = Maps.newHashMap();
 
-    private long mBucketDuration;
+    private final long mBucketDuration;
 
     private long mStartMillis;
     private long mEndMillis;
@@ -95,6 +95,18 @@
         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 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<Key, NetworkStatsHistory> entry : mStats.entrySet()) {
@@ -128,7 +149,7 @@
             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 @@
         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<Key, NetworkStatsHistory> mapEntry : mStats.entrySet()) {
             final Key key = mapEntry.getKey();
             if (templateMatches(template, key.ident)) {
@@ -175,8 +199,9 @@
      */
     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 0e93b0a..ba122ec 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_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 long getPollInterval();
         public long getTimeCacheMaxAge();
         public boolean getSampleEnabled();
+        public boolean getReportXtOverDev();
 
         public static class Config {
             public final long bucketDuration;
@@ -221,6 +223,8 @@
 
     /** 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 @@
             // 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 @@
         mUidTagRecorder = null;
 
         mDevStatsCached = null;
+        mXtStatsCached = null;
 
         mSystemReady = false;
     }
@@ -469,12 +475,12 @@
             @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 @@
         };
     }
 
+    /**
+     * 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 @@
             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 332d198..cdc4d78 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 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 @@
 
     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 @@
     }
 
     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 @@
         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 @@
 
     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 8634821..1a6c289 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 @@
     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 @@
                 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.