第二十六章:系统级应用开发:将距离传感器功能集成到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处理,再到触摸拦截,一目了然。

全局防误触系统流程图 距离传感器 SensorManager 事件回调 SystemUI (ProximitySensorManager) 判断 isNear ? PhoneWindowManager 拦截触摸 正常触摸事件传递 触摸被屏蔽

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