UID network stats, secure settings, and random.

Collect UID-granularity network stats during regular poll event.  Add
dumpsys argument to generate fake historical data for debugging, and
move stats parameters to Settings.Secure.

Change-Id: I09b36a2955dc10c697d4b9c3ff23dcb3ac37bd70
diff --git a/core/java/android/net/NetworkStats.java b/core/java/android/net/NetworkStats.java
index 588bf64..ee415fa 100644
--- a/core/java/android/net/NetworkStats.java
+++ b/core/java/android/net/NetworkStats.java
@@ -19,6 +19,7 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.SystemClock;
+import android.util.SparseBooleanArray;
 
 import java.io.CharArrayWriter;
 import java.io.PrintWriter;
@@ -125,7 +126,7 @@
     /**
      * Return list of unique interfaces known by this data structure.
      */
-    public String[] getKnownIfaces() {
+    public String[] getUniqueIfaces() {
         final HashSet<String> ifaces = new HashSet<String>();
         for (String iface : this.iface) {
             if (iface != IFACE_ALL) {
@@ -136,6 +137,23 @@
     }
 
     /**
+     * Return list of unique UIDs known by this data structure.
+     */
+    public int[] getUniqueUids() {
+        final SparseBooleanArray uids = new SparseBooleanArray();
+        for (int uid : this.uid) {
+            uids.put(uid, true);
+        }
+
+        final int size = uids.size();
+        final int[] result = new int[size];
+        for (int i = 0; i < size; i++) {
+            result[i] = uids.keyAt(i);
+        }
+        return result;
+    }
+
+    /**
      * Subtract the given {@link NetworkStats}, effectively leaving the delta
      * between two snapshots in time. Assumes that statistics rows collect over
      * time, and that none of them have disappeared.
diff --git a/core/java/android/net/NetworkStatsHistory.java b/core/java/android/net/NetworkStatsHistory.java
index 60af475..45d8e27 100644
--- a/core/java/android/net/NetworkStatsHistory.java
+++ b/core/java/android/net/NetworkStatsHistory.java
@@ -24,7 +24,9 @@
 import java.io.DataOutputStream;
 import java.io.IOException;
 import java.io.PrintWriter;
+import java.net.ProtocolException;
 import java.util.Arrays;
+import java.util.Random;
 
 /**
  * Collection of historical network statistics, recorded into equally-sized
@@ -38,7 +40,7 @@
  * @hide
  */
 public class NetworkStatsHistory implements Parcelable {
-    private static final int VERSION = 1;
+    private static final int VERSION_CURRENT = 1;
 
     // TODO: teach about zigzag encoding to use less disk space
     // TODO: teach how to convert between bucket sizes
@@ -76,15 +78,23 @@
 
     public NetworkStatsHistory(DataInputStream in) throws IOException {
         final int version = in.readInt();
-        bucketDuration = in.readLong();
-        bucketStart = readLongArray(in);
-        rx = readLongArray(in);
-        tx = readLongArray(in);
-        bucketCount = bucketStart.length;
+        switch (version) {
+            case VERSION_CURRENT: {
+                bucketDuration = in.readLong();
+                bucketStart = readLongArray(in);
+                rx = readLongArray(in);
+                tx = readLongArray(in);
+                bucketCount = bucketStart.length;
+                break;
+            }
+            default: {
+                throw new ProtocolException("unexpected version: " + version);
+            }
+        }
     }
 
     public void writeToStream(DataOutputStream out) throws IOException {
-        out.writeInt(VERSION);
+        out.writeInt(VERSION_CURRENT);
         out.writeLong(bucketDuration);
         writeLongArray(out, bucketStart, bucketCount);
         writeLongArray(out, rx, bucketCount);
@@ -192,12 +202,37 @@
         }
     }
 
+    /**
+     * @deprecated only for temporary testing
+     */
+    @Deprecated
+    public void generateRandom(long start, long end, long rx, long tx) {
+        ensureBuckets(start, end);
+
+        final Random r = new Random();
+        while (rx > 1024 && tx > 1024) {
+            final long curStart = randomLong(r, start, end);
+            final long curEnd = randomLong(r, curStart, end);
+            final long curRx = randomLong(r, 0, rx);
+            final long curTx = randomLong(r, 0, tx);
+
+            recordData(curStart, curEnd, curRx, curTx);
+
+            rx -= curRx;
+            tx -= curTx;
+        }
+    }
+
+    private static long randomLong(Random r, long start, long end) {
+        return (long) (start + (r.nextFloat() * (end - start)));
+    }
+
     public void dump(String prefix, PrintWriter pw) {
         pw.print(prefix);
-        pw.println("NetworkStatsHistory:");
+        pw.print("NetworkStatsHistory: bucketDuration="); pw.println(bucketDuration);
         for (int i = 0; i < bucketCount; i++) {
             pw.print(prefix);
-            pw.print("  timestamp="); pw.print(bucketStart[i]);
+            pw.print("  bucketStart="); pw.print(bucketStart[i]);
             pw.print(" rx="); pw.print(rx[i]);
             pw.print(" tx="); pw.println(tx[i]);
         }
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 2126793..6ab7738 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -3795,6 +3795,19 @@
         public static final String DREAM_TIMEOUT =
                 "dream_timeout";
 
+        /** {@hide} */
+        public static final String NETSTATS_POLL_INTERVAL = "netstats_poll_interval";
+        /** {@hide} */
+        public static final String NETSTATS_PERSIST_THRESHOLD = "netstats_persist_threshold";
+        /** {@hide} */
+        public static final String NETSTATS_SUMMARY_BUCKET_DURATION = "netstats_summary_bucket_duration";
+        /** {@hide} */
+        public static final String NETSTATS_SUMMARY_MAX_HISTORY = "netstats_summary_max_history";
+        /** {@hide} */
+        public static final String NETSTATS_DETAIL_BUCKET_DURATION = "netstats_detail_bucket_duration";
+        /** {@hide} */
+        public static final String NETSTATS_DETAIL_MAX_HISTORY = "netstats_detail_max_history";
+
         /**
          * @hide
          */
diff --git a/services/java/com/android/server/net/NetworkStatsService.java b/services/java/com/android/server/net/NetworkStatsService.java
index 66d1274..967e491 100644
--- a/services/java/com/android/server/net/NetworkStatsService.java
+++ b/services/java/com/android/server/net/NetworkStatsService.java
@@ -21,7 +21,17 @@
 import static android.Manifest.permission.SHUTDOWN;
 import static android.Manifest.permission.UPDATE_DEVICE_STATS;
 import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
+import static android.net.NetworkStats.IFACE_ALL;
 import static android.net.NetworkStats.UID_ALL;
+import static android.provider.Settings.Secure.NETSTATS_DETAIL_BUCKET_DURATION;
+import static android.provider.Settings.Secure.NETSTATS_DETAIL_MAX_HISTORY;
+import static android.provider.Settings.Secure.NETSTATS_PERSIST_THRESHOLD;
+import static android.provider.Settings.Secure.NETSTATS_POLL_INTERVAL;
+import static android.provider.Settings.Secure.NETSTATS_SUMMARY_BUCKET_DURATION;
+import static android.provider.Settings.Secure.NETSTATS_SUMMARY_MAX_HISTORY;
+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 com.android.internal.util.Preconditions.checkNotNull;
 
 import android.app.AlarmManager;
@@ -31,6 +41,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
 import android.net.IConnectivityManager;
 import android.net.INetworkStatsService;
 import android.net.NetworkInfo;
@@ -42,10 +53,11 @@
 import android.os.INetworkManagementService;
 import android.os.RemoteException;
 import android.os.SystemClock;
+import android.provider.Settings;
 import android.telephony.TelephonyManager;
-import android.text.format.DateUtils;
 import android.util.NtpTrustedTime;
 import android.util.Slog;
+import android.util.SparseArray;
 import android.util.TrustedTime;
 
 import com.google.android.collect.Lists;
@@ -55,6 +67,7 @@
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.HashSet;
 
 /**
  * Collect and persist detailed network statistics, and provide this data to
@@ -76,34 +89,42 @@
 
     private PendingIntent mPollIntent;
 
-    // TODO: move tweakable params to Settings.Secure
     // TODO: listen for kernel push events through netd instead of polling
 
     private static final long KB_IN_BYTES = 1024;
+    private static final long MB_IN_BYTES = 1024 * KB_IN_BYTES;
+    private static final long GB_IN_BYTES = 1024 * MB_IN_BYTES;
 
-    private static final long POLL_INTERVAL = AlarmManager.INTERVAL_FIFTEEN_MINUTES;
-    private static final long SUMMARY_BUCKET_DURATION = 6 * DateUtils.HOUR_IN_MILLIS;
-    private static final long SUMMARY_MAX_HISTORY = 90 * DateUtils.DAY_IN_MILLIS;
+    private LongSecureSetting mPollInterval = new LongSecureSetting(
+            NETSTATS_POLL_INTERVAL, 15 * MINUTE_IN_MILLIS);
+    private LongSecureSetting mPersistThreshold = new LongSecureSetting(
+            NETSTATS_PERSIST_THRESHOLD, 64 * KB_IN_BYTES);
 
-    // TODO: remove these high-frequency testing values
-//    private static final long POLL_INTERVAL = 5 * DateUtils.SECOND_IN_MILLIS;
-//    private static final long SUMMARY_BUCKET_DURATION = 10 * DateUtils.SECOND_IN_MILLIS;
-//    private static final long SUMMARY_MAX_HISTORY = 2 * DateUtils.MINUTE_IN_MILLIS;
+    private LongSecureSetting mSummaryBucketDuration = new LongSecureSetting(
+            NETSTATS_SUMMARY_BUCKET_DURATION, 6 * HOUR_IN_MILLIS);
+    private LongSecureSetting mSummaryMaxHistory = new LongSecureSetting(
+            NETSTATS_SUMMARY_MAX_HISTORY, 90 * DAY_IN_MILLIS);
+    private LongSecureSetting mDetailBucketDuration = new LongSecureSetting(
+            NETSTATS_DETAIL_BUCKET_DURATION, 6 * HOUR_IN_MILLIS);
+    private LongSecureSetting mDetailMaxHistory = new LongSecureSetting(
+            NETSTATS_DETAIL_MAX_HISTORY, 90 * DAY_IN_MILLIS);
 
-    /** Minimum delta required to persist to disk. */
-    private static final long SUMMARY_PERSIST_THRESHOLD = 64 * KB_IN_BYTES;
-
-    private static final long TIME_CACHE_MAX_AGE = DateUtils.DAY_IN_MILLIS;
+    private static final long TIME_CACHE_MAX_AGE = DAY_IN_MILLIS;
 
     private final Object mStatsLock = new Object();
 
     /** Set of active ifaces during this boot. */
     private HashMap<String, InterfaceIdentity> mActiveIface = Maps.newHashMap();
-    /** Set of historical stats for known ifaces. */
-    private HashMap<InterfaceIdentity, NetworkStatsHistory> mStats = Maps.newHashMap();
 
-    private NetworkStats mLastPollStats;
-    private NetworkStats mLastPersistStats;
+    /** Set of historical stats for known ifaces. */
+    private HashMap<InterfaceIdentity, NetworkStatsHistory> mSummaryStats = Maps.newHashMap();
+    /** Set of historical stats for known UIDs. */
+    private SparseArray<NetworkStatsHistory> mDetailStats = new SparseArray<NetworkStatsHistory>();
+
+    private NetworkStats mLastSummaryPoll;
+    private NetworkStats mLastSummaryPersist;
+
+    private NetworkStats mLastDetailPoll;
 
     private final HandlerThread mHandlerThread;
     private final Handler mHandler;
@@ -161,7 +182,7 @@
 
     /**
      * Clear any existing {@link #ACTION_NETWORK_STATS_POLL} alarms, and
-     * reschedule based on current {@link #POLL_INTERVAL} value.
+     * reschedule based on current {@link #mPollInterval} value.
      */
     private void registerPollAlarmLocked() throws RemoteException {
         if (mPollIntent != null) {
@@ -173,7 +194,7 @@
 
         final long currentRealtime = SystemClock.elapsedRealtime();
         mAlarmManager.setInexactRepeating(
-                AlarmManager.ELAPSED_REALTIME, currentRealtime, POLL_INTERVAL, mPollIntent);
+                AlarmManager.ELAPSED_REALTIME, currentRealtime, mPollInterval.get(), mPollIntent);
     }
 
     @Override
@@ -184,9 +205,10 @@
         synchronized (mStatsLock) {
             // combine all interfaces that match template
             final String subscriberId = getActiveSubscriberId();
-            final NetworkStatsHistory combined = new NetworkStatsHistory(SUMMARY_BUCKET_DURATION);
-            for (InterfaceIdentity ident : mStats.keySet()) {
-                final NetworkStatsHistory history = mStats.get(ident);
+            final NetworkStatsHistory combined = new NetworkStatsHistory(
+                    mSummaryBucketDuration.get());
+            for (InterfaceIdentity ident : mSummaryStats.keySet()) {
+                final NetworkStatsHistory history = mSummaryStats.get(ident);
                 if (ident.matchesTemplate(networkTemplate, subscriberId)) {
                     // TODO: combine all matching history data into a single history
                 }
@@ -299,59 +321,97 @@
         final long currentTime = mTime.hasCache() ? mTime.currentTimeMillis()
                 : System.currentTimeMillis();
 
-        final NetworkStats current;
+        final NetworkStats summary;
+        final NetworkStats detail;
         try {
-            current = mNetworkManager.getNetworkStatsSummary();
+            summary = mNetworkManager.getNetworkStatsSummary();
+            detail = mNetworkManager.getNetworkStatsDetail();
         } catch (RemoteException e) {
             Slog.w(TAG, "problem reading network stats");
             return;
         }
 
+        performSummaryPollLocked(summary, currentTime);
+        performDetailPollLocked(detail, currentTime);
+
+        // decide if enough has changed to trigger persist
+        final NetworkStats persistDelta = computeStatsDelta(mLastSummaryPersist, summary);
+        final long persistThreshold = mPersistThreshold.get();
+        for (String iface : persistDelta.getUniqueIfaces()) {
+            final int index = persistDelta.findIndex(iface, UID_ALL);
+            if (persistDelta.rx[index] > persistThreshold
+                    || persistDelta.tx[index] > persistThreshold) {
+                writeStatsLocked();
+                mLastSummaryPersist = summary;
+                break;
+            }
+        }
+    }
+
+    /**
+     * Update {@link #mSummaryStats} historical usage.
+     */
+    private void performSummaryPollLocked(NetworkStats summary, long currentTime) {
         final ArrayList<String> unknownIface = Lists.newArrayList();
 
-        // update historical usage with delta since last poll
-        final NetworkStats pollDelta = computeStatsDelta(mLastPollStats, current);
-        final long timeStart = currentTime - pollDelta.elapsedRealtime;
-        for (String iface : pollDelta.getKnownIfaces()) {
+        final NetworkStats delta = computeStatsDelta(mLastSummaryPoll, summary);
+        final long timeStart = currentTime - delta.elapsedRealtime;
+        final long maxHistory = mSummaryMaxHistory.get();
+        for (String iface : delta.getUniqueIfaces()) {
             final InterfaceIdentity ident = mActiveIface.get(iface);
             if (ident == null) {
                 unknownIface.add(iface);
                 continue;
             }
 
-            final int index = pollDelta.findIndex(iface, UID_ALL);
-            final long rx = pollDelta.rx[index];
-            final long tx = pollDelta.tx[index];
+            final int index = delta.findIndex(iface, UID_ALL);
+            final long rx = delta.rx[index];
+            final long tx = delta.tx[index];
 
-            final NetworkStatsHistory history = findOrCreateHistoryLocked(ident);
+            final NetworkStatsHistory history = findOrCreateSummaryLocked(ident);
             history.recordData(timeStart, currentTime, rx, tx);
-            history.removeBucketsBefore(currentTime - SUMMARY_MAX_HISTORY);
+            history.removeBucketsBefore(currentTime - maxHistory);
         }
+        mLastSummaryPoll = summary;
 
         if (LOGD && unknownIface.size() > 0) {
             Slog.w(TAG, "unknown interfaces " + unknownIface.toString() + ", ignoring those stats");
         }
-
-        mLastPollStats = current;
-
-        // decide if enough has changed to trigger persist
-        final NetworkStats persistDelta = computeStatsDelta(mLastPersistStats, current);
-        for (String iface : persistDelta.getKnownIfaces()) {
-            final int index = persistDelta.findIndex(iface, UID_ALL);
-            if (persistDelta.rx[index] > SUMMARY_PERSIST_THRESHOLD
-                    || persistDelta.tx[index] > SUMMARY_PERSIST_THRESHOLD) {
-                writeStatsLocked();
-                mLastPersistStats = current;
-                break;
-            }
-        }
     }
 
-    private NetworkStatsHistory findOrCreateHistoryLocked(InterfaceIdentity ident) {
-        NetworkStatsHistory stats = mStats.get(ident);
+    /**
+     * Update {@link #mDetailStats} historical usage.
+     */
+    private void performDetailPollLocked(NetworkStats detail, long currentTime) {
+        final NetworkStats delta = computeStatsDelta(mLastDetailPoll, detail);
+        final long timeStart = currentTime - delta.elapsedRealtime;
+        final long maxHistory = mDetailMaxHistory.get();
+        for (int uid : delta.getUniqueUids()) {
+            final int index = delta.findIndex(IFACE_ALL, uid);
+            final long rx = delta.rx[index];
+            final long tx = delta.tx[index];
+
+            final NetworkStatsHistory history = findOrCreateDetailLocked(uid);
+            history.recordData(timeStart, currentTime, rx, tx);
+            history.removeBucketsBefore(currentTime - maxHistory);
+        }
+        mLastDetailPoll = detail;
+    }
+
+    private NetworkStatsHistory findOrCreateSummaryLocked(InterfaceIdentity ident) {
+        NetworkStatsHistory stats = mSummaryStats.get(ident);
         if (stats == null) {
-            stats = new NetworkStatsHistory(SUMMARY_BUCKET_DURATION);
-            mStats.put(ident, stats);
+            stats = new NetworkStatsHistory(mSummaryBucketDuration.get());
+            mSummaryStats.put(ident, stats);
+        }
+        return stats;
+    }
+
+    private NetworkStatsHistory findOrCreateDetailLocked(int uid) {
+        NetworkStatsHistory stats = mDetailStats.get(uid);
+        if (stats == null) {
+            stats = new NetworkStatsHistory(mDetailBucketDuration.get());
+            mDetailStats.put(uid, stats);
         }
         return stats;
     }
@@ -380,18 +440,89 @@
     protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         mContext.enforceCallingOrSelfPermission(DUMP, TAG);
 
-        pw.println("Active interfaces:");
-        for (String iface : mActiveIface.keySet()) {
-            final InterfaceIdentity ident = mActiveIface.get(iface);
-            pw.print("  iface="); pw.print(iface);
-            pw.print(" ident="); pw.println(ident.toString());
+        final HashSet<String> argSet = new HashSet<String>();
+        for (String arg : args) {
+            argSet.add(arg);
         }
 
-        pw.println("Known historical stats:");
-        for (InterfaceIdentity ident : mStats.keySet()) {
-            final NetworkStatsHistory stats = mStats.get(ident);
-            pw.print("  ident="); pw.println(ident.toString());
-            stats.dump("    ", pw);
+        synchronized (mStatsLock) {
+            // TODO: remove this testing code, since it corrupts stats
+            if (argSet.contains("generate")) {
+                generateRandomLocked();
+                pw.println("Generated stub stats");
+                return;
+            }
+
+            pw.println("Active interfaces:");
+            for (String iface : mActiveIface.keySet()) {
+                final InterfaceIdentity ident = mActiveIface.get(iface);
+                pw.print("  iface="); pw.print(iface);
+                pw.print(" ident="); pw.println(ident.toString());
+            }
+
+            pw.println("Known historical stats:");
+            for (InterfaceIdentity ident : mSummaryStats.keySet()) {
+                final NetworkStatsHistory stats = mSummaryStats.get(ident);
+                pw.print("  ident="); pw.println(ident.toString());
+                stats.dump("    ", pw);
+            }
+
+            if (argSet.contains("detail")) {
+                pw.println("Known detail stats:");
+                for (int i = 0; i < mDetailStats.size(); i++) {
+                    final int uid = mDetailStats.keyAt(i);
+                    final NetworkStatsHistory stats = mDetailStats.valueAt(i);
+                    pw.print("  UID="); pw.println(uid);
+                    stats.dump("    ", pw);
+                }
+            }
+        }
+    }
+
+    /**
+     * @deprecated only for temporary testing
+     */
+    @Deprecated
+    private void generateRandomLocked() {
+        long end = System.currentTimeMillis();
+        long start = end - mSummaryMaxHistory.get();
+        long rx = 3 * GB_IN_BYTES;
+        long tx = 2 * GB_IN_BYTES;
+
+        mSummaryStats.clear();
+        for (InterfaceIdentity ident : mActiveIface.values()) {
+            final NetworkStatsHistory stats = findOrCreateSummaryLocked(ident);
+            stats.generateRandom(start, end, rx, tx);
+        }
+
+        end = System.currentTimeMillis();
+        start = end - mDetailMaxHistory.get();
+        rx = 500 * MB_IN_BYTES;
+        tx = 100 * MB_IN_BYTES;
+
+        mDetailStats.clear();
+        for (ApplicationInfo info : mContext.getPackageManager().getInstalledApplications(0)) {
+            final int uid = info.uid;
+            final NetworkStatsHistory stats = findOrCreateDetailLocked(uid);
+            stats.generateRandom(start, end, rx, tx);
+        }
+    }
+
+    private class LongSecureSetting {
+        private String mKey;
+        private long mDefaultValue;
+
+        public LongSecureSetting(String key, long defaultValue) {
+            mKey = key;
+            mDefaultValue = defaultValue;
+        }
+
+        public long get() {
+            if (mContext != null) {
+                return Settings.Secure.getLong(mContext.getContentResolver(), mKey, mDefaultValue);
+            } else {
+                return mDefaultValue;
+            }
         }
     }