Relax permissions around NetworkStatsManager APIs.

Currently, access to network usage history and statistics requires a
signature|privileged permission, an AppOps bit (associated with the
PACKAGE_USAGE_STATS permission), or device/profile ownership. Once
access is granted via one of these mechanisms, it generally applies to
any UID running in the same user as the caller.

This CL expands access as follows:

-Any app can access its own usage history with no extra requirements.
-Carrier-privileged applications can access usage history for the
entire device.
-Device owners can access per-UID breakdowns for usage. Previously
they could access the summary for the whole device, but not the
individual breakdowns.

We simplify the permission model by defining three access levels -
DEFAULT (own app only), USER (all apps in the same user), and DEVICE
(all apps on the device), and propagate these levels throughout.

Finally, this CL fixes an apparent bug in
NetworkStatsSerice#hasAppOpsPermissions - if the AppOp bit was in
MODE_DEFAULT, hasAppOpsPermission would always return false instead of
falling back to the PackageManager permission check.

Bug: 25812859
Bug: 25813856
Change-Id: Ic96e0776e2a4215a400163872acea1ededfaced9
diff --git a/core/java/android/app/usage/NetworkStatsManager.java b/core/java/android/app/usage/NetworkStatsManager.java
index 2886cda..0fce7a9 100644
--- a/core/java/android/app/usage/NetworkStatsManager.java
+++ b/core/java/android/app/usage/NetworkStatsManager.java
@@ -21,6 +21,7 @@
 import android.net.ConnectivityManager;
 import android.net.NetworkIdentity;
 import android.net.NetworkTemplate;
+import android.os.Build;
 import android.os.RemoteException;
 import android.util.Log;
 
