/* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.net; import static android.Manifest.permission.NETWORK_STATS_PROVIDER; import static android.Manifest.permission.READ_NETWORK_USAGE_HISTORY; import static android.Manifest.permission.UPDATE_DEVICE_STATS; import static android.app.usage.NetworkStatsManager.PREFIX_DEV; import static android.content.Intent.ACTION_SHUTDOWN; import static android.content.Intent.ACTION_UID_REMOVED; import static android.content.Intent.ACTION_USER_REMOVED; import static android.content.Intent.EXTRA_UID; import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; import static android.net.NetworkCapabilities.TRANSPORT_WIFI; import static android.net.NetworkStats.DEFAULT_NETWORK_ALL; import static android.net.NetworkStats.DEFAULT_NETWORK_NO; import static android.net.NetworkStats.IFACE_ALL; import static android.net.NetworkStats.IFACE_VT; import static android.net.NetworkStats.INTERFACES_ALL; import static android.net.NetworkStats.METERED_ALL; import static android.net.NetworkStats.METERED_NO; import static android.net.NetworkStats.METERED_YES; import static android.net.NetworkStats.ROAMING_ALL; import static android.net.NetworkStats.ROAMING_NO; import static android.net.NetworkStats.SET_ALL; 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; import static android.net.NetworkTemplate.MATCH_MOBILE; import static android.net.NetworkTemplate.MATCH_WIFI; import static android.net.TrafficStats.KB_IN_BYTES; import static android.net.TrafficStats.MB_IN_BYTES; import static android.net.TrafficStats.UID_TETHERING; import static android.net.TrafficStats.UNSUPPORTED; import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID; import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID_TAG; import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_XT; import static android.os.Trace.TRACE_TAG_NETWORK; import static android.system.OsConstants.ENOENT; import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID; import static android.text.format.DateUtils.DAY_IN_MILLIS; import static android.text.format.DateUtils.HOUR_IN_MILLIS; import static android.text.format.DateUtils.MINUTE_IN_MILLIS; import static android.text.format.DateUtils.SECOND_IN_MILLIS; import static com.android.net.module.util.NetworkCapabilitiesUtils.getDisplayTransport; import static com.android.net.module.util.NetworkStatsUtils.LIMIT_GLOBAL_ALERT; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.TargetApi; import android.app.AlarmManager; import android.app.BroadcastOptions; import android.app.PendingIntent; import android.app.usage.NetworkStatsManager; import android.content.ApexEnvironment; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.Resources; import android.database.ContentObserver; import android.net.ConnectivityManager; import android.net.DataUsageRequest; import android.net.INetd; import android.net.INetworkStatsService; import android.net.INetworkStatsSession; import android.net.Network; import android.net.NetworkCapabilities; import android.net.NetworkIdentity; import android.net.NetworkIdentitySet; import android.net.NetworkPolicyManager; import android.net.NetworkSpecifier; import android.net.NetworkStack; import android.net.NetworkStateSnapshot; import android.net.NetworkStats; import android.net.NetworkStats.NonMonotonicObserver; import android.net.NetworkStatsAccess; import android.net.NetworkStatsCollection; import android.net.NetworkStatsHistory; import android.net.NetworkTemplate; import android.net.TelephonyNetworkSpecifier; import android.net.TetherStatsParcel; import android.net.TetheringManager; import android.net.TrafficStats; import android.net.TransportInfo; import android.net.UnderlyingNetworkInfo; import android.net.Uri; import android.net.netstats.IUsageCallback; import android.net.netstats.NetworkStatsDataMigrationUtils; import android.net.netstats.provider.INetworkStatsProvider; import android.net.netstats.provider.INetworkStatsProviderCallback; import android.net.netstats.provider.NetworkStatsProvider; import android.net.wifi.WifiInfo; import android.os.Binder; import android.os.Build; import android.os.Bundle; import android.os.DropBoxManager; import android.os.Environment; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.PowerManager; import android.os.RemoteException; import android.os.ServiceSpecificException; import android.os.SystemClock; import android.os.Trace; import android.os.UserHandle; import android.provider.DeviceConfig; import android.provider.Settings; import android.provider.Settings.Global; import android.service.NetworkInterfaceProto; import android.service.NetworkStatsServiceDumpProto; import android.system.ErrnoException; import android.telephony.PhoneStateListener; import android.telephony.SubscriptionPlan; import android.text.TextUtils; import android.text.format.DateUtils; import android.util.ArrayMap; import android.util.ArraySet; import android.util.EventLog; import android.util.IndentingPrintWriter; import android.util.Log; import android.util.SparseIntArray; import android.util.proto.ProtoOutputStream; import com.android.connectivity.resources.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.FileRotator; import com.android.modules.utils.build.SdkLevel; import com.android.net.module.util.BaseNetdUnsolicitedEventListener; import com.android.net.module.util.BestClock; import com.android.net.module.util.BinderUtils; import com.android.net.module.util.BpfDump; import com.android.net.module.util.BpfMap; import com.android.net.module.util.CollectionUtils; import com.android.net.module.util.DeviceConfigUtils; import com.android.net.module.util.IBpfMap; import com.android.net.module.util.LocationPermissionChecker; import com.android.net.module.util.NetworkStatsUtils; import com.android.net.module.util.PermissionUtils; import com.android.net.module.util.SharedLog; import com.android.net.module.util.Struct; import com.android.net.module.util.Struct.S32; import com.android.net.module.util.Struct.U8; import com.android.net.module.util.bpf.CookieTagMapKey; import com.android.net.module.util.bpf.CookieTagMapValue; import com.android.networkstack.apishim.BroadcastOptionsShimImpl; import com.android.networkstack.apishim.ConstantsShim; import com.android.networkstack.apishim.common.UnsupportedApiLevelException; import com.android.server.BpfNetMaps; import com.android.server.connectivity.ConnectivityResources; import java.io.File; import java.io.FileDescriptor; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintWriter; import java.nio.file.Path; import java.time.Clock; import java.time.Instant; import java.time.ZoneOffset; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executor; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; /** * Collect and persist detailed network statistics, and provide this data to * other system services. */ @TargetApi(Build.VERSION_CODES.TIRAMISU) public class NetworkStatsService extends INetworkStatsService.Stub { static { System.loadLibrary("service-connectivity"); } static final String TAG = "NetworkStats"; static final boolean LOGD = Log.isLoggable(TAG, Log.DEBUG); static final boolean LOGV = Log.isLoggable(TAG, Log.VERBOSE); // Perform polling and persist all (FLAG_PERSIST_ALL). private static final int MSG_PERFORM_POLL = 1; // Perform polling, persist network, and register the global alert again. private static final int MSG_PERFORM_POLL_REGISTER_ALERT = 2; private static final int MSG_NOTIFY_NETWORK_STATUS = 3; // A message for broadcasting ACTION_NETWORK_STATS_UPDATED in handler thread to prevent // deadlock. private static final int MSG_BROADCAST_NETWORK_STATS_UPDATED = 4; /** Flags to control detail level of poll event. */ private static final int FLAG_PERSIST_NETWORK = 0x1; private static final int FLAG_PERSIST_UID = 0x2; private static final int FLAG_PERSIST_ALL = FLAG_PERSIST_NETWORK | FLAG_PERSIST_UID; private static final int FLAG_PERSIST_FORCE = 0x100; /** * When global alert quota is high, wait for this delay before processing each polling, * and do not schedule further polls once there is already one queued. * This avoids firing the global alert too often on devices with high transfer speeds and * high quota. */ private static final int DEFAULT_PERFORM_POLL_DELAY_MS = 1000; private static final String TAG_NETSTATS_ERROR = "netstats_error"; /** * EventLog tags used when logging into the event log. Note the values must be sync with * frameworks/base/services/core/java/com/android/server/EventLogTags.logtags to get correct * name translation. */ private static final int LOG_TAG_NETSTATS_MOBILE_SAMPLE = 51100; private static final int LOG_TAG_NETSTATS_WIFI_SAMPLE = 51101; // TODO: Replace the hardcoded string and move it into ConnectivitySettingsManager. private static final String NETSTATS_COMBINE_SUBTYPE_ENABLED = "netstats_combine_subtype_enabled"; private static final String UID_COUNTERSET_MAP_PATH = "/sys/fs/bpf/netd_shared/map_netd_uid_counterset_map"; private static final String COOKIE_TAG_MAP_PATH = "/sys/fs/bpf/netd_shared/map_netd_cookie_tag_map"; private static final String APP_UID_STATS_MAP_PATH = "/sys/fs/bpf/netd_shared/map_netd_app_uid_stats_map"; private static final String STATS_MAP_A_PATH = "/sys/fs/bpf/netd_shared/map_netd_stats_map_A"; private static final String STATS_MAP_B_PATH = "/sys/fs/bpf/netd_shared/map_netd_stats_map_B"; private static final String IFACE_STATS_MAP_PATH = "/sys/fs/bpf/netd_shared/map_netd_iface_stats_map"; /** * DeviceConfig flag used to indicate whether the files should be stored in the apex data * directory. */ static final String NETSTATS_STORE_FILES_IN_APEXDATA = "netstats_store_files_in_apexdata"; /** * DeviceConfig flag is used to indicate whether the legacy files need to be imported, and * retry count before giving up. Only valid when {@link #NETSTATS_STORE_FILES_IN_APEXDATA} * set to true. Note that the value gets rollback when the mainline module gets rollback. */ static final String NETSTATS_IMPORT_LEGACY_TARGET_ATTEMPTS = "netstats_import_legacy_target_attempts"; static final int DEFAULT_NETSTATS_IMPORT_LEGACY_TARGET_ATTEMPTS = 1; static final String NETSTATS_IMPORT_ATTEMPTS_COUNTER_NAME = "import.attempts"; static final String NETSTATS_IMPORT_SUCCESSES_COUNTER_NAME = "import.successes"; static final String NETSTATS_IMPORT_FALLBACKS_COUNTER_NAME = "import.fallbacks"; private final Context mContext; private final NetworkStatsFactory mStatsFactory; private final AlarmManager mAlarmManager; private final Clock mClock; private final NetworkStatsSettings mSettings; private final NetworkStatsObservers mStatsObservers; private final File mStatsDir; private final PowerManager.WakeLock mWakeLock; private final ContentObserver mContentObserver; private final ContentResolver mContentResolver; protected INetd mNetd; private final AlertObserver mAlertObserver = new AlertObserver(); // Persistent counters that backed by AtomicFile which stored in the data directory as a file, // to track attempts/successes/fallbacks count across reboot. Note that these counter values // will be rollback as the module rollbacks. private PersistentInt mImportLegacyAttemptsCounter = null; private PersistentInt mImportLegacySuccessesCounter = null; private PersistentInt mImportLegacyFallbacksCounter = null; @VisibleForTesting public static final String ACTION_NETWORK_STATS_POLL = "com.android.server.action.NETWORK_STATS_POLL"; public static final String ACTION_NETWORK_STATS_UPDATED = "com.android.server.action.NETWORK_STATS_UPDATED"; private PendingIntent mPollIntent; /** * Settings that can be changed externally. */ public interface NetworkStatsSettings { long getPollInterval(); long getPollDelay(); boolean getSampleEnabled(); boolean getAugmentEnabled(); /** * When enabled, all mobile data is reported under {@link NetworkTemplate#NETWORK_TYPE_ALL}. * When disabled, mobile data is broken down by a granular ratType representative of the * actual ratType. See {@link android.app.usage.NetworkStatsManager#getCollapsedRatType}. * Enabling this decreases the level of detail but saves performance, disk space and * amount of data logged. */ boolean getCombineSubtypeEnabled(); class Config { public final long bucketDuration; public final long rotateAgeMillis; public final long deleteAgeMillis; public Config(long bucketDuration, long rotateAgeMillis, long deleteAgeMillis) { this.bucketDuration = bucketDuration; this.rotateAgeMillis = rotateAgeMillis; this.deleteAgeMillis = deleteAgeMillis; } } Config getXtConfig(); Config getUidConfig(); Config getUidTagConfig(); long getGlobalAlertBytes(long def); long getXtPersistBytes(long def); long getUidPersistBytes(long def); long getUidTagPersistBytes(long def); } private final Object mStatsLock = new Object(); /** Set of currently active ifaces. */ @GuardedBy("mStatsLock") private final ArrayMap mActiveIfaces = new ArrayMap<>(); /** Set of currently active ifaces for UID stats. */ @GuardedBy("mStatsLock") private final ArrayMap mActiveUidIfaces = new ArrayMap<>(); /** Current default active iface. */ @GuardedBy("mStatsLock") private String mActiveIface; /** Set of all ifaces currently associated with mobile networks. */ private volatile String[] mMobileIfaces = new String[0]; /* A set of all interfaces that have ever been associated with mobile networks since boot. */ @GuardedBy("mStatsLock") private final Set mAllMobileIfacesSinceBoot = new ArraySet<>(); /* A set of all interfaces that have ever been associated with wifi networks since boot. */ @GuardedBy("mStatsLock") private final Set mAllWifiIfacesSinceBoot = new ArraySet<>(); /** Set of all ifaces currently used by traffic that does not explicitly specify a Network. */ @GuardedBy("mStatsLock") private Network[] mDefaultNetworks = new Network[0]; /** Last states of all networks sent from ConnectivityService. */ @GuardedBy("mStatsLock") @Nullable private NetworkStateSnapshot[] mLastNetworkStateSnapshots = null; private final DropBoxNonMonotonicObserver mNonMonotonicObserver = new DropBoxNonMonotonicObserver(); private static final int MAX_STATS_PROVIDER_POLL_WAIT_TIME_MS = 100; private final CopyOnWriteArrayList mStatsProviderCbList = new CopyOnWriteArrayList<>(); /** Semaphore used to wait for stats provider to respond to request stats update. */ private final Semaphore mStatsProviderSem = new Semaphore(0, true); @GuardedBy("mStatsLock") private NetworkStatsRecorder mXtRecorder; @GuardedBy("mStatsLock") private NetworkStatsRecorder mUidRecorder; @GuardedBy("mStatsLock") private NetworkStatsRecorder mUidTagRecorder; /** Cached {@link #mXtRecorder} stats. */ @GuardedBy("mStatsLock") private NetworkStatsCollection mXtStatsCached; /** * Current counter sets for each UID. * TODO: maybe remove mActiveUidCounterSet and read UidCouneterSet value from mUidCounterSetMap * directly ? But if mActiveUidCounterSet would be accessed very frequently, maybe keep * mActiveUidCounterSet to avoid accessing kernel too frequently. */ private SparseIntArray mActiveUidCounterSet = new SparseIntArray(); private final IBpfMap mUidCounterSetMap; private final IBpfMap mCookieTagMap; private final IBpfMap mStatsMapA; private final IBpfMap mStatsMapB; private final IBpfMap mAppUidStatsMap; private final IBpfMap mIfaceStatsMap; /** Data layer operation counters for splicing into other structures. */ private NetworkStats mUidOperations = new NetworkStats(0L, 10); @NonNull private final Handler mHandler; private volatile boolean mSystemReady; private long mPersistThreshold = 2 * MB_IN_BYTES; private long mGlobalAlertBytes; private static final long POLL_RATE_LIMIT_MS = 15_000; private long mLastStatsSessionPoll; private final Object mOpenSessionCallsLock = new Object(); /** * Map from key {@code OpenSessionKey} to count of opened sessions. This is for recording * the caller of open session and it is only for debugging. */ @GuardedBy("mOpenSessionCallsLock") private final HashMap mOpenSessionCallsPerCaller = new HashMap<>(); private final static int DUMP_STATS_SESSION_COUNT = 20; @NonNull private final Dependencies mDeps; @NonNull private final NetworkStatsSubscriptionsMonitor mNetworkStatsSubscriptionsMonitor; @NonNull private final LocationPermissionChecker mLocationPermissionChecker; @NonNull private final BpfInterfaceMapUpdater mInterfaceMapUpdater; @Nullable private final SkDestroyListener mSkDestroyListener; private static @NonNull Clock getDefaultClock() { return new BestClock(ZoneOffset.UTC, SystemClock.currentNetworkTimeClock(), Clock.systemUTC()); } /** * This class is a key that used in {@code mOpenSessionCallsPerCaller} to identify the count of * the caller. */ private static class OpenSessionKey { public final int uid; public final String packageName; OpenSessionKey(int uid, @NonNull String packageName) { this.uid = uid; this.packageName = packageName; } @Override public String toString() { final StringBuilder sb = new StringBuilder(); sb.append("{"); sb.append("uid=").append(uid).append(","); sb.append("package=").append(packageName); sb.append("}"); return sb.toString(); } @Override public boolean equals(@NonNull Object o) { if (this == o) return true; if (o.getClass() != getClass()) return false; final OpenSessionKey key = (OpenSessionKey) o; return this.uid == key.uid && TextUtils.equals(this.packageName, key.packageName); } @Override public int hashCode() { return Objects.hash(uid, packageName); } } private final class NetworkStatsHandler extends Handler { NetworkStatsHandler(@NonNull Looper looper) { super(looper); } @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_PERFORM_POLL: { performPoll(FLAG_PERSIST_ALL); break; } case MSG_NOTIFY_NETWORK_STATUS: { // If no cached states, ignore. if (mLastNetworkStateSnapshots == null) break; // TODO (b/181642673): Protect mDefaultNetworks from concurrent accessing. handleNotifyNetworkStatus( mDefaultNetworks, mLastNetworkStateSnapshots, mActiveIface); break; } case MSG_PERFORM_POLL_REGISTER_ALERT: { performPoll(FLAG_PERSIST_NETWORK); registerGlobalAlert(); break; } case MSG_BROADCAST_NETWORK_STATS_UPDATED: { final Intent updatedIntent = new Intent(ACTION_NETWORK_STATS_UPDATED); updatedIntent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); Bundle opts = null; if (SdkLevel.isAtLeastU()) { try { // This allows us to discard older broadcasts still waiting to // be delivered. opts = BroadcastOptionsShimImpl.newInstance( BroadcastOptions.makeBasic()) .setDeliveryGroupPolicy( ConstantsShim.DELIVERY_GROUP_POLICY_MOST_RECENT) .setDeferralPolicy( ConstantsShim.DEFERRAL_POLICY_UNTIL_ACTIVE) .toBundle(); } catch (UnsupportedApiLevelException e) { Log.wtf(TAG, "Using unsupported API" + e); } } mContext.sendBroadcastAsUser(updatedIntent, UserHandle.ALL, READ_NETWORK_USAGE_HISTORY, opts); break; } } } } /** Creates a new NetworkStatsService */ public static NetworkStatsService create(Context context) { AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); PowerManager.WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); final INetd netd = INetd.Stub.asInterface( (IBinder) context.getSystemService(Context.NETD_SERVICE)); final NetworkStatsService service = new NetworkStatsService(context, INetd.Stub.asInterface((IBinder) context.getSystemService(Context.NETD_SERVICE)), alarmManager, wakeLock, getDefaultClock(), new DefaultNetworkStatsSettings(), new NetworkStatsFactory(context), new NetworkStatsObservers(), new Dependencies()); return service; } // This must not be called outside of tests, even within the same package, as this constructor // does not register the local service. Use the create() helper above. @VisibleForTesting NetworkStatsService(Context context, INetd netd, AlarmManager alarmManager, PowerManager.WakeLock wakeLock, Clock clock, NetworkStatsSettings settings, NetworkStatsFactory factory, NetworkStatsObservers statsObservers, @NonNull Dependencies deps) { mContext = Objects.requireNonNull(context, "missing Context"); mNetd = Objects.requireNonNull(netd, "missing Netd"); mAlarmManager = Objects.requireNonNull(alarmManager, "missing AlarmManager"); mClock = Objects.requireNonNull(clock, "missing Clock"); mSettings = Objects.requireNonNull(settings, "missing NetworkStatsSettings"); mWakeLock = Objects.requireNonNull(wakeLock, "missing WakeLock"); mStatsFactory = Objects.requireNonNull(factory, "missing factory"); mStatsObservers = Objects.requireNonNull(statsObservers, "missing NetworkStatsObservers"); mDeps = Objects.requireNonNull(deps, "missing Dependencies"); mStatsDir = mDeps.getOrCreateStatsDir(); if (!mStatsDir.exists()) { throw new IllegalStateException("Persist data directory does not exist: " + mStatsDir); } final HandlerThread handlerThread = mDeps.makeHandlerThread(); handlerThread.start(); mHandler = new NetworkStatsHandler(handlerThread.getLooper()); mNetworkStatsSubscriptionsMonitor = deps.makeSubscriptionsMonitor(mContext, (command) -> mHandler.post(command) , this); mContentResolver = mContext.getContentResolver(); mContentObserver = mDeps.makeContentObserver(mHandler, mSettings, mNetworkStatsSubscriptionsMonitor); mLocationPermissionChecker = mDeps.makeLocationPermissionChecker(mContext); mInterfaceMapUpdater = mDeps.makeBpfInterfaceMapUpdater(mContext, mHandler); mInterfaceMapUpdater.start(); mUidCounterSetMap = mDeps.getUidCounterSetMap(); mCookieTagMap = mDeps.getCookieTagMap(); mStatsMapA = mDeps.getStatsMapA(); mStatsMapB = mDeps.getStatsMapB(); mAppUidStatsMap = mDeps.getAppUidStatsMap(); mIfaceStatsMap = mDeps.getIfaceStatsMap(); // TODO: Remove bpfNetMaps creation and always start SkDestroyListener // Following code is for the experiment to verify the SkDestroyListener refactoring. Based // on the experiment flag, BpfNetMaps starts C SkDestroyListener (existing code) or // NetworkStatsService starts Java SkDestroyListener (new code). final BpfNetMaps bpfNetMaps = mDeps.makeBpfNetMaps(mContext); if (bpfNetMaps.isSkDestroyListenerRunning()) { mSkDestroyListener = null; } else { mSkDestroyListener = mDeps.makeSkDestroyListener(mCookieTagMap, mHandler); mHandler.post(mSkDestroyListener::start); } } /** * Dependencies of NetworkStatsService, for injection in tests. */ // TODO: Move more stuff into dependencies object. @VisibleForTesting public static class Dependencies { /** * Get legacy platform stats directory. */ @NonNull public File getLegacyStatsDir() { final File systemDataDir = new File(Environment.getDataDirectory(), "system"); return new File(systemDataDir, "netstats"); } /** * Get or create the directory that stores the persisted data usage. */ @NonNull public File getOrCreateStatsDir() { final boolean storeInApexDataDir = getStoreFilesInApexData(); final File statsDataDir; if (storeInApexDataDir) { final File apexDataDir = ApexEnvironment .getApexEnvironment(DeviceConfigUtils.TETHERING_MODULE_NAME) .getDeviceProtectedDataDir(); statsDataDir = new File(apexDataDir, "netstats"); } else { statsDataDir = getLegacyStatsDir(); } if (statsDataDir.exists() || statsDataDir.mkdirs()) { return statsDataDir; } throw new IllegalStateException("Cannot write into stats data directory: " + statsDataDir); } /** * Get the count of import legacy target attempts. */ public int getImportLegacyTargetAttempts() { return DeviceConfigUtils.getDeviceConfigPropertyInt( DeviceConfig.NAMESPACE_TETHERING, NETSTATS_IMPORT_LEGACY_TARGET_ATTEMPTS, DEFAULT_NETSTATS_IMPORT_LEGACY_TARGET_ATTEMPTS); } /** * Create a persistent counter for given directory and name. */ public PersistentInt createPersistentCounter(@NonNull Path dir, @NonNull String name) throws IOException { // TODO: Modify PersistentInt to call setStartTime every time a write is made. // Create and pass a real logger here. final String path = dir.resolve(name).toString(); return new PersistentInt(path, null /* logger */); } /** * Get the flag of storing files in the apex data directory. * @return whether to store files in the apex data directory. */ public boolean getStoreFilesInApexData() { return DeviceConfigUtils.getDeviceConfigPropertyBoolean( DeviceConfig.NAMESPACE_TETHERING, NETSTATS_STORE_FILES_IN_APEXDATA, true); } /** * Read legacy persisted network stats from disk. */ @NonNull public NetworkStatsCollection readPlatformCollection( @NonNull String prefix, long bucketDuration) throws IOException { return NetworkStatsDataMigrationUtils.readPlatformCollection(prefix, bucketDuration); } /** * Create a HandlerThread to use in NetworkStatsService. */ @NonNull public HandlerThread makeHandlerThread() { return new HandlerThread(TAG); } /** * Create a {@link NetworkStatsSubscriptionsMonitor}, can be used to monitor RAT change * event in NetworkStatsService. */ @NonNull public NetworkStatsSubscriptionsMonitor makeSubscriptionsMonitor(@NonNull Context context, @NonNull Executor executor, @NonNull NetworkStatsService service) { // TODO: Update RatType passively in NSS, instead of querying into the monitor // when notifyNetworkStatus. return new NetworkStatsSubscriptionsMonitor(context, executor, (subscriberId, type) -> service.handleOnCollapsedRatTypeChanged()); } /** * Create a ContentObserver instance which is used to observe settings changes, * and dispatch onChange events on handler thread. */ public @NonNull ContentObserver makeContentObserver(@NonNull Handler handler, @NonNull NetworkStatsSettings settings, @NonNull NetworkStatsSubscriptionsMonitor monitor) { return new ContentObserver(handler) { @Override public void onChange(boolean selfChange, @NonNull Uri uri) { if (!settings.getCombineSubtypeEnabled()) { monitor.start(); } else { monitor.stop(); } } }; } /** * @see LocationPermissionChecker */ public LocationPermissionChecker makeLocationPermissionChecker(final Context context) { return new LocationPermissionChecker(context); } /** Create BpfInterfaceMapUpdater to update bpf interface map. */ @NonNull public BpfInterfaceMapUpdater makeBpfInterfaceMapUpdater( @NonNull Context ctx, @NonNull Handler handler) { return new BpfInterfaceMapUpdater(ctx, handler); } /** Get counter sets map for each UID. */ public IBpfMap getUidCounterSetMap() { try { return new BpfMap(UID_COUNTERSET_MAP_PATH, BpfMap.BPF_F_RDWR, S32.class, U8.class); } catch (ErrnoException e) { Log.wtf(TAG, "Cannot open uid counter set map: " + e); return null; } } /** Gets the cookie tag map */ public IBpfMap getCookieTagMap() { try { return new BpfMap(COOKIE_TAG_MAP_PATH, BpfMap.BPF_F_RDWR, CookieTagMapKey.class, CookieTagMapValue.class); } catch (ErrnoException e) { Log.wtf(TAG, "Cannot open cookie tag map: " + e); return null; } } /** Gets stats map A */ public IBpfMap getStatsMapA() { try { return new BpfMap(STATS_MAP_A_PATH, BpfMap.BPF_F_RDWR, StatsMapKey.class, StatsMapValue.class); } catch (ErrnoException e) { Log.wtf(TAG, "Cannot open stats map A: " + e); return null; } } /** Gets stats map B */ public IBpfMap getStatsMapB() { try { return new BpfMap(STATS_MAP_B_PATH, BpfMap.BPF_F_RDWR, StatsMapKey.class, StatsMapValue.class); } catch (ErrnoException e) { Log.wtf(TAG, "Cannot open stats map B: " + e); return null; } } /** Gets the uid stats map */ public IBpfMap getAppUidStatsMap() { try { return new BpfMap(APP_UID_STATS_MAP_PATH, BpfMap.BPF_F_RDWR, UidStatsMapKey.class, StatsMapValue.class); } catch (ErrnoException e) { Log.wtf(TAG, "Cannot open app uid stats map: " + e); return null; } } /** Gets interface stats map */ public IBpfMap getIfaceStatsMap() { try { return new BpfMap(IFACE_STATS_MAP_PATH, BpfMap.BPF_F_RDWR, S32.class, StatsMapValue.class); } catch (ErrnoException e) { throw new IllegalStateException("Failed to open interface stats map", e); } } /** Gets whether the build is userdebug. */ public boolean isDebuggable() { return Build.isDebuggable(); } /** Create a new BpfNetMaps. */ public BpfNetMaps makeBpfNetMaps(Context ctx) { return new BpfNetMaps(ctx); } /** Create a new SkDestroyListener. */ public SkDestroyListener makeSkDestroyListener( IBpfMap cookieTagMap, Handler handler) { return new SkDestroyListener(cookieTagMap, handler, new SharedLog(TAG)); } } /** * Observer that watches for {@link INetdUnsolicitedEventListener} alerts. */ @VisibleForTesting public class AlertObserver extends BaseNetdUnsolicitedEventListener { @Override public void onQuotaLimitReached(@NonNull String alertName, @NonNull String ifName) { PermissionUtils.enforceNetworkStackPermission(mContext); if (LIMIT_GLOBAL_ALERT.equals(alertName)) { // kick off background poll to collect network stats unless there is already // such a call pending; UID stats are handled during normal polling interval. if (!mHandler.hasMessages(MSG_PERFORM_POLL_REGISTER_ALERT)) { mHandler.sendEmptyMessageDelayed(MSG_PERFORM_POLL_REGISTER_ALERT, mSettings.getPollDelay()); } } } } public void systemReady() { synchronized (mStatsLock) { mSystemReady = true; // create data recorders along with historical rotators mXtRecorder = buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false, mStatsDir, true /* wipeOnError */); mUidRecorder = buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false, mStatsDir, true /* wipeOnError */); mUidTagRecorder = buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true, mStatsDir, true /* wipeOnError */); updatePersistThresholdsLocked(); // upgrade any legacy stats maybeUpgradeLegacyStatsLocked(); // read historical network stats from disk, since policy service // might need them right away. mXtStatsCached = mXtRecorder.getOrLoadCompleteLocked(); // bootstrap initial stats to prevent double-counting later bootstrapStatsLocked(); } // watch for tethering changes final TetheringManager tetheringManager = mContext.getSystemService(TetheringManager.class); tetheringManager.registerTetheringEventCallback( (command) -> mHandler.post(command), mTetherListener); // listen for periodic polling events final IntentFilter pollFilter = new IntentFilter(ACTION_NETWORK_STATS_POLL); mContext.registerReceiver(mPollReceiver, pollFilter, READ_NETWORK_USAGE_HISTORY, mHandler); // listen for uid removal to clean stats final IntentFilter removedFilter = new IntentFilter(ACTION_UID_REMOVED); mContext.registerReceiver(mRemovedReceiver, removedFilter, null, mHandler); // listen for user changes to clean stats final IntentFilter userFilter = new IntentFilter(ACTION_USER_REMOVED); mContext.registerReceiver(mUserReceiver, userFilter, null, mHandler); // persist stats during clean shutdown final IntentFilter shutdownFilter = new IntentFilter(ACTION_SHUTDOWN); mContext.registerReceiver(mShutdownReceiver, shutdownFilter); try { mNetd.registerUnsolicitedEventListener(mAlertObserver); } catch (RemoteException | ServiceSpecificException e) { Log.wtf(TAG, "Error registering event listener :", e); } // schedule periodic pall alarm based on {@link NetworkStatsSettings#getPollInterval()}. final PendingIntent pollIntent = PendingIntent.getBroadcast(mContext, 0, new Intent(ACTION_NETWORK_STATS_POLL), PendingIntent.FLAG_IMMUTABLE); final long currentRealtime = SystemClock.elapsedRealtime(); mAlarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME, currentRealtime, mSettings.getPollInterval(), pollIntent); mContentResolver.registerContentObserver(Settings.Global .getUriFor(NETSTATS_COMBINE_SUBTYPE_ENABLED), false /* notifyForDescendants */, mContentObserver); // Post a runnable on handler thread to call onChange(). It's for getting current value of // NETSTATS_COMBINE_SUBTYPE_ENABLED to decide start or stop monitoring RAT type changes. mHandler.post(() -> mContentObserver.onChange(false, Settings.Global .getUriFor(NETSTATS_COMBINE_SUBTYPE_ENABLED))); registerGlobalAlert(); } private NetworkStatsRecorder buildRecorder( String prefix, NetworkStatsSettings.Config config, boolean includeTags, File baseDir, boolean wipeOnError) { final DropBoxManager dropBox = (DropBoxManager) mContext.getSystemService( Context.DROPBOX_SERVICE); return new NetworkStatsRecorder(new FileRotator( baseDir, prefix, config.rotateAgeMillis, config.deleteAgeMillis), mNonMonotonicObserver, dropBox, prefix, config.bucketDuration, includeTags, wipeOnError); } @GuardedBy("mStatsLock") private void shutdownLocked() { final TetheringManager tetheringManager = mContext.getSystemService(TetheringManager.class); try { tetheringManager.unregisterTetheringEventCallback(mTetherListener); } catch (IllegalStateException e) { Log.i(TAG, "shutdownLocked: error when unregister tethering, ignored. e=" + e); } mContext.unregisterReceiver(mPollReceiver); mContext.unregisterReceiver(mRemovedReceiver); mContext.unregisterReceiver(mUserReceiver); mContext.unregisterReceiver(mShutdownReceiver); if (!mSettings.getCombineSubtypeEnabled()) { mNetworkStatsSubscriptionsMonitor.stop(); } mContentResolver.unregisterContentObserver(mContentObserver); final long currentTime = mClock.millis(); // persist any pending stats mXtRecorder.forcePersistLocked(currentTime); mUidRecorder.forcePersistLocked(currentTime); mUidTagRecorder.forcePersistLocked(currentTime); mSystemReady = false; } private static class MigrationInfo { public final NetworkStatsRecorder recorder; public NetworkStatsCollection collection; public boolean imported; MigrationInfo(@NonNull final NetworkStatsRecorder recorder) { this.recorder = recorder; collection = null; imported = false; } } @GuardedBy("mStatsLock") private void maybeUpgradeLegacyStatsLocked() { final boolean storeFilesInApexData = mDeps.getStoreFilesInApexData(); if (!storeFilesInApexData) { return; } try { mImportLegacyAttemptsCounter = mDeps.createPersistentCounter(mStatsDir.toPath(), NETSTATS_IMPORT_ATTEMPTS_COUNTER_NAME); mImportLegacySuccessesCounter = mDeps.createPersistentCounter(mStatsDir.toPath(), NETSTATS_IMPORT_SUCCESSES_COUNTER_NAME); mImportLegacyFallbacksCounter = mDeps.createPersistentCounter(mStatsDir.toPath(), NETSTATS_IMPORT_FALLBACKS_COUNTER_NAME); } catch (IOException e) { Log.wtf(TAG, "Failed to create persistent counters, skip.", e); return; } final int targetAttempts = mDeps.getImportLegacyTargetAttempts(); final int attempts; final int fallbacks; final boolean runComparison; try { attempts = mImportLegacyAttemptsCounter.get(); // Fallbacks counter would be set to non-zero value to indicate the migration was // not successful. fallbacks = mImportLegacyFallbacksCounter.get(); runComparison = shouldRunComparison(); } catch (IOException e) { Log.wtf(TAG, "Failed to read counters, skip.", e); return; } // If the target number of attempts are reached, don't import any data. // However, if comparison is requested, still read the legacy data and compare // it to the importer output. This allows OEMs to debug issues with the // importer code and to collect signals from the field. final boolean dryRunImportOnly = fallbacks != 0 && runComparison && (attempts >= targetAttempts); // Return if target attempts are reached and there is no need to dry run. if (attempts >= targetAttempts && !dryRunImportOnly) return; if (dryRunImportOnly) { Log.i(TAG, "Starting import : only perform read"); } else { Log.i(TAG, "Starting import : attempts " + attempts + "/" + targetAttempts); } // Still create a legacy dev recorder locally but the service doesn't really use it. // This is for backward compatibility where the OEMs might call readPlatformCollection to // perform proprietary operations and relying on the side-effects to complete the follow-up // import process. final NetworkStatsSettings.Config devConfig = new NetworkStatsSettings.Config(HOUR_IN_MILLIS, 15 * DAY_IN_MILLIS, 90 * DAY_IN_MILLIS); final NetworkStatsRecorder devRecorder = buildRecorder(PREFIX_DEV, devConfig, false, mStatsDir, true /* wipeOnError */); final MigrationInfo[] migrations = new MigrationInfo[]{ new MigrationInfo(devRecorder), new MigrationInfo(mXtRecorder), new MigrationInfo(mUidRecorder), new MigrationInfo(mUidTagRecorder) }; // Legacy directories will be created by recorders if they do not exist final NetworkStatsRecorder[] legacyRecorders; if (runComparison) { final File legacyBaseDir = mDeps.getLegacyStatsDir(); // Set wipeOnError flag false so the recorder won't damage persistent data if reads // failed and calling deleteAll. // Set DEV legacy recorder as null since the DEV recorder has been removed. // Thus it doesn't need to build DEV legacy recorder for comparing with imported data. legacyRecorders = new NetworkStatsRecorder[]{ null /* dev Recorder */, buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false, legacyBaseDir, false /* wipeOnError */), buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false, legacyBaseDir, false /* wipeOnError */), buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true, legacyBaseDir, false /* wipeOnError */)}; } else { legacyRecorders = null; } long migrationEndTime = Long.MIN_VALUE; try { // First, read all legacy collections. This is OEM code and it can throw. Don't // commit any data to disk until all are read. for (int i = 0; i < migrations.length; i++) { final MigrationInfo migration = migrations[i]; // Read the collection from platform code, and set fallbacks counter if throws // for better debugging. try { migration.collection = readPlatformCollectionForRecorder(migration.recorder); } catch (Throwable e) { if (dryRunImportOnly) { Log.wtf(TAG, "Platform data read failed. ", e); return; } else { // Data is not imported successfully, set fallbacks counter to non-zero // value to trigger dry run every later boot when the runComparison is // true, in order to make it easier to debug issues. tryIncrementLegacyFallbacksCounter(); // Re-throw for error handling. This will increase attempts counter. throw e; } } if (runComparison) { final boolean success = compareImportedToLegacyStats(migration, legacyRecorders[i]); if (!success && !dryRunImportOnly) { tryIncrementLegacyFallbacksCounter(); } } } // For cases where the fallbacks are not zero but target attempts counts reached, // only perform reads above and return here. if (dryRunImportOnly) return; // Find the latest end time. for (final MigrationInfo migration : migrations) { if (PREFIX_DEV.equals(migration.recorder.getCookie())) continue; final long migrationEnd = migration.collection.getEndMillis(); if (migrationEnd > migrationEndTime) migrationEndTime = migrationEnd; } // Reading all collections from legacy data has succeeded. At this point it is // safe to start overwriting the files on disk. The next step is to remove all // data in the new location that overlaps with imported data. This ensures that // any data in the new location that was created by a previous failed import is // ignored. After that, write the imported data into the recorder. The code // below can still possibly throw (disk error or OutOfMemory for example), but // does not depend on code from non-mainline code. Log.i(TAG, "Rewriting data with imported collections with cutoff " + Instant.ofEpochMilli(migrationEndTime)); for (final MigrationInfo migration : migrations) { migration.imported = true; migration.recorder.removeDataBefore(migrationEndTime); if (migration.collection.isEmpty() || PREFIX_DEV.equals(migration.recorder.getCookie())) continue; migration.recorder.importCollectionLocked(migration.collection); } // Success normally or uses fallback method. } catch (Throwable e) { // The code above calls OEM code that may behave differently across devices. // It can throw any exception including RuntimeExceptions and // OutOfMemoryErrors. Try to recover anyway. Log.wtf(TAG, "Platform data import failed. Remaining tries " + (targetAttempts - attempts), e); // Failed this time around : try again next time unless we're out of tries. try { mImportLegacyAttemptsCounter.set(attempts + 1); } catch (IOException ex) { Log.wtf(TAG, "Failed to update attempts counter.", ex); } // Try to remove any data from the failed import. if (migrationEndTime > Long.MIN_VALUE) { try { for (final MigrationInfo migration : migrations) { if (PREFIX_DEV.equals(migration.recorder.getCookie())) continue; if (migration.imported) { migration.recorder.removeDataBefore(migrationEndTime); } } } catch (Throwable f) { // If rollback still throws, there isn't much left to do. Try nuking // all data, since that's the last stop. If nuking still throws, the // framework will reboot, and if there are remaining tries, the migration // process will retry, which is fine because it's idempotent. for (final MigrationInfo migration : migrations) { if (PREFIX_DEV.equals(migration.recorder.getCookie())) continue; migration.recorder.recoverAndDeleteData(); } } } return; } // Success ! No need to import again next time. try { mImportLegacyAttemptsCounter.set(targetAttempts); Log.i(TAG, "Successfully imported platform collections"); // The successes counter is only for debugging. Hence, the synchronization // between successes counter and attempts counter are not very critical. final int successCount = mImportLegacySuccessesCounter.get(); mImportLegacySuccessesCounter.set(successCount + 1); } catch (IOException e) { Log.wtf(TAG, "Succeed but failed to update counters.", e); } } void tryIncrementLegacyFallbacksCounter() { try { final int fallbacks = mImportLegacyFallbacksCounter.get(); mImportLegacyFallbacksCounter.set(fallbacks + 1); } catch (IOException e) { Log.wtf(TAG, "Failed to update fallback counter.", e); } } @VisibleForTesting boolean shouldRunComparison() { final ConnectivityResources resources = new ConnectivityResources(mContext); // 0 if id not found. Boolean overlayValue = null; try { switch (resources.get().getInteger(R.integer.config_netstats_validate_import)) { case 1: overlayValue = Boolean.TRUE; break; case 0: overlayValue = Boolean.FALSE; break; } } catch (Resources.NotFoundException e) { // Overlay value is not defined. } return overlayValue != null ? overlayValue : mDeps.isDebuggable(); } /** * Compare imported data with the data returned by legacy recorders. * * @return true if the data matches or if {@code legacyRecorder} is null, false if the data * does not match or throw with exceptions. */ private boolean compareImportedToLegacyStats(@NonNull MigrationInfo migration, @Nullable NetworkStatsRecorder legacyRecorder) { final NetworkStatsCollection legacyStats; // Skip the recorder that doesn't need to be compared. if (legacyRecorder == null) return true; try { legacyStats = legacyRecorder.getOrLoadCompleteLocked(); } catch (Throwable e) { Log.wtf(TAG, "Failed to read stats with legacy method for recorder " + legacyRecorder.getCookie(), e); // Cannot read data from legacy method, skip comparison. return false; } // The result of comparison is only for logging. try { final String error = compareStats(migration.collection, legacyStats); if (error != null) { Log.wtf(TAG, "Unexpected comparison result for recorder " + legacyRecorder.getCookie() + ": " + error); return false; } } catch (Throwable e) { Log.wtf(TAG, "Failed to compare migrated stats with legacy stats for recorder " + legacyRecorder.getCookie(), e); return false; } return true; } private static String str(NetworkStatsCollection.Key key) { StringBuilder sb = new StringBuilder() .append(key.ident.toString()) .append(" uid=").append(key.uid); if (key.set != SET_FOREGROUND) { sb.append(" set=").append(key.set); } if (key.tag != 0) { sb.append(" tag=").append(key.tag); } return sb.toString(); } // The importer will modify some keys when importing them. // In order to keep the comparison code simple, add such special cases here and simply // ignore them. This should not impact fidelity much because the start/end checks and the total // bytes check still need to pass. private static boolean couldKeyChangeOnImport(NetworkStatsCollection.Key key) { if (key.ident.isEmpty()) return false; final NetworkIdentity firstIdent = key.ident.iterator().next(); // Non-mobile network with non-empty RAT type. // This combination is invalid and the NetworkIdentity.Builder will throw if it is passed // in, but it looks like it was previously possible to persist it to disk. The importer sets // the RAT type to NETWORK_TYPE_ALL. if (firstIdent.getType() != ConnectivityManager.TYPE_MOBILE && firstIdent.getRatType() != NetworkTemplate.NETWORK_TYPE_ALL) { return true; } return false; } @Nullable private static String compareStats( NetworkStatsCollection migrated, NetworkStatsCollection legacy) { final Map migEntries = migrated.getEntries(); final Map legEntries = legacy.getEntries(); final ArraySet unmatchedLegKeys = new ArraySet<>(legEntries.keySet()); for (NetworkStatsCollection.Key legKey : legEntries.keySet()) { final NetworkStatsHistory legHistory = legEntries.get(legKey); final NetworkStatsHistory migHistory = migEntries.get(legKey); if (migHistory == null && couldKeyChangeOnImport(legKey)) { unmatchedLegKeys.remove(legKey); continue; } if (migHistory == null) { return "Missing migrated history for legacy key " + str(legKey) + ", legacy history was " + legHistory; } if (!migHistory.isSameAs(legHistory)) { return "Difference in history for key " + legKey + "; legacy history " + legHistory + ", migrated history " + migHistory; } unmatchedLegKeys.remove(legKey); } if (!unmatchedLegKeys.isEmpty()) { final NetworkStatsHistory first = legEntries.get(unmatchedLegKeys.valueAt(0)); return "Found unmatched legacy keys: count=" + unmatchedLegKeys.size() + ", first unmatched collection " + first; } if (migrated.getStartMillis() != legacy.getStartMillis() || migrated.getEndMillis() != legacy.getEndMillis()) { return "Start / end of the collections " + migrated.getStartMillis() + "/" + legacy.getStartMillis() + " and " + migrated.getEndMillis() + "/" + legacy.getEndMillis() + " don't match"; } if (migrated.getTotalBytes() != legacy.getTotalBytes()) { return "Total bytes " + migrated.getTotalBytes() + " and " + legacy.getTotalBytes() + " don't match for collections with start/end " + migrated.getStartMillis() + "/" + legacy.getStartMillis(); } return null; } @GuardedBy("mStatsLock") @NonNull private NetworkStatsCollection readPlatformCollectionForRecorder( @NonNull final NetworkStatsRecorder rec) throws IOException { final String prefix = rec.getCookie(); Log.i(TAG, "Importing platform collection for prefix " + prefix); final NetworkStatsCollection collection = Objects.requireNonNull( mDeps.readPlatformCollection(prefix, rec.getBucketDuration()), "Imported platform collection for prefix " + prefix + " must not be null"); final long bootTimestamp = System.currentTimeMillis() - SystemClock.elapsedRealtime(); if (!collection.isEmpty() && bootTimestamp < collection.getStartMillis()) { throw new IllegalArgumentException("Platform collection for prefix " + prefix + " contains data that could not possibly come from the previous boot " + "(start timestamp = " + Instant.ofEpochMilli(collection.getStartMillis()) + ", last booted at " + Instant.ofEpochMilli(bootTimestamp)); } Log.i(TAG, "Successfully read platform collection spanning from " // Instant uses ISO-8601 for toString() + Instant.ofEpochMilli(collection.getStartMillis()).toString() + " to " + Instant.ofEpochMilli(collection.getEndMillis()).toString()); return collection; } /** * Register for a global alert that is delivered through {@link AlertObserver} * or {@link NetworkStatsProviderCallback#onAlertReached()} once a threshold amount of data has * been transferred. */ private void registerGlobalAlert() { try { mNetd.bandwidthSetGlobalAlert(mGlobalAlertBytes); } catch (IllegalStateException e) { Log.w(TAG, "problem registering for global alert: " + e); } catch (RemoteException | ServiceSpecificException e) { // ignored; service lives in system_server } invokeForAllStatsProviderCallbacks((cb) -> cb.mProvider.onSetAlert(mGlobalAlertBytes)); } @Override public INetworkStatsSession openSession() { return openSessionInternal(NetworkStatsManager.FLAG_AUGMENT_WITH_SUBSCRIPTION_PLAN, null); } @Override public INetworkStatsSession openSessionForUsageStats(int flags, String callingPackage) { return openSessionInternal(flags, callingPackage); } private boolean isRateLimitedForPoll(@NonNull OpenSessionKey key) { final long lastCallTime; final long now = SystemClock.elapsedRealtime(); synchronized (mOpenSessionCallsLock) { Integer callsPerCaller = mOpenSessionCallsPerCaller.get(key); if (callsPerCaller == null) { mOpenSessionCallsPerCaller.put((key), 1); } else { mOpenSessionCallsPerCaller.put(key, Integer.sum(callsPerCaller, 1)); } if (key.uid == android.os.Process.SYSTEM_UID) { return false; } // To avoid a non-system user to be rate-limited after system users open sessions, // so update mLastStatsSessionPoll after checked if the uid is SYSTEM_UID. lastCallTime = mLastStatsSessionPoll; mLastStatsSessionPoll = now; } return now - lastCallTime < POLL_RATE_LIMIT_MS; } private int restrictFlagsForCaller(int flags, @NonNull String callingPackage) { // All non-privileged callers are not allowed to turn off POLL_ON_OPEN. final boolean isPrivileged = PermissionUtils.checkAnyPermissionOf(mContext, NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK); if (!isPrivileged) { flags |= NetworkStatsManager.FLAG_POLL_ON_OPEN; } // Non-system uids are rate limited for POLL_ON_OPEN. final int callingUid = Binder.getCallingUid(); final OpenSessionKey key = new OpenSessionKey(callingUid, callingPackage); flags = isRateLimitedForPoll(key) ? flags & (~NetworkStatsManager.FLAG_POLL_ON_OPEN) : flags; return flags; } private INetworkStatsSession openSessionInternal(final int flags, final String callingPackage) { final int restrictedFlags = restrictFlagsForCaller(flags, callingPackage); if ((restrictedFlags & (NetworkStatsManager.FLAG_POLL_ON_OPEN | NetworkStatsManager.FLAG_POLL_FORCE)) != 0) { final long ident = Binder.clearCallingIdentity(); try { performPoll(FLAG_PERSIST_ALL); } finally { Binder.restoreCallingIdentity(ident); } } // return an IBinder which holds strong references to any loaded stats // for its lifetime; when caller closes only weak references remain. return new INetworkStatsSession.Stub() { private final int mCallingUid = Binder.getCallingUid(); private final String mCallingPackage = callingPackage; private final @NetworkStatsAccess.Level int mAccessLevel = checkAccessLevel( callingPackage); private NetworkStatsCollection mUidComplete; private NetworkStatsCollection mUidTagComplete; private NetworkStatsCollection getUidComplete() { synchronized (mStatsLock) { if (mUidComplete == null) { mUidComplete = mUidRecorder.getOrLoadCompleteLocked(); } return mUidComplete; } } private NetworkStatsCollection getUidTagComplete() { synchronized (mStatsLock) { if (mUidTagComplete == null) { mUidTagComplete = mUidTagRecorder.getOrLoadCompleteLocked(); } return mUidTagComplete; } } @Override public int[] getRelevantUids() { return getUidComplete().getRelevantUids(mAccessLevel); } @Override public NetworkStats getDeviceSummaryForNetwork( NetworkTemplate template, long start, long end) { enforceTemplatePermissions(template, callingPackage); return internalGetSummaryForNetwork(template, restrictedFlags, start, end, mAccessLevel, mCallingUid); } @Override public NetworkStats getSummaryForNetwork( NetworkTemplate template, long start, long end) { enforceTemplatePermissions(template, callingPackage); return internalGetSummaryForNetwork(template, restrictedFlags, start, end, mAccessLevel, mCallingUid); } // TODO: Remove this after all callers are removed. @Override public NetworkStatsHistory getHistoryForNetwork(NetworkTemplate template, int fields) { enforceTemplatePermissions(template, callingPackage); return internalGetHistoryForNetwork(template, restrictedFlags, fields, mAccessLevel, mCallingUid, Long.MIN_VALUE, Long.MAX_VALUE); } @Override public NetworkStatsHistory getHistoryIntervalForNetwork(NetworkTemplate template, int fields, long start, long end) { enforceTemplatePermissions(template, callingPackage); // TODO(b/200768422): Redact returned history if the template is location // sensitive but the caller is not privileged. return internalGetHistoryForNetwork(template, restrictedFlags, fields, mAccessLevel, mCallingUid, start, end); } @Override public NetworkStats getSummaryForAllUid( NetworkTemplate template, long start, long end, boolean includeTags) { enforceTemplatePermissions(template, callingPackage); try { final NetworkStats stats = getUidComplete() .getSummary(template, start, end, mAccessLevel, mCallingUid); if (includeTags) { final NetworkStats tagStats = getUidTagComplete() .getSummary(template, start, end, mAccessLevel, mCallingUid); stats.combineAllValues(tagStats); } return stats; } catch (NullPointerException e) { throw e; } } @Override public NetworkStats getTaggedSummaryForAllUid( NetworkTemplate template, long start, long end) { enforceTemplatePermissions(template, callingPackage); try { final NetworkStats tagStats = getUidTagComplete() .getSummary(template, start, end, mAccessLevel, mCallingUid); return tagStats; } catch (NullPointerException e) { throw e; } } @Override public NetworkStatsHistory getHistoryForUid( NetworkTemplate template, int uid, int set, int tag, int fields) { enforceTemplatePermissions(template, callingPackage); // NOTE: We don't augment UID-level statistics if (tag == TAG_NONE) { return getUidComplete().getHistory(template, null, uid, set, tag, fields, Long.MIN_VALUE, Long.MAX_VALUE, mAccessLevel, mCallingUid); } else { return getUidTagComplete().getHistory(template, null, uid, set, tag, fields, Long.MIN_VALUE, Long.MAX_VALUE, mAccessLevel, mCallingUid); } } @Override public NetworkStatsHistory getHistoryIntervalForUid( NetworkTemplate template, int uid, int set, int tag, int fields, long start, long end) { enforceTemplatePermissions(template, callingPackage); // TODO(b/200768422): Redact returned history if the template is location // sensitive but the caller is not privileged. // NOTE: We don't augment UID-level statistics if (tag == TAG_NONE) { return getUidComplete().getHistory(template, null, uid, set, tag, fields, start, end, mAccessLevel, mCallingUid); } else if (uid == Binder.getCallingUid()) { return getUidTagComplete().getHistory(template, null, uid, set, tag, fields, start, end, mAccessLevel, mCallingUid); } else { throw new SecurityException("Calling package " + mCallingPackage + " cannot access tag information from a different uid"); } } @Override public void close() { mUidComplete = null; mUidTagComplete = null; } }; } private void enforceTemplatePermissions(@NonNull NetworkTemplate template, @NonNull String callingPackage) { // For a template with wifi network keys, it is possible for a malicious // client to track the user locations via querying data usage. Thus, enforce // fine location permission check. if (!template.getWifiNetworkKeys().isEmpty()) { final boolean canAccessFineLocation = mLocationPermissionChecker .checkCallersLocationPermission(callingPackage, null /* featureId */, Binder.getCallingUid(), false /* coarseForTargetSdkLessThanQ */, null /* message */); if (!canAccessFineLocation) { throw new SecurityException("Access fine location is required when querying" + " with wifi network keys, make sure the app has the necessary" + "permissions and the location toggle is on."); } } } private @NetworkStatsAccess.Level int checkAccessLevel(String callingPackage) { return NetworkStatsAccess.checkAccessLevel( mContext, Binder.getCallingPid(), Binder.getCallingUid(), callingPackage); } /** * Find the most relevant {@link SubscriptionPlan} for the given * {@link NetworkTemplate} and flags. This is typically used to augment * local measurement results to match a known anchor from the carrier. */ private SubscriptionPlan resolveSubscriptionPlan(NetworkTemplate template, int flags) { SubscriptionPlan plan = null; if ((flags & NetworkStatsManager.FLAG_AUGMENT_WITH_SUBSCRIPTION_PLAN) != 0 && mSettings.getAugmentEnabled()) { if (LOGD) Log.d(TAG, "Resolving plan for " + template); final long token = Binder.clearCallingIdentity(); try { plan = mContext.getSystemService(NetworkPolicyManager.class) .getSubscriptionPlan(template); } finally { Binder.restoreCallingIdentity(token); } if (LOGD) Log.d(TAG, "Resolved to plan " + plan); } return plan; } /** * Return network summary, splicing between DEV and XT stats when * appropriate. */ private NetworkStats internalGetSummaryForNetwork(NetworkTemplate template, int flags, long start, long end, @NetworkStatsAccess.Level int accessLevel, int callingUid) { // We've been using pure XT stats long enough that we no longer need to // splice DEV and XT together. final NetworkStatsHistory history = internalGetHistoryForNetwork(template, flags, FIELD_ALL, accessLevel, callingUid, Long.MIN_VALUE, Long.MAX_VALUE); final long now = mClock.millis(); final NetworkStatsHistory.Entry entry = history.getValues(start, end, now, null); final NetworkStats stats = new NetworkStats(end - start, 1); stats.insertEntry(new NetworkStats.Entry(IFACE_ALL, UID_ALL, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, entry.rxBytes, entry.rxPackets, entry.txBytes, entry.txPackets, entry.operations)); return stats; } /** * Return network history, splicing between DEV and XT stats when * appropriate. */ private NetworkStatsHistory internalGetHistoryForNetwork(NetworkTemplate template, int flags, int fields, @NetworkStatsAccess.Level int accessLevel, int callingUid, long start, long end) { // We've been using pure XT stats long enough that we no longer need to // splice DEV and XT together. final SubscriptionPlan augmentPlan = resolveSubscriptionPlan(template, flags); synchronized (mStatsLock) { return mXtStatsCached.getHistory(template, augmentPlan, UID_ALL, SET_ALL, TAG_NONE, fields, start, end, accessLevel, callingUid); } } private long getNetworkTotalBytes(NetworkTemplate template, long start, long end) { assertSystemReady(); return internalGetSummaryForNetwork(template, NetworkStatsManager.FLAG_AUGMENT_WITH_SUBSCRIPTION_PLAN, start, end, NetworkStatsAccess.Level.DEVICE, Binder.getCallingUid()).getTotalBytes(); } private NetworkStats getNetworkUidBytes(NetworkTemplate template, long start, long end) { assertSystemReady(); final NetworkStatsCollection uidComplete; synchronized (mStatsLock) { uidComplete = mUidRecorder.getOrLoadCompleteLocked(); } return uidComplete.getSummary(template, start, end, NetworkStatsAccess.Level.DEVICE, android.os.Process.SYSTEM_UID); } @Override public NetworkStats getDataLayerSnapshotForUid(int uid) throws RemoteException { if (Binder.getCallingUid() != uid) { Log.w(TAG, "Snapshots only available for calling UID"); return new NetworkStats(SystemClock.elapsedRealtime(), 0); } // TODO: switch to data layer stats once kernel exports // for now, read network layer stats and flatten across all ifaces. // This function is used to query NeworkStats for calle's uid. The only caller method // TrafficStats#getDataLayerSnapshotForUid alrady claim no special permission to query // its own NetworkStats. final long ident = Binder.clearCallingIdentity(); final NetworkStats networkLayer; try { networkLayer = readNetworkStatsUidDetail(uid, INTERFACES_ALL, TAG_ALL); } finally { Binder.restoreCallingIdentity(ident); } // splice in operation counts networkLayer.spliceOperationsFrom(mUidOperations); final NetworkStats dataLayer = new NetworkStats( networkLayer.getElapsedRealtime(), networkLayer.size()); NetworkStats.Entry entry = null; for (int i = 0; i < networkLayer.size(); i++) { entry = networkLayer.getValues(i, entry); entry.iface = IFACE_ALL; dataLayer.combineValues(entry); } return dataLayer; } private String[] getAllIfacesSinceBoot(int transport) { synchronized (mStatsLock) { final Set ifaceSet; if (transport == TRANSPORT_WIFI) { ifaceSet = mAllWifiIfacesSinceBoot; } else if (transport == TRANSPORT_CELLULAR) { ifaceSet = mAllMobileIfacesSinceBoot; } else { throw new IllegalArgumentException("Invalid transport " + transport); } return ifaceSet.toArray(new String[0]); } } @Override public NetworkStats getUidStatsForTransport(int transport) { PermissionUtils.enforceNetworkStackPermission(mContext); try { final String[] ifaceArray = getAllIfacesSinceBoot(transport); final NetworkStats stats = getNetworkStatsUidDetail(ifaceArray); // Clear the interfaces of the stats before returning, so callers won't get this // information. This is because no caller needs this information for now, and it // makes it easier to change the implementation later by using the histories in the // recorder. stats.clearInterfaces(); return stats; } catch (RemoteException e) { Log.wtf(TAG, "Error compiling UID stats", e); return new NetworkStats(0L, 0); } } @Override public String[] getMobileIfaces() { return mMobileIfaces.clone(); } @Override public void incrementOperationCount(int uid, int tag, int operationCount) { if (Binder.getCallingUid() != uid) { mContext.enforceCallingOrSelfPermission(UPDATE_DEVICE_STATS, TAG); } if (operationCount < 0) { throw new IllegalArgumentException("operation count can only be incremented"); } if (tag == TAG_NONE) { throw new IllegalArgumentException("operation count must have specific tag"); } synchronized (mStatsLock) { final int set = mActiveUidCounterSet.get(uid, SET_DEFAULT); mUidOperations.combineValues( mActiveIface, uid, set, tag, 0L, 0L, 0L, 0L, operationCount); mUidOperations.combineValues( mActiveIface, uid, set, TAG_NONE, 0L, 0L, 0L, 0L, operationCount); } } private void setKernelCounterSet(int uid, int set) { if (mUidCounterSetMap == null) { Log.wtf(TAG, "Fail to set UidCounterSet: Null bpf map"); return; } if (set == SET_DEFAULT) { try { mUidCounterSetMap.deleteEntry(new S32(uid)); } catch (ErrnoException e) { Log.w(TAG, "UidCounterSetMap.deleteEntry(" + uid + ") failed with errno: " + e); } return; } try { mUidCounterSetMap.updateEntry(new S32(uid), new U8((short) set)); } catch (ErrnoException e) { Log.w(TAG, "UidCounterSetMap.updateEntry(" + uid + ", " + set + ") failed with errno: " + e); } } @VisibleForTesting public void noteUidForeground(int uid, boolean uidForeground) { PermissionUtils.enforceNetworkStackPermission(mContext); synchronized (mStatsLock) { final int set = uidForeground ? SET_FOREGROUND : SET_DEFAULT; final int oldSet = mActiveUidCounterSet.get(uid, SET_DEFAULT); if (oldSet != set) { mActiveUidCounterSet.put(uid, set); setKernelCounterSet(uid, set); } } } /** * Notify {@code NetworkStatsService} about network status changed. */ public void notifyNetworkStatus( @NonNull Network[] defaultNetworks, @NonNull NetworkStateSnapshot[] networkStates, @Nullable String activeIface, @NonNull UnderlyingNetworkInfo[] underlyingNetworkInfos) { PermissionUtils.enforceNetworkStackPermission(mContext); final long token = Binder.clearCallingIdentity(); try { handleNotifyNetworkStatus(defaultNetworks, networkStates, activeIface); } finally { Binder.restoreCallingIdentity(token); } // Update the VPN underlying interfaces only after the poll is made and tun data has been // migrated. Otherwise the migration would use the new interfaces instead of the ones that // were current when the polled data was transferred. mStatsFactory.updateUnderlyingNetworkInfos(underlyingNetworkInfos); } @Override public void forceUpdate() { PermissionUtils.enforceNetworkStackPermission(mContext); final long token = Binder.clearCallingIdentity(); try { performPoll(FLAG_PERSIST_ALL); } finally { Binder.restoreCallingIdentity(token); } } /** Advise persistence threshold; may be overridden internally. */ public void advisePersistThreshold(long thresholdBytes) { PermissionUtils.enforceNetworkStackPermission(mContext); // clamp threshold into safe range mPersistThreshold = NetworkStatsUtils.constrain(thresholdBytes, 128 * KB_IN_BYTES, 2 * MB_IN_BYTES); if (LOGV) { Log.v(TAG, "advisePersistThreshold() given " + thresholdBytes + ", clamped to " + mPersistThreshold); } final long oldGlobalAlertBytes = mGlobalAlertBytes; // update and persist if beyond new thresholds final long currentTime = mClock.millis(); synchronized (mStatsLock) { if (!mSystemReady) return; updatePersistThresholdsLocked(); mXtRecorder.maybePersistLocked(currentTime); mUidRecorder.maybePersistLocked(currentTime); mUidTagRecorder.maybePersistLocked(currentTime); } if (oldGlobalAlertBytes != mGlobalAlertBytes) { registerGlobalAlert(); } } @Override public DataUsageRequest registerUsageCallback(@NonNull String callingPackage, @NonNull DataUsageRequest request, @NonNull IUsageCallback callback) { Objects.requireNonNull(callingPackage, "calling package is null"); Objects.requireNonNull(request, "DataUsageRequest is null"); Objects.requireNonNull(request.template, "NetworkTemplate is null"); Objects.requireNonNull(callback, "callback is null"); final int callingPid = Binder.getCallingPid(); final int callingUid = Binder.getCallingUid(); @NetworkStatsAccess.Level int accessLevel = checkAccessLevel(callingPackage); DataUsageRequest normalizedRequest; final long token = Binder.clearCallingIdentity(); try { normalizedRequest = mStatsObservers.register(mContext, request, callback, callingPid, callingUid, callingPackage, accessLevel); } finally { Binder.restoreCallingIdentity(token); } // Create baseline stats mHandler.sendMessage(mHandler.obtainMessage(MSG_PERFORM_POLL)); return normalizedRequest; } @Override public void unregisterUsageRequest(DataUsageRequest request) { Objects.requireNonNull(request, "DataUsageRequest is null"); int callingUid = Binder.getCallingUid(); final long token = Binder.clearCallingIdentity(); try { mStatsObservers.unregister(request, callingUid); } finally { Binder.restoreCallingIdentity(token); } } @Override public long getUidStats(int uid, int type) { final int callingUid = Binder.getCallingUid(); if (callingUid != android.os.Process.SYSTEM_UID && callingUid != uid) { return UNSUPPORTED; } return nativeGetUidStat(uid, type); } @Override public long getIfaceStats(@NonNull String iface, int type) { Objects.requireNonNull(iface); long nativeIfaceStats = nativeGetIfaceStat(iface, type); if (nativeIfaceStats == -1) { return nativeIfaceStats; } else { // When tethering offload is in use, nativeIfaceStats does not contain usage from // offload, add it back here. Note that the included statistics might be stale // since polling newest stats from hardware might impact system health and not // suitable for TrafficStats API use cases. return nativeIfaceStats + getProviderIfaceStats(iface, type); } } @Override public long getTotalStats(int type) { long nativeTotalStats = nativeGetTotalStat(type); if (nativeTotalStats == -1) { return nativeTotalStats; } else { // Refer to comment in getIfaceStats return nativeTotalStats + getProviderIfaceStats(IFACE_ALL, type); } } private long getProviderIfaceStats(@Nullable String iface, int type) { final NetworkStats providerSnapshot = getNetworkStatsFromProviders(STATS_PER_IFACE); final HashSet limitIfaces; if (iface == IFACE_ALL) { limitIfaces = null; } else { limitIfaces = new HashSet<>(); limitIfaces.add(iface); } final NetworkStats.Entry entry = providerSnapshot.getTotal(null, limitIfaces); switch (type) { case TrafficStats.TYPE_RX_BYTES: return entry.rxBytes; case TrafficStats.TYPE_RX_PACKETS: return entry.rxPackets; case TrafficStats.TYPE_TX_BYTES: return entry.txBytes; case TrafficStats.TYPE_TX_PACKETS: return entry.txPackets; default: return 0; } } /** * Update {@link NetworkStatsRecorder} and {@link #mGlobalAlertBytes} to * reflect current {@link #mPersistThreshold} value. Always defers to * {@link Global} values when defined. */ @GuardedBy("mStatsLock") private void updatePersistThresholdsLocked() { mXtRecorder.setPersistThreshold(mSettings.getXtPersistBytes(mPersistThreshold)); mUidRecorder.setPersistThreshold(mSettings.getUidPersistBytes(mPersistThreshold)); mUidTagRecorder.setPersistThreshold(mSettings.getUidTagPersistBytes(mPersistThreshold)); mGlobalAlertBytes = mSettings.getGlobalAlertBytes(mPersistThreshold); } /** * Listener that watches for {@link TetheringManager} to claim interface pairs. */ private final TetheringManager.TetheringEventCallback mTetherListener = new TetheringManager.TetheringEventCallback() { @Override public void onUpstreamChanged(@Nullable Network network) { performPoll(FLAG_PERSIST_NETWORK); } }; private BroadcastReceiver mPollReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { // on background handler thread, and verified UPDATE_DEVICE_STATS // permission above. performPoll(FLAG_PERSIST_ALL); // verify that we're watching global alert registerGlobalAlert(); } }; private BroadcastReceiver mRemovedReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { // on background handler thread, and UID_REMOVED is protected // broadcast. final int uid = intent.getIntExtra(EXTRA_UID, -1); if (uid == -1) return; synchronized (mStatsLock) { mWakeLock.acquire(); try { removeUidsLocked(uid); } finally { mWakeLock.release(); } } } }; private BroadcastReceiver mUserReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { // On background handler thread, and USER_REMOVED is protected // broadcast. final UserHandle userHandle = intent.getParcelableExtra(Intent.EXTRA_USER); if (userHandle == null) return; synchronized (mStatsLock) { mWakeLock.acquire(); try { removeUserLocked(userHandle); } finally { mWakeLock.release(); } } } }; private BroadcastReceiver mShutdownReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { // SHUTDOWN is protected broadcast. synchronized (mStatsLock) { shutdownLocked(); } } }; /** * Handle collapsed RAT type changed event. */ @VisibleForTesting public void handleOnCollapsedRatTypeChanged() { // Protect service from frequently updating. Remove pending messages if any. mHandler.removeMessages(MSG_NOTIFY_NETWORK_STATUS); mHandler.sendMessageDelayed( mHandler.obtainMessage(MSG_NOTIFY_NETWORK_STATUS), mSettings.getPollDelay()); } private void handleNotifyNetworkStatus( Network[] defaultNetworks, NetworkStateSnapshot[] snapshots, String activeIface) { synchronized (mStatsLock) { mWakeLock.acquire(); try { mActiveIface = activeIface; handleNotifyNetworkStatusLocked(defaultNetworks, snapshots); } finally { mWakeLock.release(); } } } /** * Inspect all current {@link NetworkStateSnapshot}s to derive mapping from {@code iface} to * {@link NetworkStatsHistory}. When multiple networks are active on a single {@code iface}, * they are combined under a single {@link NetworkIdentitySet}. */ @GuardedBy("mStatsLock") private void handleNotifyNetworkStatusLocked(@NonNull Network[] defaultNetworks, @NonNull NetworkStateSnapshot[] snapshots) { if (!mSystemReady) return; if (LOGV) Log.v(TAG, "handleNotifyNetworkStatusLocked()"); // take one last stats snapshot before updating iface mapping. this // isn't perfect, since the kernel may already be counting traffic from // the updated network. // poll, but only persist network stats to keep codepath fast. UID stats // will be persisted during next alarm poll event. performPollLocked(FLAG_PERSIST_NETWORK); // Rebuild active interfaces based on connected networks mActiveIfaces.clear(); mActiveUidIfaces.clear(); // Update the list of default networks. mDefaultNetworks = defaultNetworks; mLastNetworkStateSnapshots = snapshots; final boolean combineSubtypeEnabled = mSettings.getCombineSubtypeEnabled(); final ArraySet mobileIfaces = new ArraySet<>(); for (NetworkStateSnapshot snapshot : snapshots) { final int displayTransport = getDisplayTransport(snapshot.getNetworkCapabilities().getTransportTypes()); final boolean isMobile = (NetworkCapabilities.TRANSPORT_CELLULAR == displayTransport); final boolean isWifi = (NetworkCapabilities.TRANSPORT_WIFI == displayTransport); final boolean isDefault = CollectionUtils.contains( mDefaultNetworks, snapshot.getNetwork()); final int ratType = combineSubtypeEnabled ? NetworkTemplate.NETWORK_TYPE_ALL : getRatTypeForStateSnapshot(snapshot); final NetworkIdentity ident = NetworkIdentity.buildNetworkIdentity(mContext, snapshot, isDefault, ratType); // If WifiInfo contains a null network key then this identity should not be added into // the network identity set. See b/266598304. final TransportInfo transportInfo = snapshot.getNetworkCapabilities() .getTransportInfo(); if (transportInfo instanceof WifiInfo) { final WifiInfo info = (WifiInfo) transportInfo; if (info.getNetworkKey() == null) continue; } // Traffic occurring on the base interface is always counted for // both total usage and UID details. final String baseIface = snapshot.getLinkProperties().getInterfaceName(); if (baseIface != null) { findOrCreateNetworkIdentitySet(mActiveIfaces, baseIface).add(ident); findOrCreateNetworkIdentitySet(mActiveUidIfaces, baseIface).add(ident); // Build a separate virtual interface for VT (Video Telephony) data usage. // Only do this when IMS is not metered, but VT is metered. // If IMS is metered, then the IMS network usage has already included VT usage. // VT is considered always metered in framework's layer. If VT is not metered // per carrier's policy, modem will report 0 usage for VT calls. if (snapshot.getNetworkCapabilities().hasCapability( NetworkCapabilities.NET_CAPABILITY_IMS) && !ident.isMetered()) { // Copy the identify from IMS one but mark it as metered. NetworkIdentity vtIdent = new NetworkIdentity.Builder() .setType(ident.getType()) .setRatType(ident.getRatType()) .setSubscriberId(ident.getSubscriberId()) .setWifiNetworkKey(ident.getWifiNetworkKey()) .setRoaming(ident.isRoaming()).setMetered(true) .setDefaultNetwork(true) .setOemManaged(ident.getOemManaged()) .setSubId(ident.getSubId()).build(); final String ifaceVt = IFACE_VT + getSubIdForMobile(snapshot); findOrCreateNetworkIdentitySet(mActiveIfaces, ifaceVt).add(vtIdent); findOrCreateNetworkIdentitySet(mActiveUidIfaces, ifaceVt).add(vtIdent); } if (isMobile) { mobileIfaces.add(baseIface); // If the interface name was present in the wifi set, the interface won't // be removed from it to prevent stats from getting rollback. mAllMobileIfacesSinceBoot.add(baseIface); } if (isWifi) { mAllWifiIfacesSinceBoot.add(baseIface); } } // Traffic occurring on stacked interfaces is usually clatd. // // UID stats are always counted on the stacked interface and never on the base // interface, because the packets on the base interface do not actually match // application sockets (they're not IPv4) and thus the app uid is not known. // For receive this is obvious: packets must be translated from IPv6 to IPv4 // before the application socket can be found. // For transmit: either they go through the clat daemon which by virtue of going // through userspace strips the original socket association during the IPv4 to // IPv6 translation process, or they are offloaded by eBPF, which doesn't: // However, on an ebpf device the accounting is done in cgroup ebpf hooks, // which don't trigger again post ebpf translation. // (as such stats accounted to the clat uid are ignored) // // Interface stats are more complicated. // // eBPF offloaded 464xlat'ed packets never hit base interface ip6tables, and thus // *all* statistics are collected by iptables on the stacked v4-* interface. // // Additionally for ingress all packets bound for the clat IPv6 address are dropped // in ip6tables raw prerouting and thus even non-offloaded packets are only // accounted for on the stacked interface. // // For egress, packets subject to eBPF offload never appear on the base interface // and only appear on the stacked interface. Thus to ensure packets increment // interface stats, we must collate data from stacked interfaces. For xt_qtaguid // (or non eBPF offloaded) TX they would appear on both, however egress interface // accounting is explicitly bypassed for traffic from the clat uid. // // TODO: This code might be combined to above code. for (String iface : snapshot.getLinkProperties().getAllInterfaceNames()) { // baseIface has been handled, so ignore it. if (TextUtils.equals(baseIface, iface)) continue; if (iface != null) { findOrCreateNetworkIdentitySet(mActiveIfaces, iface).add(ident); findOrCreateNetworkIdentitySet(mActiveUidIfaces, iface).add(ident); if (isMobile) { mobileIfaces.add(iface); mAllMobileIfacesSinceBoot.add(iface); } if (isWifi) { mAllWifiIfacesSinceBoot.add(iface); } mStatsFactory.noteStackedIface(iface, baseIface); } } } mMobileIfaces = mobileIfaces.toArray(new String[0]); } private static int getSubIdForMobile(@NonNull NetworkStateSnapshot state) { if (!state.getNetworkCapabilities().hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { throw new IllegalArgumentException("Mobile state need capability TRANSPORT_CELLULAR"); } final NetworkSpecifier spec = state.getNetworkCapabilities().getNetworkSpecifier(); if (spec instanceof TelephonyNetworkSpecifier) { return ((TelephonyNetworkSpecifier) spec).getSubscriptionId(); } else { Log.wtf(TAG, "getSubIdForState invalid NetworkSpecifier"); return INVALID_SUBSCRIPTION_ID; } } /** * For networks with {@code TRANSPORT_CELLULAR}, get ratType that was obtained through * {@link PhoneStateListener}. Otherwise, return 0 given that other networks with different * transport types do not actually fill this value. */ private int getRatTypeForStateSnapshot(@NonNull NetworkStateSnapshot state) { if (!state.getNetworkCapabilities().hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { return 0; } return mNetworkStatsSubscriptionsMonitor.getRatTypeForSubscriberId(state.getSubscriberId()); } private static NetworkIdentitySet findOrCreateNetworkIdentitySet( ArrayMap map, K key) { NetworkIdentitySet ident = map.get(key); if (ident == null) { ident = new NetworkIdentitySet(); map.put(key, ident); } return ident; } @GuardedBy("mStatsLock") private void recordSnapshotLocked(long currentTime) throws RemoteException { // snapshot and record current counters; read UID stats first to // avoid over counting xt stats. Trace.traceBegin(TRACE_TAG_NETWORK, "snapshotUid"); final NetworkStats uidSnapshot = getNetworkStatsUidDetail(INTERFACES_ALL); Trace.traceEnd(TRACE_TAG_NETWORK); Trace.traceBegin(TRACE_TAG_NETWORK, "snapshotXt"); final NetworkStats xtSnapshot = readNetworkStatsSummaryXt(); Trace.traceEnd(TRACE_TAG_NETWORK); // Snapshot for xt stats from all custom stats providers. Counts per-interface data // from stats providers that isn't already counted by XT stats. Trace.traceBegin(TRACE_TAG_NETWORK, "snapshotStatsProvider"); final NetworkStats providersnapshot = getNetworkStatsFromProviders(STATS_PER_IFACE); Trace.traceEnd(TRACE_TAG_NETWORK); xtSnapshot.combineAllValues(providersnapshot); // For xt, we pass a null VPN array because usage is aggregated by UID, so VPN traffic // can't be reattributed to responsible apps. Trace.traceBegin(TRACE_TAG_NETWORK, "recordXt"); mXtRecorder.recordSnapshotLocked(xtSnapshot, mActiveIfaces, currentTime); Trace.traceEnd(TRACE_TAG_NETWORK); // For per-UID stats, pass the VPN info so VPN traffic is reattributed to responsible apps. Trace.traceBegin(TRACE_TAG_NETWORK, "recordUid"); mUidRecorder.recordSnapshotLocked(uidSnapshot, mActiveUidIfaces, currentTime); Trace.traceEnd(TRACE_TAG_NETWORK); Trace.traceBegin(TRACE_TAG_NETWORK, "recordUidTag"); mUidTagRecorder.recordSnapshotLocked(uidSnapshot, mActiveUidIfaces, currentTime); Trace.traceEnd(TRACE_TAG_NETWORK); // We need to make copies of member fields that are sent to the observer to avoid // a race condition between the service handler thread and the observer's mStatsObservers.updateStats(xtSnapshot, uidSnapshot, new ArrayMap<>(mActiveIfaces), new ArrayMap<>(mActiveUidIfaces), currentTime); } /** * Bootstrap initial stats snapshot, usually during {@link #systemReady()} * so we have baseline values without double-counting. */ @GuardedBy("mStatsLock") private void bootstrapStatsLocked() { final long currentTime = mClock.millis(); try { recordSnapshotLocked(currentTime); } catch (IllegalStateException e) { Log.w(TAG, "problem reading network stats: " + e); } catch (RemoteException e) { // ignored; service lives in system_server } } private void performPoll(int flags) { synchronized (mStatsLock) { mWakeLock.acquire(); try { performPollLocked(flags); } finally { mWakeLock.release(); } } } /** * Periodic poll operation, reading current statistics and recording into * {@link NetworkStatsHistory}. */ @GuardedBy("mStatsLock") private void performPollLocked(int flags) { if (!mSystemReady) return; if (LOGV) Log.v(TAG, "performPollLocked(flags=0x" + Integer.toHexString(flags) + ")"); Trace.traceBegin(TRACE_TAG_NETWORK, "performPollLocked"); final boolean persistNetwork = (flags & FLAG_PERSIST_NETWORK) != 0; final boolean persistUid = (flags & FLAG_PERSIST_UID) != 0; final boolean persistForce = (flags & FLAG_PERSIST_FORCE) != 0; performPollFromProvidersLocked(); // TODO: consider marking "untrusted" times in historical stats final long currentTime = mClock.millis(); try { recordSnapshotLocked(currentTime); } catch (IllegalStateException e) { Log.wtf(TAG, "problem reading network stats", e); return; } catch (RemoteException e) { // ignored; service lives in system_server return; } // persist any pending data depending on requested flags Trace.traceBegin(TRACE_TAG_NETWORK, "[persisting]"); if (persistForce) { mXtRecorder.forcePersistLocked(currentTime); mUidRecorder.forcePersistLocked(currentTime); mUidTagRecorder.forcePersistLocked(currentTime); } else { if (persistNetwork) { mXtRecorder.maybePersistLocked(currentTime); } if (persistUid) { mUidRecorder.maybePersistLocked(currentTime); mUidTagRecorder.maybePersistLocked(currentTime); } } Trace.traceEnd(TRACE_TAG_NETWORK); if (mSettings.getSampleEnabled()) { // sample stats after each full poll performSampleLocked(); } // finally, dispatch updated event to any listeners mHandler.sendMessage(mHandler.obtainMessage(MSG_BROADCAST_NETWORK_STATS_UPDATED)); Trace.traceEnd(TRACE_TAG_NETWORK); } @GuardedBy("mStatsLock") private void performPollFromProvidersLocked() { // Request asynchronous stats update from all providers for next poll. And wait a bit of // time to allow providers report-in given that normally binder call should be fast. Note // that size of list might be changed because addition/removing at the same time. For // addition, the stats of the missed provider can only be collected in next poll; // for removal, wait might take up to MAX_STATS_PROVIDER_POLL_WAIT_TIME_MS // once that happened. // TODO: request with a valid token. Trace.traceBegin(TRACE_TAG_NETWORK, "provider.requestStatsUpdate"); final int registeredCallbackCount = mStatsProviderCbList.size(); mStatsProviderSem.drainPermits(); invokeForAllStatsProviderCallbacks( (cb) -> cb.mProvider.onRequestStatsUpdate(0 /* unused */)); try { mStatsProviderSem.tryAcquire(registeredCallbackCount, MAX_STATS_PROVIDER_POLL_WAIT_TIME_MS, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { // Strictly speaking it's possible a provider happened to deliver between the timeout // and the log, and that doesn't matter too much as this is just a debug log. Log.d(TAG, "requestStatsUpdate - providers responded " + mStatsProviderSem.availablePermits() + "/" + registeredCallbackCount + " : " + e); } Trace.traceEnd(TRACE_TAG_NETWORK); } /** * Sample recent statistics summary into {@link EventLog}. */ @GuardedBy("mStatsLock") private void performSampleLocked() { // TODO: migrate trustedtime fixes to separate binary log events final long currentTime = mClock.millis(); NetworkTemplate template; NetworkStats.Entry xtTotal; NetworkStats.Entry uidTotal; // collect mobile sample template = new NetworkTemplate.Builder(MATCH_MOBILE).setMeteredness(METERED_YES).build(); xtTotal = mXtRecorder.getTotalSinceBootLocked(template); uidTotal = mUidRecorder.getTotalSinceBootLocked(template); if (SdkLevel.isAtLeastU()) { EventLog.writeEvent(LOG_TAG_NETSTATS_MOBILE_SAMPLE, xtTotal.rxBytes, xtTotal.txBytes, xtTotal.rxPackets, xtTotal.txPackets, uidTotal.rxBytes, uidTotal.txBytes, uidTotal.rxPackets, uidTotal.txPackets, currentTime); } else { // To keep the format of event log, here replaces the value of DevRecorder with the // value of XtRecorder because they have the same content in old design. EventLog.writeEvent(LOG_TAG_NETSTATS_MOBILE_SAMPLE, xtTotal.rxBytes, xtTotal.rxPackets, xtTotal.txBytes, xtTotal.txPackets, xtTotal.rxBytes, xtTotal.rxPackets, xtTotal.txBytes, xtTotal.txPackets, uidTotal.rxBytes, uidTotal.rxPackets, uidTotal.txBytes, uidTotal.txPackets, currentTime); } // collect wifi sample template = new NetworkTemplate.Builder(MATCH_WIFI).build(); xtTotal = mXtRecorder.getTotalSinceBootLocked(template); uidTotal = mUidRecorder.getTotalSinceBootLocked(template); if (SdkLevel.isAtLeastU()) { EventLog.writeEvent(LOG_TAG_NETSTATS_WIFI_SAMPLE, xtTotal.rxBytes, xtTotal.txBytes, xtTotal.rxPackets, xtTotal.txPackets, uidTotal.rxBytes, uidTotal.txBytes, uidTotal.rxPackets, uidTotal.txPackets, currentTime); } else { // To keep the format of event log, here replaces the value of DevRecorder with the // value of XtRecorder because they have the same content in old design. EventLog.writeEvent(LOG_TAG_NETSTATS_WIFI_SAMPLE, xtTotal.rxBytes, xtTotal.rxPackets, xtTotal.txBytes, xtTotal.txPackets, xtTotal.rxBytes, xtTotal.rxPackets, xtTotal.txBytes, xtTotal.txPackets, uidTotal.rxBytes, uidTotal.rxPackets, uidTotal.txBytes, uidTotal.txPackets, currentTime); } } // deleteKernelTagData can ignore ENOENT; otherwise we should log an error private void logErrorIfNotErrNoent(final ErrnoException e, final String msg) { if (e.errno != ENOENT) Log.e(TAG, msg, e); } private void deleteStatsMapTagData( IBpfMap statsMap, int uid) { try { statsMap.forEach((key, value) -> { if (key.uid == uid) { try { statsMap.deleteEntry(key); } catch (ErrnoException e) { logErrorIfNotErrNoent(e, "Failed to delete data(uid = " + key.uid + ")"); } } }); } catch (ErrnoException e) { Log.e(TAG, "FAILED to delete tag data from stats map", e); } } /** * Deletes uid tag data from CookieTagMap, StatsMapA, StatsMapB, and UidStatsMap * @param uid */ private void deleteKernelTagData(int uid) { try { mCookieTagMap.forEach((key, value) -> { // If SkDestroyListener deletes the socket tag while this code is running, // forEach will either restart iteration from the beginning or return null, // depending on when the deletion happens. // If it returns null, continue iteration to delete the data and in fact it would // just iterate from first key because BpfMap#getNextKey would return first key // if the current key is not exist. if (value != null && value.uid == uid) { try { mCookieTagMap.deleteEntry(key); } catch (ErrnoException e) { logErrorIfNotErrNoent(e, "Failed to delete data(cookie = " + key + ")"); } } }); } catch (ErrnoException e) { Log.e(TAG, "Failed to delete tag data from cookie tag map", e); } deleteStatsMapTagData(mStatsMapA, uid); deleteStatsMapTagData(mStatsMapB, uid); try { mUidCounterSetMap.deleteEntry(new S32(uid)); } catch (ErrnoException e) { logErrorIfNotErrNoent(e, "Failed to delete tag data from uid counter set map"); } try { mAppUidStatsMap.deleteEntry(new UidStatsMapKey(uid)); } catch (ErrnoException e) { logErrorIfNotErrNoent(e, "Failed to delete tag data from app uid stats map"); } } /** * Clean up {@link #mUidRecorder} after UID is removed. */ @GuardedBy("mStatsLock") private void removeUidsLocked(int... uids) { if (LOGV) Log.v(TAG, "removeUidsLocked() for UIDs " + Arrays.toString(uids)); // Perform one last poll before removing performPollLocked(FLAG_PERSIST_ALL); mUidRecorder.removeUidsLocked(uids); mUidTagRecorder.removeUidsLocked(uids); mStatsFactory.removeUidsLocked(uids); // Clear kernel stats associated with UID for (int uid : uids) { deleteKernelTagData(uid); } // TODO: Remove the UID's entries from mOpenSessionCallsPerCaller. } /** * Clean up {@link #mUidRecorder} after user is removed. */ @GuardedBy("mStatsLock") private void removeUserLocked(@NonNull UserHandle userHandle) { if (LOGV) Log.v(TAG, "removeUserLocked() for UserHandle=" + userHandle); // Build list of UIDs that we should clean up final ArrayList uids = new ArrayList<>(); final List apps = mContext.getPackageManager().getInstalledApplications( PackageManager.MATCH_ANY_USER | PackageManager.MATCH_DISABLED_COMPONENTS); for (ApplicationInfo app : apps) { final int uid = userHandle.getUid(app.uid); uids.add(uid); } removeUidsLocked(CollectionUtils.toIntArray(uids)); } /** * Set the warning and limit to all registered custom network stats providers. * Note that invocation of any interface will be sent to all providers. */ public void setStatsProviderWarningAndLimitAsync( @NonNull String iface, long warning, long limit) { PermissionUtils.enforceNetworkStackPermission(mContext); if (LOGV) { Log.v(TAG, "setStatsProviderWarningAndLimitAsync(" + iface + "," + warning + "," + limit + ")"); } invokeForAllStatsProviderCallbacks((cb) -> cb.mProvider.onSetWarningAndLimit(iface, warning, limit)); } @Override protected void dump(FileDescriptor fd, PrintWriter rawWriter, String[] args) { if (!PermissionUtils.checkDumpPermission(mContext, TAG, rawWriter)) return; long duration = DateUtils.DAY_IN_MILLIS; final HashSet argSet = new HashSet(); for (String arg : args) { argSet.add(arg); if (arg.startsWith("--duration=")) { try { duration = Long.parseLong(arg.substring(11)); } catch (NumberFormatException ignored) { } } } // usage: dumpsys netstats --full --uid --tag --poll --checkin final boolean poll = argSet.contains("--poll") || argSet.contains("poll"); final boolean checkin = argSet.contains("--checkin"); final boolean bpfRawMap = argSet.contains("--bpfRawMap"); final boolean fullHistory = argSet.contains("--full") || argSet.contains("full"); final boolean includeUid = argSet.contains("--uid") || argSet.contains("detail"); final boolean includeTag = argSet.contains("--tag") || argSet.contains("detail"); final IndentingPrintWriter pw = new IndentingPrintWriter(rawWriter, " "); synchronized (mStatsLock) { if (args.length > 0 && "--proto".equals(args[0])) { // In this case ignore all other arguments. dumpProtoLocked(fd); return; } if (poll) { performPollLocked(FLAG_PERSIST_ALL | FLAG_PERSIST_FORCE); pw.println("Forced poll"); return; } if (checkin) { final long end = System.currentTimeMillis(); final long start = end - duration; pw.print("v1,"); pw.print(start / SECOND_IN_MILLIS); pw.print(','); pw.print(end / SECOND_IN_MILLIS); pw.println(); pw.println("xt"); mXtRecorder.dumpCheckin(rawWriter, start, end); if (includeUid) { pw.println("uid"); mUidRecorder.dumpCheckin(rawWriter, start, end); } if (includeTag) { pw.println("tag"); mUidTagRecorder.dumpCheckin(rawWriter, start, end); } return; } if (bpfRawMap) { dumpRawMapLocked(pw, args); return; } pw.println("Directory:"); pw.increaseIndent(); pw.println(mStatsDir); pw.decreaseIndent(); pw.println("Configs:"); pw.increaseIndent(); pw.print(NETSTATS_COMBINE_SUBTYPE_ENABLED, mSettings.getCombineSubtypeEnabled()); pw.println(); pw.print(NETSTATS_STORE_FILES_IN_APEXDATA, mDeps.getStoreFilesInApexData()); pw.println(); pw.print(NETSTATS_IMPORT_LEGACY_TARGET_ATTEMPTS, mDeps.getImportLegacyTargetAttempts()); pw.println(); if (mDeps.getStoreFilesInApexData()) { try { pw.print("platform legacy stats import attempts count", mImportLegacyAttemptsCounter.get()); pw.println(); pw.print("platform legacy stats import successes count", mImportLegacySuccessesCounter.get()); pw.println(); pw.print("platform legacy stats import fallbacks count", mImportLegacyFallbacksCounter.get()); pw.println(); } catch (IOException e) { pw.println("(failed to dump platform legacy stats import counters)"); } } pw.decreaseIndent(); pw.println("Active interfaces:"); pw.increaseIndent(); for (int i = 0; i < mActiveIfaces.size(); i++) { pw.print("iface", mActiveIfaces.keyAt(i)); pw.print("ident", mActiveIfaces.valueAt(i)); pw.println(); } pw.decreaseIndent(); pw.println("Active UID interfaces:"); pw.increaseIndent(); for (int i = 0; i < mActiveUidIfaces.size(); i++) { pw.print("iface", mActiveUidIfaces.keyAt(i)); pw.print("ident", mActiveUidIfaces.valueAt(i)); pw.println(); } pw.decreaseIndent(); pw.println("All wifi interfaces:"); pw.increaseIndent(); for (String iface : mAllWifiIfacesSinceBoot) { pw.print(iface + " "); } pw.println(); pw.decreaseIndent(); pw.println("All mobile interfaces:"); pw.increaseIndent(); for (String iface : mAllMobileIfacesSinceBoot) { pw.print(iface + " "); } pw.println(); pw.decreaseIndent(); // Get the top openSession callers final HashMap calls; synchronized (mOpenSessionCallsLock) { calls = new HashMap<>(mOpenSessionCallsPerCaller); } final List> list = new ArrayList<>(calls.entrySet()); Collections.sort(list, (left, right) -> Integer.compare(left.getValue(), right.getValue())); final int num = list.size(); final int end = Math.max(0, num - DUMP_STATS_SESSION_COUNT); pw.println("Top openSession callers:"); pw.increaseIndent(); for (int j = num - 1; j >= end; j--) { final Map.Entry entry = list.get(j); pw.print(entry.getKey()); pw.print("="); pw.println(entry.getValue()); } pw.decreaseIndent(); pw.println(); pw.println("Stats Providers:"); pw.increaseIndent(); invokeForAllStatsProviderCallbacks((cb) -> { pw.println(cb.mTag + " Xt:"); pw.increaseIndent(); pw.print(cb.getCachedStats(STATS_PER_IFACE).toString()); pw.decreaseIndent(); if (includeUid) { pw.println(cb.mTag + " Uid:"); pw.increaseIndent(); pw.print(cb.getCachedStats(STATS_PER_UID).toString()); pw.decreaseIndent(); } }); pw.decreaseIndent(); pw.println(); pw.println("Stats Observers:"); pw.increaseIndent(); mStatsObservers.dump(pw); pw.decreaseIndent(); pw.println(); pw.println("Dev stats:"); pw.increaseIndent(); pw.println("Pending bytes: "); if (fullHistory) { pw.println("Complete history:"); } else { pw.println("History since boot:"); } pw.decreaseIndent(); pw.println("Xt stats:"); pw.increaseIndent(); mXtRecorder.dumpLocked(pw, fullHistory); pw.decreaseIndent(); if (includeUid) { pw.println("UID stats:"); pw.increaseIndent(); mUidRecorder.dumpLocked(pw, fullHistory); pw.decreaseIndent(); } if (includeTag) { pw.println("UID tag stats:"); pw.increaseIndent(); mUidTagRecorder.dumpLocked(pw, fullHistory); pw.decreaseIndent(); } pw.println(); pw.println("InterfaceMapUpdater:"); pw.increaseIndent(); mInterfaceMapUpdater.dump(pw); pw.decreaseIndent(); pw.println(); pw.println("BPF map status:"); pw.increaseIndent(); dumpMapStatus(pw); pw.decreaseIndent(); pw.println(); // Following BPF map content dump contains uid and tag regardless of the flags because // following dumps are moved from TrafficController and bug report already contains this // information. pw.println("BPF map content:"); pw.increaseIndent(); dumpCookieTagMapLocked(pw); dumpUidCounterSetMapLocked(pw); dumpAppUidStatsMapLocked(pw); dumpStatsMapLocked(mStatsMapA, pw, "mStatsMapA"); dumpStatsMapLocked(mStatsMapB, pw, "mStatsMapB"); dumpIfaceStatsMapLocked(pw); pw.decreaseIndent(); } } @GuardedBy("mStatsLock") private void dumpProtoLocked(FileDescriptor fd) { final ProtoOutputStream proto = new ProtoOutputStream(new FileOutputStream(fd)); // TODO Right now it writes all history. Should it limit to the "since-boot" log? dumpInterfaces(proto, NetworkStatsServiceDumpProto.ACTIVE_INTERFACES, mActiveIfaces); dumpInterfaces(proto, NetworkStatsServiceDumpProto.ACTIVE_UID_INTERFACES, mActiveUidIfaces); mXtRecorder.dumpDebugLocked(proto, NetworkStatsServiceDumpProto.XT_STATS); mUidRecorder.dumpDebugLocked(proto, NetworkStatsServiceDumpProto.UID_STATS); mUidTagRecorder.dumpDebugLocked(proto, NetworkStatsServiceDumpProto.UID_TAG_STATS); proto.flush(); } private void dumpRawMap(IBpfMap map, IndentingPrintWriter pw) throws ErrnoException { if (map == null) { pw.println("Map is null"); return; } if (map.isEmpty()) { pw.println(""); return; } // If there is a concurrent entry deletion, value could be null. http://b/220084230. // Also, map.forEach could restart iteration from the beginning and dump could contain // duplicated entries. User of this dump needs to take care of the duplicated entries. map.forEach((k, v) -> { if (v != null) { pw.println(BpfDump.toBase64EncodedString(k, v)); } }); } @GuardedBy("mStatsLock") private void dumpRawMapLocked(final IndentingPrintWriter pw, final String[] args) { if (CollectionUtils.contains(args, "--cookieTagMap")) { try { dumpRawMap(mCookieTagMap, pw); } catch (ErrnoException e) { pw.println("Error dumping cookieTag map: " + e); } return; } } private static void dumpInterfaces(ProtoOutputStream proto, long tag, ArrayMap ifaces) { for (int i = 0; i < ifaces.size(); i++) { final long start = proto.start(tag); proto.write(NetworkInterfaceProto.INTERFACE, ifaces.keyAt(i)); ifaces.valueAt(i).dumpDebug(proto, NetworkInterfaceProto.IDENTITIES); proto.end(start); } } private void dumpMapStatus(final IndentingPrintWriter pw) { BpfDump.dumpMapStatus(mCookieTagMap, pw, "mCookieTagMap", COOKIE_TAG_MAP_PATH); BpfDump.dumpMapStatus(mUidCounterSetMap, pw, "mUidCounterSetMap", UID_COUNTERSET_MAP_PATH); BpfDump.dumpMapStatus(mAppUidStatsMap, pw, "mAppUidStatsMap", APP_UID_STATS_MAP_PATH); BpfDump.dumpMapStatus(mStatsMapA, pw, "mStatsMapA", STATS_MAP_A_PATH); BpfDump.dumpMapStatus(mStatsMapB, pw, "mStatsMapB", STATS_MAP_B_PATH); // mIfaceStatsMap is always not null but dump status to be consistent with other maps. BpfDump.dumpMapStatus(mIfaceStatsMap, pw, "mIfaceStatsMap", IFACE_STATS_MAP_PATH); } @GuardedBy("mStatsLock") private void dumpCookieTagMapLocked(final IndentingPrintWriter pw) { if (mCookieTagMap == null) { return; } BpfDump.dumpMap(mCookieTagMap, pw, "mCookieTagMap", (key, value) -> "cookie=" + key.socketCookie + " tag=0x" + Long.toHexString(value.tag) + " uid=" + value.uid); } @GuardedBy("mStatsLock") private void dumpUidCounterSetMapLocked(final IndentingPrintWriter pw) { if (mUidCounterSetMap == null) { return; } BpfDump.dumpMap(mUidCounterSetMap, pw, "mUidCounterSetMap", (uid, set) -> "uid=" + uid.val + " set=" + set.val); } @GuardedBy("mStatsLock") private void dumpAppUidStatsMapLocked(final IndentingPrintWriter pw) { if (mAppUidStatsMap == null) { return; } BpfDump.dumpMap(mAppUidStatsMap, pw, "mAppUidStatsMap", "uid rxBytes rxPackets txBytes txPackets", (key, value) -> key.uid + " " + value.rxBytes + " " + value.rxPackets + " " + value.txBytes + " " + value.txPackets); } @GuardedBy("mStatsLock") private void dumpStatsMapLocked(final IBpfMap statsMap, final IndentingPrintWriter pw, final String mapName) { if (statsMap == null) { return; } BpfDump.dumpMap(statsMap, pw, mapName, "ifaceIndex ifaceName tag_hex uid_int cnt_set rxBytes rxPackets txBytes txPackets", (key, value) -> { final String ifName = mInterfaceMapUpdater.getIfNameByIndex(key.ifaceIndex); return key.ifaceIndex + " " + (ifName != null ? ifName : "unknown") + " " + "0x" + Long.toHexString(key.tag) + " " + key.uid + " " + key.counterSet + " " + value.rxBytes + " " + value.rxPackets + " " + value.txBytes + " " + value.txPackets; }); } @GuardedBy("mStatsLock") private void dumpIfaceStatsMapLocked(final IndentingPrintWriter pw) { BpfDump.dumpMap(mIfaceStatsMap, pw, "mIfaceStatsMap", "ifaceIndex ifaceName rxBytes rxPackets txBytes txPackets", (key, value) -> { final String ifName = mInterfaceMapUpdater.getIfNameByIndex(key.val); return key.val + " " + (ifName != null ? ifName : "unknown") + " " + value.rxBytes + " " + value.rxPackets + " " + value.txBytes + " " + value.txPackets; }); } private NetworkStats readNetworkStatsSummaryXt() { try { return mStatsFactory.readNetworkStatsSummaryXt(); } catch (IOException e) { throw new IllegalStateException(e); } } private NetworkStats readNetworkStatsUidDetail(int uid, String[] ifaces, int tag) { try { return mStatsFactory.readNetworkStatsDetail(uid, ifaces, tag); } catch (IOException e) { throw new IllegalStateException(e); } } /** * 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(String[] ifaces) throws RemoteException { final NetworkStats uidSnapshot = readNetworkStatsUidDetail(UID_ALL, ifaces, TAG_ALL); // fold tethering stats and operations into uid snapshot final NetworkStats tetherSnapshot = getNetworkStatsTethering(STATS_PER_UID); tetherSnapshot.filter(UID_ALL, ifaces, TAG_ALL); mStatsFactory.apply464xlatAdjustments(uidSnapshot, tetherSnapshot); uidSnapshot.combineAllValues(tetherSnapshot); // get a stale copy of uid stats snapshot provided by providers. final NetworkStats providerStats = getNetworkStatsFromProviders(STATS_PER_UID); providerStats.filter(UID_ALL, ifaces, TAG_ALL); mStatsFactory.apply464xlatAdjustments(uidSnapshot, providerStats); uidSnapshot.combineAllValues(providerStats); uidSnapshot.combineAllValues(mUidOperations); uidSnapshot.filter(UID_ALL, ifaces, TAG_ALL); return uidSnapshot; } /** * Return snapshot of current non-offloaded tethering statistics. Will return empty * {@link NetworkStats} if any problems are encountered, or queried by {@code STATS_PER_IFACE} * since it is already included by {@link #nativeGetIfaceStat}. * See {@code OffloadTetheringStatsProvider} for offloaded tethering stats. */ // TODO: Remove this by implementing {@link NetworkStatsProvider} for non-offloaded // tethering stats. private @NonNull NetworkStats getNetworkStatsTethering(int how) throws RemoteException { // We only need to return per-UID stats. Per-device stats are already counted by // interface counters. if (how != STATS_PER_UID) { return new NetworkStats(SystemClock.elapsedRealtime(), 0); } final NetworkStats stats = new NetworkStats(SystemClock.elapsedRealtime(), 1); try { final TetherStatsParcel[] tetherStatsParcels = mNetd.tetherGetStats(); for (TetherStatsParcel tetherStats : tetherStatsParcels) { try { stats.combineValues(new NetworkStats.Entry(tetherStats.iface, UID_TETHERING, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, tetherStats.rxBytes, tetherStats.rxPackets, tetherStats.txBytes, tetherStats.txPackets, 0L)); } catch (ArrayIndexOutOfBoundsException e) { throw new IllegalStateException("invalid tethering stats " + e); } } } catch (IllegalStateException | ServiceSpecificException e) { Log.wtf(TAG, "problem reading network stats", e); } return stats; } /** * Registers a custom provider of {@link android.net.NetworkStats} to combine the network * statistics that cannot be seen by the kernel to system. To unregister, invoke the * {@code unregister()} of the returned callback. * * @param tag a human readable identifier of the custom network stats provider. * @param provider the {@link INetworkStatsProvider} binder corresponding to the * {@link NetworkStatsProvider} to be registered. * * @return a {@link INetworkStatsProviderCallback} binder * interface, which can be used to report events to the system. */ public @NonNull INetworkStatsProviderCallback registerNetworkStatsProvider( @NonNull String tag, @NonNull INetworkStatsProvider provider) { PermissionUtils.enforceAnyPermissionOf(mContext, NETWORK_STATS_PROVIDER, NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK); Objects.requireNonNull(provider, "provider is null"); Objects.requireNonNull(tag, "tag is null"); final NetworkPolicyManager netPolicyManager = mContext .getSystemService(NetworkPolicyManager.class); try { NetworkStatsProviderCallbackImpl callback = new NetworkStatsProviderCallbackImpl( tag, provider, mStatsProviderSem, mAlertObserver, mStatsProviderCbList, netPolicyManager); mStatsProviderCbList.add(callback); Log.d(TAG, "registerNetworkStatsProvider from " + callback.mTag + " uid/pid=" + getCallingUid() + "/" + getCallingPid()); return callback; } catch (RemoteException e) { Log.e(TAG, "registerNetworkStatsProvider failed", e); } return null; } // Collect stats from local cache of providers. private @NonNull NetworkStats getNetworkStatsFromProviders(int how) { final NetworkStats ret = new NetworkStats(0L, 0); invokeForAllStatsProviderCallbacks((cb) -> ret.combineAllValues(cb.getCachedStats(how))); return ret; } @FunctionalInterface private interface ThrowingConsumer { void accept(S s) throws T; } private void invokeForAllStatsProviderCallbacks( @NonNull ThrowingConsumer task) { for (final NetworkStatsProviderCallbackImpl cb : mStatsProviderCbList) { try { task.accept(cb); } catch (RemoteException e) { Log.e(TAG, "Fail to broadcast to provider: " + cb.mTag, e); } } } private static class NetworkStatsProviderCallbackImpl extends INetworkStatsProviderCallback.Stub implements IBinder.DeathRecipient { @NonNull final String mTag; @NonNull final INetworkStatsProvider mProvider; @NonNull private final Semaphore mSemaphore; @NonNull final AlertObserver mAlertObserver; @NonNull final CopyOnWriteArrayList mStatsProviderCbList; @NonNull final NetworkPolicyManager mNetworkPolicyManager; @NonNull private final Object mProviderStatsLock = new Object(); @GuardedBy("mProviderStatsLock") // Track STATS_PER_IFACE and STATS_PER_UID separately. private final NetworkStats mIfaceStats = new NetworkStats(0L, 0); @GuardedBy("mProviderStatsLock") private final NetworkStats mUidStats = new NetworkStats(0L, 0); NetworkStatsProviderCallbackImpl( @NonNull String tag, @NonNull INetworkStatsProvider provider, @NonNull Semaphore semaphore, @NonNull AlertObserver alertObserver, @NonNull CopyOnWriteArrayList cbList, @NonNull NetworkPolicyManager networkPolicyManager) throws RemoteException { mTag = tag; mProvider = provider; mProvider.asBinder().linkToDeath(this, 0); mSemaphore = semaphore; mAlertObserver = alertObserver; mStatsProviderCbList = cbList; mNetworkPolicyManager = networkPolicyManager; } @NonNull public NetworkStats getCachedStats(int how) { synchronized (mProviderStatsLock) { NetworkStats stats; switch (how) { case STATS_PER_IFACE: stats = mIfaceStats; break; case STATS_PER_UID: stats = mUidStats; break; default: throw new IllegalArgumentException("Invalid type: " + how); } // Callers might be able to mutate the returned object. Return a defensive copy // instead of local reference. return stats.clone(); } } @Override public void notifyStatsUpdated(int token, @Nullable NetworkStats ifaceStats, @Nullable NetworkStats uidStats) { // TODO: 1. Use token to map ifaces to correct NetworkIdentity. // 2. Store the difference and store it directly to the recorder. synchronized (mProviderStatsLock) { if (ifaceStats != null) mIfaceStats.combineAllValues(ifaceStats); if (uidStats != null) mUidStats.combineAllValues(uidStats); } mSemaphore.release(); } @Override public void notifyAlertReached() throws RemoteException { // This binder object can only have been obtained by a process that holds // NETWORK_STATS_PROVIDER. Thus, no additional permission check is required. BinderUtils.withCleanCallingIdentity(() -> mAlertObserver.onQuotaLimitReached(LIMIT_GLOBAL_ALERT, null /* unused */)); } @Override public void notifyWarningReached() { Log.d(TAG, mTag + ": notifyWarningReached"); BinderUtils.withCleanCallingIdentity(() -> mNetworkPolicyManager.notifyStatsProviderWarningReached()); } @Override public void notifyLimitReached() { Log.d(TAG, mTag + ": notifyLimitReached"); BinderUtils.withCleanCallingIdentity(() -> mNetworkPolicyManager.notifyStatsProviderLimitReached()); } @Override public void binderDied() { Log.d(TAG, mTag + ": binderDied"); mStatsProviderCbList.remove(this); } @Override public void unregister() { Log.d(TAG, mTag + ": unregister"); mStatsProviderCbList.remove(this); } } private void assertSystemReady() { if (!mSystemReady) { throw new IllegalStateException("System not ready"); } } private class DropBoxNonMonotonicObserver implements NonMonotonicObserver { @Override public void foundNonMonotonic(NetworkStats left, int leftIndex, NetworkStats right, int rightIndex, String cookie) { Log.w(TAG, "Found non-monotonic values; saving to dropbox"); // record error for debugging final StringBuilder builder = new StringBuilder(); builder.append("found non-monotonic " + cookie + " values at left[" + leftIndex + "] - right[" + rightIndex + "]\n"); builder.append("left=").append(left).append('\n'); builder.append("right=").append(right).append('\n'); mContext.getSystemService(DropBoxManager.class).addText(TAG_NETSTATS_ERROR, builder.toString()); } @Override public void foundNonMonotonic( NetworkStats stats, int statsIndex, String cookie) { Log.w(TAG, "Found non-monotonic values; saving to dropbox"); final StringBuilder builder = new StringBuilder(); builder.append("Found non-monotonic " + cookie + " values at [" + statsIndex + "]\n"); builder.append("stats=").append(stats).append('\n'); mContext.getSystemService(DropBoxManager.class).addText(TAG_NETSTATS_ERROR, builder.toString()); } } /** * Default external settings that read from * {@link android.provider.Settings.Global}. */ private static class DefaultNetworkStatsSettings implements NetworkStatsSettings { DefaultNetworkStatsSettings() {} @Override public long getPollInterval() { return 30 * MINUTE_IN_MILLIS; } @Override public long getPollDelay() { return DEFAULT_PERFORM_POLL_DELAY_MS; } @Override public long getGlobalAlertBytes(long def) { return def; } @Override public boolean getSampleEnabled() { return true; } @Override public boolean getAugmentEnabled() { return true; } @Override public boolean getCombineSubtypeEnabled() { return false; } @Override public Config getXtConfig() { return new Config(HOUR_IN_MILLIS, 15 * DAY_IN_MILLIS, 90 * DAY_IN_MILLIS); } @Override public Config getUidConfig() { return new Config(2 * HOUR_IN_MILLIS, 15 * DAY_IN_MILLIS, 90 * DAY_IN_MILLIS); } @Override public Config getUidTagConfig() { return new Config(2 * HOUR_IN_MILLIS, 5 * DAY_IN_MILLIS, 15 * DAY_IN_MILLIS); } @Override public long getXtPersistBytes(long def) { return def; } @Override public long getUidPersistBytes(long def) { return def; } @Override public long getUidTagPersistBytes(long def) { return def; } } private static native long nativeGetTotalStat(int type); private static native long nativeGetIfaceStat(String iface, int type); private static native long nativeGetUidStat(int uid, int type); /** Initializes and registers the Perfetto Network Trace data source */ public static native void nativeInitNetworkTracing(); }