Android Settings 应用二 获取应用消耗的流量

了解这一块也是因为有需求要获取指定应用所消耗的流量。在Android中,流量消耗主要分为手机卡和WIFI,在Settings中,也有统计流量的使用情况。经了解,Settings中是通过Loader去加载,Loader的原理就不多说了,很多大神的分析都很赞,这里仅介绍下该方法是怎么获取到流量的,其中会附带介绍Settings关于流量统计的源码分析思路。

一、入口

Settings中有关流量统计的功能是在com.android.settings.datausage包路径下,其中DataUsageSummary是流量使用情况的入口。其中有两个方法:

    /**
     * 添加Mobile流量统计
     * @param subId
     */
    private void addMobileSection(int subId) {
        TemplatePreferenceCategory category = (TemplatePreferenceCategory)
                inflatePreferences(R.xml.data_usage_cellular);
        category.setTemplate(getNetworkTemplate(subId), subId, services);
        category.pushTemplates(services);
    }

    /**
     * 添加Wifi流量统计
     */
    private void addWifiSection() {
        TemplatePreferenceCategory category = (TemplatePreferenceCategory)
                inflatePreferences(R.xml.data_usage_wifi);
        category.setTemplate(NetworkTemplate.buildTemplateWifiWildcard(), 0, services);
    }

这两个方法分别是添加Mobile和Wifi的流量统计,顺着两份xml文件查找下去,可以发现两个流量统计的入口都是指向DataUsageList。而此处setTemplate方法传入的两个Template会在后面区分是Mobile还是Wifi时用到。

//Mobile
NetworkTemplate.buildTemplateMobileAll(
                services.mTelephonyManager.getSubscriberId(subscriptionId));
//Wifi
NetworkTemplate.buildTemplateWifiWildcard()

二、功能实现

找到具体的入口后,便可以分析具体的功能实现。由于我们只关心流量数据的变化,在抛去繁琐的UI初始化以后,我们可以发现跟数据变化有关的两个方法:updateDetailData()和bindStats()。下面利用注释分析下这两个方法的主要功能:

    /**
     * Update details based on {@link #mChart} inspection range depending on
     * current mode. Updates {@link #mAdapter} with sorted list
     * of applications data usage.
     */
    private void updateDetailData() {
        if (LOGD) Log.d(TAG, "updateDetailData()");
        //该参数主要用于指定查询的时间范围
        final long start;
        final long end;
        ...
        ...//此处省略了start,和end处理逻辑。
        ...
        
        final long now = System.currentTimeMillis();
        final Context context = getActivity();
        NetworkStatsHistory.Entry entry = null;
        if (mChartData != null) {
            entry = mChartData.network.getValues(start, end, now, null);
        }
        //初始化Loader
        // kick off loader for detailed stats
        getLoaderManager().restartLoader(LOADER_SUMMARY,
                SummaryForAllUidLoader.buildArgs(mTemplate, start, end), mSummaryCallbacks);
        //显示总的消耗量
        final long totalBytes = entry != null ? entry.rxBytes + entry.txBytes : 0;
        final String totalPhrase = Formatter.formatFileSize(context, totalBytes);
        mUsageAmount.setTitle(getString(R.string.data_used_template, totalPhrase));
        if (mShowDataUsage) {
            final int summaryRes = R.string.data_usage_total_during_range;
            final String rangePhrase = Utils.formatDateRange(context, mSelectLeft, mSelectRight);
            mUsageSummary.setSummary(getString(summaryRes, totalPhrase, rangePhrase));
        }
    }

Loader终于出现了,通过LoaderManager进行管理,其中restartLoader需要三个参数,其中一个参数由SummaryForAllUidLoader生成。该类位于com.android.settingslib.net路径下,SummaryForAllUidLoader是继承自AsyncTaskLoader实现异步加载。该类的buildArgs方法将之前提到的Template、start、end分装成Bundle格式。第二个参数LoaderCallbacks,该类是与Loader处理过程息息相关的,具体原理可以参照Loader相关的博客。

private final LoaderCallbacks mSummaryCallbacks = new LoaderCallbacks<
            NetworkStats>() {
        @Override
        public Loader onCreateLoader(int id, Bundle args) {
            //创建异步加载的Loader,这里的mStatsSession是指的是INetworkStatsSession,是获取流量的主要入口
            return new SummaryForAllUidLoader(getActivity(), mStatsSession, args);
        }

        @Override
        public void onLoadFinished(Loader loader, NetworkStats data) {            //加载结束后会调用该方法,用于更新数据源
            final int[] restrictedUids = services.mPolicyManager.getUidsWithPolicy(
                    POLICY_REJECT_METERED_BACKGROUND);
            bindStats(data, restrictedUids);
            updateEmptyVisible();
        }

        @Override
        public void onLoaderReset(Loader loader) {
            //当restartLoader时,会调用该方法,用于重置数据源
            bindStats(null, new int[0]);
            updateEmptyVisible();
        }
    };

查看SummaryForAllUidLoader,可以发现,获取流量的主要方法还是通过INetworkStatsSession.getSummaryForAllUid()这个方法来获取的,此处我们可以展开联想直接操作INetworkStatsSession是否就可以获取到流量统计结果了?

