| /* |
| * 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<Key, NetworkStatsHistory> 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<Integer> 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<Range<ZonedDateTime>> it = augmentPlan.cycleIterator(); |
| while (it.hasNext()) { |
| final Range<ZonedDateTime> 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<NetworkIdentitySet, ArrayList<Key>> keysByIdent = new HashMap<>(); |
| for (Key key : mStats.keySet()) { |
| ArrayList<Key> 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<Key> 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<Key> 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<Key> 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<Key> getSortedKeys() { |
| final ArrayList<Key> 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<Key, NetworkStatsHistory> 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<Key, NetworkStatsHistory> getEntries() { |
| return new ArrayMap(mStats); |
| } |
| |
| /** |
| * Builder class for {@link NetworkStatsCollection}. |
| */ |
| public static final class Builder { |
| private final long mBucketDurationMillis; |
| private final ArrayMap<Key, NetworkStatsHistory> 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<Entry> 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<NetworkStatsCollection.Key, NetworkStatsHistory> migEntries = |
| migrated.getEntries(); |
| final Map<NetworkStatsCollection.Key, NetworkStatsHistory> legEntries = legacy.getEntries(); |
| |
| final ArraySet<NetworkStatsCollection.Key> 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<NetworkIdentity> 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; |
| } |
| } |
| } |