[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:
191
service-t/src/com/android/server/net/NetworkIdentitySet.java
Normal file
191
service-t/src/com/android/server/net/NetworkIdentitySet.java
Normal 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);
|
||||
}
|
||||
}
|
||||
199
service-t/src/com/android/server/net/NetworkStatsAccess.java
Normal file
199
service-t/src/com/android/server/net/NetworkStatsAccess.java
Normal 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;
|
||||
}
|
||||
}
|
||||
820
service-t/src/com/android/server/net/NetworkStatsCollection.java
Normal file
820
service-t/src/com/android/server/net/NetworkStatsCollection.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
489
service-t/src/com/android/server/net/NetworkStatsFactory.java
Normal file
489
service-t/src/com/android/server/net/NetworkStatsFactory.java
Normal 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;
|
||||
}
|
||||
}
|
||||
442
service-t/src/com/android/server/net/NetworkStatsObservers.java
Normal file
442
service-t/src/com/android/server/net/NetworkStatsObservers.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
505
service-t/src/com/android/server/net/NetworkStatsRecorder.java
Normal file
505
service-t/src/com/android/server/net/NetworkStatsRecorder.java
Normal 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();
|
||||
}
|
||||
}
|
||||
2253
service-t/src/com/android/server/net/NetworkStatsService.java
Normal file
2253
service-t/src/com/android/server/net/NetworkStatsService.java
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user