onLoadFinished中的NetworkStats就是我们要获取的流量数据,而且是所有应用的流量数据。在加载结束后会调用bindStats()来处理数据,该方法不做详细介绍,我们只要获取到NetworkStats就可以用自己的逻辑来处理流量数据。接下来介绍下本人封装的方法,用于获取全部应用的流量数据:

private static final int LOADER_SUMMARY = 3;

INetworkStatsSession mSession ;
PackageManager pm = null;
NetworkStats mStats = null;

public Map forLoaderManager(NetworkTemplate template, long start, long end){
        //用于获取INetworkStatsSession
        INetworkStatsService mService
                = INetworkStatsService.Stub.asInterface(ServiceManager.getService(Context.NETWORK_STATS_SERVICE));
        try {
            mSession = mService.openSession();
            if (mSession == null) {
                Log.e(TAG, "getDataUsageWifi: the session is null" );
                return null;
            }
            mStats = null;
            ((Activity)mContext).getLoaderManager().restartLoader(LOADER_SUMMARY,
                    SummaryForAllUidLoader.buildArgs(template, start, end), mSummaryCallbacks);
            //下面是我自己实现的数据处理逻辑,由于需要实现同步返回,所以利用while去控制,如有更好的方法,欢迎指教
            int time = 50;
            while(true){
                if(mStats != null)
                    break;
                if(time-- < 0)
                    break;
                Thread.sleep(50);
            }
            if(mStats == null){
                Log.e(TAG, "getDataUsageWifi: get the netword data usage failed" );
                return null;
            }
            pm = mContext.getPackageManager();
            Map map = new HashMap<>();
            int size = mStats.size();
            NetworkStats.Entry entity = null;
            for(int i=0; i

三、另一种更直接的方式

由于需要提供Service方式的SDK,而Loader是Activity和Fragment中才有的,所以上述的方法无法实现。通过上述的分析知道数据源其实是从Context.NETWORK_STATS_SERVICE这个服务获取的。那是否可以直接通过操作服务来获取到数据呢。所以就有了另外一种直接的获取方式来实现我的功能:

public Map forNetworkStatsManager(int type, long start, long end){
        Log.d(TAG, "getAllDataUsage: start="+ start+ ", end="+ end);
        //用于获取应用UID
        PackageManager pm = (PackageManager) mContext.getPackageManager();
        if(pm == null){
            Log.e(TAG, "getAllDataUsage: get the telephone manager failed" );
            return null;
        }
        //用于获取subscriberId
        TelephonyManager tm = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
        if(pm == null){
            Log.e(TAG, "getAllDataUsage: get the telephone manager failed" );
            return null;
        }
        //用于获取流量数据
        NetworkStatsManager nsm = (NetworkStatsManager) mContext.getSystemService(Context.NETWORK_STATS_SERVICE);
        if(nsm == null){
            Log.e(TAG, "getAllDataUsage: get the network stats manager failed");
            return null;
        }
        List list = pm.getInstalledApplications(PackageManager.GET_ACTIVITIES);
        try {
            Map result = getDataUsageByUid(nsm, tm, type, start, end);
            if(result == null)return null;
            for (ApplicationInfo info: list) {
                DataUsageWifi data = result.get(info.uid);
                if(data == null)
                    continue;
                data.packageName = info.packageName;
            }
            return result;
        } catch (RemoteException e) {
            e.printStackTrace();
            return null;
        }
    }

    private Map getDataUsageByUid(NetworkStatsManager nsm, TelephonyManager tm,
                                           int type, long start, long end) throws RemoteException {
        Log.d(TAG, "listDataUsageByUid: executed");
        // 获取subscriberId
        String subId = tm.getSubscriberId();
        if((type==ConnectivityManager.TYPE_MOBILE) && (subId==null)){
            Log.e(TAG, "listDataUsageByUid: get sub id fail" );
            return null;
        }
        android.app.usage.NetworkStats summaryStats;
        android.app.usage.NetworkStats.Bucket summaryBucket
                = new android.app.usage.NetworkStats.Bucket();
        Map map = new HashMap<>();
        //此处的type用于决定是获取Mobile通道的流量数据还是获取Wifi通道的流量数据
        //ConnectivityManager.TYPE_MOBILE
        //ConnectivityManager.TYPE_WIFI
        summaryStats = nsm.querySummary(type, subId, start, end);
        do {
            summaryStats.getNextBucket(summaryBucket);
            int summaryUid = summaryBucket.getUid();
            long summaryRx = summaryBucket.getRxBytes();
            long summaryTx = summaryBucket.getTxBytes();
            Log.i(TAG, "uid:" + summaryBucket.getUid() + " rx:" + summaryBucket.getRxBytes() +
                    " tx:" + summaryBucket.getTxBytes());
            DataUsageWifi info = new DataUsageWifi(summaryUid, "", summaryRx, summaryTx);
            map.put(summaryUid, info);
        } while (summaryStats.hasNextBucket());
        return map;
    }

NetworkStatsManager还有其他几个方法,可以通过UID去获取指定应用的流量,实现起来更加方便。至于性能方面有没有差异,还没有仔细去研究,下一步将会去探讨这些,以及Context.NETWORK_STATS_SERVICE中的这个服务到底都做了哪些活。

刚开始学习总结工作内容,想到什么写什么,感觉很乱咯。稍后会将Demo的源码上传,有需要的朋友可以下载运行看看,同样需要系统签名,具体操作可以操考应用耗电量那一篇的方式进行操作。

 

 

 

你可能感兴趣的:(Settings)