[MS07.1] Move NetworkStatsCollection/IdentitySet into frameworks

These files are needed for the data migration util system Api
interfaces to allow OEMs to construct NetworkStats* objects.
Thus, they need to be moved into android.net package, and some
of them will be exposed as @SystemApi in T.

Eventually these classes will be moved into the Connectivity
module, but in the mean time they will be temporarily moved to
f/b/package/ConnectivityT for the preparation stage.
However, the tests are already in the module. Therefore for
the S-derived branch, the test cannot see the renamed classes
since any framework CLs will not be auto-merged into this branch.
Thus, the tests need to stay disabled on the S-derived branch,
and will be re-enabled after all files are moved into the module.

Test: TH
Bug: 197717846

Change-Id: I95278a99cf2437ada28001161ceea17a1d32f2a4
This commit is contained in:
Junyu Lai
2021-12-10 07:46:43 +00:00
parent ad166b43df
commit c8c87d4a01
6 changed files with 47 additions and 20 deletions

View File

@@ -0,0 +1,196 @@
/*
* 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 android.net;
import static android.net.ConnectivityManager.TYPE_MOBILE;
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;
/**
* 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));
}
}
/**
* Method to serialize this object into a {@code DataOutput}.
*/
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);
}
/**
* Method to dump this object into proto debug file.
*/
public void dumpDebug(ProtoOutputStream proto, long tag) {
final long start = proto.start(tag);
for (NetworkIdentity ident : this) {
ident.dumpDebug(proto, NetworkIdentitySetProto.IDENTITIES);
}
proto.end(start);
}
}

View File

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

View File

@@ -0,0 +1,829 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.net;
import static android.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 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.
*
* @hide
*/
public class NetworkStatsCollection implements FileRotator.Reader, FileRotator.Writer {
private static final String TAG = NetworkStatsCollection.class.getSimpleName();
/** File header magic number: "ANET" */
private static final int FILE_MAGIC = 0x414E4554;
/** 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);
}
}
}
/**
* Read legacy network summary statistics file format into the collection,
* See {@code NetworkStatsService#maybeUpgradeLegacyStatsLocked}.
*
* @deprecated
*/
@Deprecated
public void readLegacyNetwork(File file) throws IOException {
final AtomicFile inputFile = new AtomicFile(file);
DataInputStream in = null;
try {
in = new DataInputStream(new BufferedInputStream(inputFile.openRead()));
// verify file magic header intact
final int magic = in.readInt();
if (magic != FILE_MAGIC) {
throw new ProtocolException("unexpected magic: " + magic);
}
final int version = in.readInt();
switch (version) {
case VERSION_NETWORK_INIT: {
// network := size *(NetworkIdentitySet NetworkStatsHistory)
final int size = in.readInt();
for (int i = 0; i < size; i++) {
final NetworkIdentitySet ident = new NetworkIdentitySet(in);
final NetworkStatsHistory history = new NetworkStatsHistory(in);
final Key key = new Key(ident, UID_ALL, SET_ALL, TAG_NONE);
recordHistory(key, history);
}
break;
}
default: {
throw new ProtocolException("unexpected version: " + version);
}
}
} catch (FileNotFoundException e) {
// missing stats is okay, probably first boot
} finally {
IoUtils.closeQuietly(in);
}
}
/**
* Read legacy Uid statistics file format into the collection,
* See {@code NetworkStatsService#maybeUpgradeLegacyStatsLocked}.
*
* @deprecated
*/
@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 mHashCode;
Key(NetworkIdentitySet ident, int uid, int set, int tag) {
this.ident = ident;
this.uid = uid;
this.set = set;
this.tag = tag;
mHashCode = Objects.hash(ident, uid, set, tag);
}
@Override
public int hashCode() {
return mHashCode;
}
@Override
public boolean equals(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;
}
}
}