Account for interpolation loss in cumulative network stats

Bug: 352537247
Test: atest FrameworksServicesTests:NetworkStatsAccumulatorTest
Flag: com.android.server.stats.accumulate_network_stats_since_boot
Change-Id: I95a04c3b32bc8379f3cb9453f29df67ae87eefae
diff --git a/services/core/java/com/android/server/stats/pull/netstats/NetworkStatsAccumulator.java b/services/core/java/com/android/server/stats/pull/netstats/NetworkStatsAccumulator.java
index e798bc4..3f7fcee 100644
--- a/services/core/java/com/android/server/stats/pull/netstats/NetworkStatsAccumulator.java
+++ b/services/core/java/com/android/server/stats/pull/netstats/NetworkStatsAccumulator.java
@@ -19,6 +19,7 @@
 import android.annotation.NonNull;
 import android.net.NetworkStats;
 import android.net.NetworkTemplate;
+import android.util.Log;
 
 import java.util.Objects;
 
@@ -33,6 +34,7 @@
  */
 public class NetworkStatsAccumulator {
 
+    private static final String TAG = "NetworkStatsAccumulator";
     private final NetworkTemplate mTemplate;
     private final boolean mWithTags;
     private final long mBucketDurationMillis;
@@ -57,8 +59,9 @@
     @NonNull
     public NetworkStats queryStats(long currentTimeMillis,
             @NonNull StatsQueryFunction queryFunction) {
-        maybeExpandSnapshot(currentTimeMillis, queryFunction);
-        return snapshotPlusFollowingStats(currentTimeMillis, queryFunction);
+        NetworkStats completeStats = snapshotPlusFollowingStats(currentTimeMillis, queryFunction);
+        maybeExpandSnapshot(currentTimeMillis, completeStats, queryFunction);
+        return completeStats;
     }
 
     /**
@@ -72,15 +75,28 @@
      * Expands the internal cumulative stats snapshot, if possible, by querying NetworkStats.
      */
     private void maybeExpandSnapshot(long currentTimeMillis,
+            NetworkStats completeStatsUntilCurrentTime,
             @NonNull StatsQueryFunction queryFunction) {
         // Update snapshot only if it is possible to expand it by at least one full bucket, and only
         // if the new snapshot's end is not in the active bucket.
         long newEndTimeMillis = currentTimeMillis - mBucketDurationMillis;
         if (newEndTimeMillis - mSnapshotEndTimeMillis > mBucketDurationMillis) {
-            NetworkStats extraStats = queryFunction.queryNetworkStats(mTemplate, mWithTags,
-                    mSnapshotEndTimeMillis, newEndTimeMillis);
+            Log.v(TAG,
+                    "Expanding snapshot (mTemplate=" + mTemplate + ", mWithTags=" + mWithTags
+                            + ") from " + mSnapshotEndTimeMillis + " to " + newEndTimeMillis
+                            + " at " + currentTimeMillis);
+            NetworkStats extraStats = queryFunction.queryNetworkStats(
+                    mTemplate, mWithTags, mSnapshotEndTimeMillis, newEndTimeMillis);
             mSnapshot = mSnapshot.add(extraStats);
             mSnapshotEndTimeMillis = newEndTimeMillis;
+
+            // NetworkStats queries interpolate historical data using integers maths, which makes
+            // queries non-transitive: Query(t0, t1) + Query(t1, t2) <= Query(t0, t2).
+            // Compute interpolation data loss from moving the snapshot's end-point, and add it to
+            // the snapshot to avoid under-counting.
+            NetworkStats newStats = snapshotPlusFollowingStats(currentTimeMillis, queryFunction);
+            NetworkStats interpolationLoss = completeStatsUntilCurrentTime.subtract(newStats);
+            mSnapshot = mSnapshot.add(interpolationLoss);
         }
     }