Handle removed UIDs in network stats and policy.

When UID_REMOVED, clean up any existing UID network policy so it
doesn't linger for future apps.  Also move any NetworkStatsHistory
to special UID_REMOVED tracking bucket.

Tests for new removal code.  Also test detailed UID stats, including
network changes to verify template matching logic.

Bug: 4584212
Change-Id: I9faadf6b6f3830eb45d86c7f1980a27cdbcdb11e
diff --git a/core/java/android/net/NetworkIdentity.java b/core/java/android/net/NetworkIdentity.java
index f82d922..23ebbab 100644
--- a/core/java/android/net/NetworkIdentity.java
+++ b/core/java/android/net/NetworkIdentity.java
@@ -95,9 +95,13 @@
 
         final String subscriberId;
         if (isNetworkTypeMobile(type)) {
-            final TelephonyManager telephony = (TelephonyManager) context.getSystemService(
-                    Context.TELEPHONY_SERVICE);
-            subscriberId = telephony.getSubscriberId();
+            if (state.subscriberId != null) {
+                subscriberId = state.subscriberId;
+            } else {
+                final TelephonyManager telephony = (TelephonyManager) context.getSystemService(
+                        Context.TELEPHONY_SERVICE);
+                subscriberId = telephony.getSubscriberId();
+            }
         } else {
             subscriberId = null;
         }
diff --git a/core/java/android/net/NetworkState.java b/core/java/android/net/NetworkState.java
index 749039a..704111b 100644
--- a/core/java/android/net/NetworkState.java
+++ b/core/java/android/net/NetworkState.java
@@ -29,18 +29,27 @@
     public final NetworkInfo networkInfo;
     public final LinkProperties linkProperties;
     public final LinkCapabilities linkCapabilities;
+    /** Currently only used by testing. */
+    public final String subscriberId;
 
     public NetworkState(NetworkInfo networkInfo, LinkProperties linkProperties,
             LinkCapabilities linkCapabilities) {
+        this(networkInfo, linkProperties, linkCapabilities, null);
+    }
+
+    public NetworkState(NetworkInfo networkInfo, LinkProperties linkProperties,
+            LinkCapabilities linkCapabilities, String subscriberId) {
         this.networkInfo = networkInfo;
         this.linkProperties = linkProperties;
         this.linkCapabilities = linkCapabilities;
+        this.subscriberId = subscriberId;
     }
 
     public NetworkState(Parcel in) {
         networkInfo = in.readParcelable(null);
         linkProperties = in.readParcelable(null);
         linkCapabilities = in.readParcelable(null);
+        subscriberId = in.readString();
     }
 
     /** {@inheritDoc} */
@@ -53,6 +62,7 @@
         out.writeParcelable(networkInfo, flags);
         out.writeParcelable(linkProperties, flags);
         out.writeParcelable(linkCapabilities, flags);
