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