[MS08] Move NetworkStats files to f/b/package/ConnectivityT

NetworkStatsService is going to be moved into ConnectivityService
module. Move all related files to packages/ConnectivityT so that
it can be easily migrate these files to connectivity module
after clearing the hidden API usages.

Bug: 197717846
Test: TH
Change-Id: Iead00832b5eb7b1dc40a92027c5a14ae8316b16c
This commit is contained in:
Junyu Lai
2021-12-07 08:25:31 +00:00
parent 96fe4ce495
commit ad166b43df
30 changed files with 98 additions and 6 deletions

View File

@@ -0,0 +1,191 @@
/*
* Copyright (C) 2011 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 com.android.server.net;
import android.net.NetworkIdentity;
import android.service.NetworkIdentitySetProto;
import android.util.proto.ProtoOutputStream;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.util.HashSet;
import static android.net.ConnectivityManager.TYPE_MOBILE;
/**
* Identity of a {@code iface}, defined by the set of {@link NetworkIdentity}
* active on that interface.
*
* @hide
*/
public class NetworkIdentitySet extends HashSet<NetworkIdentity> implements
Comparable<NetworkIdentitySet> {
private static final int VERSION_INIT = 1;
private static final int VERSION_ADD_ROAMING = 2;
private static final int VERSION_ADD_NETWORK_ID = 3;
private static final int VERSION_ADD_METERED = 4;
private static final int VERSION_ADD_DEFAULT_NETWORK = 5;
private static final int VERSION_ADD_OEM_MANAGED_NETWORK = 6;
public NetworkIdentitySet() {
}
public NetworkIdentitySet(DataInput in) throws IOException {
final int version = in.readInt();
final int size = in.readInt();
for (int i = 0; i < size; i++) {
if (version <= VERSION_INIT) {
final int ignored = in.readInt();
}
final int type = in.readInt();
final int subType = in.readInt();
final String subscriberId = readOptionalString(in);
final String networkId;
if (version >= VERSION_ADD_NETWORK_ID) {
networkId = readOptionalString(in);
} else {
networkId = null;
}
final boolean roaming;
if (version >= VERSION_ADD_ROAMING) {
roaming = in.readBoolean();
} else {
roaming = false;
}
final boolean metered;
if (version >= VERSION_ADD_METERED) {
metered = in.readBoolean();
} else {
// If this is the old data and the type is mobile, treat it as metered. (Note that
// if this is a mobile network, TYPE_MOBILE is the only possible type that could be
// used.)
metered = (type == TYPE_MOBILE);
}
final boolean defaultNetwork;
if (version >= VERSION_ADD_DEFAULT_NETWORK) {
defaultNetwork = in.readBoolean();
} else {
defaultNetwork = true;
}
final int oemNetCapabilities;
if (version >= VERSION_ADD_OEM_MANAGED_NETWORK) {
oemNetCapabilities = in.readInt();
} else {
oemNetCapabilities = NetworkIdentity.OEM_NONE;
}
add(new NetworkIdentity(type, subType, subscriberId, networkId, roaming, metered,
defaultNetwork, oemNetCapabilities));
}
}
public void writeToStream(DataOutput out) throws IOException {
out.writeInt(VERSION_ADD_OEM_MANAGED_NETWORK);
out.writeInt(size());
for (NetworkIdentity ident : this) {
out.writeInt(ident.getType());
out.writeInt(ident.getSubType());
writeOptionalString(out, ident.getSubscriberId());
writeOptionalString(out, ident.getNetworkId());
out.writeBoolean(ident.getRoaming());
out.writeBoolean(ident.getMetered());
out.writeBoolean(ident.getDefaultNetwork());
out.writeInt(ident.getOemManaged());
}
}
/** @return whether any {@link NetworkIdentity} in this set is considered metered. */
public boolean isAnyMemberMetered() {
if (isEmpty()) {
return false;
}
for (NetworkIdentity ident : this) {
if (ident.getMetered()) {
return true;
}
}
return false;
}
/** @return whether any {@link NetworkIdentity} in this set is considered roaming. */
public boolean isAnyMemberRoaming() {
if (isEmpty()) {
return false;
}
for (NetworkIdentity ident : this) {
if (ident.getRoaming()) {
return true;
}
}
return false;
}
/** @return whether any {@link NetworkIdentity} in this set is considered on the default
network. */
public boolean areAllMembersOnDefaultNetwork() {
if (isEmpty()) {
return true;
}
for (NetworkIdentity ident : this) {
if (!ident.getDefaultNetwork()) {
return false;
}
}
return true;
}
private static void writeOptionalString(DataOutput out, String value) throws IOException {
if (value != null) {
out.writeByte(1);
out.writeUTF(value);
} else {
out.writeByte(0);
}
}
private static String readOptionalString(DataInput in) throws IOException {
if (in.readByte() != 0) {
return in.readUTF();
} else {
return null;
}
}
@Override
public int compareTo(NetworkIdentitySet another) {
if (isEmpty()) return -1;
if (another.isEmpty()) return 1;
final NetworkIdentity ident = iterator().next();
final NetworkIdentity anotherIdent = another.iterator().next();
return ident.compareTo(anotherIdent);
}
public void dumpDebug(ProtoOutputStream proto, long tag) {
final long start = proto.start(tag);
for (NetworkIdentity ident : this) {
ident.dumpDebug(proto, NetworkIdentitySetProto.IDENTITIES);
}
proto.end(start);
}
}

View File

@@ -0,0 +1,199 @@
/*
* Copyright (C) 2015 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 com.android.server.net;
import static android.Manifest.permission.READ_NETWORK_USAGE_HISTORY;
import static android.net.NetworkStats.UID_ALL;
import static android.net.TrafficStats.UID_REMOVED;
import static android.net.TrafficStats.UID_TETHERING;
import android.Manifest;
import android.annotation.IntDef;
import android.app.AppOpsManager;
import android.app.admin.DevicePolicyManagerInternal;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Binder;
import android.os.Process;
import android.os.UserHandle;
import android.telephony.TelephonyManager;
import com.android.server.LocalServices;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/** Utility methods for controlling access to network stats APIs. */
public final class NetworkStatsAccess {
private NetworkStatsAccess() {}
/**
* Represents an access level for the network usage history and statistics APIs.
*
* <p>Access levels are in increasing order; that is, it is reasonable to check access by
* verifying that the caller's access level is at least the minimum required level.
*/
@IntDef({
Level.DEFAULT,
Level.USER,
Level.DEVICESUMMARY,
Level.DEVICE,
})
@Retention(RetentionPolicy.SOURCE)
public @interface Level {
/**
* Default, unprivileged access level.
*
* <p>Can only access usage for one's own UID.
*
* <p>Every app will have at least this access level.
*/
int DEFAULT = 0;
/**
* Access level for apps which can access usage for any app running in the same user.
*
* <p>Granted to:
* <ul>
* <li>Profile owners.
* </ul>
*/
int USER = 1;
/**
* Access level for apps which can access usage summary of device. Device summary includes
* usage by apps running in any profiles/users, however this access level does not
* allow querying usage of individual apps running in other profiles/users.
*
* <p>Granted to:
* <ul>
* <li>Apps with the PACKAGE_USAGE_STATS permission granted. Note that this is an AppOps bit
* so it is not necessarily sufficient to declare this in the manifest.
* <li>Apps with the (signature/privileged) READ_NETWORK_USAGE_HISTORY permission.
* </ul>
*/
int DEVICESUMMARY = 2;
/**
* Access level for apps which can access usage for any app on the device, including apps
* running on other users/profiles.
*
* <p>Granted to:
* <ul>
* <li>Device owners.
* <li>Carrier-privileged applications.
* <li>The system UID.
* </ul>
*/
int DEVICE = 3;
}
/** Returns the {@link NetworkStatsAccess.Level} for the given caller. */
public static @NetworkStatsAccess.Level int checkAccessLevel(
Context context, int callingUid, String callingPackage) {
final DevicePolicyManagerInternal dpmi = LocalServices.getService(
DevicePolicyManagerInternal.class);
final TelephonyManager tm = (TelephonyManager)
context.getSystemService(Context.TELEPHONY_SERVICE);
boolean hasCarrierPrivileges;
final long token = Binder.clearCallingIdentity();
try {
hasCarrierPrivileges = tm != null
&& tm.checkCarrierPrivilegesForPackageAnyPhone(callingPackage)
== TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS;
} finally {
Binder.restoreCallingIdentity(token);
}
final boolean isDeviceOwner = dpmi != null && dpmi.isActiveDeviceOwner(callingUid);
final int appId = UserHandle.getAppId(callingUid);
if (hasCarrierPrivileges || isDeviceOwner
|| appId == Process.SYSTEM_UID || appId == Process.NETWORK_STACK_UID) {
// Carrier-privileged apps and device owners, and the system (including the
// network stack) can access data usage for all apps on the device.
return NetworkStatsAccess.Level.DEVICE;
}
boolean hasAppOpsPermission = hasAppOpsPermission(context, callingUid, callingPackage);
if (hasAppOpsPermission || context.checkCallingOrSelfPermission(
READ_NETWORK_USAGE_HISTORY) == PackageManager.PERMISSION_GRANTED) {
return NetworkStatsAccess.Level.DEVICESUMMARY;
}
//TODO(b/169395065) Figure out if this flow makes sense in Device Owner mode.
boolean isProfileOwner = dpmi != null && (dpmi.isActiveProfileOwner(callingUid)
|| dpmi.isActiveDeviceOwner(callingUid));
if (isProfileOwner) {
// Apps with the AppOps permission, profile owners, and apps with the privileged
// permission can access data usage for all apps in this user/profile.
return NetworkStatsAccess.Level.USER;
}
// Everyone else gets default access (only to their own UID).
return NetworkStatsAccess.Level.DEFAULT;
}
/**
* Returns whether the given caller should be able to access the given UID when the caller has
* the given {@link NetworkStatsAccess.Level}.
*/
public static boolean isAccessibleToUser(int uid, int callerUid,
@NetworkStatsAccess.Level int accessLevel) {
switch (accessLevel) {
case NetworkStatsAccess.Level.DEVICE:
// Device-level access - can access usage for any uid.
return true;
case NetworkStatsAccess.Level.DEVICESUMMARY:
// Can access usage for any app running in the same user, along
// with some special uids (system, removed, or tethering) and
// anonymized uids
return uid == android.os.Process.SYSTEM_UID || uid == UID_REMOVED
|| uid == UID_TETHERING || uid == UID_ALL
|| UserHandle.getUserId(uid) == UserHandle.getUserId(callerUid);
case NetworkStatsAccess.Level.USER:
// User-level access - can access usage for any app running in the same user, along
// with some special uids (system, removed, or tethering).
return uid == android.os.Process.SYSTEM_UID || uid == UID_REMOVED
|| uid == UID_TETHERING
|| UserHandle.getUserId(uid) == UserHandle.getUserId(callerUid);
case NetworkStatsAccess.Level.DEFAULT:
default:
// Default access level - can only access one's own usage.
return uid == callerUid;
}
}
private static boolean hasAppOpsPermission(
Context context, int callingUid, String callingPackage) {
if (callingPackage != null) {
AppOpsManager appOps = (AppOpsManager) context.getSystemService(
Context.APP_OPS_SERVICE);
final int mode = appOps.noteOp(AppOpsManager.OP_GET_USAGE_STATS,
callingUid, callingPackage);
if (mode == AppOpsManager.MODE_DEFAULT) {
// The default behavior here is to check if PackageManager has given the app
// permission.
final int permissionCheck = context.checkCallingPermission(
Manifest.permission.PACKAGE_USAGE_STATS);
return permissionCheck == PackageManager.PERMISSION_GRANTED;
}
return (mode == AppOpsManager.MODE_ALLOWED);
}
return false;
}
}

