| /* |
| * 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.os.Process.CLAT_UID; |
| |
| import android.annotation.NonNull; |
| import android.compat.annotation.UnsupportedAppUsage; |
| import android.os.Parcel; |
| import android.os.Parcelable; |
| import android.os.SystemClock; |
| import android.util.SparseBooleanArray; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.util.ArrayUtils; |
| |
| import libcore.util.EmptyArray; |
| |
| import java.io.CharArrayWriter; |
| import java.io.PrintWriter; |
| import java.util.Arrays; |
| import java.util.HashSet; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.function.Predicate; |
| |
| /** |
| * Collection of active network statistics. Can contain summary details across |
| * all interfaces, or details with per-UID granularity. Internally stores data |
| * as a large table, closely matching {@code /proc/} data format. This structure |
| * optimizes for rapid in-memory comparison, but consider using |
| * {@link NetworkStatsHistory} when persisting. |
| * |
| * @hide |
| */ |
| // @NotThreadSafe |
| public class NetworkStats implements Parcelable { |
| private static final String TAG = "NetworkStats"; |
| /** {@link #iface} value when interface details unavailable. */ |
| public static final String IFACE_ALL = null; |
| /** {@link #uid} value when UID details unavailable. */ |
| public static final int UID_ALL = -1; |
| /** {@link #tag} value matching any tag. */ |
| // TODO: Rename TAG_ALL to TAG_ANY. |
| public static final int TAG_ALL = -1; |
| /** {@link #set} value for all sets combined, not including debug sets. */ |
| public static final int SET_ALL = -1; |
| /** {@link #set} value where background data is accounted. */ |
| public static final int SET_DEFAULT = 0; |
| /** {@link #set} value where foreground data is accounted. */ |
| public static final int SET_FOREGROUND = 1; |
| /** All {@link #set} value greater than SET_DEBUG_START are debug {@link #set} values. */ |
| public static final int SET_DEBUG_START = 1000; |
| /** Debug {@link #set} value when the VPN stats are moved in. */ |
| public static final int SET_DBG_VPN_IN = 1001; |
| /** 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; |
| |
| /** {@link #metered} value to account for all metered states. */ |
| public static final int METERED_ALL = -1; |
| /** {@link #metered} value where native, unmetered data is accounted. */ |
| public static final int METERED_NO = 0; |
| /** {@link #metered} value where metered data is accounted. */ |
| public static final int METERED_YES = 1; |
| |
| /** {@link #roaming} value to account for all roaming states. */ |
| public static final int ROAMING_ALL = -1; |
| /** {@link #roaming} value where native, non-roaming data is accounted. */ |
| public static final int ROAMING_NO = 0; |
| /** {@link #roaming} value where roaming data is accounted. */ |
| public static final int ROAMING_YES = 1; |
| |
| /** {@link #onDefaultNetwork} value to account for all default network states. */ |
| public static final int DEFAULT_NETWORK_ALL = -1; |
| /** {@link #onDefaultNetwork} value to account for usage while not the default network. */ |
| public static final int DEFAULT_NETWORK_NO = 0; |
| /** {@link #onDefaultNetwork} value to account for usage while the default network. */ |
| public static final int DEFAULT_NETWORK_YES = 1; |
| |
| /** Denotes a request for stats at the interface level. */ |
| public static final int STATS_PER_IFACE = 0; |
| /** Denotes a request for stats at the interface and UID level. */ |
| public static final int STATS_PER_UID = 1; |
| |
| private static final String CLATD_INTERFACE_PREFIX = "v4-"; |
| // Delta between IPv4 header (20b) and IPv6 header (40b). |
| // Used for correct stats accounting on clatd interfaces. |
| private static final int IPV4V6_HEADER_DELTA = 20; |
| |
| // TODO: move fields to "mVariable" notation |
| |
| /** |
| * {@link SystemClock#elapsedRealtime()} timestamp when this data was |
| * generated. |
| */ |
| private long elapsedRealtime; |
| @UnsupportedAppUsage |
| private int size; |
| @UnsupportedAppUsage |
| private int capacity; |
| @UnsupportedAppUsage |
| private String[] iface; |
| @UnsupportedAppUsage |
| private int[] uid; |
| @UnsupportedAppUsage |
| private int[] set; |
| @UnsupportedAppUsage |
| private int[] tag; |
| @UnsupportedAppUsage |
| private int[] metered; |
| @UnsupportedAppUsage |
| private int[] roaming; |
| @UnsupportedAppUsage |
| private int[] defaultNetwork; |
| @UnsupportedAppUsage |
| private long[] rxBytes; |
| @UnsupportedAppUsage |
| private long[] rxPackets; |
| @UnsupportedAppUsage |
| private long[] txBytes; |
| @UnsupportedAppUsage |
| private long[] txPackets; |
| @UnsupportedAppUsage |
| private long[] operations; |
| |
| public static class Entry { |
| @UnsupportedAppUsage |
| public String iface; |
| @UnsupportedAppUsage |
| public int uid; |
| @UnsupportedAppUsage |
| public int set; |
| @UnsupportedAppUsage |
| public int tag; |
| /** |
| * Note that this is only populated w/ the default value when read from /proc or written |
| * to disk. We merge in the correct value when reporting this value to clients of |
| * getSummary(). |
| */ |
| public int metered; |
| /** |
| * Note that this is only populated w/ the default value when read from /proc or written |
| * to disk. We merge in the correct value when reporting this value to clients of |
| * getSummary(). |
| */ |
| public int roaming; |
| /** |
| * Note that this is only populated w/ the default value when read from /proc or written |
| * to disk. We merge in the correct value when reporting this value to clients of |
| * getSummary(). |
| */ |
| public int defaultNetwork; |
| @UnsupportedAppUsage |
| public long rxBytes; |
| @UnsupportedAppUsage |
| public long rxPackets; |
| @UnsupportedAppUsage |
| public long txBytes; |
| @UnsupportedAppUsage |
| public long txPackets; |
| public long operations; |
| |
| @UnsupportedAppUsage |
| public Entry() { |
| this(IFACE_ALL, UID_ALL, SET_DEFAULT, TAG_NONE, 0L, 0L, 0L, 0L, 0L); |
| } |
| |
| public Entry(long rxBytes, long rxPackets, long txBytes, long txPackets, long operations) { |
| this(IFACE_ALL, UID_ALL, SET_DEFAULT, TAG_NONE, rxBytes, rxPackets, txBytes, txPackets, |
| operations); |
| } |
| |
| public Entry(String iface, int uid, int set, int tag, long rxBytes, long rxPackets, |
| long txBytes, long txPackets, long operations) { |
| this(iface, uid, set, tag, METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, |
| rxBytes, rxPackets, txBytes, txPackets, operations); |
| } |
| |
| public Entry(String iface, int uid, int set, int tag, int metered, int roaming, |
| int defaultNetwork, long rxBytes, long rxPackets, long txBytes, long txPackets, |
| long operations) { |
| this.iface = iface; |
| this.uid = uid; |
| this.set = set; |
| this.tag = tag; |
| this.metered = metered; |
| this.roaming = roaming; |
| this.defaultNetwork = defaultNetwork; |
| this.rxBytes = rxBytes; |
| this.rxPackets = rxPackets; |
| this.txBytes = txBytes; |
| this.txPackets = txPackets; |
| this.operations = operations; |
| } |
| |
| public boolean isNegative() { |
| return rxBytes < 0 || rxPackets < 0 || txBytes < 0 || txPackets < 0 || operations < 0; |
| } |
| |
| public boolean isEmpty() { |
| return rxBytes == 0 && rxPackets == 0 && txBytes == 0 && txPackets == 0 |
| && operations == 0; |
| } |
| |
| public void add(Entry another) { |
| this.rxBytes += another.rxBytes; |
| this.rxPackets += another.rxPackets; |
| this.txBytes += another.txBytes; |
| this.txPackets += another.txPackets; |
| this.operations += another.operations; |
| } |
| |
| @Override |
| public String toString() { |
| final StringBuilder builder = new StringBuilder(); |
| builder.append("iface=").append(iface); |
| builder.append(" uid=").append(uid); |
| builder.append(" set=").append(setToString(set)); |
| builder.append(" tag=").append(tagToString(tag)); |
| builder.append(" metered=").append(meteredToString(metered)); |
| builder.append(" roaming=").append(roamingToString(roaming)); |
| builder.append(" defaultNetwork=").append(defaultNetworkToString(defaultNetwork)); |
| builder.append(" rxBytes=").append(rxBytes); |
| builder.append(" rxPackets=").append(rxPackets); |
| builder.append(" txBytes=").append(txBytes); |
| builder.append(" txPackets=").append(txPackets); |
| builder.append(" operations=").append(operations); |
| return builder.toString(); |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (o instanceof Entry) { |
| final Entry e = (Entry) o; |
| return uid == e.uid && set == e.set && tag == e.tag && metered == e.metered |
| && roaming == e.roaming && defaultNetwork == e.defaultNetwork |
| && rxBytes == e.rxBytes && rxPackets == e.rxPackets |
| && txBytes == e.txBytes && txPackets == e.txPackets |
| && operations == e.operations && iface.equals(e.iface); |
| } |
| return false; |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hash(uid, set, tag, metered, roaming, defaultNetwork, iface); |
| } |
| } |
| |
| @UnsupportedAppUsage |
| public NetworkStats(long elapsedRealtime, int initialSize) { |
| this.elapsedRealtime = elapsedRealtime; |
| this.size = 0; |
| if (initialSize > 0) { |
| this.capacity = initialSize; |
| this.iface = new String[initialSize]; |
| this.uid = new int[initialSize]; |
| this.set = new int[initialSize]; |
| this.tag = new int[initialSize]; |
| this.metered = new int[initialSize]; |
| this.roaming = new int[initialSize]; |
| this.defaultNetwork = new int[initialSize]; |
| this.rxBytes = new long[initialSize]; |
| this.rxPackets = new long[initialSize]; |
| this.txBytes = new long[initialSize]; |
| this.txPackets = new long[initialSize]; |
| this.operations = new long[initialSize]; |
| } else { |
| // Special case for use by NetworkStatsFactory to start out *really* empty. |
| clear(); |
| } |
| } |
| |
| @UnsupportedAppUsage |
| public NetworkStats(Parcel parcel) { |
| elapsedRealtime = parcel.readLong(); |
| size = parcel.readInt(); |
| capacity = parcel.readInt(); |
| iface = parcel.createStringArray(); |
| uid = parcel.createIntArray(); |
| set = parcel.createIntArray(); |
| tag = parcel.createIntArray(); |
| metered = parcel.createIntArray(); |
| roaming = parcel.createIntArray(); |
| defaultNetwork = parcel.createIntArray(); |
| rxBytes = parcel.createLongArray(); |
| rxPackets = parcel.createLongArray(); |
| txBytes = parcel.createLongArray(); |
| txPackets = parcel.createLongArray(); |
| operations = parcel.createLongArray(); |
| } |
| |
| @Override |
| public void writeToParcel(Parcel dest, int flags) { |
| dest.writeLong(elapsedRealtime); |
| dest.writeInt(size); |
| dest.writeInt(capacity); |
| dest.writeStringArray(iface); |
| dest.writeIntArray(uid); |
| dest.writeIntArray(set); |
| dest.writeIntArray(tag); |
| dest.writeIntArray(metered); |
| dest.writeIntArray(roaming); |
| dest.writeIntArray(defaultNetwork); |
| dest.writeLongArray(rxBytes); |
| dest.writeLongArray(rxPackets); |
| dest.writeLongArray(txBytes); |
| dest.writeLongArray(txPackets); |
| dest.writeLongArray(operations); |
| } |
| |
| @Override |
| public NetworkStats clone() { |
| final NetworkStats clone = new NetworkStats(elapsedRealtime, size); |
| NetworkStats.Entry entry = null; |
| for (int i = 0; i < size; i++) { |
| entry = getValues(i, entry); |
| clone.addValues(entry); |
| } |
| return clone; |
| } |
| |
| /** |
| * Clear all data stored in this object. |
| */ |
| public void clear() { |
| this.capacity = 0; |
| this.iface = EmptyArray.STRING; |
| this.uid = EmptyArray.INT; |
| this.set = EmptyArray.INT; |
| this.tag = EmptyArray.INT; |
| this.metered = EmptyArray.INT; |
| this.roaming = EmptyArray.INT; |
| this.defaultNetwork = EmptyArray.INT; |
| this.rxBytes = EmptyArray.LONG; |
| this.rxPackets = EmptyArray.LONG; |
| this.txBytes = EmptyArray.LONG; |
| this.txPackets = EmptyArray.LONG; |
| this.operations = EmptyArray.LONG; |
| } |
| |
| @VisibleForTesting |
| public NetworkStats addIfaceValues( |
| String iface, long rxBytes, long rxPackets, long txBytes, long txPackets) { |
| return addValues( |
| iface, UID_ALL, SET_DEFAULT, TAG_NONE, rxBytes, rxPackets, txBytes, txPackets, 0L); |
| } |
| |
| @VisibleForTesting |
| public NetworkStats addValues(String iface, int uid, int set, int tag, long rxBytes, |
| long rxPackets, long txBytes, long txPackets, long operations) { |
| return addValues(new Entry( |
| iface, uid, set, tag, rxBytes, rxPackets, txBytes, txPackets, operations)); |
| } |
| |
| @VisibleForTesting |
| public NetworkStats addValues(String iface, int uid, int set, int tag, int metered, int roaming, |
| int defaultNetwork, long rxBytes, long rxPackets, long txBytes, long txPackets, |
| long operations) { |
| return addValues(new Entry( |
| iface, uid, set, tag, metered, roaming, defaultNetwork, rxBytes, rxPackets, |
| txBytes, txPackets, operations)); |
| } |
| |
| /** |
| * Add new stats entry, copying from given {@link Entry}. The {@link Entry} |
| * object can be recycled across multiple calls. |
| */ |
| public NetworkStats addValues(Entry entry) { |
| if (size >= capacity) { |
| final int newLength = Math.max(size, 10) * 3 / 2; |
| iface = Arrays.copyOf(iface, newLength); |
| uid = Arrays.copyOf(uid, newLength); |
| set = Arrays.copyOf(set, newLength); |
| tag = Arrays.copyOf(tag, newLength); |
| metered = Arrays.copyOf(metered, newLength); |
| roaming = Arrays.copyOf(roaming, newLength); |
| defaultNetwork = Arrays.copyOf(defaultNetwork, newLength); |
| rxBytes = Arrays.copyOf(rxBytes, newLength); |
| rxPackets = Arrays.copyOf(rxPackets, newLength); |
| txBytes = Arrays.copyOf(txBytes, newLength); |
| txPackets = Arrays.copyOf(txPackets, newLength); |
| operations = Arrays.copyOf(operations, newLength); |
| capacity = newLength; |
| } |
| |
| 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. |
| */ |
| @UnsupportedAppUsage |
| public Entry getValues(int i, Entry recycle) { |
| final Entry entry = recycle != null ? recycle : new Entry(); |
| entry.iface = iface[i]; |
| entry.uid = uid[i]; |
| entry.set = set[i]; |
| entry.tag = tag[i]; |
| entry.metered = metered[i]; |
| entry.roaming = roaming[i]; |
| entry.defaultNetwork = defaultNetwork[i]; |
| entry.rxBytes = rxBytes[i]; |
| entry.rxPackets = rxPackets[i]; |
| entry.txBytes = txBytes[i]; |
| entry.txPackets = txPackets[i]; |
| entry.operations = operations[i]; |
| return entry; |
| } |
| |
| /** |
| * If @{code dest} is not equal to @{code src}, copy entry from index @{code src} to index |
| * @{code dest}. |
| */ |
| private void maybeCopyEntry(int dest, int src) { |
| if (dest == src) return; |
| iface[dest] = iface[src]; |
| uid[dest] = uid[src]; |
| set[dest] = set[src]; |
| tag[dest] = tag[src]; |
| metered[dest] = metered[src]; |
| roaming[dest] = roaming[src]; |
| defaultNetwork[dest] = defaultNetwork[src]; |
| rxBytes[dest] = rxBytes[src]; |
| rxPackets[dest] = rxPackets[src]; |
| txBytes[dest] = txBytes[src]; |
| txPackets[dest] = txPackets[src]; |
| operations[dest] = operations[src]; |
| } |
| |
| public long getElapsedRealtime() { |
| return elapsedRealtime; |
| } |
| |
| public void setElapsedRealtime(long time) { |
| elapsedRealtime = time; |
| } |
| |
| /** |
| * Return age of this {@link NetworkStats} object with respect to |
| * {@link SystemClock#elapsedRealtime()}. |
| */ |
| public long getElapsedRealtimeAge() { |
| return SystemClock.elapsedRealtime() - elapsedRealtime; |
| } |
| |
| @UnsupportedAppUsage |
| public int size() { |
| return size; |
| } |
| |
| @VisibleForTesting |
| public int internalSize() { |
| return capacity; |
| } |
| |
| @Deprecated |
| public NetworkStats combineValues(String iface, int uid, int tag, long rxBytes, long rxPackets, |
| long txBytes, long txPackets, long operations) { |
| return combineValues( |
| iface, uid, SET_DEFAULT, tag, rxBytes, rxPackets, txBytes, |
| txPackets, operations); |
| } |
| |
| public NetworkStats combineValues(String iface, int uid, int set, int tag, |
| long rxBytes, long rxPackets, long txBytes, long txPackets, long operations) { |
| return combineValues(new Entry( |
| iface, uid, set, tag, rxBytes, rxPackets, txBytes, txPackets, operations)); |
| } |
| |
| /** |
| * Combine given values with an existing row, or create a new row if |
| * {@link #findIndex(String, int, int, int, int)} is unable to find match. Can |
| * also be used to subtract values from existing rows. |
| */ |
| @UnsupportedAppUsage |
| public NetworkStats combineValues(Entry entry) { |
| final int i = findIndex(entry.iface, entry.uid, entry.set, entry.tag, entry.metered, |
| entry.roaming, entry.defaultNetwork); |
| if (i == -1) { |
| // only create new entry when positive contribution |
| addValues(entry); |
| } else { |
| rxBytes[i] += entry.rxBytes; |
| rxPackets[i] += entry.rxPackets; |
| txBytes[i] += entry.txBytes; |
| txPackets[i] += entry.txPackets; |
| operations[i] += entry.operations; |
| } |
| return this; |
| } |
| |
| /** |
| * Combine all values from another {@link NetworkStats} into this object. |
| */ |
| @UnsupportedAppUsage |
| public void combineAllValues(NetworkStats another) { |
| NetworkStats.Entry entry = null; |
| for (int i = 0; i < another.size; i++) { |
| entry = another.getValues(i, entry); |
| combineValues(entry); |
| } |
| } |
| |
| /** |
| * Find first stats index that matches the requested parameters. |
| */ |
| public int findIndex(String iface, int uid, int set, int tag, int metered, int roaming, |
| int defaultNetwork) { |
| for (int i = 0; i < size; i++) { |
| if (uid == this.uid[i] && set == this.set[i] && tag == this.tag[i] |
| && metered == this.metered[i] && roaming == this.roaming[i] |
| && defaultNetwork == this.defaultNetwork[i] |
| && Objects.equals(iface, this.iface[i])) { |
| return i; |
| } |
| } |
| return -1; |
| } |
| |
| /** |
| * Find first stats index that matches the requested parameters, starting |
| * search around the hinted index as an optimization. |
| */ |
| @VisibleForTesting |
| public int findIndexHinted(String iface, int uid, int set, int tag, int metered, int roaming, |
| int defaultNetwork, int hintIndex) { |
| for (int offset = 0; offset < size; offset++) { |
| final int halfOffset = offset / 2; |
| |
| // search outwards from hint index, alternating forward and backward |
| final int i; |
| if (offset % 2 == 0) { |
| i = (hintIndex + halfOffset) % size; |
| } else { |
| i = (size + hintIndex - halfOffset - 1) % size; |
| } |
| |
| if (uid == this.uid[i] && set == this.set[i] && tag == this.tag[i] |
| && metered == this.metered[i] && roaming == this.roaming[i] |
| && defaultNetwork == this.defaultNetwork[i] |
| && Objects.equals(iface, this.iface[i])) { |
| return i; |
| } |
| } |
| return -1; |
| } |
| |
| /** |
| * Splice in {@link #operations} from the given {@link NetworkStats} based |
| * on matching {@link #uid} and {@link #tag} rows. Ignores {@link #iface}, |
| * since operation counts are at data layer. |
| */ |
| public void spliceOperationsFrom(NetworkStats stats) { |
| for (int i = 0; i < size; i++) { |
| final int j = stats.findIndex(iface[i], uid[i], set[i], tag[i], metered[i], roaming[i], |
| defaultNetwork[i]); |
| if (j == -1) { |
| operations[i] = 0; |
| } else { |
| operations[i] = stats.operations[j]; |
| } |
| } |
| } |
| |
| /** |
| * Return list of unique interfaces known by this data structure. |
| */ |
| public String[] getUniqueIfaces() { |
| final HashSet<String> ifaces = new HashSet<String>(); |
| for (String iface : this.iface) { |
| if (iface != IFACE_ALL) { |
| ifaces.add(iface); |
| } |
| } |
| return ifaces.toArray(new String[ifaces.size()]); |
| } |
| |
| /** |
| * Return list of unique UIDs known by this data structure. |
| */ |
| @UnsupportedAppUsage |
| public int[] getUniqueUids() { |
| final SparseBooleanArray uids = new SparseBooleanArray(); |
| for (int uid : this.uid) { |
| uids.put(uid, true); |
| } |
| |
| final int size = uids.size(); |
| final int[] result = new int[size]; |
| for (int i = 0; i < size; i++) { |
| result[i] = uids.keyAt(i); |
| } |
| return result; |
| } |
| |
| /** |
| * Return total bytes represented by this snapshot object, usually used when |
| * checking if a {@link #subtract(NetworkStats)} delta passes a threshold. |
| */ |
| @UnsupportedAppUsage |
| public long getTotalBytes() { |
| final Entry entry = getTotal(null); |
| return entry.rxBytes + entry.txBytes; |
| } |
| |
| /** |
| * Return total of all fields represented by this snapshot object. |
| */ |
| @UnsupportedAppUsage |
| public Entry getTotal(Entry recycle) { |
| return getTotal(recycle, null, UID_ALL, false); |
| } |
| |
| /** |
| * Return total of all fields represented by this snapshot object matching |
| * the requested {@link #uid}. |
| */ |
| @UnsupportedAppUsage |
| public Entry getTotal(Entry recycle, int limitUid) { |
| return getTotal(recycle, null, limitUid, false); |
| } |
| |
| /** |
| * Return total of all fields represented by this snapshot object matching |
| * the requested {@link #iface}. |
| */ |
| public Entry getTotal(Entry recycle, HashSet<String> limitIface) { |
| return getTotal(recycle, limitIface, UID_ALL, false); |
| } |
| |
| @UnsupportedAppUsage |
| public Entry getTotalIncludingTags(Entry recycle) { |
| return getTotal(recycle, null, UID_ALL, true); |
| } |
| |
| /** |
| * Return total of all fields represented by this snapshot object matching |
| * the requested {@link #iface} and {@link #uid}. |
| * |
| * @param limitIface Set of {@link #iface} to include in total; or {@code |
| * null} to include all ifaces. |
| */ |
| private Entry getTotal( |
| Entry recycle, HashSet<String> limitIface, int limitUid, boolean includeTags) { |
| final Entry entry = recycle != null ? recycle : new Entry(); |
| |
| entry.iface = IFACE_ALL; |
| entry.uid = limitUid; |
| entry.set = SET_ALL; |
| entry.tag = TAG_NONE; |
| entry.metered = METERED_ALL; |
| entry.roaming = ROAMING_ALL; |
| entry.defaultNetwork = DEFAULT_NETWORK_ALL; |
| entry.rxBytes = 0; |
| entry.rxPackets = 0; |
| entry.txBytes = 0; |
| entry.txPackets = 0; |
| entry.operations = 0; |
| |
| for (int i = 0; i < size; i++) { |
| final boolean matchesUid = (limitUid == UID_ALL) || (limitUid == uid[i]); |
| final boolean matchesIface = (limitIface == null) || (limitIface.contains(iface[i])); |
| |
| if (matchesUid && matchesIface) { |
| // skip specific tags, since already counted in TAG_NONE |
| if (tag[i] != TAG_NONE && !includeTags) continue; |
| |
| entry.rxBytes += rxBytes[i]; |
| entry.rxPackets += rxPackets[i]; |
| entry.txBytes += txBytes[i]; |
| entry.txPackets += txPackets[i]; |
| entry.operations += operations[i]; |
| } |
| } |
| return entry; |
| } |
| |
| /** |
| * Fast path for battery stats. |
| */ |
| public long getTotalPackets() { |
| long total = 0; |
| for (int i = size-1; i >= 0; i--) { |
| total += rxPackets[i] + txPackets[i]; |
| } |
| return total; |
| } |
| |
| /** |
| * Subtract the given {@link NetworkStats}, effectively leaving the delta |
| * between two snapshots in time. Assumes that statistics rows collect over |
| * time, and that none of them have disappeared. |
| */ |
| public NetworkStats subtract(NetworkStats right) { |
| return subtract(this, right, null, null); |
| } |
| |
| /** |
| * Subtract the two given {@link NetworkStats} objects, returning the delta |
| * between two snapshots in time. Assumes that statistics rows collect over |
| * time, and that none of them have disappeared. |
| * <p> |
| * If counters have rolled backwards, they are clamped to {@code 0} and |
| * reported to the given {@link NonMonotonicObserver}. |
| */ |
| public static <C> NetworkStats subtract(NetworkStats left, NetworkStats right, |
| NonMonotonicObserver<C> observer, C cookie) { |
| return subtract(left, right, observer, cookie, null); |
| } |
| |
| /** |
| * Subtract the two given {@link NetworkStats} objects, returning the delta |
| * between two snapshots in time. Assumes that statistics rows collect over |
| * time, and that none of them have disappeared. |
| * <p> |
| * If counters have rolled backwards, they are clamped to {@code 0} and |
| * reported to the given {@link NonMonotonicObserver}. |
| * <p> |
| * If <var>recycle</var> is supplied, this NetworkStats object will be |
| * reused (and returned) as the result if it is large enough to contain |
| * the data. |
| */ |
| public static <C> NetworkStats subtract(NetworkStats left, NetworkStats right, |
| NonMonotonicObserver<C> observer, C cookie, NetworkStats recycle) { |
| long deltaRealtime = left.elapsedRealtime - right.elapsedRealtime; |
| if (deltaRealtime < 0) { |
| if (observer != null) { |
| observer.foundNonMonotonic(left, -1, right, -1, cookie); |
| } |
| deltaRealtime = 0; |
| } |
| |
| // result will have our rows, and elapsed time between snapshots |
| final Entry entry = new Entry(); |
| final NetworkStats result; |
| if (recycle != null && recycle.capacity >= left.size) { |
| result = recycle; |
| result.size = 0; |
| result.elapsedRealtime = deltaRealtime; |
| } else { |
| result = new NetworkStats(deltaRealtime, left.size); |
| } |
| for (int i = 0; i < left.size; i++) { |
| entry.iface = left.iface[i]; |
| entry.uid = left.uid[i]; |
| entry.set = left.set[i]; |
| entry.tag = left.tag[i]; |
| entry.metered = left.metered[i]; |
| entry.roaming = left.roaming[i]; |
| entry.defaultNetwork = left.defaultNetwork[i]; |
| entry.rxBytes = left.rxBytes[i]; |
| entry.rxPackets = left.rxPackets[i]; |
| entry.txBytes = left.txBytes[i]; |
| entry.txPackets = left.txPackets[i]; |
| entry.operations = left.operations[i]; |
| |
| // find remote row that matches, and subtract |
| final int j = right.findIndexHinted(entry.iface, entry.uid, entry.set, entry.tag, |
| entry.metered, entry.roaming, entry.defaultNetwork, i); |
| if (j != -1) { |
| // Found matching row, subtract remote value. |
| entry.rxBytes -= right.rxBytes[j]; |
| entry.rxPackets -= right.rxPackets[j]; |
| entry.txBytes -= right.txBytes[j]; |
| entry.txPackets -= right.txPackets[j]; |
| entry.operations -= right.operations[j]; |
| } |
| |
| if (entry.isNegative()) { |
| if (observer != null) { |
| observer.foundNonMonotonic(left, i, right, j, cookie); |
| } |
| 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); |
| } |
| |
| result.addValues(entry); |
| } |
| |
| return result; |
| } |
| |
| /** |
| * Calculate and apply adjustments to captured statistics for 464xlat traffic counted twice. |
| * |
| * <p>This mutates both base and stacked traffic stats, to account respectively for |
| * double-counted traffic and IPv4/IPv6 header size difference. |
| * |
| * <p>For 464xlat traffic, xt_qtaguid sees every IPv4 packet twice, once as a native IPv4 |
| * packet on the stacked interface, and once as translated to an IPv6 packet on the |
| * base interface. For correct stats accounting on the base interface, if using xt_qtaguid, |
| * every rx 464xlat packet needs to be subtracted from the root UID on the base interface |
| * (http://b/12249687, http:/b/33681750), and every tx 464xlat packet which was counted onto |
| * clat uid should be ignored. |
| * |
| * As for eBPF, the per uid stats is collected by different hook, the rx packets on base |
| * interface will not be counted. Thus, the adjustment on root uid is not needed. However, the |
| * tx traffic counted in the same way xt_qtaguid does, so the traffic on clat uid still |
| * needs to be ignored. |
| * |
| * <p>This method will behave fine if {@code stackedIfaces} is an non-synchronized but add-only |
| * {@code ConcurrentHashMap} |
| * @param baseTraffic Traffic on the base interfaces. Will be mutated. |
| * @param stackedTraffic Stats with traffic stacked on top of our ifaces. Will also be mutated. |
| * @param stackedIfaces Mapping ipv6if -> ipv4if interface where traffic is counted on both. |
| * @param useBpfStats True if eBPF is in use. |
| */ |
| public static void apply464xlatAdjustments(NetworkStats baseTraffic, |
| NetworkStats stackedTraffic, Map<String, String> stackedIfaces, boolean useBpfStats) { |
| // Total 464xlat traffic to subtract from uid 0 on all base interfaces. |
| // stackedIfaces may grow afterwards, but NetworkStats will just be resized automatically. |
| final NetworkStats adjustments = new NetworkStats(0, stackedIfaces.size()); |
| |
| // For recycling |
| Entry entry = null; |
| Entry adjust = new NetworkStats.Entry(IFACE_ALL, 0, 0, 0, 0, 0, 0, 0L, 0L, 0L, 0L, 0L); |
| |
| for (int i = 0; i < stackedTraffic.size; i++) { |
| entry = stackedTraffic.getValues(i, entry); |
| if (entry.iface == null || !entry.iface.startsWith(CLATD_INTERFACE_PREFIX)) { |
| continue; |
| } |
| final String baseIface = stackedIfaces.get(entry.iface); |
| if (baseIface == null) { |
| continue; |
| } |
| // Subtract xt_qtaguid 464lat rx traffic seen for the root UID on the current base |
| // interface. As for eBPF, the per uid stats is collected by different hook, the rx |
| // packets on base interface will not be counted. |
| adjust.iface = baseIface; |
| if (!useBpfStats) { |
| adjust.rxBytes = -(entry.rxBytes + entry.rxPackets * IPV4V6_HEADER_DELTA); |
| adjust.rxPackets = -entry.rxPackets; |
| } |
| adjustments.combineValues(adjust); |
| |
| // For 464xlat traffic, per uid stats only counts the bytes of the native IPv4 packet |
| // sent on the stacked interface with prefix "v4-" and drops the IPv6 header size after |
| // unwrapping. To account correctly for on-the-wire traffic, add the 20 additional bytes |
| // difference for all packets (http://b/12249687, http:/b/33681750). |
| entry.rxBytes += entry.rxPackets * IPV4V6_HEADER_DELTA; |
| entry.txBytes += entry.txPackets * IPV4V6_HEADER_DELTA; |
| stackedTraffic.setValues(i, entry); |
| } |
| |
| // Traffic on clat uid is v6 tx traffic that is already counted with app uid on the stacked |
| // v4 interface, so it needs to be removed to avoid double-counting. |
| baseTraffic.removeUids(new int[] {CLAT_UID}); |
| baseTraffic.combineAllValues(adjustments); |
| } |
| |
| /** |
| * Calculate and apply adjustments to captured statistics for 464xlat traffic counted twice. |
| * |
| * <p>This mutates the object this method is called on. Equivalent to calling |
| * {@link #apply464xlatAdjustments(NetworkStats, NetworkStats, Map)} with {@code this} as |
| * base and stacked traffic. |
| * @param stackedIfaces Mapping ipv6if -> ipv4if interface where traffic is counted on both. |
| */ |
| public void apply464xlatAdjustments(Map<String, String> stackedIfaces, boolean useBpfStats) { |
| apply464xlatAdjustments(this, this, stackedIfaces, useBpfStats); |
| } |
| |
| /** |
| * Return total statistics grouped by {@link #iface}; doesn't mutate the |
| * original structure. |
| */ |
| public NetworkStats groupedByIface() { |
| final NetworkStats stats = new NetworkStats(elapsedRealtime, 10); |
| |
| final Entry entry = new Entry(); |
| entry.uid = UID_ALL; |
| entry.set = SET_ALL; |
| entry.tag = TAG_NONE; |
| entry.metered = METERED_ALL; |
| entry.roaming = ROAMING_ALL; |
| entry.defaultNetwork = DEFAULT_NETWORK_ALL; |
| entry.operations = 0L; |
| |
| for (int i = 0; i < size; i++) { |
| // skip specific tags, since already counted in TAG_NONE |
| if (tag[i] != TAG_NONE) continue; |
| |
| entry.iface = iface[i]; |
| entry.rxBytes = rxBytes[i]; |
| entry.rxPackets = rxPackets[i]; |
| entry.txBytes = txBytes[i]; |
| entry.txPackets = txPackets[i]; |
| stats.combineValues(entry); |
| } |
| |
| return stats; |
| } |
| |
| /** |
| * Return total statistics grouped by {@link #uid}; doesn't mutate the |
| * original structure. |
| */ |
| public NetworkStats groupedByUid() { |
| final NetworkStats stats = new NetworkStats(elapsedRealtime, 10); |
| |
| final Entry entry = new Entry(); |
| entry.iface = IFACE_ALL; |
| entry.set = SET_ALL; |
| entry.tag = TAG_NONE; |
| entry.metered = METERED_ALL; |
| entry.roaming = ROAMING_ALL; |
| entry.defaultNetwork = DEFAULT_NETWORK_ALL; |
| |
| for (int i = 0; i < size; i++) { |
| // skip specific tags, since already counted in TAG_NONE |
| if (tag[i] != TAG_NONE) continue; |
| |
| entry.uid = uid[i]; |
| entry.rxBytes = rxBytes[i]; |
| entry.rxPackets = rxPackets[i]; |
| entry.txBytes = txBytes[i]; |
| entry.txPackets = txPackets[i]; |
| entry.operations = operations[i]; |
| stats.combineValues(entry); |
| } |
| |
| return stats; |
| } |
| |
| /** |
| * Remove all rows that match one of specified UIDs. |
| */ |
| public void removeUids(int[] uids) { |
| int nextOutputEntry = 0; |
| for (int i = 0; i < size; i++) { |
| if (!ArrayUtils.contains(uids, uid[i])) { |
| maybeCopyEntry(nextOutputEntry, i); |
| nextOutputEntry++; |
| } |
| } |
| |
| size = nextOutputEntry; |
| } |
| |
| /** |
| * Only keep entries that match all specified filters. |
| * |
| * <p>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; |
| } |
| filter(e -> (limitUid == UID_ALL || limitUid == e.uid) |
| && (limitTag == TAG_ALL || limitTag == e.tag) |
| && (limitIfaces == INTERFACES_ALL |
| || ArrayUtils.contains(limitIfaces, e.iface))); |
| } |
| |
| /** |
| * Only keep entries with {@link #set} value less than {@link #SET_DEBUG_START}. |
| * |
| * <p>This mutates the original structure in place. |
| */ |
| public void filterDebugEntries() { |
| filter(e -> e.set < SET_DEBUG_START); |
| } |
| |
| private void filter(Predicate<Entry> predicate) { |
| Entry entry = new Entry(); |
| int nextOutputEntry = 0; |
| for (int i = 0; i < size; i++) { |
| entry = getValues(i, entry); |
| if (predicate.test(entry)) { |
| if (nextOutputEntry != i) { |
| setValues(nextOutputEntry, entry); |
| } |
| nextOutputEntry++; |
| } |
| } |
| size = nextOutputEntry; |
| } |
| |
| public void dump(String prefix, PrintWriter pw) { |
| pw.print(prefix); |
| pw.print("NetworkStats: elapsedRealtime="); pw.println(elapsedRealtime); |
| for (int i = 0; i < size; i++) { |
| pw.print(prefix); |
| pw.print(" ["); pw.print(i); pw.print("]"); |
| pw.print(" iface="); pw.print(iface[i]); |
| pw.print(" uid="); pw.print(uid[i]); |
| pw.print(" set="); pw.print(setToString(set[i])); |
| pw.print(" tag="); pw.print(tagToString(tag[i])); |
| pw.print(" metered="); pw.print(meteredToString(metered[i])); |
| pw.print(" roaming="); pw.print(roamingToString(roaming[i])); |
| pw.print(" defaultNetwork="); pw.print(defaultNetworkToString(defaultNetwork[i])); |
| pw.print(" rxBytes="); pw.print(rxBytes[i]); |
| pw.print(" rxPackets="); pw.print(rxPackets[i]); |
| pw.print(" txBytes="); pw.print(txBytes[i]); |
| pw.print(" txPackets="); pw.print(txPackets[i]); |
| pw.print(" operations="); pw.println(operations[i]); |
| } |
| } |
| |
| /** |
| * Return text description of {@link #set} value. |
| */ |
| public static String setToString(int set) { |
| switch (set) { |
| case SET_ALL: |
| return "ALL"; |
| case SET_DEFAULT: |
| return "DEFAULT"; |
| case SET_FOREGROUND: |
| return "FOREGROUND"; |
| case SET_DBG_VPN_IN: |
| return "DBG_VPN_IN"; |
| case SET_DBG_VPN_OUT: |
| return "DBG_VPN_OUT"; |
| default: |
| return "UNKNOWN"; |
| } |
| } |
| |
| /** |
| * Return text description of {@link #set} value. |
| */ |
| public static String setToCheckinString(int set) { |
| switch (set) { |
| case SET_ALL: |
| return "all"; |
| case SET_DEFAULT: |
| return "def"; |
| case SET_FOREGROUND: |
| return "fg"; |
| case SET_DBG_VPN_IN: |
| return "vpnin"; |
| case SET_DBG_VPN_OUT: |
| return "vpnout"; |
| default: |
| return "unk"; |
| } |
| } |
| |
| /** |
| * @return true if the querySet matches the dataSet. |
| */ |
| public static boolean setMatches(int querySet, int dataSet) { |
| if (querySet == dataSet) { |
| return true; |
| } |
| // SET_ALL matches all non-debugging sets. |
| return querySet == SET_ALL && dataSet < SET_DEBUG_START; |
| } |
| |
| /** |
| * Return text description of {@link #tag} value. |
| */ |
| public static String tagToString(int tag) { |
| return "0x" + Integer.toHexString(tag); |
| } |
| |
| /** |
| * Return text description of {@link #metered} value. |
| */ |
| public static String meteredToString(int metered) { |
| switch (metered) { |
| case METERED_ALL: |
| return "ALL"; |
| case METERED_NO: |
| return "NO"; |
| case METERED_YES: |
| return "YES"; |
| default: |
| return "UNKNOWN"; |
| } |
| } |
| |
| /** |
| * Return text description of {@link #roaming} value. |
| */ |
| public static String roamingToString(int roaming) { |
| switch (roaming) { |
| case ROAMING_ALL: |
| return "ALL"; |
| case ROAMING_NO: |
| return "NO"; |
| case ROAMING_YES: |
| return "YES"; |
| default: |
| return "UNKNOWN"; |
| } |
| } |
| |
| /** |
| * Return text description of {@link #defaultNetwork} value. |
| */ |
| public static String defaultNetworkToString(int defaultNetwork) { |
| switch (defaultNetwork) { |
| case DEFAULT_NETWORK_ALL: |
| return "ALL"; |
| case DEFAULT_NETWORK_NO: |
| return "NO"; |
| case DEFAULT_NETWORK_YES: |
| return "YES"; |
| default: |
| return "UNKNOWN"; |
| } |
| } |
| |
| @Override |
| public String toString() { |
| final CharArrayWriter writer = new CharArrayWriter(); |
| dump("", new PrintWriter(writer)); |
| return writer.toString(); |
| } |
| |
| @Override |
| public int describeContents() { |
| return 0; |
| } |
| |
| @UnsupportedAppUsage |
| public static final @android.annotation.NonNull Creator<NetworkStats> CREATOR = new Creator<NetworkStats>() { |
| @Override |
| public NetworkStats createFromParcel(Parcel in) { |
| return new NetworkStats(in); |
| } |
| |
| @Override |
| public NetworkStats[] newArray(int size) { |
| return new NetworkStats[size]; |
| } |
| }; |
| |
| public interface NonMonotonicObserver<C> { |
| public void foundNonMonotonic( |
| NetworkStats left, int leftIndex, NetworkStats right, int rightIndex, C cookie); |
| public void foundNonMonotonic( |
| NetworkStats stats, int statsIndex, C cookie); |
| } |
| |
| /** |
| * VPN accounting. Move some VPN's underlying traffic to other UIDs that use tun0 iface. |
| * |
| * <p>This method should only be called on delta NetworkStats. Do not call this method on a |
| * snapshot {@link NetworkStats} object because the tunUid and/or the underlyingIface may change |
| * over time. |
| * |
| * <p>This method performs adjustments for one active VPN package and one VPN iface at a time. |
| * |
| * @param tunUid uid of the VPN application |
| * @param tunIface iface of the vpn tunnel |
| * @param underlyingIfaces underlying network ifaces used by the VPN application |
| */ |
| public void migrateTun(int tunUid, @NonNull String tunIface, |
| @NonNull String[] underlyingIfaces) { |
| // Combined usage by all apps using VPN. |
| final Entry tunIfaceTotal = new Entry(); |
| // Usage by VPN, grouped by its {@code underlyingIfaces}. |
| final Entry[] perInterfaceTotal = new Entry[underlyingIfaces.length]; |
| // Usage by VPN, summed across all its {@code underlyingIfaces}. |
| final Entry underlyingIfacesTotal = new Entry(); |
| |
| for (int i = 0; i < perInterfaceTotal.length; i++) { |
| perInterfaceTotal[i] = new Entry(); |
| } |
| |
| tunAdjustmentInit(tunUid, tunIface, underlyingIfaces, tunIfaceTotal, perInterfaceTotal, |
| underlyingIfacesTotal); |
| |
| // If tunIface < underlyingIfacesTotal, it leaves the overhead traffic in the VPN app. |
| // If tunIface > underlyingIfacesTotal, the VPN app doesn't get credit for data compression. |
| // Negative stats should be avoided. |
| final Entry[] moved = |
| addTrafficToApplications(tunUid, tunIface, underlyingIfaces, tunIfaceTotal, |
| perInterfaceTotal, underlyingIfacesTotal); |
| deductTrafficFromVpnApp(tunUid, underlyingIfaces, moved); |
| } |
| |
| /** |
| * Initializes the data used by the migrateTun() method. |
| * |
| * <p>This is the first pass iteration which does the following work: |
| * |
| * <ul> |
| * <li>Adds up all the traffic through the tunUid's underlyingIfaces (both foreground and |
| * background). |
| * <li>Adds up all the traffic through tun0 excluding traffic from the vpn app itself. |
| * </ul> |
| * |
| * @param tunUid uid of the VPN application |
| * @param tunIface iface of the vpn tunnel |
| * @param underlyingIfaces underlying network ifaces used by the VPN application |
| * @param tunIfaceTotal output parameter; combined data usage by all apps using VPN |
| * @param perInterfaceTotal output parameter; data usage by VPN app, grouped by its {@code |
| * underlyingIfaces} |
| * @param underlyingIfacesTotal output parameter; data usage by VPN, summed across all of its |
| * {@code underlyingIfaces} |
| */ |
| private void tunAdjustmentInit(int tunUid, @NonNull String tunIface, |
| @NonNull String[] underlyingIfaces, @NonNull Entry tunIfaceTotal, |
| @NonNull Entry[] perInterfaceTotal, @NonNull Entry underlyingIfacesTotal) { |
| final Entry recycle = new Entry(); |
| for (int i = 0; i < size; i++) { |
| getValues(i, recycle); |
| if (recycle.uid == UID_ALL) { |
| throw new IllegalStateException( |
| "Cannot adjust VPN accounting on an iface aggregated NetworkStats."); |
| } |
| if (recycle.set == SET_DBG_VPN_IN || recycle.set == SET_DBG_VPN_OUT) { |
| throw new IllegalStateException( |
| "Cannot adjust VPN accounting on a NetworkStats containing SET_DBG_VPN_*"); |
| } |
| if (recycle.tag != TAG_NONE) { |
| // TODO(b/123666283): Take all tags for tunUid into account. |
| continue; |
| } |
| |
| if (recycle.uid == tunUid) { |
| // Add up traffic through tunUid's underlying interfaces. |
| for (int j = 0; j < underlyingIfaces.length; j++) { |
| if (Objects.equals(underlyingIfaces[j], recycle.iface)) { |
| perInterfaceTotal[j].add(recycle); |
| underlyingIfacesTotal.add(recycle); |
| break; |
| } |
| } |
| } else if (tunIface.equals(recycle.iface)) { |
| // Add up all tunIface traffic excluding traffic from the vpn app itself. |
| tunIfaceTotal.add(recycle); |
| } |
| } |
| } |
| |
| /** |
| * Distributes traffic across apps that are using given {@code tunIface}, and returns the total |
| * traffic that should be moved off of {@code tunUid} grouped by {@code underlyingIfaces}. |
| * |
| * @param tunUid uid of the VPN application |
| * @param tunIface iface of the vpn tunnel |
| * @param underlyingIfaces underlying network ifaces used by the VPN application |
| * @param tunIfaceTotal combined data usage across all apps using {@code tunIface} |
| * @param perInterfaceTotal data usage by VPN app, grouped by its {@code underlyingIfaces} |
| * @param underlyingIfacesTotal data usage by VPN, summed across all of its {@code |
| * underlyingIfaces} |
| */ |
| private Entry[] addTrafficToApplications(int tunUid, @NonNull String tunIface, |
| @NonNull String[] underlyingIfaces, @NonNull Entry tunIfaceTotal, |
| @NonNull Entry[] perInterfaceTotal, @NonNull Entry underlyingIfacesTotal) { |
| // Traffic that should be moved off of each underlying interface for tunUid (see |
| // deductTrafficFromVpnApp below). |
| final Entry[] moved = new Entry[underlyingIfaces.length]; |
| for (int i = 0; i < underlyingIfaces.length; i++) { |
| moved[i] = new Entry(); |
| } |
| |
| final Entry tmpEntry = new Entry(); |
| final int origSize = size; |
| for (int i = 0; i < origSize; i++) { |
| if (!Objects.equals(iface[i], tunIface)) { |
| // Consider only entries that go onto the VPN interface. |
| continue; |
| } |
| if (uid[i] == tunUid) { |
| // Exclude VPN app from the redistribution, as it can choose to create packet |
| // streams by writing to itself. |
| continue; |
| } |
| tmpEntry.uid = uid[i]; |
| tmpEntry.tag = tag[i]; |
| tmpEntry.metered = metered[i]; |
| tmpEntry.roaming = roaming[i]; |
| tmpEntry.defaultNetwork = defaultNetwork[i]; |
| |
| // In a first pass, compute this entry's total share of data across all |
| // underlyingIfaces. This is computed on the basis of the share of this entry's usage |
| // over tunIface. |
| // TODO: Consider refactoring first pass into a separate helper method. |
| long totalRxBytes = 0; |
| if (tunIfaceTotal.rxBytes > 0) { |
| // Note - The multiplication below should not overflow since NetworkStatsService |
| // processes this every time device has transmitted/received amount equivalent to |
| // global threshold alert (~ 2MB) across all interfaces. |
| final long rxBytesAcrossUnderlyingIfaces = |
| underlyingIfacesTotal.rxBytes * rxBytes[i] / tunIfaceTotal.rxBytes; |
| // app must not be blamed for more than it consumed on tunIface |
| totalRxBytes = Math.min(rxBytes[i], rxBytesAcrossUnderlyingIfaces); |
| } |
| long totalRxPackets = 0; |
| if (tunIfaceTotal.rxPackets > 0) { |
| final long rxPacketsAcrossUnderlyingIfaces = |
| underlyingIfacesTotal.rxPackets * rxPackets[i] / tunIfaceTotal.rxPackets; |
| totalRxPackets = Math.min(rxPackets[i], rxPacketsAcrossUnderlyingIfaces); |
| } |
| long totalTxBytes = 0; |
| if (tunIfaceTotal.txBytes > 0) { |
| final long txBytesAcrossUnderlyingIfaces = |
| underlyingIfacesTotal.txBytes * txBytes[i] / tunIfaceTotal.txBytes; |
| totalTxBytes = Math.min(txBytes[i], txBytesAcrossUnderlyingIfaces); |
| } |
| long totalTxPackets = 0; |
| if (tunIfaceTotal.txPackets > 0) { |
| final long txPacketsAcrossUnderlyingIfaces = |
| underlyingIfacesTotal.txPackets * txPackets[i] / tunIfaceTotal.txPackets; |
| totalTxPackets = Math.min(txPackets[i], txPacketsAcrossUnderlyingIfaces); |
| } |
| long totalOperations = 0; |
| if (tunIfaceTotal.operations > 0) { |
| final long operationsAcrossUnderlyingIfaces = |
| underlyingIfacesTotal.operations * operations[i] / tunIfaceTotal.operations; |
| totalOperations = Math.min(operations[i], operationsAcrossUnderlyingIfaces); |
| } |
| // In a second pass, distribute these values across interfaces in the proportion that |
| // each interface represents of the total traffic of the underlying interfaces. |
| for (int j = 0; j < underlyingIfaces.length; j++) { |
| tmpEntry.iface = underlyingIfaces[j]; |
| tmpEntry.rxBytes = 0; |
| // Reset 'set' to correct value since it gets updated when adding debug info below. |
| tmpEntry.set = set[i]; |
| if (underlyingIfacesTotal.rxBytes > 0) { |
| tmpEntry.rxBytes = |
| totalRxBytes |
| * perInterfaceTotal[j].rxBytes |
| / underlyingIfacesTotal.rxBytes; |
| } |
| tmpEntry.rxPackets = 0; |
| if (underlyingIfacesTotal.rxPackets > 0) { |
| tmpEntry.rxPackets = |
| totalRxPackets |
| * perInterfaceTotal[j].rxPackets |
| / underlyingIfacesTotal.rxPackets; |
| } |
| tmpEntry.txBytes = 0; |
| if (underlyingIfacesTotal.txBytes > 0) { |
| tmpEntry.txBytes = |
| totalTxBytes |
| * perInterfaceTotal[j].txBytes |
| / underlyingIfacesTotal.txBytes; |
| } |
| tmpEntry.txPackets = 0; |
| if (underlyingIfacesTotal.txPackets > 0) { |
| tmpEntry.txPackets = |
| totalTxPackets |
| * perInterfaceTotal[j].txPackets |
| / underlyingIfacesTotal.txPackets; |
| } |
| tmpEntry.operations = 0; |
| if (underlyingIfacesTotal.operations > 0) { |
| tmpEntry.operations = |
| totalOperations |
| * perInterfaceTotal[j].operations |
| / underlyingIfacesTotal.operations; |
| } |
| // tmpEntry now contains the migrated data of the i-th entry for the j-th underlying |
| // interface. Add that data usage to this object. |
| combineValues(tmpEntry); |
| if (tag[i] == TAG_NONE) { |
| // Add the migrated data to moved so it is deducted from the VPN app later. |
| moved[j].add(tmpEntry); |
| // Add debug info |
| tmpEntry.set = SET_DBG_VPN_IN; |
| combineValues(tmpEntry); |
| } |
| } |
| } |
| return moved; |
| } |
| |
| private void deductTrafficFromVpnApp( |
| int tunUid, |
| @NonNull String[] underlyingIfaces, |
| @NonNull Entry[] moved) { |
| for (int i = 0; i < underlyingIfaces.length; i++) { |
| moved[i].uid = tunUid; |
| // Add debug info |
| moved[i].set = SET_DBG_VPN_OUT; |
| moved[i].tag = TAG_NONE; |
| moved[i].iface = underlyingIfaces[i]; |
| moved[i].metered = METERED_ALL; |
| moved[i].roaming = ROAMING_ALL; |
| moved[i].defaultNetwork = DEFAULT_NETWORK_ALL; |
| combineValues(moved[i]); |
| |
| // Caveat: if the vpn software uses tag, the total tagged traffic may be greater than |
| // the TAG_NONE traffic. |
| // |
| // Relies on the fact that the underlying traffic only has state ROAMING_NO and |
| // METERED_NO, which should be the case as it comes directly from the /proc file. |
| // We only blend in the roaming data after applying these adjustments, by checking the |
| // NetworkIdentity of the underlying iface. |
| final int idxVpnBackground = findIndex(underlyingIfaces[i], tunUid, SET_DEFAULT, |
| TAG_NONE, METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO); |
| if (idxVpnBackground != -1) { |
| // Note - tunSubtract also updates moved[i]; whatever traffic that's left is removed |
| // from foreground usage. |
| tunSubtract(idxVpnBackground, this, moved[i]); |
| } |
| |
| final int idxVpnForeground = findIndex(underlyingIfaces[i], tunUid, SET_FOREGROUND, |
| TAG_NONE, METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO); |
| if (idxVpnForeground != -1) { |
| tunSubtract(idxVpnForeground, this, moved[i]); |
| } |
| } |
| } |
| |
| private static void tunSubtract(int i, @NonNull NetworkStats left, @NonNull Entry right) { |
| long rxBytes = Math.min(left.rxBytes[i], right.rxBytes); |
| left.rxBytes[i] -= rxBytes; |
| right.rxBytes -= rxBytes; |
| |
| long rxPackets = Math.min(left.rxPackets[i], right.rxPackets); |
| left.rxPackets[i] -= rxPackets; |
| right.rxPackets -= rxPackets; |
| |
| long txBytes = Math.min(left.txBytes[i], right.txBytes); |
| left.txBytes[i] -= txBytes; |
| right.txBytes -= txBytes; |
| |
| long txPackets = Math.min(left.txPackets[i], right.txPackets); |
| left.txPackets[i] -= txPackets; |
| right.txPackets -= txPackets; |
| } |
| } |