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