/* * Copyright (C) 2012 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.net; import static android.annotation.SystemApi.Client.MODULE_LIBRARIES; import static android.net.NetworkStats.DEFAULT_NETWORK_NO; import static android.net.NetworkStats.DEFAULT_NETWORK_YES; import static android.net.NetworkStats.IFACE_ALL; import static android.net.NetworkStats.METERED_NO; import static android.net.NetworkStats.METERED_YES; import static android.net.NetworkStats.ROAMING_NO; import static android.net.NetworkStats.ROAMING_YES; import static android.net.NetworkStats.SET_ALL; import static android.net.NetworkStats.SET_DEFAULT; import static android.net.NetworkStats.SET_FOREGROUND; import static android.net.NetworkStats.TAG_NONE; import static android.net.NetworkStats.UID_ALL; import static android.net.NetworkTemplate.MATCH_BLUETOOTH; import static android.net.NetworkTemplate.MATCH_ETHERNET; import static android.net.NetworkTemplate.MATCH_MOBILE; import static android.net.NetworkTemplate.MATCH_PROXY; import static android.net.NetworkTemplate.MATCH_WIFI; import static android.net.TrafficStats.UID_REMOVED; import static android.text.format.DateUtils.WEEK_IN_MILLIS; import static com.android.net.module.util.NetworkStatsUtils.multiplySafeByRational; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; import android.net.NetworkStats.State; import android.net.NetworkStatsHistory.Entry; import android.os.Binder; import android.service.NetworkStatsCollectionKeyProto; import android.service.NetworkStatsCollectionProto; import android.service.NetworkStatsCollectionStatsProto; import android.telephony.SubscriptionPlan; import android.text.format.DateUtils; import android.util.ArrayMap; import android.util.ArraySet; import android.util.AtomicFile; import android.util.IndentingPrintWriter; import android.util.Log; import android.util.Range; import android.util.proto.ProtoOutputStream; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.FileRotator; import com.android.modules.utils.FastDataInput; import com.android.net.module.util.CollectionUtils; import com.android.net.module.util.NetworkStatsUtils; import libcore.io.IoUtils; import java.io.BufferedInputStream; import java.io.DataInput; import java.io.DataInputStream; import java.io.DataOutput; import java.io.DataOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.net.ProtocolException; import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; /** * Collection of {@link NetworkStatsHistory}, stored based on combined key of * {@link NetworkIdentitySet}, UID, set, and tag. Knows how to persist itself. * * @hide */ @SystemApi(client = MODULE_LIBRARIES) public class NetworkStatsCollection implements FileRotator.Reader, FileRotator.Writer { private static final String TAG = NetworkStatsCollection.class.getSimpleName(); /** File header magic number: "ANET" */ private static final int FILE_MAGIC = 0x414E4554; private static final int VERSION_NETWORK_INIT = 1; private static final int VERSION_UID_INIT = 1; private static final int VERSION_UID_WITH_IDENT = 2; private static final int VERSION_UID_WITH_TAG = 3; private static final int VERSION_UID_WITH_SET = 4; private static final int VERSION_UNIFIED_INIT = 16; private ArrayMap mStats = new ArrayMap<>(); private final long mBucketDurationMillis; private long mStartMillis; private long mEndMillis; private long mTotalBytes; private boolean mDirty; private final boolean mUseFastDataInput; /** * Construct a {@link NetworkStatsCollection} object. * * @param bucketDurationMillis duration of the buckets in this object, in milliseconds. * @hide */ public NetworkStatsCollection(long bucketDurationMillis) { this(bucketDurationMillis, false /* useFastDataInput */); } /** * Construct a {@link NetworkStatsCollection} object. * * @param bucketDurationMillis duration of the buckets in this object, in milliseconds. * @param useFastDataInput true if using {@link FastDataInput} is preferred. Otherwise, false. * @hide */ public NetworkStatsCollection(long bucketDurationMillis, boolean useFastDataInput) { mBucketDurationMillis = bucketDurationMillis; mUseFastDataInput = useFastDataInput; reset(); } /** @hide */ public void clear() { reset(); } /** @hide */ public void reset() { mStats.clear(); mStartMillis = Long.MAX_VALUE; mEndMillis = Long.MIN_VALUE; mTotalBytes = 0; mDirty = false; } /** @hide */ public long getStartMillis() { return mStartMillis; } /** * Return first atomic bucket in this collection, which is more conservative * than {@link #mStartMillis}. * @hide */ public long getFirstAtomicBucketMillis() { if (mStartMillis == Long.MAX_VALUE) { return Long.MAX_VALUE; } else { return mStartMillis + mBucketDurationMillis; } } /** @hide */ public long getEndMillis() { return mEndMillis; } /** @hide */ public long getTotalBytes() { return mTotalBytes; } /** @hide */ public boolean isDirty() { return mDirty; } /** @hide */ public void clearDirty() { mDirty = false; } /** @hide */ public boolean isEmpty() { return mStartMillis == Long.MAX_VALUE && mEndMillis == Long.MIN_VALUE; } /** @hide */ @VisibleForTesting public long roundUp(long time) { if (time == Long.MIN_VALUE || time == Long.MAX_VALUE || time == SubscriptionPlan.TIME_UNKNOWN) { return time; } else { final long mod = time % mBucketDurationMillis; if (mod > 0) { time -= mod; time += mBucketDurationMillis; } return time; } } /** @hide */ @VisibleForTesting public long roundDown(long time) { if (time == Long.MIN_VALUE || time == Long.MAX_VALUE || time == SubscriptionPlan.TIME_UNKNOWN) { return time; } else { final long mod = time % mBucketDurationMillis; if (mod > 0) { time -= mod; } return time; } } /** @hide */ public int[] getRelevantUids(@NetworkStatsAccess.Level int accessLevel) { return getRelevantUids(accessLevel, Binder.getCallingUid()); } /** @hide */ public int[] getRelevantUids(@NetworkStatsAccess.Level int accessLevel, final int callerUid) { final ArrayList uids = new ArrayList<>(); for (int i = 0; i < mStats.size(); i++) { final Key key = mStats.keyAt(i); if (NetworkStatsAccess.isAccessibleToUser(key.uid, callerUid, accessLevel)) { int j = Collections.binarySearch(uids, new Integer(key.uid)); if (j < 0) { j = ~j; uids.add(j, key.uid); } } } return CollectionUtils.toIntArray(uids); } /** * Combine all {@link NetworkStatsHistory} in this collection which match * the requested parameters. * @hide */ public NetworkStatsHistory getHistory(NetworkTemplate template, SubscriptionPlan augmentPlan, int uid, int set, int tag, int fields, long start, long end, @NetworkStatsAccess.Level int accessLevel, int callerUid) { if (!NetworkStatsAccess.isAccessibleToUser(uid, callerUid, accessLevel)) { throw new SecurityException("Network stats history of uid " + uid + " is forbidden for caller " + callerUid); } // 180 days of history should be enough for anyone; if we end up needing // more, we'll dynamically grow the history object. final int bucketEstimate = (int) NetworkStatsUtils.constrain( ((end - start) / mBucketDurationMillis), 0, (180 * DateUtils.DAY_IN_MILLIS) / mBucketDurationMillis); final NetworkStatsHistory combined = new NetworkStatsHistory( mBucketDurationMillis, bucketEstimate, fields); // shortcut when we know stats will be empty if (start == end) return combined; // Figure out the window of time that we should be augmenting (if any) long augmentStart = SubscriptionPlan.TIME_UNKNOWN; long augmentEnd = (augmentPlan != null) ? augmentPlan.getDataUsageTime() : SubscriptionPlan.TIME_UNKNOWN; // And if augmenting, we might need to collect more data to adjust with long collectStart = start; long collectEnd = end; if (augmentEnd != SubscriptionPlan.TIME_UNKNOWN) { final Iterator> it = augmentPlan.cycleIterator(); while (it.hasNext()) { final Range cycle = it.next(); final long cycleStart = cycle.getLower().toInstant().toEpochMilli(); final long cycleEnd = cycle.getUpper().toInstant().toEpochMilli(); if (cycleStart <= augmentEnd && augmentEnd < cycleEnd) { augmentStart = cycleStart; collectStart = Long.min(collectStart, augmentStart); collectEnd = Long.max(collectEnd, augmentEnd); break; } } } if (augmentStart != SubscriptionPlan.TIME_UNKNOWN) { // Shrink augmentation window so we don't risk undercounting. augmentStart = roundUp(augmentStart); augmentEnd = roundDown(augmentEnd); // Grow collection window so we get all the stats needed. collectStart = roundDown(collectStart); collectEnd = roundUp(collectEnd); } for (int i = 0; i < mStats.size(); i++) { final Key key = mStats.keyAt(i); if (key.uid == uid && NetworkStats.setMatches(set, key.set) && key.tag == tag && templateMatches(template, key.ident)) { final NetworkStatsHistory value = mStats.valueAt(i); combined.recordHistory(value, collectStart, collectEnd); } } if (augmentStart != SubscriptionPlan.TIME_UNKNOWN) { final NetworkStatsHistory.Entry entry = combined.getValues( augmentStart, augmentEnd, null); // If we don't have any recorded data for this time period, give // ourselves something to scale with. if (entry.rxBytes == 0 || entry.txBytes == 0) { combined.recordData(augmentStart, augmentEnd, new NetworkStats.Entry(IFACE_ALL, UID_ALL, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 1L, 0L, 1L, 0L, 0L)); combined.getValues(augmentStart, augmentEnd, entry); } final long rawBytes = (entry.rxBytes + entry.txBytes) == 0 ? 1 : (entry.rxBytes + entry.txBytes); final long rawRxBytes = entry.rxBytes == 0 ? 1 : entry.rxBytes; final long rawTxBytes = entry.txBytes == 0 ? 1 : entry.txBytes; final long targetBytes = augmentPlan.getDataUsageBytes(); final long targetRxBytes = multiplySafeByRational(targetBytes, rawRxBytes, rawBytes); final long targetTxBytes = multiplySafeByRational(targetBytes, rawTxBytes, rawBytes); // Scale all matching buckets to reach anchor target final long beforeTotal = combined.getTotalBytes(); for (int i = 0; i < combined.size(); i++) { combined.getValues(i, entry); if (entry.bucketStart >= augmentStart && entry.bucketStart + entry.bucketDuration <= augmentEnd) { entry.rxBytes = multiplySafeByRational( targetRxBytes, entry.rxBytes, rawRxBytes); entry.txBytes = multiplySafeByRational( targetTxBytes, entry.txBytes, rawTxBytes); // We purposefully clear out packet counters to indicate // that this data has been augmented. entry.rxPackets = 0; entry.txPackets = 0; combined.setValues(i, entry); } } final long deltaTotal = combined.getTotalBytes() - beforeTotal; if (deltaTotal != 0) { Log.d(TAG, "Augmented network usage by " + deltaTotal + " bytes"); } // Finally we can slice data as originally requested final NetworkStatsHistory sliced = new NetworkStatsHistory( mBucketDurationMillis, bucketEstimate, fields); sliced.recordHistory(combined, start, end); return sliced; } else { return combined; } } /** * Summarize all {@link NetworkStatsHistory} in this collection which match * the requested parameters across the requested range. * * @param template - a predicate for filtering netstats. * @param start - start of the range, timestamp in milliseconds since the epoch. * @param end - end of the range, timestamp in milliseconds since the epoch. * @param accessLevel - caller access level. * @param callerUid - caller UID. * @hide */ public NetworkStats getSummary(NetworkTemplate template, long start, long end, @NetworkStatsAccess.Level int accessLevel, int callerUid) { final long now = System.currentTimeMillis(); final NetworkStats stats = new NetworkStats(end - start, 24); // shortcut when we know stats will be empty if (start == end) return stats; final NetworkStats.Entry entry = new NetworkStats.Entry(); NetworkStatsHistory.Entry historyEntry = null; for (int i = 0; i < mStats.size(); i++) { final Key key = mStats.keyAt(i); if (templateMatches(template, key.ident) && NetworkStatsAccess.isAccessibleToUser(key.uid, callerUid, accessLevel) && key.set < NetworkStats.SET_DEBUG_START) { final NetworkStatsHistory value = mStats.valueAt(i); historyEntry = value.getValues(start, end, now, historyEntry); entry.iface = IFACE_ALL; entry.uid = key.uid; entry.set = key.set; entry.tag = key.tag; entry.defaultNetwork = key.ident.areAllMembersOnDefaultNetwork() ? DEFAULT_NETWORK_YES : DEFAULT_NETWORK_NO; entry.metered = key.ident.isAnyMemberMetered() ? METERED_YES : METERED_NO; entry.roaming = key.ident.isAnyMemberRoaming() ? ROAMING_YES : ROAMING_NO; entry.rxBytes = historyEntry.rxBytes; entry.rxPackets = historyEntry.rxPackets; entry.txBytes = historyEntry.txBytes; entry.txPackets = historyEntry.txPackets; entry.operations = historyEntry.operations; if (!entry.isEmpty()) { stats.combineValues(entry); } } } return stats; } /** * Record given {@link android.net.NetworkStats.Entry} into this collection. * @hide */ public void recordData(NetworkIdentitySet ident, int uid, int set, int tag, long start, long end, NetworkStats.Entry entry) { final NetworkStatsHistory history = findOrCreateHistory(ident, uid, set, tag); history.recordData(start, end, entry); noteRecordedHistory(history.getStart(), history.getEnd(), entry.rxBytes + entry.txBytes); } /** * Record given {@link NetworkStatsHistory} into this collection. * * @hide */ public void recordHistory(@NonNull Key key, @NonNull NetworkStatsHistory history) { Objects.requireNonNull(key); Objects.requireNonNull(history); if (history.size() == 0) return; noteRecordedHistory(history.getStart(), history.getEnd(), history.getTotalBytes()); NetworkStatsHistory target = mStats.get(key); if (target == null) { target = new NetworkStatsHistory(history.getBucketDuration()); mStats.put(key, target); } target.recordEntireHistory(history); } /** * Record all {@link NetworkStatsHistory} contained in the given collection * into this collection. * * @hide */ public void recordCollection(@NonNull NetworkStatsCollection another) { Objects.requireNonNull(another); for (int i = 0; i < another.mStats.size(); i++) { final Key key = another.mStats.keyAt(i); final NetworkStatsHistory value = another.mStats.valueAt(i); recordHistory(key, value); } } private NetworkStatsHistory findOrCreateHistory( NetworkIdentitySet ident, int uid, int set, int tag) { final Key key = new Key(ident, uid, set, tag); final NetworkStatsHistory existing = mStats.get(key); // update when no existing, or when bucket duration changed NetworkStatsHistory updated = null; if (existing == null) { updated = new NetworkStatsHistory(mBucketDurationMillis, 10); } else if (existing.getBucketDuration() != mBucketDurationMillis) { updated = new NetworkStatsHistory(existing, mBucketDurationMillis); } if (updated != null) { mStats.put(key, updated); return updated; } else { return existing; } } /** @hide */ @Override public void read(InputStream in) throws IOException { if (mUseFastDataInput) { read(FastDataInput.obtain(in)); } else { read((DataInput) new DataInputStream(in)); } } private void read(DataInput in) throws IOException { // verify file magic header intact final int magic = in.readInt(); if (magic != FILE_MAGIC) { throw new ProtocolException("unexpected magic: " + magic); } final int version = in.readInt(); switch (version) { case VERSION_UNIFIED_INIT: { // uid := size *(NetworkIdentitySet size *(uid set tag NetworkStatsHistory)) final int identSize = in.readInt(); for (int i = 0; i < identSize; i++) { final NetworkIdentitySet ident = new NetworkIdentitySet(in); final int size = in.readInt(); for (int j = 0; j < size; j++) { final int uid = in.readInt(); final int set = in.readInt(); final int tag = in.readInt(); final Key key = new Key(ident, uid, set, tag); final NetworkStatsHistory history = new NetworkStatsHistory(in); recordHistory(key, history); } } break; } default: { throw new ProtocolException("unexpected version: " + version); } } } /** @hide */ @Override public void write(OutputStream out) throws IOException { write((DataOutput) new DataOutputStream(out)); out.flush(); } private void write(DataOutput out) throws IOException { // cluster key lists grouped by ident final HashMap> keysByIdent = new HashMap<>(); for (Key key : mStats.keySet()) { ArrayList keys = keysByIdent.get(key.ident); if (keys == null) { keys = new ArrayList<>(); keysByIdent.put(key.ident, keys); } keys.add(key); } out.writeInt(FILE_MAGIC); out.writeInt(VERSION_UNIFIED_INIT); out.writeInt(keysByIdent.size()); for (NetworkIdentitySet ident : keysByIdent.keySet()) { final ArrayList keys = keysByIdent.get(ident); ident.writeToStream(out); out.writeInt(keys.size()); for (Key key : keys) { final NetworkStatsHistory history = mStats.get(key); out.writeInt(key.uid); out.writeInt(key.set); out.writeInt(key.tag); history.writeToStream(out); } } } /** * Read legacy network summary statistics file format into the collection, * See {@code NetworkStatsService#maybeUpgradeLegacyStatsLocked}. * * @deprecated * @hide */ @Deprecated public void readLegacyNetwork(File file) throws IOException { final AtomicFile inputFile = new AtomicFile(file); DataInputStream in = null; try { in = new DataInputStream(new BufferedInputStream(inputFile.openRead())); // verify file magic header intact final int magic = in.readInt(); if (magic != FILE_MAGIC) { throw new ProtocolException("unexpected magic: " + magic); } final int version = in.readInt(); switch (version) { case VERSION_NETWORK_INIT: { // network := size *(NetworkIdentitySet NetworkStatsHistory) final int size = in.readInt(); for (int i = 0; i < size; i++) { final NetworkIdentitySet ident = new NetworkIdentitySet(in); final NetworkStatsHistory history = new NetworkStatsHistory(in); final Key key = new Key(ident, UID_ALL, SET_ALL, TAG_NONE); recordHistory(key, history); } break; } default: { throw new ProtocolException("unexpected version: " + version); } } } catch (FileNotFoundException e) { // missing stats is okay, probably first boot } finally { IoUtils.closeQuietly(in); } } /** * Read legacy Uid statistics file format into the collection, * See {@code NetworkStatsService#maybeUpgradeLegacyStatsLocked}. * * @deprecated * @hide */ @Deprecated public void readLegacyUid(File file, boolean onlyTags) throws IOException { final AtomicFile inputFile = new AtomicFile(file); DataInputStream in = null; try { in = new DataInputStream(new BufferedInputStream(inputFile.openRead())); // verify file magic header intact final int magic = in.readInt(); if (magic != FILE_MAGIC) { throw new ProtocolException("unexpected magic: " + magic); } final int version = in.readInt(); switch (version) { case VERSION_UID_INIT: { // uid := size *(UID NetworkStatsHistory) // drop this data version, since we don't have a good // mapping into NetworkIdentitySet. break; } case VERSION_UID_WITH_IDENT: { // uid := size *(NetworkIdentitySet size *(UID NetworkStatsHistory)) // drop this data version, since this version only existed // for a short time. break; } case VERSION_UID_WITH_TAG: case VERSION_UID_WITH_SET: { // uid := size *(NetworkIdentitySet size *(uid set tag NetworkStatsHistory)) final int identSize = in.readInt(); for (int i = 0; i < identSize; i++) { final NetworkIdentitySet ident = new NetworkIdentitySet(in); final int size = in.readInt(); for (int j = 0; j < size; j++) { final int uid = in.readInt(); final int set = (version >= VERSION_UID_WITH_SET) ? in.readInt() : SET_DEFAULT; final int tag = in.readInt(); final Key key = new Key(ident, uid, set, tag); final NetworkStatsHistory history = new NetworkStatsHistory(in); if ((tag == TAG_NONE) != onlyTags) { recordHistory(key, history); } } } break; } default: { throw new ProtocolException("unexpected version: " + version); } } } catch (FileNotFoundException e) { // missing stats is okay, probably first boot } finally { IoUtils.closeQuietly(in); } } /** * Remove any {@link NetworkStatsHistory} attributed to the requested UID, * moving any {@link NetworkStats#TAG_NONE} series to * {@link TrafficStats#UID_REMOVED}. * @hide */ public void removeUids(int[] uids) { final ArrayList knownKeys = new ArrayList<>(); knownKeys.addAll(mStats.keySet()); // migrate all UID stats into special "removed" bucket for (Key key : knownKeys) { if (CollectionUtils.contains(uids, key.uid)) { // only migrate combined TAG_NONE history if (key.tag == TAG_NONE) { final NetworkStatsHistory uidHistory = mStats.get(key); final NetworkStatsHistory removedHistory = findOrCreateHistory( key.ident, UID_REMOVED, SET_DEFAULT, TAG_NONE); removedHistory.recordEntireHistory(uidHistory); } mStats.remove(key); mDirty = true; } } } /** * Remove histories which contains or is before the cutoff timestamp. * @hide */ public void removeHistoryBefore(long cutoffMillis) { final ArrayList knownKeys = new ArrayList<>(); knownKeys.addAll(mStats.keySet()); for (Key key : knownKeys) { final NetworkStatsHistory history = mStats.get(key); if (history.getStart() > cutoffMillis) continue; history.removeBucketsStartingBefore(cutoffMillis); if (history.size() == 0) { mStats.remove(key); } mDirty = true; } } private void noteRecordedHistory(long startMillis, long endMillis, long totalBytes) { if (startMillis < mStartMillis) mStartMillis = startMillis; if (endMillis > mEndMillis) mEndMillis = endMillis; mTotalBytes += totalBytes; mDirty = true; } private int estimateBuckets() { return (int) (Math.min(mEndMillis - mStartMillis, WEEK_IN_MILLIS * 5) / mBucketDurationMillis); } private ArrayList getSortedKeys() { final ArrayList keys = new ArrayList<>(); keys.addAll(mStats.keySet()); Collections.sort(keys, (left, right) -> Key.compare(left, right)); return keys; } /** @hide */ public void dump(IndentingPrintWriter pw) { for (Key key : getSortedKeys()) { pw.print("ident="); pw.print(key.ident.toString()); pw.print(" uid="); pw.print(key.uid); pw.print(" set="); pw.print(NetworkStats.setToString(key.set)); pw.print(" tag="); pw.println(NetworkStats.tagToString(key.tag)); final NetworkStatsHistory history = mStats.get(key); pw.increaseIndent(); history.dump(pw, true); pw.decreaseIndent(); } } /** @hide */ public void dumpDebug(ProtoOutputStream proto, long tag) { final long start = proto.start(tag); for (Key key : getSortedKeys()) { final long startStats = proto.start(NetworkStatsCollectionProto.STATS); // Key final long startKey = proto.start(NetworkStatsCollectionStatsProto.KEY); key.ident.dumpDebug(proto, NetworkStatsCollectionKeyProto.IDENTITY); proto.write(NetworkStatsCollectionKeyProto.UID, key.uid); proto.write(NetworkStatsCollectionKeyProto.SET, key.set); proto.write(NetworkStatsCollectionKeyProto.TAG, key.tag); proto.end(startKey); // Value final NetworkStatsHistory history = mStats.get(key); history.dumpDebug(proto, NetworkStatsCollectionStatsProto.HISTORY); proto.end(startStats); } proto.end(start); } /** @hide */ public void dumpCheckin(PrintWriter pw, long start, long end) { dumpCheckin(pw, start, end, new NetworkTemplate.Builder(MATCH_MOBILE) .setMeteredness(METERED_YES).build(), "cell"); dumpCheckin(pw, start, end, new NetworkTemplate.Builder(MATCH_WIFI).build(), "wifi"); dumpCheckin(pw, start, end, new NetworkTemplate.Builder(MATCH_ETHERNET).build(), "eth"); dumpCheckin(pw, start, end, new NetworkTemplate.Builder(MATCH_BLUETOOTH).build(), "bt"); dumpCheckin(pw, start, end, new NetworkTemplate.Builder(MATCH_PROXY).build(), "proxy"); } /** * Dump all contained stats that match requested parameters, but group * together all matching {@link NetworkTemplate} under a single prefix. */ private void dumpCheckin(PrintWriter pw, long start, long end, NetworkTemplate groupTemplate, String groupPrefix) { final ArrayMap grouped = new ArrayMap<>(); // Walk through all history, grouping by matching network templates for (int i = 0; i < mStats.size(); i++) { final Key key = mStats.keyAt(i); final NetworkStatsHistory value = mStats.valueAt(i); if (!templateMatches(groupTemplate, key.ident)) continue; if (key.set >= NetworkStats.SET_DEBUG_START) continue; final Key groupKey = new Key(new NetworkIdentitySet(), key.uid, key.set, key.tag); NetworkStatsHistory groupHistory = grouped.get(groupKey); if (groupHistory == null) { groupHistory = new NetworkStatsHistory(value.getBucketDuration()); grouped.put(groupKey, groupHistory); } groupHistory.recordHistory(value, start, end); } for (int i = 0; i < grouped.size(); i++) { final Key key = grouped.keyAt(i); final NetworkStatsHistory value = grouped.valueAt(i); if (value.size() == 0) continue; pw.print("c,"); pw.print(groupPrefix); pw.print(','); pw.print(key.uid); pw.print(','); pw.print(NetworkStats.setToCheckinString(key.set)); pw.print(','); pw.print(key.tag); pw.println(); value.dumpCheckin(pw); } } /** * Test if given {@link NetworkTemplate} matches any {@link NetworkIdentity} * in the given {@link NetworkIdentitySet}. */ private static boolean templateMatches(NetworkTemplate template, NetworkIdentitySet identSet) { for (NetworkIdentity ident : identSet) { if (template.matches(ident)) { return true; } } return false; } /** * Get the all historical stats of the collection {@link NetworkStatsCollection}. * * @return All {@link NetworkStatsHistory} in this collection. */ @NonNull public Map getEntries() { return new ArrayMap(mStats); } /** * Builder class for {@link NetworkStatsCollection}. */ public static final class Builder { private final long mBucketDurationMillis; private final ArrayMap mEntries = new ArrayMap<>(); /** * Creates a new Builder with given bucket duration. * * @param bucketDuration Duration of the buckets of the object, in milliseconds. */ public Builder(long bucketDurationMillis) { mBucketDurationMillis = bucketDurationMillis; } /** * Add association of the history with the specified key in this map. * * @param key The object used to identify a network, see {@link Key}. * If history already exists for this key, then the passed-in history is appended * to the previously-passed in history. The caller must ensure that the history * passed-in timestamps are greater than all previously-passed-in timestamps. * @param history {@link NetworkStatsHistory} instance associated to the given {@link Key}. * @return The builder object. */ @NonNull public NetworkStatsCollection.Builder addEntry(@NonNull Key key, @NonNull NetworkStatsHistory history) { Objects.requireNonNull(key); Objects.requireNonNull(history); final List historyEntries = history.getEntries(); final NetworkStatsHistory existing = mEntries.get(key); final int size = historyEntries.size() + ((existing != null) ? existing.size() : 0); final NetworkStatsHistory.Builder historyBuilder = new NetworkStatsHistory.Builder(mBucketDurationMillis, size); // TODO: this simply appends the entries to any entries that were already present in // the builder, which requires the caller to pass in entries in order. We might be // able to do better with something like recordHistory. if (existing != null) { for (Entry entry : existing.getEntries()) { historyBuilder.addEntry(entry); } } for (Entry entry : historyEntries) { historyBuilder.addEntry(entry); } mEntries.put(key, historyBuilder.build()); return this; } /** * Builds the instance of the {@link NetworkStatsCollection}. * * @return the built instance of {@link NetworkStatsCollection}. */ @NonNull public NetworkStatsCollection build() { final NetworkStatsCollection collection = new NetworkStatsCollection(mBucketDurationMillis); for (int i = 0; i < mEntries.size(); i++) { collection.recordHistory(mEntries.keyAt(i), mEntries.valueAt(i)); } return collection; } } private static String str(NetworkStatsCollection.Key key) { StringBuilder sb = new StringBuilder() .append(key.ident.toString()) .append(" uid=").append(key.uid); if (key.set != SET_FOREGROUND) { sb.append(" set=").append(key.set); } if (key.tag != 0) { sb.append(" tag=").append(key.tag); } return sb.toString(); } // The importer will modify some keys when importing them. // In order to keep the comparison code simple, add such special cases here and simply // ignore them. This should not impact fidelity much because the start/end checks and the total // bytes check still need to pass. private static boolean couldKeyChangeOnImport(NetworkStatsCollection.Key key) { if (key.ident.isEmpty()) return false; final NetworkIdentity firstIdent = key.ident.iterator().next(); // Non-mobile network with non-empty RAT type. // This combination is invalid and the NetworkIdentity.Builder will throw if it is passed // in, but it looks like it was previously possible to persist it to disk. The importer sets // the RAT type to NETWORK_TYPE_ALL. if (firstIdent.getType() != ConnectivityManager.TYPE_MOBILE && firstIdent.getRatType() != NetworkTemplate.NETWORK_TYPE_ALL) { return true; } return false; } /** * Compare two {@link NetworkStatsCollection} instances and returning a human-readable * string description of difference for debugging purpose. * * @hide */ @Nullable public static String compareStats(NetworkStatsCollection migrated, NetworkStatsCollection legacy, boolean allowKeyChange) { final Map migEntries = migrated.getEntries(); final Map legEntries = legacy.getEntries(); final ArraySet unmatchedLegKeys = new ArraySet<>(legEntries.keySet()); for (NetworkStatsCollection.Key legKey : legEntries.keySet()) { final NetworkStatsHistory legHistory = legEntries.get(legKey); final NetworkStatsHistory migHistory = migEntries.get(legKey); if (migHistory == null && allowKeyChange && couldKeyChangeOnImport(legKey)) { unmatchedLegKeys.remove(legKey); continue; } if (migHistory == null) { return "Missing migrated history for legacy key " + str(legKey) + ", legacy history was " + legHistory; } if (!migHistory.isSameAs(legHistory)) { return "Difference in history for key " + legKey + "; legacy history " + legHistory + ", migrated history " + migHistory; } unmatchedLegKeys.remove(legKey); } if (!unmatchedLegKeys.isEmpty()) { final NetworkStatsHistory first = legEntries.get(unmatchedLegKeys.valueAt(0)); return "Found unmatched legacy keys: count=" + unmatchedLegKeys.size() + ", first unmatched collection " + first; } if (migrated.getStartMillis() != legacy.getStartMillis() || migrated.getEndMillis() != legacy.getEndMillis()) { return "Start / end of the collections " + migrated.getStartMillis() + "/" + legacy.getStartMillis() + " and " + migrated.getEndMillis() + "/" + legacy.getEndMillis() + " don't match"; } if (migrated.getTotalBytes() != legacy.getTotalBytes()) { return "Total bytes " + migrated.getTotalBytes() + " and " + legacy.getTotalBytes() + " don't match for collections with start/end " + migrated.getStartMillis() + "/" + legacy.getStartMillis(); } return null; } /** * the identifier that associate with the {@link NetworkStatsHistory} object to identify * a certain record in the {@link NetworkStatsCollection} object. */ public static final class Key { /** @hide */ public final NetworkIdentitySet ident; /** @hide */ public final int uid; /** @hide */ public final int set; /** @hide */ public final int tag; private final int mHashCode; /** * Construct a {@link Key} object. * * @param ident a Set of {@link NetworkIdentity} that associated with the record. * @param uid Uid of the record. * @param set Set of the record, see {@code NetworkStats#SET_*}. * @param tag Tag of the record, see {@link TrafficStats#setThreadStatsTag(int)}. */ public Key(@NonNull Set ident, int uid, @State int set, int tag) { this(new NetworkIdentitySet(Objects.requireNonNull(ident)), uid, set, tag); } /** @hide */ public Key(@NonNull NetworkIdentitySet ident, int uid, int set, int tag) { this.ident = Objects.requireNonNull(ident); this.uid = uid; this.set = set; this.tag = tag; mHashCode = Objects.hash(ident, uid, set, tag); } @Override public int hashCode() { return mHashCode; } @Override public boolean equals(@Nullable Object obj) { if (obj instanceof Key) { final Key key = (Key) obj; return uid == key.uid && set == key.set && tag == key.tag && Objects.equals(ident, key.ident); } return false; } /** @hide */ public static int compare(@NonNull Key left, @NonNull Key right) { Objects.requireNonNull(left); Objects.requireNonNull(right); int res = 0; if (left.ident != null && right.ident != null) { res = NetworkIdentitySet.compare(left.ident, right.ident); } if (res == 0) { res = Integer.compare(left.uid, right.uid); } if (res == 0) { res = Integer.compare(left.set, right.set); } if (res == 0) { res = Integer.compare(left.tag, right.tag); } return res; } } }