1 /* 2 * Copyright (C) 2012 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 com.android.server.net; 18 19 import static android.net.NetworkStats.IFACE_ALL; 20 import static android.net.NetworkStats.SET_ALL; 21 import static android.net.NetworkStats.SET_DEFAULT; 22 import static android.net.NetworkStats.TAG_NONE; 23 import static android.net.NetworkStats.UID_ALL; 24 import static android.net.TrafficStats.UID_REMOVED; 25 import static android.net.TrafficStats.UID_TETHERING; 26 import static android.text.format.DateUtils.SECOND_IN_MILLIS; 27 import static android.text.format.DateUtils.WEEK_IN_MILLIS; 28 29 import android.net.ConnectivityManager; 30 import android.net.NetworkIdentity; 31 import android.net.NetworkStats; 32 import android.net.NetworkStatsHistory; 33 import android.net.NetworkTemplate; 34 import android.net.TrafficStats; 35 import android.os.Binder; 36 import android.os.UserHandle; 37 import android.util.ArrayMap; 38 import android.util.AtomicFile; 39 import android.util.IntArray; 40 41 import libcore.io.IoUtils; 42 43 import com.android.internal.util.ArrayUtils; 44 import com.android.internal.util.FileRotator; 45 import com.android.internal.util.IndentingPrintWriter; 46 47 import com.google.android.collect.Lists; 48 import com.google.android.collect.Maps; 49 50 import java.io.BufferedInputStream; 51 import java.io.DataInputStream; 52 import java.io.DataOutputStream; 53 import java.io.File; 54 import java.io.FileNotFoundException; 55 import java.io.IOException; 56 import java.io.InputStream; 57 import java.io.PrintWriter; 58 import java.net.ProtocolException; 59 import java.util.ArrayList; 60 import java.util.Collections; 61 import java.util.HashMap; 62 import java.util.Objects; 63 64 /** 65 * Collection of {@link NetworkStatsHistory}, stored based on combined key of 66 * {@link NetworkIdentitySet}, UID, set, and tag. Knows how to persist itself. 67 */ 68 public class NetworkStatsCollection implements FileRotator.Reader { 69 /** File header magic number: "ANET" */ 70 private static final int FILE_MAGIC = 0x414E4554; 71 72 private static final int VERSION_NETWORK_INIT = 1; 73 74 private static final int VERSION_UID_INIT = 1; 75 private static final int VERSION_UID_WITH_IDENT = 2; 76 private static final int VERSION_UID_WITH_TAG = 3; 77 private static final int VERSION_UID_WITH_SET = 4; 78 79 private static final int VERSION_UNIFIED_INIT = 16; 80 81 private ArrayMap<Key, NetworkStatsHistory> mStats = new ArrayMap<>(); 82 83 private final long mBucketDuration; 84 85 private long mStartMillis; 86 private long mEndMillis; 87 private long mTotalBytes; 88 private boolean mDirty; 89 90 public NetworkStatsCollection(long bucketDuration) { 91 mBucketDuration = bucketDuration; 92 reset(); 93 } 94 95 public void reset() { 96 mStats.clear(); 97 mStartMillis = Long.MAX_VALUE; 98 mEndMillis = Long.MIN_VALUE; 99 mTotalBytes = 0; 100 mDirty = false; 101 } 102 103 public long getStartMillis() { 104 return mStartMillis; 105 } 106 107 /** 108 * Return first atomic bucket in this collection, which is more conservative 109 * than {@link #mStartMillis}. 110 */ 111 public long getFirstAtomicBucketMillis() { 112 if (mStartMillis == Long.MAX_VALUE) { 113 return Long.MAX_VALUE; 114 } else { 115 return mStartMillis + mBucketDuration; 116 } 117 } 118 119 public long getEndMillis() { 120 return mEndMillis; 121 } 122 123 public long getTotalBytes() { 124 return mTotalBytes; 125 } 126 127 public boolean isDirty() { 128 return mDirty; 129 } 130 131 public void clearDirty() { 132 mDirty = false; 133 } 134 135 public boolean isEmpty() { 136 return mStartMillis == Long.MAX_VALUE && mEndMillis == Long.MIN_VALUE; 137 } 138 139 public int[] getRelevantUids() { 140 final int callerUid = Binder.getCallingUid(); 141 IntArray uids = new IntArray(); 142 for (int i = 0; i < mStats.size(); i++) { 143 final Key key = mStats.keyAt(i); 144 if (isAccessibleToUser(key.uid, callerUid)) { 145 int j = uids.binarySearch(key.uid); 146 147 if (j < 0) { 148 j = ~j; 149 uids.add(j, key.uid); 150 } 151 } 152 } 153 return uids.toArray(); 154 } 155 156 /** 157 * Combine all {@link NetworkStatsHistory} in this collection which match 158 * the requested parameters. 159 */ 160 public NetworkStatsHistory getHistory( 161 NetworkTemplate template, int uid, int set, int tag, int fields) { 162 return getHistory(template, uid, set, tag, fields, Long.MIN_VALUE, Long.MAX_VALUE); 163 } 164 165 /** 166 * Combine all {@link NetworkStatsHistory} in this collection which match 167 * the requested parameters. 168 */ 169 public NetworkStatsHistory getHistory( 170 NetworkTemplate template, int uid, int set, int tag, int fields, long start, long end) { 171 final int callerUid = Binder.getCallingUid(); 172 if (!isAccessibleToUser(uid, callerUid)) { 173 throw new SecurityException("Network stats history of uid " + uid 174 + " is forbidden for caller " + callerUid); 175 } 176 177 final NetworkStatsHistory combined = new NetworkStatsHistory( 178 mBucketDuration, start == end ? 1 : estimateBuckets(), fields); 179 180 // shortcut when we know stats will be empty 181 if (start == end) return combined; 182 183 for (int i = 0; i < mStats.size(); i++) { 184 final Key key = mStats.keyAt(i); 185 if (key.uid == uid && NetworkStats.setMatches(set, key.set) && key.tag == tag 186 && templateMatches(template, key.ident)) { 187 final NetworkStatsHistory value = mStats.valueAt(i); 188 combined.recordHistory(value, start, end); 189 } 190 } 191 return combined; 192 } 193 194 /** 195 * Summarize all {@link NetworkStatsHistory} in this collection which match 196 * the requested parameters. 197 */ 198 public NetworkStats getSummary(NetworkTemplate template, long start, long end) { 199 final long now = System.currentTimeMillis(); 200 201 final NetworkStats stats = new NetworkStats(end - start, 24); 202 // shortcut when we know stats will be empty 203 if (start == end) return stats; 204 205 final NetworkStats.Entry entry = new NetworkStats.Entry(); 206 NetworkStatsHistory.Entry historyEntry = null; 207 208 final int callerUid = Binder.getCallingUid(); 209 for (int i = 0; i < mStats.size(); i++) { 210 final Key key = mStats.keyAt(i); 211 if (templateMatches(template, key.ident) && isAccessibleToUser(key.uid, callerUid) 212 && key.set < NetworkStats.SET_DEBUG_START) { 213 final NetworkStatsHistory value = mStats.valueAt(i); 214 historyEntry = value.getValues(start, end, now, historyEntry); 215 216 entry.iface = IFACE_ALL; 217 entry.uid = key.uid; 218 entry.set = key.set; 219 entry.tag = key.tag; 220 entry.rxBytes = historyEntry.rxBytes; 221 entry.rxPackets = historyEntry.rxPackets; 222 entry.txBytes = historyEntry.txBytes; 223 entry.txPackets = historyEntry.txPackets; 224 entry.operations = historyEntry.operations; 225 226 if (!entry.isEmpty()) { 227 stats.combineValues(entry); 228 } 229 } 230 } 231 232 return stats; 233 } 234 235 /** 236 * Record given {@link android.net.NetworkStats.Entry} into this collection. 237 */ 238 public void recordData(NetworkIdentitySet ident, int uid, int set, int tag, long start, 239 long end, NetworkStats.Entry entry) { 240 final NetworkStatsHistory history = findOrCreateHistory(ident, uid, set, tag); 241 history.recordData(start, end, entry); 242 noteRecordedHistory(history.getStart(), history.getEnd(), entry.rxBytes + entry.txBytes); 243 } 244 245 /** 246 * Record given {@link NetworkStatsHistory} into this collection. 247 */ 248 private void recordHistory(Key key, NetworkStatsHistory history) { 249 if (history.size() == 0) return; 250 noteRecordedHistory(history.getStart(), history.getEnd(), history.getTotalBytes()); 251 252 NetworkStatsHistory target = mStats.get(key); 253 if (target == null) { 254 target = new NetworkStatsHistory(history.getBucketDuration()); 255 mStats.put(key, target); 256 } 257 target.recordEntireHistory(history); 258 } 259 260 /** 261 * Record all {@link NetworkStatsHistory} contained in the given collection 262 * into this collection. 263 */ 264 public void recordCollection(NetworkStatsCollection another) { 265 for (int i = 0; i < another.mStats.size(); i++) { 266 final Key key = another.mStats.keyAt(i); 267 final NetworkStatsHistory value = another.mStats.valueAt(i); 268 recordHistory(key, value); 269 } 270 } 271 272 private NetworkStatsHistory findOrCreateHistory( 273 NetworkIdentitySet ident, int uid, int set, int tag) { 274 final Key key = new Key(ident, uid, set, tag); 275 final NetworkStatsHistory existing = mStats.get(key); 276 277 // update when no existing, or when bucket duration changed 278 NetworkStatsHistory updated = null; 279 if (existing == null) { 280 updated = new NetworkStatsHistory(mBucketDuration, 10); 281 } else if (existing.getBucketDuration() != mBucketDuration) { 282 updated = new NetworkStatsHistory(existing, mBucketDuration); 283 } 284 285 if (updated != null) { 286 mStats.put(key, updated); 287 return updated; 288 } else { 289 return existing; 290 } 291 } 292 293 @Override 294 public void read(InputStream in) throws IOException { 295 read(new DataInputStream(in)); 296 } 297 298 public void read(DataInputStream in) throws IOException { 299 // verify file magic header intact 300 final int magic = in.readInt(); 301 if (magic != FILE_MAGIC) { 302 throw new ProtocolException("unexpected magic: " + magic); 303 } 304 305 final int version = in.readInt(); 306 switch (version) { 307 case VERSION_UNIFIED_INIT: { 308 // uid := size *(NetworkIdentitySet size *(uid set tag NetworkStatsHistory)) 309 final int identSize = in.readInt(); 310 for (int i = 0; i < identSize; i++) { 311 final NetworkIdentitySet ident = new NetworkIdentitySet(in); 312 313 final int size = in.readInt(); 314 for (int j = 0; j < size; j++) { 315 final int uid = in.readInt(); 316 final int set = in.readInt(); 317 final int tag = in.readInt(); 318 319 final Key key = new Key(ident, uid, set, tag); 320 final NetworkStatsHistory history = new NetworkStatsHistory(in); 321 recordHistory(key, history); 322 } 323 } 324 break; 325 } 326 default: { 327 throw new ProtocolException("unexpected version: " + version); 328 } 329 } 330 } 331 332 public void write(DataOutputStream out) throws IOException { 333 // cluster key lists grouped by ident 334 final HashMap<NetworkIdentitySet, ArrayList<Key>> keysByIdent = Maps.newHashMap(); 335 for (Key key : mStats.keySet()) { 336 ArrayList<Key> keys = keysByIdent.get(key.ident); 337 if (keys == null) { 338 keys = Lists.newArrayList(); 339 keysByIdent.put(key.ident, keys); 340 } 341 keys.add(key); 342 } 343 344 out.writeInt(FILE_MAGIC); 345 out.writeInt(VERSION_UNIFIED_INIT); 346 347 out.writeInt(keysByIdent.size()); 348 for (NetworkIdentitySet ident : keysByIdent.keySet()) { 349 final ArrayList<Key> keys = keysByIdent.get(ident); 350 ident.writeToStream(out); 351 352 out.writeInt(keys.size()); 353 for (Key key : keys) { 354 final NetworkStatsHistory history = mStats.get(key); 355 out.writeInt(key.uid); 356 out.writeInt(key.set); 357 out.writeInt(key.tag); 358 history.writeToStream(out); 359 } 360 } 361 362 out.flush(); 363 } 364 365 @Deprecated 366 public void readLegacyNetwork(File file) throws IOException { 367 final AtomicFile inputFile = new AtomicFile(file); 368 369 DataInputStream in = null; 370 try { 371 in = new DataInputStream(new BufferedInputStream(inputFile.openRead())); 372 373 // verify file magic header intact 374 final int magic = in.readInt(); 375 if (magic != FILE_MAGIC) { 376 throw new ProtocolException("unexpected magic: " + magic); 377 } 378 379 final int version = in.readInt(); 380 switch (version) { 381 case VERSION_NETWORK_INIT: { 382 // network := size *(NetworkIdentitySet NetworkStatsHistory) 383 final int size = in.readInt(); 384 for (int i = 0; i < size; i++) { 385 final NetworkIdentitySet ident = new NetworkIdentitySet(in); 386 final NetworkStatsHistory history = new NetworkStatsHistory(in); 387 388 final Key key = new Key(ident, UID_ALL, SET_ALL, TAG_NONE); 389 recordHistory(key, history); 390 } 391 break; 392 } 393 default: { 394 throw new ProtocolException("unexpected version: " + version); 395 } 396 } 397 } catch (FileNotFoundException e) { 398 // missing stats is okay, probably first boot 399 } finally { 400 IoUtils.closeQuietly(in); 401 } 402 } 403 404 @Deprecated 405 public void readLegacyUid(File file, boolean onlyTags) throws IOException { 406 final AtomicFile inputFile = new AtomicFile(file); 407 408 DataInputStream in = null; 409 try { 410 in = new DataInputStream(new BufferedInputStream(inputFile.openRead())); 411 412 // verify file magic header intact 413 final int magic = in.readInt(); 414 if (magic != FILE_MAGIC) { 415 throw new ProtocolException("unexpected magic: " + magic); 416 } 417 418 final int version = in.readInt(); 419 switch (version) { 420 case VERSION_UID_INIT: { 421 // uid := size *(UID NetworkStatsHistory) 422 423 // drop this data version, since we don't have a good 424 // mapping into NetworkIdentitySet. 425 break; 426 } 427 case VERSION_UID_WITH_IDENT: { 428 // uid := size *(NetworkIdentitySet size *(UID NetworkStatsHistory)) 429 430 // drop this data version, since this version only existed 431 // for a short time. 432 break; 433 } 434 case VERSION_UID_WITH_TAG: 435 case VERSION_UID_WITH_SET: { 436 // uid := size *(NetworkIdentitySet size *(uid set tag NetworkStatsHistory)) 437 final int identSize = in.readInt(); 438 for (int i = 0; i < identSize; i++) { 439 final NetworkIdentitySet ident = new NetworkIdentitySet(in); 440 441 final int size = in.readInt(); 442 for (int j = 0; j < size; j++) { 443 final int uid = in.readInt(); 444 final int set = (version >= VERSION_UID_WITH_SET) ? in.readInt() 445 : SET_DEFAULT; 446 final int tag = in.readInt(); 447 448 final Key key = new Key(ident, uid, set, tag); 449 final NetworkStatsHistory history = new NetworkStatsHistory(in); 450 451 if ((tag == TAG_NONE) != onlyTags) { 452 recordHistory(key, history); 453 } 454 } 455 } 456 break; 457 } 458 default: { 459 throw new ProtocolException("unexpected version: " + version); 460 } 461 } 462 } catch (FileNotFoundException e) { 463 // missing stats is okay, probably first boot 464 } finally { 465 IoUtils.closeQuietly(in); 466 } 467 } 468 469 /** 470 * Remove any {@link NetworkStatsHistory} attributed to the requested UID, 471 * moving any {@link NetworkStats#TAG_NONE} series to 472 * {@link TrafficStats#UID_REMOVED}. 473 */ 474 public void removeUids(int[] uids) { 475 final ArrayList<Key> knownKeys = Lists.newArrayList(); 476 knownKeys.addAll(mStats.keySet()); 477 478 // migrate all UID stats into special "removed" bucket 479 for (Key key : knownKeys) { 480 if (ArrayUtils.contains(uids, key.uid)) { 481 // only migrate combined TAG_NONE history 482 if (key.tag == TAG_NONE) { 483 final NetworkStatsHistory uidHistory = mStats.get(key); 484 final NetworkStatsHistory removedHistory = findOrCreateHistory( 485 key.ident, UID_REMOVED, SET_DEFAULT, TAG_NONE); 486 removedHistory.recordEntireHistory(uidHistory); 487 } 488 mStats.remove(key); 489 mDirty = true; 490 } 491 } 492 } 493 494 private void noteRecordedHistory(long startMillis, long endMillis, long totalBytes) { 495 if (startMillis < mStartMillis) mStartMillis = startMillis; 496 if (endMillis > mEndMillis) mEndMillis = endMillis; 497 mTotalBytes += totalBytes; 498 mDirty = true; 499 } 500 501 private int estimateBuckets() { 502 return (int) (Math.min(mEndMillis - mStartMillis, WEEK_IN_MILLIS * 5) 503 / mBucketDuration); 504 } 505 506 public void dump(IndentingPrintWriter pw) { 507 final ArrayList<Key> keys = Lists.newArrayList(); 508 keys.addAll(mStats.keySet()); 509 Collections.sort(keys); 510 511 for (Key key : keys) { 512 pw.print("ident="); pw.print(key.ident.toString()); 513 pw.print(" uid="); pw.print(key.uid); 514 pw.print(" set="); pw.print(NetworkStats.setToString(key.set)); 515 pw.print(" tag="); pw.println(NetworkStats.tagToString(key.tag)); 516 517 final NetworkStatsHistory history = mStats.get(key); 518 pw.increaseIndent(); 519 history.dump(pw, true); 520 pw.decreaseIndent(); 521 } 522 } 523 524 public void dumpCheckin(PrintWriter pw, long start, long end) { 525 dumpCheckin(pw, start, end, NetworkTemplate.buildTemplateMobileWildcard(), "cell"); 526 dumpCheckin(pw, start, end, NetworkTemplate.buildTemplateWifiWildcard(), "wifi"); 527 dumpCheckin(pw, start, end, NetworkTemplate.buildTemplateEthernet(), "eth"); 528 dumpCheckin(pw, start, end, NetworkTemplate.buildTemplateBluetooth(), "bt"); 529 } 530 531 /** 532 * Dump all contained stats that match requested parameters, but group 533 * together all matching {@link NetworkTemplate} under a single prefix. 534 */ 535 private void dumpCheckin(PrintWriter pw, long start, long end, NetworkTemplate groupTemplate, 536 String groupPrefix) { 537 final ArrayMap<Key, NetworkStatsHistory> grouped = new ArrayMap<>(); 538 539 // Walk through all history, grouping by matching network templates 540 for (int i = 0; i < mStats.size(); i++) { 541 final Key key = mStats.keyAt(i); 542 final NetworkStatsHistory value = mStats.valueAt(i); 543 544 if (!templateMatches(groupTemplate, key.ident)) continue; 545 if (key.set >= NetworkStats.SET_DEBUG_START) continue; 546 547 final Key groupKey = new Key(null, key.uid, key.set, key.tag); 548 NetworkStatsHistory groupHistory = grouped.get(groupKey); 549 if (groupHistory == null) { 550 groupHistory = new NetworkStatsHistory(value.getBucketDuration()); 551 grouped.put(groupKey, groupHistory); 552 } 553 groupHistory.recordHistory(value, start, end); 554 } 555 556 for (int i = 0; i < grouped.size(); i++) { 557 final Key key = grouped.keyAt(i); 558 final NetworkStatsHistory value = grouped.valueAt(i); 559 560 if (value.size() == 0) continue; 561 562 pw.print("c,"); 563 pw.print(groupPrefix); pw.print(','); 564 pw.print(key.uid); pw.print(','); 565 pw.print(NetworkStats.setToCheckinString(key.set)); pw.print(','); 566 pw.print(key.tag); 567 pw.println(); 568 569 value.dumpCheckin(pw); 570 } 571 } 572 573 private static boolean isAccessibleToUser(int uid, int callerUid) { 574 return UserHandle.getAppId(callerUid) == android.os.Process.SYSTEM_UID || 575 uid == android.os.Process.SYSTEM_UID || uid == UID_REMOVED || uid == UID_TETHERING 576 || UserHandle.getUserId(uid) == UserHandle.getUserId(callerUid); 577 } 578 579 /** 580 * Test if given {@link NetworkTemplate} matches any {@link NetworkIdentity} 581 * in the given {@link NetworkIdentitySet}. 582 */ 583 private static boolean templateMatches(NetworkTemplate template, NetworkIdentitySet identSet) { 584 for (NetworkIdentity ident : identSet) { 585 if (template.matches(ident)) { 586 return true; 587 } 588 } 589 return false; 590 } 591 592 private static class Key implements Comparable<Key> { 593 public final NetworkIdentitySet ident; 594 public final int uid; 595 public final int set; 596 public final int tag; 597 598 private final int hashCode; 599 600 public Key(NetworkIdentitySet ident, int uid, int set, int tag) { 601 this.ident = ident; 602 this.uid = uid; 603 this.set = set; 604 this.tag = tag; 605 hashCode = Objects.hash(ident, uid, set, tag); 606 } 607 608 @Override 609 public int hashCode() { 610 return hashCode; 611 } 612 613 @Override 614 public boolean equals(Object obj) { 615 if (obj instanceof Key) { 616 final Key key = (Key) obj; 617 return uid == key.uid && set == key.set && tag == key.tag 618 && Objects.equals(ident, key.ident); 619 } 620 return false; 621 } 622 623 @Override 624 public int compareTo(Key another) { 625 int res = 0; 626 if (ident != null && another.ident != null) { 627 res = ident.compareTo(another.ident); 628 } 629 if (res == 0) { 630 res = Integer.compare(uid, another.uid); 631 } 632 if (res == 0) { 633 res = Integer.compare(set, another.set); 634 } 635 if (res == 0) { 636 res = Integer.compare(tag, another.tag); 637 } 638 return res; 639 } 640 } 641 } 642