View File

@@ -0,0 +1,820 @@
/*
* 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 com.android.server.net;
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.TAG_NONE;
import static android.net.NetworkStats.UID_ALL;
import static android.net.TrafficStats.UID_REMOVED;
import static android.text.format.DateUtils.WEEK_IN_MILLIS;
import static com.android.internal.net.NetworkUtilsInternal.multiplySafeByRational;
import static com.android.server.net.NetworkStatsService.TAG;
import android.net.NetworkIdentity;
import android.net.NetworkStats;
import android.net.NetworkStatsHistory;
import android.net.NetworkTemplate;
import android.net.TrafficStats;
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.AtomicFile;
import android.util.IntArray;
import android.util.MathUtils;
import android.util.Range;
import android.util.Slog;
import android.util.proto.ProtoOutputStream;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.FastDataInput;
import com.android.internal.util.FastDataOutput;
import com.android.internal.util.FileRotator;
import com.android.internal.util.IndentingPrintWriter;
import com.google.android.collect.Lists;
import com.google.android.collect.Maps;
import libcore.io.IoUtils;
import java.io.BufferedInputStream;
import java.io.DataInput;
import java.io.DataInputStream;
import java.io.DataOutput;
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.Objects;
/**
* Collection of {@link NetworkStatsHistory}, stored based on combined key of
* {@link NetworkIdentitySet}, UID, set, and tag. Knows how to persist itself.
*/
public class NetworkStatsCollection implements FileRotator.Reader, FileRotator.Writer {
/** File header magic number: "ANET" */
private static final int FILE_MAGIC = 0x414E4554;
/** Default buffer size from BufferedInputStream */
private static final int BUFFER_SIZE = 8192;
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 mBucketDuration;
private long mStartMillis;
private long mEndMillis;
private long mTotalBytes;
private boolean mDirty;
public NetworkStatsCollection(long bucketDuration) {
mBucketDuration = bucketDuration;
reset();
}
public void clear() {
reset();
}
public void reset() {
mStats.clear();
mStartMillis = Long.MAX_VALUE;
mEndMillis = Long.MIN_VALUE;
mTotalBytes = 0;
mDirty = false;
}
public long getStartMillis() {
return mStartMillis;
}
/**
* Return first atomic bucket in this collection, which is more conservative
* than {@link #mStartMillis}.
*/
public long getFirstAtomicBucketMillis() {
if (mStartMillis == Long.MAX_VALUE) {
return Long.MAX_VALUE;
} else {
return mStartMillis + mBucketDuration;
}
}
public long getEndMillis() {
return mEndMillis;
}
public long getTotalBytes() {
return mTotalBytes;
}
public boolean isDirty() {
return mDirty;
}
public void clearDirty() {
mDirty = false;
}
public boolean isEmpty() {
return mStartMillis == Long.MAX_VALUE && mEndMillis == Long.MIN_VALUE;
}
@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 % mBucketDuration;
if (mod > 0) {
time -= mod;
time += mBucketDuration;
}
return time;
}
}
@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 % mBucketDuration;
if (mod > 0) {
time -= mod;
}
return time;
}
}
public int[] getRelevantUids(@NetworkStatsAccess.Level int accessLevel) {
return getRelevantUids(accessLevel, Binder.getCallingUid());
}
public int[] getRelevantUids(@NetworkStatsAccess.Level int accessLevel,
final int callerUid) {
IntArray uids = new IntArray();
for (int i = 0; i < mStats.size(); i++) {
final Key key = mStats.keyAt(i);
if (NetworkStatsAccess.isAccessibleToUser(key.uid, callerUid, accessLevel)) {
int j = uids.binarySearch(key.uid);
if (j < 0) {
j = ~j;
uids.add(j, key.uid);
}
}
}
return uids.toArray();
}
/**
* Combine all {@link NetworkStatsHistory} in this collection which match
* the requested parameters.
*/
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) MathUtils.constrain(((end - start) / mBucketDuration), 0,
(180 * DateUtils.DAY_IN_MILLIS) / mBucketDuration);
final NetworkStatsHistory combined = new NetworkStatsHistory(
mBucketDuration, 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(1, 0, 1, 0, 0));
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) {
Slog.d(TAG, "Augmented network usage by " + deltaTotal + " bytes");
}
// Finally we can slice data as originally requested
final NetworkStatsHistory sliced = new NetworkStatsHistory(
mBucketDuration, 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.
*/
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.
*/
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.
*/
private void recordHistory(Key key, NetworkStatsHistory 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.
*/
public void recordCollection(NetworkStatsCollection 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(mBucketDuration, 10);
} else if (existing.getBucketDuration() != mBucketDuration) {
updated = new NetworkStatsHistory(existing, mBucketDuration);
}
if (updated != null) {
mStats.put(key, updated);
return updated;
} else {
return existing;
}
}
@Override
public void read(InputStream in) throws IOException {
final FastDataInput dataIn = new FastDataInput(in, BUFFER_SIZE);
read(dataIn);
}
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);
}
}
}
@Override
public void write(OutputStream out) throws IOException {
final FastDataOutput dataOut = new FastDataOutput(out, BUFFER_SIZE);
write(dataOut);
dataOut.flush();
}
private void write(DataOutput out) throws IOException {
// cluster key lists grouped by ident
final HashMap<NetworkIdentitySet, ArrayList<Key>> keysByIdent = Maps.newHashMap();
for (Key key : mStats.keySet()) {
ArrayList<Key> keys = keysByIdent.get(key.ident);
if (keys == null) {
keys = Lists.newArrayList();
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);
}
}
}
@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);
}
}
@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}.
*/
public void removeUids(int[] uids) {
final ArrayList<Key> knownKeys = Lists.newArrayList();
knownKeys.addAll(mStats.keySet());
// migrate all UID stats into special "removed" bucket
for (Key key : knownKeys) {
if (ArrayUtils.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;
}
}
}
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)
/ mBucketDuration);
}
private ArrayList<Key> getSortedKeys() {
final ArrayList<Key> keys = Lists.newArrayList();
keys.addAll(mStats.keySet());
Collections.sort(keys);
return keys;
}
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();
}
}
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);
}
public void dumpCheckin(PrintWriter pw, long start, long end) {
dumpCheckin(pw, start, end, NetworkTemplate.buildTemplateMobileWildcard(), "cell");
dumpCheckin(pw, start, end, NetworkTemplate.buildTemplateWifiWildcard(), "wifi");
dumpCheckin(pw, start, end, NetworkTemplate.buildTemplateEthernet(), "eth");
dumpCheckin(pw, start, end, NetworkTemplate.buildTemplateBluetooth(), "bt");
}
/**
* 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(null, 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;
}
private static class Key implements Comparable<Key> {
public final NetworkIdentitySet ident;
public final int uid;
public final int set;
public final int tag;
private final int hashCode;
public Key(NetworkIdentitySet ident, int uid, int set, int tag) {
this.ident = ident;
this.uid = uid;
this.set = set;
this.tag = tag;
hashCode = Objects.hash(ident, uid, set, tag);
}
@Override
public int hashCode() {
return hashCode;
}
@Override
public boolean equals(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;
}
@Override
public int compareTo(Key another) {
int res = 0;
if (ident != null && another.ident != null) {
res = ident.compareTo(another.ident);
}
if (res == 0) {
res = Integer.compare(uid, another.uid);
}
if (res == 0) {
res = Integer.compare(set, another.set);
}
if (res == 0) {
res = Integer.compare(tag, another.tag);
}
return res;
}
}
}

View File

@@ -0,0 +1,489 @@
/*
* Copyright (C) 2011 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 com.android.server.net;
import static android.net.NetworkStats.INTERFACES_ALL;
import static android.net.NetworkStats.SET_ALL;
import static android.net.NetworkStats.TAG_ALL;
import static android.net.NetworkStats.TAG_NONE;
import static android.net.NetworkStats.UID_ALL;
import static com.android.server.NetworkManagementSocketTagger.kernelToTag;
import android.annotation.Nullable;
import android.net.INetd;
import android.net.NetworkStats;
import android.net.UnderlyingNetworkInfo;
import android.net.util.NetdService;
import android.os.RemoteException;
import android.os.StrictMode;
import android.os.SystemClock;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.ProcFileReader;
import libcore.io.IoUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.ProtocolException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Creates {@link NetworkStats} instances by parsing various {@code /proc/}
* files as needed.
*
* @hide
*/
public class NetworkStatsFactory {
private static final String TAG = "NetworkStatsFactory";
private static final boolean USE_NATIVE_PARSING = true;
private static final boolean VALIDATE_NATIVE_STATS = false;
/** Path to {@code /proc/net/xt_qtaguid/iface_stat_all}. */
private final File mStatsXtIfaceAll;
/** Path to {@code /proc/net/xt_qtaguid/iface_stat_fmt}. */
private final File mStatsXtIfaceFmt;
/** Path to {@code /proc/net/xt_qtaguid/stats}. */
private final File mStatsXtUid;
private final boolean mUseBpfStats;
private INetd mNetdService;
/**
* Guards persistent data access in this class
*
* <p>In order to prevent deadlocks, critical sections protected by this lock SHALL NOT call out
* to other code that will acquire other locks within the system server. See b/134244752.
*/
private final Object mPersistentDataLock = new Object();
/** Set containing info about active VPNs and their underlying networks. */
private volatile UnderlyingNetworkInfo[] mUnderlyingNetworkInfos = new UnderlyingNetworkInfo[0];
// A persistent snapshot of cumulative stats since device start
@GuardedBy("mPersistentDataLock")
private NetworkStats mPersistSnapshot;
// The persistent snapshot of tun and 464xlat adjusted stats since device start
@GuardedBy("mPersistentDataLock")
private NetworkStats mTunAnd464xlatAdjustedStats;
/**
* (Stacked interface) -> (base interface) association for all connected ifaces since boot.
*
* Because counters must never roll backwards, once a given interface is stacked on top of an
* underlying interface, the stacked interface can never be stacked on top of
* another interface. */
private final ConcurrentHashMap<String, String> mStackedIfaces
= new ConcurrentHashMap<>();
/** Informs the factory of a new stacked interface. */
public void noteStackedIface(String stackedIface, String baseIface) {
if (stackedIface != null && baseIface != null) {
mStackedIfaces.put(stackedIface, baseIface);
}
}
/**
* Set active VPN information for data usage migration purposes
*
* <p>Traffic on TUN-based VPNs inherently all appear to be originated from the VPN providing
* app's UID. This method is used to support migration of VPN data usage, ensuring data is
* accurately billed to the real owner of the traffic.
*
* @param vpnArray The snapshot of the currently-running VPNs.
*/
public void updateUnderlyingNetworkInfos(UnderlyingNetworkInfo[] vpnArray) {
mUnderlyingNetworkInfos = vpnArray.clone();
}
/**
* Get a set of interfaces containing specified ifaces and stacked interfaces.
*
* <p>The added stacked interfaces are ifaces stacked on top of the specified ones, or ifaces
* on which the specified ones are stacked. Stacked interfaces are those noted with
* {@link #noteStackedIface(String, String)}, but only interfaces noted before this method
* is called are guaranteed to be included.
*/
public String[] augmentWithStackedInterfaces(@Nullable String[] requiredIfaces) {
if (requiredIfaces == NetworkStats.INTERFACES_ALL) {
return null;
}
HashSet<String> relatedIfaces = new HashSet<>(Arrays.asList(requiredIfaces));
// ConcurrentHashMap's EntrySet iterators are "guaranteed to traverse
// elements as they existed upon construction exactly once, and may
// (but are not guaranteed to) reflect any modifications subsequent to construction".
// This is enough here.
for (Map.Entry<String, String> entry : mStackedIfaces.entrySet()) {
if (relatedIfaces.contains(entry.getKey())) {
relatedIfaces.add(entry.getValue());
} else if (relatedIfaces.contains(entry.getValue())) {
relatedIfaces.add(entry.getKey());
}
}
String[] outArray = new String[relatedIfaces.size()];
return relatedIfaces.toArray(outArray);
}
/**
* Applies 464xlat adjustments with ifaces noted with {@link #noteStackedIface(String, String)}.
* @see NetworkStats#apply464xlatAdjustments(NetworkStats, NetworkStats, Map)
*/
public void apply464xlatAdjustments(NetworkStats baseTraffic, NetworkStats stackedTraffic) {
NetworkStats.apply464xlatAdjustments(baseTraffic, stackedTraffic, mStackedIfaces);
}
public NetworkStatsFactory() {
this(new File("/proc/"), true);
}
@VisibleForTesting
public NetworkStatsFactory(File procRoot, boolean useBpfStats) {
mStatsXtIfaceAll = new File(procRoot, "net/xt_qtaguid/iface_stat_all");
mStatsXtIfaceFmt = new File(procRoot, "net/xt_qtaguid/iface_stat_fmt");
mStatsXtUid = new File(procRoot, "net/xt_qtaguid/stats");
mUseBpfStats = useBpfStats;
synchronized (mPersistentDataLock) {
mPersistSnapshot = new NetworkStats(SystemClock.elapsedRealtime(), -1);
mTunAnd464xlatAdjustedStats = new NetworkStats(SystemClock.elapsedRealtime(), -1);
}
}
public NetworkStats readBpfNetworkStatsDev() throws IOException {
final NetworkStats stats = new NetworkStats(SystemClock.elapsedRealtime(), 6);
if (nativeReadNetworkStatsDev(stats) != 0) {
throw new IOException("Failed to parse bpf iface stats");
}
return stats;
}
/**
* Parse and return interface-level summary {@link NetworkStats} measured
* using {@code /proc/net/dev} style hooks, which may include non IP layer
* traffic. Values monotonically increase since device boot, and may include
* details about inactive interfaces.
*
* @throws IllegalStateException when problem parsing stats.
*/
public NetworkStats readNetworkStatsSummaryDev() throws IOException {
// Return xt_bpf stats if switched to bpf module.
if (mUseBpfStats)
return readBpfNetworkStatsDev();
final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
final NetworkStats stats = new NetworkStats(SystemClock.elapsedRealtime(), 6);
final NetworkStats.Entry entry = new NetworkStats.Entry();
ProcFileReader reader = null;
try {
reader = new ProcFileReader(new FileInputStream(mStatsXtIfaceAll));
while (reader.hasMoreData()) {
entry.iface = reader.nextString();
entry.uid = UID_ALL;
entry.set = SET_ALL;
entry.tag = TAG_NONE;
final boolean active = reader.nextInt() != 0;
// always include snapshot values
entry.rxBytes = reader.nextLong();
entry.rxPackets = reader.nextLong();
entry.txBytes = reader.nextLong();
entry.txPackets = reader.nextLong();
// fold in active numbers, but only when active
if (active) {
entry.rxBytes += reader.nextLong();
entry.rxPackets += reader.nextLong();
entry.txBytes += reader.nextLong();
entry.txPackets += reader.nextLong();
}
stats.insertEntry(entry);
reader.finishLine();
}
} catch (NullPointerException|NumberFormatException e) {
throw protocolExceptionWithCause("problem parsing stats", e);
} finally {
IoUtils.closeQuietly(reader);
StrictMode.setThreadPolicy(savedPolicy);
}
return stats;
}
/**
* Parse and return interface-level summary {@link NetworkStats}. Designed
* to return only IP layer traffic. Values monotonically increase since
* device boot, and may include details about inactive interfaces.
*
* @throws IllegalStateException when problem parsing stats.
*/
public NetworkStats readNetworkStatsSummaryXt() throws IOException {
// Return xt_bpf stats if qtaguid module is replaced.
if (mUseBpfStats)
return readBpfNetworkStatsDev();
final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
// return null when kernel doesn't support
if (!mStatsXtIfaceFmt.exists()) return null;
final NetworkStats stats = new NetworkStats(SystemClock.elapsedRealtime(), 6);
final NetworkStats.Entry entry = new NetworkStats.Entry();
ProcFileReader reader = null;
try {
// open and consume header line
reader = new ProcFileReader(new FileInputStream(mStatsXtIfaceFmt));
reader.finishLine();
while (reader.hasMoreData()) {
entry.iface = reader.nextString();
entry.uid = UID_ALL;
entry.set = SET_ALL;
entry.tag = TAG_NONE;
entry.rxBytes = reader.nextLong();
entry.rxPackets = reader.nextLong();
entry.txBytes = reader.nextLong();
entry.txPackets = reader.nextLong();
stats.insertEntry(entry);
reader.finishLine();
}
} catch (NullPointerException|NumberFormatException e) {
throw protocolExceptionWithCause("problem parsing stats", e);
} finally {
IoUtils.closeQuietly(reader);
StrictMode.setThreadPolicy(savedPolicy);
}
return stats;
}
public NetworkStats readNetworkStatsDetail() throws IOException {
return readNetworkStatsDetail(UID_ALL, INTERFACES_ALL, TAG_ALL);
}
@GuardedBy("mPersistentDataLock")
private void requestSwapActiveStatsMapLocked() throws RemoteException {
// Ask netd to do a active map stats swap. When the binder call successfully returns,
// the system server should be able to safely read and clean the inactive map
// without race problem.
if (mNetdService == null) {
mNetdService = NetdService.getInstance();
}
mNetdService.trafficSwapActiveStatsMap();
}
/**
* Reads the detailed UID stats based on the provided parameters
*
* @param limitUid the UID to limit this query to
* @param limitIfaces the interfaces to limit this query to. Use {@link
* NetworkStats.INTERFACES_ALL} to select all interfaces
* @param limitTag the tags to limit this query to
* @return the NetworkStats instance containing network statistics at the present time.
*/
public NetworkStats readNetworkStatsDetail(
int limitUid, String[] limitIfaces, int limitTag) throws IOException {
// In order to prevent deadlocks, anything protected by this lock MUST NOT call out to other
// code that will acquire other locks within the system server. See b/134244752.
synchronized (mPersistentDataLock) {
// Take a reference. If this gets swapped out, we still have the old reference.
final UnderlyingNetworkInfo[] vpnArray = mUnderlyingNetworkInfos;
// Take a defensive copy. mPersistSnapshot is mutated in some cases below
final NetworkStats prev = mPersistSnapshot.clone();
if (USE_NATIVE_PARSING) {
final NetworkStats stats =
new NetworkStats(SystemClock.elapsedRealtime(), 0 /* initialSize */);
if (mUseBpfStats) {
try {
requestSwapActiveStatsMapLocked();
} catch (RemoteException e) {
throw new IOException(e);
}
// Stats are always read from the inactive map, so they must be read after the
// swap
if (nativeReadNetworkStatsDetail(stats, mStatsXtUid.getAbsolutePath(), UID_ALL,
INTERFACES_ALL, TAG_ALL, mUseBpfStats) != 0) {
throw new IOException("Failed to parse network stats");
}
// BPF stats are incremental; fold into mPersistSnapshot.
mPersistSnapshot.setElapsedRealtime(stats.getElapsedRealtime());
mPersistSnapshot.combineAllValues(stats);
} else {
if (nativeReadNetworkStatsDetail(stats, mStatsXtUid.getAbsolutePath(), UID_ALL,
INTERFACES_ALL, TAG_ALL, mUseBpfStats) != 0) {
throw new IOException("Failed to parse network stats");
}
if (VALIDATE_NATIVE_STATS) {
final NetworkStats javaStats = javaReadNetworkStatsDetail(mStatsXtUid,
UID_ALL, INTERFACES_ALL, TAG_ALL);
assertEquals(javaStats, stats);
}
mPersistSnapshot = stats;
}
} else {
mPersistSnapshot = javaReadNetworkStatsDetail(mStatsXtUid, UID_ALL, INTERFACES_ALL,
TAG_ALL);
}
NetworkStats adjustedStats = adjustForTunAnd464Xlat(mPersistSnapshot, prev, vpnArray);
// Filter return values
adjustedStats.filter(limitUid, limitIfaces, limitTag);
return adjustedStats;
}
}
@GuardedBy("mPersistentDataLock")
private NetworkStats adjustForTunAnd464Xlat(NetworkStats uidDetailStats,
NetworkStats previousStats, UnderlyingNetworkInfo[] vpnArray) {
// Calculate delta from last snapshot
final NetworkStats delta = uidDetailStats.subtract(previousStats);
// Apply 464xlat adjustments before VPN adjustments. If VPNs are using v4 on a v6 only
// network, the overhead is their fault.
// No locking here: apply464xlatAdjustments behaves fine with an add-only
// ConcurrentHashMap.
delta.apply464xlatAdjustments(mStackedIfaces);
// Migrate data usage over a VPN to the TUN network.
for (UnderlyingNetworkInfo info : vpnArray) {
delta.migrateTun(info.getOwnerUid(), info.getInterface(),
info.getUnderlyingInterfaces());
// Filter out debug entries as that may lead to over counting.
delta.filterDebugEntries();
}
// Update mTunAnd464xlatAdjustedStats with migrated delta.
mTunAnd464xlatAdjustedStats.combineAllValues(delta);
mTunAnd464xlatAdjustedStats.setElapsedRealtime(uidDetailStats.getElapsedRealtime());
return mTunAnd464xlatAdjustedStats.clone();
}
/**
* Parse and return {@link NetworkStats} with UID-level details. Values are
* expected to monotonically increase since device boot.
*/
@VisibleForTesting
public static NetworkStats javaReadNetworkStatsDetail(File detailPath, int limitUid,
String[] limitIfaces, int limitTag)
throws IOException {
final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
final NetworkStats stats = new NetworkStats(SystemClock.elapsedRealtime(), 24);
final NetworkStats.Entry entry = new NetworkStats.Entry();
int idx = 1;
int lastIdx = 1;
ProcFileReader reader = null;
try {
// open and consume header line
reader = new ProcFileReader(new FileInputStream(detailPath));
reader.finishLine();
while (reader.hasMoreData()) {
idx = reader.nextInt();
if (idx != lastIdx + 1) {
throw new ProtocolException(
"inconsistent idx=" + idx + " after lastIdx=" + lastIdx);
}
lastIdx = idx;
entry.iface = reader.nextString();
entry.tag = kernelToTag(reader.nextString());
entry.uid = reader.nextInt();
entry.set = reader.nextInt();
entry.rxBytes = reader.nextLong();
entry.rxPackets = reader.nextLong();
entry.txBytes = reader.nextLong();
entry.txPackets = reader.nextLong();
if ((limitIfaces == null || ArrayUtils.contains(limitIfaces, entry.iface))
&& (limitUid == UID_ALL || limitUid == entry.uid)
&& (limitTag == TAG_ALL || limitTag == entry.tag)) {
stats.insertEntry(entry);
}
reader.finishLine();
}
} catch (NullPointerException|NumberFormatException e) {
throw protocolExceptionWithCause("problem parsing idx " + idx, e);
} finally {
IoUtils.closeQuietly(reader);
StrictMode.setThreadPolicy(savedPolicy);
}
return stats;
}
public void assertEquals(NetworkStats expected, NetworkStats actual) {
if (expected.size() != actual.size()) {
throw new AssertionError(
"Expected size " + expected.size() + ", actual size " + actual.size());
}
NetworkStats.Entry expectedRow = null;
NetworkStats.Entry actualRow = null;
for (int i = 0; i < expected.size(); i++) {
expectedRow = expected.getValues(i, expectedRow);
actualRow = actual.getValues(i, actualRow);
if (!expectedRow.equals(actualRow)) {
throw new AssertionError(
"Expected row " + i + ": " + expectedRow + ", actual row " + actualRow);
}
}
}
/**
* Parse statistics from file into given {@link NetworkStats} object. Values
* are expected to monotonically increase since device boot.
*/
@VisibleForTesting
public static native int nativeReadNetworkStatsDetail(NetworkStats stats, String path,
int limitUid, String[] limitIfaces, int limitTag, boolean useBpfStats);
@VisibleForTesting
public static native int nativeReadNetworkStatsDev(NetworkStats stats);
private static ProtocolException protocolExceptionWithCause(String message, Throwable cause) {
ProtocolException pe = new ProtocolException(message);
pe.initCause(cause);
return pe;
}
}

