第二十六章:系统级应用开发:将距离传感器功能集成到SystemUI、修改AOSP源码实现全局防误触
说实话,到了这一章,才真正触及到距离传感器在Android系统中的“核心玩法”。前面我们聊了那么多驱动、HAL、Framework层的调用,但那些都是基础。真正让距离传感器发挥最大价值的场景,就是系统级的防误触。
你想想看,用户把手机放在口袋里,屏幕亮了,然后误触拨打了紧急电话——这种体验有多糟糕?我当年在项目里就遇到过这种投诉,用户直接说“你们这手机是给我找麻烦的”。所以,把距离传感器集成到SystemUI里,实现全局防误触,是每个成熟Android系统必须做的事。
26.1 为什么是SystemUI?
SystemUI是什么?它是Android系统最顶层的UI进程,负责状态栏、导航栏、锁屏、通知面板这些核心界面。说白了,用户能看到的系统界面,基本都在SystemUI里。
把距离传感器功能放在SystemUI里,有几个好处:
- 响应快:SystemUI是常驻进程,优先级高,传感器事件能第一时间被处理
- 覆盖广:只要SystemUI在运行,防误触就能生效,不受单个App影响
- 权限高:SystemUI有系统级权限,可以直接操作窗口、拦截触摸事件
我在做第一个集成项目时,一开始把防误触逻辑放在PhoneWindowManager里,结果发现锁屏界面和通知栏的响应总是慢半拍。后来迁移到SystemUI,问题就解决了。嗯,选对位置很重要。
26.2 全局防误触的核心逻辑
全局防误触,说白了就是一句话:当距离传感器检测到物体靠近(比如手机在口袋里),系统应该屏蔽触摸事件,并且阻止屏幕点亮。
但实现起来,要考虑几个关键点:
- 检测时机:什么时候开始检测?屏幕熄灭时?还是始终检测?
- 响应策略:检测到遮挡后,是直接灭屏?还是只屏蔽触摸?
- 退出条件:什么时候恢复触摸?传感器数值恢复正常后多久?
我个人习惯的做法是:屏幕熄灭后立即开启防误触检测,屏幕亮起后延迟500ms再关闭。为什么要有延迟?因为用户从口袋里拿出手机时,传感器数值不会瞬间跳变,需要一点缓冲时间。
核心原则:防误触的触发要快,退出要慢。宁可多防一会儿,也不要误触。
26.3 修改AOSP源码:SystemUI集成距离传感器
好,我们直接看代码。AOSP中SystemUI的源码路径在 frameworks/base/packages/SystemUI/。我们需要新增一个服务类,专门管理距离传感器。
26.3.1 创建ProximitySensorManager
先创建一个管理类,负责注册传感器监听、处理事件、回调通知。
// ProximitySensorManager.java
public class ProximitySensorManager {
private static final String TAG = "ProximitySensorManager";
private SensorManager mSensorManager;
private Sensor mProximitySensor;
private boolean mIsNear = false;
private ProximityListener mListener;
public interface ProximityListener {
void onProximityChanged(boolean isNear);
}
public ProximitySensorManager(Context context) {
mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
mProximitySensor = mSensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
}
public void registerListener(ProximityListener listener) {
mListener = listener;
if (mProximitySensor != null) {
mSensorManager.registerListener(mSensorEventListener,
mProximitySensor, SensorManager.SENSOR_DELAY_NORMAL);
}
}
public void unregisterListener() {
mSensorManager.unregisterListener(mSensorEventListener);
mListener = null;
}
private SensorEventListener mSensorEventListener = new SensorEventListener() {
@Override
public void onSensorChanged(SensorEvent event) {
float distance = event.values[0];
boolean near = distance < mProximitySensor.getMaximumRange();
if (near != mIsNear) {
mIsNear = near;
if (mListener != null) {
mListener.onProximityChanged(near);
}
}
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
// 精度变化时一般不做处理
}
};
}
这段代码看起来简单,但有个细节要注意:distance < mProximitySensor.getMaximumRange() 这个判断。不同手机的传感器最大范围不一样,有的是5cm,有的是10cm。用最大范围做阈值,比写死一个数值要通用得多。
小技巧:我建议在注册监听时,把传感器延迟设为 SENSOR_DELAY_NORMAL 就够了。防误触不需要高频采样,200ms一次完全够用,还能省电。
26.3.2 在SystemUI中集成防误触逻辑
接下来,我们需要在SystemUI的某个核心组件中启动这个管理器。我个人推荐放在 StatusBar.java 或者 SystemUIService.java 中。
// 在StatusBar.java中
public class StatusBar extends SystemUI {
private ProximitySensorManager mProximitySensorManager;
private boolean mProximityBlocked = false;
@Override
public void start() {
mProximitySensorManager = new ProximitySensorManager(mContext);
mProximitySensorManager.registerListener(isNear -> {
mProximityBlocked = isNear;
updateTouchBlocking();
});
}
private void updateTouchBlocking() {
// 这里通过WindowManager或InputManager来屏蔽触摸
if (mProximityBlocked) {
// 屏蔽触摸事件
disableTouchInput();
} else {
// 恢复触摸
enableTouchInput();
}
}
}
这里有个关键问题:如何屏蔽触摸事件? 我见过几种做法:
- 方法一:通过WindowManager添加一个全屏的透明窗口,拦截所有触摸事件。简单粗暴,但会影响性能。
- 方法二:通过InputManager直接禁用触摸屏。这个权限要求高,但效果最好。
- 方法三:在PhoneWindowManager中拦截触摸事件分发。这是最优雅的方式。
我个人推荐方法三,因为它不额外消耗资源,而且与系统事件分发机制结合得最好。
26.4 修改PhoneWindowManager实现触摸拦截
PhoneWindowManager位于 frameworks/base/services/core/java/com/android/server/policy/PhoneWindowManager.java。我们需要在触摸事件分发前加入距离传感器状态判断。
// PhoneWindowManager.java
public class PhoneWindowManager implements WindowManagerPolicy {
private boolean mProximityTouchBlocked = false;
// 这个方法会在触摸事件分发前被调用
@Override
public int interceptMotionBeforeQueueingWhenScreenOn(int policyFlags) {
if (mProximityTouchBlocked) {
// 返回FLAG_PASS_TO_USER的相反值,阻止事件传递
return 0;
}
return FLAG_PASS_TO_USER;
}
// 提供一个公开方法供SystemUI调用
public void setProximityTouchBlocked(boolean blocked) {
mProximityTouchBlocked = blocked;
}
}
然后,在SystemUI中通过Binder调用这个接口:
// 在ProximitySensorManager中
private void updateTouchBlocking(boolean blocked) {
try {
IBinder windowManager = ServiceManager.getService("window");
IWindowManager wm = IWindowManager.Stub.asInterface(windowManager);
wm.setProximityTouchBlocked(blocked);
} catch (RemoteException e) {
Log.e(TAG, "Failed to set touch blocking", e);
}
}
注意:跨进程调用时,一定要处理好异常。我曾经遇到过SystemUI崩溃后,防误触状态卡死的情况——触摸永远被屏蔽了,用户只能强制重启。所以,建议在SystemUI的onDestroy中强制恢复触摸状态。
26.5 全局防误触的完整流程图
下面我用一张SVG图,把整个流程串起来。从传感器数据采集,到SystemUI处理,再到触摸拦截,一目了然。
26.6 避坑指南:我踩过的几个坑
做这个功能,我前前后后踩了不少坑。挑几个典型的说说:
- 坑一:传感器注册时机不对。我曾经在SystemUI的构造函数里就注册了传感器监听,结果SystemUI还没完全启动,传感器事件来了,导致空指针。后来我改到
onBootCompleted回调里注册,问题解决。 - 坑二:防误触和抬手亮屏冲突。有些手机有“抬手亮屏”功能,用的是加速度传感器。如果距离传感器检测到遮挡,但抬手亮屏又试图点亮屏幕,两者就会打架。我的解决方案是:在防误触状态下,直接屏蔽抬手亮屏的请求。
- 坑三:传感器校准问题。不同批次的手机,距离传感器的基线值可能不一样。我遇到过一批手机,传感器在没有遮挡时返回的值是5.0,另一批返回的是8.0。如果阈值写死,就会出问题。所以一定要用
getMaximumRange()动态计算。
我的建议:在开发阶段,一定要在SystemUI的日志中打印传感器原始值。这样调试时能快速定位问题。我曾经靠这个日志,发现某款手机的传感器在靠近时返回的不是0,而是0.5——差点以为是bug,其实是硬件特性。
26.7 性能与功耗考量
全局防误触是一个常驻功能,所以性能和功耗必须重视。我列一个对比表:
| 方案 | 功耗 | 响应速度 | 实现复杂度 |
|---|---|---|---|
| 传感器常开,高频采样 | 高(增加20%待机功耗) | 快(<50ms) | 低 |
| 传感器常开,低频采样 | 中(增加5%待机功耗) | 中(~200ms) | 低 |
| 屏幕熄灭后开启,低频采样 | 低(几乎无影响) | 中(~200ms) | 中 |
| 结合加速度传感器联动 | 最低 | 慢(~500ms) | 高 |
我个人推荐第三种方案:屏幕熄灭后开启,低频采样。既保证了防误触效果,又不会明显增加功耗。至于第四种方案,虽然功耗最低,但响应太慢,用户体验不好。
26.8 小结
把距离传感器集成到SystemUI,实现全局防误触,是Android系统级开发中一个非常典型的场景。它涉及传感器管理、跨进程通信、触摸事件拦截等多个知识点。
做这个功能,关键要记住三点:
- 选对位置:SystemUI是最合适的宿主
- 处理好时机:触发要快,退出要慢
- 注意异常:跨进程调用、传感器校准、功耗平衡,一个都不能少
好了,这一章的内容就到这里。如果你在实际开发中遇到什么问题,欢迎随时交流。
公众号:蓝海资料掘金营,微信deep3321