No shutdown animation in the electricity display only 1%
低电量自动关机时无关机动画
1. 问题描述
- DEFECT DESCRIPTION:
No shutdown animation in the electricity display only 1%. - REPRODUCING PROCEDURES:
电量消耗显示只有1%时,手机突然黑屏关机,没有关机动画,长按power键后手机又可以正常开机使用.(黑屏关机后插上充电器,电量显示为1%) - EXPECTED BEHAVIOUR:
There should a shutdown animation in the electricity display only 1%.
2. 分析
关机动画的逻辑主要是在上层进行控制的,该上层的入口是文件BatteryService.java。系统会通过Binder机制将底层检测的到电池电量传入到属性mBatteryLevel中,然后对属性mBatteryLevel进行if操作。
- 首先,底层由c/c++来检测电池信息并周期性更新电池状态。
- 系统通过bingder机制将底层检测到的电池状态传入上层(BatteryService.java)
- 上层接收底层传来的电池信息,并根据不同的电池状态做出相应的操作,如当电池电量为0时,会播放关机动画。
1.电池信息
更新电池电量的服务位于health守护进程中,其在init.rc中的定义如下:
service healthd /sbin/healthd
class core
critical
seclabel u:r:healthd:s0
group root system wakelock
而该服务的相关代码位于目录system/core/health下,文件health.cpp的main函数如下:
int main(int argc,char**argv){
int ch;
int ret;
klog_set_level(KLOG_LEVEL);
healthd_mode_ops =&android_ops;
/*通过main函数的参数来判断当前手机状态*/
if(!strcmp(basename(argv[0]),"charger")){
/*从字面意思看 应该是在充电状态*/
healthd_mode_ops =&charger_ops;
}else{
while((ch = getopt(argc, argv,"cr"))!=-1){
switch(ch){
case'c':
healthd_mode_ops =&charger_ops;
break;
case'r':
healthd_mode_ops =&recovery_ops;
break;
case'?':
default:
KLOG_ERROR(LOG_TAG,"Unrecognized healthd option: %c\n",
optopt);
exit(1);
}
}
}
ret = healthd_init();
/*错误处理*/
if(ret){
KLOG_ERROR("Initialization failed, exiting\n");
exit(2);
}
/*进入循环,也就是说手机从开启该服务开始,之后一直停留在循环中*/
healthd_mainloop();
KLOG_ERROR("Main loop terminated, exiting\n");
return3;
}
然后看看函数healthd_init()做了些什么事,其代码如下:
staticint healthd_init(){
/*没看懂是什么意思,好像是调用的bionic基础库的方法*/
epollfd = epoll_create(MAX_EPOLL_EVENTS);
if(epollfd ==-1){
KLOG_ERROR(LOG_TAG,
"epoll_create failed; errno=%d\n",
errno);
return-1;
}
/*看名字该是用电池配置信息进行初始化操作*/
healthd_board_init(&healthd_config);
/*应该是设置电池模式之类的操作*/
healthd_mode_ops->init(&healthd_config);
/*创建一个定时器*/
wakealarm_init();
/*初始化uevent环境*/
uevent_init();
/*创建BatteryMonitor对象*/
gBatteryMonitor =newBatteryMonitor();
/*对创建的BatteryMonitor对象进行初始化*/
gBatteryMonitor->init(&healthd_config);
return0;
}
然后看看这个定时器怎么创建的,具体代码如下:
staticvoid wakealarm_init(void){
wakealarm_fd = timerfd_create(CLOCK_BOOTTIME_ALARM, TFD_NONBLOCK);
if(wakealarm_fd ==-1){
KLOG_ERROR(LOG_TAG,"wakealarm_init: timerfd_create failed\n");
return;
}
if(healthd_register_event(wakealarm_fd, wakealarm_event))
KLOG_ERROR(LOG_TAG,
"Registration of wakealarm event failed\n"); wakealarm_set_interval(healthd_config.periodic_chores_interval_fast);
}
用方法timerfd_create()创建一个定时器句柄,然后把它存入到全局变量wakealarm_fd中,函数wakealarm_set_interval()设置超时时间。
然后会创建一个BatteryMonitor对象并调用该对象的init方法,BatteryMonitor文件位于system/core/healthd/BatteryMonitor.cpp,该文件主要是读取电池的各种参数并返回给上层,先来看看该对象的init方法,具体如下:
voidBatteryMonitor::init(struct healthd_config *hc){
String8 path;
char pval[PROPERTY_VALUE_MAX];
mHealthdConfig = hc;
/*
#define POWER_SUPPLY_SUBSYSTEM "power_supply"
#define POWER_SUPPLY_SYSFS_PATH "/sys/class/" POWER_SUPPLY_SUBSYSTEM
根据宏定义 POWER_SUPPLY_SYSFS_PATH="/sys/class/power_supply"即打开该目录
,而这个目录下的文件就是存储了电池的各中信息,如我们需要的电池电量值就在/sys/class/power_supply/battery/capacity文件中
,通过命令cat /sys/class/power_supply/battery/capacity就可以查看到当前电池电量值
*/
DIR* dir = opendir(POWER_SUPPLY_SYSFS_PATH);
if(dir == NULL){
KLOG_ERROR(LOG_TAG,"Could not open %s\n", POWER_SUPPLY_SYSFS_PATH);
}else{
struct dirent* entry;
while((entry = readdir(dir))){
constchar* name = entry->d_name;
/*过滤掉./目录和../目录*/
if(!strcmp(name,".")||!strcmp(name,".."))
continue;
// Look for "type" file in each subdirectory
path.clear();
path.appendFormat("%s/%s/type", POWER_SUPPLY_SYSFS_PATH, name);
switch(readPowerSupplyType(path)){
case ANDROID_POWER_SUPPLY_TYPE_AC:
case ANDROID_POWER_SUPPLY_TYPE_USB:
case ANDROID_POWER_SUPPLY_TYPE_WIRELESS:
path.clear();
path.appendFormat("%s/%s/online", POWER_SUPPLY_SYSFS_PATH, name);
if(access(path.string(), R_OK)==0)
mChargerNames.add(String8(name));
break;
case ANDROID_POWER_SUPPLY_TYPE_BATTERY:
mBatteryDevicePresent =true;
if(mHealthdConfig->batteryStatusPath.isEmpty()){
path.clear();
path.appendFormat("%s/%s/status", POWER_SUPPLY_SYSFS_PATH,
name);
if(access(path, R_OK)==0)
mHealthdConfig->batteryStatusPath = path;
}
if(mHealthdConfig->batteryHealthPath.isEmpty()){
path.clear();
path.appendFormat("%s/%s/health", POWER_SUPPLY_SYSFS_PATH,
name);
if(access(path, R_OK)==0)
mHealthdConfig->batteryHealthPath = path;
}
if(mHealthdConfig->batteryPresentPath.isEmpty()){
path.clear();
path.appendFormat("%s/%s/present", POWER_SUPPLY_SYSFS_PATH,
name);
if(access(path, R_OK)==0)
mHealthdConfig->batteryPresentPath = path;
}
/*此处就是读取电池电量值,并把读到的值保存到mHealthdConfig成员变量中*/
if(mHealthdConfig->batteryCapacityPath.isEmpty()){
path.clear();
path.appendFormat("%s/%s/capacity", POWER_SUPPLY_SYSFS_PATH,
name);
if(access(path, R_OK)==0)
mHealthdConfig->batteryCapacityPath = path;
}
if(mHealthdConfig->batteryVoltagePath.isEmpty()){
path.clear();
path.appendFormat("%s/%s/voltage_now",
POWER_SUPPLY_SYSFS_PATH, name);
if(access(path, R_OK)==0){
mHealthdConfig->batteryVoltagePath = path;
}else{
path.clear();
path.appendFormat("%s/%s/batt_vol",
POWER_SUPPLY_SYSFS_PATH, name);
if(access(path, R_OK)==0)
mHealthdConfig->batteryVoltagePath = path;
}
}
if(mHealthdConfig->batteryFullChargePath.isEmpty()){
path.clear();
path.appendFormat("%s/%s/charge_full",
POWER_SUPPLY_SYSFS_PATH, name);
if(access(path, R_OK)==0)
mHealthdConfig->batteryFullChargePath = path;
}
if(mHealthdConfig->batteryCurrentNowPath.isEmpty()){
path.clear();
path.appendFormat("%s/%s/current_now",
POWER_SUPPLY_SYSFS_PATH, name);
if(access(path, R_OK)==0)
mHealthdConfig->batteryCurrentNowPath = path;
}
if(mHealthdConfig->batteryCycleCountPath.isEmpty()){
path.clear();
path.appendFormat("%s/%s/cycle_count",
POWER_SUPPLY_SYSFS_PATH, name);
if(access(path, R_OK)==0)
mHealthdConfig->batteryCycleCountPath = path;
}
if(mHealthdConfig->batteryCurrentAvgPath.isEmpty()){
path.clear();
path.appendFormat("%s/%s/current_avg",
POWER_SUPPLY_SYSFS_PATH, name);
if(access(path, R_OK)==0)
mHealthdConfig->batteryCurrentAvgPath = path;
}
if(mHealthdConfig->batteryChargeCounterPath.isEmpty()){
path.clear();
path.appendFormat("%s/%s/charge_counter",
POWER_SUPPLY_SYSFS_PATH, name);
if(access(path, R_OK)==0)
mHealthdConfig->batteryChargeCounterPath = path;
}
if(mHealthdConfig->batteryTemperaturePath.isEmpty()){
path.clear();
path.appendFormat("%s/%s/temp", POWER_SUPPLY_SYSFS_PATH,
name);
if(access(path, R_OK)==0){
mHealthdConfig->batteryTemperaturePath = path;
}else{
path.clear();
path.appendFormat("%s/%s/batt_temp",
POWER_SUPPLY_SYSFS_PATH, name);
if(access(path, R_OK)==0)
mHealthdConfig->batteryTemperaturePath = path;
}
}
if(mHealthdConfig->batteryTechnologyPath.isEmpty()){
path.clear();
path.appendFormat("%s/%s/technology",
POWER_SUPPLY_SYSFS_PATH, name);
if(access(path, R_OK)==0)
mHealthdConfig->batteryTechnologyPath = path;
}
if(mHealthdConfig->batteryStatusPath_smb.isEmpty()){
path.clear();
path.appendFormat("%s/%s/status_smb", POWER_SUPPLY_SYSFS_PATH,
name);
if(access(path, R_OK)==0)
mHealthdConfig->batteryStatusPath_smb = path;
}
if(mHealthdConfig->batteryPresentPath_smb.isEmpty()){
path.clear();
path.appendFormat("%s/%s/present_smb", POWER_SUPPLY_SYSFS_PATH,
name);
if(access(path, R_OK)==0)
mHealthdConfig->batteryPresentPath_smb = path;
}
if(mHealthdConfig->batteryCapacityPath_smb.isEmpty()){
path.clear();
path.appendFormat("%s/%s/capacity_smb", POWER_SUPPLY_SYSFS_PATH,
name);
if(access(path, R_OK)==0)
mHealthdConfig->batteryCapacityPath_smb = path;
}
if(mHealthdConfig->batteryAdjustPowerPath.isEmpty()){
path.clear();
path.appendFormat("%s/%s/adjust_power",
POWER_SUPPLY_SYSFS_PATH, name);
if(access(path, R_OK)==0)
mHealthdConfig->batteryAdjustPowerPath = path;
}
break;
case ANDROID_POWER_SUPPLY_TYPE_UNKNOWN:
break;
}
}
closedir(dir);
}
// Typically the case for devices which do not have a battery and
// and are always plugged into AC mains.
if(!mBatteryDevicePresent){
KLOG_WARNING(LOG_TAG,"No battery devices found\n");
hc->periodic_chores_interval_fast =-1;
hc->periodic_chores_interval_slow =-1;
mBatteryFixedCapacity = ALWAYS_PLUGGED_CAPACITY;
mBatteryFixedTemperature = FAKE_BATTERY_TEMPERATURE;
mAlwaysPluggedDevice =true;
}else{
if(mHealthdConfig->batteryStatusPath.isEmpty())
KLOG_WARNING(LOG_TAG,"BatteryStatusPath not found\n");
if(mHealthdConfig->batteryHealthPath.isEmpty())
KLOG_WARNING(LOG_TAG,"BatteryHealthPath not found\n");
if(mHealthdConfig->batteryPresentPath.isEmpty())
KLOG_WARNING(LOG_TAG,"BatteryPresentPath not found\n");
if(mHealthdConfig->batteryCapacityPath.isEmpty())
KLOG_WARNING(LOG_TAG,"BatteryCapacityPath not found\n");
if(mHealthdConfig->batteryVoltagePath.isEmpty())
KLOG_WARNING(LOG_TAG,"BatteryVoltagePath not found\n");
if(mHealthdConfig->batteryTemperaturePath.isEmpty())
KLOG_WARNING(LOG_TAG,"BatteryTemperaturePath not found\n");
if(mHealthdConfig->batteryTechnologyPath.isEmpty())
KLOG_WARNING(LOG_TAG,"BatteryTechnologyPath not found\n");
if(mHealthdConfig->batteryCurrentNowPath.isEmpty())
KLOG_WARNING(LOG_TAG,"BatteryCurrentNowPath not found\n");
if(mHealthdConfig->batteryFullChargePath.isEmpty())
KLOG_WARNING(LOG_TAG,"BatteryFullChargePath not found\n");
if(mHealthdConfig->batteryCycleCountPath.isEmpty())
KLOG_WARNING(LOG_TAG,"BatteryCycleCountPath not found\n");
if(mHealthdConfig->batteryPresentPath_smb.isEmpty())
KLOG_WARNING(LOG_TAG,"BatteryPresentPath_smb not found\n");
if(mHealthdConfig->batteryCapacityPath_smb.isEmpty())
KLOG_WARNING(LOG_TAG,"BatteryCapacityPath_smb not found\n");
if(mHealthdConfig->batteryStatusPath_smb.isEmpty())
KLOG_WARNING(LOG_TAG,"BatteryStatusPath_smb not found\n");
}
if(property_get("ro.boot.fake_battery", pval, NULL)>0
&& strtol(pval, NULL,10)!=0){
mBatteryFixedCapacity = FAKE_BATTERY_CAPACITY;
mBatteryFixedCapacity_smb = FAKE_BATTERY_CAPACITY_SMB;
mBatteryFixedTemperature = FAKE_BATTERY_TEMPERATURE;
}
}
该方法会打开/sys/class/power_supply目录,该目录中包含了一些子目录,/sys/class/power_supply/battery目录下存放得就是电池信息。在case ANDROID_POWER_SUPPLY_TYPE_BATTERY:
情况下会生成与电池相关的文件名,并把对应的信息保存到成员变量中,方便后面读取使用。
然后是main函数进入healthd_mainloop()循环,其代码如下:
staticvoid healthd_mainloop(void){
while(1){
struct epoll_event events[eventct];
int nevents;
int timeout = awake_poll_interval;
int mode_timeout;
mode_timeout = healthd_mode_ops->preparetowait();
if(timeout <0||(mode_timeout >0&& mode_timeout < timeout))
timeout = mode_timeout;
nevents = epoll_wait(epollfd, events, eventct, timeout);
if(nevents ==-1){
if(errno == EINTR)
continue;
KLOG_ERROR(LOG_TAG,"healthd_mainloop: epoll_wait failed\n");
break;
}
for(int n =0; n < nevents;++n){
if(events[n].data.ptr)
/* (*(void (*)(int))events[n].data.ptr)(events[n].events)这一行代码看起来好狗屎
,应该是把events[n].data.ptr(应该是某个函数指针)强制转换为参数为int型的返回值为空的函数的指针
,反正是一个指定类型的函数指针,然后再加了*号,表示调用该函数*/
(*(void(*)(int))events[n].data.ptr)(events[n].events);
}
if(!nevents)
periodic_chores();
healthd_mode_ops->heartbeat();
}
return;
}
staticvoid wakealarm_event(uint32_t/*epevents*/){
unsignedlonglong wakeups;
if(read(wakealarm_fd,&wakeups,sizeof(wakeups))==-1){
KLOG_ERROR(LOG_TAG,"wakealarm_event: read wakealarm fd failed\n");
return;
}
periodic_chores();
}
staticvoid periodic_chores(){
healthd_battery_update();
}
void healthd_battery_update(void){
// Fast wake interval when on charger (watch for overheat);
// slow wake interval when on battery (watch for drained battery).
/*调用BatteryMonitor的update方法更新电池状态*/
int new_wake_interval = gBatteryMonitor->update()?
healthd_config.periodic_chores_interval_fast :
healthd_config.periodic_chores_interval_slow;
if(new_wake_interval != wakealarm_wake_interval)
wakealarm_set_interval(new_wake_interval);
// During awake periods poll at fast rate. If wake alarm is set at fast
// rate then just use the alarm; if wake alarm is set at slow rate then
// poll at fast rate while awake and let alarm wake up at slow rate when
// asleep.
if(healthd_config.periodic_chores_interval_fast ==-1)
awake_poll_interval =-1;
else
awake_poll_interval =
new_wake_interval == healthd_config.periodic_chores_interval_fast ?
-1: healthd_config.periodic_chores_interval_fast *1000;
}
然后查看文件system/core/healthd/BatteryMonitor.cpp中的update,该方法主要是用来读取电池信息,在health的main方法中,会通过health_mainloop()函数周期性调用它。
boolBatteryMonitor::update(void){
bool logthis;
/*每次调用前会把BatteryProperties 对象中的值清空*/
initBatteryProperties(&props);
if(!mHealthdConfig->batteryPresentPath.isEmpty())
props.batteryPresent = getBooleanField(mHealthdConfig->batteryPresentPath);
else
props.batteryPresent = mBatteryDevicePresent;
/*将保存在mHealthdConfig中的电池信息赋值给BatteryProperties对象props中的各个成员变量,后面还会通过该对象把电池信息发送给上层*/
props.batteryLevel = mBatteryFixedCapacity ?
mBatteryFixedCapacity :
getIntField(mHealthdConfig->batteryCapacityPath);
props.batteryVoltage = getIntField(mHealthdConfig->batteryVoltagePath);
if(!mHealthdConfig->batteryCurrentNowPath.isEmpty())
props.batteryCurrent = getIntField(mHealthdConfig->batteryCurrentNowPath)/1000;
if(!mHealthdConfig->batteryFullChargePath.isEmpty())
props.batteryFullCharge = getIntField(mHealthdConfig->batteryFullChargePath);
if(!mHealthdConfig->batteryCycleCountPath.isEmpty())
props.batteryCycleCount = getIntField(mHealthdConfig->batteryCycleCountPath);
if(!mHealthdConfig->batteryChargeCounterPath.isEmpty())
props.batteryChargeCounter = getIntField(mHealthdConfig->batteryChargeCounterPath);
props.batteryTemperature = mBatteryFixedTemperature ?
mBatteryFixedTemperature :
getIntField(mHealthdConfig->batteryTemperaturePath);
update_smb();
// For devices which do not have battery and are always plugged
// into power souce.
if(mAlwaysPluggedDevice){
props.chargerAcOnline =true;
props.batteryPresent =true;
props.batteryStatus = BATTERY_STATUS_CHARGING;
props.batteryHealth = BATTERY_HEALTH_GOOD;
}
constint SIZE =128;
char buf[SIZE];
String8 btech;
if(readFromFile(mHealthdConfig->batteryStatusPath, buf, SIZE)>0)
props.batteryStatus = getBatteryStatus(buf);
if(readFromFile(mHealthdConfig->batteryHealthPath, buf, SIZE)>0)
props.batteryHealth = getBatteryHealth(buf);
if(readFromFile(mHealthdConfig->batteryTechnologyPath, buf, SIZE)>0)
props.batteryTechnology =String8(buf);
unsignedint i;
doubleMaxPower=0;
for(i =0; i < mChargerNames.size(); i++){
String8 path;
path.appendFormat("%s/%s/online", POWER_SUPPLY_SYSFS_PATH,
mChargerNames[i].string());
if(readFromFile(path, buf, SIZE)>0){
if(buf[0]!='0'){
path.clear();
path.appendFormat("%s/%s/type", POWER_SUPPLY_SYSFS_PATH,
mChargerNames[i].string());
switch(readPowerSupplyType(path)){
case ANDROID_POWER_SUPPLY_TYPE_AC:
props.chargerAcOnline =true;
break;
case ANDROID_POWER_SUPPLY_TYPE_USB:
props.chargerUsbOnline =true;
break;
case ANDROID_POWER_SUPPLY_TYPE_WIRELESS:
props.chargerWirelessOnline =true;
break;
default:
KLOG_WARNING(LOG_TAG,"%s: Unknown power supply type\n",
mChargerNames[i].string());
}
path.clear();
path.appendFormat("%s/%s/current_max", POWER_SUPPLY_SYSFS_PATH,
mChargerNames[i].string());
intChargingCurrent=
(access(path.string(), R_OK)==0)? getIntField(path):0;
path.clear();
path.appendFormat("%s/%s/voltage_max", POWER_SUPPLY_SYSFS_PATH,
mChargerNames[i].string());
intChargingVoltage=
(access(path.string(), R_OK)==0)? getIntField(path):
DEFAULT_VBUS_VOLTAGE;
double power =((double)ChargingCurrent/ MILLION)*
((double)ChargingVoltage/ MILLION);
if(MaxPower< power){
props.maxChargingCurrent =ChargingCurrent;
props.maxChargingVoltage =ChargingVoltage;
MaxPower= power;
}
}
}
}
logthis =!healthd_board_battery_update(&props);
if(logthis){
char dmesgline[256];
size_t len;
if(props.batteryPresent){
snprintf(dmesgline,sizeof(dmesgline),
"battery l=%d v=%d t=%s%d.%d h=%d st=%d",
props.batteryLevel, props.batteryVoltage,
props.batteryTemperature <0?"-":"",
abs(props.batteryTemperature /10),
abs(props.batteryTemperature %10), props.batteryHealth,
props.batteryStatus);
len = strlen(dmesgline);
if(props.batteryPresent_smb){
snprintf(dmesgline,sizeof(dmesgline),
"battery l2=%d st2=%d ext=%d",
props.batteryLevel_smb,
props.batteryStatus_smb,
props.batteryPresent_smb);
}
if(!mHealthdConfig->batteryCurrentNowPath.isEmpty()){
len += snprintf(dmesgline + len,sizeof(dmesgline)- len,
" c=%d", props.batteryCurrent);
}
if(!mHealthdConfig->batteryFullChargePath.isEmpty()){
len += snprintf(dmesgline + len,sizeof(dmesgline)- len,
" fc=%d", props.batteryFullCharge);
}
if(!mHealthdConfig->batteryCycleCountPath.isEmpty()){
len += snprintf(dmesgline + len,sizeof(dmesgline)- len,
" cc=%d", props.batteryCycleCount);
}
}else{
snprintf(dmesgline,sizeof(dmesgline),
"battery none");
}
len = strlen(dmesgline);
KLOG_WARNING(LOG_TAG,"%s chg=%s%s%s\n", dmesgline,
props.chargerAcOnline ?"a":"",
props.chargerUsbOnline ?"u":"",
props.chargerWirelessOnline ?"w":"");
snprintf(dmesgline + len,sizeof(dmesgline)- len," chg=%s%s%s",
props.chargerAcOnline ?"a":"",
props.chargerUsbOnline ?"u":"",
props.chargerWirelessOnline ?"w":"");
KLOG_WARNING(LOG_TAG,"%s\n", dmesgline);
}
cmd_send();
healthd_mode_ops->battery_update(&props);
return props.chargerAcOnline | props.chargerUsbOnline |
props.chargerWirelessOnline;
}
然后是通过binder把电池信息props传入到上层batteryService中,在该Service中有个BatteryListener对象,该类的定义如下:
privatefinalclassBatteryListenerextendsIBatteryPropertiesListener.Stub{
@Overridepublicvoid batteryPropertiesChanged(BatteryProperties props){
finallong identity =Binder.clearCallingIdentity();
try{
BatteryService.this.update(props);
}finally{
Binder.restoreCallingIdentity(identity);
}
}
}
然后会把传入的BatteryProperties对象props(里面包含的都是电池信息)通过update方法赋值给mLastBatteryProps属性,update方法具体如下:
privatevoid update(BatteryProperties props){
synchronized(mLock){
if(!mUpdatesStopped){
mBatteryProps = props;
if(SystemProperties.get("ro.mtk_ipo_support").equals("1")){
if(mIPOShutdown)
return;
}
// Process the new values.
if(mBootCompleted)
processValuesLocked(false);
}else{
mLastBatteryProps.set(props);
}
}
}
然后看看BatteryProperties类里到底有些什么,具体如下:
publicclassBatteryPropertiesimplementsParcelable{
publicboolean chargerAcOnline;
publicboolean chargerUsbOnline;
publicboolean chargerWirelessOnline;
publicint maxChargingCurrent;
publicint maxChargingVoltage;
publicint batteryStatus;
publicint batteryStatus_smb;
publicint batteryHealth;
publicboolean batteryPresent;
publicboolean batteryPresent_smb;
publicint batteryLevel;
publicint batteryLevel_smb;
publicint batteryVoltage;
publicint batteryTemperature;
publicint batteryCurrentNow;
publicint batteryChargeCounter;
publicint adjustPower;
publicString batteryTechnology;
publicBatteryProperties(){
}
publicvoid set(BatteryProperties other){
chargerAcOnline = other.chargerAcOnline;
chargerUsbOnline = other.chargerUsbOnline;
chargerWirelessOnline = other.chargerWirelessOnline;
maxChargingCurrent = other.maxChargingCurrent;
maxChargingVoltage = other.maxChargingVoltage;
batteryStatus = other.batteryStatus;
batteryHealth = other.batteryHealth;
batteryPresent = other.batteryPresent;
batteryLevel = other.batteryLevel;
batteryVoltage = other.batteryVoltage;
batteryTemperature = other.batteryTemperature;
batteryStatus_smb = other.batteryStatus_smb;
batteryPresent_smb = other.batteryPresent_smb;
batteryLevel_smb = other.batteryLevel_smb;
batteryCurrentNow = other.batteryCurrentNow;
batteryChargeCounter = other.batteryChargeCounter;
adjustPower = other.adjustPower;
batteryTechnology = other.batteryTechnology;
}
/*
* Parcel read/write code must be kept in sync with
* frameworks/native/services/batteryservice/BatteryProperties.cpp
*/
privateBatteryProperties(Parcel p){
chargerAcOnline = p.readInt()==1?true:false;
chargerUsbOnline = p.readInt()==1?true:false;
chargerWirelessOnline = p.readInt()==1?true:false;
maxChargingCurrent = p.readInt();
maxChargingVoltage = p.readInt();
batteryStatus = p.readInt();
batteryHealth = p.readInt();
batteryPresent = p.readInt()==1?true:false;
batteryLevel = p.readInt();
batteryVoltage = p.readInt();
batteryTemperature = p.readInt();
batteryStatus_smb = p.readInt();
batteryPresent_smb = p.readInt()==1?true:false;
batteryLevel_smb = p.readInt();
batteryCurrentNow = p.readInt();
batteryChargeCounter = p.readInt();
adjustPower = p.readInt();
batteryTechnology = p.readString();
}
publicvoid writeToParcel(Parcel p,int flags){
p.writeInt(chargerAcOnline ?1:0);
p.writeInt(chargerUsbOnline ?1:0);
p.writeInt(chargerWirelessOnline ?1:0);
p.writeInt(maxChargingCurrent);
p.writeInt(maxChargingVoltage);
p.writeInt(batteryStatus);
p.writeInt(batteryHealth);
p.writeInt(batteryPresent ?1:0);
p.writeInt(batteryLevel);
p.writeInt(batteryVoltage);
p.writeInt(batteryTemperature);
p.writeInt(batteryStatus_smb);
p.writeInt(batteryPresent_smb ?1:0);
p.writeInt(batteryLevel_smb);
p.writeInt(batteryCurrentNow);
p.writeInt(batteryChargeCounter);
p.writeInt(adjustPower);
p.writeString(batteryTechnology);
}
publicstaticfinalParcelable.Creator<BatteryProperties> CREATOR
=newParcelable.Creator<BatteryProperties>(){
publicBatteryProperties createFromParcel(Parcel p){
returnnewBatteryProperties(p);
}
publicBatteryProperties[] newArray(int size){
returnnewBatteryProperties[size];
}
};
publicint describeContents(){
return0;
}
}
这个类是个工具类,主要用来打包存放电池信息和为各个电池属性进行赋值操作。然后转入batteryService.java文件。在上层处理关机动画的的流程中,主要根据如下逻辑进行操作的。
st=>start:Start
e=>end:End
op1=>operation:BatteryService.java中shutdownIfNoPowerLocked()方法
op2=>operation:ShutdownActivity.java
op3=>operation:PowerManagerService.java
op4=>operation:ShutdownThread.java
op5=>operation:PowerManager.java
st->op1->op2->op3->op4->op5->e
首先看看batteryService类的构造方法,具体代码如下:
publicBatteryService(Context context){
super(context);
mContext = context;
mHandler =newHandler(true/*async*/);
mLed =newLed(context, getLocalService(LightsManager.class));
mSettingsObserver =newSettingsObserver();
mBatteryStats =BatteryStatsService.getService();
mCriticalBatteryLevel = mContext.getResources().getInteger(
com.android.internal.R.integer.config_criticalBatteryWarningLevel);
mLowBatteryWarningLevel = mContext.getResources().getInteger(
com.android.internal.R.integer.config_lowBatteryWarningLevel);
mLowBatteryCloseWarningLevel = mLowBatteryWarningLevel + mContext.getResources().getInteger(
com.android.internal.R.integer.config_lowBatteryCloseWarningBump);
mShutdownBatteryTemperature = mContext.getResources().getInteger(
com.android.internal.R.integer.config_shutdownBatteryTemperature);
// watch for invalid charger messages if the invalid_charger switch exists
if(newFile("/sys/devices/virtual/switch/invalid_charger/state").exists()){
UEventObserver invalidChargerObserver =newUEventObserver(){
@Override
publicvoid onUEvent(UEvent event){
finalint invalidCharger ="1".equals(event.get("SWITCH_STATE"))?1:0;
synchronized(mLock){
if(mInvalidCharger != invalidCharger){
mInvalidCharger = invalidCharger;
}
}
}
};
invalidChargerObserver.startObserving(
"DEVPATH=/devices/virtual/switch/invalid_charger");
}
}
构造方法中会先获得BatteryStatsService的对象,BatteryStatsService主要功能是手机系统中各个模块和进程的耗电情况,通过BatteryStatsService来得到相关数据。然后是系统设定各种低电量报警值:
- mLowBatteryWarningLevel
这个是电池电量不足,系统将自动发出警报 - mLowBatteryCloseWarningLevel
表示停止电量不足警告的值 - mShutdownBatteryTemperature
表示电池温度过高,系统将自动关闭 - mCriticalBatteryLevel
表示电池电量太低了,低于它会自动关机
然后还会创建一个UEventObserver对象,用来监听UEvent事件,主要监听设备插入无效充电器的事件,如果发生该事件,会调用onUEvent方法。
根据控件生命周期该服务会执行onStart()方法,该方法具体代码如下:
publicvoid onStart(){
IBinder b =ServiceManager.getService("batteryproperties");
finalIBatteryPropertiesRegistrar batteryPropertiesRegistrar =
IBatteryPropertiesRegistrar.Stub.asInterface(b);
try{
/*系统注册监听,实时获得底层传来的电池信息*/
batteryPropertiesRegistrar.registerListener(newBatteryListener());
}catch(RemoteException e){
// Should never happen.
}
/*这个if里面的逻辑实际上不会调用*/
if(SystemProperties.get("ro.mtk_ipo_support").equals("1")){
IntentFilter filter =newIntentFilter();
filter.addAction(IPO_POWER_ON);
filter.addAction(IPO_POWER_OFF);
mContext.registerReceiver(newBroadcastReceiver(){
@Override
publicvoid onReceive(Context context,Intent intent){
if(IPO_POWER_ON.equals(intent.getAction())){
mIPOShutdown =false;
mIPOBoot =true;
// Let BatteryService to handle low battery warning.
mLastBatteryLevel = mLowBatteryWarningLevel +1;
update(mBatteryProps);
}else
if(IPO_POWER_OFF.equals(intent.getAction())){
mIPOShutdown =true;
}
}
}, filter);
}
mBinderService =newBinderService();
publishBinderService("battery", mBinderService);
publishLocalService(BatteryManagerInternal.class,newLocalService());
}
该方法中主要是注册一个监听BatteryListener(),接收底层传来的电池信息,并调用 processValuesLocked()方法,BatteryListener类的具体实现前面已经展示过,这里不再累述。接下来看看processValuesLocked方法的具体实现:
privatevoid processValuesLocked(boolean force){
boolean logOutlier =false;
long dischargeDuration =0;
mBatteryLevelCritical =(mBatteryProps.batteryLevel <= mCriticalBatteryLevel);
/*设置手机连接充电设备的方法*/
if(mBatteryProps.chargerAcOnline){
mPlugType =BatteryManager.BATTERY_PLUGGED_AC;
}elseif(mBatteryProps.chargerUsbOnline){
mPlugType =BatteryManager.BATTERY_PLUGGED_USB;
}elseif(mBatteryProps.chargerWirelessOnline){
mPlugType =BatteryManager.BATTERY_PLUGGED_WIRELESS;
}else{
mPlugType = BATTERY_PLUGGED_NONE;
}
/// M: Add for DUAL_INPUT_CHARGER_SUPPORT @{
if(SystemProperties.get("ro.mtk_diso_support").equals("true")){
if(mBatteryProps.chargerAcOnline && mBatteryProps.chargerUsbOnline){
mPlugType =BatteryManager.BATTERY_PLUGGED_AC |BatteryManager.BATTERY_PLUGGED_USB;
}
}
/// M: @}
if(DEBUG){
Slog.d(TAG,"Processing new values: "
+"chargerAcOnline="+ mBatteryProps.chargerAcOnline
+", chargerUsbOnline="+ mBatteryProps.chargerUsbOnline
+", chargerWirelessOnline="+ mBatteryProps.chargerWirelessOnline
+", maxChargingCurrent"+ mBatteryProps.maxChargingCurrent
+", maxChargingVoltage"+ mBatteryProps.maxChargingVoltage
+", chargeCounter"+ mBatteryProps.batteryChargeCounter
+", batteryStatus="+ mBatteryProps.batteryStatus
+", batteryHealth="+ mBatteryProps.batteryHealth
+", batteryPresent="+ mBatteryProps.batteryPresent
+", batteryLevel="+ mBatteryProps.batteryLevel
+", batteryTechnology="+ mBatteryProps.batteryTechnology
+", batteryVoltage="+ mBatteryProps.batteryVoltage
+", batteryTemperature="+ mBatteryProps.batteryTemperature
+", mBatteryLevelCritical="+ mBatteryLevelCritical
+", mPlugType="+ mPlugType);
}
if(mLastBatteryVoltage != mBatteryProps.batteryVoltage){
Log.d(TAG,"mBatteryVoltage="+ mBatteryProps.batteryVoltage +", batteryLevel="+ mBatteryProps.batteryLevel_smb);
}
// Update the battery LED
mLed.updateLightsLocked();
// Let the battery stats keep track of the current level.
try{
mBatteryStats.setBatteryState(mBatteryProps.batteryStatus, mBatteryProps.batteryHealth,
mPlugType, mBatteryProps.batteryLevel, mBatteryProps.batteryTemperature,
mBatteryProps.batteryVoltage, mBatteryProps.batteryChargeCounter);
}catch(RemoteException e){
// Should never happen.
}
/*这里就是判断低电量关机的逻辑*/
shutdownIfNoPowerLocked();
shutdownIfOverTempLocked();
if(force ||(mBatteryProps.batteryStatus != mLastBatteryStatus ||
mBatteryProps.batteryStatus_smb != mLastBatteryStatus_smb ||
mBatteryProps.batteryHealth != mLastBatteryHealth ||
mBatteryProps.batteryPresent != mLastBatteryPresent ||
mBatteryProps.batteryPresent_smb != mLastBatteryPresent_smb ||
mBatteryProps.batteryLevel != mLastBatteryLevel ||
mBatteryProps.batteryLevel_smb != mLastBatteryLevel_smb ||
mPlugType != mLastPlugType ||
mBatteryProps.batteryVoltage != mLastBatteryVoltage ||
mBatteryProps.batteryTemperature != mLastBatteryTemperature ||
mBatteryProps.maxChargingCurrent != mLastMaxChargingCurrent ||
mBatteryProps.maxChargingVoltage != mLastMaxChargingVoltage ||
mBatteryProps.batteryChargeCounter != mLastChargeCounter ||
mInvalidCharger != mLastInvalidCharger)){
if(mPlugType != mLastPlugType){
if(mLastPlugType == BATTERY_PLUGGED_NONE){
// discharging -> charging
// There's no value in this data unless we've discharged at least once and the
// battery level has changed; so don't log until it does.
if(mDischargeStartTime !=0&& mDischargeStartLevel != mBatteryProps.batteryLevel){
dischargeDuration =SystemClock.elapsedRealtime()- mDischargeStartTime;
logOutlier =true;
EventLog.writeEvent(EventLogTags.BATTERY_DISCHARGE, dischargeDuration,
mDischargeStartLevel, mBatteryProps.batteryLevel);
// make sure we see a discharge event before logging again
mDischargeStartTime =0;
}
}elseif(mPlugType == BATTERY_PLUGGED_NONE){
// charging -> discharging or we just powered up
mDischargeStartTime =SystemClock.elapsedRealtime();
mDischargeStartLevel = mBatteryProps.batteryLevel;
}
}
if(mBatteryProps.batteryStatus != mLastBatteryStatus ||
mBatteryProps.batteryHealth != mLastBatteryHealth ||
mBatteryProps.batteryPresent != mLastBatteryPresent ||
mPlugType != mLastPlugType){
EventLog.writeEvent(EventLogTags.BATTERY_STATUS,
mBatteryProps.batteryStatus, mBatteryProps.batteryHealth, mBatteryProps.batteryPresent ?1:0,
mPlugType, mBatteryProps.batteryTechnology);
}
if(mBatteryProps.batteryLevel != mLastBatteryLevel){
// Don't do this just from voltage or temperature changes, that is
// too noisy.
EventLog.writeEvent(EventLogTags.BATTERY_LEVEL,
mBatteryProps.batteryLevel, mBatteryProps.batteryVoltage, mBatteryProps.batteryTemperature);
}
if(mBatteryLevelCritical &&!mLastBatteryLevelCritical &&
mPlugType == BATTERY_PLUGGED_NONE){
// We want to make sure we log discharge cycle outliers
// if the battery is about to die.
dischargeDuration =SystemClock.elapsedRealtime()- mDischargeStartTime;
logOutlier =true;
}
if(!mBatteryLevelLow){
// Should we now switch in to low battery mode?
if(mPlugType == BATTERY_PLUGGED_NONE
&& mBatteryProps.batteryLevel <= mLowBatteryWarningLevel){
mBatteryLevelLow =true;
}
}else{
// Should we now switch out of low battery mode?
if(mPlugType != BATTERY_PLUGGED_NONE){
mBatteryLevelLow =false;
}elseif(mBatteryProps.batteryLevel >= mLowBatteryCloseWarningLevel){
mBatteryLevelLow =false;
}elseif(force && mBatteryProps.batteryLevel >= mLowBatteryWarningLevel){
// If being forced, the previous state doesn't matter, we will just
// absolutely check to see if we are now above the warning level.
mBatteryLevelLow =false;
}
}
sendIntentLocked();
// Separate broadcast is sent for power connected / not connected
// since the standard intent will not wake any applications and some
// applications may want to have smart behavior based on this.
if(mPlugType !=0&& mLastPlugType ==0){
mHandler.post(newRunnable(){
@Override
publicvoid run(){
Intent statusIntent =newIntent(Intent.ACTION_POWER_CONNECTED);
statusIntent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
mContext.sendBroadcastAsUser(statusIntent,UserHandle.ALL);
}
});
}
elseif(mPlugType ==0&& mLastPlugType !=0){
mHandler.post(newRunnable(){
@Override
publicvoid run(){
Intent statusIntent =newIntent(Intent.ACTION_POWER_DISCONNECTED);
statusIntent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
mContext.sendBroadcastAsUser(statusIntent,UserHandle.ALL);
}
});
}
if(shouldSendBatteryLowLocked()){
mSentLowBatteryBroadcast =true;
mHandler.post(newRunnable(){
@Override
publicvoid run(){
Intent statusIntent =newIntent(Intent.ACTION_BATTERY_LOW);
statusIntent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
mContext.sendBroadcastAsUser(statusIntent,UserHandle.ALL);
}
});
}elseif(mSentLowBatteryBroadcast && mLastBatteryLevel >= mLowBatteryCloseWarningLevel){
mSentLowBatteryBroadcast =false;
mHandler.post(newRunnable(){
@Override
publicvoid run(){
Intent statusIntent =newIntent(Intent.ACTION_BATTERY_OKAY);
statusIntent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
mContext.sendBroadcastAsUser(statusIntent,UserHandle.ALL);
}
});
}
if(mBatteryProps.batteryStatus != mLastBatteryStatus &&
mBatteryProps.batteryStatus ==BatteryManager.BATTERY_STATUS_CMD_DISCHARGING){
mHandler.post(newRunnable(){
@Override
publicvoid run(){
finalString ACTION_IGNORE_DATA_USAGE_ALERT =
"android.intent.action.IGNORE_DATA_USAGE_ALERT";
Log.d(TAG,"sendBroadcast ACTION_IGNORE_DATA_USAGE_ALERT");
Intent statusIntent =newIntent(ACTION_IGNORE_DATA_USAGE_ALERT);
statusIntent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
mContext.sendBroadcastAsUser(statusIntent,UserHandle.ALL);
}
});
}
// Update the battery LED
// mLed.updateLightsLocked();
// This needs to be done after sendIntent() so that we get the lastest battery stats.
if(logOutlier && dischargeDuration !=0){
logOutlierLocked(dischargeDuration);
}
mLastBatteryStatus = mBatteryProps.batteryStatus;
mLastBatteryStatus_smb = mBatteryProps.batteryStatus_smb;
mLastBatteryHealth = mBatteryProps.batteryHealth;
mLastBatteryPresent = mBatteryProps.batteryPresent;
mLastBatteryPresent_smb = mBatteryProps.batteryPresent_smb;
mLastBatteryLevel = mBatteryProps.batteryLevel;
mLastBatteryLevel_smb = mBatteryProps.batteryLevel_smb;
mLastPlugType = mPlugType;
mLastBatteryVoltage = mBatteryProps.batteryVoltage;
mLastBatteryTemperature = mBatteryProps.batteryTemperature;
mLastMaxChargingCurrent = mBatteryProps.maxChargingCurrent;
mLastMaxChargingVoltage = mBatteryProps.maxChargingVoltage;
mLastChargeCounter = mBatteryProps.batteryChargeCounter;
mLastBatteryLevelCritical = mBatteryLevelCritical;
mLastInvalidCharger = mInvalidCharger;
}
}
每次有电池信息改变是,都会调用processValuesLocked这个方法,并通过shutdownIfNoPowerLocked();来判断是否进入低电量关机逻辑。然后看看shutdownIfNoPowerLocked的实现:
privatevoid shutdownIfNoPowerLocked(){
// shut down gracefully if our battery is critically low and we are not powered.
// wait until the system has booted before attempting to display the shutdown dialog.
/*即判断如果电池电量为0,并且没有连接充电设备 就进入if逻辑中*/
if(mBatteryProps.batteryLevel ==0&&!isPoweredLocked(BatteryManager.BATTERY_PLUGGED_ANY)){
mHandler.post(newRunnable(){
@Override
publicvoid run(){
if(ActivityManagerNative.isSystemReady()){
if(SystemProperties.get("ro.mtk_ipo_support").equals("1")){
SystemProperties.set("sys.ipo.battlow","1");
}
Intent intent =newIntent(Intent.ACTION_REQUEST_SHUTDOWN);
/*把一些相关信息通过intent,传入目标activity中*/
intent.putExtra(Intent.EXTRA_KEY_CONFIRM,false);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivityAsUser(intent,UserHandle.CURRENT);
}
}
});
}
}
<activityandroid:name="com.android.internal.app.ShutdownActivity"
android:permission="android.permission.SHUTDOWN"
android:theme="@style/Theme.NoDisplay"
android:excludeFromRecents="true">
<intent-filter>
<actionandroid:name="android.intent.action.ACTION_REQUEST_SHUTDOWN"/>
<categoryandroid:name="android.intent.category.DEFAULT"/>
</intent-filter>
<intent-filter>
<actionandroid:name="android.intent.action.REBOOT"/>
<categoryandroid:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
然后查看ShutdownActivity.java的具体实现
publicclassShutdownActivityextendsActivity{
privatestaticfinalString TAG ="ShutdownActivity";
privateboolean mReboot;
privateboolean mConfirm;
privateboolean mUserRequested;
@Override
protectedvoid onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
Intent intent = getIntent();
mReboot =Intent.ACTION_REBOOT.equals(intent.getAction());
mConfirm = intent.getBooleanExtra(Intent.EXTRA_KEY_CONFIRM,false);
mUserRequested = intent.getBooleanExtra(Intent.EXTRA_USER_REQUESTED_SHUTDOWN,false);
Slog.i(TAG,"onCreate(): confirm="+ mConfirm);
Thread thr =newThread("ShutdownActivity"){
@Override
publicvoid run(){
IPowerManager pm =IPowerManager.Stub.asInterface(
ServiceManager.getService(Context.POWER_SERVICE));
try{
if(mReboot){
pm.reboot(mConfirm,null,false);
}else{
pm.shutdown(mConfirm,
mUserRequested ?PowerManager.SHUTDOWN_USER_REQUESTED :null,
false);
}
}catch(RemoteException e){
}
}
};
thr.start();
finish();
// Wait for us to tell the power manager to shutdown.
try{
thr.join();
}catch(InterruptedException e){
}
}
}
在该activity中,首先会得到intent中的附件信息,然后创建一个线程,在线程中获得一个IPowerManager对象,并调用它的相关方法。然后转到PowerManagerService.java,PowerManagerService继承了SystemService,它的shutdown方法具体实现如下:
publicvoid shutdown(boolean confirm,String reason,boolean wait){ mContext.enforceCallingOrSelfPermission(android.Manifest.permission.REBOOT,null);
finallong ident =Binder.clearCallingIdentity();
try{
shutdownOrRebootInternal(HALT_MODE_SHUTDOWN, confirm, reason, wait);
}finally{
Binder.restoreCallingIdentity(ident);
}
}
privatevoid shutdownOrRebootInternal(final@HaltModeint haltMode,finalboolean confirm,
finalString reason,boolean wait){
if(mHandler ==null||!mSystemReady){
thrownewIllegalStateException("Too early to call shutdown() or reboot()");
}
/*创建一个线程来做关机出来*/
Runnable runnable =newRunnable(){
@Override
publicvoid run(){
synchronized(this){
/*选择重启或关机*/
if(haltMode == HALT_MODE_REBOOT_SAFE_MODE){
ShutdownThread.rebootSafeMode(mContext, confirm);
}elseif(haltMode == HALT_MODE_REBOOT){
ShutdownThread.reboot(mContext, reason, confirm);
}else{
ShutdownThread.shutdown(mContext, reason, confirm);
}
}
}
};
// ShutdownThread must run on a looper capable of displaying the UI.
Message msg =Message.obtain(mHandler, runnable);
msg.setAsynchronous(true);
/*把线程对象通过Message发出去??*/
mHandler.sendMessage(msg);
// PowerManager.reboot() is documented not to return so just wait for the inevitable.
if(wait){
synchronized(runnable){
while(true){
try{
runnable.wait();
}catch(InterruptedException e){
}
}
}
}
}
然后查看ShutdownThread.java的shutdown方法,其实现如下:
publicstaticvoid shutdown(finalContext context,String reason,boolean confirm){
mReboot =false;
mRebootSafeMode =false;
mReason = reason;
Log.d(TAG,"!!! Request to shutdown !!!");
if(mSpew){
StackTraceElement[] stack =newThrowable().getStackTrace();
for(StackTraceElement element : stack)
{
Log.d(TAG," |----"+ element.toString());
}
}
if(SystemProperties.getBoolean("ro.monkey",false)){
Log.d(TAG,"Cannot request to shutdown when Monkey is running, returning.");
return;
}
shutdownInner(context, confirm);
}
staticvoid shutdownInner(finalContext context,boolean confirm){
// ensure that only one thread is trying to power down.
// any additional calls are just returned
synchronized(sIsStartedGuard){
if(sIsStarted){
Log.d(TAG,"Request to shutdown already running, returning.");
return;
}
}
bConfirmForAnimation = confirm;
finalint longPressBehavior = context.getResources().getInteger(
com.android.internal.R.integer.config_longPressOnPowerBehavior);
finalint resourceId = mRebootSafeMode
? com.android.internal.R.string.reboot_safemode_confirm
:(mReboot
? com.android.internal.R.string.reboot_confirm
: com.android.internal.R.string.shutdown_confirm);
Log.d(TAG,"Notifying thread to start shutdown longPressBehavior="+ longPressBehavior);
/*低电量关机流程时,confirm==false,因此走else分支*/
if(confirm){
finalCloseDialogReceiver closer =newCloseDialogReceiver(context);
if(sConfirmDialog !=null){
sConfirmDialog.dismiss();
}
bConfirmForAnimation = confirm;
Log.d(TAG,"PowerOff dialog doesn't exist. Create it first");
sConfirmDialog =newAlertDialog.Builder(context)
.setTitle(mRebootSafeMode
? com.android.internal.R.string.reboot_safemode_title
:(mReboot
? com.android.internal.R.string.reboot_title
: com.android.internal.R.string.power_off))
/*OLD : com.android.internal.R.string.power_off)*/
.setMessage(resourceId)
.setPositiveButton(mReboot
? com.android.internal.R.string.yes
: com.android.internal.R.string.power_off_confirm_bt,
newDialogInterface.OnClickListener(){
publicvoid onClick(DialogInterface dialog,int which){
beginShutdownSequence(context);
}
})
.setNegativeButton(com.android.internal.R.string.no,null)
.create();
closer.dialog = sConfirmDialog;
sConfirmDialog.setOnDismissListener(closer);
sConfirmDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
sConfirmDialog.show();
}else{/*进入低电量关机流程*/
beginShutdownSequence(context);
}
}
privatestaticvoid beginShutdownSequence(Context context){
Intent newIntent =newIntent("begin.Shutdown.Sequence");
context.sendBroadcast(newIntent);
synchronized(sIsStartedGuard){
if(sIsStarted){
Log.d(TAG,"Shutdown sequence already running, returning.");
return;
}
sIsStarted =true;
}
// Throw up a system dialog to indicate the device is rebooting / shutting down.
/*此处应该是创建Dialog,如正常关机前 会让用户选择是重启 ?关机? 飞行模式?等等 ,不过低电量关机 和这个Dialog 没什么关系*/
ProgressDialog pd =newProgressDialog(context);
// Path 1: Reboot to recovery for update
// Condition: mReason == REBOOT_RECOVERY_UPDATE
//
// Path 1a: uncrypt needed
// Condition: if /cache/recovery/uncrypt_file exists but
// /cache/recovery/block.map doesn't.
// UI: determinate progress bar (mRebootHasProgressBar == True)
//
// * Path 1a is expected to be removed once the GmsCore shipped on
// device always calls uncrypt prior to reboot.
//
// Path 1b: uncrypt already done
// UI: spinning circle only (no progress bar)
//
// Path 2: Reboot to recovery for factory reset
// Condition: mReason == REBOOT_RECOVERY
// UI: spinning circle only (no progress bar)
//
// Path 3: Regular reboot / shutdown
// Condition: Otherwise
// UI: spinning circle only (no progress bar)
/*又是一大拨if 判断 */
if(PowerManager.REBOOT_RECOVERY_UPDATE.equals(mReason)){
// We need the progress bar if uncrypt will be invoked during the
// reboot, which might be time-consuming.
mRebootHasProgressBar =RecoverySystem.UNCRYPT_PACKAGE_FILE.exists()
&&!(RecoverySystem.BLOCK_MAP_FILE.exists());
pd.setTitle(context.getText(com.android.internal.R.string.reboot_to_update_title));
if(mRebootHasProgressBar){
pd.setMax(100);
pd.setProgress(0);
pd.setIndeterminate(false);
pd.setProgressNumberFormat(null);
pd.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
pd.setMessage(context.getText(
com.android.internal.R.string.reboot_to_update_prepare));
pd.setCancelable(false);
pd.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
pd.show();
sInstance.mProgressDialog = pd;
}else{
pd.setIndeterminate(true);
pd.setMessage(context.getText(
com.android.internal.R.string.reboot_to_update_reboot));
}
}elseif(PowerManager.REBOOT_RECOVERY.equals(mReason)){
// Factory reset path. Set the dialog message accordingly.
pd.setTitle(context.getText(com.android.internal.R.string.reboot_to_reset_title));
pd.setMessage(context.getText(
com.android.internal.R.string.reboot_to_reset_message));
pd.setIndeterminate(true);
}else{
pd.setTitle(context.getText(com.android.internal.R.string.power_off));
pd.setMessage(context.getText(com.android.internal.R.string.shutdown_progress));
pd.setIndeterminate(true);
}
pd.setCancelable(false);
pd.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
// start the thread that initiates shutdown
sInstance.mContext = context;
sInstance.mPowerManager =(PowerManager)context.getSystemService(Context.POWER_SERVICE);
sInstance.mHandler =newHandler(){
};
beginAnimationTime =0;
/*关机动画客制化*/
boolean mShutOffAnimation = configShutdownAnimation(context);
/*即获得关机时 动画的播放时间???*/
int screenTurnOffTime = getScreenTurnOffTime(context);
synchronized(mEnableAnimatingSync){
if(mEnableAnimating){
if(mRebootHasProgressBar){
pd.show();
screenTurnOffTime += MAX_UNCRYPT_WAIT_TIME;
sInstance.mProgressDialog = pd;
}else{
/*低电量关机会走这个分支*/
Log.d(TAG,"mIBootAnim.isCustBootAnim() is true");
/*这里调用 bootanimCust方法来播放关机动画,实际上是开启一个播放关机动画的服务*/
bootanimCust(context);
}
sInstance.mHandler.postDelayed(mDelayDim, screenTurnOffTime);
}
}
// make sure we never fall asleep again
sInstance.mCpuWakeLock =null;
try{
sInstance.mCpuWakeLock = sInstance.mPowerManager.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, TAG +"-cpu");
sInstance.mCpuWakeLock.setReferenceCounted(false);
sInstance.mCpuWakeLock.acquire();
}catch(SecurityException e){
Log.w(TAG,"No permission to acquire wake lock", e);
sInstance.mCpuWakeLock =null;
}
// also make sure the screen stays on for better user experience
sInstance.mScreenWakeLock =null;
if(sInstance.mPowerManager.isScreenOn()){
try{
sInstance.mScreenWakeLock = sInstance.mPowerManager.newWakeLock(
PowerManager.FULL_WAKE_LOCK, TAG +"-screen");
sInstance.mScreenWakeLock.setReferenceCounted(false);
sInstance.mScreenWakeLock.acquire();
}catch(SecurityException e){
Log.w(TAG,"No permission to acquire wake lock", e);
sInstance.mScreenWakeLock =null;
}
}
// also make sure the screen stays on for better user experience
//sInstance.mScreenWakeLock = null;
if(sInstance.mPowerManager.isScreenOn()){
// if Screen old state is on, keep it on
if(sInstance.mScreenWakeLock ==null){
Log.d(TAG,"Screen old state is on, keep screen on !");
try{
sInstance.mScreenWakeLock = sInstance.mPowerManager.newWakeLock(
PowerManager.FULL_WAKE_LOCK, TAG +"-screen");
sInstance.mScreenWakeLock.setReferenceCounted(false);
sInstance.mScreenWakeLock.acquire();
}catch(SecurityException e){
Log.w(TAG,"No permission to acquire wake lock", e);
sInstance.mScreenWakeLock =null;
}
}else{
Log.d(TAG,"Screen is already wake up !");
}
}else{
// WARING: if system is in low power level, pls charge your phone immediately
Log.d(TAG,"First time wake up Screen failed, try to wake up Screen second time !");
wakeUpScreen();
}
if(sInstance.getState()!=Thread.State.NEW || sInstance.isAlive()){
if(sInstance.mShutdownFlow == IPO_SHUTDOWN_FLOW){
Log.d(TAG,"ShutdownThread exists already");
checkShutdownFlow();
synchronized(mShutdownThreadSync){
mShutdownThreadSync.notify();
}
}else{
Log.e(TAG,"Thread state is not normal! froce to shutdown!");
delayForPlayAnimation();
//unmout data/cache partitions while performing shutdown
sInstance.mPowerManager.goToSleep(SystemClock.uptimeMillis(),
PowerManager.GO_TO_SLEEP_REASON_SHUTDOWN,0);
PowerManagerService.lowLevelShutdown(mReason);
}
}else{
/*开启线程 关闭系统的所有服务*/
sInstance.start();
}
}
beginShutdownSequence主要处理关机过程中的一系列流程,通过调用bootanimCust(context)来开机播放关机动画的服务,然后会计算关机动画时间并保证这个时间中屏幕不会熄灭,同时开启一个sInstance线程来关闭系统中的服务。bootanimCust方法现象代码如下:
privatestaticvoid bootanimCust(Context context){
// [MTK] fix shutdown animation timing issue
//==================================================================
SystemProperties.set("service.shutanim.running","0");
Log.i(TAG,"set service.shutanim.running to 0");
//==================================================================
boolean isRotaionEnabled =false;
try{
isRotaionEnabled =Settings.System.getInt(context.getContentResolver(),
Settings.System.ACCELEROMETER_ROTATION,1)!=0;
if(isRotaionEnabled){
finalIWindowManager wm =IWindowManager.Stub.asInterface(
ServiceManager.getService(Context.WINDOW_SERVICE));
if(wm !=null){
wm.freezeRotation(Surface.ROTATION_0);
}
Settings.System.putInt(context.getContentResolver(),
Settings.System.ACCELEROMETER_ROTATION,0);
Settings.System.putInt(context.getContentResolver(),
Settings.System.ACCELEROMETER_ROTATION_RESTORE,1);
}
}catch(NullPointerException ex){
Log.e(TAG,"check Rotation: context object is null when get Rotation");
}catch(RemoteException e){
e.printStackTrace();
}
beginAnimationTime =SystemClock.elapsedRealtime()+ MIN_SHUTDOWN_ANIMATION_PLAY_TIME;
try{
finalIWindowManager wm =IWindowManager.Stub.asInterface(
ServiceManager.getService(Context.WINDOW_SERVICE));
if(wm !=null){
wm.setEventDispatching(false);
}
}catch(RemoteException e){
e.printStackTrace();
}
startBootAnimation();
}
privatestaticvoid startBootAnimation(){
Log.d(TAG,"Set 'service.bootanim.exit' = 0).");
SystemProperties.set("service.bootanim.exit","0");
if(bPlayaudio){
/*在文件ServiceManager.cpp中有如下注释
- Starting a service is done by writing its name to the "ctl.start" system property. This triggers the init daemon to actually start
the service for us.
大概意思就是向ctl.start属性中写入某个服务名,触发器会开启该服务吧(英文不好 反正大概知道是会开启某个服务的意思)
Stopping the service is done by writing its name to "ctl.stop"
in a similar way
低电量关机进入if逻辑中 此处会开启一个服务来播放关机动画*/
SystemProperties.set("ctl.start","banim_shutmp3");
Log.d(TAG,"bootanim:shut mp3");
}else{
//SystemProperties.set("ctl.start", "bootanim:shut nomp3");
SystemProperties.set("ctl.start","banim_shutnomp3");
Log.d(TAG,"bootanim:shut nomp3");
}
}
然后SystemProperties.set("ctl.start", "banim_shutmp3");
到底会开启什么服务呢?通过文件frameworks/base/cmds/bootanimation/bootanim.rc可以看到如下代码:
service bootanim /system/bin/bootanimation
class core
user graphics
group graphics audio
disabled
oneshot
# MTK add
service banim_shutmp3 /system/bin/bootanimation shut mp3
class core
user graphics
group graphics audio
disabled
oneshot
service banim_shutnomp3 /system/bin/bootanimation shut nomp3
class core
user graphics
group graphics audio
disabled
oneshot
所以会开启banim_shutmp3这个服务器,该服务的可执行文件为/system/bin/bootanimation,这个服务就是播放关机动画的服务了,该服务的实现代码在frameworks/base/cmds/bootanimation/目录下。
首先查看下frameworks/base/cmds/bootanimation/bootanimation_main.cpp具体代码:
int main(int argc,char** argv)
{
setpriority(PRIO_PROCESS,0, ANDROID_PRIORITY_DISPLAY);
char value[PROPERTY_VALUE_MAX];
/*获得debug.sf.nobootanimation的值 存入value属性中 如果为1 不会进入下面的if中即不会播放动画*/
property_get("debug.sf.nobootanimation", value,"0");
int noBootAnimation = atoi(value);
ALOGI_IF(noBootAnimation,"boot animation disabled");
if(!noBootAnimation){
sp<ProcessState> proc(ProcessState::self());
ProcessState::self()->startThreadPool();
/* M: use our construction method, because we support shut down animation @{
// create the boot animation object
sp<BootAnimation> boot = new BootAnimation();
@} */
bool setBoot =true;
bool setRotated =false;
bool sePaly =true;
/*因为传入了2个参数shut和mp3 */
if(argc >1){
if(!strcmp(argv[1],"shut"))
/*会进入该逻辑*/
setBoot =false;
}
if(argc >2){
if(!strcmp(argv[2],"nomp3"))
sePaly =false;
}
if(argc >3){
if(!strcmp(argv[3],"rotate"))
setRotated =true;
}
ALOGD("[BootAnimation %s %d]setBoot=%d,sePaly=%d,setRotated=%d",__FUNCTION__,__LINE__,setBoot,sePaly,setRotated);
char volume[PROPERTY_VALUE_MAX];
intLoopCounter=60;
int nVolume =-1;
while(LoopCounter-->0&&(nVolume ==-1)){
property_get("persist.sys.mute.state", volume,"-1");
nVolume = atoi(volume);
ALOGD("BootAnimation wait for sample ready, sleep 100ms");
usleep(100*1000);
}
ALOGD("[BootAnimation %s %d]nVolume=%d",__FUNCTION__,__LINE__,nVolume);
if(nVolume ==0|| nVolume ==1){
sePaly =false;
}
ALOGD("before new BootAnimation...");
ALOGD("[BootAnimation %s %d]before new BootAnimation...",__FUNCTION__,__LINE__);
/*创建一个BootAnimation对象 其中 参数值setBoot=0,sePaly=1,setRotated=0*/
sp<BootAnimation> boot =newBootAnimation(setBoot,sePaly,setRotated);
ALOGD("joinThreadPool...");
ALOGD("[BootAnimation %s %d]before joinThreadPool...",__FUNCTION__,__LINE__);
IPCThreadState::self()->joinThreadPool();
}
ALOGD("[BootAnimation %s %s %d]end",__FILE__,__FUNCTION__,__LINE__);
return0;
}
然后看看BootAnimation类,该类的声明如下:
classBootAnimation:publicThread,publicIBinder::DeathRecipient
{
public:
BootAnimation();
BootAnimation(bool bSetBootOrShutDown,bool bSetPlayMP3,bool bSetRotated);
virtual~BootAnimation();
void setBootVideoPlayState(int playState);
sp<SurfaceComposerClient> session()const;
private:
virtualbool threadLoop();
virtualstatus_t readyToRun();
virtualvoid onFirstRef();
virtualvoid binderDied(const wp<IBinder>& who);
structTexture{
GLint w;
GLint h;
GLuint name;
};
structAnimation{
structFrame{
String8 name;
FileMap* map;
String8 fullPath;
mutableGLuint tid;
booloperator<(constFrame& rhs)const{
return name < rhs.name;
}
};
structPart{
int count;// The number of times this part should repeat, 0 for infinite
int pause;// The number of frames to pause for at the end of this part
int clockPosY;// The y position of the clock, in pixels, from the bottom of the
// display (the clock is centred horizontally). -1 to disable the clock
String8 path;
SortedVector<Frame> frames;
bool playUntilComplete;
float backgroundColor[3];
FileMap* audioFile;
Animation* animation;
};
int fps;
int width;
int height;
Vector<Part> parts;
String8 audioConf;
String8 fileName;
ZipFileRO* zip;
};
status_t initTexture(Texture* texture,AssetManager& asset,constchar* name);
status_t initTexture(constAnimation::Frame& frame);
bool android();
bool movie();
status_t initTexture(constSkBitmap* bitmap);
bool gifMovie();
void drawTime(constTexture& clockTex,constint yPos);
Animation* loadAnimation(constString8&);
bool playAnimation(constAnimation&);
void releaseAnimation(Animation*)const;
bool parseAnimationDesc(Animation&);
bool preloadZip(Animation&animation);
void checkExit();
sp<SurfaceComposerClient> mSession;
sp<AudioPlayer> mAudioPlayer;
AssetManager mAssets;
Texture mAndroid[2];
Texture mClock;
int mWidth;
int mHeight;
EGLDisplay mDisplay;
EGLDisplay mContext;
EGLDisplay mSurface;
sp<SurfaceControl> mFlingerSurfaceControl;
sp<Surface> mFlingerSurface;
bool mClockEnabled;
String8 mZipFileName;
SortedVector<String8> mLoadedFiles;
ZipFileRO*mZip;
status_t initTexture(constchar*EntryName);
void initBootanimationZip(char* naimationname);
void initShutanimationZip(char* naimationname);
constchar* initAudioPath();
bool ETC1movie();
void initShader();
GLuint buildShader(constchar* source,GLenum shaderType);
GLuint buildProgram (constchar* vertexShaderSource,constchar* fragmentShaderSource);
bool bBootOrShutDown;
bool bShutRotate;
bool bPlayMP3;
GLuint mProgram;
GLint mAttribPosition;
GLint mAttribTexCoord;
GLint mUniformTexture;
bool bETC1Movie;
int mBootVideoPlayType;
int mBootVideoPlayState;
SkMovie* mSkMovie;
Texture mJRD;
int mMcc;
int mMnc;
int mSpn;
bool isTelefonica;
char mMccMnc[PROPERTY_VALUE_MAX];
char mOperator[PROPERTY_VALUE_MAX];
bool mEnableSsv;
bool mIsValidMccMnc;
char mSsvAnimationPath[PROPERTY_VALUE_MAX];
void getSsvAnimationPath(char* ssvAnimationPath,constchar* mccmnc,constchar* animationName);
void initSsv();
};
3. solution
通过查看关机log,发现电量值为0的时候,方法shutdownIfNoPowerLocked()通过binder接受到从底层传入的电池电量值并不为0,即条件判断if (mBatteryProps.batteryLevel == 0 && !isPoweredLocked(BatteryManager.BATTERY_PLUGGED_ANY))
为false。所以后面的播放关机动画的逻辑是不会走的,而电池的实际电量值已经为0了,所以才会没有播放关机动画就黑屏关机。
既然找到了问题,所以查看为什么在电池电量值为0的时候,上层收到mBatteryProps.batteryLevel属性并不为0呢。在文件battery_common_fg_20.c中函数mt_battery_update_EM实现如下:
staticvoid mt_battery_update_EM(struct battery_data *bat_data)
{
if((BMT_status.UI_SOC2 <=0)&&(BMT_status.bat_vol >= SYSTEM_OFF_VOLTAGE ))
bat_data->BAT_CAPACITY =1;
else
bat_data->BAT_CAPACITY = BMT_status.UI_SOC2;
bat_data->BAT_TemperatureR = BMT_status.temperatureR;/* API */
bat_data->BAT_TempBattVoltage = BMT_status.temperatureV;/* API */
bat_data->BAT_InstatVolt = BMT_status.bat_vol;/* VBAT */
bat_data->BAT_BatteryAverageCurrent = BMT_status.ICharging;
bat_data->BAT_BatterySenseVoltage = BMT_status.bat_vol;
bat_data->BAT_ISenseVoltage = BMT_status.Vsense;/* API */
bat_data->BAT_ChargerVoltage = BMT_status.charger_vol;
bat_data->BAT_batt_id = BMT_status.id_vol;
/* Dual battery */
bat_data->status_smb = g_status_smb;
bat_data->capacity_smb = g_capacity_smb;
bat_data->present_smb = g_present_smb;
battery_log(BAT_LOG_FULL,"status_smb = %d, capacity_smb = %d, present_smb = %d\n",
bat_data->status_smb, bat_data->capacity_smb, bat_data->present_smb);
if((BMT_status.UI_SOC2 ==100)&&(BMT_status.charger_exist == KAL_TRUE)
&&(BMT_status.bat_charging_state != CHR_ERROR))
bat_data->BAT_STATUS = POWER_SUPPLY_STATUS_FULL;
#ifdef CONFIG_MTK_DISABLE_POWER_ON_OFF_VOLTAGE_LIMITATION
if(bat_data->BAT_CAPACITY <=0)
bat_data->BAT_CAPACITY =1;
battery_log(BAT_LOG_CRTI,
"BAT_CAPACITY=1, due to define CONFIG_MTK_DISABLE_POWER_ON_OFF_VOLTAGE_LIMITATION\r\n");
#endif
}
在该函数中就是将电池驱动的信息写入对应节点的操作,属性BMT_status.UI_SOC2
就是对应电池的实际电量值。在该if条件语句中,首先判断实际电量值是否低于或等于0,如果是ture就继续判断电池电压值BMT_status.bat_vol是否高于设定关机电压,如果电量值为0但是电压值还是过高(高于SYSTEM_OFF_VOLTAGE),会将bat_data->BAT_CAPACITY设定为1,即不进行关机操作,会让实际电压值继续降一段时间。等到同时满足BMT_status.UI_SOC2 =0
而且电压值BMT_status.bat_vol
低于设定的关机电压时,进入else逻辑中,即让属性bat_data->BAT_CAPACITY = BMT_status.UI_SOC2;
即bat_data->BAT_CAPACITY置为0,然后将bat_data->BAT_CAPACITY值写入节点/sys/class/power_supply/battery/capacity。然后BatteryMonitor.cpp会读取该节点,然后将读到的电量值props.batteryLevel传入上层。
导致本问题出现的原因是设置的关机电压值BMT_status.bat_vol
过低,在达到低电量关机条件的时候BMT_status.UI_SOC2 <=0)&&(BMT_status.bat_vol >= SYSTEM_OFF_VOLTAGE
依然为true,所以传到上层的bat_data->BAT_CAPACITY不为0,无播放关机动画。
最后需要修改battery_common.h文件中的宏定义#define SYSTEM_OFF_VOLTAGE
,把它的值调整的高一点即可,具体修改到多少,可根据具体情况而定。
4. 总结
这个问题解决,首先需要了解电池电量值的读取、传递的逻辑,手机中的电量值会通过底层电池驱动把电池信息写入对应的节点中,然后通过BatteryMonitor.cpp来读取相关信息,然后通过binder机制将电池信息不间断的传到上层,然后上层通过获得的电池信息值来做出相关操作,如电量低时通过广播来发送警告,如果电池电量值非常低时,进行关机操作。同时在关机操作前,还会开机线程来对系统中的所有服务进行关闭操作。至于关机动画则是通过开启一个播放关机动画的服务来处理。通过关机log来定位问题,发现在电量值为0的时候底层通过binder传入到上层的电量值不是0,所以不会进入上层播放关机动画的逻辑中来,而底层检测到电量低,则直接黑屏关机处理了。
No shutdown animation in the electricity display only 1%的更多相关文章
- 百度前端技术学院2018笔记 之 利用 CSS animation 制作一个炫酷的 Slider
前言 题目地址 利用 CSS animation 制作一个炫酷的 Slider 思路整理 首先页面包含三种东西 一个是type为radio的input其实就是单选框 二是每个单选框对应的label 三 ...
- Android 开机动画源码分析
Android系统在启动SystemServer进程时,通过两个阶段来启动系统所有服务,在第一阶段启动本地服务,如SurfaceFlinger,SensorService等,在第二阶段则启动一系列的J ...
- Android 开机画面和wallpaper总结
Android 开机画面和wallpaper总结 1 kernel的开机画面修改 1.图片需求:图片格式:png图片大小:1024x600(具体示lcd分辨率而定). 2.转换图片png图片. 假设 ...
- [原创]webapp/css3实战,制作一个《炉石传说》宣传页
在移动网页,尤其是webapp中常需要用到大量的css3动画,来获得良好交互体验 我之前帮朋友做了一个,可惜没帮上忙现在和大家分享一下 目标是要做一个<炉石传说>游戏的介绍宣传页面,文字内 ...
- Java性能提示(全)
http://www.onjava.com/pub/a/onjava/2001/05/30/optimization.htmlComparing the performance of LinkedLi ...
- 一. Linux 常用命令总结
1. linux 基础命令 who, which, basename, dirname, echo, type, hash, whatis, makewhatis, man, info, help, ...
- vue2.0 transition -- demo实践填坑
前言 vue1.0版本和2.0版本的过渡系统改变还是蛮彻底的,具体请自行详看文档介绍:https://vuefe.cn/v2/guide/migration.html#过渡.在使用2.0版本做过渡效果 ...
- Android 系统默认参数的修改
转自: http://www.th7.cn/Program/Android/201505/447097.shtml 写在前面的话 一般在新项目开始之初,我们需要针对客户需求进行各种系统默认属性的配置, ...
- 绝对炫的幻灯片插件-SKITTER
绝对炫的幻灯片插件-SKITTER 所属分类:媒体-幻灯片和轮播图,图片展示,滑块和旋转 Includes code source // Styles <link href="css/ ...
随机推荐
- 丈量你的代码,从cloc开始
如果我想统计我当前的项目有多少代码量?行数最高的代码文件有哪些?并且排除某些目录,怎么统计?要统计出注释多少行,和代码多少行?使用cloc就行. cloc是一款使用Perl语言开发的开源代码统计工具, ...
- mapbox获取各种经纬度
点击地图即可获取经纬度,也可以手动输入经纬度来换算 在线查看运行效果 实现方法 mapbox中通过地图点击事件来获取到坐标,然后转换为其他的坐标系并输出在屏幕上即可 获取坐标 方法很简单,给地图添加一 ...
- 测试开发实战[提测平台]17-Flask&Vue文件上传实现
微信搜索[大奇测试开],关注这个坚持分享测试开发干货的家伙. 先回顾下在此系列第8次分享给出的预期实现的产品原型和需求说明,如下图整体上和前两节实现很相似,只不过一般测试报告要写的内容可能比较多,就多 ...
- Spring Boot发布2.6.2、2.5.8:升级log4j2到2.17.0
12月22日,Spring官方发布了Spring Boot 2.5.8(包括46个错误修复.文档改进和依赖项升级)和2.6.2(包括55个错误修复.文档改进和依赖项升级). 这两个版本均为缺陷修复版本 ...
- 分布式文件系统fastdfs安装以及python调用
fastfds的安装和使用 一.所需依赖 操作系统:centos7.x(注意的是centos使用yum安装相关依赖) fastdfs:V6.06.tar.gz libfastcommon:V1.0.4 ...
- Python 属性方法、类方法、静态方法、 特殊属性__doc__ (内建属性)
总结:和类的关联性讲:属性方法>类方法>静态方法 属性方法@property:仅仅是调用方式不用+括号. 类方法@classmethod:访问不了累的属性变量,只可以访问类变量. 静态方法 ...
- Linux C++获取磁盘剩余空间和可用空间
完整源码 #include <sys/statfs.h> #include <string> #include <iostream> #include <li ...
- google protobuf学习笔记:windows下环境配置
欢迎转载,转载请注明原文地址:http://blog.csdn.net/majianfei1023/article/details/45371743 protobuf的使用和原理,请查看:http:/ ...
- 【LeetCode】87. Scramble String 解题报告(Python & C++)
作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 递归 动态规划 日期 题目地址:https://le ...
- 【LeetCode】526. Beautiful Arrangement 解题报告(Python & C++)
作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 日期 题目地址:https://leetcode.c ...