• 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 android.net;
18 
19 import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
20 import static android.net.NetworkStats.DEFAULT_NETWORK_NO;
21 import static android.net.NetworkStats.DEFAULT_NETWORK_YES;
22 import static android.net.NetworkStats.IFACE_ALL;
23 import static android.net.NetworkStats.METERED_NO;
24 import static android.net.NetworkStats.METERED_YES;
25 import static android.net.NetworkStats.ROAMING_NO;
26 import static android.net.NetworkStats.ROAMING_YES;
27 import static android.net.NetworkStats.SET_ALL;
28 import static android.net.NetworkStats.SET_DEFAULT;
29 import static android.net.NetworkStats.TAG_NONE;
30 import static android.net.NetworkStats.UID_ALL;
31 import static android.net.TrafficStats.UID_REMOVED;
32 import static android.text.format.DateUtils.WEEK_IN_MILLIS;
33 
34 import static com.android.net.module.util.NetworkStatsUtils.multiplySafeByRational;
35 
36 import android.annotation.NonNull;
37 import android.annotation.Nullable;
38 import android.annotation.SystemApi;
39 import android.net.NetworkStats.State;
40 import android.net.NetworkStatsHistory.Entry;
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.IndentingPrintWriter;
50 import android.util.Log;
51 import android.util.Range;
52 import android.util.proto.ProtoOutputStream;
53 
54 import com.android.internal.annotations.VisibleForTesting;
55 import com.android.internal.util.FileRotator;
56 import com.android.net.module.util.CollectionUtils;
57 import com.android.net.module.util.NetworkStatsUtils;
58 
59 import libcore.io.IoUtils;
60 
61 import java.io.BufferedInputStream;
62 import java.io.DataInput;
63 import java.io.DataInputStream;
64 import java.io.DataOutput;
65 import java.io.DataOutputStream;
66 import java.io.File;
67 import java.io.FileNotFoundException;
68 import java.io.IOException;
69 import java.io.InputStream;
70 import java.io.OutputStream;
71 import java.io.PrintWriter;
72 import java.net.ProtocolException;
73 import java.time.ZonedDateTime;
74 import java.util.ArrayList;
75 import java.util.Collections;
76 import java.util.HashMap;
77 import java.util.Iterator;
78 import java.util.List;
79 import java.util.Map;
80 import java.util.Objects;
81 import java.util.Set;
82 
83 /**
84  * Collection of {@link NetworkStatsHistory}, stored based on combined key of
85  * {@link NetworkIdentitySet}, UID, set, and tag. Knows how to persist itself.
86  *
87  * @hide
88  */
89 @SystemApi(client = MODULE_LIBRARIES)
90 public class NetworkStatsCollection implements FileRotator.Reader, FileRotator.Writer {
91     private static final String TAG = NetworkStatsCollection.class.getSimpleName();
92     /** File header magic number: "ANET" */
93     private static final int FILE_MAGIC = 0x414E4554;
94 
95     private static final int VERSION_NETWORK_INIT = 1;
96 
97     private static final int VERSION_UID_INIT = 1;
98     private static final int VERSION_UID_WITH_IDENT = 2;
99     private static final int VERSION_UID_WITH_TAG = 3;
100     private static final int VERSION_UID_WITH_SET = 4;
101 
102     private static final int VERSION_UNIFIED_INIT = 16;
103 
104     private ArrayMap<Key, NetworkStatsHistory> mStats = new ArrayMap<>();
105 
106     private final long mBucketDurationMillis;
107 
108     private long mStartMillis;
109     private long mEndMillis;
110     private long mTotalBytes;
111     private boolean mDirty;
112 
113     /**
114      * Construct a {@link NetworkStatsCollection} object.
115      *
116      * @param bucketDuration duration of the buckets in this object, in milliseconds.
117      * @hide
118      */
NetworkStatsCollection(long bucketDurationMillis)119     public NetworkStatsCollection(long bucketDurationMillis) {
120         mBucketDurationMillis = bucketDurationMillis;
121         reset();
122     }
123 
124     /** @hide */
clear()125     public void clear() {
126         reset();
127     }
128 
129     /** @hide */
reset()130     public void reset() {
131         mStats.clear();
132         mStartMillis = Long.MAX_VALUE;
133         mEndMillis = Long.MIN_VALUE;
134         mTotalBytes = 0;
135         mDirty = false;
136     }
137 
138     /** @hide */
getStartMillis()139     public long getStartMillis() {
140         return mStartMillis;
141     }
142 
143     /**
144      * Return first atomic bucket in this collection, which is more conservative
145      * than {@link #mStartMillis}.
146      * @hide
147      */
getFirstAtomicBucketMillis()148     public long getFirstAtomicBucketMillis() {
149         if (mStartMillis == Long.MAX_VALUE) {
150             return Long.MAX_VALUE;
151         } else {
152             return mStartMillis + mBucketDurationMillis;
153         }
154     }
155 
156     /** @hide */
getEndMillis()157     public long getEndMillis() {
158         return mEndMillis;
159     }
160 
161     /** @hide */
getTotalBytes()162     public long getTotalBytes() {
163         return mTotalBytes;
164     }
165 
166     /** @hide */
isDirty()167     public boolean isDirty() {
168         return mDirty;
169     }
170 
171     /** @hide */
clearDirty()172     public void clearDirty() {
173         mDirty = false;
174     }
175 
176     /** @hide */
isEmpty()177     public boolean isEmpty() {
178         return mStartMillis == Long.MAX_VALUE && mEndMillis == Long.MIN_VALUE;
179     }
180 
181     /** @hide */
182     @VisibleForTesting
roundUp(long time)183     public long roundUp(long time) {
184         if (time == Long.MIN_VALUE || time == Long.MAX_VALUE
185                 || time == SubscriptionPlan.TIME_UNKNOWN) {
186             return time;
187         } else {
188             final long mod = time % mBucketDurationMillis;
189             if (mod > 0) {
190                 time -= mod;
191                 time += mBucketDurationMillis;
192             }
193             return time;
194         }
195     }
196 
197     /** @hide */
198     @VisibleForTesting
roundDown(long time)199     public long roundDown(long time) {
200         if (time == Long.MIN_VALUE || time == Long.MAX_VALUE
201                 || time == SubscriptionPlan.TIME_UNKNOWN) {
202             return time;
203         } else {
204             final long mod = time % mBucketDurationMillis;
205             if (mod > 0) {
206                 time -= mod;
207             }
208             return time;
209         }
210     }
211 
212     /** @hide */
getRelevantUids(@etworkStatsAccess.Level int accessLevel)213     public int[] getRelevantUids(@NetworkStatsAccess.Level int accessLevel) {
214         return getRelevantUids(accessLevel, Binder.getCallingUid());
215     }
216 
217     /** @hide */
getRelevantUids(@etworkStatsAccess.Level int accessLevel, final int callerUid)218     public int[] getRelevantUids(@NetworkStatsAccess.Level int accessLevel,
219                 final int callerUid) {
220         final ArrayList<Integer> uids = new ArrayList<>();
221         for (int i = 0; i < mStats.size(); i++) {
222             final Key key = mStats.keyAt(i);
223             if (NetworkStatsAccess.isAccessibleToUser(key.uid, callerUid, accessLevel)) {
224                 int j = Collections.binarySearch(uids, new Integer(key.uid));
225 
226                 if (j < 0) {
227                     j = ~j;
228                     uids.add(j, key.uid);
229                 }
230             }
231         }
232         return CollectionUtils.toIntArray(uids);
233     }
234 
235     /**
236      * Combine all {@link NetworkStatsHistory} in this collection which match
237      * the requested parameters.
238      * @hide
239      */
getHistory(NetworkTemplate template, SubscriptionPlan augmentPlan, int uid, int set, int tag, int fields, long start, long end, @NetworkStatsAccess.Level int accessLevel, int callerUid)240     public NetworkStatsHistory getHistory(NetworkTemplate template, SubscriptionPlan augmentPlan,
241             int uid, int set, int tag, int fields, long start, long end,
242             @NetworkStatsAccess.Level int accessLevel, int callerUid) {
243         if (!NetworkStatsAccess.isAccessibleToUser(uid, callerUid, accessLevel)) {
244             throw new SecurityException("Network stats history of uid " + uid
245                     + " is forbidden for caller " + callerUid);
246         }
247 
248         // 180 days of history should be enough for anyone; if we end up needing
249         // more, we'll dynamically grow the history object.
250         final int bucketEstimate = (int) NetworkStatsUtils.constrain(
251                 ((end - start) / mBucketDurationMillis), 0,
252                 (180 * DateUtils.DAY_IN_MILLIS) / mBucketDurationMillis);
253         final NetworkStatsHistory combined = new NetworkStatsHistory(
254                 mBucketDurationMillis, bucketEstimate, fields);
255 
256         // shortcut when we know stats will be empty
257         if (start == end) return combined;
258 
259         // Figure out the window of time that we should be augmenting (if any)
260         long augmentStart = SubscriptionPlan.TIME_UNKNOWN;
261         long augmentEnd = (augmentPlan != null) ? augmentPlan.getDataUsageTime()
262                 : SubscriptionPlan.TIME_UNKNOWN;
263         // And if augmenting, we might need to collect more data to adjust with
264         long collectStart = start;
265         long collectEnd = end;
266 
267         if (augmentEnd != SubscriptionPlan.TIME_UNKNOWN) {
268             final Iterator<Range<ZonedDateTime>> it = augmentPlan.cycleIterator();
269             while (it.hasNext()) {
270                 final Range<ZonedDateTime> cycle = it.next();
271                 final long cycleStart = cycle.getLower().toInstant().toEpochMilli();
272                 final long cycleEnd = cycle.getUpper().toInstant().toEpochMilli();
273                 if (cycleStart <= augmentEnd && augmentEnd < cycleEnd) {
274                     augmentStart = cycleStart;
275                     collectStart = Long.min(collectStart, augmentStart);
276                     collectEnd = Long.max(collectEnd, augmentEnd);
277                     break;
278                 }
279             }
280         }
281 
282         if (augmentStart != SubscriptionPlan.TIME_UNKNOWN) {
283             // Shrink augmentation window so we don't risk undercounting.
284             augmentStart = roundUp(augmentStart);
285             augmentEnd = roundDown(augmentEnd);
286             // Grow collection window so we get all the stats needed.
287             collectStart = roundDown(collectStart);
288             collectEnd = roundUp(collectEnd);
289         }
290 
291         for (int i = 0; i < mStats.size(); i++) {
292             final Key key = mStats.keyAt(i);
293             if (key.uid == uid && NetworkStats.setMatches(set, key.set) && key.tag == tag
294                     && templateMatches(template, key.ident)) {
295                 final NetworkStatsHistory value = mStats.valueAt(i);
296                 combined.recordHistory(value, collectStart, collectEnd);
297             }
298         }
299 
300         if (augmentStart != SubscriptionPlan.TIME_UNKNOWN) {
301             final NetworkStatsHistory.Entry entry = combined.getValues(
302                     augmentStart, augmentEnd, null);
303 
304             // If we don't have any recorded data for this time period, give
305             // ourselves something to scale with.
306             if (entry.rxBytes == 0 || entry.txBytes == 0) {
307                 combined.recordData(augmentStart, augmentEnd,
308                         new NetworkStats.Entry(1, 0, 1, 0, 0));
309                 combined.getValues(augmentStart, augmentEnd, entry);
310             }
311 
312             final long rawBytes = (entry.rxBytes + entry.txBytes) == 0 ? 1 :
313                     (entry.rxBytes + entry.txBytes);
314             final long rawRxBytes = entry.rxBytes == 0 ? 1 : entry.rxBytes;
315             final long rawTxBytes = entry.txBytes == 0 ? 1 : entry.txBytes;
316             final long targetBytes = augmentPlan.getDataUsageBytes();
317 
318             final long targetRxBytes = multiplySafeByRational(targetBytes, rawRxBytes, rawBytes);
319             final long targetTxBytes = multiplySafeByRational(targetBytes, rawTxBytes, rawBytes);
320 
321 
322             // Scale all matching buckets to reach anchor target
323             final long beforeTotal = combined.getTotalBytes();
324             for (int i = 0; i < combined.size(); i++) {
325                 combined.getValues(i, entry);
326                 if (entry.bucketStart >= augmentStart
327                         && entry.bucketStart + entry.bucketDuration <= augmentEnd) {
328                     entry.rxBytes = multiplySafeByRational(
329                             targetRxBytes, entry.rxBytes, rawRxBytes);
330                     entry.txBytes = multiplySafeByRational(
331                             targetTxBytes, entry.txBytes, rawTxBytes);
332                     // We purposefully clear out packet counters to indicate
333                     // that this data has been augmented.
334                     entry.rxPackets = 0;
335                     entry.txPackets = 0;
336                     combined.setValues(i, entry);
337                 }
338             }
339 
340             final long deltaTotal = combined.getTotalBytes() - beforeTotal;
341             if (deltaTotal != 0) {
342                 Log.d(TAG, "Augmented network usage by " + deltaTotal + " bytes");
343             }
344 
345             // Finally we can slice data as originally requested
346             final NetworkStatsHistory sliced = new NetworkStatsHistory(
347                     mBucketDurationMillis, bucketEstimate, fields);
348             sliced.recordHistory(combined, start, end);
349             return sliced;
350         } else {
351             return combined;
352         }
353     }
354 
355     /**
356      * Summarize all {@link NetworkStatsHistory} in this collection which match
357      * the requested parameters across the requested range.
358      *
359      * @param template - a predicate for filtering netstats.
360      * @param start - start of the range, timestamp in milliseconds since the epoch.
361      * @param end - end of the range, timestamp in milliseconds since the epoch.
362      * @param accessLevel - caller access level.
363      * @param callerUid - caller UID.
364      * @hide
365      */
getSummary(NetworkTemplate template, long start, long end, @NetworkStatsAccess.Level int accessLevel, int callerUid)366     public NetworkStats getSummary(NetworkTemplate template, long start, long end,
367             @NetworkStatsAccess.Level int accessLevel, int callerUid) {
368         final long now = System.currentTimeMillis();
369 
370         final NetworkStats stats = new NetworkStats(end - start, 24);
371 
372         // shortcut when we know stats will be empty
373         if (start == end) return stats;
374 
375         final NetworkStats.Entry entry = new NetworkStats.Entry();
376         NetworkStatsHistory.Entry historyEntry = null;
377 
378         for (int i = 0; i < mStats.size(); i++) {
379             final Key key = mStats.keyAt(i);
380             if (templateMatches(template, key.ident)
381                     && NetworkStatsAccess.isAccessibleToUser(key.uid, callerUid, accessLevel)
382                     && key.set < NetworkStats.SET_DEBUG_START) {
383                 final NetworkStatsHistory value = mStats.valueAt(i);
384                 historyEntry = value.getValues(start, end, now, historyEntry);
385 
386                 entry.iface = IFACE_ALL;
387                 entry.uid = key.uid;
388                 entry.set = key.set;
389                 entry.tag = key.tag;
390                 entry.defaultNetwork = key.ident.areAllMembersOnDefaultNetwork()
391                         ? DEFAULT_NETWORK_YES : DEFAULT_NETWORK_NO;
392                 entry.metered = key.ident.isAnyMemberMetered() ? METERED_YES : METERED_NO;
393                 entry.roaming = key.ident.isAnyMemberRoaming() ? ROAMING_YES : ROAMING_NO;
394                 entry.rxBytes = historyEntry.rxBytes;
395                 entry.rxPackets = historyEntry.rxPackets;
396                 entry.txBytes = historyEntry.txBytes;
397                 entry.txPackets = historyEntry.txPackets;
398                 entry.operations = historyEntry.operations;
399 
400                 if (!entry.isEmpty()) {
401                     stats.combineValues(entry);
402                 }
403             }
404         }
405 
406         return stats;
407     }
408 
409     /**
410      * Record given {@link android.net.NetworkStats.Entry} into this collection.
411      * @hide
412      */
recordData(NetworkIdentitySet ident, int uid, int set, int tag, long start, long end, NetworkStats.Entry entry)413     public void recordData(NetworkIdentitySet ident, int uid, int set, int tag, long start,
414             long end, NetworkStats.Entry entry) {
415         final NetworkStatsHistory history = findOrCreateHistory(ident, uid, set, tag);
416         history.recordData(start, end, entry);
417         noteRecordedHistory(history.getStart(), history.getEnd(), entry.rxBytes + entry.txBytes);
418     }
419 
420     /**
421      * Record given {@link NetworkStatsHistory} into this collection.
422      *
423      * @hide
424      */
recordHistory(@onNull Key key, @NonNull NetworkStatsHistory history)425     public void recordHistory(@NonNull Key key, @NonNull NetworkStatsHistory history) {
426         Objects.requireNonNull(key);
427         Objects.requireNonNull(history);
428         if (history.size() == 0) return;
429         noteRecordedHistory(history.getStart(), history.getEnd(), history.getTotalBytes());
430 
431         NetworkStatsHistory target = mStats.get(key);
432         if (target == null) {
433             target = new NetworkStatsHistory(history.getBucketDuration());
434             mStats.put(key, target);
435         }
436         target.recordEntireHistory(history);
437     }
438 
439     /**
440      * Record all {@link NetworkStatsHistory} contained in the given collection
441      * into this collection.
442      *
443      * @hide
444      */
recordCollection(@onNull NetworkStatsCollection another)445     public void recordCollection(@NonNull NetworkStatsCollection another) {
446         Objects.requireNonNull(another);
447         for (int i = 0; i < another.mStats.size(); i++) {
448             final Key key = another.mStats.keyAt(i);
449             final NetworkStatsHistory value = another.mStats.valueAt(i);
450             recordHistory(key, value);
451         }
452     }
453 
findOrCreateHistory( NetworkIdentitySet ident, int uid, int set, int tag)454     private NetworkStatsHistory findOrCreateHistory(
455             NetworkIdentitySet ident, int uid, int set, int tag) {
456         final Key key = new Key(ident, uid, set, tag);
457         final NetworkStatsHistory existing = mStats.get(key);
458 
459         // update when no existing, or when bucket duration changed
460         NetworkStatsHistory updated = null;
461         if (existing == null) {
462             updated = new NetworkStatsHistory(mBucketDurationMillis, 10);
463         } else if (existing.getBucketDuration() != mBucketDurationMillis) {
464             updated = new NetworkStatsHistory(existing, mBucketDurationMillis);
465         }
466 
467         if (updated != null) {
468             mStats.put(key, updated);
469             return updated;
470         } else {
471             return existing;
472         }
473     }
474 
475     /** @hide */
476     @Override
read(InputStream in)477     public void read(InputStream in) throws IOException {
478         read((DataInput) new DataInputStream(in));
479     }
480 
read(DataInput in)481     private void read(DataInput in) throws IOException {
482         // verify file magic header intact
483         final int magic = in.readInt();
484         if (magic != FILE_MAGIC) {
485             throw new ProtocolException("unexpected magic: " + magic);
486         }
487 
488         final int version = in.readInt();
489         switch (version) {
490             case VERSION_UNIFIED_INIT: {
491                 // uid := size *(NetworkIdentitySet size *(uid set tag NetworkStatsHistory))
492                 final int identSize = in.readInt();
493                 for (int i = 0; i < identSize; i++) {
494                     final NetworkIdentitySet ident = new NetworkIdentitySet(in);
495 
496                     final int size = in.readInt();
497                     for (int j = 0; j < size; j++) {
498                         final int uid = in.readInt();
499                         final int set = in.readInt();
500                         final int tag = in.readInt();
501 
502                         final Key key = new Key(ident, uid, set, tag);
503                         final NetworkStatsHistory history = new NetworkStatsHistory(in);
504                         recordHistory(key, history);
505                     }
506                 }
507                 break;
508             }
509             default: {
510                 throw new ProtocolException("unexpected version: " + version);
511             }
512         }
513     }
514 
515     /** @hide */
516     @Override
write(OutputStream out)517     public void write(OutputStream out) throws IOException {
518         write((DataOutput) new DataOutputStream(out));
519         out.flush();
520     }
521 
write(DataOutput out)522     private void write(DataOutput out) throws IOException {
523         // cluster key lists grouped by ident
524         final HashMap<NetworkIdentitySet, ArrayList<Key>> keysByIdent = new HashMap<>();
525         for (Key key : mStats.keySet()) {
526             ArrayList<Key> keys = keysByIdent.get(key.ident);
527             if (keys == null) {
528                 keys = new ArrayList<>();
529                 keysByIdent.put(key.ident, keys);
530             }
531             keys.add(key);
532         }
533 
534         out.writeInt(FILE_MAGIC);
535         out.writeInt(VERSION_UNIFIED_INIT);
536 
537         out.writeInt(keysByIdent.size());
538         for (NetworkIdentitySet ident : keysByIdent.keySet()) {
539             final ArrayList<Key> keys = keysByIdent.get(ident);
540             ident.writeToStream(out);
541 
542             out.writeInt(keys.size());
543             for (Key key : keys) {
544                 final NetworkStatsHistory history = mStats.get(key);
545                 out.writeInt(key.uid);
546                 out.writeInt(key.set);
547                 out.writeInt(key.tag);
548                 history.writeToStream(out);
549             }
550         }
551     }
552 
553     /**
554      * Read legacy network summary statistics file format into the collection,
555      * See {@code NetworkStatsService#maybeUpgradeLegacyStatsLocked}.
556      *
557      * @deprecated
558      * @hide
559      */
560     @Deprecated
readLegacyNetwork(File file)561     public void readLegacyNetwork(File file) throws IOException {
562         final AtomicFile inputFile = new AtomicFile(file);
563 
564         DataInputStream in = null;
565         try {
566             in = new DataInputStream(new BufferedInputStream(inputFile.openRead()));
567 
568             // verify file magic header intact
569             final int magic = in.readInt();
570             if (magic != FILE_MAGIC) {
571                 throw new ProtocolException("unexpected magic: " + magic);
572             }
573 
574             final int version = in.readInt();
575             switch (version) {
576                 case VERSION_NETWORK_INIT: {
577                     // network := size *(NetworkIdentitySet NetworkStatsHistory)
578                     final int size = in.readInt();
579                     for (int i = 0; i < size; i++) {
580                         final NetworkIdentitySet ident = new NetworkIdentitySet(in);
581                         final NetworkStatsHistory history = new NetworkStatsHistory(in);
582 
583                         final Key key = new Key(ident, UID_ALL, SET_ALL, TAG_NONE);
584                         recordHistory(key, history);
585                     }
586                     break;
587                 }
588                 default: {
589                     throw new ProtocolException("unexpected version: " + version);
590                 }
591             }
592         } catch (FileNotFoundException e) {
593             // missing stats is okay, probably first boot
594         } finally {
595             IoUtils.closeQuietly(in);
596         }
597     }
598 
599     /**
600      * Read legacy Uid statistics file format into the collection,
601      * See {@code NetworkStatsService#maybeUpgradeLegacyStatsLocked}.
602      *
603      * @deprecated
604      * @hide
605      */
606     @Deprecated
readLegacyUid(File file, boolean onlyTags)607     public void readLegacyUid(File file, boolean onlyTags) throws IOException {
608         final AtomicFile inputFile = new AtomicFile(file);
609 
610         DataInputStream in = null;
611         try {
612             in = new DataInputStream(new BufferedInputStream(inputFile.openRead()));
613 
614             // verify file magic header intact
615             final int magic = in.readInt();
616             if (magic != FILE_MAGIC) {
617                 throw new ProtocolException("unexpected magic: " + magic);
618             }
619 
620             final int version = in.readInt();
621             switch (version) {
622                 case VERSION_UID_INIT: {
623                     // uid := size *(UID NetworkStatsHistory)
624 
625                     // drop this data version, since we don't have a good
626                     // mapping into NetworkIdentitySet.
627                     break;
628                 }
629                 case VERSION_UID_WITH_IDENT: {
630                     // uid := size *(NetworkIdentitySet size *(UID NetworkStatsHistory))
631 
632                     // drop this data version, since this version only existed
633                     // for a short time.
634                     break;
635                 }
636                 case VERSION_UID_WITH_TAG:
637                 case VERSION_UID_WITH_SET: {
638                     // uid := size *(NetworkIdentitySet size *(uid set tag NetworkStatsHistory))
639                     final int identSize = in.readInt();
640                     for (int i = 0; i < identSize; i++) {
641                         final NetworkIdentitySet ident = new NetworkIdentitySet(in);
642 
643                         final int size = in.readInt();
644                         for (int j = 0; j < size; j++) {
645                             final int uid = in.readInt();
646                             final int set = (version >= VERSION_UID_WITH_SET) ? in.readInt()
647                                     : SET_DEFAULT;
648                             final int tag = in.readInt();
649 
650                             final Key key = new Key(ident, uid, set, tag);
651                             final NetworkStatsHistory history = new NetworkStatsHistory(in);
652 
653                             if ((tag == TAG_NONE) != onlyTags) {
654                                 recordHistory(key, history);
655                             }
656                         }
657                     }
658                     break;
659                 }
660                 default: {
661                     throw new ProtocolException("unexpected version: " + version);
662                 }
663             }
664         } catch (FileNotFoundException e) {
665             // missing stats is okay, probably first boot
666         } finally {
667             IoUtils.closeQuietly(in);
668         }
669     }
670 
671     /**
672      * Remove any {@link NetworkStatsHistory} attributed to the requested UID,
673      * moving any {@link NetworkStats#TAG_NONE} series to
674      * {@link TrafficStats#UID_REMOVED}.
675      * @hide
676      */
removeUids(int[] uids)677     public void removeUids(int[] uids) {
678         final ArrayList<Key> knownKeys = new ArrayList<>();
679         knownKeys.addAll(mStats.keySet());
680 
681         // migrate all UID stats into special "removed" bucket
682         for (Key key : knownKeys) {
683             if (CollectionUtils.contains(uids, key.uid)) {
684                 // only migrate combined TAG_NONE history
685                 if (key.tag == TAG_NONE) {
686                     final NetworkStatsHistory uidHistory = mStats.get(key);
687                     final NetworkStatsHistory removedHistory = findOrCreateHistory(
688                             key.ident, UID_REMOVED, SET_DEFAULT, TAG_NONE);
689                     removedHistory.recordEntireHistory(uidHistory);
690                 }
691                 mStats.remove(key);
692                 mDirty = true;
693             }
694         }
695     }
696 
697     /**
698      * Remove histories which contains or is before the cutoff timestamp.
699      * @hide
700      */
removeHistoryBefore(long cutoffMillis)701     public void removeHistoryBefore(long cutoffMillis) {
702         final ArrayList<Key> knownKeys = new ArrayList<>();
703         knownKeys.addAll(mStats.keySet());
704 
705         for (Key key : knownKeys) {
706             final NetworkStatsHistory history = mStats.get(key);
707             if (history.getStart() > cutoffMillis) continue;
708 
709             history.removeBucketsStartingBefore(cutoffMillis);
710             if (history.size() == 0) {
711                 mStats.remove(key);
712             }
713             mDirty = true;
714         }
715     }
716 
noteRecordedHistory(long startMillis, long endMillis, long totalBytes)717     private void noteRecordedHistory(long startMillis, long endMillis, long totalBytes) {
718         if (startMillis < mStartMillis) mStartMillis = startMillis;
719         if (endMillis > mEndMillis) mEndMillis = endMillis;
720         mTotalBytes += totalBytes;
721         mDirty = true;
722     }
723 
estimateBuckets()724     private int estimateBuckets() {
725         return (int) (Math.min(mEndMillis - mStartMillis, WEEK_IN_MILLIS * 5)
726                 / mBucketDurationMillis);
727     }
728 
getSortedKeys()729     private ArrayList<Key> getSortedKeys() {
730         final ArrayList<Key> keys = new ArrayList<>();
731         keys.addAll(mStats.keySet());
732         Collections.sort(keys, (left, right) -> Key.compare(left, right));
733         return keys;
734     }
735 
736     /** @hide */
dump(IndentingPrintWriter pw)737     public void dump(IndentingPrintWriter pw) {
738         for (Key key : getSortedKeys()) {
739             pw.print("ident="); pw.print(key.ident.toString());
740             pw.print(" uid="); pw.print(key.uid);
741             pw.print(" set="); pw.print(NetworkStats.setToString(key.set));
742             pw.print(" tag="); pw.println(NetworkStats.tagToString(key.tag));
743 
744             final NetworkStatsHistory history = mStats.get(key);
745             pw.increaseIndent();
746             history.dump(pw, true);
747             pw.decreaseIndent();
748         }
749     }
750 
751     /** @hide */
dumpDebug(ProtoOutputStream proto, long tag)752     public void dumpDebug(ProtoOutputStream proto, long tag) {
753         final long start = proto.start(tag);
754 
755         for (Key key : getSortedKeys()) {
756             final long startStats = proto.start(NetworkStatsCollectionProto.STATS);
757 
758             // Key
759             final long startKey = proto.start(NetworkStatsCollectionStatsProto.KEY);
760             key.ident.dumpDebug(proto, NetworkStatsCollectionKeyProto.IDENTITY);
761             proto.write(NetworkStatsCollectionKeyProto.UID, key.uid);
762             proto.write(NetworkStatsCollectionKeyProto.SET, key.set);
763             proto.write(NetworkStatsCollectionKeyProto.TAG, key.tag);
764             proto.end(startKey);
765 
766             // Value
767             final NetworkStatsHistory history = mStats.get(key);
768             history.dumpDebug(proto, NetworkStatsCollectionStatsProto.HISTORY);
769             proto.end(startStats);
770         }
771 
772         proto.end(start);
773     }
774 
775     /** @hide */
dumpCheckin(PrintWriter pw, long start, long end)776     public void dumpCheckin(PrintWriter pw, long start, long end) {
777         dumpCheckin(pw, start, end, NetworkTemplate.buildTemplateMobileWildcard(), "cell");
778         dumpCheckin(pw, start, end, NetworkTemplate.buildTemplateWifiWildcard(), "wifi");
779         dumpCheckin(pw, start, end, NetworkTemplate.buildTemplateEthernet(), "eth");
780         dumpCheckin(pw, start, end, NetworkTemplate.buildTemplateBluetooth(), "bt");
781     }
782 
783     /**
784      * Dump all contained stats that match requested parameters, but group
785      * together all matching {@link NetworkTemplate} under a single prefix.
786      */
dumpCheckin(PrintWriter pw, long start, long end, NetworkTemplate groupTemplate, String groupPrefix)787     private void dumpCheckin(PrintWriter pw, long start, long end, NetworkTemplate groupTemplate,
788             String groupPrefix) {
789         final ArrayMap<Key, NetworkStatsHistory> grouped = new ArrayMap<>();
790 
791         // Walk through all history, grouping by matching network templates
792         for (int i = 0; i < mStats.size(); i++) {
793             final Key key = mStats.keyAt(i);
794             final NetworkStatsHistory value = mStats.valueAt(i);
795 
796             if (!templateMatches(groupTemplate, key.ident)) continue;
797             if (key.set >= NetworkStats.SET_DEBUG_START) continue;
798 
799             final Key groupKey = new Key(new NetworkIdentitySet(), key.uid, key.set, key.tag);
800             NetworkStatsHistory groupHistory = grouped.get(groupKey);
801             if (groupHistory == null) {
802                 groupHistory = new NetworkStatsHistory(value.getBucketDuration());
803                 grouped.put(groupKey, groupHistory);
804             }
805             groupHistory.recordHistory(value, start, end);
806         }
807 
808         for (int i = 0; i < grouped.size(); i++) {
809             final Key key = grouped.keyAt(i);
810             final NetworkStatsHistory value = grouped.valueAt(i);
811 
812             if (value.size() == 0) continue;
813 
814             pw.print("c,");
815             pw.print(groupPrefix); pw.print(',');
816             pw.print(key.uid); pw.print(',');
817             pw.print(NetworkStats.setToCheckinString(key.set)); pw.print(',');
818             pw.print(key.tag);
819             pw.println();
820 
821             value.dumpCheckin(pw);
822         }
823     }
824 
825     /**
826      * Test if given {@link NetworkTemplate} matches any {@link NetworkIdentity}
827      * in the given {@link NetworkIdentitySet}.
828      */
templateMatches(NetworkTemplate template, NetworkIdentitySet identSet)829     private static boolean templateMatches(NetworkTemplate template, NetworkIdentitySet identSet) {
830         for (NetworkIdentity ident : identSet) {
831             if (template.matches(ident)) {
832                 return true;
833             }
834         }
835         return false;
836     }
837 
838     /**
839      * Get the all historical stats of the collection {@link NetworkStatsCollection}.
840      *
841      * @return All {@link NetworkStatsHistory} in this collection.
842      */
843     @NonNull
getEntries()844     public Map<Key, NetworkStatsHistory> getEntries() {
845         return new ArrayMap(mStats);
846     }
847 
848     /**
849      * Builder class for {@link NetworkStatsCollection}.
850      */
851     public static final class Builder {
852         private final long mBucketDurationMillis;
853         private final ArrayMap<Key, NetworkStatsHistory> mEntries = new ArrayMap<>();
854 
855         /**
856          * Creates a new Builder with given bucket duration.
857          *
858          * @param bucketDuration Duration of the buckets of the object, in milliseconds.
859          */
Builder(long bucketDurationMillis)860         public Builder(long bucketDurationMillis) {
861             mBucketDurationMillis = bucketDurationMillis;
862         }
863 
864         /**
865          * Add association of the history with the specified key in this map.
866          *
867          * @param key The object used to identify a network, see {@link Key}.
868          *            If history already exists for this key, then the passed-in history is appended
869          *            to the previously-passed in history. The caller must ensure that the history
870          *            passed-in timestamps are greater than all previously-passed-in timestamps.
871          * @param history {@link NetworkStatsHistory} instance associated to the given {@link Key}.
872          * @return The builder object.
873          */
874         @NonNull
addEntry(@onNull Key key, @NonNull NetworkStatsHistory history)875         public NetworkStatsCollection.Builder addEntry(@NonNull Key key,
876                 @NonNull NetworkStatsHistory history) {
877             Objects.requireNonNull(key);
878             Objects.requireNonNull(history);
879             final List<Entry> historyEntries = history.getEntries();
880             final NetworkStatsHistory existing = mEntries.get(key);
881 
882             final int size = historyEntries.size() + ((existing != null) ? existing.size() : 0);
883             final NetworkStatsHistory.Builder historyBuilder =
884                     new NetworkStatsHistory.Builder(mBucketDurationMillis, size);
885 
886             // TODO: this simply appends the entries to any entries that were already present in
887             // the builder, which requires the caller to pass in entries in order. We might be
888             // able to do better with something like recordHistory.
889             if (existing != null) {
890                 for (Entry entry : existing.getEntries()) {
891                     historyBuilder.addEntry(entry);
892                 }
893             }
894 
895             for (Entry entry : historyEntries) {
896                 historyBuilder.addEntry(entry);
897             }
898 
899             mEntries.put(key, historyBuilder.build());
900             return this;
901         }
902 
903         /**
904          * Builds the instance of the {@link NetworkStatsCollection}.
905          *
906          * @return the built instance of {@link NetworkStatsCollection}.
907          */
908         @NonNull
build()909         public NetworkStatsCollection build() {
910             final NetworkStatsCollection collection =
911                     new NetworkStatsCollection(mBucketDurationMillis);
912             for (int i = 0; i < mEntries.size(); i++) {
913                 collection.recordHistory(mEntries.keyAt(i), mEntries.valueAt(i));
914             }
915             return collection;
916         }
917     }
918 
919     /**
920      * the identifier that associate with the {@link NetworkStatsHistory} object to identify
921      * a certain record in the {@link NetworkStatsCollection} object.
922      */
923     public static final class Key {
924         /** @hide */
925         public final NetworkIdentitySet ident;
926         /** @hide */
927         public final int uid;
928         /** @hide */
929         public final int set;
930         /** @hide */
931         public final int tag;
932 
933         private final int mHashCode;
934 
935         /**
936          * Construct a {@link Key} object.
937          *
938          * @param ident a Set of {@link NetworkIdentity} that associated with the record.
939          * @param uid Uid of the record.
940          * @param set Set of the record, see {@code NetworkStats#SET_*}.
941          * @param tag Tag of the record, see {@link TrafficStats#setThreadStatsTag(int)}.
942          */
Key(@onNull Set<NetworkIdentity> ident, int uid, @State int set, int tag)943         public Key(@NonNull Set<NetworkIdentity> ident, int uid, @State int set, int tag) {
944             this(new NetworkIdentitySet(Objects.requireNonNull(ident)), uid, set, tag);
945         }
946 
947         /** @hide */
Key(@onNull NetworkIdentitySet ident, int uid, int set, int tag)948         public Key(@NonNull NetworkIdentitySet ident, int uid, int set, int tag) {
949             this.ident = Objects.requireNonNull(ident);
950             this.uid = uid;
951             this.set = set;
952             this.tag = tag;
953             mHashCode = Objects.hash(ident, uid, set, tag);
954         }
955 
956         @Override
hashCode()957         public int hashCode() {
958             return mHashCode;
959         }
960 
961         @Override
equals(@ullable Object obj)962         public boolean equals(@Nullable Object obj) {
963             if (obj instanceof Key) {
964                 final Key key = (Key) obj;
965                 return uid == key.uid && set == key.set && tag == key.tag
966                         && Objects.equals(ident, key.ident);
967             }
968             return false;
969         }
970 
971         /** @hide */
compare(@onNull Key left, @NonNull Key right)972         public static int compare(@NonNull Key left, @NonNull Key right) {
973             Objects.requireNonNull(left);
974             Objects.requireNonNull(right);
975             int res = 0;
976             if (left.ident != null && right.ident != null) {
977                 res = NetworkIdentitySet.compare(left.ident, right.ident);
978             }
979             if (res == 0) {
980                 res = Integer.compare(left.uid, right.uid);
981             }
982             if (res == 0) {
983                 res = Integer.compare(left.set, right.set);
984             }
985             if (res == 0) {
986                 res = Integer.compare(left.tag, right.tag);
987             }
988             return res;
989         }
990     }
991 }
992