Understanding and managing data consumption is a critical feature for many modern Android applications, from device utility tools to parental control suites. Users are more conscious than ever of their mobile data limits, and providing them with clear insights into which apps are consuming their data can significantly enhance their experience. Fortunately, Android provides a powerful, albeit nuanced, API for this very purpose: the NetworkStatsManager
. This article provides a comprehensive walkthrough of how to leverage this API using Kotlin to accurately measure data usage for any specific application installed on a device.
We will move beyond a simple code snippet and explore the entire process, including the essential prerequisites like handling permissions, understanding Android's security model with UIDs, and structuring the code in a robust and reusable manner. By the end, you will have a solid foundation for building sophisticated network monitoring features into your own applications.
The Foundation: Understanding NetworkStatsManager and UIDs
At the heart of Android's network usage tracking is the NetworkStatsManager
class. This system service acts as a gateway to the historical network data collected by the operating system. It doesn't provide real-time packet-by-packet analysis; instead, it offers aggregated data totals over specified time intervals for different network types (like Wi-Fi or mobile data).
A crucial concept to grasp is that Android doesn't track network statistics by package name (e.g., com.example.app
). Instead, it attributes all system resource usage, including network activity, to a **UID (User ID)**. When an application is installed, the Android OS assigns it a unique UID. This UID is used by the Linux kernel underneath to manage permissions and track resource consumption. Therefore, to query the data usage for a specific app, we must first find its unique UID.
The First Hurdle: Requesting the Necessary Permissions
Access to detailed app usage data is sensitive information. As such, Android gatekeeps the NetworkStatsManager
API behind a special permission: PACKAGE_USAGE_STATS
. This is not a standard runtime permission that can be requested with a simple dialog. It's an "app-op" (application operation) permission that the user must grant manually from a special screen within the device's settings.
Your application's responsibility is to detect if it has been granted this permission and, if not, guide the user to the correct settings screen to enable it.
1. Declare the Permission in the Manifest
First, you must declare your intention to use this permission in your AndroidManifest.xml
file.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Required permission to access network usage stats -->
<uses-permission
android:name="android.permission.PACKAGE_USAGE_STATS"
tools:ignore="ProtectedPermissions" />
<application
...>
...
</application>
</manifest>
Note the use of tools:ignore="ProtectedPermissions"
. This is necessary because Android Studio will flag this as a signature/system-level permission, but it is indeed available to regular apps, provided the user grants access.
2. Check for Permission and Redirect the User
Next, you need to implement a check in your code. Before attempting to query any data, you must verify that the user has enabled access for your app. If they haven't, you should launch an Intent
to take them directly to the appropriate settings page.
Here’s a helper function in Kotlin to perform this check:
import android.app.AppOpsManager
import android.content.Context
import android.content.Intent
import android.os.Process
import android.provider.Settings
fun hasUsageStatsPermission(context: Context): Boolean {
val appOpsManager = context.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager
val mode = appOpsManager.checkOpNoThrow(
AppOpsManager.OPSTR_GET_USAGE_STATS,
Process.myUid(),
context.packageName
)
return mode == AppOpsManager.MODE_ALLOWED
}
fun requestUsageStatsPermission(context: Context) {
// Redirect the user to the system settings screen
val intent = Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS)
context.startActivity(intent)
}
In your Activity or Fragment, you would use this logic like so:
override fun onResume() {
super.onResume()
if (!hasUsageStatsPermission(this)) {
// Optionally show a dialog explaining why the permission is needed
// before redirecting the user.
requestUsageStatsPermission(this)
} else {
// Permission is granted, proceed with fetching data.
fetchDataUsage()
}
}
This proactive approach ensures a smooth user experience and prevents your app from crashing due to a SecurityException
when trying to access the API without permission.
The Core Implementation: Querying Network Stats
With permissions handled, we can now dive into the main logic of fetching the data. The process can be broken down into several distinct steps: obtaining the UID, defining the query parameters, executing the query, and processing the results.
Step 1: Get the UID for a Target Package
As established, we need the UID. The PackageManager
is the tool for this job. Given a package name string, we can retrieve the corresponding ApplicationInfo
object, which contains the UID.
import android.content.Context
import android.content.pm.PackageManager
fun getUidForPackage(context: Context, packageName: String): Int? {
return try {
val pm = context.packageManager
val appInfo = pm.getApplicationInfo(packageName, 0)
appInfo.uid
} catch (e: PackageManager.NameNotFoundException) {
// The package is not installed on the device
e.printStackTrace()
null
}
}
This function gracefully handles the case where the requested package name is not installed on the device, returning null
.
Step 2: Define Query Parameters
The primary query method we'll use is NetworkStatsManager.queryDetailsForUid()
. It requires several key parameters to define the scope of our search:
- Network Type: This specifies whether we want to query for Wi-Fi, mobile data, or other network types. The constants are defined in
ConnectivityManager
, such asConnectivityManager.TYPE_WIFI
andConnectivityManager.TYPE_MOBILE
. - Subscriber ID: This is used to identify a specific SIM card's mobile data usage. For Wi-Fi queries, this parameter is ignored and should be
null
. For mobile data, you would typically get this fromTelephonyManager
. However, for general mobile data usage across all SIMs, you can often passnull
, and the system will aggregate the data.Note: Accessing the subscriber ID (IMSI) often requires the
READ_PHONE_STATE
permission, which is a sensitive runtime permission. Passingnull
is often sufficient and less intrusive. - Start and End Time: These are
Long
values representing the time interval for the query in milliseconds since the epoch. You can useSystem.currentTimeMillis()
or aCalendar
instance to define periods like "the last 24 hours" or "the current month." - UID: The unique user ID of the target application, which we obtained in the previous step.
Step 3: Execute the Query and Process the Results
The queryDetailsForUid()
method returns a NetworkStats
object. This object isn't a simple data class; it's an iterable cursor-like object that contains a series of NetworkStats.Bucket
objects. Each bucket represents a snapshot of data usage for the given UID within a sub-interval of the total time range you specified.
Therefore, we must iterate through all available buckets and sum their usage data to get the total for our desired period. The key data points in each bucket are rxBytes
(received bytes) and txBytes
(transmitted bytes).
Let's combine these steps into a cohesive function.
import android.app.usage.NetworkStats
import android.app.usage.NetworkStatsManager
import android.net.ConnectivityManager
import android.os.Build
import android.content.Context
fun getPackageDataUsage(
context: Context,
uid: Int,
startTime: Long,
endTime: Long
): Map<String, Long> {
val networkStatsManager =
context.getSystemService(Context.NETWORK_STATS_SERVICE) as NetworkStatsManager
var totalWifiRx: Long = 0
var totalWifiTx: Long = 0
var totalMobileRx: Long = 0
var totalMobileTx: Long = 0
// --- Query for Wi-Fi usage ---
try {
val wifiStats: NetworkStats = networkStatsManager.queryDetailsForUid(
ConnectivityManager.TYPE_WIFI,
null, // subscriberId - null for Wi-Fi
startTime,
endTime,
uid
)
// Use a 'use' block for automatic resource management (closes wifiStats)
wifiStats.use { stats ->
val bucket = NetworkStats.Bucket()
while (stats.hasNextBucket()) {
stats.getNextBucket(bucket)
totalWifiRx += bucket.rxBytes
totalWifiTx += bucket.txBytes
}
}
} catch (e: Exception) {
// Handle exceptions, e.g., SecurityException if permission is missing
e.printStackTrace()
}
// --- Query for Mobile data usage ---
try {
val mobileStats: NetworkStats = networkStatsManager.queryDetailsForUid(
ConnectivityManager.TYPE_MOBILE,
null, // subscriberId - null aggregates for all SIMs
startTime,
endTime,
uid
)
mobileStats.use { stats ->
val bucket = NetworkStats.Bucket()
while (stats.hasNextBucket()) {
stats.getNextBucket(bucket)
totalMobileRx += bucket.rxBytes
totalMobileTx += bucket.txBytes
}
}
} catch (e: Exception) {
e.printStackTrace()
}
return mapOf(
"wifiReceived" to totalWifiRx,
"wifiSent" to totalWifiTx,
"mobileReceived" to totalMobileRx,
"mobileSent" to totalMobileTx,
"total" to (totalWifiRx + totalWifiTx + totalMobileRx + totalMobileTx)
)
}
This implementation is more robust than a simple single-network function. It queries both Wi-Fi and mobile data separately and returns a detailed map of the results. Crucially, it uses Kotlin's .use { ... }
block, which is equivalent to a `try-with-resources` statement in Java. This ensures that the NetworkStats
object (which holds system resources) is automatically closed after use, preventing resource leaks.
Putting It All Together: A Reusable Utility Class
To make this functionality easy to use throughout an application, it's best to encapsulate it within a utility object or class. This class can manage the context, handle permissions, and provide simple, high-level functions.
Here is a complete example of a `DataUsageMonitor` object:import android.app.AppOpsManager
import android.app.usage.NetworkStats
import android.app.usage.NetworkStatsManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.ConnectivityManager
import android.os.Process
import android.provider.Settings
import java.util.Calendar
object DataUsageMonitor {
// A data class to hold the results in a structured way
data class UsageData(
val wifiReceivedBytes: Long = 0,
val wifiSentBytes: Long = 0,
val mobileReceivedBytes: Long = 0,
val mobileSentBytes: Long = 0
) {
val totalWifiBytes: Long get() = wifiReceivedBytes + wifiSentBytes
val totalMobileBytes: Long get() = mobileReceivedBytes + mobileSentBytes
val totalBytes: Long get() = totalWifiBytes + totalMobileBytes
}
/**
* Checks if the app has the necessary PACKAGE_USAGE_STATS permission.
*/
fun hasPermission(context: Context): Boolean {
val appOpsManager = context.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager
val mode = appOpsManager.checkOpNoThrow(
AppOpsManager.OPSTR_GET_USAGE_STATS,
Process.myUid(),
context.packageName
)
return mode == AppOpsManager.MODE_ALLOWED
}
/**
* Redirects the user to the settings screen to grant the usage stats permission.
*/
fun requestPermission(context: Context) {
val intent = Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
}
/**
* Retrieves data usage for a specific package name over a given time interval.
* Returns null if permission is not granted or the package is not found.
*/
fun getUsageForApp(context: Context, packageName: String, startTime: Long, endTime: Long): UsageData? {
if (!hasPermission(context)) {
return null
}
val uid = getUidForPackage(context, packageName) ?: return null
val nsm = context.getSystemService(Context.NETWORK_STATS_SERVICE) as NetworkStatsManager
val wifiUsage = queryUsageForUid(nsm, ConnectivityManager.TYPE_WIFI, uid, startTime, endTime)
val mobileUsage = queryUsageForUid(nsm, ConnectivityManager.TYPE_MOBILE, uid, startTime, endTime)
return UsageData(
wifiReceivedBytes = wifiUsage.first,
wifiSentBytes = wifiUsage.second,
mobileReceivedBytes = mobileUsage.first,
mobileSentBytes = mobileUsage.second
)
}
/**
* A helper function to query the NetworkStatsManager for a specific UID and network type.
* Returns a Pair of (receivedBytes, sentBytes).
*/
private fun queryUsageForUid(
networkStatsManager: NetworkStatsManager,
networkType: Int,
uid: Int,
startTime: Long,
endTime: Long
): Pair<Long, Long> {
var receivedBytes = 0L
var sentBytes = 0L
try {
networkStatsManager.queryDetailsForUid(
networkType,
null, // subscriberId
startTime,
endTime,
uid
).use { networkStats ->
val bucket = NetworkStats.Bucket()
while (networkStats.hasNextBucket()) {
networkStats.getNextBucket(bucket)
receivedBytes += bucket.rxBytes
sentBytes += bucket.txBytes
}
}
} catch (e: Exception) {
// Can be SecurityException or RemoteException
e.printStackTrace()
}
return Pair(receivedBytes, sentBytes)
}
private fun getUidForPackage(context: Context, packageName: String): Int? {
return try {
context.packageManager.getApplicationInfo(packageName, 0).uid
} catch (e: PackageManager.NameNotFoundException) {
null
}
}
/**
* Helper to get the start of the current day in milliseconds.
*/
fun getStartOfTodayMillis(): Long {
val calendar = Calendar.getInstance()
calendar.set(Calendar.HOUR_OF_DAY, 0)
calendar.set(Calendar.MINUTE, 0)
calendar.set(Calendar.SECOND, 0)
calendar.set(Calendar.MILLISECOND, 0)
return calendar.timeInMillis
}
}
Example Usage
Using this utility class in your Activity becomes incredibly clean and readable.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Example: Check permission and fetch data for Google Chrome
val targetPackage = "com.android.chrome"
if (DataUsageMonitor.hasPermission(this)) {
val startTime = DataUsageMonitor.getStartOfTodayMillis()
val endTime = System.currentTimeMillis()
val usageData = DataUsageMonitor.getUsageForApp(this, targetPackage, startTime, endTime)
usageData?.let {
val totalMB = it.totalBytes / (1024.0 * 1024.0)
Log.d("DataUsage", "$targetPackage has used ${"%.2f".format(totalMB)} MB of data today.")
Log.d("DataUsage", " - Wi-Fi: ${it.totalWifiBytes / 1024} KB")
Log.d("DataUsage", " - Mobile: ${it.totalMobileBytes / 1024} KB")
} ?: Log.e("DataUsage", "Could not retrieve usage for $targetPackage.")
} else {
// Guide user to grant permission
Log.d("DataUsage", "Permission not granted. Requesting...")
DataUsageMonitor.requestPermission(this)
}
}
}
Advanced Considerations and Final Thoughts
- Foreground vs. Background Usage: The
NetworkStats.Bucket
object also contains state information viabucket.state
. You can check this against constants likeNetworkStats.Bucket.STATE_FOREGROUND
andNetworkStats.Bucket.STATE_DEFAULT
to differentiate between data used while the app was actively on screen versus in the background. - Aggregated Device Data: If you want to get the total data usage for the entire device (not just one app), you can use
NetworkStatsManager.querySummaryForDevice()
. This method works similarly but doesn't require a UID. - Data Accuracy: The data provided by
NetworkStatsManager
is historically aggregated by the system. There can be a slight delay before the most recent usage is reflected in the query results. It is not a real-time monitoring tool for instantaneous bandwidth. - Removed and Uninstalled Apps: The system may still retain usage data for apps that have been uninstalled. The data is keyed by UID, and if that UID has not been reused, you might still see its historical data if you query all UIDs.
By using the NetworkStatsManager
, you can build powerful and user-centric features related to data management. The key to a successful implementation lies in a deep understanding of the Android permission model, the distinction between UIDs and package names, and careful resource management when handling the API's results. The comprehensive utility class provided here serves as a production-ready starting point for integrating this functionality into any modern Android application written in Kotlin.
0 개의 댓글:
Post a Comment