Allow null subscriberId in NetworkStatsManager.

Use a MATCH_MOBILE_WILDCARD template to avoid filtering by
subscriberId when querying statistics from NetworkStatsService.

Bug: 74038898
Change-Id: I4b39e7031416cb33b23d89aa36ff0f774eaa942f
Fixes: 74038898
Test: runtest frameworks-net, CTS tests pass
diff --git a/core/java/android/app/usage/NetworkStats.java b/core/java/android/app/usage/NetworkStats.java
index d33af4f..6ca2349 100644
--- a/core/java/android/app/usage/NetworkStats.java
+++ b/core/java/android/app/usage/NetworkStats.java
@@ -24,7 +24,6 @@
 import android.net.NetworkTemplate;
 import android.net.TrafficStats;
 import android.os.RemoteException;
-import android.os.ServiceManager;
 import android.util.IntArray;
 import android.util.Log;
 
@@ -98,9 +97,8 @@
 
     /** @hide */
     NetworkStats(Context context, NetworkTemplate template, int flags, long startTimestamp,
-            long endTimestamp) throws RemoteException, SecurityException {
-        final INetworkStatsService statsService = INetworkStatsService.Stub.asInterface(
-                ServiceManager.getService(Context.NETWORK_STATS_SERVICE));
+            long endTimestamp, INetworkStatsService statsService)
+            throws RemoteException, SecurityException {
         // Open network stats session
         mSession = statsService.openSessionForUsageStats(flags, context.getOpPackageName());
         mCloseGuard.open("close");
diff --git a/core/java/android/app/usage/NetworkStatsManager.java b/core/java/android/app/usage/NetworkStatsManager.java
index 5576e86..2357637 100644
--- a/core/java/android/app/usage/NetworkStatsManager.java
+++ b/core/java/android/app/usage/NetworkStatsManager.java
@@ -37,6 +37,8 @@
 import android.os.ServiceManager.ServiceNotFoundException;
 import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 /**
  * Provides access to network usage history and statistics. Usage data is collected in
  * discrete bins of time called 'Buckets'. See {@link NetworkStats.Bucket} for details.
@@ -107,9 +109,15 @@
      * {@hide}
      */
     public NetworkStatsManager(Context context) throws ServiceNotFoundException {
+        this(context, INetworkStatsService.Stub.asInterface(
+                ServiceManager.getServiceOrThrow(Context.NETWORK_STATS_SERVICE)));
+    }
+
+    /** @hide */
+    @VisibleForTesting
+    public NetworkStatsManager(Context context, INetworkStatsService service) {
         mContext = context;
-        mService = INetworkStatsService.Stub.asInterface(
-                ServiceManager.getServiceOrThrow(Context.NETWORK_STATS_SERVICE));
+        mService = service;
         setPollOnOpen(true);
     }
 
@@ -135,7 +143,8 @@
     public Bucket querySummaryForDevice(NetworkTemplate template,
             long startTime, long endTime) throws SecurityException, RemoteException {
         Bucket bucket = null;
-        NetworkStats stats = new NetworkStats(mContext, template, mFlags, startTime, endTime);
+        NetworkStats stats = new NetworkStats(mContext, template, mFlags, startTime, endTime,
+                mService);
         bucket = stats.getDeviceSummaryForNetwork();
 
         stats.close();
@@ -208,7 +217,7 @@
         }
 
         NetworkStats stats;
-        stats = new NetworkStats(mContext, template, mFlags, startTime, endTime);
+        stats = new NetworkStats(mContext, template, mFlags, startTime, endTime, mService);
         stats.startSummaryEnumeration();
 
         stats.close();
@@ -245,7 +254,7 @@
         }
 
         NetworkStats result;
-        result = new NetworkStats(mContext, template, mFlags, startTime, endTime);
+        result = new NetworkStats(mContext, template, mFlags, startTime, endTime, mService);
         result.startSummaryEnumeration();
 
         return result;
@@ -295,7 +304,7 @@
 
         NetworkStats result;
         try {
-            result = new NetworkStats(mContext, template, mFlags, startTime, endTime);
+            result = new NetworkStats(mContext, template, mFlags, startTime, endTime, mService);
             result.startHistoryEnumeration(uid, tag);
         } catch (RemoteException e) {
             Log.e(TAG, "Error while querying stats for uid=" + uid + " tag=" + tag, e);
@@ -341,7 +350,7 @@
         }
 
         NetworkStats result;
-        result = new NetworkStats(mContext, template, mFlags, startTime, endTime);
+        result = new NetworkStats(mContext, template, mFlags, startTime, endTime, mService);
         result.startUserUidEnumeration();
         return result;
     }
