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