+        out.writeString(subscriberId);
     }
 
     public static final Creator<NetworkState> CREATOR = new Creator<NetworkState>() {
diff --git a/core/java/android/net/TrafficStats.java b/core/java/android/net/TrafficStats.java
index e163abf..cb47193 100644
--- a/core/java/android/net/TrafficStats.java
+++ b/core/java/android/net/TrafficStats.java
@@ -42,6 +42,14 @@
     public final static int UNSUPPORTED = -1;
 
     /**
+     * Special UID value used when collecting {@link NetworkStatsHistory} for
+     * removed applications.
+     *
+     * @hide
+     */
+    public static final int UID_REMOVED = -4;
+
+    /**
      * Snapshot of {@link NetworkStats} when the currently active profiling
      * session started, or {@code null} if no session active.
      *
diff --git a/services/java/com/android/server/net/NetworkPolicyManagerService.java b/services/java/com/android/server/net/NetworkPolicyManagerService.java
index ada9ba4..55d83a5 100644
--- a/services/java/com/android/server/net/NetworkPolicyManagerService.java
+++ b/services/java/com/android/server/net/NetworkPolicyManagerService.java
@@ -22,6 +22,8 @@
 import static android.Manifest.permission.MANAGE_NETWORK_POLICY;
 import static android.Manifest.permission.READ_NETWORK_USAGE_HISTORY;
 import static android.Manifest.permission.READ_PHONE_STATE;
+import static android.content.Intent.ACTION_UID_REMOVED;
+import static android.content.Intent.EXTRA_UID;
 import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
 import static android.net.ConnectivityManager.TYPE_MOBILE;
 import static android.net.NetworkPolicy.LIMIT_DISABLED;
@@ -247,9 +249,12 @@
         mContext.registerReceiver(mScreenReceiver, screenFilter);
 
         // watch for network interfaces to be claimed
-        final IntentFilter ifaceFilter = new IntentFilter();
-        ifaceFilter.addAction(CONNECTIVITY_ACTION);
-        mContext.registerReceiver(mIfaceReceiver, ifaceFilter, CONNECTIVITY_INTERNAL, mHandler);
+        final IntentFilter connFilter = new IntentFilter(CONNECTIVITY_ACTION);
+        mContext.registerReceiver(mConnReceiver, connFilter, CONNECTIVITY_INTERNAL, mHandler);
+
+        // listen for uid removal to clean policy
+        final IntentFilter removedFilter = new IntentFilter(ACTION_UID_REMOVED);
+        mContext.registerReceiver(mRemovedReceiver, removedFilter, null, mHandler);
 
         // listen for warning polling events; currently dispatched by
         final IntentFilter statsFilter = new IntentFilter(ACTION_NETWORK_STATS_UPDATED);
@@ -312,6 +317,21 @@
         }
     };
 
+    private BroadcastReceiver mRemovedReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            // on background handler thread, and UID_REMOVED is protected
+            // broadcast.
+            final int uid = intent.getIntExtra(EXTRA_UID, 0);
+            synchronized (mRulesLock) {
+                // remove any policy and update rules to clean up
+                mUidPolicy.delete(uid);
+                updateRulesForUidLocked(uid);
+                writePolicyLocked();
+            }
+        }
+    };
+
     /**
      * Receiver that watches for {@link INetworkStatsService} updates, which we
      * use to check against {@link NetworkPolicy#warningBytes}.
@@ -473,7 +493,7 @@
      * Receiver that watches for {@link IConnectivityManager} to claim network
      * interfaces. Used to apply {@link NetworkPolicy} to matching networks.
      */
-    private BroadcastReceiver mIfaceReceiver = new BroadcastReceiver() {
+    private BroadcastReceiver mConnReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
             // on background handler thread, and verified CONNECTIVITY_INTERNAL
diff --git a/services/java/com/android/server/net/NetworkStatsService.java b/services/java/com/android/server/net/NetworkStatsService.java
index 54a806a..79612e3 100644
--- a/services/java/com/android/server/net/NetworkStatsService.java
+++ b/services/java/com/android/server/net/NetworkStatsService.java
@@ -19,11 +19,14 @@
 import static android.Manifest.permission.CONNECTIVITY_INTERNAL;
 import static android.Manifest.permission.DUMP;
 import static android.Manifest.permission.READ_NETWORK_USAGE_HISTORY;
-import static android.Manifest.permission.SHUTDOWN;
+import static android.content.Intent.ACTION_SHUTDOWN;
+import static android.content.Intent.ACTION_UID_REMOVED;
+import static android.content.Intent.EXTRA_UID;
 import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
 import static android.net.NetworkStats.IFACE_ALL;
 import static android.net.NetworkStats.TAG_NONE;
 import static android.net.NetworkStats.UID_ALL;
+import static android.net.TrafficStats.UID_REMOVED;
 import static android.provider.Settings.Secure.NETSTATS_NETWORK_BUCKET_DURATION;
 import static android.provider.Settings.Secure.NETSTATS_NETWORK_MAX_HISTORY;
 import static android.provider.Settings.Secure.NETSTATS_PERSIST_THRESHOLD;
@@ -206,17 +209,20 @@
         }
 
         // watch for network interfaces to be claimed
-        final IntentFilter ifaceFilter = new IntentFilter();
-        ifaceFilter.addAction(CONNECTIVITY_ACTION);
-        mContext.registerReceiver(mIfaceReceiver, ifaceFilter, CONNECTIVITY_INTERNAL, mHandler);
+        final IntentFilter connFilter = new IntentFilter(CONNECTIVITY_ACTION);
+        mContext.registerReceiver(mConnReceiver, connFilter, CONNECTIVITY_INTERNAL, mHandler);
 
         // listen for periodic polling events
         final IntentFilter pollFilter = new IntentFilter(ACTION_NETWORK_STATS_POLL);
         mContext.registerReceiver(mPollReceiver, pollFilter, READ_NETWORK_USAGE_HISTORY, mHandler);
 
+        // listen for uid removal to clean stats
+        final IntentFilter removedFilter = new IntentFilter(ACTION_UID_REMOVED);
+        mContext.registerReceiver(mRemovedReceiver, removedFilter, null, mHandler);
+
         // persist stats during clean shutdown
-        final IntentFilter shutdownFilter = new IntentFilter(Intent.ACTION_SHUTDOWN);
-        mContext.registerReceiver(mShutdownReceiver, shutdownFilter, SHUTDOWN, null);
+        final IntentFilter shutdownFilter = new IntentFilter(ACTION_SHUTDOWN);
+        mContext.registerReceiver(mShutdownReceiver, shutdownFilter);
 
         try {
             registerPollAlarmLocked();
@@ -226,8 +232,9 @@
     }
 
     private void shutdownLocked() {
-        mContext.unregisterReceiver(mIfaceReceiver);
+        mContext.unregisterReceiver(mConnReceiver);
         mContext.unregisterReceiver(mPollReceiver);
+        mContext.unregisterReceiver(mRemovedReceiver);
         mContext.unregisterReceiver(mShutdownReceiver);
 
         writeNetworkStatsLocked();
@@ -352,7 +359,7 @@
      * interfaces. Used to associate {@link TelephonyManager#getSubscriberId()}
      * with mobile interfaces.
      */
-    private BroadcastReceiver mIfaceReceiver = new BroadcastReceiver() {
+    private BroadcastReceiver mConnReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
             // on background handler thread, and verified CONNECTIVITY_INTERNAL
@@ -375,10 +382,22 @@
         }
     };
 
+    private BroadcastReceiver mRemovedReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            // on background handler thread, and UID_REMOVED is protected
+            // broadcast.
+            final int uid = intent.getIntExtra(EXTRA_UID, 0);
+            synchronized (mStatsLock) {
+                removeUidLocked(uid);
+            }
+        }
+    };
+
     private BroadcastReceiver mShutdownReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
