From f5c4e5adaedbde3f5dde6e4b42b5de34f99898da Mon Sep 17 00:00:00 2001 From: fuleyi Date: Tue, 13 Oct 2026 17:56:39 +0800 Subject: [PATCH 1/2] fix: ambient Supported reports hardware, not sampling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Supported now reports whether the machine has a usable ambient light sensor, independent of the switch, lid, sleep and session state. 2. Probe capability asynchronously via LookupAmbientLightSensor, with no synchronous D-Bus round trip on lid/sleep/session hot paths. 3. Stop treating "should not sample" as "no hardware": stopping sampling keeps Supported and moves State to Disabled instead of Unavailable. 4. Make the model capability flag service-owned, and stop disconnectSensor from writing model state so one exit no longer answers every cause. 5. Unify both switch-off paths into stopSampling, and keep Disabled or Unavailable across policy rebuilds. 6. Notify Enabled on cold start, and change the default of ambientLightAdjustBrightness to off. 7. Cover the capability semantics with a self-contained test: drive the service state machine directly and point the only bus address at an unreachable path, so no dbus-daemon is required. Log: Ambient brightness reported itself unsupported when the switch was off Influence: 1. On a machine with an ambient light sensor and the switch off at start, verify Supported is true and State is Disabled, and that no ClaimLight is issued. 2. Turn the switch off at runtime and verify Supported stays true while State becomes Disabled. 3. Verify a machine without iio-sensor-proxy reports Supported=false and State=Unavailable, and that a non-lux unit is rejected. 4. Verify lid close/sleep/session background only stop sampling and never clear Supported. 5. Run ctest for ambient-brightness-policy and ambient-brightness-capability. fix: 修正 Supported 语义为硬件能力而非采样状态 1. Supported 改为上报本机是否具备可用环境光传感器,与开关、盖子、 休眠和会话状态无关。 2. 通过 LookupAmbientLightSensor 异步探测能力,合盖/休眠/会话切换等 热路径上不再有同步 D-Bus 往返。 3. 不再把"不应采样"当成"没有硬件":停止采样保留 Supported,状态转为 Disabled 而非 Unavailable。 4. 能力标志改由 Service 独占写入,disconnectSensor 不再写模型状态, 一个出口不再同时解释所有停止原因。 5. 合并两条关闭开关路径到 stopSampling,策略重建时保留 Disabled 或 Unavailable。 6. 冷启动补发 Enabled 通知,并将 ambientLightAdjustBrightness 默认值 改为关闭。 7. 补充自包含的能力语义用例:直接驱动服务状态机,并把唯一使用的总线 地址指向不可达路径,因此不需要 dbus-daemon。 Log: 自动亮度在开关关闭时把自己误报为不支持 Influence: 1. 在具备环境光传感器且启动时开关关闭的机器上,验证 Supported 为 true、 State 为 Disabled,且未调用 ClaimLight。 2. 运行中关闭开关,验证 Supported 保持 true 且 State 变为 Disabled。 3. 验证无 iio-sensor-proxy 时 Supported=false、State=Unavailable, 且非 lux 单位被拒绝。 4. 验证合盖/休眠/会话切后台只停止采样,不会清掉 Supported。 5. 运行 ctest 的 ambient-brightness-policy 与 ambient-brightness-capability。 PMS: BUG-378895 --- src/plugin-qt/ambient-brightness/README.md | 43 +++-- .../ambientbrightnessmodel.cpp | 9 +- .../ambientbrightnessmodel.h | 7 +- .../ambientbrightnessservice.cpp | 150 +++++++++++----- .../ambientbrightnessservice.h | 13 ++ .../ambient-brightness/tests/CMakeLists.txt | 21 +++ .../tests/tst_ambientbrightnesscapability.cpp | 168 ++++++++++++++++++ 7 files changed, 350 insertions(+), 61 deletions(-) create mode 100644 src/plugin-qt/ambient-brightness/tests/tst_ambientbrightnesscapability.cpp diff --git a/src/plugin-qt/ambient-brightness/README.md b/src/plugin-qt/ambient-brightness/README.md index 7c9608d9..d15c1b81 100644 --- a/src/plugin-qt/ambient-brightness/README.md +++ b/src/plugin-qt/ambient-brightness/README.md @@ -44,10 +44,22 @@ | 属性 | 类型 | 含义 | | --- | --- | --- | -| `Supported` | `bool` | 当前是否已连接并成功声明环境光传感器 | -| `State` | `string` | 当前状态:`Unavailable`、`Disabled`、`WaitingForSample` 或 `Active` | +| `Supported` | `bool` | 本机是否具备可用的环境光传感器(硬件能力事实)。与开关是否开启、上盖是否合上、系统是否休眠、会话是否前台都无关 | +| `State` | `string` | 当前运行状态:`Unavailable`(无可用传感器)、`Disabled`(有传感器但未采样)、`WaitingForSample`、`Active` | | `RecommendedBrightness` | `double` | 推荐亮度,范围 `[0.0, 1.0]` | +`Supported` 由 Service 通过一次**无副作用、完全异步**的能力探测得出:调用 +`net.hadess.SensorProxy.LookupAmbientLightSensor()`,返回非空对象路径即视为本机具备光感。 +探测不调用 `ClaimLight`,因此关闭开关时不会向传感器发起任何采样调用;探测全程使用 +`QDBusPendingCallWatcher`,在合盖、休眠、会话切换、开关切换等热路径上都不会产生同步 +D-Bus 往返。探测结果先写入 `Supported`,随后才由 `HasAmbientLight` 与 `LightLevelUnit` +(`lux` 或未设置)做最终校验——校验失败会撤销本次 `Supported=true`。 + +只有探测结果能改写 `Supported`;关闭开关、合盖、休眠、会话切后台等"不采样"的原因一律 +只影响 `State`,不得把 `Supported` 清成 `false`。探测在以下时机触发:服务初始化、 +传感器服务注册/注销、自动亮度开关切换。传感器服务退出后重探测会得到"不支持", +`Supported` 随之变为 `false` 并对外广播。 + 属性变化通过标准 `PropertiesChanged` 信号发布。 每次重新 Claim、策略重建或从 `Disabled` 恢复后,首个有效 Recommendation 都会重新发布,即使数值与停止前相同;消费者不需要依赖旧推荐值推断是否应重新应用亮度。 @@ -107,7 +119,11 @@ flowchart LR 初始化光感时,Service 创建 `QDBusInterface` 并校验 ALS 能力和 lux 单位。随后先订阅 `PropertiesChanged`,再调用 `ClaimLight`,以免漏掉驱动在 Claim 调用期间同步上报的样本。Claim 成功后,首个有效 `LightLevel` 信号作为初始样本;若 2 秒内没有收到信号,则读取一次此时的 `LightLevel` 属性兜底,以覆盖真实首帧等于代理缓存值、重复 Claim 或已有其他客户端占用时不产生变化信号的情况。策略已经被 stop 流程重置,因此首个有效样本会立即生成推荐亮度。 -任一运行条件失效,或传感器服务注销时,Service 停止评估定时器、调用 `ReleaseLight`、清空最近样本,并让 Model 进入 `Unavailable`。 +能力探测(`Supported`)与是否采样(`State`)是两件事:探测只读属性、不调用 `ClaimLight`, +在生命周期闸门之前进行;闸门只决定是否 Claim 采样。任一运行条件失效(关闭自动亮度、合盖、 +休眠、会话切后台)时,Service 停止采样——断开信号、调用 `ReleaseLight`、清空最近样本, +`State` 回到 `Disabled`,`Supported` 保持能力事实不变。只有传感器服务注销或能力探测判定 +无可用传感器时才进入 `Unavailable` 并把 `Supported` 置为 `false`。 ### 3. 样本处理 @@ -141,9 +157,9 @@ DConfig 发生变化时,Service 会: | 事件 | 动作 | | --- | --- | -| 用户关闭自动亮度 | stop:`ReleaseLight`,停止定时器并重置策略 | +| 用户关闭自动亮度 | stop:`ReleaseLight`,停止定时器并重置策略;`State=Disabled`,`Supported` 不变 | | 用户重新开启自动亮度 | init:若上盖打开且系统已唤醒,则先监听再 `ClaimLight`,等待首个有效 `LightLevel`;2 秒无信号时延迟读取属性兜底 | -| 合盖 | stop | +| 合盖 | stop,`State=Disabled` | | 开盖 | init;仍处于休眠或自动亮度关闭时保持停止 | | 进入待机、休眠 | stop | | 待机、休眠唤醒 | init;上盖仍关闭、会话非前台或自动亮度关闭时保持停止 | @@ -157,11 +173,16 @@ DConfig 发生变化时,Service 会: ```mermaid stateDiagram-v2 [*] --> Unavailable - Unavailable --> WaitingForSample: 运行条件满足并 ClaimLight 成功 + Unavailable --> Disabled: 能力探测发现可用传感器(未采样) + Disabled --> WaitingForSample: 运行条件满足并 ClaimLight 成功 + Unavailable --> WaitingForSample: 能力探测通过且运行条件满足,ClaimLight 成功 WaitingForSample --> Active: Claim 后首个有效 LightLevel(或超时兜底值)产生 Recommendation Active --> WaitingForSample: 算法配置更新并重建策略 - WaitingForSample --> Unavailable: 关闭自动亮度、合盖、休眠、会话非前台或传感器断开 - Active --> Unavailable: 关闭自动亮度、合盖、休眠、会话非前台或传感器断开 + WaitingForSample --> Disabled: 关闭自动亮度、合盖、休眠、会话非前台 + Active --> Disabled: 关闭自动亮度、合盖、休眠、会话非前台 + Disabled --> WaitingForSample: 运行条件恢复 + Disabled --> Unavailable: 传感器服务注销或能力探测判定不可用 + Active --> Unavailable: 传感器服务注销或能力探测判定不可用 ``` 无效样本会被策略拒绝,不会使 `WaitingForSample` 错误进入 `Active`。 @@ -171,9 +192,9 @@ stateDiagram-v2 | 类、结构或入口 | 文件 | 职责 | | --- | --- | --- | | `DSMRegister` / `DSMUnRegister` | `plugin.cpp` | 插件 ABI 入口。创建、初始化和销毁全局 `AmbientBrightnessService` 实例,处理重复注册和初始化失败。 | -| `AmbientBrightnessService` | `ambientbrightnessservice.h/cpp` | 模块编排层。导出 D-Bus 属性;按自动亮度开关、盖子和休眠状态执行光感 stop/init;声明和释放传感器;监听服务上下线及 `LightLevel`;管理定时器、DConfig、策略重建和属性发布。它不实现亮度算法。 | -| `AmbientLightLifecycleState` | `ambientlightlifecyclestate.h` | 保存自动亮度开关、盖子、休眠和会话前台四个独立条件;仅在开关开启、上盖打开、系统唤醒且会话前台时允许 Claim ALS。 | -| `AmbientBrightnessModel` | `ambientbrightnessmodel.h/cpp` | Qt 状态适配层。独占一个 `AmbientBrightnessPolicy`;把 `submitSample()`、`tick()` 转发给策略;维护 `Supported`、`State` 和推荐亮度;将策略结果转换为 Qt 信号。它不访问 D-Bus 或 DConfig。 | +| `AmbientBrightnessService` | `ambientbrightnessservice.h/cpp` | 模块编排层。导出 D-Bus 属性;执行无副作用的能力探测并独占写入 `Supported`;按自动亮度开关、盖子和休眠状态执行光感 stop/init;声明和释放传感器;监听服务上下线及 `LightLevel`;管理定时器、DConfig、策略重建和属性发布。它不实现亮度算法。 | +| `AmbientLightLifecycleState` | `ambientlightlifecyclestate.h` | 保存自动亮度开关、盖子、休眠和会话前台四个独立条件;仅在开关开启、上盖打开、系统唤醒且会话前台时允许 Claim ALS。它不参与能力判定。 | +| `AmbientBrightnessModel` | `ambientbrightnessmodel.h/cpp` | Qt 状态适配层。独占一个 `AmbientBrightnessPolicy`;把 `submitSample()`、`tick()` 转发给策略;维护 `Supported`(由 Service 探测结果写入,状态转换不得改写)、`State` 和推荐亮度;将策略结果转换为 Qt 信号。它不访问 D-Bus 或 DConfig。 | | `AmbientBrightnessPolicy` | `ambientbrightnesspolicy.h` | 与 Qt、D-Bus 无关的算法抽象接口,定义 `update()`、`tick()`、`reset()` 和 `nextEvaluationDelayMs()`,使 Model 不依赖具体算法。 | | `SensorSample` | `ambientbrightnesspolicy.h` | 策略输入,包含 lux、单调时间戳和样本来源。 | | `Recommendation` | `ambientbrightnesspolicy.h` | 策略输出,包含 raw、fast、slow、stable lux 和最终亮度,便于状态更新、日志及调试。 | diff --git a/src/plugin-qt/ambient-brightness/ambientbrightnessmodel.cpp b/src/plugin-qt/ambient-brightness/ambientbrightnessmodel.cpp index 3c005317..a310c876 100644 --- a/src/plugin-qt/ambient-brightness/ambientbrightnessmodel.cpp +++ b/src/plugin-qt/ambient-brightness/ambientbrightnessmodel.cpp @@ -19,7 +19,6 @@ void AmbientBrightnessModel::waitForSample() { m_policy->reset(); m_haveRecommendation = false; - setSupported(true); setState(QStringLiteral("WaitingForSample")); } @@ -27,7 +26,6 @@ void AmbientBrightnessModel::makeUnavailable() { m_policy->reset(); m_haveRecommendation = false; - setSupported(false); setState(QStringLiteral("Unavailable")); } @@ -35,7 +33,6 @@ void AmbientBrightnessModel::setDisabled() { m_policy->reset(); m_haveRecommendation = false; - setSupported(true); setState(QStringLiteral("Disabled")); } @@ -88,7 +85,11 @@ void AmbientBrightnessModel::setPolicy(std::unique_ptr m_policy = std::move(policy); m_policy->reset(); m_haveRecommendation = false; - setState(m_supported ? QStringLiteral("WaitingForSample") : QStringLiteral("Unavailable")); + // 重建策略只影响采样阶段的状态,不能把 Disabled/Unavailable 顶成 WaitingForSample。 + if (m_state == QLatin1String("Disabled") || m_state == QLatin1String("Unavailable")) + return; + setState(m_supported ? QStringLiteral("WaitingForSample") + : QStringLiteral("Unavailable")); } void AmbientBrightnessModel::setSupported(bool value) diff --git a/src/plugin-qt/ambient-brightness/ambientbrightnessmodel.h b/src/plugin-qt/ambient-brightness/ambientbrightnessmodel.h index 7358f941..da9f7248 100644 --- a/src/plugin-qt/ambient-brightness/ambientbrightnessmodel.h +++ b/src/plugin-qt/ambient-brightness/ambientbrightnessmodel.h @@ -22,7 +22,7 @@ class AmbientBrightnessModel : public QObject public: explicit AmbientBrightnessModel(std::unique_ptr policy, QObject *parent = nullptr); - + /// 本机是否具备环境光传感器(硬件能力)。与开关是否开启、当前是否在采样无关。 bool supported() const { return m_supported; } QString state() const { return m_state; } @@ -40,13 +40,16 @@ class AmbientBrightnessModel : public QObject void tick(double monotonicTimestampMs); void setPolicy(std::unique_ptr policy); + /// 由 Service 依据能力探测结果写入。关闭开关或暂停采样不得清掉它; + /// 只有确认本机没有可用传感器时才置为 false。 + void setSupported(bool value); + Q_SIGNALS: void supportedChanged(bool value); void stateChanged(const QString &value); void recommendedBrightnessChanged(double value); private: - void setSupported(bool value); void setState(const QString &value); std::unique_ptr m_policy; diff --git a/src/plugin-qt/ambient-brightness/ambientbrightnessservice.cpp b/src/plugin-qt/ambient-brightness/ambientbrightnessservice.cpp index 0473563d..e090b666 100644 --- a/src/plugin-qt/ambient-brightness/ambientbrightnessservice.cpp +++ b/src/plugin-qt/ambient-brightness/ambientbrightnessservice.cpp @@ -10,6 +10,8 @@ #include #include #include +#include +#include #include #include #include @@ -54,6 +56,22 @@ bool variantToValidLux(const QVariant &value, double *lux) return true; } +/// 判定 LookupAmbientLightSensor 的异步回复是否代表"本机有可用光感"。 +bool sensorLookupSucceeded(const QDBusPendingReply &reply) +{ + if (!reply.isError()) + return !reply.value().path().isEmpty(); + + // 服务不存在(未注册)时视为不支持;方法缺失等其它错误回退为支持, + // 由 connectSensor() 的 HasAmbientLight / 单位校验做最终判定。 + const QString errorName = reply.error().name(); + if (errorName == QLatin1String("org.freedesktop.DBus.Error.ServiceUnknown") + || errorName == QLatin1String("org.freedesktop.DBus.Error.NameHasNoOwner")) { + return false; + } + return true; +} + } // namespace AmbientBrightnessService::AmbientBrightnessService(QDBusConnection connection, QObject *parent) @@ -105,19 +123,25 @@ bool AmbientBrightnessService::initialize() connect(m_watcher, &QDBusServiceWatcher::serviceUnregistered, this, &AmbientBrightnessService::onSensorServiceUnregistered); - m_runtimeReady = true; - refreshSensorConnection(); + start(); return true; } +void AmbientBrightnessService::start() +{ + m_runtimeReady = true; + refreshSensorCapability(); +} + void AmbientBrightnessService::onSensorServiceRegistered() { - refreshSensorConnection(); + refreshSensorCapability(); } void AmbientBrightnessService::onSensorServiceUnregistered() { - disconnectSensor(); + // 传感器服务退出后需要重新探测:Supported 必须随之变为 false 并对外广播。 + refreshSensorCapability(); } void AmbientBrightnessService::onLidClosed() @@ -194,18 +218,10 @@ void AmbientBrightnessService::onAutomaticBrightnessEnabledChanged(bool enabled) m_lifecycle.enabled = enabled; Q_EMIT enabledChanged(enabled); publishPropertyChange(QStringLiteral("Enabled"), enabled); - if (!enabled) { - stopInitialSampleWait(); - if (m_evaluationTimer) - m_evaluationTimer->stop(); - if (m_sensor && m_claimed) - m_sensor->call(QStringLiteral("ReleaseLight")); - m_claimed = false; - m_haveSample = false; - m_model.setDisabled(); - } else { - refreshSensorConnection(); - } + if (!enabled) + stopSampling(); + else + refreshSensorCapability(); } void AmbientBrightnessService::Enable(bool active) @@ -218,17 +234,9 @@ void AmbientBrightnessService::Enable(bool active) m_config->setValue(QString::fromLatin1(kAmbientLightAdjustBrightnessKey), active); if (!active) { - // 关闭:释放传感器但保留 Supported=true,状态变为 Disabled - stopInitialSampleWait(); - if (m_evaluationTimer) - m_evaluationTimer->stop(); - if (m_sensor && m_claimed) - m_sensor->call(QStringLiteral("ReleaseLight")); - m_claimed = false; - m_haveSample = false; - m_model.setDisabled(); + stopSampling(); } else { - refreshSensorConnection(); + refreshSensorCapability(); } } @@ -271,7 +279,7 @@ void AmbientBrightnessService::connectSensor() qCWarning(logAmbientBrightness) << "sensor interface invalid"; delete m_sensor; m_sensor = nullptr; - m_model.makeUnavailable(); + m_model.setSupported(false); return; } @@ -282,7 +290,7 @@ void AmbientBrightnessService::connectSensor() << "unit=" << unit; delete m_sensor; m_sensor = nullptr; - m_model.makeUnavailable(); + m_model.setSupported(false); return; } @@ -352,7 +360,7 @@ void AmbientBrightnessService::disconnectSensor() if (m_evaluationTimer) m_evaluationTimer->stop(); m_haveSample = false; - m_model.makeUnavailable(); + // 采样状态由 Supported 与 State 分别表达,这里只负责断开信号与释放 Claim。 } void AmbientBrightnessService::initRuntimeControl() @@ -451,33 +459,83 @@ void AmbientBrightnessService::onSessionPropertiesChanged( onSessionActiveChanged(it->toBool()); } +void AmbientBrightnessService::refreshSensorCapability() +{ + // 能力探测完全异步:refreshSensorConnection() 会在合盖、休眠、会话切换、 + // 开关切换等热路径上被调用,任何同步 D-Bus 往返都可能卡住主线程。 + QDBusPendingCall call = QDBusConnection::systemBus().asyncCall( + QDBusMessage::createMethodCall(QString::fromLatin1(kSensorService), + QString::fromLatin1(kSensorPath), + QString::fromLatin1(kSensorInterface), + QStringLiteral("LookupAmbientLightSensor"))); + auto *watcher = new QDBusPendingCallWatcher(call, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, + [this](QDBusPendingCallWatcher *self) { + self->deleteLater(); + handleSensorPresentChanged(sensorLookupSucceeded(*self)); + }); +} + +void AmbientBrightnessService::handleSensorPresentChanged(bool present) +{ + m_present = present; + m_model.setSupported(present); + + if (!present) { + if (m_sensor || m_claimed) + disconnectSensor(); + m_model.makeUnavailable(); + return; + } + if (!m_lifecycle.shouldRun()) { + // 硬件存在但不该采样:Supported 保持 true,只同步 State。 + if (m_sensor || m_claimed) + disconnectSensor(); + m_model.setDisabled(); + return; + } + // 直接启动,不经 refreshSensorConnection(),避免探测→刷新→探测的自我循环。 + if (!m_claimed) + connectSensor(); +} + +void AmbientBrightnessService::stopSampling() +{ + // 只停采样并同步 State;Supported 仅由能力探测改写,关闭开关不得把它清成 false。 + disconnectSensor(); + if (m_model.supported()) + m_model.setDisabled(); + else + m_model.makeUnavailable(); +} + void AmbientBrightnessService::refreshSensorConnection() { if (!m_runtimeReady) return; + // 只读缓存的能力结论;探测由 initialize/服务上下线/开关切换触发, + // 这里绝不同步访问 D-Bus,也不会再次触发探测(否则会自激)。 + + if (!m_present) { + if (m_sensor || m_claimed) + disconnectSensor(); + m_model.makeUnavailable(); + return; + } + + // 硬件存在但不该采样(用户关闭、合盖、休眠、会话非前台):Supported 保持 true。 if (!m_lifecycle.shouldRun()) { if (m_sensor || m_claimed) disconnectSensor(); - else - m_model.makeUnavailable(); + m_model.setDisabled(); return; } if (m_claimed) return; - auto *interface = QDBusConnection::systemBus().interface(); - if (!interface) { - qCWarning(logAmbientBrightness) << "no system bus interface; sensor unavailable"; - m_model.makeUnavailable(); - return; - } - const auto registered = interface->isServiceRegistered(QString::fromLatin1(kSensorService)); - if (registered.isValid() && registered.value()) - connectSensor(); - else - m_model.makeUnavailable(); + connectSensor(); } void AmbientBrightnessService::startInitialSampleTimeout() @@ -581,11 +639,15 @@ void AmbientBrightnessService::initAlgorithmConfig() return; } - // 读取光感开关初始值 + // 冷启动时开关状态只从 DConfig 读出,必须与运行中切换一样对外广播一次。 const QVariant enabled = m_config->value( QString::fromLatin1(kAmbientLightAdjustBrightnessKey)); - if (enabled.isValid() && enabled.canConvert()) + if (enabled.isValid() && enabled.canConvert() + && m_lifecycle.enabled != enabled.toBool()) { m_lifecycle.enabled = enabled.toBool(); + Q_EMIT enabledChanged(m_lifecycle.enabled); + publishPropertyChange(QStringLiteral("Enabled"), m_lifecycle.enabled); + } rebuildCurrentPolicy(); connect(m_config, &Dtk::Core::DConfig::valueChanged, this, [this](const QString &key) { diff --git a/src/plugin-qt/ambient-brightness/ambientbrightnessservice.h b/src/plugin-qt/ambient-brightness/ambientbrightnessservice.h index 013521d2..d986af99 100644 --- a/src/plugin-qt/ambient-brightness/ambientbrightnessservice.h +++ b/src/plugin-qt/ambient-brightness/ambientbrightnessservice.h @@ -52,6 +52,14 @@ public Q_SLOTS: void enabledChanged(bool value); void recommendedBrightnessChanged(double value); +public: + // 刻意声明为普通方法而非槽:ExportAllSlots 会把槽导出到 D-Bus, + // 这两个是内部/测试入口,不得出现在 org.deepin.dde.AmbientBrightness1 上。 + /// 启动运行时逻辑。与 initialize() 分开,便于测试在没有系统总线时构造服务。 + void start(); + /// 异步能力探测的回调入口。 + void handleSensorPresentChanged(bool present); + private Q_SLOTS: void onSensorServiceRegistered(); void onSensorServiceUnregistered(); @@ -68,7 +76,9 @@ private Q_SLOTS: const QStringList &invalidated); private: + void stopSampling(); void connectSensor(); + void refreshSensorCapability(); void disconnectSensor(); void initRuntimeControl(); void initLogin1Session(); @@ -85,6 +95,9 @@ private Q_SLOTS: void initAlgorithmConfig(); void rebuildCurrentPolicy(); + /// 最近一次异步能力探测的结果。只在 handleSensorPresentChanged() 里写入。 + bool m_present = false; + QDBusConnection m_connection; QDBusInterface *m_sensor = nullptr; QDBusServiceWatcher *m_watcher = nullptr; diff --git a/src/plugin-qt/ambient-brightness/tests/CMakeLists.txt b/src/plugin-qt/ambient-brightness/tests/CMakeLists.txt index 86a0097f..f3619567 100644 --- a/src/plugin-qt/ambient-brightness/tests/CMakeLists.txt +++ b/src/plugin-qt/ambient-brightness/tests/CMakeLists.txt @@ -23,3 +23,24 @@ target_link_libraries(tst-ambientbrightnesspolicy PRIVATE ) add_test(NAME ambient-brightness-policy COMMAND tst-ambientbrightnesspolicy) + +add_executable(tst-ambientbrightnesscapability + tst_ambientbrightnesscapability.cpp + ../ambientbrightnessservice.cpp + ../ambientbrightnesspolicyfactory.cpp + ../ambientbrightnessmodel.cpp + ../ambientbrightnesslogging.cpp + ../brightnesscurve.cpp + ../continuous/continuousambientlightpolicy.cpp +) + +target_include_directories(tst-ambientbrightnesscapability PRIVATE ..) + +target_link_libraries(tst-ambientbrightnesscapability PRIVATE + Qt6::Core + Qt6::DBus + Dtk6::Core + Qt6::Test +) + +add_test(NAME ambient-brightness-capability COMMAND tst-ambientbrightnesscapability) diff --git a/src/plugin-qt/ambient-brightness/tests/tst_ambientbrightnesscapability.cpp b/src/plugin-qt/ambient-brightness/tests/tst_ambientbrightnesscapability.cpp new file mode 100644 index 00000000..08dafc3e --- /dev/null +++ b/src/plugin-qt/ambient-brightness/tests/tst_ambientbrightnesscapability.cpp @@ -0,0 +1,168 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: LGPL-3.0-or-later +// +// Supported 是"本机是否具备环境光传感器"的能力事实,与开关、盖子、休眠、 +// 会话状态无关。 +// +// 本用例不启动 D-Bus 守护进程,也不访问宿主机真实总线:服务状态机由 +// handleSensorPresentChanged() 等公开入口直接驱动,能力结论等价于异步探测 +// 回调的结果;私有连接与进程级 DBUS_SYSTEM_BUS_ADDRESS 都指向一个不可达 +// 地址。因此构建沙箱里没有 dbus-daemon 也能由 ctest 运行。 +// +// 不覆盖(需要真实总线,属集成范畴):LookupAmbientLightSensor 的回复解析、 +// ClaimLight/ReleaseLight 调用、LightLevelUnit 单位校验、WaitingForSample→Active。 + +#include "ambientbrightnessservice.h" + +#include +#include + +#include + +using namespace dde::ambient_brightness; + +namespace { + +/// 本用例全程使用这个不可达地址:私有连接指向它,进程级 DBUS_SYSTEM_BUS_ADDRESS +/// 也指向它,因此连析构路径里的静态 systemBus() 也不会去连宿主机真实总线。 +constexpr auto kOfflineBusAddress = "unix:path=/tmp/tst-ambient-brightness-offline"; + +/// 服务构造函数需要一个 QDBusConnection;本用例不依赖总线,给一个连接不上的 +/// 地址即可,任何意外的总线访问都只会失败。 +QDBusConnection offlineConnection() +{ + return QDBusConnection::connectToBus(QString::fromLatin1(kOfflineBusAddress), + QStringLiteral("tst-ambient-brightness")); +} + +} // namespace + +class AmbientBrightnessCapabilityTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void supportedFollowsHardwareNotSwitch(); + void supportedTrueWhileLidClosedKeepsSwitchOn(); + void stopSamplingKeepsSupported(); + void probeAbsentReportsUnavailable(); + void supportedTracksHardwareGoingOffline(); + void staysUnavailableAfterSamplingStop(); +}; + +void AmbientBrightnessCapabilityTest::initTestCase() +{ + QLoggingCategory::setFilterRules(QStringLiteral("dde.ambientbrightness=false")); + // 必须在第一次 QDBusConnection::systemBus() 之前设置(Qt 会缓存该连接): + // 否则服务析构路径里的静态 systemBus().disconnect() 会连上宿主机真实总线。 + qputenv("DBUS_SYSTEM_BUS_ADDRESS", kOfflineBusAddress); + // 前提断言:未探测时能力为"未知"(Supported=false、State=Unavailable), + // 且开关默认开启;本文件多个用例依赖这两个前提。 + AmbientBrightnessService service(offlineConnection()); + QVERIFY(!service.supported()); + QCOMPARE(service.state(), QStringLiteral("Unavailable")); + QVERIFY(service.enabled()); +} + +// 冷启动前就把开关置为关闭,随后探测到硬件:Supported 必须为 true —— 这正是 +// 原先 Supported 被误报为 false 的场景。 +void AmbientBrightnessCapabilityTest::supportedFollowsHardwareNotSwitch() +{ + AmbientBrightnessService service(offlineConnection()); + QSignalSpy supportedSpy(&service, &AmbientBrightnessService::supportedChanged); + QVERIFY(QMetaObject::invokeMethod(&service, "onAutomaticBrightnessEnabledChanged", + Qt::DirectConnection, Q_ARG(bool, false))); + QVERIFY(!service.enabled()); + + service.handleSensorPresentChanged(true); + + QCOMPARE(supportedSpy.count(), 1); + QVERIFY(service.supported()); + QVERIFY(!service.enabled()); + QCOMPARE(service.state(), QStringLiteral("Disabled")); +} + +// 开关保持开启、盖子合上(硬件存在但不该采样):Supported 依然为 true, +// 证明它来自硬件探测而不是开关状态。 +void AmbientBrightnessCapabilityTest::supportedTrueWhileLidClosedKeepsSwitchOn() +{ + AmbientBrightnessService service(offlineConnection()); + QSignalSpy supportedSpy(&service, &AmbientBrightnessService::supportedChanged); + QVERIFY(QMetaObject::invokeMethod(&service, "onLidClosed", Qt::DirectConnection)); + + service.handleSensorPresentChanged(true); + + QCOMPARE(supportedSpy.count(), 1); + QVERIFY(service.supported()); + QVERIFY(service.enabled()); + QCOMPARE(service.state(), QStringLiteral("Disabled")); +} + +// 运行中关闭开关(→ stopSampling):只停止采样,Supported 不得被改写。 +void AmbientBrightnessCapabilityTest::stopSamplingKeepsSupported() +{ + AmbientBrightnessService service(offlineConnection()); + QSignalSpy supportedSpy(&service, &AmbientBrightnessService::supportedChanged); + QVERIFY(QMetaObject::invokeMethod(&service, "onLidClosed", Qt::DirectConnection)); + service.handleSensorPresentChanged(true); + QVERIFY(service.supported()); + + service.Enable(false); + + QVERIFY(!service.enabled()); + QVERIFY(service.supported()); + QCOMPARE(service.state(), QStringLiteral("Disabled")); + QCOMPARE(supportedSpy.count(), 1); +} + +void AmbientBrightnessCapabilityTest::probeAbsentReportsUnavailable() +{ + AmbientBrightnessService service(offlineConnection()); + QSignalSpy supportedSpy(&service, &AmbientBrightnessService::supportedChanged); + + service.handleSensorPresentChanged(false); + + QVERIFY(!service.supported()); + QCOMPARE(service.state(), QStringLiteral("Unavailable")); + // 本来就是 false:不得发出多余的 supportedChanged。 + QCOMPARE(supportedSpy.count(), 0); +} + +void AmbientBrightnessCapabilityTest::supportedTracksHardwareGoingOffline() +{ + AmbientBrightnessService service(offlineConnection()); + QSignalSpy supportedSpy(&service, &AmbientBrightnessService::supportedChanged); + QVERIFY(QMetaObject::invokeMethod(&service, "onLidClosed", Qt::DirectConnection)); + + service.handleSensorPresentChanged(true); + QVERIFY(service.supported()); + QCOMPARE(supportedSpy.count(), 1); + + // 传感器服务退出:Supported 随之变 false 并对外广播。 + service.handleSensorPresentChanged(false); + + QCOMPARE(supportedSpy.count(), 2); + QCOMPARE(supportedSpy.at(1).at(0).toBool(), false); + QVERIFY(!service.supported()); + QCOMPARE(service.state(), QStringLiteral("Unavailable")); +} + +void AmbientBrightnessCapabilityTest::staysUnavailableAfterSamplingStop() +{ + AmbientBrightnessService service(offlineConnection()); + QSignalSpy supportedSpy(&service, &AmbientBrightnessService::supportedChanged); + service.handleSensorPresentChanged(false); + QVERIFY(!service.supported()); + + service.Enable(false); + + QVERIFY(!service.enabled()); + QVERIFY(!service.supported()); + QCOMPARE(service.state(), QStringLiteral("Unavailable")); + QCOMPARE(supportedSpy.count(), 0); +} + +QTEST_GUILESS_MAIN(AmbientBrightnessCapabilityTest) + +#include "tst_ambientbrightnesscapability.moc" From 2966685ad5abd998cbb495bc3aee3786c63291bd Mon Sep 17 00:00:00 2001 From: fuleyi Date: Tue, 13 Oct 2026 20:14:29 +0800 Subject: [PATCH 2/2] fix: bound sensor calls and release light asynchronously MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Give the synchronous sensor calls an explicit 3 s cap: ClaimLight, the property reads, and the gate taken before constructing QDBusInterface, whose synchronous introspection no interface timeout can bound. 2. Send ReleaseLight asynchronously: the stop-sampling path no longer waits for a reply, which it never used. 3. A sensor that does not answer now only moves State to Unavailable, leaving Supported to the capability probe, and the next lifecycle refresh retries. Log: An unresponsive iio-sensor-proxy froze ambient brightness for up to 25 seconds; the worst case is now 3 s and light release no longer waits Influence: 1. With an ambient light sensor present, toggle the switch repeatedly and verify Supported stays true while State goes Disabled and back to WaitingForSample/Active. 2. Stop iio-sensor-proxy or point DBUS_SYSTEM_BUS_ADDRESS at an unreachable address; verify Enable() and the properties still answer, State is Unavailable, and nothing freezes for 25 seconds. 3. Bring the sensor service back and verify the next lid/sleep/session refresh claims the sensor again and brightness recommendations resume. fix: 传感器调用限时并异步释放光线 1. 给同步的传感器调用加 3 秒上限:ClaimLight、属性读取,以及构造 QDBusInterface 之前的那道闸门(构造期的同步内省不受接口超时约束)。 2. ReleaseLight 改为异步发出:停止采样路径不再等回复,它本来就不用返回值。 3. 传感器不应答时只把 State 置为 Unavailable,Supported 仍归能力探测改写, 下一次生命周期刷新会自动重试。 Log: 光感服务无响应时,自动亮度不再把服务卡住 25 秒;最坏 3 秒, 且释放光线不再等待 Influence: 1. 在有光感的机器上反复开关自动亮度,确认 Supported 保持 true,State 在 Disabled 与 WaitingForSample/Active 之间正确切换。 2. 停掉 iio-sensor-proxy(或把 DBUS_SYSTEM_BUS_ADDRESS 指向不可达地址), 确认 Enable() 与属性读取仍有响应、State 为 Unavailable,且不会卡 25 秒。 3. 再把传感器服务拉起来,确认下一次合盖/休眠/会话刷分会重新 claim 并恢复 亮度推荐。 PMS: BUG-378895 --- .../ambientbrightnessservice.cpp | 50 ++++++++++++++++--- .../tests/tst_ambientbrightnesscapability.cpp | 15 ++++++ 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/src/plugin-qt/ambient-brightness/ambientbrightnessservice.cpp b/src/plugin-qt/ambient-brightness/ambientbrightnessservice.cpp index e090b666..97f92e01 100644 --- a/src/plugin-qt/ambient-brightness/ambientbrightnessservice.cpp +++ b/src/plugin-qt/ambient-brightness/ambientbrightnessservice.cpp @@ -45,6 +45,9 @@ constexpr auto kBrightenDebounceMsKey = "brightenDebounceMs"; constexpr auto kDarkenDebounceMsKey = "darkenDebounceMs"; constexpr int kInitialSampleTimeoutMs = 2000; constexpr int kRuntimeRecoveryRefreshDelayMs = 1000; +/// 与 iio-sensor-proxy 之间单次同步调用的上限。对端正常时是百微秒级,这个值只为兜住 +/// "进程还在但不回消息"这一种情况:不让主线程落到 Qt 默认的 25 秒超时上。 +constexpr int kSensorCallTimeoutMs = 3000; bool variantToValidLux(const QVariant &value, double *lux) { @@ -271,10 +274,28 @@ void AmbientBrightnessService::connectSensor() { disconnectSensor(); auto systemBus = QDBusConnection::systemBus(); + + // QDBusInterface 构造时会同步内省远端,而且 setTimeout() 管不到那一步:对端卡死会落回 + // 默认 25 秒。先用带显式超时的属性读取当闸门,确认对端会应答再构造接口。 + // 读不通只改 State(Supported 由能力探测改写),下一次生命周期刷新会自动重试。 + QDBusMessage gateRequest = QDBusMessage::createMethodCall( + QString::fromLatin1(kSensorService), QString::fromLatin1(kSensorPath), + QString::fromLatin1(kPropertiesInterface), QStringLiteral("Get")); + gateRequest << QString::fromLatin1(kSensorInterface) << QStringLiteral("HasAmbientLight"); + const QDBusMessage gateReply = systemBus.call(gateRequest, QDBus::Block, kSensorCallTimeoutMs); + if (gateReply.type() != QDBusMessage::ReplyMessage) { + qCWarning(logAmbientBrightness) << "sensor not responding:" << gateReply.errorName() + << gateReply.errorMessage(); + m_model.makeUnavailable(); + return; + } + m_sensor = new QDBusInterface(QString::fromLatin1(kSensorService), QString::fromLatin1(kSensorPath), QString::fromLatin1(kSensorInterface), systemBus, this); + // 这个接口上的属性读取与 ClaimLight 同样封顶。 + m_sensor->setTimeout(kSensorCallTimeoutMs); if (!m_sensor->isValid()) { qCWarning(logAmbientBrightness) << "sensor interface invalid"; delete m_sensor; @@ -343,15 +364,30 @@ void AmbientBrightnessService::connectSensor() void AmbientBrightnessService::disconnectSensor() { + auto systemBus = QDBusConnection::systemBus(); if (m_sensor || m_claimed) qCDebug(logAmbientBrightness) << "disconnecting sensor: claimed=" << m_claimed; - QDBusConnection::systemBus().disconnect(QString::fromLatin1(kSensorService), - QString::fromLatin1(kSensorPath), - QString::fromLatin1(kPropertiesInterface), - QStringLiteral("PropertiesChanged"), this, - SLOT(onPropertiesChanged(QString,QVariantMap,QStringList))); - if (m_sensor && m_claimed) - m_sensor->call(QStringLiteral("ReleaseLight")); + systemBus.disconnect(QString::fromLatin1(kSensorService), + QString::fromLatin1(kSensorPath), + QString::fromLatin1(kPropertiesInterface), + QStringLiteral("PropertiesChanged"), this, + SLOT(onPropertiesChanged(QString,QVariantMap,QStringList))); + if (m_sensor && m_claimed) { + // 释放不需要结果(下面本来就直接把 m_claimed 置 false),所以异步发出即可: + // 停止采样路径不再等对端回话。走连接而不是 m_sensor,也不受接口生命周期影响。 + auto *watcher = new QDBusPendingCallWatcher( + systemBus.asyncCall(QDBusMessage::createMethodCall( + QString::fromLatin1(kSensorService), QString::fromLatin1(kSensorPath), + QString::fromLatin1(kSensorInterface), QStringLiteral("ReleaseLight"))), + this); + connect(watcher, &QDBusPendingCallWatcher::finished, watcher, + [](QDBusPendingCallWatcher *self) { + if (self->isError()) + qCWarning(logAmbientBrightness) + << "ReleaseLight failed" << self->error().message(); + self->deleteLater(); + }); + } m_claimed = false; m_claimInProgress = false; stopInitialSampleWait(); diff --git a/src/plugin-qt/ambient-brightness/tests/tst_ambientbrightnesscapability.cpp b/src/plugin-qt/ambient-brightness/tests/tst_ambientbrightnesscapability.cpp index 08dafc3e..d767f830 100644 --- a/src/plugin-qt/ambient-brightness/tests/tst_ambientbrightnesscapability.cpp +++ b/src/plugin-qt/ambient-brightness/tests/tst_ambientbrightnesscapability.cpp @@ -49,6 +49,7 @@ private Q_SLOTS: void probeAbsentReportsUnavailable(); void supportedTracksHardwareGoingOffline(); void staysUnavailableAfterSamplingStop(); + void unresponsiveSensorKeepsSupported(); }; void AmbientBrightnessCapabilityTest::initTestCase() @@ -163,6 +164,20 @@ void AmbientBrightnessCapabilityTest::staysUnavailableAfterSamplingStop() QCOMPARE(supportedSpy.count(), 0); } +// 传感器不应答(总线不可达)时,采样路径只能改 State:Supported 归能力探测独占, +// 不能因为"连不上传感器"就被清成 false,否则开关一开一关就会误报"本机没有光感"。 +void AmbientBrightnessCapabilityTest::unresponsiveSensorKeepsSupported() +{ + AmbientBrightnessService service(offlineConnection()); + QSignalSpy supportedSpy(&service, &AmbientBrightnessService::supportedChanged); + + service.handleSensorPresentChanged(true); // shouldRun() 为真 → 同步尝试连接传感器 + + QVERIFY(service.supported()); + QCOMPARE(service.state(), QStringLiteral("Unavailable")); + QCOMPARE(supportedSpy.count(), 1); // 只被探测改写这一次 +} + QTEST_GUILESS_MAIN(AmbientBrightnessCapabilityTest) #include "tst_ambientbrightnesscapability.moc"