如何获取 Android Q 上应用程序的网络使用情况?

How to get network usage of apps on Android Q?

背景

我知道我们可以通过使用类似的东西(过去问过,)来获取指定应用程序的网络使用情况(到目前为止,移动和 Wifi 使用的总带宽,从某个特定时间开始):

    private final static int[] NETWORKS_TYPES = new int[]{ConnectivityManager.TYPE_WIFI, ConnectivityManager.TYPE_MOBILE};

    long rxBytes=0L, txBytes=0L;
    final TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
    final String subscriberId = telephonyManager.getSubscriberId();
    final ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(packageName, 0);
    final int uid = applicationInfo.uid;
    for (int networkType : NETWORKS_TYPES) {
        final NetworkStats networkStats = networkStatsManager.queryDetailsForUid(networkType, subscriberId, 0, System.currentTimeMillis(), uid); 
        final Bucket bucketOut = new Bucket();
        while (true) {
                networkStats.getNextBucket(bucketOut);
                final long rxBytes = bucketOut.getRxBytes();
                if (rxBytes >= 0)
                    totalRx += rxBytes;
                final long txBytes = bucketOut.getTxBytes();
                if (txBytes >= 0)
                    totalTx += txBytes;
                if (!networkStats.hasNextBucket())
                    break;
            }
        }
    }

文档:

https://developer.android.com/reference/android/app/usage/NetworkStatsManager.html#queryDetailsForUid(int,%20java.lang.String,%20long,%20long,%20int)

也可以获取全球网络使用情况(使用 TrafficStats.getUidRxBytes(applicationInfo.uid)TrafficStats.getUidTxBytes(applicationInfo.uid) ),但这不是本帖的全部内容。

问题

似乎 Android Q 计划导致许多设备身份功能不再工作,getSubscriberId 就是其中之一。

我试过的

我尝试将 targetSdk 设置为 29 (Q),看看当我尝试使用它时会发生什么。

不出所料,我得到了一个异常,表明我不能再这样做了。它说:

019-06-11 02:08:01.871 13558-13558/com.android.myapplication E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.android.myapplication, PID: 13558
    java.lang.SecurityException: getSubscriberId: The user 10872 does not meet the requirements to access device identifiers.
        at android.os.Parcel.createException(Parcel.java:2069)
        at android.os.Parcel.readException(Parcel.java:2037)
        at android.os.Parcel.readException(Parcel.java:1986)
        at com.android.internal.telephony.IPhoneSubInfo$Stub$Proxy.getSubscriberIdForSubscriber(IPhoneSubInfo.java:984)
        at android.telephony.TelephonyManager.getSubscriberId(TelephonyManager.java:3498)
        at android.telephony.TelephonyManager.getSubscriberId(TelephonyManager.java:3473)

在互联网和此处进行搜索,我没有看到提到这一点,但我发现了类似的问题,即获取 IMEI 和其他标识符:

所以现在我只是在这里做了一个关于它的错误报告(包括一个示例项目):

https://issuetracker.google.com/issues/134919382

问题

是否可以在 Android Q 上获取指定应用的网络使用情况(当定位到它时)?也许没有 subscriberId?

如果是,怎么做?

如果没有,是否可以通过 root 或通过 adb 来实现?


编辑:

好的,我不知道如何正式使用它,但至少对于 root 访问,可以使用 this solution, found from here.

获取 subscriberId

意思是这样的:

@SuppressLint("MissingPermission", "HardwareIds")
fun getSubscriberId(telephonyManager: TelephonyManager): String? {
    try {
        return telephonyManager.subscriberId
    } catch (e: Exception) {
    }
    val commandResult = Root.runCommands("service call iphonesubinfo 1 | grep -o \"[0-9a-f]\{8\} \" | tail -n+3 | while read a; do echo -n \"\u${a:4:4}\u${a:0:4}\"; done")
    val subscriberId = commandResult?.getOrNull(0)
    return if (subscriberId.isNullOrBlank()) null else subscriberId
}

当然,这不是官方解决方案,但总比没有好...

编辑:通过 root 获取它的部分是错误的。它没有任何帮助。

您提到的话题 comment 中的 google 团队说: " 状态:不会修复(预期行为) 这是按预期工作。 IMEI 是个人标识符,根据政策不会将其提供给应用程序。没有解决方法。”。所以我猜 class NetworkStatsManager 中的方法需要 IMSI(也被视为个人标识符)才能工作(如 queryDetailsForUid(int, String, long, long, int)) 现在在 Android 中被破坏 问:您可以使用这些方法获取应用程序的 Wifi 使用详细信息(通过为 subscriberId 传递空字符串)但是要获取移动使用详细信息,您现在必须依赖良好的old TrafficStats class 直到问题被注意到并解决。

我们正在使用 NetworkStatsManager.querySummaryForDevice()。由于一个偶然的错误,我们在 Q 中将 null 作为 MOBILE 的 subscriberId 传递。它似乎在我们的设备上工作。我不确定这是错误还是功能,但这些值符合我们预期的蜂窝网络使用情况。

综上所述,我们可以将 TrafficStats 用于此用例,但在 Pie 之前它是不稳定的。

您可以为 API 29 及以上提供空值。它returns WIFI 和移动数据的值。

Documentation:

If applicable, the subscriber id of the network interface. Starting with API level 29, the subscriberId is guarded by additional restrictions. Calling apps that do not meet the new requirements to access the subscriberId can provide a null value when querying for the mobile network type to receive usage for all mobile networks. For additional details see TelephonyManager.getSubscriberId().

权限(不要忘记获得用户的许可):

<uses-permission android:name="android.permission.READ_PHONE_STATE"
    android:maxSdkVersion="28"/>
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"
    tools:ignore="ProtectedPermissions" />

示例代码:

//network stats
NetworkStatsManager networkStatsManager = (NetworkStatsManager) activity.getSystemService(Context.NETWORK_STATS_SERVICE);
int[] networkTypes = new int[]{NetworkCapabilities.TRANSPORT_CELLULAR, NetworkCapabilities.TRANSPORT_WIFI};
String subscriberId;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
   TelephonyManager telephonyManager = (TelephonyManager) activity.getSystemService(Context.TELEPHONY_SERVICE);
   try {
     subscriberId = telephonyManager.getSubscriberId(); //MissingPermission
   } catch (SecurityException e) {
     subscriberId = null;
   }

} else {
   subscriberId = null;
}

获取应用程序的 NetworkStats:

long receivedBytes = 0;
long transmittedBytes = 0;

for (int networkType : networkTypes) {
    NetworkStats networkStats;
    try {
        networkStats = networkStatsManager
                .queryDetailsForUid(networkType,
                        subscriberId,
                        0,
                        System.currentTimeMillis(),
                        appUid);
    } catch (SecurityException e) {
        networkStats = null;
    }

    if(networkStats != null) {
        NetworkStats.Bucket bucketOut = new NetworkStats.Bucket();
        while (networkStats.hasNextBucket()) {
            networkStats.getNextBucket(bucketOut);
            long rxBytes = bucketOut.getRxBytes();
            long txBytes = bucketOut.getTxBytes();
            if (rxBytes >= 0) {
                receivedBytes += rxBytes;
            }
            if (txBytes >= 0) {
                transmittedBytes += txBytes;
            }
        }
        networkStats.close();
    }
}