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