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