Home | History | Annotate | Download | only in net
      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