Android 呼吸灯控制
android中灯光除闪光灯外是由Lights统一控制的,使用adb shell dumpsys lights我们可以看到当前灯光的一些状态
Service: aidl (android.hardware.light.ILights$Stub$Proxy@33dae0f)
Lights:
Light id=0 ordinal=0 color=ff2c2c2c
Light id=1 ordinal=0 color=00000000
Light id=2 ordinal=0 color=00000000
Light id=3 ordinal=0 color=ff00ff00
Light id=4 ordinal=0 color=00000000
Light id=5 ordinal=0 color=00000000
Session clients:
id:灯的id
ordinal:未发现实际用途,0代表aidl 1代表hidl
color:当前的灯所要显示的颜色,16进制,两位两位分别是A,R,G,B
我们可以看到中共有6的Light的对象,这个6个灯分别为
public static final int LIGHT_ID_BACKLIGHT = Type.BACKLIGHT;
public static final int LIGHT_ID_KEYBOARD = Type.KEYBOARD;
public static final int LIGHT_ID_BUTTONS = Type.BUTTONS;
public static final int LIGHT_ID_BATTERY = Type.BATTERY;
public static final int LIGHT_ID_NOTIFICATIONS = Type.NOTIFICATIONS;
public static final int LIGHT_ID_ATTENTION = Type.ATTENTION;
背光灯,键盘灯,按钮灯,电源灯,通知灯,警示灯,
根据实际情况可能存在蓝牙灯,wifi灯,麦克风灯等,并且灯是可以复用的
一般常见的android机器其实只会用到其中的两个灯,以上面为例
- 背光灯,控制屏幕显示亮度,对应id=0,ff2c2c2c,背光灯只有亮度,没有颜色,所以一般RGB都是一样的。转化为10进制2c=44,通过adb shell settings get system screen_brightness获取到的亮度也是44,一般这是对应的,但也有一些定制情况出现,比如有的项目最大亮度是2047,有的在触发阳光屏逻辑时实际最大亮度能达到4095的,这个需要看具体的项目
- 呼吸灯,经常是电源指示灯和通知灯复用的,id=3或者4,目前带这个灯的项目不多,ff00ff00 表示绿灯,满电状态
电源状态对应的指示灯颜色一般是定义好的,在frameworks/base/core/res/res/values/config.xml下
<!-- Default value for led color when battery is low on charge -->
<integer name="config_notificationsBatteryLowARGB">0xFFFF0000</integer>
<!-- Default value for led color when battery is medium charged -->
<integer name="config_notificationsBatteryMediumARGB">0xFFFFFF00</integer>
<!-- Default value for led color when battery is fully charged -->
<integer name="config_notificationsBatteryFullARGB">0xFF00FF00</integer>
系统是通过LightsService来控制灯的,我们看他的构造方法
public LightsService(Context context) {
this(context, new VintfHalCache(), Looper.myLooper());
}
LightsService(Context context, Supplier<ILights> service, Looper looper) {
super(context);
mH = new Handler(looper);
mVintfLights = service.get() != null ? service : null;
populateAvailableLights(context);
mManagerService = new LightsManagerBinderService();
}
VintfHalCache是他的内部类,通过Binder和底层的服务通信
IBinder binder = Binder.allowBlocking(ServiceManager.waitForDeclaredService(
"android.hardware.light.ILights/default"));
mInstance = ILights.Stub.asInterface(binder);
通过populateAvailableLights方法将底层的light对象,转化成上层对象,方便之后调用,根据实现方式有区分aidl和hidl,比如本例中使用的就是aidl,可以从dumpsys中看出来
private void populateAvailableLightsFromAidl(Context context) {
try {
for (HwLight hwLight : mVintfLights.get().getLights()) {
mLightsById.put(hwLight.id, new LightImpl(context, hwLight));
}
} catch (RemoteException ex) {
Slog.e(TAG, "Unable to get lights from HAL", ex);
}
}
LightImpl是内部类继承自LogicalLight,实现了setBrightness,setColor,setFlashing,turnOff等方法,这些方法最终都会通过setLightLocked调用到底层
/*
* color 颜色
*mode 模式 参数在LogicalLight中定义
*onMS 闪烁时亮的时间段
*offMS 闪烁时灭的时间段
*/
private void setLightLocked(int color, int mode, int onMS, int offMS, int brightnessMode) {
if (shouldBeInLowPersistenceMode()) {
brightnessMode = BRIGHTNESS_MODE_LOW_PERSISTENCE;
} else if (brightnessMode == BRIGHTNESS_MODE_LOW_PERSISTENCE) {
brightnessMode = mLastBrightnessMode;
}
//判断相应灯的是否有状态上的变化,若没有跳过执行
if (!mInitialized || color != mColor || mode != mMode || onMS != mOnMS ||
offMS != mOffMS || mBrightnessMode != brightnessMode) {
if (DEBUG) {
Slog.v(TAG, "setLight #" + mHwLight.id + ": color=#"
+ Integer.toHexString(color) + ": brightnessMode=" + brightnessMode);
}
mInitialized = true;
mLastColor = mColor;
mColor = color;
mMode = mode;
mOnMS = onMS;
mOffMS = offMS;
mBrightnessMode = brightnessMode;
setLightUnchecked(color, mode, onMS, offMS, brightnessMode);
}
}
private void setLightUnchecked(int color, int mode, int onMS, int offMS,
int brightnessMode) {
Trace.traceBegin(Trace.TRACE_TAG_POWER, "setLightState(" + mHwLight.id + ", 0x"
+ Integer.toHexString(color) + ")");
try {
//判断是否是走aidl
if (mVintfLights != null) {
//HwLightState存储灯的信息
HwLightState lightState = new HwLightState();
lightState.color = color;
lightState.flashMode = (byte) mode;
lightState.flashOnMs = onMS;
lightState.flashOffMs = offMS;
lightState.brightnessMode = (byte) brightnessMode;
//调用到底层
mVintfLights.get().setLightState(mHwLight.id, lightState);
} else {
setLight_native(mHwLight.id, color, mode, onMS, offMS, brightnessMode);
}
} catch (ServiceSpecificException | RemoteException | UnsupportedOperationException ex) {
Slog.e(TAG, "Failed issuing setLightState", ex);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_POWER);
}
}
我们再来看看底层,MTK主要的实现是在vendor/mediatek/proprietary/hardware/liblights/aidl/default/Lights.cpp下
这里对应了上面我们获取到的6种灯的实例,这些都是和Java代码对应的
void addLight(LightType const type, int const ordinal) {
HwLight light{};
light.id = availableLights.size();
light.type = type;
light.ordinal = ordinal;
availableLights.emplace_back(light);
}
//初始化所有支持的灯
ndk::ScopedAStatus Lights::getLights(std::vector<HwLight>* lights) {
ALOGD("Lights reporting supported lights");
if (availableLights.size() == 0) {
addLight(LightType::BACKLIGHT, 0);
addLight(LightType::KEYBOARD, 0);
addLight(LightType::BUTTONS, 0);
addLight(LightType::BATTERY, 0);
addLight(LightType::NOTIFICATIONS, 0);
addLight(LightType::ATTENTION, 0);
}
for (auto i = availableLights.begin(); i != availableLights.end(); i++) {
lights->push_back(*i);
}
return ndk::ScopedAStatus::ok();
};
当我们在上层调用setLightState时,实际是调用到这里,这里会根据不同灯的id,分别处理
ndk::ScopedAStatus Lights::setLightState(int id, const HwLightState& state) {
if (!(0 <= id && id < availableLights.size())) {
ALOGI("Lights id %d does not exist.", id);
return ndk::ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
}
HwLight const* light = &(availableLights[id]);
int ret = 0;
switch (light->type) {
case LightType::BACKLIGHT: //背光灯
ret = set_light_backlight(light, &state);
break;
case LightType::KEYBOARD: //键盘灯
ret = set_light_keyboard(light, &state);
break;
case LightType::BUTTONS: //按键灯
ret = set_light_buttons(light, &state);
break;
case LightType::BATTERY: //电源灯
ret = set_light_battery(light, &state);
break;
case LightType::NOTIFICATIONS://通知灯
ret = set_light_notifications(light, &state);
break;
case LightType::ATTENTION: //警示灯
ret = set_light_attention(light, &state);
break;
default:
return ndk::ScopedAStatus::fromServiceSpecificError(EX_UNSUPPORTED_OPERATION);
}
//判断相应灯的操作是否成功
switch (ret) {
case -ENXIO:
ALOGI("Lights id %d return %d.", id, ret);
case 0:
return ndk::ScopedAStatus::ok();
default:
ALOGI("Lights id %d return %d.", id, ret);
return ndk::ScopedAStatus::fromServiceSpecificError(EX_UNSUPPORTED_OPERATION);
}
}
不同灯的控制都是差不多的,都是通过write_int去写驱动层的节点值,以set_light_backlight,对应的底层节点LCD_FILE = “/sys/class/leds/lcd-backlight/brightness”,这些节点值,在可root的机器上,可以直接 echo xx > /sys/class/leds/lcd-backlight/brightness进行显示效果的调试
static int
set_light_backlight(HwLight const* light, HwLightState const* state)
{
if(!light) {
return -1;
}
int err = 0;
//背光只支持大小,没有颜色
int brightness = rgb_to_brightness(state);
//判断节点是否存在
if (!device_exists(LCD_FILE)) {
return -ENXIO;
}
//底层互斥锁
pthread_mutex_lock(&g_lock);
g_backlight = brightness;
//写节点值
err = write_int(LCD_FILE, brightness);
if (g_haveTrackballLight) {
handle_trackball_light_locked(light, state);
}
pthread_mutex_unlock(&g_lock);
return err;
}
案例分析1
客户需求低电量充电时亮红灯,此时灭屏来通知时绿灯闪烁,若取消了绿灯闪烁,需要亮回红灯常亮状态
实际情况:
充电时控制红的常亮,来通知需要亮绿灯时红灯灭,绿灯开始闪烁,此时取消通知或者亮屏,绿灯熄灭,这个情况下显示充电状态的红灯就不再亮了,需要拔插一下电池才行
逻辑分析
充电状态灯是由BatteryService控制的,这个服务会监听充电状态以及电量等级变化,主要控制逻辑在updateLightsLocked方法中实现
public void updateLightsLocked() {
if (mBatteryLight == null) {
return;
}
//获取状态和电量
final int level = mHealthInfo.batteryLevel;
final int status = mHealthInfo.batteryStatus;
if (level < mLowBatteryWarningLevel) {
if (status == BatteryManager.BATTERY_STATUS_CHARGING) {
// Solid red when battery is charging 低电量
mBatteryLight.setColor(mBatteryLowARGB);
} else {
// Flash red when battery is low and not charging 未充电时低电量闪烁
mBatteryLight.setFlashing(mBatteryLowARGB, LogicalLight.LIGHT_FLASH_TIMED,
mBatteryLedOn, mBatteryLedOff);
}
} else if (status == BatteryManager.BATTERY_STATUS_CHARGING
|| status == BatteryManager.BATTERY_STATUS_FULL) {
if (status == BatteryManager.BATTERY_STATUS_FULL || level >= 90) {
// Solid green when full or charging and nearly full 满电
mBatteryLight.setColor(mBatteryFullARGB);
} else {
// Solid orange when charging and halfway full 中等电量
mBatteryLight.setColor(mBatteryMediumARGB);
}
} else {
// No lights if not charging and not low 关闭指示灯
mBatteryLight.turnOff();
}
}
通知灯是由NotificationManagerService进行控制的,会根据亮灭屏状态,通知状态等进行判断,主要控制逻辑也是在其updateLightsLocked方法中实现
void updateLightsLocked()
{
if (mNotificationLight == null) {
return;
}
// handle notification lights 判断当前是否有需要亮灯的通知
NotificationRecord ledNotification = null;
while (ledNotification == null && !mLights.isEmpty()) {
final String owner = mLights.get(mLights.size() - 1);
ledNotification = mNotificationsByKey.get(owner);
if (ledNotification == null) {
Slog.wtfStack(TAG, "LED Notification does not exist: " + owner);
mLights.remove(owner);
}
}
// Don't flash while we are in a call or screen is on 电话或者亮屏
if (ledNotification == null || isInCall() || mScreenOn) {
mNotificationLight.turnOff();
} else {
NotificationRecord.Light light = ledNotification.getLight();
if (light != null && mNotificationPulseEnabled) {
// pulse repeatedly
mNotificationLight.setFlashing(light.color, LogicalLight.LIGHT_FLASH_TIMED,
light.onMs, light.offMs);
}
}
}
这两个是运行在不同的进程中的,因为LogicalLight.setLightLocked方法的逻辑,对于一个灯而言,若状态信息,也就是color mode onMs offMs等未改变,它就认为状态未变,不需要修改底层节点值,实际不调用底层的方法的,这就导致了,通知灯修改了灯的状态,但对于电源灯来说,他并不知实际灯的状态被修改了,即使上层电量信息更新,但只要电源灯对比其上一次的状态未发生改变,BatteryService就不会实际控制这个灯。
解决方案
在NotificationManagerService关闭通知灯时,通知BatteryService重置电源灯的状态,达到电源灯能进行状态更新的效果,这里提供一种使用SettingsProvider通过监听数据库变化来进行跨进程的通信的方式,
1.新建 key LIGHT_BATTERY_RESET
2.在NotificationManagerService相应方法中修改相应的value
3.在BatteryService中监听value的变化,触发reset
附Patch
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index ba8d16a..ef9aebf 100755
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -4781,6 +4781,9 @@
/** @hide */
public static final String LIGHT_CHARGING_OPEN = "notification_light_charging";
+
+ /** @hide */
+ public static final String LIGHT_BATTERY_RESET = "light_battery_reset";
/* AX3207 code for AX3207-531 by chenyantong at 2020.10.28 end*/
/**
diff --git a/services/core/java/com/android/server/BatteryService.java b/services/core/java/com/android/server/BatteryService.java
index 38a2491..e93a4ef 100644
--- a/services/core/java/com/android/server/BatteryService.java
+++ b/services/core/java/com/android/server/BatteryService.java
@@ -293,6 +293,24 @@
mContext.getContentResolver().registerContentObserver(
Settings.System.getUriFor(Settings.System.LIGHT_CHARGING_OPEN), false, mLedSwitchObserver);
/* AX3207 code for AX3207-531 by chenyantong at 2020.10.28 end*/
+ mContext.getContentResolver().registerContentObserver(
+ Settings.System.getUriFor(Settings.System.LIGHT_BATTERY_RESET), false, new ContentObserver(mHandler) {
+ @Override
+ public void onChange(boolean selfChange) {
+ Slog.i(TAG, "reset light------check");
+ if(!selfChange){
+ int test_temp = Settings.System.getInt(mContext.getContentResolver(),Settings.System.LIGHT_BATTERY_RESET,0);
+ if(test_temp==1){
+ Slog.i(TAG, "reset light------reset");
+ if (mLed != null && mLed.mBatteryLight != null) {
+ mLed.mBatteryLight.turnOff();
+ mLed.updateLightsLocked();
+ }
+ Settings.System.putInt(mContext.getContentResolver(),Settings.System.LIGHT_BATTERY_RESET,0);
+ }
+ }
+ }
+ });
}
}
}
@@ -1163,7 +1181,7 @@
* Synchronize on BatteryService.
*/
public void updateLightsLocked() {
- if (mBatteryLight == null) {
+ if (mBatteryLight == null || mHealthInfo == null) {
return;
}
final int level = mHealthInfo.batteryLevel;
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 25bf1cd..7e435f6 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -8176,6 +8176,7 @@
// Don't flash while we are in a call or screen is on
if (ledNotification == null || isInCall() || mScreenOn) {
mNotificationLight.turnOff();
+ Settings.System.putInt(getContext().getContentResolver(),Settings.System.LIGHT_BATTERY_RESET,1);
} else {
NotificationRecord.Light light = ledNotification.getLight();
if (light != null && mNotificationPulseEnabled) {
如何修改背光最大亮度到2047
因系统限制代码导致上层能下发的最大亮度值为255,但实际屏幕背光支持的最大亮度为2047,需要修改源码
我们先来看看造成这个255值的原因,首先brightness是一个float,0~1之间的一个值,这个一般需要乘以最大亮度
上层这边无论我们下发的brightnessInt无论有多大,最终只会通过位运算限制他的值最大为255
比如我们下发一个400的值通过& 0x000000ff运算,0xff,即为255.所有400–>0xffffffff,这个值到底层
public void setBrightness(float brightness, int brightnessMode) {
...
int brightnessInt = BrightnessSynchronizer.brightnessFloatToInt(
getContext(), brightness);
int color = brightnessInt & 0x000000ff;
color = 0xff000000 | (color << 16) | (color << 8) | color;
setLightLocked(color, LIGHT_FLASH_NONE, 0, 0, brightnessMode);
...
}
底层得到0xffffffff,
color=0x00ffffff
77*0x00ff +
150*0x00ff +
29*0x00ff = (77+150+29)*0x00ff >>8=256*0xff>>8=0xff
所以底层这边最大值也为0xff
static int
rgb_to_brightness(const HwLightState* state)
{
int color = state->color & 0x00ffffff;
return (int)(((77*((color>>16)&0x00ff))
+ (150*((color>>8)&0x00ff)) + (29*(color&0x00ff))) >> 8);
}
static int
set_light_backlight(HwLight const* light, HwLightState const* state)
{
if(!light) {
return -1;
}
int err = 0;
int brightness = rgb_to_brightness(state);
...
err = write_int(LCD_FILE, brightness);
return err;
}
这里面传值其实是将背光灯当成一个有色彩的灯来看的ARGB为0xffffffff,但其实不需要这样,我们可以是有其中多余的位来记录这个超过255的值,2047为0xfff只需就可以表示
修改方案:
//PowerManager.java
public static final int BRIGHTNESS_ON = 2047;
//LightsService.java
public void setBrightness(float brightness, int brightnessMode) {
...
int color = brightnessInt & 0x00000fff;
setLightLocked(color, LIGHT_FLASH_NONE, 0, 0, brightnessMode);
...
}
//Lights.cpp
static int
rgb_to_brightness(const HwLightState* state)
{
int color = state->color & 0x00000fff;
return color;
}