diff --git a/core/java/android/net/INetworkStatsService.aidl b/core/java/android/net/INetworkStatsService.aidl index 381cfb61de..8e6f272388 100644 --- a/core/java/android/net/INetworkStatsService.aidl +++ b/core/java/android/net/INetworkStatsService.aidl @@ -41,6 +41,16 @@ interface INetworkStatsService { /** Return data layer snapshot of UID network usage. */ NetworkStats getDataLayerSnapshotForUid(int uid); + + /** Get a detailed snapshot of stats since boot for all UIDs. + * + *

Results will not always be limited to stats on requiredIfaces when specified: stats for + * interfaces stacked on the specified interfaces, or for interfaces on which the specified + * interfaces are stacked on, will also be included. + * @param requiredIfaces Interface names to get data for, or {@link NetworkStats#INTERFACES_ALL}. + */ + NetworkStats getDetailedUidStats(in String[] requiredIfaces); + /** Return set of any ifaces associated with mobile networks since boot. */ String[] getMobileIfaces(); diff --git a/core/java/android/net/NetworkStats.java b/core/java/android/net/NetworkStats.java index 16fb858a58..292bf8e196 100644 --- a/core/java/android/net/NetworkStats.java +++ b/core/java/android/net/NetworkStats.java @@ -64,6 +64,9 @@ public class NetworkStats implements Parcelable { /** Debug {@link #set} value when the VPN stats are moved out of a vpn UID. */ public static final int SET_DBG_VPN_OUT = 1002; + /** Include all interfaces when filtering */ + public static final String[] INTERFACES_ALL = null; + /** {@link #tag} value for total data across all tags. */ // TODO: Rename TAG_NONE to TAG_ALL. public static final int TAG_NONE = 0; @@ -366,23 +369,27 @@ public class NetworkStats implements Parcelable { capacity = newLength; } - iface[size] = entry.iface; - uid[size] = entry.uid; - set[size] = entry.set; - tag[size] = entry.tag; - metered[size] = entry.metered; - roaming[size] = entry.roaming; - defaultNetwork[size] = entry.defaultNetwork; - rxBytes[size] = entry.rxBytes; - rxPackets[size] = entry.rxPackets; - txBytes[size] = entry.txBytes; - txPackets[size] = entry.txPackets; - operations[size] = entry.operations; + setValues(size, entry); size++; return this; } + private void setValues(int i, Entry entry) { + iface[i] = entry.iface; + uid[i] = entry.uid; + set[i] = entry.set; + tag[i] = entry.tag; + metered[i] = entry.metered; + roaming[i] = entry.roaming; + defaultNetwork[i] = entry.defaultNetwork; + rxBytes[i] = entry.rxBytes; + rxPackets[i] = entry.rxPackets; + txBytes[i] = entry.txBytes; + txPackets[i] = entry.txPackets; + operations[i] = entry.operations; + } + /** * Return specific stats entry. */ @@ -831,6 +838,39 @@ public class NetworkStats implements Parcelable { return stats; } + /** + * Only keep entries that match all specified filters. + * + *

This mutates the original structure in place. After this method is called, + * size is the number of matching entries, and capacity is the previous capacity. + * @param limitUid UID to filter for, or {@link #UID_ALL}. + * @param limitIfaces Interfaces to filter for, or {@link #INTERFACES_ALL}. + * @param limitTag Tag to filter for, or {@link #TAG_ALL}. + */ + public void filter(int limitUid, String[] limitIfaces, int limitTag) { + if (limitUid == UID_ALL && limitTag == TAG_ALL && limitIfaces == INTERFACES_ALL) { + return; + } + + Entry entry = new Entry(); + int nextOutputEntry = 0; + for (int i = 0; i < size; i++) { + entry = getValues(i, entry); + final boolean matches = + (limitUid == UID_ALL || limitUid == entry.uid) + && (limitTag == TAG_ALL || limitTag == entry.tag) + && (limitIfaces == INTERFACES_ALL + || ArrayUtils.contains(limitIfaces, entry.iface)); + + if (matches) { + setValues(nextOutputEntry, entry); + nextOutputEntry++; + } + } + + size = nextOutputEntry; + } + public void dump(String prefix, PrintWriter pw) { pw.print(prefix); pw.print("NetworkStats: elapsedRealtime="); pw.println(elapsedRealtime); diff --git a/core/java/com/android/internal/net/NetworkStatsFactory.java b/core/java/com/android/internal/net/NetworkStatsFactory.java index 43abadec0d..0cf5375076 100644 --- a/core/java/com/android/internal/net/NetworkStatsFactory.java +++ b/core/java/com/android/internal/net/NetworkStatsFactory.java @@ -22,16 +22,14 @@ 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.NetworkStats; import android.os.StrictMode; import android.os.SystemClock; -import android.util.ArrayMap; -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 com.google.android.collect.Lists; import libcore.io.IoUtils; @@ -41,8 +39,10 @@ import java.io.FileInputStream; import java.io.FileReader; import java.io.IOException; import java.net.ProtocolException; -import java.util.ArrayList; -import java.util.Objects; +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/} @@ -72,18 +72,55 @@ public class NetworkStatsFactory { private boolean mUseBpfStats; - // TODO: to improve testability and avoid global state, do not use a static variable. - @GuardedBy("sStackedIfaces") - private static final ArrayMap sStackedIfaces = new ArrayMap<>(); + // TODO: only do adjustments in NetworkStatsService and remove this. + /** + * (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 static final ConcurrentHashMap sStackedIfaces + = new ConcurrentHashMap<>(); public static void noteStackedIface(String stackedIface, String baseIface) { - synchronized (sStackedIfaces) { - if (baseIface != null) { - sStackedIfaces.put(stackedIface, baseIface); - } else { - sStackedIfaces.remove(stackedIface); + if (stackedIface != null && baseIface != null) { + sStackedIfaces.put(stackedIface, baseIface); + } + } + + /** + * Get a set of interfaces containing specified ifaces and stacked interfaces. + * + *

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 static String[] augmentWithStackedInterfacesLocked(@Nullable String[] requiredIfaces) { + if (requiredIfaces == NetworkStats.INTERFACES_ALL) { + return null; + } + + HashSet 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 entry : sStackedIfaces.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); + } + + @VisibleForTesting + public static void clearStackedIfaces() { + sStackedIfaces.clear(); } public NetworkStatsFactory() { @@ -252,12 +289,9 @@ public class NetworkStatsFactory { NetworkStats lastStats) throws IOException { final NetworkStats stats = readNetworkStatsDetailInternal(limitUid, limitIfaces, limitTag, lastStats); - final ArrayMap stackedIfaces; - synchronized (sStackedIfaces) { - stackedIfaces = new ArrayMap<>(sStackedIfaces); - } // Total 464xlat traffic to subtract from uid 0 on all base interfaces. - final NetworkStats adjustments = new NetworkStats(0, stackedIfaces.size()); + // sStackedIfaces may grow afterwards, but NetworkStats will just be resized automatically. + final NetworkStats adjustments = new NetworkStats(0, sStackedIfaces.size()); NetworkStats.Entry entry = null; // For recycling @@ -271,7 +305,7 @@ public class NetworkStatsFactory { if (entry.iface == null || !entry.iface.startsWith(CLATD_INTERFACE_PREFIX)) { continue; } - final String baseIface = stackedIfaces.get(entry.iface); + final String baseIface = sStackedIfaces.get(entry.iface); if (baseIface == null) { continue; } diff --git a/services/core/java/com/android/server/net/NetworkStatsService.java b/services/core/java/com/android/server/net/NetworkStatsService.java index 93c04fe206..57828ca5dc 100644 --- a/services/core/java/com/android/server/net/NetworkStatsService.java +++ b/services/core/java/com/android/server/net/NetworkStatsService.java @@ -27,6 +27,7 @@ import static android.net.ConnectivityManager.ACTION_TETHER_STATE_CHANGED; import static android.net.ConnectivityManager.isNetworkTypeMobile; import static android.net.NetworkStats.DEFAULT_NETWORK_ALL; import static android.net.NetworkStats.IFACE_ALL; +import static android.net.NetworkStats.INTERFACES_ALL; import static android.net.NetworkStats.METERED_ALL; import static android.net.NetworkStats.ROAMING_ALL; import static android.net.NetworkStats.SET_ALL; @@ -34,6 +35,7 @@ import static android.net.NetworkStats.SET_DEFAULT; import static android.net.NetworkStats.SET_FOREGROUND; import static android.net.NetworkStats.STATS_PER_IFACE; import static android.net.NetworkStats.STATS_PER_UID; +import static android.net.NetworkStats.TAG_ALL; import static android.net.NetworkStats.TAG_NONE; import static android.net.NetworkStats.UID_ALL; import static android.net.NetworkStatsHistory.FIELD_ALL; @@ -127,6 +129,7 @@ import android.util.proto.ProtoOutputStream; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.net.NetworkStatsFactory; import com.android.internal.net.VpnInfo; import com.android.internal.util.ArrayUtils; import com.android.internal.util.DumpUtils; @@ -145,6 +148,7 @@ import java.time.ZoneOffset; import java.util.Arrays; import java.util.HashSet; import java.util.List; +import java.util.Map; /** * Collect and persist detailed network statistics, and provide this data to @@ -739,7 +743,8 @@ public class NetworkStatsService extends INetworkStatsService.Stub { final long token = Binder.clearCallingIdentity(); final NetworkStats networkLayer; try { - networkLayer = mNetworkManager.getNetworkStatsUidDetail(uid); + networkLayer = mNetworkManager.getNetworkStatsUidDetail(uid, + NetworkStats.INTERFACES_ALL); } finally { Binder.restoreCallingIdentity(token); } @@ -760,6 +765,18 @@ public class NetworkStatsService extends INetworkStatsService.Stub { return dataLayer; } + @Override + public NetworkStats getDetailedUidStats(String[] requiredIfaces) { + try { + final String[] ifacesToQuery = + NetworkStatsFactory.augmentWithStackedInterfacesLocked(requiredIfaces); + return getNetworkStatsUidDetail(ifacesToQuery); + } catch (RemoteException e) { + Log.wtf(TAG, "Error compiling UID stats", e); + return new NetworkStats(0L, 0); + } + } + @Override public String[] getMobileIfaces() { return mMobileIfaces; @@ -1119,6 +1136,8 @@ public class NetworkStatsService extends INetworkStatsService.Stub { if (isMobile) { mobileIfaces.add(stackedIface); } + + NetworkStatsFactory.noteStackedIface(stackedIface, baseIface); } } } @@ -1141,7 +1160,7 @@ public class NetworkStatsService extends INetworkStatsService.Stub { private void recordSnapshotLocked(long currentTime) throws RemoteException { // snapshot and record current counters; read UID stats first to // avoid over counting dev stats. - final NetworkStats uidSnapshot = getNetworkStatsUidDetail(); + final NetworkStats uidSnapshot = getNetworkStatsUidDetail(INTERFACES_ALL); final NetworkStats xtSnapshot = getNetworkStatsXt(); final NetworkStats devSnapshot = mNetworkManager.getNetworkStatsSummaryDev(); @@ -1501,12 +1520,19 @@ public class NetworkStatsService extends INetworkStatsService.Stub { * Return snapshot of current UID statistics, including any * {@link TrafficStats#UID_TETHERING}, video calling data usage, and {@link #mUidOperations} * values. + * + * @param ifaces A list of interfaces the stats should be restricted to, or + * {@link NetworkStats#INTERFACES_ALL}. */ - private NetworkStats getNetworkStatsUidDetail() throws RemoteException { - final NetworkStats uidSnapshot = mNetworkManager.getNetworkStatsUidDetail(UID_ALL); + private NetworkStats getNetworkStatsUidDetail(String[] ifaces) + throws RemoteException { + + final NetworkStats uidSnapshot = mNetworkManager.getNetworkStatsUidDetail(UID_ALL, + ifaces); // fold tethering stats and operations into uid snapshot final NetworkStats tetherSnapshot = getNetworkStatsTethering(STATS_PER_UID); + tetherSnapshot.filter(UID_ALL, ifaces, TAG_ALL); uidSnapshot.combineAllValues(tetherSnapshot); final TelephonyManager telephonyManager = (TelephonyManager) mContext.getSystemService( @@ -1515,10 +1541,14 @@ public class NetworkStatsService extends INetworkStatsService.Stub { // fold video calling data usage stats into uid snapshot final NetworkStats vtStats = telephonyManager.getVtDataUsage(STATS_PER_UID); if (vtStats != null) { + vtStats.filter(UID_ALL, ifaces, TAG_ALL); uidSnapshot.combineAllValues(vtStats); } + uidSnapshot.combineAllValues(mUidOperations); + // TODO: apply tethering & VC 464xlat adjustments here + return uidSnapshot; }