天天看点

Android Settings 分析一 应用耗电量获取

之前都没有写博客的习惯,工作上的一些想法和思路过一段时间就忘的差不多了。最近在做Settings相关功能的SDK定制,所以想趁这个机会把手上遇到的思路和解决方法整理下,方便日后回顾。想做成一个系列,其中有的也是参考网络大神的思路,有的是翻看源码整理的,请大家多多关照。

Android的电量信息通常都是指设备的电量信息,例如充电状态、耗电量、充电方式等等。常用的获取方式是注册“Intent.ACTION_BATTERY_CHANGED”广播获取,并且需要申请“<uses-permission android:name="android.permission.BATTERY_STATS"/>”权限。如果想获取某个应用对应的耗电量呢,要怎么做?Settings已经提供了实现方法,我们可以参考源码来实现。

具体实现在Settings “com.android.settings.fuelgauge”路径下,主要入口是PowerUsageSummary.java,该类继承自PowerUsageBase.java,其实就是一个“SettingsPreferenceFragment”。在排除UI方面的创建实现后,关注“refreshStats()”这个方法就行了,所有有关数据的处理都在这个方法里面实现。以下是代码,其中跟UI有关的代码就不贴了,具体分析见注释。

(此处先插入对于BatteryStatsHelper的分析,略读refreshStats()方法,可发现主要的数据源是从BatteryStatsHelper中获取的,而BatteryStatsHelper在PowerUsageBase.java中定义,在其生命周期中,主要做了一下几个操作:

//onAttach 中调用,创建实例
mUm = (UserManager) activity.getSystemService(Context.USER_SERVICE);
mStatsHelper = new BatteryStatsHelper(activity, true);
//onCreate 中调用,初始化BatteryStatsHelper
mStatsHelper.create(icicle);
//onStart 中调用,清除历史数据
mStatsHelper.clearStats();
//onResume 中调用,删除历史数据文件,并注册电量状态广播,同时再次清除历史数据
BatteryStatsHelper.dropFile(getActivity(), BatteryHistoryPreference.BATTERY_HISTORY_FILE);
        updateBatteryStatus(getActivity().registerReceiver(mBatteryInfoReceiver,
                new IntentFilter(Intent.ACTION_BATTERY_CHANGED)));
        if (mHandler.hasMessages(MSG_REFRESH_STATS)) {
            mHandler.removeMessages(MSG_REFRESH_STATS);
            mStatsHelper.clearStats();
        }

//子类中会重写该方法,主要是在UserProfile和BatteryStatsHelper之间建立连接
protected void refreshStats() {
        mStatsHelper.refreshStats(BatteryStats.STATS_SINCE_CHARGED, mUm.getUserProfiles());
    }
           

这些操作需要在从BatteryStatsHelper中获取数据前执行,如果想自己封装方法的话,在创建BatteryStatsHelper实例后执行即可)

/**
 * 该方法执行前,上述的关于BatteryStatsHelper的几个初始化方法已经执行。
 */
protected void refreshStats() {
        super.refreshStats();//调用父类的更新方法,更新UserProfile配置
        ...  
        ...//此处省略关于UI的更新实现
        ...
        //电源配置信息
        final PowerProfile powerProfile = mStatsHelper.getPowerProfile();
        //电池状态信息
        final BatteryStats stats = mStatsHelper.getStats();
        //获取平均耗电量,用于和阈值对比
        final double averagePower = powerProfile.getAveragePower(PowerProfile.POWER_SCREEN_FULL);

        ...
        ...//省略获取UI相关的数据
        ...
        //如果平均耗电量小于阈值,或不使用假数据的话,就不显示应用耗电量
        if (averagePower >= MIN_AVERAGE_POWER_THRESHOLD_MILLI_AMP || USE_FAKE_DATA) {
            //根据UID进行合并分组,
            final List<BatterySipper> usageList = getCoalescedUsageList(
                    //getFakeStat 伪造数据显示
                    USE_FAKE_DATA ? getFakeStats() : mStatsHelper.getUsageList());
            //获取上次充满电后消耗的电量。 
            //mStatsType的值为BatteryStats.STATS_SINCE_CHARGED,代表了我们的计算规则是从上次充满电后数据,还有一种规则是STATS_SINCE_UNPLUGGED是拔掉USB线后的数据
            final int dischargeAmount = USE_FAKE_DATA ? 5000
                    : stats != null ? stats.getDischargeAmount(mStatsType) : 0;
            final int numSippers = usageList.size();
            //遍历BatterySipper进行处理
            for (int i = 0; i < numSippers; i++) {
                final BatterySipper sipper = usageList.get(i);
                //如果耗电功率小于 阈值,则不显示
                if ((sipper.totalPowerMah * SECONDS_IN_HOUR) < MIN_POWER_THRESHOLD_MILLI_AMP) {
                    continue;
                }
                //设备总耗电量
                double totalPower = USE_FAKE_DATA ? 4000 : mStatsHelper.getTotalPower();
                //计算百分比
                final double percentOfTotal =
                        ((sipper.totalPowerMah / totalPower) * dischargeAmount);
                //如果百分比小于0.5,则不显示
                if (((int) (percentOfTotal + .5)) < 1) {
                    continue;
                }
                if (sipper.drainType == BatterySipper.DrainType.OVERCOUNTED) {
                    // Don't show over-counted unless it is at least 2/3 the size of
                    // the largest real entry, and its percent of total is more significant
                    if (sipper.totalPowerMah < ((mStatsHelper.getMaxRealPower()*2)/3)) {
                        continue;
                    }
                    if (percentOfTotal < 10) {
                        continue;
                    }
                    if ("user".equals(Build.TYPE)) {
                        continue;
                    }
                }
                if (sipper.drainType == BatterySipper.DrainType.UNACCOUNTED) {
                    // Don't show over-counted unless it is at least 1/2 the size of
                    // the largest real entry, and its percent of total is more significant
                    if (sipper.totalPowerMah < (mStatsHelper.getMaxRealPower()/2)) {
                        continue;
                    }
                    if (percentOfTotal < 5) {
                        continue;
                    }
                    if ("user".equals(Build.TYPE)) {
                        continue;
                    }
                }
                ...
                ...省略UI更新
                ...
                final double percentOfMax = (sipper.totalPowerMah * 100)
                        / mStatsHelper.getMaxPower();
                sipper.percent = percentOfTotal;
                ...
                ...
                ...省略UI更新
            }
        }
        ...
        ...//省略UI更新
        ...
    }
           

至此,应用就可以获取到Settings电池中显示的应用耗电量信息。如果想获取全部已安装应用的耗电量,则控制上面continue的判断即可,全部屏蔽就可以获取全部的应用耗电量。在不同的API版本,有些方法可能被隐藏起来,需要用反射的方式获取。还有的计算策略有偏差,或者干脆某个参数被舍弃了,这些差异只要查看源码就可以了解。

最重要的权限忘记说了,应用需要申请如下权限:

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    <uses-permission android:name="android.permission.DEVICE_POWER"/>
    <uses-permission android:name="android.permission.BATTERY_STATS"/>
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
           

并且对于普通应用来说,是没有权限通过这种方式获取应用耗电量信息的,系统会抛出如下异常:

java.lang.SecurityException: uid 10089 does not have android.permission.BATTERY_STATS.
           

应用需要配置android.uid.system,并且进行系统签名:就是获取系统平台下的这两个文件,然后用java -jar命令进行签名。

Android Settings 分析一 应用耗电量获取

稍后会将源码上传,仅供参考。如果需要运行的话,需要将对应系统的上述两个文件放在sign目录下,并用install.sh中的命令签名后再安装。