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