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..97f92e01 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 @@ -43,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) { @@ -54,6 +59,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 +126,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 +221,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 +237,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(); } } @@ -263,15 +274,33 @@ 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; m_sensor = nullptr; - m_model.makeUnavailable(); + m_model.setSupported(false); return; } @@ -282,7 +311,7 @@ void AmbientBrightnessService::connectSensor() << "unit=" << unit; delete m_sensor; m_sensor = nullptr; - m_model.makeUnavailable(); + m_model.setSupported(false); return; } @@ -335,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(); @@ -352,7 +396,7 @@ void AmbientBrightnessService::disconnectSensor() if (m_evaluationTimer) m_evaluationTimer->stop(); m_haveSample = false; - m_model.makeUnavailable(); + // 采样状态由 Supported 与 State 分别表达,这里只负责断开信号与释放 Claim。 } void AmbientBrightnessService::initRuntimeControl() @@ -451,33 +495,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 +675,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..d767f830 --- /dev/null +++ b/src/plugin-qt/ambient-brightness/tests/tst_ambientbrightnesscapability.cpp @@ -0,0 +1,183 @@ +// 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 unresponsiveSensorKeepsSupported(); +}; + +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); +} + +// 传感器不应答(总线不可达)时,采样路径只能改 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"