1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.net; 18 19 import static android.annotation.SystemApi.Client.MODULE_LIBRARIES; 20 import static android.net.NetworkStats.DEFAULT_NETWORK_NO; 21 import static android.net.NetworkStats.IFACE_ALL; 22 import static android.net.NetworkStats.METERED_NO; 23 import static android.net.NetworkStats.ROAMING_NO; 24 import static android.net.NetworkStats.SET_DEFAULT; 25 import static android.net.NetworkStats.TAG_NONE; 26 import static android.net.NetworkStats.UID_ALL; 27 import static android.net.NetworkStatsHistory.DataStreamUtils.readFullLongArray; 28 import static android.net.NetworkStatsHistory.DataStreamUtils.readVarLongArray; 29 import static android.net.NetworkStatsHistory.DataStreamUtils.writeVarLongArray; 30 import static android.net.NetworkStatsHistory.Entry.UNKNOWN; 31 import static android.net.NetworkStatsHistory.ParcelUtils.readLongArray; 32 import static android.net.NetworkStatsHistory.ParcelUtils.writeLongArray; 33 import static android.text.format.DateUtils.SECOND_IN_MILLIS; 34 35 import static com.android.net.module.util.NetworkStatsUtils.multiplySafeByRational; 36 37 import android.annotation.NonNull; 38 import android.annotation.Nullable; 39 import android.annotation.SystemApi; 40 import android.compat.annotation.UnsupportedAppUsage; 41 import android.os.Build; 42 import android.os.Parcel; 43 import android.os.Parcelable; 44 import android.service.NetworkStatsHistoryBucketProto; 45 import android.service.NetworkStatsHistoryProto; 46 import android.util.IndentingPrintWriter; 47 import android.util.proto.ProtoOutputStream; 48 49 import com.android.net.module.util.CollectionUtils; 50 import com.android.net.module.util.NetworkStatsUtils; 51 52 import libcore.util.EmptyArray; 53 54 import java.io.CharArrayWriter; 55 import java.io.DataInput; 56 import java.io.DataOutput; 57 import java.io.IOException; 58 import java.io.PrintWriter; 59 import java.net.ProtocolException; 60 import java.util.ArrayList; 61 import java.util.Arrays; 62 import java.util.List; 63 import java.util.Random; 64 import java.util.TreeMap; 65 66 /** 67 * Collection of historical network statistics, recorded into equally-sized 68 * "buckets" in time. Internally it stores data in {@code long} series for more 69 * efficient persistence. 70 * <p> 71 * Each bucket is defined by a {@link #bucketStart} timestamp, and lasts for 72 * {@link #bucketDuration}. Internally assumes that {@link #bucketStart} is 73 * sorted at all times. 74 * 75 * @hide 76 */ 77 @SystemApi(client = MODULE_LIBRARIES) 78 public final class NetworkStatsHistory implements Parcelable { 79 private static final int VERSION_INIT = 1; 80 private static final int VERSION_ADD_PACKETS = 2; 81 private static final int VERSION_ADD_ACTIVE = 3; 82 83 /** @hide */ 84 public static final int FIELD_ACTIVE_TIME = 0x01; 85 /** @hide */ 86 public static final int FIELD_RX_BYTES = 0x02; 87 /** @hide */ 88 public static final int FIELD_RX_PACKETS = 0x04; 89 /** @hide */ 90 public static final int FIELD_TX_BYTES = 0x08; 91 /** @hide */ 92 public static final int FIELD_TX_PACKETS = 0x10; 93 /** @hide */ 94 public static final int FIELD_OPERATIONS = 0x20; 95 /** @hide */ 96 public static final int FIELD_ALL = 0xFFFFFFFF; 97 98 private long bucketDuration; 99 private int bucketCount; 100 private long[] bucketStart; 101 private long[] activeTime; 102 private long[] rxBytes; 103 private long[] rxPackets; 104 private long[] txBytes; 105 private long[] txPackets; 106 private long[] operations; 107 private long totalBytes; 108 109 /** @hide */ NetworkStatsHistory(long bucketDuration, long[] bucketStart, long[] activeTime, long[] rxBytes, long[] rxPackets, long[] txBytes, long[] txPackets, long[] operations, int bucketCount, long totalBytes)110 public NetworkStatsHistory(long bucketDuration, long[] bucketStart, long[] activeTime, 111 long[] rxBytes, long[] rxPackets, long[] txBytes, long[] txPackets, 112 long[] operations, int bucketCount, long totalBytes) { 113 this.bucketDuration = bucketDuration; 114 this.bucketStart = bucketStart; 115 this.activeTime = activeTime; 116 this.rxBytes = rxBytes; 117 this.rxPackets = rxPackets; 118 this.txBytes = txBytes; 119 this.txPackets = txPackets; 120 this.operations = operations; 121 this.bucketCount = bucketCount; 122 this.totalBytes = totalBytes; 123 } 124 125 /** 126 * An instance to represent a single record in a {@link NetworkStatsHistory} object. 127 */ 128 public static final class Entry { 129 /** @hide */ 130 public static final long UNKNOWN = -1; 131 132 /** @hide */ 133 // TODO: Migrate all callers to get duration from the history object and remove this field. 134 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 135 public long bucketDuration; 136 /** @hide */ 137 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 138 public long bucketStart; 139 /** @hide */ 140 public long activeTime; 141 /** @hide */ 142 @UnsupportedAppUsage 143 public long rxBytes; 144 /** @hide */ 145 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 146 public long rxPackets; 147 /** @hide */ 148 @UnsupportedAppUsage 149 public long txBytes; 150 /** @hide */ 151 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 152 public long txPackets; 153 /** @hide */ 154 public long operations; 155 /** @hide */ Entry()156 Entry() {} 157 158 /** 159 * Construct a {@link Entry} instance to represent a single record in a 160 * {@link NetworkStatsHistory} object. 161 * 162 * @param bucketStart Start of period for this {@link Entry}, in milliseconds since the 163 * Unix epoch, see {@link java.lang.System#currentTimeMillis}. 164 * @param activeTime Active time for this {@link Entry}, in milliseconds. 165 * @param rxBytes Number of bytes received for this {@link Entry}. Statistics should 166 * represent the contents of IP packets, including IP headers. 167 * @param rxPackets Number of packets received for this {@link Entry}. Statistics should 168 * represent the contents of IP packets, including IP headers. 169 * @param txBytes Number of bytes transmitted for this {@link Entry}. Statistics should 170 * represent the contents of IP packets, including IP headers. 171 * @param txPackets Number of bytes transmitted for this {@link Entry}. Statistics should 172 * represent the contents of IP packets, including IP headers. 173 * @param operations count of network operations performed for this {@link Entry}. This can 174 * be used to derive bytes-per-operation. 175 */ Entry(long bucketStart, long activeTime, long rxBytes, long rxPackets, long txBytes, long txPackets, long operations)176 public Entry(long bucketStart, long activeTime, long rxBytes, 177 long rxPackets, long txBytes, long txPackets, long operations) { 178 this.bucketStart = bucketStart; 179 this.activeTime = activeTime; 180 this.rxBytes = rxBytes; 181 this.rxPackets = rxPackets; 182 this.txBytes = txBytes; 183 this.txPackets = txPackets; 184 this.operations = operations; 185 } 186 187 /** 188 * Get start timestamp of the bucket's time interval, in milliseconds since the Unix epoch. 189 */ getBucketStart()190 public long getBucketStart() { 191 return bucketStart; 192 } 193 194 /** 195 * Get active time of the bucket's time interval, in milliseconds. 196 */ getActiveTime()197 public long getActiveTime() { 198 return activeTime; 199 } 200 201 /** Get number of bytes received for this {@link Entry}. */ getRxBytes()202 public long getRxBytes() { 203 return rxBytes; 204 } 205 206 /** Get number of packets received for this {@link Entry}. */ getRxPackets()207 public long getRxPackets() { 208 return rxPackets; 209 } 210 211 /** Get number of bytes transmitted for this {@link Entry}. */ getTxBytes()212 public long getTxBytes() { 213 return txBytes; 214 } 215 216 /** Get number of packets transmitted for this {@link Entry}. */ getTxPackets()217 public long getTxPackets() { 218 return txPackets; 219 } 220 221 /** Get count of network operations performed for this {@link Entry}. */ getOperations()222 public long getOperations() { 223 return operations; 224 } 225 226 @Override equals(Object o)227 public boolean equals(Object o) { 228 if (this == o) return true; 229 if (o.getClass() != getClass()) return false; 230 Entry entry = (Entry) o; 231 return bucketStart == entry.bucketStart 232 && activeTime == entry.activeTime && rxBytes == entry.rxBytes 233 && rxPackets == entry.rxPackets && txBytes == entry.txBytes 234 && txPackets == entry.txPackets && operations == entry.operations; 235 } 236 237 @Override hashCode()238 public int hashCode() { 239 return (int) (bucketStart * 2 240 + activeTime * 3 241 + rxBytes * 5 242 + rxPackets * 7 243 + txBytes * 11 244 + txPackets * 13 245 + operations * 17); 246 } 247 248 @Override toString()249 public String toString() { 250 return "Entry{" 251 + "bucketStart=" + bucketStart 252 + ", activeTime=" + activeTime 253 + ", rxBytes=" + rxBytes 254 + ", rxPackets=" + rxPackets 255 + ", txBytes=" + txBytes 256 + ", txPackets=" + txPackets 257 + ", operations=" + operations 258 + "}"; 259 } 260 261 /** 262 * Add the given {@link Entry} with this instance and return a new {@link Entry} 263 * instance as the result. 264 * 265 * @hide 266 */ 267 @NonNull plus(@onNull Entry another, long bucketDuration)268 public Entry plus(@NonNull Entry another, long bucketDuration) { 269 if (this.bucketStart != another.bucketStart) { 270 throw new IllegalArgumentException("bucketStart " + this.bucketStart 271 + " is not equal to " + another.bucketStart); 272 } 273 return new Entry(this.bucketStart, 274 // Active time should not go over bucket duration. 275 Math.min(this.activeTime + another.activeTime, bucketDuration), 276 this.rxBytes + another.rxBytes, 277 this.rxPackets + another.rxPackets, 278 this.txBytes + another.txBytes, 279 this.txPackets + another.txPackets, 280 this.operations + another.operations); 281 } 282 } 283 284 /** @hide */ 285 @UnsupportedAppUsage NetworkStatsHistory(long bucketDuration)286 public NetworkStatsHistory(long bucketDuration) { 287 this(bucketDuration, 10, FIELD_ALL); 288 } 289 290 /** @hide */ NetworkStatsHistory(long bucketDuration, int initialSize)291 public NetworkStatsHistory(long bucketDuration, int initialSize) { 292 this(bucketDuration, initialSize, FIELD_ALL); 293 } 294 295 /** @hide */ NetworkStatsHistory(long bucketDuration, int initialSize, int fields)296 public NetworkStatsHistory(long bucketDuration, int initialSize, int fields) { 297 this.bucketDuration = bucketDuration; 298 bucketStart = new long[initialSize]; 299 if ((fields & FIELD_ACTIVE_TIME) != 0) activeTime = new long[initialSize]; 300 if ((fields & FIELD_RX_BYTES) != 0) rxBytes = new long[initialSize]; 301 if ((fields & FIELD_RX_PACKETS) != 0) rxPackets = new long[initialSize]; 302 if ((fields & FIELD_TX_BYTES) != 0) txBytes = new long[initialSize]; 303 if ((fields & FIELD_TX_PACKETS) != 0) txPackets = new long[initialSize]; 304 if ((fields & FIELD_OPERATIONS) != 0) operations = new long[initialSize]; 305 bucketCount = 0; 306 totalBytes = 0; 307 } 308 309 /** @hide */ NetworkStatsHistory(NetworkStatsHistory existing, long bucketDuration)310 public NetworkStatsHistory(NetworkStatsHistory existing, long bucketDuration) { 311 this(bucketDuration, existing.estimateResizeBuckets(bucketDuration)); 312 recordEntireHistory(existing); 313 } 314 315 /** @hide */ 316 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) NetworkStatsHistory(Parcel in)317 public NetworkStatsHistory(Parcel in) { 318 bucketDuration = in.readLong(); 319 bucketStart = readLongArray(in); 320 activeTime = readLongArray(in); 321 rxBytes = readLongArray(in); 322 rxPackets = readLongArray(in); 323 txBytes = readLongArray(in); 324 txPackets = readLongArray(in); 325 operations = readLongArray(in); 326 bucketCount = bucketStart.length; 327 totalBytes = in.readLong(); 328 } 329 330 @Override writeToParcel(@onNull Parcel out, int flags)331 public void writeToParcel(@NonNull Parcel out, int flags) { 332 out.writeLong(bucketDuration); 333 writeLongArray(out, bucketStart, bucketCount); 334 writeLongArray(out, activeTime, bucketCount); 335 writeLongArray(out, rxBytes, bucketCount); 336 writeLongArray(out, rxPackets, bucketCount); 337 writeLongArray(out, txBytes, bucketCount); 338 writeLongArray(out, txPackets, bucketCount); 339 writeLongArray(out, operations, bucketCount); 340 out.writeLong(totalBytes); 341 } 342 343 /** @hide */ NetworkStatsHistory(DataInput in)344 public NetworkStatsHistory(DataInput in) throws IOException { 345 final int version = in.readInt(); 346 switch (version) { 347 case VERSION_INIT: { 348 bucketDuration = in.readLong(); 349 bucketStart = readFullLongArray(in); 350 rxBytes = readFullLongArray(in); 351 rxPackets = new long[bucketStart.length]; 352 txBytes = readFullLongArray(in); 353 txPackets = new long[bucketStart.length]; 354 operations = new long[bucketStart.length]; 355 bucketCount = bucketStart.length; 356 totalBytes = CollectionUtils.total(rxBytes) + CollectionUtils.total(txBytes); 357 break; 358 } 359 case VERSION_ADD_PACKETS: 360 case VERSION_ADD_ACTIVE: { 361 bucketDuration = in.readLong(); 362 bucketStart = readVarLongArray(in); 363 activeTime = (version >= VERSION_ADD_ACTIVE) ? readVarLongArray(in) 364 : new long[bucketStart.length]; 365 rxBytes = readVarLongArray(in); 366 rxPackets = readVarLongArray(in); 367 txBytes = readVarLongArray(in); 368 txPackets = readVarLongArray(in); 369 operations = readVarLongArray(in); 370 bucketCount = bucketStart.length; 371 totalBytes = CollectionUtils.total(rxBytes) + CollectionUtils.total(txBytes); 372 break; 373 } 374 default: { 375 throw new ProtocolException("unexpected version: " + version); 376 } 377 } 378 379 if (bucketStart.length != bucketCount || rxBytes.length != bucketCount 380 || rxPackets.length != bucketCount || txBytes.length != bucketCount 381 || txPackets.length != bucketCount || operations.length != bucketCount) { 382 throw new ProtocolException("Mismatched history lengths"); 383 } 384 } 385 386 /** @hide */ writeToStream(DataOutput out)387 public void writeToStream(DataOutput out) throws IOException { 388 out.writeInt(VERSION_ADD_ACTIVE); 389 out.writeLong(bucketDuration); 390 writeVarLongArray(out, bucketStart, bucketCount); 391 writeVarLongArray(out, activeTime, bucketCount); 392 writeVarLongArray(out, rxBytes, bucketCount); 393 writeVarLongArray(out, rxPackets, bucketCount); 394 writeVarLongArray(out, txBytes, bucketCount); 395 writeVarLongArray(out, txPackets, bucketCount); 396 writeVarLongArray(out, operations, bucketCount); 397 } 398 399 @Override describeContents()400 public int describeContents() { 401 return 0; 402 } 403 404 /** @hide */ 405 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) size()406 public int size() { 407 return bucketCount; 408 } 409 410 /** @hide */ getBucketDuration()411 public long getBucketDuration() { 412 return bucketDuration; 413 } 414 415 /** @hide */ 416 @UnsupportedAppUsage getStart()417 public long getStart() { 418 if (bucketCount > 0) { 419 return bucketStart[0]; 420 } else { 421 return Long.MAX_VALUE; 422 } 423 } 424 425 /** @hide */ 426 @UnsupportedAppUsage getEnd()427 public long getEnd() { 428 if (bucketCount > 0) { 429 return bucketStart[bucketCount - 1] + bucketDuration; 430 } else { 431 return Long.MIN_VALUE; 432 } 433 } 434 435 /** 436 * Return total bytes represented by this history. 437 * @hide 438 */ getTotalBytes()439 public long getTotalBytes() { 440 return totalBytes; 441 } 442 443 /** 444 * Return index of bucket that contains or is immediately before the 445 * requested time. 446 * @hide 447 */ 448 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) getIndexBefore(long time)449 public int getIndexBefore(long time) { 450 int index = Arrays.binarySearch(bucketStart, 0, bucketCount, time); 451 if (index < 0) { 452 index = (~index) - 1; 453 } else { 454 index -= 1; 455 } 456 return NetworkStatsUtils.constrain(index, 0, bucketCount - 1); 457 } 458 459 /** 460 * Return index of bucket that contains or is immediately after the 461 * requested time. 462 * @hide 463 */ getIndexAfter(long time)464 public int getIndexAfter(long time) { 465 int index = Arrays.binarySearch(bucketStart, 0, bucketCount, time); 466 if (index < 0) { 467 index = ~index; 468 } else { 469 index += 1; 470 } 471 return NetworkStatsUtils.constrain(index, 0, bucketCount - 1); 472 } 473 474 /** 475 * Return specific stats entry. 476 * @hide 477 */ 478 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) getValues(int i, Entry recycle)479 public Entry getValues(int i, Entry recycle) { 480 final Entry entry = recycle != null ? recycle : new Entry(); 481 entry.bucketStart = bucketStart[i]; 482 entry.bucketDuration = bucketDuration; 483 entry.activeTime = getLong(activeTime, i, UNKNOWN); 484 entry.rxBytes = getLong(rxBytes, i, UNKNOWN); 485 entry.rxPackets = getLong(rxPackets, i, UNKNOWN); 486 entry.txBytes = getLong(txBytes, i, UNKNOWN); 487 entry.txPackets = getLong(txPackets, i, UNKNOWN); 488 entry.operations = getLong(operations, i, UNKNOWN); 489 return entry; 490 } 491 492 /** 493 * Get List of {@link Entry} of the {@link NetworkStatsHistory} instance. 494 * 495 * @return 496 */ 497 @NonNull getEntries()498 public List<Entry> getEntries() { 499 // TODO: Return a wrapper that uses this list instead, to prevent the returned result 500 // from being changed. 501 final ArrayList<Entry> ret = new ArrayList<>(size()); 502 for (int i = 0; i < size(); i++) { 503 ret.add(getValues(i, null /* recycle */)); 504 } 505 return ret; 506 } 507 508 /** @hide */ setValues(int i, Entry entry)509 public void setValues(int i, Entry entry) { 510 // Unwind old values 511 if (rxBytes != null) totalBytes -= rxBytes[i]; 512 if (txBytes != null) totalBytes -= txBytes[i]; 513 514 bucketStart[i] = entry.bucketStart; 515 setLong(activeTime, i, entry.activeTime); 516 setLong(rxBytes, i, entry.rxBytes); 517 setLong(rxPackets, i, entry.rxPackets); 518 setLong(txBytes, i, entry.txBytes); 519 setLong(txPackets, i, entry.txPackets); 520 setLong(operations, i, entry.operations); 521 522 // Apply new values 523 if (rxBytes != null) totalBytes += rxBytes[i]; 524 if (txBytes != null) totalBytes += txBytes[i]; 525 } 526 527 /** 528 * Record that data traffic occurred in the given time range. Will 529 * distribute across internal buckets, creating new buckets as needed. 530 * @hide 531 */ 532 @Deprecated recordData(long start, long end, long rxBytes, long txBytes)533 public void recordData(long start, long end, long rxBytes, long txBytes) { 534 recordData(start, end, new NetworkStats.Entry( 535 IFACE_ALL, UID_ALL, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, 536 DEFAULT_NETWORK_NO, rxBytes, 0L, txBytes, 0L, 0L)); 537 } 538 539 /** 540 * Record that data traffic occurred in the given time range. Will 541 * distribute across internal buckets, creating new buckets as needed. 542 * @hide 543 */ recordData(long start, long end, NetworkStats.Entry entry)544 public void recordData(long start, long end, NetworkStats.Entry entry) { 545 long rxBytes = entry.rxBytes; 546 long rxPackets = entry.rxPackets; 547 long txBytes = entry.txBytes; 548 long txPackets = entry.txPackets; 549 long operations = entry.operations; 550 551 if (entry.isNegative()) { 552 throw new IllegalArgumentException("tried recording negative data"); 553 } 554 if (entry.isEmpty()) { 555 return; 556 } 557 558 // create any buckets needed by this range 559 ensureBuckets(start, end); 560 // Return fast if there is still no entry. This would typically happen when the start, 561 // end or duration are not valid values, e.g. start > end, negative duration value, etc. 562 if (bucketCount == 0) return; 563 564 // distribute data usage into buckets 565 long duration = end - start; 566 final int startIndex = getIndexAfter(end); 567 for (int i = startIndex; i >= 0; i--) { 568 final long curStart = bucketStart[i]; 569 final long curEnd = curStart + bucketDuration; 570 571 // bucket is older than record; we're finished 572 if (curEnd < start) break; 573 // bucket is newer than record; keep looking 574 if (curStart > end) continue; 575 576 final long overlap = Math.min(curEnd, end) - Math.max(curStart, start); 577 if (overlap <= 0) continue; 578 579 // integer math each time is faster than floating point 580 final long fracRxBytes = multiplySafeByRational(rxBytes, overlap, duration); 581 final long fracRxPackets = multiplySafeByRational(rxPackets, overlap, duration); 582 final long fracTxBytes = multiplySafeByRational(txBytes, overlap, duration); 583 final long fracTxPackets = multiplySafeByRational(txPackets, overlap, duration); 584 final long fracOperations = multiplySafeByRational(operations, overlap, duration); 585 586 587 addLong(activeTime, i, overlap); 588 addLong(this.rxBytes, i, fracRxBytes); rxBytes -= fracRxBytes; 589 addLong(this.rxPackets, i, fracRxPackets); rxPackets -= fracRxPackets; 590 addLong(this.txBytes, i, fracTxBytes); txBytes -= fracTxBytes; 591 addLong(this.txPackets, i, fracTxPackets); txPackets -= fracTxPackets; 592 addLong(this.operations, i, fracOperations); operations -= fracOperations; 593 594 duration -= overlap; 595 } 596 597 totalBytes += entry.rxBytes + entry.txBytes; 598 } 599 600 /** 601 * Record an entire {@link NetworkStatsHistory} into this history. Usually 602 * for combining together stats for external reporting. 603 * @hide 604 */ 605 @UnsupportedAppUsage recordEntireHistory(NetworkStatsHistory input)606 public void recordEntireHistory(NetworkStatsHistory input) { 607 recordHistory(input, Long.MIN_VALUE, Long.MAX_VALUE); 608 } 609 610 /** 611 * Record given {@link NetworkStatsHistory} into this history, copying only 612 * buckets that atomically occur in the inclusive time range. Doesn't 613 * interpolate across partial buckets. 614 * @hide 615 */ recordHistory(NetworkStatsHistory input, long start, long end)616 public void recordHistory(NetworkStatsHistory input, long start, long end) { 617 final NetworkStats.Entry entry = new NetworkStats.Entry( 618 IFACE_ALL, UID_ALL, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, 619 DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0L); 620 for (int i = 0; i < input.bucketCount; i++) { 621 final long bucketStart = input.bucketStart[i]; 622 final long bucketEnd = bucketStart + input.bucketDuration; 623 624 // skip when bucket is outside requested range 625 if (bucketStart < start || bucketEnd > end) continue; 626 627 entry.rxBytes = getLong(input.rxBytes, i, 0L); 628 entry.rxPackets = getLong(input.rxPackets, i, 0L); 629 entry.txBytes = getLong(input.txBytes, i, 0L); 630 entry.txPackets = getLong(input.txPackets, i, 0L); 631 entry.operations = getLong(input.operations, i, 0L); 632 633 recordData(bucketStart, bucketEnd, entry); 634 } 635 } 636 637 /** 638 * Ensure that buckets exist for given time range, creating as needed. 639 */ ensureBuckets(long start, long end)640 private void ensureBuckets(long start, long end) { 641 // normalize incoming range to bucket boundaries 642 start -= start % bucketDuration; 643 end += (bucketDuration - (end % bucketDuration)) % bucketDuration; 644 645 for (long now = start; now < end; now += bucketDuration) { 646 // try finding existing bucket 647 final int index = Arrays.binarySearch(bucketStart, 0, bucketCount, now); 648 if (index < 0) { 649 // bucket missing, create and insert 650 insertBucket(~index, now); 651 } 652 } 653 } 654 655 /** 656 * Insert new bucket at requested index and starting time. 657 */ insertBucket(int index, long start)658 private void insertBucket(int index, long start) { 659 // create more buckets when needed 660 if (bucketCount >= bucketStart.length) { 661 final int newLength = Math.max(bucketStart.length, 10) * 3 / 2; 662 bucketStart = Arrays.copyOf(bucketStart, newLength); 663 if (activeTime != null) activeTime = Arrays.copyOf(activeTime, newLength); 664 if (rxBytes != null) rxBytes = Arrays.copyOf(rxBytes, newLength); 665 if (rxPackets != null) rxPackets = Arrays.copyOf(rxPackets, newLength); 666 if (txBytes != null) txBytes = Arrays.copyOf(txBytes, newLength); 667 if (txPackets != null) txPackets = Arrays.copyOf(txPackets, newLength); 668 if (operations != null) operations = Arrays.copyOf(operations, newLength); 669 } 670 671 // create gap when inserting bucket in middle 672 if (index < bucketCount) { 673 final int dstPos = index + 1; 674 final int length = bucketCount - index; 675 676 System.arraycopy(bucketStart, index, bucketStart, dstPos, length); 677 if (activeTime != null) System.arraycopy(activeTime, index, activeTime, dstPos, length); 678 if (rxBytes != null) System.arraycopy(rxBytes, index, rxBytes, dstPos, length); 679 if (rxPackets != null) System.arraycopy(rxPackets, index, rxPackets, dstPos, length); 680 if (txBytes != null) System.arraycopy(txBytes, index, txBytes, dstPos, length); 681 if (txPackets != null) System.arraycopy(txPackets, index, txPackets, dstPos, length); 682 if (operations != null) System.arraycopy(operations, index, operations, dstPos, length); 683 } 684 685 bucketStart[index] = start; 686 setLong(activeTime, index, 0L); 687 setLong(rxBytes, index, 0L); 688 setLong(rxPackets, index, 0L); 689 setLong(txBytes, index, 0L); 690 setLong(txPackets, index, 0L); 691 setLong(operations, index, 0L); 692 bucketCount++; 693 } 694 695 /** 696 * Clear all data stored in this object. 697 * @hide 698 */ clear()699 public void clear() { 700 bucketStart = EmptyArray.LONG; 701 if (activeTime != null) activeTime = EmptyArray.LONG; 702 if (rxBytes != null) rxBytes = EmptyArray.LONG; 703 if (rxPackets != null) rxPackets = EmptyArray.LONG; 704 if (txBytes != null) txBytes = EmptyArray.LONG; 705 if (txPackets != null) txPackets = EmptyArray.LONG; 706 if (operations != null) operations = EmptyArray.LONG; 707 bucketCount = 0; 708 totalBytes = 0; 709 } 710 711 /** 712 * Remove buckets that start older than requested cutoff. 713 * 714 * This method will remove any bucket that contains any data older than the requested 715 * cutoff, even if that same bucket includes some data from after the cutoff. 716 * 717 * @hide 718 */ removeBucketsStartingBefore(final long cutoff)719 public void removeBucketsStartingBefore(final long cutoff) { 720 // TODO: Consider use getIndexBefore. 721 int i; 722 for (i = 0; i < bucketCount; i++) { 723 final long curStart = bucketStart[i]; 724 725 // This bucket starts after or at the cutoff, so it should be kept. 726 if (curStart >= cutoff) break; 727 } 728 729 if (i > 0) { 730 final int length = bucketStart.length; 731 bucketStart = Arrays.copyOfRange(bucketStart, i, length); 732 if (activeTime != null) activeTime = Arrays.copyOfRange(activeTime, i, length); 733 if (rxBytes != null) rxBytes = Arrays.copyOfRange(rxBytes, i, length); 734 if (rxPackets != null) rxPackets = Arrays.copyOfRange(rxPackets, i, length); 735 if (txBytes != null) txBytes = Arrays.copyOfRange(txBytes, i, length); 736 if (txPackets != null) txPackets = Arrays.copyOfRange(txPackets, i, length); 737 if (operations != null) operations = Arrays.copyOfRange(operations, i, length); 738 bucketCount -= i; 739 740 totalBytes = 0; 741 if (rxBytes != null) totalBytes += CollectionUtils.total(rxBytes); 742 if (txBytes != null) totalBytes += CollectionUtils.total(txBytes); 743 } 744 } 745 746 /** 747 * Return interpolated data usage across the requested range. Interpolates 748 * across buckets, so values may be rounded slightly. 749 * 750 * <p>If the active bucket is not completed yet, it returns the proportional value of it 751 * based on its duration and the {@code end} param. 752 * 753 * @param start - start of the range, timestamp in milliseconds since the epoch. 754 * @param end - end of the range, timestamp in milliseconds since the epoch. 755 * @param recycle - entry instance for performance, could be null. 756 * @hide 757 */ 758 @UnsupportedAppUsage getValues(long start, long end, Entry recycle)759 public Entry getValues(long start, long end, Entry recycle) { 760 return getValues(start, end, Long.MAX_VALUE, recycle); 761 } 762 763 /** 764 * Return interpolated data usage across the requested range. Interpolates 765 * across buckets, so values may be rounded slightly. 766 * 767 * @param start - start of the range, timestamp in milliseconds since the epoch. 768 * @param end - end of the range, timestamp in milliseconds since the epoch. 769 * @param now - current timestamp in milliseconds since the epoch (wall clock). 770 * @param recycle - entry instance for performance, could be null. 771 * @hide 772 */ 773 @UnsupportedAppUsage getValues(long start, long end, long now, Entry recycle)774 public Entry getValues(long start, long end, long now, Entry recycle) { 775 final Entry entry = recycle != null ? recycle : new Entry(); 776 entry.bucketDuration = end - start; 777 entry.bucketStart = start; 778 entry.activeTime = activeTime != null ? 0 : UNKNOWN; 779 entry.rxBytes = rxBytes != null ? 0 : UNKNOWN; 780 entry.rxPackets = rxPackets != null ? 0 : UNKNOWN; 781 entry.txBytes = txBytes != null ? 0 : UNKNOWN; 782 entry.txPackets = txPackets != null ? 0 : UNKNOWN; 783 entry.operations = operations != null ? 0 : UNKNOWN; 784 785 // Return fast if there is no entry. 786 if (bucketCount == 0) return entry; 787 788 final int startIndex = getIndexAfter(end); 789 for (int i = startIndex; i >= 0; i--) { 790 final long curStart = bucketStart[i]; 791 long curEnd = curStart + bucketDuration; 792 793 // bucket is older than request; we're finished 794 if (curEnd <= start) break; 795 // bucket is newer than request; keep looking 796 if (curStart >= end) continue; 797 798 // the active bucket is shorter then a normal completed bucket 799 if (curEnd > now) curEnd = now; 800 // usually this is simply bucketDuration 801 final long bucketSpan = curEnd - curStart; 802 // prevent division by zero 803 if (bucketSpan <= 0) continue; 804 805 final long overlapEnd = curEnd < end ? curEnd : end; 806 final long overlapStart = curStart > start ? curStart : start; 807 final long overlap = overlapEnd - overlapStart; 808 if (overlap <= 0) continue; 809 810 // integer math each time is faster than floating point 811 if (activeTime != null) { 812 entry.activeTime += multiplySafeByRational(activeTime[i], overlap, bucketSpan); 813 } 814 if (rxBytes != null) { 815 entry.rxBytes += multiplySafeByRational(rxBytes[i], overlap, bucketSpan); 816 } 817 if (rxPackets != null) { 818 entry.rxPackets += multiplySafeByRational(rxPackets[i], overlap, bucketSpan); 819 } 820 if (txBytes != null) { 821 entry.txBytes += multiplySafeByRational(txBytes[i], overlap, bucketSpan); 822 } 823 if (txPackets != null) { 824 entry.txPackets += multiplySafeByRational(txPackets[i], overlap, bucketSpan); 825 } 826 if (operations != null) { 827 entry.operations += multiplySafeByRational(operations[i], overlap, bucketSpan); 828 } 829 } 830 return entry; 831 } 832 833 /** 834 * @deprecated only for temporary testing 835 * @hide 836 */ 837 @Deprecated generateRandom(long start, long end, long bytes)838 public void generateRandom(long start, long end, long bytes) { 839 final Random r = new Random(); 840 841 final float fractionRx = r.nextFloat(); 842 final long rxBytes = (long) (bytes * fractionRx); 843 final long txBytes = (long) (bytes * (1 - fractionRx)); 844 845 final long rxPackets = rxBytes / 1024; 846 final long txPackets = txBytes / 1024; 847 final long operations = rxBytes / 2048; 848 849 generateRandom(start, end, rxBytes, rxPackets, txBytes, txPackets, operations, r); 850 } 851 852 /** 853 * @deprecated only for temporary testing 854 * @hide 855 */ 856 @Deprecated generateRandom(long start, long end, long rxBytes, long rxPackets, long txBytes, long txPackets, long operations, Random r)857 public void generateRandom(long start, long end, long rxBytes, long rxPackets, long txBytes, 858 long txPackets, long operations, Random r) { 859 ensureBuckets(start, end); 860 861 final NetworkStats.Entry entry = new NetworkStats.Entry( 862 IFACE_ALL, UID_ALL, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, 863 DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0L); 864 while (rxBytes > 1024 || rxPackets > 128 || txBytes > 1024 || txPackets > 128 865 || operations > 32) { 866 final long curStart = randomLong(r, start, end); 867 final long curEnd = curStart + randomLong(r, 0, (end - curStart) / 2); 868 869 entry.rxBytes = randomLong(r, 0, rxBytes); 870 entry.rxPackets = randomLong(r, 0, rxPackets); 871 entry.txBytes = randomLong(r, 0, txBytes); 872 entry.txPackets = randomLong(r, 0, txPackets); 873 entry.operations = randomLong(r, 0, operations); 874 875 rxBytes -= entry.rxBytes; 876 rxPackets -= entry.rxPackets; 877 txBytes -= entry.txBytes; 878 txPackets -= entry.txPackets; 879 operations -= entry.operations; 880 881 recordData(curStart, curEnd, entry); 882 } 883 } 884 885 /** @hide */ randomLong(Random r, long start, long end)886 public static long randomLong(Random r, long start, long end) { 887 return (long) (start + (r.nextFloat() * (end - start))); 888 } 889 890 /** 891 * Quickly determine if this history intersects with given window. 892 * @hide 893 */ intersects(long start, long end)894 public boolean intersects(long start, long end) { 895 final long dataStart = getStart(); 896 final long dataEnd = getEnd(); 897 if (start >= dataStart && start <= dataEnd) return true; 898 if (end >= dataStart && end <= dataEnd) return true; 899 if (dataStart >= start && dataStart <= end) return true; 900 if (dataEnd >= start && dataEnd <= end) return true; 901 return false; 902 } 903 904 /** @hide */ dump(IndentingPrintWriter pw, boolean fullHistory)905 public void dump(IndentingPrintWriter pw, boolean fullHistory) { 906 pw.print("NetworkStatsHistory: bucketDuration="); 907 pw.println(bucketDuration / SECOND_IN_MILLIS); 908 pw.increaseIndent(); 909 910 final int start = fullHistory ? 0 : Math.max(0, bucketCount - 32); 911 if (start > 0) { 912 pw.print("(omitting "); pw.print(start); pw.println(" buckets)"); 913 } 914 915 for (int i = start; i < bucketCount; i++) { 916 pw.print("st="); pw.print(bucketStart[i] / SECOND_IN_MILLIS); 917 if (rxBytes != null) { pw.print(" rb="); pw.print(rxBytes[i]); } 918 if (rxPackets != null) { pw.print(" rp="); pw.print(rxPackets[i]); } 919 if (txBytes != null) { pw.print(" tb="); pw.print(txBytes[i]); } 920 if (txPackets != null) { pw.print(" tp="); pw.print(txPackets[i]); } 921 if (operations != null) { pw.print(" op="); pw.print(operations[i]); } 922 pw.println(); 923 } 924 925 pw.decreaseIndent(); 926 } 927 928 /** @hide */ dumpCheckin(PrintWriter pw)929 public void dumpCheckin(PrintWriter pw) { 930 pw.print("d,"); 931 pw.print(bucketDuration / SECOND_IN_MILLIS); 932 pw.println(); 933 934 for (int i = 0; i < bucketCount; i++) { 935 pw.print("b,"); 936 pw.print(bucketStart[i] / SECOND_IN_MILLIS); pw.print(','); 937 if (rxBytes != null) { pw.print(rxBytes[i]); } else { pw.print("*"); } pw.print(','); 938 if (rxPackets != null) { pw.print(rxPackets[i]); } else { pw.print("*"); } pw.print(','); 939 if (txBytes != null) { pw.print(txBytes[i]); } else { pw.print("*"); } pw.print(','); 940 if (txPackets != null) { pw.print(txPackets[i]); } else { pw.print("*"); } pw.print(','); 941 if (operations != null) { pw.print(operations[i]); } else { pw.print("*"); } 942 pw.println(); 943 } 944 } 945 946 /** @hide */ dumpDebug(ProtoOutputStream proto, long tag)947 public void dumpDebug(ProtoOutputStream proto, long tag) { 948 final long start = proto.start(tag); 949 950 proto.write(NetworkStatsHistoryProto.BUCKET_DURATION_MS, bucketDuration); 951 952 for (int i = 0; i < bucketCount; i++) { 953 final long startBucket = proto.start(NetworkStatsHistoryProto.BUCKETS); 954 955 proto.write(NetworkStatsHistoryBucketProto.BUCKET_START_MS, 956 bucketStart[i]); 957 dumpDebug(proto, NetworkStatsHistoryBucketProto.RX_BYTES, rxBytes, i); 958 dumpDebug(proto, NetworkStatsHistoryBucketProto.RX_PACKETS, rxPackets, i); 959 dumpDebug(proto, NetworkStatsHistoryBucketProto.TX_BYTES, txBytes, i); 960 dumpDebug(proto, NetworkStatsHistoryBucketProto.TX_PACKETS, txPackets, i); 961 dumpDebug(proto, NetworkStatsHistoryBucketProto.OPERATIONS, operations, i); 962 963 proto.end(startBucket); 964 } 965 966 proto.end(start); 967 } 968 dumpDebug(ProtoOutputStream proto, long tag, long[] array, int index)969 private static void dumpDebug(ProtoOutputStream proto, long tag, long[] array, int index) { 970 if (array != null) { 971 proto.write(tag, array[index]); 972 } 973 } 974 975 @Override toString()976 public String toString() { 977 final CharArrayWriter writer = new CharArrayWriter(); 978 dump(new IndentingPrintWriter(writer, " "), false); 979 return writer.toString(); 980 } 981 982 /** 983 * Same as "equals", but not actually called equals as this would affect public API behavior. 984 * @hide 985 */ 986 @Nullable isSameAs(NetworkStatsHistory other)987 public boolean isSameAs(NetworkStatsHistory other) { 988 return bucketCount == other.bucketCount 989 && Arrays.equals(bucketStart, other.bucketStart) 990 // Don't check activeTime since it can change on import due to the importer using 991 // recordHistory. It's also not exposed by the APIs or present in dumpsys or 992 // toString(). 993 && Arrays.equals(rxBytes, other.rxBytes) 994 && Arrays.equals(rxPackets, other.rxPackets) 995 && Arrays.equals(txBytes, other.txBytes) 996 && Arrays.equals(txPackets, other.txPackets) 997 && Arrays.equals(operations, other.operations) 998 && totalBytes == other.totalBytes; 999 } 1000 1001 @UnsupportedAppUsage 1002 public static final @android.annotation.NonNull Creator<NetworkStatsHistory> CREATOR = new Creator<NetworkStatsHistory>() { 1003 @Override 1004 public NetworkStatsHistory createFromParcel(Parcel in) { 1005 return new NetworkStatsHistory(in); 1006 } 1007 1008 @Override 1009 public NetworkStatsHistory[] newArray(int size) { 1010 return new NetworkStatsHistory[size]; 1011 } 1012 }; 1013 getLong(long[] array, int i, long value)1014 private static long getLong(long[] array, int i, long value) { 1015 return array != null ? array[i] : value; 1016 } 1017 setLong(long[] array, int i, long value)1018 private static void setLong(long[] array, int i, long value) { 1019 if (array != null) array[i] = value; 1020 } 1021 addLong(long[] array, int i, long value)1022 private static void addLong(long[] array, int i, long value) { 1023 if (array != null) array[i] += value; 1024 } 1025 1026 /** @hide */ estimateResizeBuckets(long newBucketDuration)1027 public int estimateResizeBuckets(long newBucketDuration) { 1028 return (int) (size() * getBucketDuration() / newBucketDuration); 1029 } 1030 1031 /** 1032 * Utility methods for interacting with {@link DataInputStream} and 1033 * {@link DataOutputStream}, mostly dealing with writing partial arrays. 1034 * @hide 1035 */ 1036 public static class DataStreamUtils { 1037 @Deprecated readFullLongArray(DataInput in)1038 public static long[] readFullLongArray(DataInput in) throws IOException { 1039 final int size = in.readInt(); 1040 if (size < 0) throw new ProtocolException("negative array size"); 1041 final long[] values = new long[size]; 1042 for (int i = 0; i < values.length; i++) { 1043 values[i] = in.readLong(); 1044 } 1045 return values; 1046 } 1047 1048 /** 1049 * Read variable-length {@link Long} using protobuf-style approach. 1050 */ readVarLong(DataInput in)1051 public static long readVarLong(DataInput in) throws IOException { 1052 int shift = 0; 1053 long result = 0; 1054 while (shift < 64) { 1055 byte b = in.readByte(); 1056 result |= (long) (b & 0x7F) << shift; 1057 if ((b & 0x80) == 0) 1058 return result; 1059 shift += 7; 1060 } 1061 throw new ProtocolException("malformed long"); 1062 } 1063 1064 /** 1065 * Write variable-length {@link Long} using protobuf-style approach. 1066 */ writeVarLong(DataOutput out, long value)1067 public static void writeVarLong(DataOutput out, long value) throws IOException { 1068 while (true) { 1069 if ((value & ~0x7FL) == 0) { 1070 out.writeByte((int) value); 1071 return; 1072 } else { 1073 out.writeByte(((int) value & 0x7F) | 0x80); 1074 value >>>= 7; 1075 } 1076 } 1077 } 1078 readVarLongArray(DataInput in)1079 public static long[] readVarLongArray(DataInput in) throws IOException { 1080 final int size = in.readInt(); 1081 if (size == -1) return null; 1082 if (size < 0) throw new ProtocolException("negative array size"); 1083 final long[] values = new long[size]; 1084 for (int i = 0; i < values.length; i++) { 1085 values[i] = readVarLong(in); 1086 } 1087 return values; 1088 } 1089 writeVarLongArray(DataOutput out, long[] values, int size)1090 public static void writeVarLongArray(DataOutput out, long[] values, int size) 1091 throws IOException { 1092 if (values == null) { 1093 out.writeInt(-1); 1094 return; 1095 } 1096 if (size > values.length) { 1097 throw new IllegalArgumentException("size larger than length"); 1098 } 1099 out.writeInt(size); 1100 for (int i = 0; i < size; i++) { 1101 writeVarLong(out, values[i]); 1102 } 1103 } 1104 } 1105 1106 /** 1107 * Utility methods for interacting with {@link Parcel} structures, mostly 1108 * dealing with writing partial arrays. 1109 * @hide 1110 */ 1111 public static class ParcelUtils { readLongArray(Parcel in)1112 public static long[] readLongArray(Parcel in) { 1113 final int size = in.readInt(); 1114 if (size == -1) return null; 1115 final long[] values = new long[size]; 1116 for (int i = 0; i < values.length; i++) { 1117 values[i] = in.readLong(); 1118 } 1119 return values; 1120 } 1121 writeLongArray(Parcel out, long[] values, int size)1122 public static void writeLongArray(Parcel out, long[] values, int size) { 1123 if (values == null) { 1124 out.writeInt(-1); 1125 return; 1126 } 1127 if (size > values.length) { 1128 throw new IllegalArgumentException("size larger than length"); 1129 } 1130 out.writeInt(size); 1131 for (int i = 0; i < size; i++) { 1132 out.writeLong(values[i]); 1133 } 1134 } 1135 } 1136 1137 /** 1138 * Builder class for {@link NetworkStatsHistory}. 1139 */ 1140 public static final class Builder { 1141 private final TreeMap<Long, Entry> mEntries; 1142 private final long mBucketDuration; 1143 1144 /** 1145 * Creates a new Builder with given bucket duration and initial capacity to construct 1146 * {@link NetworkStatsHistory} objects. 1147 * 1148 * @param bucketDuration Duration of the buckets of the object, in milliseconds. 1149 * @param initialCapacity Estimated number of records. 1150 */ Builder(long bucketDuration, int initialCapacity)1151 public Builder(long bucketDuration, int initialCapacity) { 1152 mBucketDuration = bucketDuration; 1153 // Create a collection that is always sorted and can deduplicate items by the timestamp. 1154 mEntries = new TreeMap<>(); 1155 } 1156 1157 /** 1158 * Add an {@link Entry} into the {@link NetworkStatsHistory} instance. If the timestamp 1159 * already exists, the given {@link Entry} will be combined into existing entry. 1160 * 1161 * @param entry The target {@link Entry} object. 1162 * @return The builder object. 1163 */ 1164 @NonNull addEntry(@onNull Entry entry)1165 public Builder addEntry(@NonNull Entry entry) { 1166 final Entry existing = mEntries.get(entry.bucketStart); 1167 if (existing != null) { 1168 mEntries.put(entry.bucketStart, existing.plus(entry, mBucketDuration)); 1169 } else { 1170 mEntries.put(entry.bucketStart, entry); 1171 } 1172 return this; 1173 } 1174 sum(@onNull long[] array)1175 private static long sum(@NonNull long[] array) { 1176 long sum = 0L; 1177 for (long entry : array) { 1178 sum += entry; 1179 } 1180 return sum; 1181 } 1182 1183 /** 1184 * Builds the instance of the {@link NetworkStatsHistory}. 1185 * 1186 * @return the built instance of {@link NetworkStatsHistory}. 1187 */ 1188 @NonNull build()1189 public NetworkStatsHistory build() { 1190 int size = mEntries.size(); 1191 final long[] bucketStart = new long[size]; 1192 final long[] activeTime = new long[size]; 1193 final long[] rxBytes = new long[size]; 1194 final long[] rxPackets = new long[size]; 1195 final long[] txBytes = new long[size]; 1196 final long[] txPackets = new long[size]; 1197 final long[] operations = new long[size]; 1198 1199 int i = 0; 1200 for (Entry entry : mEntries.values()) { 1201 bucketStart[i] = entry.bucketStart; 1202 activeTime[i] = entry.activeTime; 1203 rxBytes[i] = entry.rxBytes; 1204 rxPackets[i] = entry.rxPackets; 1205 txBytes[i] = entry.txBytes; 1206 txPackets[i] = entry.txPackets; 1207 operations[i] = entry.operations; 1208 i++; 1209 } 1210 1211 return new NetworkStatsHistory(mBucketDuration, bucketStart, activeTime, 1212 rxBytes, rxPackets, txBytes, txPackets, operations, 1213 size, sum(rxBytes) + sum(txBytes)); 1214 } 1215 } 1216 } 1217