View File

@@ -0,0 +1,442 @@
/*
* Copyright (C) 2016 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 com.android.server.net;
import static android.app.usage.NetworkStatsManager.MIN_THRESHOLD_BYTES;
import static com.android.internal.util.Preconditions.checkArgument;
import android.app.usage.NetworkStatsManager;
import android.net.DataUsageRequest;
import android.net.NetworkStats;
import android.net.NetworkStatsHistory;
import android.net.NetworkTemplate;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.Messenger;
import android.os.Process;
import android.os.RemoteException;
import android.util.ArrayMap;
import android.util.Slog;
import android.util.SparseArray;
import com.android.internal.annotations.VisibleForTesting;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Manages observers of {@link NetworkStats}. Allows observers to be notified when
* data usage has been reported in {@link NetworkStatsService}. An observer can set
* a threshold of how much data it cares about to be notified.
*/
class NetworkStatsObservers {
private static final String TAG = "NetworkStatsObservers";
private static final boolean LOGV = false;
private static final int MSG_REGISTER = 1;
private static final int MSG_UNREGISTER = 2;
private static final int MSG_UPDATE_STATS = 3;
// All access to this map must be done from the handler thread.
// indexed by DataUsageRequest#requestId
private final SparseArray<RequestInfo> mDataUsageRequests = new SparseArray<>();
// Sequence number of DataUsageRequests
private final AtomicInteger mNextDataUsageRequestId = new AtomicInteger();
// Lazily instantiated when an observer is registered.
private volatile Handler mHandler;
/**
* Creates a wrapper that contains the caller context and a normalized request.
* The request should be returned to the caller app, and the wrapper should be sent to this
* object through #addObserver by the service handler.
*
* <p>It will register the observer asynchronously, so it is safe to call from any thread.
*
* @return the normalized request wrapped within {@link RequestInfo}.
*/
public DataUsageRequest register(DataUsageRequest inputRequest, Messenger messenger,
IBinder binder, int callingUid, @NetworkStatsAccess.Level int accessLevel) {
DataUsageRequest request = buildRequest(inputRequest);
RequestInfo requestInfo = buildRequestInfo(request, messenger, binder, callingUid,
accessLevel);
if (LOGV) Slog.v(TAG, "Registering observer for " + request);
getHandler().sendMessage(mHandler.obtainMessage(MSG_REGISTER, requestInfo));
return request;
}
/**
* Unregister a data usage observer.
*
* <p>It will unregister the observer asynchronously, so it is safe to call from any thread.
*/
public void unregister(DataUsageRequest request, int callingUid) {
getHandler().sendMessage(mHandler.obtainMessage(MSG_UNREGISTER, callingUid, 0 /* ignore */,
request));
}
/**
* Updates data usage statistics of registered observers and notifies if limits are reached.
*
* <p>It will update stats asynchronously, so it is safe to call from any thread.
*/
public void updateStats(NetworkStats xtSnapshot, NetworkStats uidSnapshot,
ArrayMap<String, NetworkIdentitySet> activeIfaces,
ArrayMap<String, NetworkIdentitySet> activeUidIfaces,
long currentTime) {
StatsContext statsContext = new StatsContext(xtSnapshot, uidSnapshot, activeIfaces,
activeUidIfaces, currentTime);
getHandler().sendMessage(mHandler.obtainMessage(MSG_UPDATE_STATS, statsContext));
}
private Handler getHandler() {
if (mHandler == null) {
synchronized (this) {
if (mHandler == null) {
if (LOGV) Slog.v(TAG, "Creating handler");
mHandler = new Handler(getHandlerLooperLocked(), mHandlerCallback);
}
}
}
return mHandler;
}
@VisibleForTesting
protected Looper getHandlerLooperLocked() {
HandlerThread handlerThread = new HandlerThread(TAG);
handlerThread.start();
return handlerThread.getLooper();
}
private Handler.Callback mHandlerCallback = new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case MSG_REGISTER: {
handleRegister((RequestInfo) msg.obj);
return true;
}
case MSG_UNREGISTER: {
handleUnregister((DataUsageRequest) msg.obj, msg.arg1 /* callingUid */);
return true;
}
case MSG_UPDATE_STATS: {
handleUpdateStats((StatsContext) msg.obj);
return true;
}
default: {
return false;
}
}
}
};
/**
* Adds a {@link RequestInfo} as an observer.
* Should only be called from the handler thread otherwise there will be a race condition
* on mDataUsageRequests.
*/
private void handleRegister(RequestInfo requestInfo) {
mDataUsageRequests.put(requestInfo.mRequest.requestId, requestInfo);
}
/**
* Removes a {@link DataUsageRequest} if the calling uid is authorized.
* Should only be called from the handler thread otherwise there will be a race condition
* on mDataUsageRequests.
*/
private void handleUnregister(DataUsageRequest request, int callingUid) {
RequestInfo requestInfo;
requestInfo = mDataUsageRequests.get(request.requestId);
if (requestInfo == null) {
if (LOGV) Slog.v(TAG, "Trying to unregister unknown request " + request);
return;
}
if (Process.SYSTEM_UID != callingUid && requestInfo.mCallingUid != callingUid) {
Slog.w(TAG, "Caller uid " + callingUid + " is not owner of " + request);
return;
}
if (LOGV) Slog.v(TAG, "Unregistering " + request);
mDataUsageRequests.remove(request.requestId);
requestInfo.unlinkDeathRecipient();
requestInfo.callCallback(NetworkStatsManager.CALLBACK_RELEASED);
}
private void handleUpdateStats(StatsContext statsContext) {
if (mDataUsageRequests.size() == 0) {
return;
}
for (int i = 0; i < mDataUsageRequests.size(); i++) {
RequestInfo requestInfo = mDataUsageRequests.valueAt(i);
requestInfo.updateStats(statsContext);
}
}
private DataUsageRequest buildRequest(DataUsageRequest request) {
// Cap the minimum threshold to a safe default to avoid too many callbacks
long thresholdInBytes = Math.max(MIN_THRESHOLD_BYTES, request.thresholdInBytes);
if (thresholdInBytes < request.thresholdInBytes) {
Slog.w(TAG, "Threshold was too low for " + request
+ ". Overriding to a safer default of " + thresholdInBytes + " bytes");
}
return new DataUsageRequest(mNextDataUsageRequestId.incrementAndGet(),
request.template, thresholdInBytes);
}
private RequestInfo buildRequestInfo(DataUsageRequest request,
Messenger messenger, IBinder binder, int callingUid,
@NetworkStatsAccess.Level int accessLevel) {
if (accessLevel <= NetworkStatsAccess.Level.USER) {
return new UserUsageRequestInfo(this, request, messenger, binder, callingUid,
accessLevel);
} else {
// Safety check in case a new access level is added and we forgot to update this
checkArgument(accessLevel >= NetworkStatsAccess.Level.DEVICESUMMARY);
return new NetworkUsageRequestInfo(this, request, messenger, binder, callingUid,
accessLevel);
}
}
/**
* Tracks information relevant to a data usage observer.
* It will notice when the calling process dies so we can self-expire.
*/
private abstract static class RequestInfo implements IBinder.DeathRecipient {
private final NetworkStatsObservers mStatsObserver;
protected final DataUsageRequest mRequest;
private final Messenger mMessenger;
private final IBinder mBinder;
protected final int mCallingUid;
protected final @NetworkStatsAccess.Level int mAccessLevel;
protected NetworkStatsRecorder mRecorder;
protected NetworkStatsCollection mCollection;
RequestInfo(NetworkStatsObservers statsObserver, DataUsageRequest request,
Messenger messenger, IBinder binder, int callingUid,
@NetworkStatsAccess.Level int accessLevel) {
mStatsObserver = statsObserver;
mRequest = request;
mMessenger = messenger;
mBinder = binder;
mCallingUid = callingUid;
mAccessLevel = accessLevel;
try {
mBinder.linkToDeath(this, 0);
} catch (RemoteException e) {
binderDied();
}
}
@Override
public void binderDied() {
if (LOGV) Slog.v(TAG, "RequestInfo binderDied("
+ mRequest + ", " + mBinder + ")");
mStatsObserver.unregister(mRequest, Process.SYSTEM_UID);
callCallback(NetworkStatsManager.CALLBACK_RELEASED);
}
@Override
public String toString() {
return "RequestInfo from uid:" + mCallingUid
+ " for " + mRequest + " accessLevel:" + mAccessLevel;
}
private void unlinkDeathRecipient() {
if (mBinder != null) {
mBinder.unlinkToDeath(this, 0);
}
}
/**
* Update stats given the samples and interface to identity mappings.
*/
private void updateStats(StatsContext statsContext) {
if (mRecorder == null) {
// First run; establish baseline stats
resetRecorder();
recordSample(statsContext);
return;
}
recordSample(statsContext);
if (checkStats()) {
resetRecorder();
callCallback(NetworkStatsManager.CALLBACK_LIMIT_REACHED);
}
}
private void callCallback(int callbackType) {
Bundle bundle = new Bundle();
bundle.putParcelable(DataUsageRequest.PARCELABLE_KEY, mRequest);
Message msg = Message.obtain();
msg.what = callbackType;
msg.setData(bundle);
try {
if (LOGV) {
Slog.v(TAG, "sending notification " + callbackTypeToName(callbackType)
+ " for " + mRequest);
}
mMessenger.send(msg);
} catch (RemoteException e) {
// May occur naturally in the race of binder death.
Slog.w(TAG, "RemoteException caught trying to send a callback msg for " + mRequest);
}
}
private void resetRecorder() {
mRecorder = new NetworkStatsRecorder();
mCollection = mRecorder.getSinceBoot();
}
protected abstract boolean checkStats();
protected abstract void recordSample(StatsContext statsContext);
private String callbackTypeToName(int callbackType) {
switch (callbackType) {
case NetworkStatsManager.CALLBACK_LIMIT_REACHED:
return "LIMIT_REACHED";
case NetworkStatsManager.CALLBACK_RELEASED:
return "RELEASED";
default:
return "UNKNOWN";
}
}
}
private static class NetworkUsageRequestInfo extends RequestInfo {
NetworkUsageRequestInfo(NetworkStatsObservers statsObserver, DataUsageRequest request,
Messenger messenger, IBinder binder, int callingUid,
@NetworkStatsAccess.Level int accessLevel) {
super(statsObserver, request, messenger, binder, callingUid, accessLevel);
}
@Override
protected boolean checkStats() {
long bytesSoFar = getTotalBytesForNetwork(mRequest.template);
if (LOGV) {
Slog.v(TAG, bytesSoFar + " bytes so far since notification for "
+ mRequest.template);
}
if (bytesSoFar > mRequest.thresholdInBytes) {
return true;
}
return false;
}
@Override
protected void recordSample(StatsContext statsContext) {
// Recorder does not need to be locked in this context since only the handler
// thread will update it. We pass a null VPN array because usage is aggregated by uid
// for this snapshot, so VPN traffic can't be reattributed to responsible apps.
mRecorder.recordSnapshotLocked(statsContext.mXtSnapshot, statsContext.mActiveIfaces,
statsContext.mCurrentTime);
}
/**
* Reads stats matching the given template. {@link NetworkStatsCollection} will aggregate
* over all buckets, which in this case should be only one since we built it big enough
* that it will outlive the caller. If it doesn't, then there will be multiple buckets.
*/
private long getTotalBytesForNetwork(NetworkTemplate template) {
NetworkStats stats = mCollection.getSummary(template,
Long.MIN_VALUE /* start */, Long.MAX_VALUE /* end */,
mAccessLevel, mCallingUid);
return stats.getTotalBytes();
}
}
private static class UserUsageRequestInfo extends RequestInfo {
UserUsageRequestInfo(NetworkStatsObservers statsObserver, DataUsageRequest request,
Messenger messenger, IBinder binder, int callingUid,
@NetworkStatsAccess.Level int accessLevel) {
super(statsObserver, request, messenger, binder, callingUid, accessLevel);
}
@Override
protected boolean checkStats() {
int[] uidsToMonitor = mCollection.getRelevantUids(mAccessLevel, mCallingUid);
for (int i = 0; i < uidsToMonitor.length; i++) {
long bytesSoFar = getTotalBytesForNetworkUid(mRequest.template, uidsToMonitor[i]);
if (bytesSoFar > mRequest.thresholdInBytes) {
return true;
}
}
return false;
}
@Override
protected void recordSample(StatsContext statsContext) {
// Recorder does not need to be locked in this context since only the handler
// thread will update it. We pass the VPN info so VPN traffic is reattributed to
// responsible apps.
mRecorder.recordSnapshotLocked(statsContext.mUidSnapshot, statsContext.mActiveUidIfaces,
statsContext.mCurrentTime);
}
/**
* Reads all stats matching the given template and uid. Ther history will likely only
* contain one bucket per ident since we build it big enough that it will outlive the
* caller lifetime.
*/
private long getTotalBytesForNetworkUid(NetworkTemplate template, int uid) {
try {
NetworkStatsHistory history = mCollection.getHistory(template, null, uid,
NetworkStats.SET_ALL, NetworkStats.TAG_NONE,
NetworkStatsHistory.FIELD_ALL,
Long.MIN_VALUE /* start */, Long.MAX_VALUE /* end */,
mAccessLevel, mCallingUid);
return history.getTotalBytes();
} catch (SecurityException e) {
if (LOGV) {
Slog.w(TAG, "CallerUid " + mCallingUid + " may have lost access to uid "
+ uid);
}
return 0;
}
}
}
private static class StatsContext {
NetworkStats mXtSnapshot;
NetworkStats mUidSnapshot;
ArrayMap<String, NetworkIdentitySet> mActiveIfaces;
ArrayMap<String, NetworkIdentitySet> mActiveUidIfaces;
long mCurrentTime;
StatsContext(NetworkStats xtSnapshot, NetworkStats uidSnapshot,
ArrayMap<String, NetworkIdentitySet> activeIfaces,
ArrayMap<String, NetworkIdentitySet> activeUidIfaces,
long currentTime) {
mXtSnapshot = xtSnapshot;
mUidSnapshot = uidSnapshot;
mActiveIfaces = activeIfaces;
mActiveUidIfaces = activeUidIfaces;
mCurrentTime = currentTime;
}
}
}