@@ -451,19 +460,20 @@
     }
 
     private static NetworkTemplate createTemplate(int networkType, String subscriberId) {
-        NetworkTemplate template = null;
+        final NetworkTemplate template;
         switch (networkType) {
-            case ConnectivityManager.TYPE_MOBILE: {
-                template = NetworkTemplate.buildTemplateMobileAll(subscriberId);
-                } break;
-            case ConnectivityManager.TYPE_WIFI: {
+            case ConnectivityManager.TYPE_MOBILE:
+                template = subscriberId == null
+                        ? NetworkTemplate.buildTemplateMobileWildcard()
+                        : NetworkTemplate.buildTemplateMobileAll(subscriberId);
+                break;
+            case ConnectivityManager.TYPE_WIFI:
                 template = NetworkTemplate.buildTemplateWifiWildcard();
-                } break;
-            default: {
+                break;
+            default:
                 throw new IllegalArgumentException("Cannot create template for network type "
                         + networkType + ", subscriberId '"
                         + NetworkIdentity.scrubSubscriberId(subscriberId) + "'.");
-            }
         }
         return template;
     }
diff --git a/tests/net/java/android/app/usage/NetworkStatsManagerTest.java b/tests/net/java/android/app/usage/NetworkStatsManagerTest.java
new file mode 100644
index 0000000..25e1474
--- /dev/null
+++ b/tests/net/java/android/app/usage/NetworkStatsManagerTest.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.usage;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.net.ConnectivityManager;
+import android.net.INetworkStatsService;
+import android.net.INetworkStatsSession;
+import android.net.NetworkStats.Entry;
+import android.net.NetworkStatsHistory;
+import android.net.NetworkTemplate;
+import android.os.RemoteException;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class NetworkStatsManagerTest {
+
+    private @Mock INetworkStatsService mService;
+    private @Mock INetworkStatsSession mStatsSession;
+
+    private NetworkStatsManager mManager;
+
+    // TODO: change to NetworkTemplate.MATCH_MOBILE once internal constant rename is merged to aosp.
+    private static final int MATCH_MOBILE_ALL = 1;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mManager = new NetworkStatsManager(InstrumentationRegistry.getContext(), mService);
+    }
+
+    @Test
+    public void testQueryDetails() throws RemoteException {
+        final String subscriberId = "subid";
+        final long startTime = 1;
+        final long endTime = 100;
+        final int uid1 = 10001;
+        final int uid2 = 10002;
+        final int uid3 = 10003;
+
+        Entry uid1Entry1 = new Entry("if1", uid1,
+                android.net.NetworkStats.SET_DEFAULT, android.net.NetworkStats.TAG_NONE,
+                100, 10, 200, 20, 0);
+
+        Entry uid1Entry2 = new Entry(
+                "if2", uid1,
+                android.net.NetworkStats.SET_DEFAULT, android.net.NetworkStats.TAG_NONE,
+                100, 10, 200, 20, 0);
+
+        Entry uid2Entry1 = new Entry("if1", uid2,
+                android.net.NetworkStats.SET_DEFAULT, android.net.NetworkStats.TAG_NONE,
+                150, 10, 250, 20, 0);
+
+        Entry uid2Entry2 = new Entry(
+                "if2", uid2,
+                android.net.NetworkStats.SET_DEFAULT, android.net.NetworkStats.TAG_NONE,
+                150, 10, 250, 20, 0);
+
+        NetworkStatsHistory history1 = new NetworkStatsHistory(10, 2);
+        history1.recordData(10, 20, uid1Entry1);
+        history1.recordData(20, 30, uid1Entry2);
+
+        NetworkStatsHistory history2 = new NetworkStatsHistory(10, 2);
+        history1.recordData(30, 40, uid2Entry1);
+        history1.recordData(35, 45, uid2Entry2);
+
+
+        when(mService.openSessionForUsageStats(anyInt(), anyString())).thenReturn(mStatsSession);
+        when(mStatsSession.getRelevantUids()).thenReturn(new int[] { uid1, uid2, uid3 });
+
+        when(mStatsSession.getHistoryIntervalForUid(any(NetworkTemplate.class),
+                eq(uid1), eq(android.net.NetworkStats.SET_ALL),
+                eq(android.net.NetworkStats.TAG_NONE),
+                eq(NetworkStatsHistory.FIELD_ALL), eq(startTime), eq(endTime)))
+                .then((InvocationOnMock inv) -> {
+                    NetworkTemplate template = inv.getArgument(0);
+                    assertEquals(MATCH_MOBILE_ALL, template.getMatchRule());
+                    assertEquals(subscriberId, template.getSubscriberId());
+                    return history1;
+                });
+
+        when(mStatsSession.getHistoryIntervalForUid(any(NetworkTemplate.class),
+                eq(uid2), eq(android.net.NetworkStats.SET_ALL),
+                eq(android.net.NetworkStats.TAG_NONE),
+                eq(NetworkStatsHistory.FIELD_ALL), eq(startTime), eq(endTime)))
+                .then((InvocationOnMock inv) -> {
+                    NetworkTemplate template = inv.getArgument(0);
+                    assertEquals(MATCH_MOBILE_ALL, template.getMatchRule());
+                    assertEquals(subscriberId, template.getSubscriberId());
+                    return history2;
+                });
+
+
+        NetworkStats stats = mManager.queryDetails(
+                ConnectivityManager.TYPE_MOBILE, subscriberId, startTime, endTime);
+
+        NetworkStats.Bucket bucket = new NetworkStats.Bucket();
+
+        // First 2 buckets exactly match entry timings
+        assertTrue(stats.getNextBucket(bucket));
+        assertEquals(10, bucket.getStartTimeStamp());
+        assertEquals(20, bucket.getEndTimeStamp());
+        assertBucketMatches(uid1Entry1, bucket);
+
+        assertTrue(stats.getNextBucket(bucket));
+        assertEquals(20, bucket.getStartTimeStamp());
+        assertEquals(30, bucket.getEndTimeStamp());
+        assertBucketMatches(uid1Entry2, bucket);
+
+        // 30 -> 40: contains uid2Entry1 and half of uid2Entry2
+        assertTrue(stats.getNextBucket(bucket));
+        assertEquals(30, bucket.getStartTimeStamp());
+        assertEquals(40, bucket.getEndTimeStamp());
+        assertEquals(225, bucket.getRxBytes());
+        assertEquals(15, bucket.getRxPackets());
+        assertEquals(375, bucket.getTxBytes());
+        assertEquals(30, bucket.getTxPackets());
+
+        // 40 -> 50: contains half of uid2Entry2
+        assertTrue(stats.getNextBucket(bucket));
+        assertEquals(40, bucket.getStartTimeStamp());
+        assertEquals(50, bucket.getEndTimeStamp());
+        assertEquals(75, bucket.getRxBytes());
+        assertEquals(5, bucket.getRxPackets());
+        assertEquals(125, bucket.getTxBytes());
+        assertEquals(10, bucket.getTxPackets());
+
+        assertFalse(stats.hasNextBucket());
+    }
+
+    @Test
+    public void testQueryDetails_NoSubscriberId() throws RemoteException {
+        final long startTime = 1;
+        final long endTime = 100;
+        final int uid1 = 10001;
+        final int uid2 = 10002;
+
+        when(mService.openSessionForUsageStats(anyInt(), anyString())).thenReturn(mStatsSession);
+        when(mStatsSession.getRelevantUids()).thenReturn(new int[] { uid1, uid2 });
+
+        NetworkStats stats = mManager.queryDetails(
+                ConnectivityManager.TYPE_MOBILE, null, startTime, endTime);
+
+        when(mStatsSession.getHistoryIntervalForUid(any(NetworkTemplate.class),
+                anyInt(), anyInt(), anyInt(), anyInt(), anyLong(), anyLong()))
+                .thenReturn(new NetworkStatsHistory(10, 0));
+
+        verify(mStatsSession, times(1)).getHistoryIntervalForUid(
+                argThat((NetworkTemplate t) ->
+                        // No subscriberId: MATCH_MOBILE_WILDCARD template
+                        t.getMatchRule() == NetworkTemplate.MATCH_MOBILE_WILDCARD),
+                eq(uid1), eq(android.net.NetworkStats.SET_ALL),
+                eq(android.net.NetworkStats.TAG_NONE),
+                eq(NetworkStatsHistory.FIELD_ALL), eq(startTime), eq(endTime));
+
+        verify(mStatsSession, times(1)).getHistoryIntervalForUid(
+                argThat((NetworkTemplate t) ->
+                        // No subscriberId: MATCH_MOBILE_WILDCARD template
+                        t.getMatchRule() == NetworkTemplate.MATCH_MOBILE_WILDCARD),
+                eq(uid2), eq(android.net.NetworkStats.SET_ALL),
+                eq(android.net.NetworkStats.TAG_NONE),
+                eq(NetworkStatsHistory.FIELD_ALL), eq(startTime), eq(endTime));
+
+        assertFalse(stats.hasNextBucket());
+    }
+
+    private void assertBucketMatches(Entry expected,
+            NetworkStats.Bucket actual) {
+        assertEquals(expected.uid, actual.getUid());
+        assertEquals(expected.rxBytes, actual.getRxBytes());
+        assertEquals(expected.rxPackets, actual.getRxPackets());
+        assertEquals(expected.txBytes, actual.getTxBytes());
+        assertEquals(expected.txPackets, actual.getTxPackets());
+    }
+}