android4.4 車載滅屏 按任意鍵及觸控式螢幕幕恢復亮屏
車載上的android4.4系統,基本上常亮。但最近需要一個新功能可以在launcher新增一個按鈕,點選的時候。螢幕亮度為0,但實際上不等於按power鍵,不會睡眠。
然後可以按任意鍵恢復亮度,包括觸屏事件。
一、PowerManagerService原先螢幕亮度流程
PowerManagerService是通過updateDisplayPowerStateLocked函式,把亮度更新到DisplayPowerController那塊,然後再去呼叫lightsService獲取背光的light,再去設定背景光的亮度。
但是這有問題,在PowerManagerService的updateDisplayPowerStateLocked函式,通常有個最低的亮度值(一般為10),如果低於這個亮度還是為這個值。
所以呼叫PowerManager的setBacklightBrightness,哪怕設的為0,最終亮度也為10.
private void updateDisplayPowerStateLocked(int dirty) { if ((dirty & (DIRTY_WAKE_LOCKS | DIRTY_USER_ACTIVITY | DIRTY_WAKEFULNESS | DIRTY_ACTUAL_DISPLAY_POWER_STATE_UPDATED | DIRTY_BOOT_COMPLETED | DIRTY_SETTINGS | DIRTY_SCREEN_ON_BLOCKER_RELEASED)) != 0) { int newScreenState = getDesiredScreenPowerStateLocked(); if (newScreenState != mDisplayPowerRequest.screenState) { if (newScreenState == DisplayPowerRequest.SCREEN_STATE_OFF && mDisplayPowerRequest.screenState != DisplayPowerRequest.SCREEN_STATE_OFF) { mLastScreenOffEventElapsedRealTime = SystemClock.elapsedRealtime(); } mDisplayPowerRequest.screenState = newScreenState; nativeSetPowerState( newScreenState != DisplayPowerRequest.SCREEN_STATE_OFF, newScreenState == DisplayPowerRequest.SCREEN_STATE_BRIGHT); } int screenBrightness = mScreenBrightnessSettingDefault; float screenAutoBrightnessAdjustment = 0.0f; boolean autoBrightness = (mScreenBrightnessModeSetting == Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC); if (isValidBrightness(mScreenBrightnessOverrideFromWindowManager)) { screenBrightness = mScreenBrightnessOverrideFromWindowManager; autoBrightness = false; } else if (isValidBrightness(mTemporaryScreenBrightnessSettingOverride)) { screenBrightness = mTemporaryScreenBrightnessSettingOverride; } else if (isValidBrightness(mScreenBrightnessSetting)) { screenBrightness = mScreenBrightnessSetting; } if (autoBrightness) { screenBrightness = mScreenBrightnessSettingDefault; if (isValidAutoBrightnessAdjustment( mTemporaryScreenAutoBrightnessAdjustmentSettingOverride)) { screenAutoBrightnessAdjustment = mTemporaryScreenAutoBrightnessAdjustmentSettingOverride; } else if (isValidAutoBrightnessAdjustment( mScreenAutoBrightnessAdjustmentSetting)) { screenAutoBrightnessAdjustment = mScreenAutoBrightnessAdjustmentSetting; } } screenBrightness = Math.max(Math.min(screenBrightness,//這個就是最小亮度的調整。 mScreenBrightnessSettingMaximum), mScreenBrightnessSettingMinimum); screenAutoBrightnessAdjustment = Math.max(Math.min( screenAutoBrightnessAdjustment, 1.0f), -1.0f); mDisplayPowerRequest.screenBrightness = screenBrightness; mDisplayPowerRequest.screenAutoBrightnessAdjustment = screenAutoBrightnessAdjustment; mDisplayPowerRequest.useAutoBrightness = autoBrightness; mDisplayPowerRequest.useProximitySensor = shouldUseProximitySensorLocked(); mDisplayPowerRequest.blockScreenOn = mScreenOnBlocker.isHeld(); mDisplayReady = mDisplayPowerController.requestPowerState(mDisplayPowerRequest, mRequestWaitForNegativeProximity);//最終都是設定到DisplayPowerController mRequestWaitForNegativeProximity = false;
二、PowerManagerService設定背光檔案節點
因此我們的選擇還是直接設定背光的檔案節點,況且我們還要事先獲取背光值,用來下次恢復亮度的時候,設定之前的亮度值。
程式碼如下我們再PowerManager中做了兩個介面turnoffBacklight和turnOnBacklight來控制背光,最終通過binder呼叫到PowerManagerService中。
在PowerManagerService中直接通過了寫節點/讀節點的方式,來設定/獲取背光。
@Override // Binder call public void turnOffBacklight() { int lightBrightness = getBackLightBrightness(); if (lightBrightness != 0) { mBackLightBrightness = getBackLightBrightness(); setBackLightBrightness(0); } } @Override // Binder call public boolean turnOnBacklight() { int lightBrightness = getBackLightBrightness(); if (lightBrightness == 0) { Slog.d(TAG, "turnOnBacklight setBackLightBrightness :" + mBackLightBrightness); setBackLightBrightness(mBackLightBrightness); return true; } return false; } private int getBackLightBrightness() { int level = -1; File localFile = new File("/sys/class/leds/lcd-backlight/brightness"); if (!localFile.canRead()) { Slog.w(TAG, "/sys/class/leds/lcd-backlight can not read!"); return level; } try { FileInputStream in = new FileInputStream(localFile); byte[] b = new byte[4]; in.read(b); int count = 0; for (int i = 0; i < 4; i++) { if (b[i] >= '0' && b[i] <= '9') {//非數字去除 count++; } else { break; } } String str = new String(b, 0, count); str = str.replaceAll("\\s+", "");//去除空格,換行符等 level = Integer.parseInt(str); in.close(); } catch (Exception e) { } return level; } private void setBackLightBrightness(int level) { File localFile = new File("/sys/class/leds/lcd-backlight/brightness"); if (!localFile.canWrite()) { Slog.w(TAG, "/sys/class/leds/lcd-backlight can not write!"); return; } try { FileOutputStream fos = new FileOutputStream(localFile); fos.write(String.valueOf(level).getBytes()); fos.close(); } catch (Exception e) { } }
我們再Launcher的按鈕點選後呼叫PowerManager中的turnoffBacklight函式,滅屏。
三、恢復螢幕亮度
3.1 按鍵
最後我們在按鍵和觸屏的時候恢復螢幕亮度,普通按鍵的流程會先到PhoneWindowManager的interceptKeyBeforeQueueing函式先處理,我們可以在這個函式中先進行背光亮度的判讀, 部分程式碼如下:
@Override
public int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags, boolean isScreenOn) {
if (!mSystemBooted) {
// If we have not yet booted, don't let key events do anything.
return 0;
}
if (mPowerManager.isScreenOn()) {
if (mPowerManager.turnOnBacklight()) {
return 0;
}
}.......
邏輯無非兩種情況,呼叫mPowerManager.isScreenOn函式
1.如果isScreenOn是返回true的。因為PowerManagerService的流程沒有改變,系統還認為螢幕是亮著的。
所以我們turnOnBacklight,如果這個時候螢幕的亮度為0,代表我們之前呼叫過turnoffBacklight函式把背景光亮度設定為0了,我們把它恢復的亮度,而函式turnOnBacklight返回true,在interceptKeyBeforeQueueing函式中直接return了,代表這次的按鍵就點亮螢幕了,不會走interceptKeyBeforeQueueing的後續流程了,返回0也不會最後傳給使用者了。
如果這個時候有亮度,代表之前沒有呼叫turnoffBacklight,函式turnOnBacklight返回false,繼續interceptKeyBeforeQueueing的原有流程。代表如果螢幕亮著,按鍵處理走自己的流程。
2.如果isScreenOn返回false,代表現在滅屏了。那麼這是PowerManagerService的流程了,和我們的背景光無關。
3.2 觸屏
觸屏的話流程會有一個不一樣,因為在NativeInputManager的interceptMotionBeforeQueueing函式中,只有當isScreenOn是false的時候才會呼叫上層的interceptMotionBeforeQueueingWhenScreenOff函式。也就是滅屏的時候才會到PhoneWindowManager的interceptMotionBeforeQueueingWhenScreenOff函式,而我們恰恰是要在PowerManagerService亮屏的時候,進行我們的邏輯。
void NativeInputManager::interceptMotionBeforeQueueing(nsecs_t when, uint32_t& policyFlags) {
// Policy:
// - Ignore untrusted events and pass them along.
// - No special filtering for injected events required at this time.
// - Filter normal events based on screen state.
// - For normal events brighten (but do not wake) the screen if currently dim.
if ((policyFlags & POLICY_FLAG_TRUSTED) && !(policyFlags & POLICY_FLAG_INJECTED)) {
if (isScreenOn()) {
policyFlags |= POLICY_FLAG_PASS_TO_USER;
if (!isScreenBright()) {
policyFlags |= POLICY_FLAG_BRIGHT_HERE;
}
} else {
JNIEnv* env = jniEnv();
jint wmActions = env->CallIntMethod(mServiceObj,
gServiceClassInfo.interceptMotionBeforeQueueingWhenScreenOff,
policyFlags);
if (checkAndClearExceptionFromCallback(env,
"interceptMotionBeforeQueueingWhenScreenOff")) {
wmActions = 0;
}
policyFlags |= POLICY_FLAG_WOKE_HERE | POLICY_FLAG_BRIGHT_HERE;
handleInterceptActions(wmActions, when, /*byref*/ policyFlags);
}
} else {
policyFlags |= POLICY_FLAG_PASS_TO_USER;
}
}
所以在PhoneWindowManager的interceptMotionBeforeQueueingWhenScreenOff函式處理不行,最後只能到應用程序處理,看我之前幾篇分析按鍵的部落格知道,觸屏事件最終會呼叫到ViewRootImpl的各個InputStage中,而觸屏和按鍵會走到ViewPostImeInputStage中去 final class ViewPostImeInputStage extends InputStage {
public ViewPostImeInputStage(InputStage next) {
super(next);
}
@Override
protected int onProcess(QueuedInputEvent q) {
if (q.mEvent instanceof KeyEvent) {
return processKeyEvent(q);
} else {
// If delivering a new non-key event, make sure the window is
// now allowed to start updating.
handleDispatchDoneAnimating();
final int source = q.mEvent.getSource();
if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {//觸屏事件
if (mPowerManager.isScreenOn()) {
if (mPowerManager.turnOnBacklight()) {
return FORWARD;
}
}
return processPointerEvent(q);
} else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
return processTrackballEvent(q);
} else {
return processGenericMotionEvent(q);
}
}
}
這裡的話流程和按鍵那邊的處理邏輯是一樣的,就不分析了。四、注意
有一點我們必須注意,手機如果要殺應用的話,輸入法絕對不能殺,殺了輸入法後,會導致輸入法沒有重啟的這段時間,底層按鍵不能分發到上層應用。具體原因,後續分析。