Merge changes from topic "stats-migration"
* changes: Skip PersistentIntTest on S- device Don't clobber existing history entries. Ensure NetworkStats migrated snapshot is identical [MS82.1] Support network stats data migration process Add a PersistentInt class.
This commit is contained in:
@@ -865,6 +865,9 @@ public class NetworkStatsCollection implements FileRotator.Reader, FileRotator.W
|
|||||||
* Add association of the history with the specified key in this map.
|
* Add association of the history with the specified key in this map.
|
||||||
*
|
*
|
||||||
* @param key The object used to identify a network, see {@link Key}.
|
* @param key The object used to identify a network, see {@link Key}.
|
||||||
|
* If history already exists for this key, then the passed-in history is appended
|
||||||
|
* to the previously-passed in history. The caller must ensure that the history
|
||||||
|
* passed-in timestamps are greater than all previously-passed-in timestamps.
|
||||||
* @param history {@link NetworkStatsHistory} instance associated to the given {@link Key}.
|
* @param history {@link NetworkStatsHistory} instance associated to the given {@link Key}.
|
||||||
* @return The builder object.
|
* @return The builder object.
|
||||||
*/
|
*/
|
||||||
@@ -874,9 +877,21 @@ public class NetworkStatsCollection implements FileRotator.Reader, FileRotator.W
|
|||||||
Objects.requireNonNull(key);
|
Objects.requireNonNull(key);
|
||||||
Objects.requireNonNull(history);
|
Objects.requireNonNull(history);
|
||||||
final List<Entry> historyEntries = history.getEntries();
|
final List<Entry> historyEntries = history.getEntries();
|
||||||
|
final NetworkStatsHistory existing = mEntries.get(key);
|
||||||
|
|
||||||
|
final int size = historyEntries.size() + ((existing != null) ? existing.size() : 0);
|
||||||
final NetworkStatsHistory.Builder historyBuilder =
|
final NetworkStatsHistory.Builder historyBuilder =
|
||||||
new NetworkStatsHistory.Builder(mBucketDurationMillis, historyEntries.size());
|
new NetworkStatsHistory.Builder(mBucketDurationMillis, size);
|
||||||
|
|
||||||
|
// TODO: this simply appends the entries to any entries that were already present in
|
||||||
|
// the builder, which requires the caller to pass in entries in order. We might be
|
||||||
|
// able to do better with something like recordHistory.
|
||||||
|
if (existing != null) {
|
||||||
|
for (Entry entry : existing.getEntries()) {
|
||||||
|
historyBuilder.addEntry(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (Entry entry : historyEntries) {
|
for (Entry entry : historyEntries) {
|
||||||
historyBuilder.addEntry(entry);
|
historyBuilder.addEntry(entry);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import static android.text.format.DateUtils.SECOND_IN_MILLIS;
|
|||||||
import static com.android.net.module.util.NetworkStatsUtils.multiplySafeByRational;
|
import static com.android.net.module.util.NetworkStatsUtils.multiplySafeByRational;
|
||||||
|
|
||||||
import android.annotation.NonNull;
|
import android.annotation.NonNull;
|
||||||
|
import android.annotation.Nullable;
|
||||||
import android.annotation.SystemApi;
|
import android.annotation.SystemApi;
|
||||||
import android.compat.annotation.UnsupportedAppUsage;
|
import android.compat.annotation.UnsupportedAppUsage;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
@@ -949,6 +950,25 @@ public final class NetworkStatsHistory implements Parcelable {
|
|||||||
return writer.toString();
|
return writer.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Same as "equals", but not actually called equals as this would affect public API behavior.
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public boolean isSameAs(NetworkStatsHistory other) {
|
||||||
|
return bucketCount == other.bucketCount
|
||||||
|
&& Arrays.equals(bucketStart, other.bucketStart)
|
||||||
|
// Don't check activeTime since it can change on import due to the importer using
|
||||||
|
// recordHistory. It's also not exposed by the APIs or present in dumpsys or
|
||||||
|
// toString().
|
||||||
|
&& Arrays.equals(rxBytes, other.rxBytes)
|
||||||
|
&& Arrays.equals(rxPackets, other.rxPackets)
|
||||||
|
&& Arrays.equals(txBytes, other.txBytes)
|
||||||
|
&& Arrays.equals(txPackets, other.txPackets)
|
||||||
|
&& Arrays.equals(operations, other.operations)
|
||||||
|
&& totalBytes == other.totalBytes;
|
||||||
|
}
|
||||||
|
|
||||||
@UnsupportedAppUsage
|
@UnsupportedAppUsage
|
||||||
public static final @android.annotation.NonNull Creator<NetworkStatsHistory> CREATOR = new Creator<NetworkStatsHistory>() {
|
public static final @android.annotation.NonNull Creator<NetworkStatsHistory> CREATOR = new Creator<NetworkStatsHistory>() {
|
||||||
@Override
|
@Override
|
||||||
@@ -1116,14 +1136,44 @@ public final class NetworkStatsHistory implements Parcelable {
|
|||||||
mOperations = new ArrayList<>(initialCapacity);
|
mOperations = new ArrayList<>(initialCapacity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void addToElement(List<Long> list, int pos, long value) {
|
||||||
|
list.set(pos, list.get(pos) + value);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add an {@link Entry} into the {@link NetworkStatsHistory} instance.
|
* Add an {@link Entry} into the {@link NetworkStatsHistory} instance.
|
||||||
*
|
*
|
||||||
* @param entry The target {@link Entry} object.
|
* @param entry The target {@link Entry} object. The entry timestamp must be greater than
|
||||||
|
* that of any previously-added entry.
|
||||||
* @return The builder object.
|
* @return The builder object.
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
public Builder addEntry(@NonNull Entry entry) {
|
public Builder addEntry(@NonNull Entry entry) {
|
||||||
|
final int lastBucket = mBucketStart.size() - 1;
|
||||||
|
final long lastBucketStart = (lastBucket != -1) ? mBucketStart.get(lastBucket) : 0;
|
||||||
|
|
||||||
|
// If last bucket has the same timestamp, modify it instead of adding another bucket.
|
||||||
|
// This allows callers to pass in the same bucket twice (e.g., to accumulate
|
||||||
|
// data over time), but still requires that entries must be sorted.
|
||||||
|
// The importer will do this in case a rotated file has the same timestamp as
|
||||||
|
// the previous file.
|
||||||
|
if (lastBucket != -1 && entry.bucketStart == lastBucketStart) {
|
||||||
|
addToElement(mActiveTime, lastBucket, entry.activeTime);
|
||||||
|
addToElement(mRxBytes, lastBucket, entry.rxBytes);
|
||||||
|
addToElement(mRxPackets, lastBucket, entry.rxPackets);
|
||||||
|
addToElement(mTxBytes, lastBucket, entry.txBytes);
|
||||||
|
addToElement(mTxPackets, lastBucket, entry.txPackets);
|
||||||
|
addToElement(mOperations, lastBucket, entry.operations);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inserting in the middle is prohibited for performance reasons.
|
||||||
|
if (entry.bucketStart <= lastBucketStart) {
|
||||||
|
throw new IllegalArgumentException("new bucket start " + entry.bucketStart
|
||||||
|
+ " must be greater than last bucket start " + lastBucketStart);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common case: add entries at the end of the list.
|
||||||
mBucketStart.add(entry.bucketStart);
|
mBucketStart.add(entry.bucketStart);
|
||||||
mActiveTime.add(entry.activeTime);
|
mActiveTime.add(entry.activeTime);
|
||||||
mRxBytes.add(entry.rxBytes);
|
mRxBytes.add(entry.rxBytes);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import static android.net.TrafficStats.KB_IN_BYTES;
|
|||||||
import static android.net.TrafficStats.MB_IN_BYTES;
|
import static android.net.TrafficStats.MB_IN_BYTES;
|
||||||
import static android.text.format.DateUtils.YEAR_IN_MILLIS;
|
import static android.text.format.DateUtils.YEAR_IN_MILLIS;
|
||||||
|
|
||||||
|
import android.annotation.NonNull;
|
||||||
import android.net.NetworkIdentitySet;
|
import android.net.NetworkIdentitySet;
|
||||||
import android.net.NetworkStats;
|
import android.net.NetworkStats;
|
||||||
import android.net.NetworkStats.NonMonotonicObserver;
|
import android.net.NetworkStats.NonMonotonicObserver;
|
||||||
@@ -68,7 +69,7 @@ public class NetworkStatsRecorder {
|
|||||||
|
|
||||||
private static final String TAG_NETSTATS_DUMP = "netstats_dump";
|
private static final String TAG_NETSTATS_DUMP = "netstats_dump";
|
||||||
|
|
||||||
/** Dump before deleting in {@link #recoverFromWtf()}. */
|
/** Dump before deleting in {@link #recoverAndDeleteData()}. */
|
||||||
private static final boolean DUMP_BEFORE_DELETE = true;
|
private static final boolean DUMP_BEFORE_DELETE = true;
|
||||||
|
|
||||||
private final FileRotator mRotator;
|
private final FileRotator mRotator;
|
||||||
@@ -156,6 +157,15 @@ public class NetworkStatsRecorder {
|
|||||||
return mSinceBoot;
|
return mSinceBoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getBucketDuration() {
|
||||||
|
return mBucketDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public String getCookie() {
|
||||||
|
return mCookie;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load complete history represented by {@link FileRotator}. Caches
|
* Load complete history represented by {@link FileRotator}. Caches
|
||||||
* internally as a {@link WeakReference}, and updated with future
|
* internally as a {@link WeakReference}, and updated with future
|
||||||
@@ -189,10 +199,10 @@ public class NetworkStatsRecorder {
|
|||||||
res.recordCollection(mPending);
|
res.recordCollection(mPending);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.wtf(TAG, "problem completely reading network stats", e);
|
Log.wtf(TAG, "problem completely reading network stats", e);
|
||||||
recoverFromWtf();
|
recoverAndDeleteData();
|
||||||
} catch (OutOfMemoryError e) {
|
} catch (OutOfMemoryError e) {
|
||||||
Log.wtf(TAG, "problem completely reading network stats", e);
|
Log.wtf(TAG, "problem completely reading network stats", e);
|
||||||
recoverFromWtf();
|
recoverAndDeleteData();
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
@@ -300,10 +310,10 @@ public class NetworkStatsRecorder {
|
|||||||
mPending.reset();
|
mPending.reset();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.wtf(TAG, "problem persisting pending stats", e);
|
Log.wtf(TAG, "problem persisting pending stats", e);
|
||||||
recoverFromWtf();
|
recoverAndDeleteData();
|
||||||
} catch (OutOfMemoryError e) {
|
} catch (OutOfMemoryError e) {
|
||||||
Log.wtf(TAG, "problem persisting pending stats", e);
|
Log.wtf(TAG, "problem persisting pending stats", e);
|
||||||
recoverFromWtf();
|
recoverAndDeleteData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -319,10 +329,10 @@ public class NetworkStatsRecorder {
|
|||||||
mRotator.rewriteAll(new RemoveUidRewriter(mBucketDuration, uids));
|
mRotator.rewriteAll(new RemoveUidRewriter(mBucketDuration, uids));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.wtf(TAG, "problem removing UIDs " + Arrays.toString(uids), e);
|
Log.wtf(TAG, "problem removing UIDs " + Arrays.toString(uids), e);
|
||||||
recoverFromWtf();
|
recoverAndDeleteData();
|
||||||
} catch (OutOfMemoryError e) {
|
} catch (OutOfMemoryError e) {
|
||||||
Log.wtf(TAG, "problem removing UIDs " + Arrays.toString(uids), e);
|
Log.wtf(TAG, "problem removing UIDs " + Arrays.toString(uids), e);
|
||||||
recoverFromWtf();
|
recoverAndDeleteData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,8 +357,7 @@ public class NetworkStatsRecorder {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Rewriter that will combine current {@link NetworkStatsCollection} values
|
* Rewriter that will combine current {@link NetworkStatsCollection} values
|
||||||
* with anything read from disk, and write combined set to disk. Clears the
|
* with anything read from disk, and write combined set to disk.
|
||||||
* original {@link NetworkStatsCollection} when finished writing.
|
|
||||||
*/
|
*/
|
||||||
private static class CombiningRewriter implements FileRotator.Rewriter {
|
private static class CombiningRewriter implements FileRotator.Rewriter {
|
||||||
private final NetworkStatsCollection mCollection;
|
private final NetworkStatsCollection mCollection;
|
||||||
@@ -375,7 +384,6 @@ public class NetworkStatsRecorder {
|
|||||||
@Override
|
@Override
|
||||||
public void write(OutputStream out) throws IOException {
|
public void write(OutputStream out) throws IOException {
|
||||||
mCollection.write(out);
|
mCollection.write(out);
|
||||||
mCollection.reset();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,6 +463,23 @@ public class NetworkStatsRecorder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import a specified {@link NetworkStatsCollection} instance into this recorder,
|
||||||
|
* and write it into a standalone file.
|
||||||
|
* @param collection The target {@link NetworkStatsCollection} instance to be imported.
|
||||||
|
*/
|
||||||
|
public void importCollectionLocked(@NonNull NetworkStatsCollection collection)
|
||||||
|
throws IOException {
|
||||||
|
if (mRotator != null) {
|
||||||
|
mRotator.rewriteSingle(new CombiningRewriter(collection), collection.getStartMillis(),
|
||||||
|
collection.getEndMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mComplete != null) {
|
||||||
|
throw new IllegalStateException("cannot import data when data already loaded");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rewriter that will remove any histories or persisted data points before the
|
* Rewriter that will remove any histories or persisted data points before the
|
||||||
* specified cutoff time, only writing data back when modified.
|
* specified cutoff time, only writing data back when modified.
|
||||||
@@ -501,10 +526,10 @@ public class NetworkStatsRecorder {
|
|||||||
mBucketDuration, cutoffMillis));
|
mBucketDuration, cutoffMillis));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.wtf(TAG, "problem importing netstats", e);
|
Log.wtf(TAG, "problem importing netstats", e);
|
||||||
recoverFromWtf();
|
recoverAndDeleteData();
|
||||||
} catch (OutOfMemoryError e) {
|
} catch (OutOfMemoryError e) {
|
||||||
Log.wtf(TAG, "problem importing netstats", e);
|
Log.wtf(TAG, "problem importing netstats", e);
|
||||||
recoverFromWtf();
|
recoverAndDeleteData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -555,7 +580,7 @@ public class NetworkStatsRecorder {
|
|||||||
* Recover from {@link FileRotator} failure by dumping state to
|
* Recover from {@link FileRotator} failure by dumping state to
|
||||||
* {@link DropBoxManager} and deleting contents.
|
* {@link DropBoxManager} and deleting contents.
|
||||||
*/
|
*/
|
||||||
private void recoverFromWtf() {
|
void recoverAndDeleteData() {
|
||||||
if (DUMP_BEFORE_DELETE) {
|
if (DUMP_BEFORE_DELETE) {
|
||||||
final ByteArrayOutputStream os = new ByteArrayOutputStream();
|
final ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ import android.annotation.TargetApi;
|
|||||||
import android.app.AlarmManager;
|
import android.app.AlarmManager;
|
||||||
import android.app.PendingIntent;
|
import android.app.PendingIntent;
|
||||||
import android.app.usage.NetworkStatsManager;
|
import android.app.usage.NetworkStatsManager;
|
||||||
|
import android.content.ApexEnvironment;
|
||||||
import android.content.BroadcastReceiver;
|
import android.content.BroadcastReceiver;
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
@@ -75,6 +76,7 @@ import android.content.IntentFilter;
|
|||||||
import android.content.pm.ApplicationInfo;
|
import android.content.pm.ApplicationInfo;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.database.ContentObserver;
|
import android.database.ContentObserver;
|
||||||
|
import android.net.ConnectivityManager;
|
||||||
import android.net.DataUsageRequest;
|
import android.net.DataUsageRequest;
|
||||||
import android.net.INetd;
|
import android.net.INetd;
|
||||||
import android.net.INetworkStatsService;
|
import android.net.INetworkStatsService;
|
||||||
@@ -100,6 +102,7 @@ import android.net.TrafficStats;
|
|||||||
import android.net.UnderlyingNetworkInfo;
|
import android.net.UnderlyingNetworkInfo;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.net.netstats.IUsageCallback;
|
import android.net.netstats.IUsageCallback;
|
||||||
|
import android.net.netstats.NetworkStatsDataMigrationUtils;
|
||||||
import android.net.netstats.provider.INetworkStatsProvider;
|
import android.net.netstats.provider.INetworkStatsProvider;
|
||||||
import android.net.netstats.provider.INetworkStatsProviderCallback;
|
import android.net.netstats.provider.INetworkStatsProviderCallback;
|
||||||
import android.net.netstats.provider.NetworkStatsProvider;
|
import android.net.netstats.provider.NetworkStatsProvider;
|
||||||
@@ -118,6 +121,7 @@ import android.os.ServiceSpecificException;
|
|||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
import android.os.Trace;
|
import android.os.Trace;
|
||||||
import android.os.UserHandle;
|
import android.os.UserHandle;
|
||||||
|
import android.provider.DeviceConfig;
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
import android.provider.Settings.Global;
|
import android.provider.Settings.Global;
|
||||||
import android.service.NetworkInterfaceProto;
|
import android.service.NetworkInterfaceProto;
|
||||||
@@ -143,6 +147,7 @@ import com.android.net.module.util.BestClock;
|
|||||||
import com.android.net.module.util.BinderUtils;
|
import com.android.net.module.util.BinderUtils;
|
||||||
import com.android.net.module.util.BpfMap;
|
import com.android.net.module.util.BpfMap;
|
||||||
import com.android.net.module.util.CollectionUtils;
|
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.IBpfMap;
|
||||||
import com.android.net.module.util.LocationPermissionChecker;
|
import com.android.net.module.util.LocationPermissionChecker;
|
||||||
import com.android.net.module.util.NetworkStatsUtils;
|
import com.android.net.module.util.NetworkStatsUtils;
|
||||||
@@ -155,7 +160,9 @@ import java.io.FileDescriptor;
|
|||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.time.Clock;
|
import java.time.Clock;
|
||||||
|
import java.time.Instant;
|
||||||
import java.time.ZoneOffset;
|
import java.time.ZoneOffset;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@@ -232,6 +239,22 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
|
|||||||
private static final String STATS_MAP_B_PATH =
|
private static final String STATS_MAP_B_PATH =
|
||||||
"/sys/fs/bpf/netd_shared/map_netd_stats_map_B";
|
"/sys/fs/bpf/netd_shared/map_netd_stats_map_B";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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_SUCCESS_COUNTER_NAME = "import.successes";
|
||||||
|
|
||||||
private final Context mContext;
|
private final Context mContext;
|
||||||
private final NetworkStatsFactory mStatsFactory;
|
private final NetworkStatsFactory mStatsFactory;
|
||||||
private final AlarmManager mAlarmManager;
|
private final AlarmManager mAlarmManager;
|
||||||
@@ -239,8 +262,7 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
|
|||||||
private final NetworkStatsSettings mSettings;
|
private final NetworkStatsSettings mSettings;
|
||||||
private final NetworkStatsObservers mStatsObservers;
|
private final NetworkStatsObservers mStatsObservers;
|
||||||
|
|
||||||
private final File mSystemDir;
|
private final File mStatsDir;
|
||||||
private final File mBaseDir;
|
|
||||||
|
|
||||||
private final PowerManager.WakeLock mWakeLock;
|
private final PowerManager.WakeLock mWakeLock;
|
||||||
|
|
||||||
@@ -250,6 +272,12 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
|
|||||||
protected INetd mNetd;
|
protected INetd mNetd;
|
||||||
private final AlertObserver mAlertObserver = new AlertObserver();
|
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 count across reboot. Note that these counter values will be
|
||||||
|
// rollback as the module rollbacks.
|
||||||
|
private PersistentInt mImportLegacyAttemptsCounter = null;
|
||||||
|
private PersistentInt mImportLegacySuccessesCounter = null;
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
public static final String ACTION_NETWORK_STATS_POLL =
|
public static final String ACTION_NETWORK_STATS_POLL =
|
||||||
"com.android.server.action.NETWORK_STATS_POLL";
|
"com.android.server.action.NETWORK_STATS_POLL";
|
||||||
@@ -405,16 +433,6 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
|
|||||||
@NonNull
|
@NonNull
|
||||||
private final BpfInterfaceMapUpdater mInterfaceMapUpdater;
|
private final BpfInterfaceMapUpdater mInterfaceMapUpdater;
|
||||||
|
|
||||||
private static @NonNull File getDefaultSystemDir() {
|
|
||||||
return new File(Environment.getDataDirectory(), "system");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static @NonNull File getDefaultBaseDir() {
|
|
||||||
File baseDir = new File(getDefaultSystemDir(), "netstats");
|
|
||||||
baseDir.mkdirs();
|
|
||||||
return baseDir;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static @NonNull Clock getDefaultClock() {
|
private static @NonNull Clock getDefaultClock() {
|
||||||
return new BestClock(ZoneOffset.UTC, SystemClock.currentNetworkTimeClock(),
|
return new BestClock(ZoneOffset.UTC, SystemClock.currentNetworkTimeClock(),
|
||||||
Clock.systemUTC());
|
Clock.systemUTC());
|
||||||
@@ -506,8 +524,7 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
|
|||||||
INetd.Stub.asInterface((IBinder) context.getSystemService(Context.NETD_SERVICE)),
|
INetd.Stub.asInterface((IBinder) context.getSystemService(Context.NETD_SERVICE)),
|
||||||
alarmManager, wakeLock, getDefaultClock(),
|
alarmManager, wakeLock, getDefaultClock(),
|
||||||
new DefaultNetworkStatsSettings(), new NetworkStatsFactory(context),
|
new DefaultNetworkStatsSettings(), new NetworkStatsFactory(context),
|
||||||
new NetworkStatsObservers(), getDefaultSystemDir(), getDefaultBaseDir(),
|
new NetworkStatsObservers(), new Dependencies());
|
||||||
new Dependencies());
|
|
||||||
|
|
||||||
return service;
|
return service;
|
||||||
}
|
}
|
||||||
@@ -517,8 +534,8 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
|
|||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
NetworkStatsService(Context context, INetd netd, AlarmManager alarmManager,
|
NetworkStatsService(Context context, INetd netd, AlarmManager alarmManager,
|
||||||
PowerManager.WakeLock wakeLock, Clock clock, NetworkStatsSettings settings,
|
PowerManager.WakeLock wakeLock, Clock clock, NetworkStatsSettings settings,
|
||||||
NetworkStatsFactory factory, NetworkStatsObservers statsObservers, File systemDir,
|
NetworkStatsFactory factory, NetworkStatsObservers statsObservers,
|
||||||
File baseDir, @NonNull Dependencies deps) {
|
@NonNull Dependencies deps) {
|
||||||
mContext = Objects.requireNonNull(context, "missing Context");
|
mContext = Objects.requireNonNull(context, "missing Context");
|
||||||
mNetd = Objects.requireNonNull(netd, "missing Netd");
|
mNetd = Objects.requireNonNull(netd, "missing Netd");
|
||||||
mAlarmManager = Objects.requireNonNull(alarmManager, "missing AlarmManager");
|
mAlarmManager = Objects.requireNonNull(alarmManager, "missing AlarmManager");
|
||||||
@@ -527,9 +544,11 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
|
|||||||
mWakeLock = Objects.requireNonNull(wakeLock, "missing WakeLock");
|
mWakeLock = Objects.requireNonNull(wakeLock, "missing WakeLock");
|
||||||
mStatsFactory = Objects.requireNonNull(factory, "missing factory");
|
mStatsFactory = Objects.requireNonNull(factory, "missing factory");
|
||||||
mStatsObservers = Objects.requireNonNull(statsObservers, "missing NetworkStatsObservers");
|
mStatsObservers = Objects.requireNonNull(statsObservers, "missing NetworkStatsObservers");
|
||||||
mSystemDir = Objects.requireNonNull(systemDir, "missing systemDir");
|
|
||||||
mBaseDir = Objects.requireNonNull(baseDir, "missing baseDir");
|
|
||||||
mDeps = Objects.requireNonNull(deps, "missing Dependencies");
|
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();
|
final HandlerThread handlerThread = mDeps.makeHandlerThread();
|
||||||
handlerThread.start();
|
handlerThread.start();
|
||||||
@@ -555,6 +574,87 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
|
|||||||
// TODO: Move more stuff into dependencies object.
|
// TODO: Move more stuff into dependencies object.
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
public static class Dependencies {
|
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 the persistent counter that counts total import legacy stats attempts.
|
||||||
|
*/
|
||||||
|
public PersistentInt createImportLegacyAttemptsCounter(@NonNull Path path)
|
||||||
|
throws IOException {
|
||||||
|
// TODO: Modify PersistentInt to call setStartTime every time a write is made.
|
||||||
|
// Create and pass a real logger here.
|
||||||
|
return new PersistentInt(path.toString(), null /* logger */);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the persistent counter that counts total import legacy stats successes.
|
||||||
|
*/
|
||||||
|
public PersistentInt createImportLegacySuccessesCounter(@NonNull Path path)
|
||||||
|
throws IOException {
|
||||||
|
return new PersistentInt(path.toString(), 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.
|
* Create a HandlerThread to use in NetworkStatsService.
|
||||||
*/
|
*/
|
||||||
@@ -690,14 +790,15 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
|
|||||||
mSystemReady = true;
|
mSystemReady = true;
|
||||||
|
|
||||||
// create data recorders along with historical rotators
|
// create data recorders along with historical rotators
|
||||||
mDevRecorder = buildRecorder(PREFIX_DEV, mSettings.getDevConfig(), false);
|
mDevRecorder = buildRecorder(PREFIX_DEV, mSettings.getDevConfig(), false, mStatsDir);
|
||||||
mXtRecorder = buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false);
|
mXtRecorder = buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false, mStatsDir);
|
||||||
mUidRecorder = buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false);
|
mUidRecorder = buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false, mStatsDir);
|
||||||
mUidTagRecorder = buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true);
|
mUidTagRecorder = buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true,
|
||||||
|
mStatsDir);
|
||||||
|
|
||||||
updatePersistThresholdsLocked();
|
updatePersistThresholdsLocked();
|
||||||
|
|
||||||
// upgrade any legacy stats, migrating them to rotated files
|
// upgrade any legacy stats
|
||||||
maybeUpgradeLegacyStatsLocked();
|
maybeUpgradeLegacyStatsLocked();
|
||||||
|
|
||||||
// read historical network stats from disk, since policy service
|
// read historical network stats from disk, since policy service
|
||||||
@@ -757,11 +858,12 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private NetworkStatsRecorder buildRecorder(
|
private NetworkStatsRecorder buildRecorder(
|
||||||
String prefix, NetworkStatsSettings.Config config, boolean includeTags) {
|
String prefix, NetworkStatsSettings.Config config, boolean includeTags,
|
||||||
|
File baseDir) {
|
||||||
final DropBoxManager dropBox = (DropBoxManager) mContext.getSystemService(
|
final DropBoxManager dropBox = (DropBoxManager) mContext.getSystemService(
|
||||||
Context.DROPBOX_SERVICE);
|
Context.DROPBOX_SERVICE);
|
||||||
return new NetworkStatsRecorder(new FileRotator(
|
return new NetworkStatsRecorder(new FileRotator(
|
||||||
mBaseDir, prefix, config.rotateAgeMillis, config.deleteAgeMillis),
|
baseDir, prefix, config.rotateAgeMillis, config.deleteAgeMillis),
|
||||||
mNonMonotonicObserver, dropBox, prefix, config.bucketDuration, includeTags);
|
mNonMonotonicObserver, dropBox, prefix, config.bucketDuration, includeTags);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -791,32 +893,285 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
|
|||||||
mSystemReady = false;
|
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")
|
@GuardedBy("mStatsLock")
|
||||||
private void maybeUpgradeLegacyStatsLocked() {
|
private void maybeUpgradeLegacyStatsLocked() {
|
||||||
File file;
|
final boolean storeFilesInApexData = mDeps.getStoreFilesInApexData();
|
||||||
try {
|
if (!storeFilesInApexData) {
|
||||||
file = new File(mSystemDir, "netstats.bin");
|
return;
|
||||||
if (file.exists()) {
|
|
||||||
mDevRecorder.importLegacyNetworkLocked(file);
|
|
||||||
file.delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
file = new File(mSystemDir, "netstats_xt.bin");
|
|
||||||
if (file.exists()) {
|
|
||||||
file.delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
file = new File(mSystemDir, "netstats_uid.bin");
|
|
||||||
if (file.exists()) {
|
|
||||||
mUidRecorder.importLegacyUidLocked(file);
|
|
||||||
mUidTagRecorder.importLegacyUidLocked(file);
|
|
||||||
file.delete();
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.wtf(TAG, "problem during legacy upgrade", e);
|
|
||||||
} catch (OutOfMemoryError e) {
|
|
||||||
Log.wtf(TAG, "problem during legacy upgrade", e);
|
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
mImportLegacyAttemptsCounter = mDeps.createImportLegacyAttemptsCounter(
|
||||||
|
mStatsDir.toPath().resolve(NETSTATS_IMPORT_ATTEMPTS_COUNTER_NAME));
|
||||||
|
mImportLegacySuccessesCounter = mDeps.createImportLegacySuccessesCounter(
|
||||||
|
mStatsDir.toPath().resolve(NETSTATS_IMPORT_SUCCESS_COUNTER_NAME));
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.wtf(TAG, "Failed to create persistent counters, skip.", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int targetAttempts = mDeps.getImportLegacyTargetAttempts();
|
||||||
|
final int attempts;
|
||||||
|
try {
|
||||||
|
attempts = mImportLegacyAttemptsCounter.get();
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.wtf(TAG, "Failed to read attempts counter, skip.", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (attempts >= targetAttempts) return;
|
||||||
|
|
||||||
|
Log.i(TAG, "Starting import : attempts " + attempts + "/" + targetAttempts);
|
||||||
|
|
||||||
|
final MigrationInfo[] migrations = new MigrationInfo[]{
|
||||||
|
new MigrationInfo(mDevRecorder), new MigrationInfo(mXtRecorder),
|
||||||
|
new MigrationInfo(mUidRecorder), new MigrationInfo(mUidTagRecorder)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Legacy directories will be created by recorders if they do not exist
|
||||||
|
final File legacyBaseDir = mDeps.getLegacyStatsDir();
|
||||||
|
final NetworkStatsRecorder[] legacyRecorders = new NetworkStatsRecorder[]{
|
||||||
|
buildRecorder(PREFIX_DEV, mSettings.getDevConfig(), false, legacyBaseDir),
|
||||||
|
buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false, legacyBaseDir),
|
||||||
|
buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false, legacyBaseDir),
|
||||||
|
buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true, legacyBaseDir)
|
||||||
|
};
|
||||||
|
|
||||||
|
long migrationEndTime = Long.MIN_VALUE;
|
||||||
|
boolean endedWithFallback = false;
|
||||||
|
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];
|
||||||
|
migration.collection = readPlatformCollectionForRecorder(migration.recorder);
|
||||||
|
|
||||||
|
// Also read the collection with legacy method
|
||||||
|
final NetworkStatsRecorder legacyRecorder = legacyRecorders[i];
|
||||||
|
|
||||||
|
final NetworkStatsCollection legacyStats;
|
||||||
|
try {
|
||||||
|
legacyStats = legacyRecorder.getOrLoadCompleteLocked();
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Log.wtf(TAG, "Failed to read stats with legacy method", e);
|
||||||
|
// Newer stats will be used here; that's the only thing that is usable
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String errMsg;
|
||||||
|
Throwable exception = null;
|
||||||
|
try {
|
||||||
|
errMsg = compareStats(migration.collection, legacyStats);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
errMsg = "Failed to compare migrated stats with all stats";
|
||||||
|
exception = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errMsg != null) {
|
||||||
|
Log.wtf(TAG, "NetworkStats import for migration " + i
|
||||||
|
+ " returned invalid data: " + errMsg, exception);
|
||||||
|
// Fall back to legacy stats for this boot. The stats for old data will be
|
||||||
|
// re-imported again on next boot until they succeed the import. This is fine
|
||||||
|
// since every import clears the previous stats for the imported timespan.
|
||||||
|
migration.collection = legacyStats;
|
||||||
|
endedWithFallback = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the latest end time.
|
||||||
|
for (final MigrationInfo migration : migrations) {
|
||||||
|
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()) continue;
|
||||||
|
migration.recorder.importCollectionLocked(migration.collection);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endedWithFallback) {
|
||||||
|
Log.wtf(TAG, "Imported platform collections with legacy fallback");
|
||||||
|
} else {
|
||||||
|
Log.i(TAG, "Successfully imported platform collections");
|
||||||
|
}
|
||||||
|
} 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 (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) {
|
||||||
|
migration.recorder.recoverAndDeleteData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success ! No need to import again next time.
|
||||||
|
try {
|
||||||
|
mImportLegacyAttemptsCounter.set(targetAttempts);
|
||||||
|
// The successes counter is only for debugging. Hence, the synchronization
|
||||||
|
// between these two counters 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<NetworkStatsCollection.Key, NetworkStatsHistory> migEntries =
|
||||||
|
migrated.getEntries();
|
||||||
|
final Map<NetworkStatsCollection.Key, NetworkStatsHistory> legEntries = legacy.getEntries();
|
||||||
|
|
||||||
|
final ArraySet<NetworkStatsCollection.Key> 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2102,10 +2457,32 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pw.println("Directory:");
|
||||||
|
pw.increaseIndent();
|
||||||
|
pw.println(mStatsDir);
|
||||||
|
pw.decreaseIndent();
|
||||||
|
|
||||||
pw.println("Configs:");
|
pw.println("Configs:");
|
||||||
pw.increaseIndent();
|
pw.increaseIndent();
|
||||||
pw.print(NETSTATS_COMBINE_SUBTYPE_ENABLED, mSettings.getCombineSubtypeEnabled());
|
pw.print(NETSTATS_COMBINE_SUBTYPE_ENABLED, mSettings.getCombineSubtypeEnabled());
|
||||||
pw.println();
|
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();
|
||||||
|
} catch (IOException e) {
|
||||||
|
pw.println("(failed to dump platform legacy stats import counters)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pw.decreaseIndent();
|
pw.decreaseIndent();
|
||||||
|
|
||||||
pw.println("Active interfaces:");
|
pw.println("Active interfaces:");
|
||||||
|
|||||||
108
service-t/src/com/android/server/net/PersistentInt.java
Normal file
108
service-t/src/com/android/server/net/PersistentInt.java
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.server.net;
|
||||||
|
|
||||||
|
import android.annotation.NonNull;
|
||||||
|
import android.annotation.Nullable;
|
||||||
|
import android.util.AtomicFile;
|
||||||
|
import android.util.SystemConfigFileCommitEventLogger;
|
||||||
|
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple integer backed by an on-disk {@link AtomicFile}. Not thread-safe.
|
||||||
|
*/
|
||||||
|
public class PersistentInt {
|
||||||
|
private final String mPath;
|
||||||
|
private final AtomicFile mFile;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code PersistentInt}. The counter is set to 0 if the file does not exist.
|
||||||
|
* Before returning, the constructor checks that the file is readable and writable. This
|
||||||
|
* indicates that in the future {@link #get} and {@link #set} are likely to succeed,
|
||||||
|
* though other events (data corruption, other code deleting the file, etc.) may cause these
|
||||||
|
* calls to fail in the future.
|
||||||
|
*
|
||||||
|
* @param path the path of the file to use.
|
||||||
|
* @param logger the logger
|
||||||
|
* @throws IOException the counter could not be read or written
|
||||||
|
*/
|
||||||
|
public PersistentInt(@NonNull String path, @Nullable SystemConfigFileCommitEventLogger logger)
|
||||||
|
throws IOException {
|
||||||
|
mPath = path;
|
||||||
|
mFile = new AtomicFile(new File(path), logger);
|
||||||
|
checkReadWrite();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkReadWrite() throws IOException {
|
||||||
|
int value;
|
||||||
|
try {
|
||||||
|
value = get();
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
// Counter does not exist. Attempt to initialize to 0.
|
||||||
|
// Note that we cannot tell here if the file does not exist or if opening it failed,
|
||||||
|
// because in Java both of those throw FileNotFoundException.
|
||||||
|
value = 0;
|
||||||
|
}
|
||||||
|
set(value);
|
||||||
|
get();
|
||||||
|
// No exceptions? Good.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current value.
|
||||||
|
*
|
||||||
|
* @return the current value of the counter.
|
||||||
|
* @throws IOException if reading the value failed.
|
||||||
|
*/
|
||||||
|
public int get() throws IOException {
|
||||||
|
try (FileInputStream fin = mFile.openRead();
|
||||||
|
DataInputStream din = new DataInputStream(fin)) {
|
||||||
|
return din.readInt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the current value.
|
||||||
|
* @param value the value to set
|
||||||
|
* @throws IOException if writing the value failed.
|
||||||
|
*/
|
||||||
|
public void set(int value) throws IOException {
|
||||||
|
FileOutputStream fout = null;
|
||||||
|
try {
|
||||||
|
fout = mFile.startWrite();
|
||||||
|
DataOutputStream dout = new DataOutputStream(fout);
|
||||||
|
dout.writeInt(value);
|
||||||
|
mFile.finishWrite(fout);
|
||||||
|
} catch (IOException e) {
|
||||||
|
if (fout != null) {
|
||||||
|
mFile.failWrite(fout);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPath() {
|
||||||
|
return mPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ import org.junit.Test
|
|||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.junit.runners.JUnit4
|
import org.junit.runners.JUnit4
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFailsWith
|
||||||
|
|
||||||
@ConnectivityModuleTest
|
@ConnectivityModuleTest
|
||||||
@RunWith(JUnit4::class)
|
@RunWith(JUnit4::class)
|
||||||
@@ -51,12 +52,22 @@ class NetworkStatsHistoryTest {
|
|||||||
.build()
|
.build()
|
||||||
statsSingle.assertEntriesEqual(entry1)
|
statsSingle.assertEntriesEqual(entry1)
|
||||||
assertEquals(DateUtils.HOUR_IN_MILLIS, statsSingle.bucketDuration)
|
assertEquals(DateUtils.HOUR_IN_MILLIS, statsSingle.bucketDuration)
|
||||||
|
|
||||||
|
// Verify the builder throws if the timestamp of added entry is not greater than
|
||||||
|
// that of any previously-added entry.
|
||||||
|
assertFailsWith(IllegalArgumentException::class) {
|
||||||
|
NetworkStatsHistory
|
||||||
|
.Builder(DateUtils.SECOND_IN_MILLIS, /* initialCapacity */ 0)
|
||||||
|
.addEntry(entry1).addEntry(entry2).addEntry(entry3)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
val statsMultiple = NetworkStatsHistory
|
val statsMultiple = NetworkStatsHistory
|
||||||
.Builder(DateUtils.SECOND_IN_MILLIS, /* initialCapacity */ 0)
|
.Builder(DateUtils.SECOND_IN_MILLIS, /* initialCapacity */ 0)
|
||||||
.addEntry(entry1).addEntry(entry2).addEntry(entry3)
|
.addEntry(entry3).addEntry(entry1).addEntry(entry2)
|
||||||
.build()
|
.build()
|
||||||
assertEquals(DateUtils.SECOND_IN_MILLIS, statsMultiple.bucketDuration)
|
assertEquals(DateUtils.SECOND_IN_MILLIS, statsMultiple.bucketDuration)
|
||||||
statsMultiple.assertEntriesEqual(entry1, entry2, entry3)
|
statsMultiple.assertEntriesEqual(entry3, entry1, entry2)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun NetworkStatsHistory.assertEntriesEqual(vararg entries: NetworkStatsHistory.Entry) {
|
fun NetworkStatsHistory.assertEntriesEqual(vararg entries: NetworkStatsHistory.Entry) {
|
||||||
|
|||||||
@@ -61,14 +61,6 @@ class NetworkStatsDataMigrationUtilsTest {
|
|||||||
assertValues(builder.build(), 55, 1814302L, 21050L, 31001636L, 26152L)
|
assertValues(builder.build(), 55, 1814302L, 21050L, 31001636L, 26152L)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testMaybeReadLegacyUid() {
|
|
||||||
val builder = NetworkStatsCollection.Builder(BUCKET_DURATION_MS)
|
|
||||||
NetworkStatsDataMigrationUtils.readLegacyUid(builder,
|
|
||||||
getInputStreamForResource(R.raw.netstats_uid_v4), false /* taggedData */)
|
|
||||||
assertValues(builder.build(), 223, 106245210L, 710722L, 1130647496L, 1103989L)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun assertValues(
|
private fun assertValues(
|
||||||
collection: NetworkStatsCollection,
|
collection: NetworkStatsCollection,
|
||||||
expectedSize: Int,
|
expectedSize: Int,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ package com.android.server.net;
|
|||||||
|
|
||||||
import static android.Manifest.permission.READ_NETWORK_USAGE_HISTORY;
|
import static android.Manifest.permission.READ_NETWORK_USAGE_HISTORY;
|
||||||
import static android.Manifest.permission.UPDATE_DEVICE_STATS;
|
import static android.Manifest.permission.UPDATE_DEVICE_STATS;
|
||||||
|
import static android.app.usage.NetworkStatsManager.PREFIX_DEV;
|
||||||
import static android.content.Intent.ACTION_UID_REMOVED;
|
import static android.content.Intent.ACTION_UID_REMOVED;
|
||||||
import static android.content.Intent.EXTRA_UID;
|
import static android.content.Intent.EXTRA_UID;
|
||||||
import static android.content.pm.PackageManager.PERMISSION_DENIED;
|
import static android.content.pm.PackageManager.PERMISSION_DENIED;
|
||||||
@@ -56,6 +57,9 @@ import static android.net.NetworkTemplate.buildTemplateWifiWildcard;
|
|||||||
import static android.net.TrafficStats.MB_IN_BYTES;
|
import static android.net.TrafficStats.MB_IN_BYTES;
|
||||||
import static android.net.TrafficStats.UID_REMOVED;
|
import static android.net.TrafficStats.UID_REMOVED;
|
||||||
import static android.net.TrafficStats.UID_TETHERING;
|
import static android.net.TrafficStats.UID_TETHERING;
|
||||||
|
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.text.format.DateUtils.DAY_IN_MILLIS;
|
import static android.text.format.DateUtils.DAY_IN_MILLIS;
|
||||||
import static android.text.format.DateUtils.HOUR_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.MINUTE_IN_MILLIS;
|
||||||
@@ -77,6 +81,7 @@ import static org.mockito.ArgumentMatchers.anyLong;
|
|||||||
import static org.mockito.ArgumentMatchers.anyString;
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
import static org.mockito.Matchers.eq;
|
import static org.mockito.Matchers.eq;
|
||||||
import static org.mockito.Mockito.doReturn;
|
import static org.mockito.Mockito.doReturn;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.reset;
|
import static org.mockito.Mockito.reset;
|
||||||
import static org.mockito.Mockito.spy;
|
import static org.mockito.Mockito.spy;
|
||||||
@@ -96,6 +101,7 @@ import android.net.Network;
|
|||||||
import android.net.NetworkCapabilities;
|
import android.net.NetworkCapabilities;
|
||||||
import android.net.NetworkStateSnapshot;
|
import android.net.NetworkStateSnapshot;
|
||||||
import android.net.NetworkStats;
|
import android.net.NetworkStats;
|
||||||
|
import android.net.NetworkStatsCollection;
|
||||||
import android.net.NetworkStatsHistory;
|
import android.net.NetworkStatsHistory;
|
||||||
import android.net.NetworkTemplate;
|
import android.net.NetworkTemplate;
|
||||||
import android.net.TelephonyNetworkSpecifier;
|
import android.net.TelephonyNetworkSpecifier;
|
||||||
@@ -104,6 +110,7 @@ import android.net.TetheringManager;
|
|||||||
import android.net.UnderlyingNetworkInfo;
|
import android.net.UnderlyingNetworkInfo;
|
||||||
import android.net.netstats.provider.INetworkStatsProviderCallback;
|
import android.net.netstats.provider.INetworkStatsProviderCallback;
|
||||||
import android.net.wifi.WifiInfo;
|
import android.net.wifi.WifiInfo;
|
||||||
|
import android.os.DropBoxManager;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.HandlerThread;
|
import android.os.HandlerThread;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
@@ -112,11 +119,13 @@ import android.os.SimpleClock;
|
|||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
import android.system.ErrnoException;
|
import android.system.ErrnoException;
|
||||||
import android.telephony.TelephonyManager;
|
import android.telephony.TelephonyManager;
|
||||||
|
import android.util.ArrayMap;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.test.InstrumentationRegistry;
|
import androidx.test.InstrumentationRegistry;
|
||||||
import androidx.test.filters.SmallTest;
|
import androidx.test.filters.SmallTest;
|
||||||
|
|
||||||
|
import com.android.internal.util.FileRotator;
|
||||||
import com.android.internal.util.test.BroadcastInterceptingContext;
|
import com.android.internal.util.test.BroadcastInterceptingContext;
|
||||||
import com.android.net.module.util.IBpfMap;
|
import com.android.net.module.util.IBpfMap;
|
||||||
import com.android.net.module.util.LocationPermissionChecker;
|
import com.android.net.module.util.LocationPermissionChecker;
|
||||||
@@ -131,6 +140,16 @@ import com.android.testutils.HandlerUtils;
|
|||||||
import com.android.testutils.TestBpfMap;
|
import com.android.testutils.TestBpfMap;
|
||||||
import com.android.testutils.TestableNetworkStatsProviderBinder;
|
import com.android.testutils.TestableNetworkStatsProviderBinder;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.time.Clock;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
import libcore.testing.io.TestIoUtils;
|
import libcore.testing.io.TestIoUtils;
|
||||||
|
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
@@ -142,13 +161,6 @@ import org.mockito.ArgumentCaptor;
|
|||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.MockitoAnnotations;
|
import org.mockito.MockitoAnnotations;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.time.Clock;
|
|
||||||
import java.time.ZoneOffset;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.concurrent.Executor;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for {@link NetworkStatsService}.
|
* Tests for {@link NetworkStatsService}.
|
||||||
*
|
*
|
||||||
@@ -187,6 +199,7 @@ public class NetworkStatsServiceTest extends NetworkStatsBaseTest {
|
|||||||
private long mElapsedRealtime;
|
private long mElapsedRealtime;
|
||||||
|
|
||||||
private File mStatsDir;
|
private File mStatsDir;
|
||||||
|
private File mLegacyStatsDir;
|
||||||
private MockContext mServiceContext;
|
private MockContext mServiceContext;
|
||||||
private @Mock TelephonyManager mTelephonyManager;
|
private @Mock TelephonyManager mTelephonyManager;
|
||||||
private static @Mock WifiInfo sWifiInfo;
|
private static @Mock WifiInfo sWifiInfo;
|
||||||
@@ -220,6 +233,12 @@ public class NetworkStatsServiceTest extends NetworkStatsBaseTest {
|
|||||||
private ContentObserver mContentObserver;
|
private ContentObserver mContentObserver;
|
||||||
private Handler mHandler;
|
private Handler mHandler;
|
||||||
private TetheringManager.TetheringEventCallback mTetheringEventCallback;
|
private TetheringManager.TetheringEventCallback mTetheringEventCallback;
|
||||||
|
private Map<String, NetworkStatsCollection> mPlatformNetworkStatsCollection =
|
||||||
|
new ArrayMap<String, NetworkStatsCollection>();
|
||||||
|
private boolean mStoreFilesInApexData = false;
|
||||||
|
private int mImportLegacyTargetAttempts = 0;
|
||||||
|
private @Mock PersistentInt mImportLegacyAttemptsCounter;
|
||||||
|
private @Mock PersistentInt mImportLegacySuccessesCounter;
|
||||||
|
|
||||||
private class MockContext extends BroadcastInterceptingContext {
|
private class MockContext extends BroadcastInterceptingContext {
|
||||||
private final Context mBaseContext;
|
private final Context mBaseContext;
|
||||||
@@ -286,6 +305,8 @@ public class NetworkStatsServiceTest extends NetworkStatsBaseTest {
|
|||||||
any(), any(), anyInt(), anyBoolean(), any())).thenReturn(true);
|
any(), any(), anyInt(), anyBoolean(), any())).thenReturn(true);
|
||||||
when(sWifiInfo.getNetworkKey()).thenReturn(TEST_WIFI_NETWORK_KEY);
|
when(sWifiInfo.getNetworkKey()).thenReturn(TEST_WIFI_NETWORK_KEY);
|
||||||
mStatsDir = TestIoUtils.createTemporaryDirectory(getClass().getSimpleName());
|
mStatsDir = TestIoUtils.createTemporaryDirectory(getClass().getSimpleName());
|
||||||
|
mLegacyStatsDir = TestIoUtils.createTemporaryDirectory(
|
||||||
|
getClass().getSimpleName() + "-legacy");
|
||||||
|
|
||||||
PowerManager powerManager = (PowerManager) mServiceContext.getSystemService(
|
PowerManager powerManager = (PowerManager) mServiceContext.getSystemService(
|
||||||
Context.POWER_SERVICE);
|
Context.POWER_SERVICE);
|
||||||
@@ -295,8 +316,7 @@ public class NetworkStatsServiceTest extends NetworkStatsBaseTest {
|
|||||||
mHandlerThread = new HandlerThread("HandlerThread");
|
mHandlerThread = new HandlerThread("HandlerThread");
|
||||||
final NetworkStatsService.Dependencies deps = makeDependencies();
|
final NetworkStatsService.Dependencies deps = makeDependencies();
|
||||||
mService = new NetworkStatsService(mServiceContext, mNetd, mAlarmManager, wakeLock,
|
mService = new NetworkStatsService(mServiceContext, mNetd, mAlarmManager, wakeLock,
|
||||||
mClock, mSettings, mStatsFactory, new NetworkStatsObservers(), mStatsDir,
|
mClock, mSettings, mStatsFactory, new NetworkStatsObservers(), deps);
|
||||||
getBaseDir(mStatsDir), deps);
|
|
||||||
|
|
||||||
mElapsedRealtime = 0L;
|
mElapsedRealtime = 0L;
|
||||||
|
|
||||||
@@ -338,6 +358,44 @@ public class NetworkStatsServiceTest extends NetworkStatsBaseTest {
|
|||||||
@NonNull
|
@NonNull
|
||||||
private NetworkStatsService.Dependencies makeDependencies() {
|
private NetworkStatsService.Dependencies makeDependencies() {
|
||||||
return new NetworkStatsService.Dependencies() {
|
return new NetworkStatsService.Dependencies() {
|
||||||
|
@Override
|
||||||
|
public File getLegacyStatsDir() {
|
||||||
|
return mLegacyStatsDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public File getOrCreateStatsDir() {
|
||||||
|
return mStatsDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean getStoreFilesInApexData() {
|
||||||
|
return mStoreFilesInApexData;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getImportLegacyTargetAttempts() {
|
||||||
|
return mImportLegacyTargetAttempts;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PersistentInt createImportLegacyAttemptsCounter(
|
||||||
|
@androidx.annotation.NonNull Path path) {
|
||||||
|
return mImportLegacyAttemptsCounter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PersistentInt createImportLegacySuccessesCounter(
|
||||||
|
@androidx.annotation.NonNull Path path) {
|
||||||
|
return mImportLegacySuccessesCounter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NetworkStatsCollection readPlatformCollection(
|
||||||
|
@NonNull String prefix, long bucketDuration) {
|
||||||
|
return mPlatformNetworkStatsCollection.get(prefix);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HandlerThread makeHandlerThread() {
|
public HandlerThread makeHandlerThread() {
|
||||||
return mHandlerThread;
|
return mHandlerThread;
|
||||||
@@ -1704,10 +1762,108 @@ public class NetworkStatsServiceTest extends NetworkStatsBaseTest {
|
|||||||
assertNetworkTotal(sTemplateImsi1, 0L, 0L, 0L, 0L, 0);
|
assertNetworkTotal(sTemplateImsi1, 0L, 0L, 0L, 0L, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static File getBaseDir(File statsDir) {
|
/**
|
||||||
File baseDir = new File(statsDir, "netstats");
|
* Verify the service will perform data migration process can be controlled by the device flag.
|
||||||
baseDir.mkdirs();
|
*/
|
||||||
return baseDir;
|
@Test
|
||||||
|
public void testDataMigration() throws Exception {
|
||||||
|
assertStatsFilesExist(false);
|
||||||
|
expectDefaultSettings();
|
||||||
|
|
||||||
|
NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildWifiState()};
|
||||||
|
|
||||||
|
mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
|
||||||
|
new UnderlyingNetworkInfo[0]);
|
||||||
|
|
||||||
|
// modify some number on wifi, and trigger poll event
|
||||||
|
incrementCurrentTime(HOUR_IN_MILLIS);
|
||||||
|
// expectDefaultSettings();
|
||||||
|
expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
|
||||||
|
.insertEntry(TEST_IFACE, 1024L, 8L, 2048L, 16L));
|
||||||
|
expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 2)
|
||||||
|
.insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 512L, 4L, 256L, 2L, 0L)
|
||||||
|
.insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xFAAD, 256L, 2L, 128L, 1L, 0L)
|
||||||
|
.insertEntry(TEST_IFACE, UID_RED, SET_FOREGROUND, TAG_NONE, 512L, 4L, 256L, 2L, 0L)
|
||||||
|
.insertEntry(TEST_IFACE, UID_RED, SET_FOREGROUND, 0xFAAD, 256L, 2L, 128L, 1L, 0L)
|
||||||
|
.insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 128L, 1L, 128L, 1L, 0L));
|
||||||
|
|
||||||
|
mService.noteUidForeground(UID_RED, false);
|
||||||
|
verify(mUidCounterSetMap, never()).deleteEntry(any());
|
||||||
|
mService.incrementOperationCount(UID_RED, 0xFAAD, 4);
|
||||||
|
mService.noteUidForeground(UID_RED, true);
|
||||||
|
verify(mUidCounterSetMap).updateEntry(
|
||||||
|
eq(new U32(UID_RED)), eq(new U8((short) SET_FOREGROUND)));
|
||||||
|
mService.incrementOperationCount(UID_RED, 0xFAAD, 6);
|
||||||
|
|
||||||
|
forcePollAndWaitForIdle();
|
||||||
|
// Simulate shutdown to force persisting data
|
||||||
|
mServiceContext.sendBroadcast(new Intent(Intent.ACTION_SHUTDOWN));
|
||||||
|
assertStatsFilesExist(true);
|
||||||
|
|
||||||
|
// Move the files to the legacy directory to simulate an import from old data
|
||||||
|
for (File f : mStatsDir.listFiles()) {
|
||||||
|
Files.move(f.toPath(), mLegacyStatsDir.toPath().resolve(f.getName()));
|
||||||
|
}
|
||||||
|
assertStatsFilesExist(false);
|
||||||
|
|
||||||
|
// Fetch the stats from the legacy files and set platform stats collection to be identical
|
||||||
|
mPlatformNetworkStatsCollection.put(PREFIX_DEV,
|
||||||
|
getLegacyCollection(PREFIX_DEV, false /* includeTags */));
|
||||||
|
mPlatformNetworkStatsCollection.put(PREFIX_XT,
|
||||||
|
getLegacyCollection(PREFIX_XT, false /* includeTags */));
|
||||||
|
mPlatformNetworkStatsCollection.put(PREFIX_UID,
|
||||||
|
getLegacyCollection(PREFIX_UID, false /* includeTags */));
|
||||||
|
mPlatformNetworkStatsCollection.put(PREFIX_UID_TAG,
|
||||||
|
getLegacyCollection(PREFIX_UID_TAG, true /* includeTags */));
|
||||||
|
|
||||||
|
// Mock zero usage and boot through serviceReady(), verify there is no imported data.
|
||||||
|
expectDefaultSettings();
|
||||||
|
expectNetworkStatsUidDetail(buildEmptyStats());
|
||||||
|
expectSystemReady();
|
||||||
|
mService.systemReady();
|
||||||
|
assertStatsFilesExist(false);
|
||||||
|
|
||||||
|
// Set the flag and reboot, verify the imported data is not there until next boot.
|
||||||
|
mStoreFilesInApexData = true;
|
||||||
|
mImportLegacyTargetAttempts = 3;
|
||||||
|
mServiceContext.sendBroadcast(new Intent(Intent.ACTION_SHUTDOWN));
|
||||||
|
assertStatsFilesExist(false);
|
||||||
|
|
||||||
|
// Boot through systemReady() again.
|
||||||
|
expectDefaultSettings();
|
||||||
|
expectNetworkStatsUidDetail(buildEmptyStats());
|
||||||
|
expectSystemReady();
|
||||||
|
mService.systemReady();
|
||||||
|
|
||||||
|
// After systemReady(), the service should have historical stats loaded again.
|
||||||
|
// Thus, verify
|
||||||
|
// 1. The stats are absorbed by the recorder.
|
||||||
|
// 2. The imported data are persisted.
|
||||||
|
// 3. The attempts count is set to target attempts count to indicate a successful
|
||||||
|
// migration.
|
||||||
|
assertNetworkTotal(sTemplateWifi, 1024L, 8L, 2048L, 16L, 0);
|
||||||
|
assertStatsFilesExist(true);
|
||||||
|
verify(mImportLegacyAttemptsCounter).set(3);
|
||||||
|
verify(mImportLegacySuccessesCounter).set(1);
|
||||||
|
|
||||||
|
// TODO: Verify upgrading with Exception won't damege original data and
|
||||||
|
// will decrease the retry counter by 1.
|
||||||
|
}
|
||||||
|
|
||||||
|
private NetworkStatsRecorder makeTestRecorder(File directory, String prefix, Config config,
|
||||||
|
boolean includeTags) {
|
||||||
|
final NetworkStats.NonMonotonicObserver observer =
|
||||||
|
mock(NetworkStats.NonMonotonicObserver.class);
|
||||||
|
final DropBoxManager dropBox = mock(DropBoxManager.class);
|
||||||
|
return new NetworkStatsRecorder(new FileRotator(
|
||||||
|
directory, prefix, config.rotateAgeMillis, config.deleteAgeMillis),
|
||||||
|
observer, dropBox, prefix, config.bucketDuration, includeTags);
|
||||||
|
}
|
||||||
|
|
||||||
|
private NetworkStatsCollection getLegacyCollection(String prefix, boolean includeTags) {
|
||||||
|
final NetworkStatsRecorder recorder = makeTestRecorder(mLegacyStatsDir, PREFIX_DEV,
|
||||||
|
mSettings.getDevConfig(), includeTags);
|
||||||
|
return recorder.getOrLoadCompleteLocked();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertNetworkTotal(NetworkTemplate template, long rxBytes, long rxPackets,
|
private void assertNetworkTotal(NetworkTemplate template, long rxBytes, long rxPackets,
|
||||||
@@ -1816,11 +1972,10 @@ public class NetworkStatsServiceTest extends NetworkStatsBaseTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void assertStatsFilesExist(boolean exist) {
|
private void assertStatsFilesExist(boolean exist) {
|
||||||
final File basePath = new File(mStatsDir, "netstats");
|
|
||||||
if (exist) {
|
if (exist) {
|
||||||
assertTrue(basePath.list().length > 0);
|
assertTrue(mStatsDir.list().length > 0);
|
||||||
} else {
|
} else {
|
||||||
assertTrue(basePath.list().length == 0);
|
assertTrue(mStatsDir.list().length == 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
133
tests/unit/java/com/android/server/net/PersistentIntTest.kt
Normal file
133
tests/unit/java/com/android/server/net/PersistentIntTest.kt
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.server.net
|
||||||
|
|
||||||
|
import android.util.SystemConfigFileCommitEventLogger
|
||||||
|
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
|
||||||
|
import com.android.testutils.DevSdkIgnoreRunner
|
||||||
|
import com.android.testutils.SC_V2
|
||||||
|
import com.android.testutils.assertThrows
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.nio.file.attribute.PosixFilePermission
|
||||||
|
import java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE
|
||||||
|
import java.nio.file.attribute.PosixFilePermission.OWNER_READ
|
||||||
|
import java.nio.file.attribute.PosixFilePermission.OWNER_WRITE
|
||||||
|
import java.util.Random
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
@RunWith(DevSdkIgnoreRunner::class)
|
||||||
|
@IgnoreUpTo(SC_V2)
|
||||||
|
class PersistentIntTest {
|
||||||
|
val tempFilesCreated = mutableSetOf<Path>()
|
||||||
|
lateinit var tempDir: Path
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
tempDir = Files.createTempDirectory("tmp.PersistentIntTest.")
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
var permissions = setOf(OWNER_READ, OWNER_WRITE, OWNER_EXECUTE)
|
||||||
|
Files.setPosixFilePermissions(tempDir, permissions)
|
||||||
|
|
||||||
|
for (file in tempFilesCreated) {
|
||||||
|
Files.deleteIfExists(file)
|
||||||
|
}
|
||||||
|
Files.delete(tempDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testNormalReadWrite() {
|
||||||
|
// New, initialized to 0.
|
||||||
|
val pi = createPersistentInt()
|
||||||
|
assertEquals(0, pi.get())
|
||||||
|
pi.set(12345)
|
||||||
|
assertEquals(12345, pi.get())
|
||||||
|
|
||||||
|
// Existing.
|
||||||
|
val pi2 = createPersistentInt(pathOf(pi))
|
||||||
|
assertEquals(12345, pi2.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testReadOrWriteFailsInCreate() {
|
||||||
|
setWritable(tempDir, false)
|
||||||
|
assertThrows(IOException::class.java) {
|
||||||
|
createPersistentInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testReadOrWriteFailsAfterCreate() {
|
||||||
|
val pi = createPersistentInt()
|
||||||
|
pi.set(42)
|
||||||
|
assertEquals(42, pi.get())
|
||||||
|
|
||||||
|
val path = pathOf(pi)
|
||||||
|
setReadable(path, false)
|
||||||
|
assertThrows(IOException::class.java) { pi.get() }
|
||||||
|
pi.set(77)
|
||||||
|
|
||||||
|
setReadable(path, true)
|
||||||
|
setWritable(path, false)
|
||||||
|
setWritable(tempDir, false) // Writing creates a new file+renames, make this fail.
|
||||||
|
assertThrows(IOException::class.java) { pi.set(99) }
|
||||||
|
assertEquals(77, pi.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addOrRemovePermission(p: Path, permission: PosixFilePermission, add: Boolean) {
|
||||||
|
val permissions = Files.getPosixFilePermissions(p)
|
||||||
|
if (add) {
|
||||||
|
permissions.add(permission)
|
||||||
|
} else {
|
||||||
|
permissions.remove(permission)
|
||||||
|
}
|
||||||
|
Files.setPosixFilePermissions(p, permissions)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setReadable(p: Path, readable: Boolean) {
|
||||||
|
addOrRemovePermission(p, OWNER_READ, readable)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setWritable(p: Path, writable: Boolean) {
|
||||||
|
addOrRemovePermission(p, OWNER_WRITE, writable)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun pathOf(pi: PersistentInt): Path {
|
||||||
|
return File(pi.path).toPath()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createPersistentInt(path: Path = randomTempPath()): PersistentInt {
|
||||||
|
tempFilesCreated.add(path)
|
||||||
|
return PersistentInt(path.toString(),
|
||||||
|
SystemConfigFileCommitEventLogger("PersistentIntTest"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun randomTempPath(): Path {
|
||||||
|
return tempDir.resolve(Integer.toHexString(Random().nextInt())).also {
|
||||||
|
tempFilesCreated.add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user