@@ -29,10 +30,9 @@
  * discrete bins of time called 'Buckets'. See {@link NetworkStats.Bucket} for details.
  * <p />
  * Queries can define a time interval in the form of start and end timestamps (Long.MIN_VALUE and
- * Long.MAX_VALUE can be used to simulate open ended intervals). All queries (except
- * {@link #querySummaryForDevice}) collect only network usage of apps belonging to the same user
- * as the client. In addition tethering usage, usage by removed users and apps, and usage by system
- * is also included in the results.
+ * Long.MAX_VALUE can be used to simulate open ended intervals). By default, apps can only obtain
+ * data about themselves. See the below note for special cases in which apps can obtain data about
+ * other applications.
  * <h3>
  * Summary queries
  * </h3>
@@ -51,13 +51,20 @@
  * multiple buckets for a particular key but all Bucket's state is going to be
  * {@link NetworkStats.Bucket#STATE_ALL}.
  * <p />
- * <b>NOTE:</b> This API requires the permission
+ * <b>NOTE:</b> Accessing stats for apps other than the calling app requires the permission
  * {@link android.Manifest.permission#PACKAGE_USAGE_STATS}, which is a system-level permission and
  * will not be granted to third-party apps. However, declaring the permission implies intention to
  * use the API and the user of the device can grant permission through the Settings application.
  * Profile owner apps are automatically granted permission to query data on the profile they manage
- * (that is, for any query except {@link #querySummaryForDevice}). Device owner apps likewise get
- * access to usage data of the primary user.
+ * (that is, for any query except {@link #querySummaryForDevice}). Device owner apps and carrier-
+ * privileged apps likewise get access to usage data for all users on the device.
+ * <p />
+ * In addition to tethering usage, usage by removed users and apps, and usage by the system
+ * is also included in the results for callers with one of these higher levels of access.
+ * <p />
+ * <b>NOTE:</b> Prior to API level {@value Build.VERSION_CODES#N}, all calls to these APIs required
+ * the above permission, even to access an app's own data usage, and carrier-privileged apps were
+ * not included.
  */
 public class NetworkStatsManager {
     private final static String TAG = "NetworkStatsManager";
diff --git a/services/core/java/com/android/server/net/NetworkStatsAccess.java b/services/core/java/com/android/server/net/NetworkStatsAccess.java
new file mode 100644
index 0000000..53ba718
--- /dev/null
+++ b/services/core/java/com/android/server/net/NetworkStatsAccess.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.net;
+
+import static android.Manifest.permission.READ_NETWORK_USAGE_HISTORY;
+import static android.net.TrafficStats.UID_REMOVED;
+import static android.net.TrafficStats.UID_TETHERING;
+
+import android.Manifest;
+import android.annotation.IntDef;
+import android.app.AppOpsManager;
+import android.app.admin.DeviceAdminInfo;
+import android.app.admin.DevicePolicyManagerInternal;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.UserHandle;
+import android.telephony.TelephonyManager;
+
+import com.android.server.LocalServices;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Utility methods for controlling access to network stats APIs. */
+public final class NetworkStatsAccess {
+    private NetworkStatsAccess() {}
+
+    /**
+     * Represents an access level for the network usage history and statistics APIs.
+     *
+     * <p>Access levels are in increasing order; that is, it is reasonable to check access by
+     * verifying that the caller's access level is at least the minimum required level.
+     */
+    @IntDef({
+            Level.DEFAULT,
+            Level.USER,
+            Level.DEVICE,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Level {
+        /**
+         * Default, unprivileged access level.
+         *
+         * <p>Can only access usage for one's own UID.
+         *
+         * <p>Every app will have at least this access level.
+         */
+        int DEFAULT = 0;
+
+        /**
+         * Access level for apps which can access usage for any app running in the same user.
+         *
+         * <p>Granted to:
+         * <ul>
+         * <li>Apps with the PACKAGE_USAGE_STATS permission granted. Note that this is an AppOps bit
+         * so it is not necessarily sufficient to declare this in the manifest.
+         * <li>Apps with the (signature/privileged) READ_NETWORK_USAGE_HISTORY permission.
+         * <li>Profile owners.
+         * </ul>
+         */
+        int USER = 1;
+
+        /**
+         * Access level for apps which can access usage for any app on the device, including apps
+         * running on other users/profiles.
+         *
+         * <p>Granted to:
+         * <ul>
+         * <li>Device owners.
+         * <li>Carrier-privileged applications.
+         * <li>The system UID.
+         * </ul>
+         */
+        int DEVICE = 2;
+    }
+
+    /** Returns the {@link NetworkStatsAccess.Level} for the given caller. */
+    public static @NetworkStatsAccess.Level int checkAccessLevel(
+            Context context, int callingUid, String callingPackage) {
+        final DevicePolicyManagerInternal dpmi = LocalServices.getService(
+                DevicePolicyManagerInternal.class);
+        final TelephonyManager tm = (TelephonyManager)
+                context.getSystemService(Context.TELEPHONY_SERVICE);
+        boolean hasCarrierPrivileges = tm != null &&
+                tm.checkCarrierPrivilegesForPackage(callingPackage) ==
+                        TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS;
+        boolean isDeviceOwner = dpmi != null && dpmi.isActiveAdminWithPolicy(callingUid,
+                DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
+        if (hasCarrierPrivileges || isDeviceOwner
+                || UserHandle.getAppId(callingUid) == android.os.Process.SYSTEM_UID) {
+            // Carrier-privileged apps and device owners, and the system can access data usage for
+            // all apps on the device.
+            return NetworkStatsAccess.Level.DEVICE;
+        }
+
+        boolean isProfileOwner = dpmi != null && dpmi.isActiveAdminWithPolicy(callingUid,
+                DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
+        if (hasAppOpsPermission(context, callingUid, callingPackage) || isProfileOwner
+                || context.checkCallingOrSelfPermission(READ_NETWORK_USAGE_HISTORY) ==
+                PackageManager.PERMISSION_GRANTED) {
+            // Apps with the AppOps permission, profile owners, and apps with the privileged
+            // permission can access data usage for all apps in this user/profile.
+            return NetworkStatsAccess.Level.USER;
+        }
+
+        // Everyone else gets default access (only to their own UID).
+        return NetworkStatsAccess.Level.DEFAULT;
+    }
+
+    /**
+     * Returns whether the given caller should be able to access the given UID when the caller has
+     * the given {@link NetworkStatsAccess.Level}.
+     */
+    public static boolean isAccessibleToUser(int uid, int callerUid,
+            @NetworkStatsAccess.Level int accessLevel) {
+        switch (accessLevel) {
+            case NetworkStatsAccess.Level.DEVICE:
+                // Device-level access - can access usage for any uid.
+                return true;
+            case NetworkStatsAccess.Level.USER:
+                // User-level access - can access usage for any app running in the same user, along
+                // with some special uids (system, removed, or tethering).
+                return uid == android.os.Process.SYSTEM_UID || uid == UID_REMOVED
+                        || uid == UID_TETHERING
+                        || UserHandle.getUserId(uid) == UserHandle.getUserId(callerUid);
+            case NetworkStatsAccess.Level.DEFAULT:
+            default:
+                // Default access level - can only access one's own usage.
+                return uid == callerUid;
+        }
+    }
+
+    private static boolean hasAppOpsPermission(
+            Context context, int callingUid, String callingPackage) {
+        if (callingPackage != null) {
+            AppOpsManager appOps = (AppOpsManager) context.getSystemService(
+                    Context.APP_OPS_SERVICE);
+
+            final int mode = appOps.checkOp(AppOpsManager.OP_GET_USAGE_STATS,
+                    callingUid, callingPackage);
+            if (mode == AppOpsManager.MODE_DEFAULT) {
+                // The default behavior here is to check if PackageManager has given the app
+                // permission.
+                final int permissionCheck = context.checkCallingPermission(
+                        Manifest.permission.PACKAGE_USAGE_STATS);
+                return permissionCheck == PackageManager.PERMISSION_GRANTED;
+            }
+            return (mode == AppOpsManager.MODE_ALLOWED);
+        }
+        return false;
+    }
+}
diff --git a/services/core/java/com/android/server/net/NetworkStatsCollection.java b/services/core/java/com/android/server/net/NetworkStatsCollection.java
index 15b68c7..102695e 100644
--- a/services/core/java/com/android/server/net/NetworkStatsCollection.java
+++ b/services/core/java/com/android/server/net/NetworkStatsCollection.java
@@ -22,24 +22,18 @@
 import static android.net.NetworkStats.TAG_NONE;
 import static android.net.NetworkStats.UID_ALL;
 import static android.net.TrafficStats.UID_REMOVED;
-import static android.net.TrafficStats.UID_TETHERING;
-import static android.text.format.DateUtils.SECOND_IN_MILLIS;
 import static android.text.format.DateUtils.WEEK_IN_MILLIS;
 
-import android.net.ConnectivityManager;
 import android.net.NetworkIdentity;
 import android.net.NetworkStats;
 import android.net.NetworkStatsHistory;
 import android.net.NetworkTemplate;
 import android.net.TrafficStats;
 import android.os.Binder;
-import android.os.UserHandle;
 import android.util.ArrayMap;
 import android.util.AtomicFile;
 import android.util.IntArray;
 
-import libcore.io.IoUtils;
-
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.FileRotator;
 import com.android.internal.util.IndentingPrintWriter;
@@ -47,6 +41,8 @@
 import com.google.android.collect.Lists;
 import com.google.android.collect.Maps;
 
+import libcore.io.IoUtils;
+
 import java.io.BufferedInputStream;
 import java.io.DataInputStream;
 import java.io.DataOutputStream;
@@ -136,12 +132,12 @@
         return mStartMillis == Long.MAX_VALUE && mEndMillis == Long.MIN_VALUE;
     }
 
-    public int[] getRelevantUids() {
+    public int[] getRelevantUids(@NetworkStatsAccess.Level int accessLevel) {
         final int callerUid = Binder.getCallingUid();
         IntArray uids = new IntArray();
         for (int i = 0; i < mStats.size(); i++) {
             final Key key = mStats.keyAt(i);
-            if (isAccessibleToUser(key.uid, callerUid)) {
+            if (NetworkStatsAccess.isAccessibleToUser(key.uid, callerUid, accessLevel)) {
                 int j = uids.binarySearch(key.uid);
 
                 if (j < 0) {
@@ -158,8 +154,10 @@
      * the requested parameters.
      */
     public NetworkStatsHistory getHistory(
-            NetworkTemplate template, int uid, int set, int tag, int fields) {
-        return getHistory(template, uid, set, tag, fields, Long.MIN_VALUE, Long.MAX_VALUE);
+            NetworkTemplate template, int uid, int set, int tag, int fields,
+            @NetworkStatsAccess.Level int accessLevel) {
+        return getHistory(template, uid, set, tag, fields, Long.MIN_VALUE, Long.MAX_VALUE,
+                accessLevel);
     }
 
     /**
@@ -167,9 +165,10 @@
      * the requested parameters.
      */
     public NetworkStatsHistory getHistory(
-            NetworkTemplate template, int uid, int set, int tag, int fields, long start, long end) {
+            NetworkTemplate template, int uid, int set, int tag, int fields, long start, long end,
+            @NetworkStatsAccess.Level int accessLevel) {
         final int callerUid = Binder.getCallingUid();
-        if (!isAccessibleToUser(uid, callerUid)) {
+        if (!NetworkStatsAccess.isAccessibleToUser(uid, callerUid, accessLevel)) {
             throw new SecurityException("Network stats history of uid " + uid
                     + " is forbidden for caller " + callerUid);
         }
@@ -195,7 +194,8 @@
      * Summarize all {@link NetworkStatsHistory} in this collection which match
      * the requested parameters.
      */
-    public NetworkStats getSummary(NetworkTemplate template, long start, long end) {
+    public NetworkStats getSummary(NetworkTemplate template, long start, long end,
+            @NetworkStatsAccess.Level int accessLevel) {
         final long now = System.currentTimeMillis();
 
         final NetworkStats stats = new NetworkStats(end - start, 24);
@@ -208,7 +208,8 @@
         final int callerUid = Binder.getCallingUid();
         for (int i = 0; i < mStats.size(); i++) {
             final Key key = mStats.keyAt(i);
-            if (templateMatches(template, key.ident) && isAccessibleToUser(key.uid, callerUid)
+            if (templateMatches(template, key.ident)
+                    && NetworkStatsAccess.isAccessibleToUser(key.uid, callerUid, accessLevel)
                     && key.set < NetworkStats.SET_DEBUG_START) {
                 final NetworkStatsHistory value = mStats.valueAt(i);
                 historyEntry = value.getValues(start, end, now, historyEntry);
@@ -570,12 +571,6 @@
         }
     }
 
-    private static boolean isAccessibleToUser(int uid, int callerUid) {
-        return UserHandle.getAppId(callerUid) == android.os.Process.SYSTEM_UID ||
-                uid == android.os.Process.SYSTEM_UID || uid == UID_REMOVED || uid == UID_TETHERING
-                || UserHandle.getUserId(uid) == UserHandle.getUserId(callerUid);
-    }
-
     /**
      * Test if given {@link NetworkTemplate} matches any {@link NetworkIdentity}
      * in the given {@link NetworkIdentitySet}.
diff --git a/services/core/java/com/android/server/net/NetworkStatsRecorder.java b/services/core/java/com/android/server/net/NetworkStatsRecorder.java
index 6490865..c091960 100644
--- a/services/core/java/com/android/server/net/NetworkStatsRecorder.java
+++ b/services/core/java/com/android/server/net/NetworkStatsRecorder.java
@@ -116,7 +116,8 @@
     }
 
     public NetworkStats.Entry getTotalSinceBootLocked(NetworkTemplate template) {
-        return mSinceBoot.getSummary(template, Long.MIN_VALUE, Long.MAX_VALUE).getTotal(null);
+        return mSinceBoot.getSummary(template, Long.MIN_VALUE, Long.MAX_VALUE,
+                NetworkStatsAccess.Level.DEVICE).getTotal(null);
     }
 
     /**
diff --git a/services/core/java/com/android/server/net/NetworkStatsService.java b/services/core/java/com/android/server/net/NetworkStatsService.java
index 8449348..b1d6f89 100644
--- a/services/core/java/com/android/server/net/NetworkStatsService.java
+++ b/services/core/java/com/android/server/net/NetworkStatsService.java
@@ -62,13 +62,9 @@
 import static com.android.server.NetworkManagementSocketTagger.resetKernelUidStats;
 import static com.android.server.NetworkManagementSocketTagger.setKernelCounterSet;
 
-import android.Manifest;
 import android.app.AlarmManager;
-import android.app.AppOpsManager;
 import android.app.IAlarmManager;
 import android.app.PendingIntent;
-import android.app.admin.DeviceAdminInfo;
-import android.app.admin.DevicePolicyManagerInternal;
 import android.content.BroadcastReceiver;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -97,9 +93,7 @@
 import android.os.INetworkManagementService;
 import android.os.Message;
 import android.os.PowerManager;
-import android.os.Process;
 import android.os.RemoteException;
-import android.os.ServiceManager;
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.provider.Settings;
@@ -122,7 +116,6 @@
 import com.android.internal.util.FileRotator;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.EventLogTags;
-import com.android.server.LocalServices;
 import com.android.server.connectivity.Tethering;
 
 import java.io.File;
@@ -484,18 +477,22 @@
 
             @Override
             public int[] getRelevantUids() {
-                enforcePermissionForManagedAdmin(mCallingPackage);
-                return getUidComplete().getRelevantUids();
+                return getUidComplete().getRelevantUids(checkAccessLevel(mCallingPackage));
             }
 
             @Override
             public NetworkStats getDeviceSummaryForNetwork(NetworkTemplate template, long start,
                     long end) {
-                enforcePermission(mCallingPackage);
+                @NetworkStatsAccess.Level int accessLevel = checkAccessLevel(mCallingPackage);
+                if (accessLevel < NetworkStatsAccess.Level.DEVICE) {
+                    throw new SecurityException("Calling package " + mCallingPackage
+                            + " cannot access device-level network stats");
+                }
                 NetworkStats result = new NetworkStats(end - start, 1);
                 final long ident = Binder.clearCallingIdentity();
                 try {
-                    result.combineAllValues(internalGetSummaryForNetwork(template, start, end));
+                    result.combineAllValues(
+                            internalGetSummaryForNetwork(template, start, end, accessLevel));
                 } finally {
                     Binder.restoreCallingIdentity(ident);
                 }
@@ -505,23 +502,25 @@
             @Override
             public NetworkStats getSummaryForNetwork(
                     NetworkTemplate template, long start, long end) {
-                enforcePermission(mCallingPackage);
-                return internalGetSummaryForNetwork(template, start, end);
+                @NetworkStatsAccess.Level int accessLevel = checkAccessLevel(mCallingPackage);
+                return internalGetSummaryForNetwork(template, start, end, accessLevel);
             }
 
             @Override
             public NetworkStatsHistory getHistoryForNetwork(NetworkTemplate template, int fields) {
-                return internalGetHistoryForNetwork(template, fields);
+                @NetworkStatsAccess.Level int accessLevel = checkAccessLevel(mCallingPackage);
+                return internalGetHistoryForNetwork(template, fields, accessLevel);
             }
 
             @Override
             public NetworkStats getSummaryForAllUid(
                     NetworkTemplate template, long start, long end, boolean includeTags) {
-                enforcePermissionForManagedAdmin(mCallingPackage);
-                final NetworkStats stats = getUidComplete().getSummary(template, start, end);
+                @NetworkStatsAccess.Level int accessLevel = checkAccessLevel(mCallingPackage);
+                final NetworkStats stats =
+                        getUidComplete().getSummary(template, start, end, accessLevel);
                 if (includeTags) {
                     final NetworkStats tagStats = getUidTagComplete()
-                            .getSummary(template, start, end);
+                            .getSummary(template, start, end, accessLevel);
                     stats.combineAllValues(tagStats);
                 }
                 return stats;
@@ -530,11 +529,13 @@
             @Override
             public NetworkStatsHistory getHistoryForUid(
                     NetworkTemplate template, int uid, int set, int tag, int fields) {
-                enforcePermissionForManagedAdmin(mCallingPackage);
+                @NetworkStatsAccess.Level int accessLevel = checkAccessLevel(mCallingPackage);
                 if (tag == TAG_NONE) {
-                    return getUidComplete().getHistory(template, uid, set, tag, fields);
+                    return getUidComplete().getHistory(template, uid, set, tag, fields,
+                            accessLevel);
                 } else {
-                    return getUidTagComplete().getHistory(template, uid, set, tag, fields);
+                    return getUidTagComplete().getHistory(template, uid, set, tag, fields,
+                            accessLevel);
                 }
             }
 
@@ -542,12 +543,13 @@
             public NetworkStatsHistory getHistoryIntervalForUid(
                     NetworkTemplate template, int uid, int set, int tag, int fields,
                     long start, long end) {
-                enforcePermissionForManagedAdmin(mCallingPackage);
+                @NetworkStatsAccess.Level int accessLevel = checkAccessLevel(mCallingPackage);
                 if (tag == TAG_NONE) {
-                    return getUidComplete().getHistory(template, uid, set, tag, fields, start, end);
+                    return getUidComplete().getHistory(template, uid, set, tag, fields, start, end,
+                            accessLevel);
                 } else {
                     return getUidTagComplete().getHistory(template, uid, set, tag, fields,
-                            start, end);
+                            start, end, accessLevel);
                 }
             }
 
@@ -559,80 +561,42 @@
         };
     }
 
-    private boolean hasAppOpsPermission(String callingPackage) {
-        final int callingUid = Binder.getCallingUid();
-        boolean appOpsAllow = false;
-        if (callingPackage != null) {
-            AppOpsManager appOps = (AppOpsManager) mContext.getSystemService(
-                    Context.APP_OPS_SERVICE);
-
-            final int mode = appOps.checkOp(AppOpsManager.OP_GET_USAGE_STATS,
-                    callingUid, callingPackage);
-            if (mode == AppOpsManager.MODE_DEFAULT) {
-                // The default behavior here is to check if PackageManager has given the app
-                // permission.
-                final int permissionCheck = mContext.checkCallingPermission(
-                        Manifest.permission.PACKAGE_USAGE_STATS);
-                appOpsAllow = permissionCheck == PackageManager.PERMISSION_GRANTED;
-            }
-            appOpsAllow = (mode == AppOpsManager.MODE_ALLOWED);
-        }
-        return appOpsAllow;
+    private @NetworkStatsAccess.Level int checkAccessLevel(String callingPackage) {
+        return NetworkStatsAccess.checkAccessLevel(
+                mContext, Binder.getCallingUid(), callingPackage);
     }
 
-    private void enforcePermissionForManagedAdmin(String callingPackage) {
-        boolean hasPermission = hasAppOpsPermission(callingPackage);
-        if (!hasPermission) {
-            // Profile and device owners are exempt from permission checking.
-            final int callingUid = Binder.getCallingUid();
-            final DevicePolicyManagerInternal dpmi = LocalServices.getService(
-                    DevicePolicyManagerInternal.class);
-
-            // Device owners are also profile owners so it is enough to check for that.
-            if (dpmi != null && dpmi.isActiveAdminWithPolicy(callingUid,
-                    DeviceAdminInfo.USES_POLICY_PROFILE_OWNER)) {
-                return;
-            }
-        }
-        if (!hasPermission) {
-            mContext.enforceCallingOrSelfPermission(READ_NETWORK_USAGE_HISTORY, TAG);
-        }
-    }
-
-    private void enforcePermission(String callingPackage) {
-        boolean appOpsAllow = hasAppOpsPermission(callingPackage);
-        if (!appOpsAllow) {
-            mContext.enforceCallingOrSelfPermission(READ_NETWORK_USAGE_HISTORY, TAG);
-        }
-    }
-
-
     /**
      * Return network summary, splicing between DEV and XT stats when
      * appropriate.
      */
     private NetworkStats internalGetSummaryForNetwork(
-            NetworkTemplate template, long start, long end) {
+            NetworkTemplate template, long start, long end,
+            @NetworkStatsAccess.Level int accessLevel) {
         // We've been using pure XT stats long enough that we no longer need to
         // splice DEV and XT together.
-        return mXtStatsCached.getSummary(template, start, end);
+        return mXtStatsCached.getSummary(template, start, end, accessLevel);
     }
 
     /**
      * Return network history, splicing between DEV and XT stats when
      * appropriate.
      */
-    private NetworkStatsHistory internalGetHistoryForNetwork(NetworkTemplate template, int fields) {
+    private NetworkStatsHistory internalGetHistoryForNetwork(NetworkTemplate template, int fields,
+            @NetworkStatsAccess.Level int accessLevel) {
         // We've been using pure XT stats long enough that we no longer need to
         // splice DEV and XT together.
-        return mXtStatsCached.getHistory(template, UID_ALL, SET_ALL, TAG_NONE, fields);
+        return mXtStatsCached.getHistory(template, UID_ALL, SET_ALL, TAG_NONE, fields, accessLevel);
     }
 
     @Override
     public long getNetworkTotalBytes(NetworkTemplate template, long start, long end) {
+        // Special case - since this is for internal use only, don't worry about a full access level
+        // check and just require the signature/privileged permission.
         mContext.enforceCallingOrSelfPermission(READ_NETWORK_USAGE_HISTORY, TAG);
         assertBandwidthControlEnabled();
-        return internalGetSummaryForNetwork(template, start, end).getTotalBytes();
+        return internalGetSummaryForNetwork(template, start, end, NetworkStatsAccess.Level.DEVICE)
+                .getTotalBytes();
     }
 
     @Override
diff --git a/services/tests/servicestests/src/com/android/server/net/NetworkStatsAccessTest.java b/services/tests/servicestests/src/com/android/server/net/NetworkStatsAccessTest.java
new file mode 100644
index 0000000..bb8f9d1
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/net/NetworkStatsAccessTest.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.net;
+
+import static org.mockito.Mockito.when;
+
+import android.Manifest;
+import android.Manifest.permission;
+import android.app.AppOpsManager;
+import android.app.admin.DeviceAdminInfo;
+import android.app.admin.DevicePolicyManagerInternal;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.telephony.TelephonyManager;
+
+import com.android.server.LocalServices;
+
+import junit.framework.TestCase;
+
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+public class NetworkStatsAccessTest extends TestCase {
+    private static final String TEST_PKG = "com.example.test";
+    private static final int TEST_UID = 12345;
+
+    @Mock private Context mContext;
+    @Mock private DevicePolicyManagerInternal mDpmi;
+    @Mock private TelephonyManager mTm;
+    @Mock private AppOpsManager mAppOps;
+
+    // Hold the real service so we can restore it when tearing down the test.
+    private DevicePolicyManagerInternal mSystemDpmi;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        MockitoAnnotations.initMocks(this);
+
+        mSystemDpmi = LocalServices.getService(DevicePolicyManagerInternal.class);
+        LocalServices.removeServiceForTest(DevicePolicyManagerInternal.class);
+        LocalServices.addService(DevicePolicyManagerInternal.class, mDpmi);
+
+        when(mContext.getSystemService(Context.TELEPHONY_SERVICE)).thenReturn(mTm);
+        when(mContext.getSystemService(Context.APP_OPS_SERVICE)).thenReturn(mAppOps);
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        LocalServices.removeServiceForTest(DevicePolicyManagerInternal.class);
+        LocalServices.addService(DevicePolicyManagerInternal.class, mSystemDpmi);
+        super.tearDown();
+    }
+
+    public void testCheckAccessLevel_hasCarrierPrivileges() throws Exception {
+        setHasCarrierPrivileges(true);
+        setIsDeviceOwner(false);
+        setIsProfileOwner(false);
+        setHasAppOpsPermission(AppOpsManager.MODE_DEFAULT, false);
+        setHasReadHistoryPermission(false);
+        assertEquals(NetworkStatsAccess.Level.DEVICE,
+                NetworkStatsAccess.checkAccessLevel(mContext, TEST_UID, TEST_PKG));
+    }
+
+    public void testCheckAccessLevel_isDeviceOwner() throws Exception {
+        setHasCarrierPrivileges(false);
+        setIsDeviceOwner(true);
+        setIsProfileOwner(false);
+        setHasAppOpsPermission(AppOpsManager.MODE_DEFAULT, false);
+        setHasReadHistoryPermission(false);
+        assertEquals(NetworkStatsAccess.Level.DEVICE,
+                NetworkStatsAccess.checkAccessLevel(mContext, TEST_UID, TEST_PKG));
+    }
+
+    public void testCheckAccessLevel_isProfileOwner() throws Exception {
+        setHasCarrierPrivileges(false);
+        setIsDeviceOwner(false);
+        setIsProfileOwner(true);
+        setHasAppOpsPermission(AppOpsManager.MODE_DEFAULT, false);
+        setHasReadHistoryPermission(false);
+        assertEquals(NetworkStatsAccess.Level.USER,
+                NetworkStatsAccess.checkAccessLevel(mContext, TEST_UID, TEST_PKG));
+    }
+
+    public void testCheckAccessLevel_hasAppOpsBitAllowed() throws Exception {
+        setHasCarrierPrivileges(false);
+        setIsDeviceOwner(false);
+        setIsProfileOwner(true);
+        setHasAppOpsPermission(AppOpsManager.MODE_ALLOWED, false);
+        setHasReadHistoryPermission(false);
+        assertEquals(NetworkStatsAccess.Level.USER,
+                NetworkStatsAccess.checkAccessLevel(mContext, TEST_UID, TEST_PKG));
+    }
+
+    public void testCheckAccessLevel_hasAppOpsBitDefault_grantedPermission() throws Exception {
+        setHasCarrierPrivileges(false);
+        setIsDeviceOwner(false);
+        setIsProfileOwner(true);
+        setHasAppOpsPermission(AppOpsManager.MODE_DEFAULT, true);
+        setHasReadHistoryPermission(false);
+        assertEquals(NetworkStatsAccess.Level.USER,
+                NetworkStatsAccess.checkAccessLevel(mContext, TEST_UID, TEST_PKG));
+    }
+
+    public void testCheckAccessLevel_hasReadHistoryPermission() throws Exception {
+        setHasCarrierPrivileges(false);
+        setIsDeviceOwner(false);
+        setIsProfileOwner(true);
+        setHasAppOpsPermission(AppOpsManager.MODE_DEFAULT, false);
+        setHasReadHistoryPermission(true);
+        assertEquals(NetworkStatsAccess.Level.USER,
+                NetworkStatsAccess.checkAccessLevel(mContext, TEST_UID, TEST_PKG));
+    }
+
+    public void testCheckAccessLevel_deniedAppOpsBit() throws Exception {
+        setHasCarrierPrivileges(false);
+        setIsDeviceOwner(false);
+        setIsProfileOwner(false);
+        setHasAppOpsPermission(AppOpsManager.MODE_ERRORED, true);
+        setHasReadHistoryPermission(false);
+        assertEquals(NetworkStatsAccess.Level.DEFAULT,
+                NetworkStatsAccess.checkAccessLevel(mContext, TEST_UID, TEST_PKG));
+    }
+
+    public void testCheckAccessLevel_deniedAppOpsBit_deniedPermission() throws Exception {
+        setHasCarrierPrivileges(false);
+        setIsDeviceOwner(false);
+        setIsProfileOwner(false);
+        setHasAppOpsPermission(AppOpsManager.MODE_DEFAULT, false);
+        setHasReadHistoryPermission(false);
+        assertEquals(NetworkStatsAccess.Level.DEFAULT,
+                NetworkStatsAccess.checkAccessLevel(mContext, TEST_UID, TEST_PKG));
+    }
+
+    private void setHasCarrierPrivileges(boolean hasPrivileges) {
+        when(mTm.checkCarrierPrivilegesForPackage(TEST_PKG)).thenReturn(
+                hasPrivileges ? TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS
+                        : TelephonyManager.CARRIER_PRIVILEGE_STATUS_NO_ACCESS);
+    }
+
+    private void setIsDeviceOwner(boolean isOwner) {
+        when(mDpmi.isActiveAdminWithPolicy(TEST_UID, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER))
+                .thenReturn(isOwner);
+    }
+
+    private void setIsProfileOwner(boolean isOwner) {
+        when(mDpmi.isActiveAdminWithPolicy(TEST_UID, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER))
+                .thenReturn(isOwner);
+    }
+
+    private void setHasAppOpsPermission(int appOpsMode, boolean hasPermission) {
+        when(mAppOps.checkOp(AppOpsManager.OP_GET_USAGE_STATS, TEST_UID, TEST_PKG))
+                .thenReturn(appOpsMode);
+        when(mContext.checkCallingPermission(Manifest.permission.PACKAGE_USAGE_STATS)).thenReturn(
+                hasPermission ? PackageManager.PERMISSION_GRANTED
+                        : PackageManager.PERMISSION_DENIED);
+    }
+
+    private void setHasReadHistoryPermission(boolean hasPermission) {
+        when(mContext.checkCallingOrSelfPermission(permission.READ_NETWORK_USAGE_HISTORY))
+                .thenReturn(hasPermission ? PackageManager.PERMISSION_GRANTED
+                        : PackageManager.PERMISSION_DENIED);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/net/NetworkStatsCollectionTest.java b/services/tests/servicestests/src/com/android/server/net/NetworkStatsCollectionTest.java
index 1a6c289..6026644 100644
--- a/services/tests/servicestests/src/com/android/server/net/NetworkStatsCollectionTest.java
+++ b/services/tests/servicestests/src/com/android/server/net/NetworkStatsCollectionTest.java
@@ -16,6 +16,7 @@
 
 package com.android.server.net;
 
+import static android.net.ConnectivityManager.TYPE_MOBILE;
 import static android.net.NetworkStats.SET_DEFAULT;
 import static android.net.NetworkStats.TAG_NONE;
 import static android.net.NetworkStats.UID_ALL;
@@ -24,9 +25,14 @@
 import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
 
 import android.content.res.Resources;
+import android.net.NetworkIdentity;
 import android.net.NetworkStats;
 import android.net.NetworkTemplate;
+import android.os.Process;
+import android.os.UserHandle;
+import android.telephony.TelephonyManager;
 import android.test.AndroidTestCase;
+import android.test.MoreAsserts;
 import android.test.suitebuilder.annotation.MediumTest;
 
 import com.android.frameworks.servicestests.R;
@@ -68,7 +74,7 @@
 
         // verify that history read correctly
         assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI),
-                636016770L, 709306L, 88038768L, 518836L);
+                636016770L, 709306L, 88038768L, 518836L, NetworkStatsAccess.Level.DEVICE);
 
         // now export into a unified format
         final ByteArrayOutputStream bos = new ByteArrayOutputStream();
@@ -77,12 +83,12 @@
         // clear structure completely
         collection.reset();
         assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI),
-                0L, 0L, 0L, 0L);
+                0L, 0L, 0L, 0L, NetworkStatsAccess.Level.DEVICE);
 
         // and read back into structure, verifying that totals are same
         collection.read(new ByteArrayInputStream(bos.toByteArray()));
         assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI),
-                636016770L, 709306L, 88038768L, 518836L);
+                636016770L, 709306L, 88038768L, 518836L, NetworkStatsAccess.Level.DEVICE);
     }
 
     public void testReadLegacyUid() throws Exception {
@@ -94,7 +100,7 @@
 
         // verify that history read correctly
         assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI),
-                637076152L, 711413L, 88343717L, 521022L);
+                637076152L, 711413L, 88343717L, 521022L, NetworkStatsAccess.Level.DEVICE);
 
         // now export into a unified format
         final ByteArrayOutputStream bos = new ByteArrayOutputStream();
@@ -103,12 +109,12 @@
         // clear structure completely
         collection.reset();
         assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI),
-                0L, 0L, 0L, 0L);
+                0L, 0L, 0L, 0L, NetworkStatsAccess.Level.DEVICE);
 
         // and read back into structure, verifying that totals are same
         collection.read(new ByteArrayInputStream(bos.toByteArray()));
         assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI),
-                637076152L, 711413L, 88343717L, 521022L);
+                637076152L, 711413L, 88343717L, 521022L, NetworkStatsAccess.Level.DEVICE);
     }
 
     public void testReadLegacyUidTags() throws Exception {
@@ -151,6 +157,66 @@
         assertEquals(2 * HOUR_IN_MILLIS, collection.getEndMillis());
     }
 
+    public void testAccessLevels() throws Exception {
+        final NetworkStatsCollection collection = new NetworkStatsCollection(HOUR_IN_MILLIS);
+        final NetworkStats.Entry entry = new NetworkStats.Entry();
+        final NetworkIdentitySet identSet = new NetworkIdentitySet();
+        identSet.add(new NetworkIdentity(TYPE_MOBILE, TelephonyManager.NETWORK_TYPE_UNKNOWN,
+                TEST_IMSI, null, false));
+
+        int myUid = Process.myUid();
+        int otherUidInSameUser = Process.myUid() + 1;
+        int uidInDifferentUser = Process.myUid() + UserHandle.PER_USER_RANGE;
+
+        // Record one entry for the current UID.
+        entry.rxBytes = 32;
+        collection.recordData(identSet, myUid, SET_DEFAULT, TAG_NONE, 0, 60 * MINUTE_IN_MILLIS,
+                entry);
+
+        // Record one entry for another UID in this user.
+        entry.rxBytes = 64;
+        collection.recordData(identSet, otherUidInSameUser, SET_DEFAULT, TAG_NONE, 0,
+                60 * MINUTE_IN_MILLIS, entry);
+
+        // Record one entry for the system UID.
+        entry.rxBytes = 128;
+        collection.recordData(identSet, Process.SYSTEM_UID, SET_DEFAULT, TAG_NONE, 0,
+                60 * MINUTE_IN_MILLIS, entry);
+
+        // Record one entry for a UID in a different user.
+        entry.rxBytes = 256;
+        collection.recordData(identSet, uidInDifferentUser, SET_DEFAULT, TAG_NONE, 0,
+                60 * MINUTE_IN_MILLIS, entry);
+
+        // Verify the set of relevant UIDs for each access level.
+        MoreAsserts.assertEquals(new int[] { myUid },
+                collection.getRelevantUids(NetworkStatsAccess.Level.DEFAULT));
+        MoreAsserts.assertEquals(new int[] { Process.SYSTEM_UID, myUid, otherUidInSameUser },
+                collection.getRelevantUids(NetworkStatsAccess.Level.USER));
+        MoreAsserts.assertEquals(
+                new int[] { Process.SYSTEM_UID, myUid, otherUidInSameUser, uidInDifferentUser },
+                collection.getRelevantUids(NetworkStatsAccess.Level.DEVICE));
+
+        // Verify security check in getHistory.
+        assertNotNull(collection.getHistory(buildTemplateMobileAll(TEST_IMSI), myUid, SET_DEFAULT,
+                TAG_NONE, 0, NetworkStatsAccess.Level.DEFAULT));
+        try {
+            collection.getHistory(buildTemplateMobileAll(TEST_IMSI), otherUidInSameUser,
+                    SET_DEFAULT, TAG_NONE, 0, NetworkStatsAccess.Level.DEFAULT);
+            fail("Should have thrown SecurityException for accessing different UID");
+        } catch (SecurityException e) {
+            // expected
+        }
+
+        // Verify appropriate aggregation in getSummary.
+        assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI), 32, 0, 0, 0,
+                NetworkStatsAccess.Level.DEFAULT);
+        assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI), 32 + 64 + 128, 0, 0, 0,
+                NetworkStatsAccess.Level.USER);
+        assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI), 32 + 64 + 128 + 256, 0, 0,
+                0, NetworkStatsAccess.Level.DEVICE);
+    }
+
     /**
      * Copy a {@link Resources#openRawResource(int)} into {@link File} for
      * testing purposes.
@@ -170,16 +236,19 @@
     }
 
     private static void assertSummaryTotal(NetworkStatsCollection collection,
-            NetworkTemplate template, long rxBytes, long rxPackets, long txBytes, long txPackets) {
+            NetworkTemplate template, long rxBytes, long rxPackets, long txBytes, long txPackets,
+            @NetworkStatsAccess.Level int accessLevel) {
         final NetworkStats.Entry entry = collection.getSummary(
-                template, Long.MIN_VALUE, Long.MAX_VALUE).getTotal(null);
+                template, Long.MIN_VALUE, Long.MAX_VALUE, accessLevel)
+                .getTotal(null);
         assertEntry(entry, rxBytes, rxPackets, txBytes, txPackets);
     }
 
     private static void assertSummaryTotalIncludingTags(NetworkStatsCollection collection,
             NetworkTemplate template, long rxBytes, long rxPackets, long txBytes, long txPackets) {
         final NetworkStats.Entry entry = collection.getSummary(
-                template, Long.MIN_VALUE, Long.MAX_VALUE).getTotalIncludingTags(null);
+                template, Long.MIN_VALUE, Long.MAX_VALUE, NetworkStatsAccess.Level.DEVICE)
+                .getTotalIncludingTags(null);
         assertEntry(entry, rxBytes, rxPackets, txBytes, txPackets);
     }