• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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