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