View File

@@ -0,0 +1,505 @@
/*
* 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 com.android.server.net;
import static android.net.NetworkStats.TAG_NONE;
import static android.net.TrafficStats.KB_IN_BYTES;
import static android.net.TrafficStats.MB_IN_BYTES;
import static android.text.format.DateUtils.YEAR_IN_MILLIS;
import android.net.NetworkStats;
import android.net.NetworkStats.NonMonotonicObserver;
import android.net.NetworkStatsHistory;
import android.net.NetworkTemplate;
import android.net.TrafficStats;
import android.os.Binder;
import android.os.DropBoxManager;
import android.service.NetworkStatsRecorderProto;
import android.util.Log;
import android.util.MathUtils;
import android.util.Slog;
import android.util.proto.ProtoOutputStream;
import com.android.internal.util.FileRotator;
import com.android.internal.util.IndentingPrintWriter;
import com.google.android.collect.Sets;
import libcore.io.IoUtils;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.lang.ref.WeakReference;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
/**
* Logic to record deltas between periodic {@link NetworkStats} snapshots into
* {@link NetworkStatsHistory} that belong to {@link NetworkStatsCollection}.
* Keeps pending changes in memory until they pass a specific threshold, in
* bytes. Uses {@link FileRotator} for persistence logic if present.
* <p>
* Not inherently thread safe.
*/
public class NetworkStatsRecorder {
private static final String TAG = "NetworkStatsRecorder";
private static final boolean LOGD = false;
private static final boolean LOGV = false;
private static final String TAG_NETSTATS_DUMP = "netstats_dump";
/** Dump before deleting in {@link #recoverFromWtf()}. */
private static final boolean DUMP_BEFORE_DELETE = true;
private final FileRotator mRotator;
private final NonMonotonicObserver<String> mObserver;
private final DropBoxManager mDropBox;
private final String mCookie;
private final long mBucketDuration;
private final boolean mOnlyTags;
private long mPersistThresholdBytes = 2 * MB_IN_BYTES;
private NetworkStats mLastSnapshot;
private final NetworkStatsCollection mPending;
private final NetworkStatsCollection mSinceBoot;
private final CombiningRewriter mPendingRewriter;
private WeakReference<NetworkStatsCollection> mComplete;
/**
* Non-persisted recorder, with only one bucket. Used by {@link NetworkStatsObservers}.
*/
public NetworkStatsRecorder() {
mRotator = null;
mObserver = null;
mDropBox = null;
mCookie = null;
// set the bucket big enough to have all data in one bucket, but allow some
// slack to avoid overflow
mBucketDuration = YEAR_IN_MILLIS;
mOnlyTags = false;
mPending = null;
mSinceBoot = new NetworkStatsCollection(mBucketDuration);
mPendingRewriter = null;
}
/**
* Persisted recorder.
*/
public NetworkStatsRecorder(FileRotator rotator, NonMonotonicObserver<String> observer,
DropBoxManager dropBox, String cookie, long bucketDuration, boolean onlyTags) {
mRotator = Objects.requireNonNull(rotator, "missing FileRotator");
mObserver = Objects.requireNonNull(observer, "missing NonMonotonicObserver");
mDropBox = Objects.requireNonNull(dropBox, "missing DropBoxManager");
mCookie = cookie;
mBucketDuration = bucketDuration;
mOnlyTags = onlyTags;
mPending = new NetworkStatsCollection(bucketDuration);
mSinceBoot = new NetworkStatsCollection(bucketDuration);
mPendingRewriter = new CombiningRewriter(mPending);
}
public void setPersistThreshold(long thresholdBytes) {
if (LOGV) Slog.v(TAG, "setPersistThreshold() with " + thresholdBytes);
mPersistThresholdBytes = MathUtils.constrain(
thresholdBytes, 1 * KB_IN_BYTES, 100 * MB_IN_BYTES);
}
public void resetLocked() {
mLastSnapshot = null;
if (mPending != null) {
mPending.reset();
}
if (mSinceBoot != null) {
mSinceBoot.reset();
}
if (mComplete != null) {
mComplete.clear();
}
}
public NetworkStats.Entry getTotalSinceBootLocked(NetworkTemplate template) {
return mSinceBoot.getSummary(template, Long.MIN_VALUE, Long.MAX_VALUE,
NetworkStatsAccess.Level.DEVICE, Binder.getCallingUid()).getTotal(null);
}
public NetworkStatsCollection getSinceBoot() {
return mSinceBoot;
}
/**
* Load complete history represented by {@link FileRotator}. Caches
* internally as a {@link WeakReference}, and updated with future
* {@link #recordSnapshotLocked(NetworkStats, Map, long)} snapshots as long
* as reference is valid.
*/
public NetworkStatsCollection getOrLoadCompleteLocked() {
Objects.requireNonNull(mRotator, "missing FileRotator");
NetworkStatsCollection res = mComplete != null ? mComplete.get() : null;
if (res == null) {
res = loadLocked(Long.MIN_VALUE, Long.MAX_VALUE);
mComplete = new WeakReference<NetworkStatsCollection>(res);
}
return res;
}
public NetworkStatsCollection getOrLoadPartialLocked(long start, long end) {
Objects.requireNonNull(mRotator, "missing FileRotator");
NetworkStatsCollection res = mComplete != null ? mComplete.get() : null;
if (res == null) {
res = loadLocked(start, end);
}
return res;
}
private NetworkStatsCollection loadLocked(long start, long end) {
if (LOGD) Slog.d(TAG, "loadLocked() reading from disk for " + mCookie);
final NetworkStatsCollection res = new NetworkStatsCollection(mBucketDuration);
try {
mRotator.readMatching(res, start, end);
res.recordCollection(mPending);
} catch (IOException e) {
Log.wtf(TAG, "problem completely reading network stats", e);
recoverFromWtf();
} catch (OutOfMemoryError e) {
Log.wtf(TAG, "problem completely reading network stats", e);
recoverFromWtf();
}
return res;
}
/**
* Record any delta that occurred since last {@link NetworkStats} snapshot, using the given
* {@link Map} to identify network interfaces. First snapshot is considered bootstrap, and is
* not counted as delta.
*/
public void recordSnapshotLocked(NetworkStats snapshot,
Map<String, NetworkIdentitySet> ifaceIdent, long currentTimeMillis) {
final HashSet<String> unknownIfaces = Sets.newHashSet();
// skip recording when snapshot missing
if (snapshot == null) return;
// assume first snapshot is bootstrap and don't record
if (mLastSnapshot == null) {
mLastSnapshot = snapshot;
return;
}
final NetworkStatsCollection complete = mComplete != null ? mComplete.get() : null;
final NetworkStats delta = NetworkStats.subtract(
snapshot, mLastSnapshot, mObserver, mCookie);
final long end = currentTimeMillis;
final long start = end - delta.getElapsedRealtime();
NetworkStats.Entry entry = null;
for (int i = 0; i < delta.size(); i++) {
entry = delta.getValues(i, entry);
// As a last-ditch check, report any negative values and
// clamp them so recording below doesn't croak.
if (entry.isNegative()) {
if (mObserver != null) {
mObserver.foundNonMonotonic(delta, i, mCookie);
}
entry.rxBytes = Math.max(entry.rxBytes, 0);
entry.rxPackets = Math.max(entry.rxPackets, 0);
entry.txBytes = Math.max(entry.txBytes, 0);
entry.txPackets = Math.max(entry.txPackets, 0);
entry.operations = Math.max(entry.operations, 0);
}
final NetworkIdentitySet ident = ifaceIdent.get(entry.iface);
if (ident == null) {
unknownIfaces.add(entry.iface);
continue;
}
// skip when no delta occurred
if (entry.isEmpty()) continue;
// only record tag data when requested
if ((entry.tag == TAG_NONE) != mOnlyTags) {
if (mPending != null) {
mPending.recordData(ident, entry.uid, entry.set, entry.tag, start, end, entry);
}
// also record against boot stats when present
if (mSinceBoot != null) {
mSinceBoot.recordData(ident, entry.uid, entry.set, entry.tag, start, end, entry);
}
// also record against complete dataset when present
if (complete != null) {
complete.recordData(ident, entry.uid, entry.set, entry.tag, start, end, entry);
}
}
}
mLastSnapshot = snapshot;
if (LOGV && unknownIfaces.size() > 0) {
Slog.w(TAG, "unknown interfaces " + unknownIfaces + ", ignoring those stats");
}
}
/**
* Consider persisting any pending deltas, if they are beyond
* {@link #mPersistThresholdBytes}.
*/
public void maybePersistLocked(long currentTimeMillis) {
Objects.requireNonNull(mRotator, "missing FileRotator");
final long pendingBytes = mPending.getTotalBytes();
if (pendingBytes >= mPersistThresholdBytes) {
forcePersistLocked(currentTimeMillis);
} else {
mRotator.maybeRotate(currentTimeMillis);
}
}
/**
* Force persisting any pending deltas.
*/
public void forcePersistLocked(long currentTimeMillis) {
Objects.requireNonNull(mRotator, "missing FileRotator");
if (mPending.isDirty()) {
if (LOGD) Slog.d(TAG, "forcePersistLocked() writing for " + mCookie);
try {
mRotator.rewriteActive(mPendingRewriter, currentTimeMillis);
mRotator.maybeRotate(currentTimeMillis);
mPending.reset();
} catch (IOException e) {
Log.wtf(TAG, "problem persisting pending stats", e);
recoverFromWtf();
} catch (OutOfMemoryError e) {
Log.wtf(TAG, "problem persisting pending stats", e);
recoverFromWtf();
}
}
}
/**
* Remove the given UID from all {@link FileRotator} history, migrating it
* to {@link TrafficStats#UID_REMOVED}.
*/
public void removeUidsLocked(int[] uids) {
if (mRotator != null) {
try {
// Rewrite all persisted data to migrate UID stats
mRotator.rewriteAll(new RemoveUidRewriter(mBucketDuration, uids));
} catch (IOException e) {
Log.wtf(TAG, "problem removing UIDs " + Arrays.toString(uids), e);
recoverFromWtf();
} catch (OutOfMemoryError e) {
Log.wtf(TAG, "problem removing UIDs " + Arrays.toString(uids), e);
recoverFromWtf();
}
}
// Remove any pending stats
if (mPending != null) {
mPending.removeUids(uids);
}
if (mSinceBoot != null) {
mSinceBoot.removeUids(uids);
}
// Clear UID from current stats snapshot
if (mLastSnapshot != null) {
mLastSnapshot.removeUids(uids);
}
final NetworkStatsCollection complete = mComplete != null ? mComplete.get() : null;
if (complete != null) {
complete.removeUids(uids);
}
}
/**
* Rewriter that will combine current {@link NetworkStatsCollection} values
* with anything read from disk, and write combined set to disk. Clears the
* original {@link NetworkStatsCollection} when finished writing.
*/
private static class CombiningRewriter implements FileRotator.Rewriter {
private final NetworkStatsCollection mCollection;
public CombiningRewriter(NetworkStatsCollection collection) {
mCollection = Objects.requireNonNull(collection, "missing NetworkStatsCollection");
}
@Override
public void reset() {
// ignored
}
@Override
public void read(InputStream in) throws IOException {
mCollection.read(in);
}
@Override
public boolean shouldWrite() {
return true;
}
@Override
public void write(OutputStream out) throws IOException {
mCollection.write(out);
mCollection.reset();
}
}
/**
* Rewriter that will remove any {@link NetworkStatsHistory} attributed to
* the requested UID, only writing data back when modified.
*/
public static class RemoveUidRewriter implements FileRotator.Rewriter {
private final NetworkStatsCollection mTemp;
private final int[] mUids;
public RemoveUidRewriter(long bucketDuration, int[] uids) {
mTemp = new NetworkStatsCollection(bucketDuration);
mUids = uids;
}
@Override
public void reset() {
mTemp.reset();
}
@Override
public void read(InputStream in) throws IOException {
mTemp.read(in);
mTemp.clearDirty();
mTemp.removeUids(mUids);
}
@Override
public boolean shouldWrite() {
return mTemp.isDirty();
}
@Override
public void write(OutputStream out) throws IOException {
mTemp.write(out);
}
}
public void importLegacyNetworkLocked(File file) throws IOException {
Objects.requireNonNull(mRotator, "missing FileRotator");
// legacy file still exists; start empty to avoid double importing
mRotator.deleteAll();
final NetworkStatsCollection collection = new NetworkStatsCollection(mBucketDuration);
collection.readLegacyNetwork(file);
final long startMillis = collection.getStartMillis();
final long endMillis = collection.getEndMillis();
if (!collection.isEmpty()) {
// process legacy data, creating active file at starting time, then
// using end time to possibly trigger rotation.
mRotator.rewriteActive(new CombiningRewriter(collection), startMillis);
mRotator.maybeRotate(endMillis);
}
}
public void importLegacyUidLocked(File file) throws IOException {
Objects.requireNonNull(mRotator, "missing FileRotator");
// legacy file still exists; start empty to avoid double importing
mRotator.deleteAll();
final NetworkStatsCollection collection = new NetworkStatsCollection(mBucketDuration);
collection.readLegacyUid(file, mOnlyTags);
final long startMillis = collection.getStartMillis();
final long endMillis = collection.getEndMillis();
if (!collection.isEmpty()) {
// process legacy data, creating active file at starting time, then
// using end time to possibly trigger rotation.
mRotator.rewriteActive(new CombiningRewriter(collection), startMillis);
mRotator.maybeRotate(endMillis);
}
}
public void dumpLocked(IndentingPrintWriter pw, boolean fullHistory) {
if (mPending != null) {
pw.print("Pending bytes: "); pw.println(mPending.getTotalBytes());
}
if (fullHistory) {
pw.println("Complete history:");
getOrLoadCompleteLocked().dump(pw);
} else {
pw.println("History since boot:");
mSinceBoot.dump(pw);
}
}
public void dumpDebugLocked(ProtoOutputStream proto, long tag) {
final long start = proto.start(tag);
if (mPending != null) {
proto.write(NetworkStatsRecorderProto.PENDING_TOTAL_BYTES, mPending.getTotalBytes());
}
getOrLoadCompleteLocked().dumpDebug(proto, NetworkStatsRecorderProto.COMPLETE_HISTORY);
proto.end(start);
}
public void dumpCheckin(PrintWriter pw, long start, long end) {
// Only load and dump stats from the requested window
getOrLoadPartialLocked(start, end).dumpCheckin(pw, start, end);
}
/**
* Recover from {@link FileRotator} failure by dumping state to
* {@link DropBoxManager} and deleting contents.
*/
private void recoverFromWtf() {
if (DUMP_BEFORE_DELETE) {
final ByteArrayOutputStream os = new ByteArrayOutputStream();
try {
mRotator.dumpAll(os);
} catch (IOException e) {
// ignore partial contents
os.reset();
} finally {
IoUtils.closeQuietly(os);
}
mDropBox.addData(TAG_NETSTATS_DUMP, os.toByteArray(), 0);
}
mRotator.deleteAll();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,243 @@
/*
* Copyright (C) 2020 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 com.android.server.net;
import static android.net.NetworkTemplate.NETWORK_TYPE_5G_NSA;
import static android.net.NetworkTemplate.getCollapsedRatType;
import android.annotation.NonNull;
import android.content.Context;
import android.os.Looper;
import android.telephony.Annotation;
import android.telephony.NetworkRegistrationInfo;
import android.telephony.PhoneStateListener;
import android.telephony.ServiceState;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.CollectionUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executor;
/**
* Helper class that watches for events that are triggered per subscription.
*/
public class NetworkStatsSubscriptionsMonitor extends
SubscriptionManager.OnSubscriptionsChangedListener {
/**
* Interface that this monitor uses to delegate event handling to NetworkStatsService.
*/
public interface Delegate {
/**
* Notify that the collapsed RAT type has been changed for any subscription. The method
* will also be triggered for any existing sub when start and stop monitoring.
*
* @param subscriberId IMSI of the subscription.
* @param collapsedRatType collapsed RAT type.
* @see android.net.NetworkTemplate#getCollapsedRatType(int).
*/
void onCollapsedRatTypeChanged(@NonNull String subscriberId,
@Annotation.NetworkType int collapsedRatType);
}
private final Delegate mDelegate;
/**
* Receivers that watches for {@link ServiceState} changes for each subscription, to
* monitor the transitioning between Radio Access Technology(RAT) types for each sub.
*/
@NonNull
private final CopyOnWriteArrayList<RatTypeListener> mRatListeners =
new CopyOnWriteArrayList<>();
@NonNull
private final SubscriptionManager mSubscriptionManager;
@NonNull
private final TelephonyManager mTeleManager;
@NonNull
private final Executor mExecutor;
NetworkStatsSubscriptionsMonitor(@NonNull Context context, @NonNull Looper looper,
@NonNull Executor executor, @NonNull Delegate delegate) {
super(looper);
mSubscriptionManager = (SubscriptionManager) context.getSystemService(
Context.TELEPHONY_SUBSCRIPTION_SERVICE);
mTeleManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
mExecutor = executor;
mDelegate = delegate;
}
@Override
public void onSubscriptionsChanged() {
// Collect active subId list, hidden subId such as opportunistic subscriptions are
// also needed to track CBRS.
final List<Integer> newSubs = getActiveSubIdList(mSubscriptionManager);
// IMSI is needed for every newly added sub. Listener stores subscriberId into it to
// prevent binder call to telephony when querying RAT. Keep listener registration with empty
// IMSI is meaningless since the RAT type changed is ambiguous for multi-SIM if reported
// with empty IMSI. So filter the subs w/o a valid IMSI to prevent such registration.
final List<Pair<Integer, String>> filteredNewSubs =
CollectionUtils.mapNotNull(newSubs, subId -> {
final String subscriberId = mTeleManager.getSubscriberId(subId);
return TextUtils.isEmpty(subscriberId) ? null : new Pair(subId, subscriberId);
});
for (final Pair<Integer, String> sub : filteredNewSubs) {
// Fully match listener with subId and IMSI, since in some rare cases, IMSI might be
// suddenly change regardless of subId, such as switch IMSI feature in modem side.
// If that happens, register new listener with new IMSI and remove old one later.
if (CollectionUtils.find(mRatListeners,
it -> it.equalsKey(sub.first, sub.second)) != null) {
continue;
}
final RatTypeListener listener =
new RatTypeListener(mExecutor, this, sub.first, sub.second);
mRatListeners.add(listener);
// Register listener to the telephony manager that associated with specific sub.
mTeleManager.createForSubscriptionId(sub.first)
.listen(listener, PhoneStateListener.LISTEN_SERVICE_STATE);
Log.d(NetworkStatsService.TAG, "RAT type listener registered for sub " + sub.first);
}
for (final RatTypeListener listener : new ArrayList<>(mRatListeners)) {
// If there is no subId and IMSI matched the listener, removes it.
if (CollectionUtils.find(filteredNewSubs,
it -> listener.equalsKey(it.first, it.second)) == null) {
handleRemoveRatTypeListener(listener);
}
}
}
@NonNull
private List<Integer> getActiveSubIdList(@NonNull SubscriptionManager subscriptionManager) {
final ArrayList<Integer> ret = new ArrayList<>();
final int[] ids = subscriptionManager.getCompleteActiveSubscriptionIdList();
for (int id : ids) ret.add(id);
return ret;
}
/**
* Get a collapsed RatType for the given subscriberId.
*
* @param subscriberId the target subscriberId
* @return collapsed RatType for the given subscriberId
*/
public int getRatTypeForSubscriberId(@NonNull String subscriberId) {
final RatTypeListener match = CollectionUtils.find(mRatListeners,
it -> TextUtils.equals(subscriberId, it.mSubscriberId));
return match != null ? match.mLastCollapsedRatType : TelephonyManager.NETWORK_TYPE_UNKNOWN;
}
/**
* Start monitoring events that triggered per subscription.
*/
public void start() {
mSubscriptionManager.addOnSubscriptionsChangedListener(mExecutor, this);
}
/**
* Unregister subscription changes and all listeners for each subscription.
*/
public void stop() {
mSubscriptionManager.removeOnSubscriptionsChangedListener(this);
for (final RatTypeListener listener : new ArrayList<>(mRatListeners)) {
handleRemoveRatTypeListener(listener);
}
}
private void handleRemoveRatTypeListener(@NonNull RatTypeListener listener) {
mTeleManager.createForSubscriptionId(listener.mSubId)
.listen(listener, PhoneStateListener.LISTEN_NONE);
Log.d(NetworkStatsService.TAG, "RAT type listener unregistered for sub " + listener.mSubId);
mRatListeners.remove(listener);
// Removal of subscriptions doesn't generate RAT changed event, fire it for every
// RatTypeListener.
mDelegate.onCollapsedRatTypeChanged(
listener.mSubscriberId, TelephonyManager.NETWORK_TYPE_UNKNOWN);
}
static class RatTypeListener extends PhoneStateListener {
// Unique id for the subscription. See {@link SubscriptionInfo#getSubscriptionId}.
@NonNull
private final int mSubId;
// IMSI to identifying the corresponding network from {@link NetworkState}.
// See {@link TelephonyManager#getSubscriberId}.
@NonNull
private final String mSubscriberId;
private volatile int mLastCollapsedRatType = TelephonyManager.NETWORK_TYPE_UNKNOWN;
@NonNull
private final NetworkStatsSubscriptionsMonitor mMonitor;
RatTypeListener(@NonNull Executor executor,
@NonNull NetworkStatsSubscriptionsMonitor monitor, int subId,
@NonNull String subscriberId) {
super(executor);
mSubId = subId;
mSubscriberId = subscriberId;
mMonitor = monitor;
}
@Override
public void onServiceStateChanged(@NonNull ServiceState ss) {
// In 5G SA (Stand Alone) mode, the primary cell itself will be 5G hence telephony
// would report RAT = 5G_NR.
// However, in 5G NSA (Non Stand Alone) mode, the primary cell is still LTE and
// network allocates a secondary 5G cell so telephony reports RAT = LTE along with
// NR state as connected. In such case, attributes the data usage to NR.
// See b/160727498.
final boolean is5GNsa = (ss.getDataNetworkType() == TelephonyManager.NETWORK_TYPE_LTE
|| ss.getDataNetworkType() == TelephonyManager.NETWORK_TYPE_LTE_CA)
&& ss.getNrState() == NetworkRegistrationInfo.NR_STATE_CONNECTED;
final int networkType =
(is5GNsa ? NETWORK_TYPE_5G_NSA : ss.getDataNetworkType());
final int collapsedRatType = getCollapsedRatType(networkType);
if (collapsedRatType == mLastCollapsedRatType) return;
if (NetworkStatsService.LOGD) {
Log.d(NetworkStatsService.TAG, "subtype changed for sub(" + mSubId + "): "
+ mLastCollapsedRatType + " -> " + collapsedRatType);
}
mLastCollapsedRatType = collapsedRatType;
mMonitor.mDelegate.onCollapsedRatTypeChanged(mSubscriberId, mLastCollapsedRatType);
}
@VisibleForTesting
public int getSubId() {
return mSubId;
}
boolean equalsKey(int subId, @NonNull String subscriberId) {
return mSubId == subId && TextUtils.equals(mSubscriberId, subscriberId);
}
}
}