-            // verified SHUTDOWN permission above.
+            // SHUTDOWN is protected broadcast.
             synchronized (mStatsLock) {
                 shutdownLocked();
             }
@@ -545,6 +564,31 @@
         mLastUidPoll = uidStats;
     }
 
+    /**
+     * Clean up {@link #mUidStats} after UID is removed.
+     */
+    private void removeUidLocked(int uid) {
+        ensureUidStatsLoadedLocked();
+
+        // migrate all UID stats into special "removed" bucket
+        for (NetworkIdentitySet ident : mUidStats.keySet()) {
+            final SparseArray<NetworkStatsHistory> uidStats = mUidStats.get(ident);
+            final NetworkStatsHistory uidHistory = uidStats.get(uid);
+            if (uidHistory != null) {
+                final NetworkStatsHistory removedHistory = findOrCreateUidStatsLocked(
+                        ident, UID_REMOVED);
+                removedHistory.recordEntireHistory(uidHistory);
+                uidStats.remove(uid);
+            }
+        }
+
+        // TODO: push kernel event to wipe stats for UID, otherwise we risk
+        // picking them up again during next poll.
+
+        // since this was radical rewrite, push to disk
+        writeUidStatsLocked();
+    }
+
     private NetworkStatsHistory findOrCreateNetworkStatsLocked(NetworkIdentitySet ident) {
         final long bucketDuration = mSettings.getNetworkBucketDuration();
         final NetworkStatsHistory existing = mNetworkStats.get(ident);
@@ -568,6 +612,8 @@
     }
 
     private NetworkStatsHistory findOrCreateUidStatsLocked(NetworkIdentitySet ident, int uid) {
+        ensureUidStatsLoadedLocked();
+
         // find bucket for identity first, then find uid
         SparseArray<NetworkStatsHistory> uidStats = mUidStats.get(ident);
         if (uidStats == null) {
@@ -734,6 +780,11 @@
     private void writeUidStatsLocked() {
         if (LOGV) Slog.v(TAG, "writeUidStatsLocked()");
 
+        if (!mUidStatsLoaded) {
+            Slog.w(TAG, "asked to write UID stats when not loaded; skipping");
+            return;
+        }
+
         // TODO: consider duplicating stats and releasing lock while writing
 
         FileOutputStream fos = null;
diff --git a/services/tests/servicestests/src/com/android/server/NetworkPolicyManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/NetworkPolicyManagerServiceTest.java
index b62687a..07e5425 100644
--- a/services/tests/servicestests/src/com/android/server/NetworkPolicyManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/NetworkPolicyManagerServiceTest.java
@@ -16,6 +16,8 @@
 
 package com.android.server;
 
+import static android.content.Intent.ACTION_UID_REMOVED;
+import static android.content.Intent.EXTRA_UID;
 import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
 import static android.net.ConnectivityManager.TYPE_WIFI;
 import static android.net.NetworkPolicyManager.POLICY_NONE;
@@ -386,6 +388,22 @@
         verifyAndReset();
     }
 
+    public void testUidRemovedPolicyCleared() throws Exception {
+        // POLICY_REJECT should RULE_REJECT in background
+        expectRulesChanged(UID_A, RULE_REJECT_METERED);
+        replay();
+        mService.setUidPolicy(UID_A, POLICY_REJECT_METERED_BACKGROUND);
+        verifyAndReset();
+
+        // uninstall should clear RULE_REJECT
+        expectRulesChanged(UID_A, RULE_ALLOW_ALL);
+        replay();
+        final Intent intent = new Intent(ACTION_UID_REMOVED);
+        intent.putExtra(EXTRA_UID, UID_A);
+        mServiceContext.sendBroadcast(intent);
+        verifyAndReset();
+    }
+
     private static long parseTime(String time) {
         final Time result = new Time();
         result.parse3339(time);
diff --git a/services/tests/servicestests/src/com/android/server/NetworkStatsServiceTest.java b/services/tests/servicestests/src/com/android/server/NetworkStatsServiceTest.java
index 1d2634c..2c6dbbf 100644
--- a/services/tests/servicestests/src/com/android/server/NetworkStatsServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/NetworkStatsServiceTest.java
@@ -16,11 +16,16 @@
 
 package com.android.server;
 
+import static android.content.Intent.ACTION_UID_REMOVED;
+import static android.content.Intent.EXTRA_UID;
 import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
+import static android.net.ConnectivityManager.TYPE_MOBILE;
 import static android.net.ConnectivityManager.TYPE_WIFI;
 import static android.net.NetworkStats.TAG_NONE;
 import static android.net.NetworkStats.UID_ALL;
+import static android.net.NetworkTemplate.MATCH_MOBILE_ALL;
 import static android.net.NetworkTemplate.MATCH_WIFI;
+import static android.net.TrafficStats.UID_REMOVED;
 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;
@@ -46,6 +51,7 @@
 import android.net.NetworkStatsHistory;
 import android.net.NetworkTemplate;
 import android.os.INetworkManagementService;
+import android.telephony.TelephonyManager;
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.LargeTest;
 import android.util.TrustedTime;
@@ -67,10 +73,16 @@
     private static final String TEST_IFACE = "test0";
     private static final long TEST_START = 1194220800000L;
 
-    private static NetworkTemplate sTemplateWifi = new NetworkTemplate(MATCH_WIFI, null);
+    private static final String IMSI_1 = "310004";
+    private static final String IMSI_2 = "310260";
 
-    private static final int TEST_UID_1 = 1001;
-    private static final int TEST_UID_2 = 1002;
+    private static NetworkTemplate sTemplateWifi = new NetworkTemplate(MATCH_WIFI, null);
+    private static NetworkTemplate sTemplateImsi1 = new NetworkTemplate(MATCH_MOBILE_ALL, IMSI_1);
+    private static NetworkTemplate sTemplateImsi2 = new NetworkTemplate(MATCH_MOBILE_ALL, IMSI_2);
+
+    private static final int TEST_UID_RED = 1001;
+    private static final int TEST_UID_BLUE = 1002;
+    private static final int TEST_UID_GREEN = 1003;
 
     private BroadcastInterceptingContext mServiceContext;
     private File mStatsDir;
@@ -121,13 +133,15 @@
         mNetManager = null;
         mAlarmManager = null;
         mTime = null;
+        mSettings = null;
+        mConnManager = null;
 
         mService = null;
 
         super.tearDown();
     }
 
-    public void testSummaryStatsWifi() throws Exception {
+    public void testNetworkStatsWifi() throws Exception {
         long elapsedRealtime = 0;
 
         // pretend that wifi network comes online; service should ask about full
@@ -202,16 +216,16 @@
         expectNetworkStatsSummary(new NetworkStats(elapsedRealtime, 1)
                 .addEntry(TEST_IFACE, UID_ALL, TAG_NONE, 1024L, 2048L));
         expectNetworkStatsDetail(new NetworkStats(elapsedRealtime, 2)
-                .addEntry(TEST_IFACE, TEST_UID_1, TAG_NONE, 512L, 256L)
-                .addEntry(TEST_IFACE, TEST_UID_2, TAG_NONE, 128L, 128L));
+                .addEntry(TEST_IFACE, TEST_UID_RED, TAG_NONE, 512L, 256L)
+                .addEntry(TEST_IFACE, TEST_UID_BLUE, TAG_NONE, 128L, 128L));
 
         replay();
         mServiceContext.sendBroadcast(new Intent(ACTION_NETWORK_STATS_POLL));
 
         // verify service recorded history
         assertNetworkTotal(sTemplateWifi, 1024L, 2048L);
-        assertUidTotal(sTemplateWifi, TEST_UID_1, 512L, 256L);
-        assertUidTotal(sTemplateWifi, TEST_UID_2, 128L, 128L);
+        assertUidTotal(sTemplateWifi, TEST_UID_RED, 512L, 256L);
+        assertUidTotal(sTemplateWifi, TEST_UID_BLUE, 128L, 128L);
         verifyAndReset();
 
         // graceful shutdown system, which should trigger persist of stats, and
@@ -236,8 +250,8 @@
 
         // after systemReady(), we should have historical stats loaded again
         assertNetworkTotal(sTemplateWifi, 1024L, 2048L);
-        assertUidTotal(sTemplateWifi, TEST_UID_1, 512L, 256L);
-        assertUidTotal(sTemplateWifi, TEST_UID_2, 128L, 128L);
+        assertUidTotal(sTemplateWifi, TEST_UID_RED, 512L, 256L);
+        assertUidTotal(sTemplateWifi, TEST_UID_BLUE, 128L, 128L);
         verifyAndReset();
 
     }
@@ -301,6 +315,135 @@
 
     }
 
+    public void testUidStatsAcrossNetworks() throws Exception {
+        long elapsedRealtime = 0;
+
+        // pretend first mobile network comes online
+        expectTime(TEST_START + elapsedRealtime);
+        expectDefaultSettings();
+        expectNetworkState(buildMobile3gState(IMSI_1));
+        expectNetworkStatsSummary(buildEmptyStats(elapsedRealtime));
+
+        replay();
+        mServiceContext.sendBroadcast(new Intent(CONNECTIVITY_ACTION));
+        verifyAndReset();
+
+        // create some traffic on first network
+        elapsedRealtime += HOUR_IN_MILLIS;
+        expectTime(TEST_START + elapsedRealtime);
+        expectDefaultSettings();
+        expectNetworkStatsSummary(new NetworkStats(elapsedRealtime, 1)
+                .addEntry(TEST_IFACE, UID_ALL, TAG_NONE, 2048L, 512L));
+        expectNetworkStatsDetail(new NetworkStats(elapsedRealtime, 3)
+                .addEntry(TEST_IFACE, TEST_UID_RED, TAG_NONE, 1024L, 0L)
+                .addEntry(TEST_IFACE, TEST_UID_RED, 0xF00D, 512L, 512L)
+                .addEntry(TEST_IFACE, TEST_UID_BLUE, TAG_NONE, 512L, 0L));
+
+        replay();
+        mServiceContext.sendBroadcast(new Intent(ACTION_NETWORK_STATS_POLL));
+
+        // verify service recorded history
+        assertNetworkTotal(sTemplateImsi1, 2048L, 512L);
+        assertNetworkTotal(sTemplateWifi, 0L, 0L);
+        assertUidTotal(sTemplateImsi1, TEST_UID_RED, 1536L, 512L);
+        assertUidTotal(sTemplateImsi1, TEST_UID_BLUE, 512L, 0L);
+        verifyAndReset();
+
+        // now switch networks; this also tests that we're okay with interfaces
+        // disappearing, to verify we don't count backwards.
+        elapsedRealtime += HOUR_IN_MILLIS;
+        expectTime(TEST_START + elapsedRealtime);
+        expectDefaultSettings();
+        expectNetworkState(buildMobile3gState(IMSI_2));
+        expectNetworkStatsSummary(buildEmptyStats(elapsedRealtime));
+        expectNetworkStatsDetail(buildEmptyStats(elapsedRealtime));
+
+        replay();
+        mServiceContext.sendBroadcast(new Intent(CONNECTIVITY_ACTION));
+        mServiceContext.sendBroadcast(new Intent(ACTION_NETWORK_STATS_POLL));
+        verifyAndReset();
+
+        // create traffic on second network
+        elapsedRealtime += HOUR_IN_MILLIS;
+        expectTime(TEST_START + elapsedRealtime);
+        expectDefaultSettings();
+        expectNetworkStatsSummary(new NetworkStats(elapsedRealtime, 1)
+                .addEntry(TEST_IFACE, UID_ALL, TAG_NONE, 128L, 1024L));
+        expectNetworkStatsDetail(new NetworkStats(elapsedRealtime, 1)
+                .addEntry(TEST_IFACE, TEST_UID_BLUE, TAG_NONE, 128L, 1024L));
+
+        replay();
+        mServiceContext.sendBroadcast(new Intent(ACTION_NETWORK_STATS_POLL));
+
+        // verify original history still intact
+        assertNetworkTotal(sTemplateImsi1, 2048L, 512L);
+        assertUidTotal(sTemplateImsi1, TEST_UID_RED, 1536L, 512L);
+        assertUidTotal(sTemplateImsi1, TEST_UID_BLUE, 512L, 0L);
+
+        // and verify new history also recorded under different template, which
+        // verifies that we didn't cross the streams.
+        assertNetworkTotal(sTemplateImsi2, 128L, 1024L);
+        assertNetworkTotal(sTemplateWifi, 0L, 0L);
+        assertUidTotal(sTemplateImsi2, TEST_UID_BLUE, 128L, 1024L);
+        verifyAndReset();
+
+    }
+
+    public void testUidRemovedIsMoved() throws Exception {
+        long elapsedRealtime = 0;
+
+        // pretend that network comes online
+        expectTime(TEST_START + elapsedRealtime);
+        expectDefaultSettings();
+        expectNetworkState(buildWifiState());
+        expectNetworkStatsSummary(buildEmptyStats(elapsedRealtime));
+
+        replay();
+        mServiceContext.sendBroadcast(new Intent(CONNECTIVITY_ACTION));
+        verifyAndReset();
+
+        // create some traffic
+        elapsedRealtime += HOUR_IN_MILLIS;
+        expectTime(TEST_START + elapsedRealtime);
+        expectDefaultSettings();
+        expectNetworkStatsSummary(new NetworkStats(elapsedRealtime, 1)
+                .addEntry(TEST_IFACE, UID_ALL, TAG_NONE, 4128L, 544L));
+        expectNetworkStatsDetail(new NetworkStats(elapsedRealtime, 1)
+                .addEntry(TEST_IFACE, TEST_UID_RED, TAG_NONE, 16L, 16L)
+                .addEntry(TEST_IFACE, TEST_UID_BLUE, TAG_NONE, 4096L, 512L)
+                .addEntry(TEST_IFACE, TEST_UID_GREEN, TAG_NONE, 16L, 16L));
+
+        replay();
+        mServiceContext.sendBroadcast(new Intent(ACTION_NETWORK_STATS_POLL));
+
+        // verify service recorded history
+        assertNetworkTotal(sTemplateWifi, 4128L, 544L);
+        assertUidTotal(sTemplateWifi, TEST_UID_RED, 16L, 16L);
+        assertUidTotal(sTemplateWifi, TEST_UID_BLUE, 4096L, 512L);
+        assertUidTotal(sTemplateWifi, TEST_UID_GREEN, 16L, 16L);
+        verifyAndReset();
+
+        // now pretend two UIDs are uninstalled, which should migrate stats to
+        // special "removed" bucket.
+        expectDefaultSettings();
+        replay();
+        final Intent intent = new Intent(ACTION_UID_REMOVED);
+        intent.putExtra(EXTRA_UID, TEST_UID_BLUE);
+        mServiceContext.sendBroadcast(intent);
+        intent.putExtra(EXTRA_UID, TEST_UID_RED);
+        mServiceContext.sendBroadcast(intent);
+
+        // existing uid and total should remain unchanged; but removed UID
+        // should be gone completely.
+        assertNetworkTotal(sTemplateWifi, 4128L, 544L);
+        assertUidTotal(sTemplateWifi, TEST_UID_RED, 0L, 0L);
+        assertUidTotal(sTemplateWifi, TEST_UID_BLUE, 0L, 0L);
+        assertUidTotal(sTemplateWifi, TEST_UID_GREEN, 16L, 16L);
+        assertUidTotal(sTemplateWifi, UID_REMOVED, 4112L, 528L);
+        verifyAndReset();
+
+    }
+
     private void assertNetworkTotal(NetworkTemplate template, long rx, long tx) {
         final NetworkStatsHistory history = mService.getHistoryForNetwork(template);
         final long[] total = history.getTotalData(Long.MIN_VALUE, Long.MAX_VALUE, null);
@@ -360,14 +503,14 @@
     }
 
     private void assertStatsFilesExist(boolean exist) {
-        final File summaryFile = new File(mStatsDir, "netstats.bin");
-        final File detailFile = new File(mStatsDir, "netstats_uid.bin");
+        final File networkFile = new File(mStatsDir, "netstats.bin");
+        final File uidFile = new File(mStatsDir, "netstats_uid.bin");
         if (exist) {
-            assertTrue(summaryFile.exists());
-            assertTrue(detailFile.exists());
+            assertTrue(networkFile.exists());
+            assertTrue(uidFile.exists());
         } else {
-            assertFalse(summaryFile.exists());
-            assertFalse(detailFile.exists());
+            assertFalse(networkFile.exists());
+            assertFalse(uidFile.exists());
         }
     }
 
@@ -379,6 +522,15 @@
         return new NetworkState(info, prop, null);
     }
 
+    private static NetworkState buildMobile3gState(String subscriberId) {
+        final NetworkInfo info = new NetworkInfo(
+                TYPE_MOBILE, TelephonyManager.NETWORK_TYPE_UMTS, null, null);
+        info.setDetailedState(DetailedState.CONNECTED, null, null);
+        final LinkProperties prop = new LinkProperties();
+        prop.setInterfaceName(TEST_IFACE);
+        return new NetworkState(info, prop, null, subscriberId);
+    }
+
     private static NetworkStats buildEmptyStats(long elapsedRealtime) {
         return new NetworkStats(elapsedRealtime, 0);
     }