refactor: simplify the clients and fix MPRIS, accounts, Bluetooth, VPN and display bugs

pull/18/head
reionwong 4 weeks ago
parent 7495101293
commit fba47b0bbf

@ -33,18 +33,13 @@ UserAccount *AccountsManagerPrivate::account(const QString &path, quint64 reques
if (!user) {
user = new UserAccount(path, service->connection(), owner);
accounts.insert(path, user);
QObject::connect(user, &UserAccount::userNameChanged, owner,
[this, user]() {
const QString path = pathFor(user);
if (!path.isEmpty())
names.insert(path, user->userName());
});
QObject::connect(user, &UserAccount::accountChanged, owner,
[this, user]() {
const QString path = pathFor(user);
if (!path.isEmpty())
names.insert(path, user->userName());
});
const auto updateName = [this, user]() {
const QString path = pathFor(user);
if (!path.isEmpty())
names.insert(path, user->userName());
};
QObject::connect(user, &UserAccount::userNameChanged, owner, updateName);
QObject::connect(user, &UserAccount::accountChanged, owner, updateName);
QObject::connect(user, &UserAccount::userIdChanged, owner,
[this, user]() {
const QString previousPath = pathFor(user);
@ -58,8 +53,8 @@ UserAccount *AccountsManagerPrivate::account(const QString &path, quint64 reques
accounts.remove(previousPath);
remove(nextPath);
accounts.insert(nextPath, user);
names.insert(nextPath, user->userName().isEmpty()
? previousName : user->userName());
const QString userName = user->userName();
names.insert(nextPath, userName.isEmpty() ? previousName : userName);
});
}
const QString currentName = user->userName();
@ -70,6 +65,17 @@ UserAccount *AccountsManagerPrivate::account(const QString &path, quint64 reques
return user;
}
UserAccountList AccountsManagerPrivate::accountsFor(const QList<QDBusObjectPath> &paths,
quint64 requestSerial)
{
UserAccountList result;
for (const QDBusObjectPath &path : paths) {
if (UserAccount *user = account(path.path(), requestSerial))
result.append(user);
}
return result;
}
QString AccountsManagerPrivate::pathFor(UserAccount *user) const
{
for (auto it = accounts.cbegin(); it != accounts.cend(); ++it) {
@ -219,12 +225,7 @@ void AccountsManager::listCachedUsers()
return;
}
UserAccountList result;
for (const QDBusObjectPath &path : reply.value()) {
if (UserAccount *user = d_ptr->account(path.path(), started))
result.append(user);
}
Q_EMIT listCachedUsersFinished(result);
Q_EMIT listCachedUsersFinished(d_ptr->accountsFor(reply.value(), started));
});
}
@ -238,12 +239,7 @@ UserAccountList AccountsManager::listCachedUsersSync()
return {};
}
UserAccountList result;
for (const QDBusObjectPath &path : reply.value()) {
if (UserAccount *user = d_ptr->account(path.path(), started))
result.append(user);
}
return result;
return d_ptr->accountsFor(reply.value(), started);
}
UserAccount *AccountsManager::cachedUser(const QString &userName) const
@ -313,11 +309,6 @@ bool AccountsManager::deleteUser(qlonglong uid, bool removeFiles)
return true;
}
bool AccountsManager::deleteUser(UserAccount *account, bool removeFiles)
{
return account && deleteUser(account->userId(), removeFiles);
}
}
#include "moc_accountsmanager.cpp"

@ -30,7 +30,6 @@ public:
Q_INVOKABLE void createUser(const QString &userName, const QString &fullName,
UserAccount::AccountType accountType);
Q_INVOKABLE bool deleteUser(qlonglong uid, bool removeFiles);
bool deleteUser(UserAccount *account, bool removeFiles);
Q_SIGNALS:
void userCreated(QtAccountsService::UserAccount *account);

@ -16,6 +16,7 @@ public:
UserAccount *account(const QString &path, quint64 requestSerial,
const QString &name = QString());
UserAccountList accountsFor(const QList<QDBusObjectPath> &paths, quint64 requestSerial);
QString pathFor(UserAccount *user) const;
void remove(const QString &path);
void removedByService(const QString &path);

@ -13,6 +13,11 @@ using namespace QtAccountsService;
static const QString serviceName = QStringLiteral("org.freedesktop.Accounts");
static const QString managerPath = QStringLiteral("/org/freedesktop/Accounts");
static const QStringList userNames = {
QStringLiteral("alice"), QStringLiteral("bob"), QStringLiteral("carol"),
QStringLiteral("error"), QStringLiteral("delete-before-cache"),
QStringLiteral("delete-before-uncache"), QStringLiteral("created")
};
class TestUserAdaptor : public QDBusAbstractAdaptor
{
@ -134,13 +139,8 @@ Q_SIGNALS:
private:
static QDBusObjectPath userPath(const QString &name)
{
static const QStringList names = {
QStringLiteral("alice"), QStringLiteral("bob"), QStringLiteral("carol"),
QStringLiteral("error"), QStringLiteral("delete-before-cache"),
QStringLiteral("delete-before-uncache"), QStringLiteral("created")
};
return QDBusObjectPath(managerPath + QStringLiteral("/User")
+ QString::number(1001 + names.indexOf(name)));
+ QString::number(1001 + userNames.indexOf(name)));
}
};
@ -417,19 +417,12 @@ int main(int argc, char **argv)
|| !bus.registerObject(managerPath, &manager, QDBusConnection::ExportAdaptors))
return 1;
static const QStringList names = {
QStringLiteral("alice"), QStringLiteral("bob"), QStringLiteral("carol"),
QStringLiteral("error"), QStringLiteral("delete-before-cache"),
QStringLiteral("delete-before-uncache"), QStringLiteral("created")
};
QList<QObject *> users;
for (int i = 0; i < names.size(); ++i) {
for (int i = 0; i < userNames.size(); ++i) {
QObject *user = new QObject(&manager);
new TestUserAdaptor(user, names.at(i), 1001 + i);
new TestUserAdaptor(user, userNames.at(i), 1001 + i);
const QString path = managerPath + QStringLiteral("/User") + QString::number(1001 + i);
if (!bus.registerObject(path, user, QDBusConnection::ExportAdaptors))
return 1;
users.append(user);
}
return app.exec();
}

@ -51,16 +51,12 @@ void UserAccount::setObjectPath(const QString &path)
d_ptr->objectPath = path;
d_ptr->values.clear();
d_ptr->values.insert(QStringLiteral("Uid"), path.mid(userPathPrefix.size()).toLongLong());
d_ptr->values.insert(QStringLiteral("AccountType"), 0);
d_ptr->values.insert(QStringLiteral("Locked"), false);
d_ptr->values.insert(QStringLiteral("AutomaticLogin"), false);
d_ptr->values.insert(QStringLiteral("PasswordMode"), static_cast<int>(NonePasswordMode));
d_ptr->service = new OrgFreedesktopAccountsUserInterface(serviceName, path,
d_ptr->bus, this);
d_ptr->service->setInteractiveAuthorizationAllowed(true);
d_ptr->service->setTimeout(120000);
connect(d_ptr->service, &OrgFreedesktopAccountsUserInterface::Changed,
this, &UserAccount::handleAccountChanged);
this, &UserAccount::refresh);
notifyAll();
}
@ -84,11 +80,6 @@ void UserAccount::notifyAll()
Q_EMIT xsessionChanged();
}
void UserAccount::handleAccountChanged()
{
refresh();
}
void UserAccount::refresh()
{
QDBusInterface properties(serviceName, d_ptr->objectPath,
@ -147,7 +138,7 @@ void UserAccount::refresh()
};
bool changed = false;
bool displayNameChanged = false;
bool nameChanged = false;
for (const Change &entry : changes) {
const QString key = QString::fromLatin1(entry.property);
if (previous.value(key) == updated.value(key))
@ -157,19 +148,20 @@ void UserAccount::refresh()
if (key == QStringLiteral("Uid"))
Q_EMIT groupIdChanged();
if (key == QStringLiteral("UserName") || key == QStringLiteral("RealName"))
displayNameChanged = true;
nameChanged = true;
}
if (displayNameChanged)
Q_EMIT this->displayNameChanged();
if (nameChanged)
Q_EMIT displayNameChanged();
if (changed)
Q_EMIT accountChanged();
}
void UserAccount::watch(const QDBusPendingCall &call)
void UserAccount::watch(const QDBusPendingCall &call, void (UserAccount::*notify)(),
bool displayName)
{
auto *watcher = new QDBusPendingCallWatcher(call, this);
connect(watcher, &QDBusPendingCallWatcher::finished, this,
[this](QDBusPendingCallWatcher *finished) {
[this, notify, displayName](QDBusPendingCallWatcher *finished) {
QDBusPendingReply<> reply = *finished;
finished->deleteLater();
if (reply.isError()) {
@ -179,6 +171,12 @@ void UserAccount::watch(const QDBusPendingCall &call)
return;
}
refresh();
// The getters read the service, which holds the requested value only once Set has
// replied; refresh() stays quiet because notePendingChange() already recorded it.
if (notify)
Q_EMIT (this->*notify)();
if (displayName)
Q_EMIT displayNameChanged();
Q_EMIT accountChanged();
});
}
@ -229,7 +227,7 @@ void UserAccount::setAccountType(AccountType type)
{
if (accountType() == type)
return;
watch(d_ptr->service->SetAccountType(static_cast<int>(type)));
watch(d_ptr->service->SetAccountType(static_cast<int>(type)), &UserAccount::accountTypeChanged);
notePendingChange("AccountType", static_cast<int>(type), &UserAccount::accountTypeChanged);
}
@ -242,7 +240,7 @@ void UserAccount::setLocked(bool locked)
{
if (isLocked() == locked)
return;
watch(d_ptr->service->SetLocked(locked));
watch(d_ptr->service->SetLocked(locked), &UserAccount::lockedChanged);
notePendingChange("Locked", locked, &UserAccount::lockedChanged);
}
@ -255,7 +253,7 @@ void UserAccount::setAutomaticLogin(bool enabled)
{
if (automaticLogin() == enabled)
return;
watch(d_ptr->service->SetAutomaticLogin(enabled));
watch(d_ptr->service->SetAutomaticLogin(enabled), &UserAccount::automaticLoginChanged);
notePendingChange("AutomaticLogin", enabled, &UserAccount::automaticLoginChanged);
}
@ -278,7 +276,8 @@ void UserAccount::setPasswordMode(PasswordMode mode)
{
if (passwordMode() == mode)
return;
watch(d_ptr->service->SetPasswordMode(static_cast<int>(mode)));
watch(d_ptr->service->SetPasswordMode(static_cast<int>(mode)),
&UserAccount::passwordModeChanged);
notePendingChange("PasswordMode", static_cast<int>(mode), &UserAccount::passwordModeChanged);
}
@ -306,7 +305,7 @@ void UserAccount::setUserName(const QString &name)
{
if (userName() == name)
return;
watch(d_ptr->service->SetUserName(name));
watch(d_ptr->service->SetUserName(name), &UserAccount::userNameChanged, true);
notePendingChange("UserName", name, &UserAccount::userNameChanged, true);
}
@ -319,7 +318,7 @@ void UserAccount::setRealName(const QString &name)
{
if (realName() == name)
return;
watch(d_ptr->service->SetRealName(name));
watch(d_ptr->service->SetRealName(name), &UserAccount::realNameChanged, true);
notePendingChange("RealName", name, &UserAccount::realNameChanged, true);
}
@ -338,7 +337,7 @@ void UserAccount::setHomeDirectory(const QString &path)
{
if (homeDirectory() == path)
return;
watch(d_ptr->service->SetHomeDirectory(path));
watch(d_ptr->service->SetHomeDirectory(path), &UserAccount::homeDirectoryChanged);
notePendingChange("HomeDirectory", path, &UserAccount::homeDirectoryChanged);
}
@ -351,7 +350,7 @@ void UserAccount::setShell(const QString &path)
{
if (shell() == path)
return;
watch(d_ptr->service->SetShell(path));
watch(d_ptr->service->SetShell(path), &UserAccount::shellChanged);
notePendingChange("Shell", path, &UserAccount::shellChanged);
}
@ -365,7 +364,7 @@ void UserAccount::setIconFileName(const QString &path)
{
if (iconFileName() == path)
return;
watch(d_ptr->service->SetIconFile(path));
watch(d_ptr->service->SetIconFile(path), &UserAccount::iconFileNameChanged);
notePendingChange("IconFile", path, &UserAccount::iconFileNameChanged);
}
@ -378,7 +377,7 @@ void UserAccount::setEmail(const QString &address)
{
if (email() == address)
return;
watch(d_ptr->service->SetEmail(address));
watch(d_ptr->service->SetEmail(address), &UserAccount::emailChanged);
notePendingChange("Email", address, &UserAccount::emailChanged);
}
@ -391,7 +390,7 @@ void UserAccount::setLanguage(const QString &locale)
{
if (language() == locale)
return;
watch(d_ptr->service->SetLanguage(locale));
watch(d_ptr->service->SetLanguage(locale), &UserAccount::languageChanged);
notePendingChange("Language", locale, &UserAccount::languageChanged);
}
@ -404,7 +403,7 @@ void UserAccount::setLocation(const QString &place)
{
if (location() == place)
return;
watch(d_ptr->service->SetLocation(place));
watch(d_ptr->service->SetLocation(place), &UserAccount::locationChanged);
notePendingChange("Location", place, &UserAccount::locationChanged);
}
@ -417,7 +416,7 @@ void UserAccount::setXSession(const QString &session)
{
if (xsession() == session)
return;
watch(d_ptr->service->SetXSession(session));
watch(d_ptr->service->SetXSession(session), &UserAccount::xsessionChanged);
notePendingChange("XSession", session, &UserAccount::xsessionChanged);
}

@ -113,14 +113,12 @@ private:
void setObjectPath(const QString &path);
void notifyAll();
void refresh();
void watch(const QDBusPendingCall &call);
void watch(const QDBusPendingCall &call, void (UserAccount::*notify)() = nullptr,
bool displayName = false);
void notePendingChange(const char *property, const QVariant &value,
void (UserAccount::*notify)(), bool displayName = false);
UserAccountPrivate *const d_ptr;
private Q_SLOTS:
void handleAccountChanged();
};
using UserAccountList = QList<UserAccount *>;

@ -6,8 +6,7 @@
namespace QtAccountsService {
UsersModelPrivate::UsersModelPrivate(UsersModel *model, const QDBusConnection &bus)
: owner(model)
, manager(new AccountsManager(bus, model))
: manager(new AccountsManager(bus, model))
{
}

@ -17,9 +17,8 @@ struct UserRow
class UsersModelPrivate
{
public:
UsersModelPrivate(UsersModel *owner, const QDBusConnection &bus);
UsersModelPrivate(UsersModel *model, const QDBusConnection &bus);
UsersModel *owner;
AccountsManager *manager;
QVector<UserRow> rows;
};

@ -5,7 +5,7 @@ set(CUTEFISH_APPEARANCE_SOURCES
wallpaper.h
)
find_package(Qt6 REQUIRED COMPONENTS Core Gui Qml DBus)
find_package(Qt6 REQUIRED COMPONENTS Core Qml DBus)
include(GNUInstallDirs)
@ -20,7 +20,6 @@ target_include_directories(cutefish-framework-appearance
target_link_libraries(cutefish-framework-appearance PUBLIC
Qt6::Core
Qt6::Gui
Qt6::DBus
)

@ -34,27 +34,26 @@ void Appearance::init()
if (!m_interface->isValid())
return;
connect(m_interface, SIGNAL(darkModeChanged(bool)), this, SLOT(onDarkModeChanged(bool)));
connect(m_interface, SIGNAL(wallpaperChanged(QString)), this,
SLOT(onWallpaperChanged(QString)));
connect(m_interface, SIGNAL(darkModeChanged(bool)), this, SIGNAL(darkModeChanged()));
connect(m_interface, SIGNAL(wallpaperChanged(QString)), this, SIGNAL(wallpaperChanged()));
connect(m_interface, SIGNAL(userWallpapersChanged()), this, SIGNAL(userWallpapersChanged()));
connect(m_interface, SIGNAL(wallpaperFoldersChanged()), this,
SIGNAL(wallpaperFoldersChanged()));
connect(m_interface, SIGNAL(wallpaperFolderImagesChanged(QString)), this,
SIGNAL(wallpaperFolderImagesChanged(QString)));
connect(m_interface, SIGNAL(accentColorChanged(int)), this, SLOT(onAccentColorChanged(int)));
connect(m_interface, SIGNAL(accentColorChanged(int)), this, SIGNAL(accentColorIndexChanged()));
connect(m_interface, SIGNAL(darkModeDimsWallpaerChanged()), this,
SLOT(onDarkModeDimsWallpaperChanged()));
SIGNAL(dimsWallpaperChanged()));
connect(m_interface, SIGNAL(continuousCornersChanged(bool)), this,
SLOT(onContinuousCornersChanged(bool)));
connect(m_interface, SIGNAL(systemFontPointSizeChanged()), this,
SLOT(onFontPointSizeChanged()));
connect(m_interface, SIGNAL(systemFontChanged()), this, SLOT(onFontFamilyChanged()));
SIGNAL(fontPointSizeChanged()));
connect(m_interface, SIGNAL(systemFontChanged()), this, SIGNAL(fontFamilyChanged()));
connect(m_interface, SIGNAL(iconThemeChanged()), this, SIGNAL(iconThemeChanged()));
connect(m_interface, SIGNAL(backgroundTypeChanged()), this, SLOT(onBackgroundTypeChanged()));
connect(m_interface, SIGNAL(backgroundColorChanged()), this, SLOT(onBackgroundColorChanged()));
connect(m_interface, SIGNAL(backgroundTypeChanged()), this, SIGNAL(backgroundTypeChanged()));
connect(m_interface, SIGNAL(backgroundColorChanged()), this, SIGNAL(backgroundColorChanged()));
connect(m_interface, SIGNAL(backgroundVisibleChanged()), this,
SLOT(onBackgroundVisibleChanged()));
SIGNAL(backgroundVisibleChanged()));
emit darkModeChanged();
emit wallpaperChanged();
@ -77,12 +76,6 @@ QVariant Appearance::serviceProperty(const char *name) const
return m_interface && m_interface->isValid() ? m_interface->property(name) : QVariant();
}
void Appearance::callService(const char *method)
{
if (m_interface && m_interface->isValid())
m_interface->call(method);
}
void Appearance::callService(const char *method, const QVariant &value)
{
if (m_interface && m_interface->isValid())
@ -272,56 +265,8 @@ void Appearance::setFontRendering(bool antialias, const QString &hintStyle)
m_interface->call("setFontRendering", antialias, hintStyle);
}
void Appearance::onDarkModeChanged(bool darkMode)
{
Q_UNUSED(darkMode);
emit darkModeChanged();
}
void Appearance::onWallpaperChanged(const QString &path)
{
Q_UNUSED(path);
emit wallpaperChanged();
}
void Appearance::onAccentColorChanged(int accentColor)
{
Q_UNUSED(accentColor);
emit accentColorIndexChanged();
}
void Appearance::onContinuousCornersChanged(bool continuousCorners)
{
m_continuousCorners = continuousCorners;
emit continuousCornersChanged();
}
void Appearance::onDarkModeDimsWallpaperChanged()
{
emit dimsWallpaperChanged();
}
void Appearance::onFontPointSizeChanged()
{
emit fontPointSizeChanged();
}
void Appearance::onFontFamilyChanged()
{
emit fontFamilyChanged();
}
void Appearance::onBackgroundTypeChanged()
{
emit backgroundTypeChanged();
}
void Appearance::onBackgroundColorChanged()
{
emit backgroundColorChanged();
}
void Appearance::onBackgroundVisibleChanged()
{
emit backgroundVisibleChanged();
}

@ -105,20 +105,10 @@ signals:
private slots:
void init();
void onDarkModeChanged(bool darkMode);
void onWallpaperChanged(const QString &path);
void onAccentColorChanged(int accentColor);
void onContinuousCornersChanged(bool continuousCorners);
void onDarkModeDimsWallpaperChanged();
void onFontPointSizeChanged();
void onFontFamilyChanged();
void onBackgroundTypeChanged();
void onBackgroundColorChanged();
void onBackgroundVisibleChanged();
private:
QVariant serviceProperty(const char *name) const;
void callService(const char *method);
void callService(const char *method, const QVariant &value);
QDBusInterface *m_interface;

@ -1,6 +1,7 @@
#include "applicationregistry.h"
#include <QCollator>
#include <QCoreApplication>
#include <QDBusConnection>
#include <QDir>
#include <QDirIterator>
@ -63,10 +64,9 @@ public:
}
}
QPointer<ApplicationRegistry> registry = m_registry;
QMetaObject::invokeMethod(
m_registry,
[registry, results]() {
QCoreApplication::instance(),
[registry = m_registry, results]() {
if (registry)
registry->applyScan(results);
},
@ -74,7 +74,7 @@ public:
}
private:
ApplicationRegistry *m_registry;
QPointer<ApplicationRegistry> m_registry;
QString m_language;
};
@ -382,16 +382,13 @@ void ApplicationRegistry::onLanguageChanged()
void ApplicationRegistry::startScan()
{
if (m_scanInProgress) {
if (m_loading) {
m_scanQueued = true;
return;
}
m_scanInProgress = true;
if (!m_loading) {
m_loading = true;
emit loadingChanged();
}
m_loading = true;
emit loadingChanged();
m_language = DesktopEntry::displayLanguage();
QThreadPool::globalInstance()->start(new DesktopEntryScanner(this, m_language));
}
@ -400,7 +397,6 @@ void ApplicationRegistry::applyScan(const QList<DesktopEntryData> &results)
{
const bool wasLoading = m_loading;
m_loading = false;
m_scanInProgress = false;
QHash<QString, DesktopEntry *> oldEntries = m_entriesById;
QHash<QString, DesktopEntry *> newEntries;
@ -451,7 +447,9 @@ void ApplicationRegistry::applyScan(const QList<DesktopEntryData> &results)
void ApplicationRegistry::monitorPaths()
{
for (const QString &rootPath : desktopPaths()) {
const QStringList roots = desktopPaths();
QStringList ancestors;
for (const QString &rootPath : roots) {
QDir root(rootPath);
if (!root.exists()) {
QString parentPath = rootPath;
@ -461,8 +459,10 @@ void ApplicationRegistry::monitorPaths()
break;
parentPath = parent;
}
if (QDir(parentPath).exists())
if (QDir(parentPath).exists()) {
m_watcher->addPath(parentPath);
ancestors.append(parentPath);
}
continue;
}
@ -477,5 +477,21 @@ void ApplicationRegistry::monitorPaths()
while (files.hasNext())
m_watcher->addPath(files.next());
}
// An ancestor stands in for a missing root only until that root exists.
const QStringList watched = m_watcher->directories();
QStringList stale;
for (const QString &dir : watched) {
if (ancestors.contains(dir))
continue;
const bool underRoot = std::any_of(roots.cbegin(), roots.cend(),
[&dir](const QString &root) {
return dir == root || dir.startsWith(root + QLatin1Char('/'));
});
if (!underRoot)
stale.append(dir);
}
if (!stale.isEmpty())
m_watcher->removePaths(stale);
}

@ -90,6 +90,5 @@ private:
QList<DesktopEntry *> m_entries;
QString m_language;
bool m_loading = false;
bool m_scanInProgress = false;
bool m_scanQueued = false;
};

@ -37,6 +37,17 @@ QProcessEnvironment applicationEnvironment()
return environment;
}
template<typename... Args>
bool callBool(QDBusInterface *iface, const QString &method, const Args &...args)
{
if (!iface->isValid())
return false;
const QDBusReply<bool> reply = iface->call(method, args...);
return reply.isValid() && reply.value();
}
}
ApplicationRuntime *ApplicationRuntime::instance()
@ -120,24 +131,12 @@ bool ApplicationRuntime::quitApplication(const QString &appId)
if (appId.isEmpty())
return false;
QDBusInterface *iface = interface();
if (!iface->isValid())
return false;
const QDBusReply<bool> reply = iface->call(QStringLiteral("quitApplication"), appId);
return reply.isValid() && reply.value();
return callBool(interface(), QStringLiteral("quitApplication"), appId);
}
bool ApplicationRuntime::quitAll()
{
QDBusInterface *iface = interface();
if (!iface->isValid())
return false;
const QDBusReply<bool> reply = iface->call(QStringLiteral("quitAll"));
return reply.isValid() && reply.value();
return callBool(interface(), QStringLiteral("quitAll"));
}
bool ApplicationRuntime::quitByPid(quint32 pid)
@ -145,24 +144,12 @@ bool ApplicationRuntime::quitByPid(quint32 pid)
if (pid == 0)
return false;
QDBusInterface *iface = interface();
if (!iface->isValid())
return false;
const QDBusReply<bool> reply = iface->call(QStringLiteral("quitByPid"), pid);
return reply.isValid() && reply.value();
return callBool(interface(), QStringLiteral("quitByPid"), pid);
}
bool ApplicationRuntime::isRunning(const QString &appId)
{
QDBusInterface *iface = interface();
if (!iface->isValid())
return false;
const QDBusReply<bool> reply = iface->call(QStringLiteral("isRunning"), appId);
return reply.isValid() && reply.value();
return callBool(interface(), QStringLiteral("isRunning"), appId);
}
QStringList ApplicationRuntime::runningApplications()

@ -4,11 +4,9 @@
#include "applicationruntime.h"
#include "languageenvironment.h"
#include <QFile>
#include <QFileInfo>
#include <QLocale>
#include <QMap>
#include <QRegularExpression>
#include <QStandardPaths>
namespace {
@ -21,15 +19,45 @@ QStringList splitList(const QString &value)
return result;
}
QString unescapeValue(QString value)
QString unescapeValue(const QString &value)
{
value.replace(QStringLiteral("\\n"), QStringLiteral("\n"));
value.replace(QStringLiteral("\\t"), QStringLiteral("\t"));
value.replace(QStringLiteral("\\r"), QStringLiteral("\r"));
value.replace(QStringLiteral("\\s"), QStringLiteral(" "));
value.replace(QStringLiteral("\\;"), QStringLiteral(";"));
value.replace(QStringLiteral("\\\\"), QStringLiteral("\\"));
return value;
// One pass, so an escaped backslash cannot start another escape.
QString result;
result.reserve(value.size());
for (int i = 0; i < value.size(); ++i) {
const QChar ch = value.at(i);
if (ch != QLatin1Char('\\') || i + 1 == value.size()) {
result.append(ch);
continue;
}
const QChar next = value.at(++i);
switch (next.unicode()) {
case 'n':
result.append(QLatin1Char('\n'));
break;
case 't':
result.append(QLatin1Char('\t'));
break;
case 'r':
result.append(QLatin1Char('\r'));
break;
case 's':
result.append(QLatin1Char(' '));
break;
case ';':
result.append(QLatin1Char(';'));
break;
case '\\':
result.append(QLatin1Char('\\'));
break;
default:
result.append(ch);
result.append(next);
break;
}
}
return result;
}
bool parseBool(const QString &value)
@ -157,10 +185,9 @@ QString substituteToken(const QString &token, const DesktopEntry *entry,
result.append(entry->path());
} else if (field == QLatin1Char('i')) {
// %i is handled as two arguments by commandForArguments().
} else if (field == QLatin1Char('f') || field == QLatin1Char('u')) {
if (!arguments.isEmpty())
result.append(arguments.first());
} else if (field == QLatin1Char('F') || field == QLatin1Char('U')) {
} else if (field == QLatin1Char('f') || field == QLatin1Char('u')
|| field == QLatin1Char('F') || field == QLatin1Char('U')) {
// Inside a larger token a list code can take only one argument.
if (!arguments.isEmpty())
result.append(arguments.first());
}
@ -361,7 +388,7 @@ bool DesktopEntry::parse(const QString &id, const QString &path,
if (equals <= 0)
continue;
QString key = line.left(equals).trimmed();
const QString key = line.left(equals).trimmed();
const QString value = unescapeValue(line.mid(equals + 1).trimmed());
if (group.startsWith(QStringLiteral("Desktop Action "))) {
actionValues[group.mid(QStringLiteral("Desktop Action ").size())][key] = value;
@ -369,7 +396,6 @@ bool DesktopEntry::parse(const QString &id, const QString &path,
const int localeStart = key.indexOf(QLatin1Char('['));
if (localeStart > 0 && key.endsWith(QLatin1Char(']'))) {
localized.insert(key, value);
key = key.left(localeStart);
} else {
values.insert(key, value);
}
@ -414,8 +440,7 @@ void DesktopEntry::update(const DesktopEntryData &data)
m_tryExec = data.tryExec;
emit tryExecChanged();
}
const bool hasExecChanged = m_exec != data.exec;
if (hasExecChanged) {
if (m_exec != data.exec) {
m_exec = data.exec;
emit execChanged();
}

@ -65,6 +65,15 @@ QString terminalId(const QString &line)
entry.remove(0, 1);
return entry.section(':', 0, 0);
}
bool writeLines(const QString &path, const QStringList &lines)
{
QSaveFile output(path);
if (!output.open(QIODevice::WriteOnly))
return false;
const auto data = (lines.join('\n') + '\n').toUtf8();
return output.write(data) == data.size() && output.commit();
}
} // namespace
QString MimeApps::userListPath()
@ -115,11 +124,7 @@ bool MimeApps::setDefault(const QString &path, const QString &mimeType, const QS
auto removed = readEntries(lines, "Removed Associations", mimeType);
if (removed.removeAll(desktopId))
writeEntry(lines, "Removed Associations", mimeType, removed);
QSaveFile output(path);
if (!output.open(QIODevice::WriteOnly))
return false;
const auto data = (lines.join('\n') + '\n').toUtf8();
return output.write(data) == data.size() && output.commit();
return writeLines(path, lines);
}
QString MimeApps::defaultTerminal(const QString &path)
@ -156,9 +161,5 @@ bool MimeApps::setDefaultTerminal(const QString &path, const QString &desktopId)
return !line.trimmed().startsWith('#') && terminalId(line) == desktopId;
});
lines.prepend(desktopId);
QSaveFile output(path);
if (!output.open(QIODevice::WriteOnly))
return false;
const auto data = (lines.join('\n') + '\n').toUtf8();
return output.write(data) == data.size() && output.commit();
return writeLines(path, lines);
}

@ -9,6 +9,16 @@
namespace Cutefish {
namespace {
void flushDisplay()
{
auto *native = qGuiApp->nativeInterface<QNativeInterface::QWaylandApplication>();
wl_display_flush(native->display());
}
} // namespace
WaylandWindowHints *WaylandWindowHints::instance()
{
static auto *manager = new WaylandWindowHints(qApp);
@ -72,8 +82,7 @@ void WaylandWindowHints::setSkipTaskbar(wl_surface *surface, bool skip)
if (!m_manager || !surface)
return;
cutefish_window_hints_v1_set_skip_taskbar(m_manager, surface, skip);
auto *native = qGuiApp->nativeInterface<QNativeInterface::QWaylandApplication>();
wl_display_flush(native->display());
flushDisplay();
}
void WaylandWindowHints::setAlwaysOnTop(wl_surface *surface, bool alwaysOnTop)
@ -81,8 +90,7 @@ void WaylandWindowHints::setAlwaysOnTop(wl_surface *surface, bool alwaysOnTop)
if (!m_manager || !surface)
return;
cutefish_window_hints_v1_set_always_on_top(m_manager, surface, alwaysOnTop);
auto *native = qGuiApp->nativeInterface<QNativeInterface::QWaylandApplication>();
wl_display_flush(native->display());
flushDisplay();
}
void WaylandWindowHints::setResizeAroundCenter(wl_surface *surface, bool center)
@ -92,8 +100,7 @@ void WaylandWindowHints::setResizeAroundCenter(wl_surface *surface, bool center)
cutefish_window_hints_v1_set_resize_anchor(m_manager, surface,
center ? CUTEFISH_WINDOW_HINTS_V1_RESIZE_ANCHOR_CENTER
: CUTEFISH_WINDOW_HINTS_V1_RESIZE_ANCHOR_TOP_LEFT);
auto *native = qGuiApp->nativeInterface<QNativeInterface::QWaylandApplication>();
wl_display_flush(native->display());
flushDisplay();
}
void WaylandWindowHints::setServerResize(wl_surface *surface, bool enabled)
@ -101,8 +108,7 @@ void WaylandWindowHints::setServerResize(wl_surface *surface, bool enabled)
if (!m_manager || !surface)
return;
cutefish_window_hints_v1_set_server_resize(m_manager, surface, enabled);
auto *native = qGuiApp->nativeInterface<QNativeInterface::QWaylandApplication>();
wl_display_flush(native->display());
flushDisplay();
}
} // namespace Cutefish

@ -107,7 +107,10 @@ void WindowHints::watchSurface()
}, Qt::DirectConnection);
m_destroyedConnection = connect(
native, &QNativeInterface::Private::QWaylandWindow::surfaceDestroyed, this,
[this] { m_surfaceDying = true; }, Qt::DirectConnection);
[this] {
m_surfaceDying = true;
setInteractiveResize(false);
}, Qt::DirectConnection);
}
bool WindowHints::eventFilter(QObject *object, QEvent *event)

@ -1,4 +1,4 @@
find_package(Qt6 REQUIRED COMPONENTS Core Gui Qml Quick)
find_package(Qt6 REQUIRED COMPONENTS Core Qml)
find_package(PkgConfig REQUIRED)
pkg_check_modules(PIPEWIRE REQUIRED IMPORTED_TARGET libpipewire-0.3>=0.3.65)
pkg_check_modules(SPA REQUIRED IMPORTED_TARGET libspa-0.2)
@ -32,8 +32,6 @@ target_include_directories(cutefish-framework-audio PUBLIC ${CMAKE_CURRENT_SOURC
target_link_libraries(cutefish-framework-audio PUBLIC
Qt6::Core
Qt6::Qml
Qt6::Gui
Qt6::Quick
PkgConfig::PIPEWIRE
PkgConfig::SPA
)

@ -40,10 +40,9 @@ int AudioModel::rowCount(const QModelIndex &parent) const
QVariant AudioModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.model() != this || index.column() != 0
|| index.row() < 0 || index.row() >= m_objects.size())
AudioObject *object = objectAt(index);
if (!object)
return {};
AudioObject *object = m_objects.at(index.row());
if (role == Qt::DisplayRole)
return object->description();
if (role == ObjectRole)
@ -56,8 +55,8 @@ QVariant AudioModel::data(const QModelIndex &index, int role) const
bool AudioModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
if (!index.isValid() || index.model() != this || index.column() != 0
|| index.row() < 0 || index.row() >= m_objects.size())
AudioObject *object = objectAt(index);
if (!object)
return false;
const int propertyIndex = m_properties.value(role, -1);
if (propertyIndex < 0)
@ -65,7 +64,7 @@ bool AudioModel::setData(const QModelIndex &index, const QVariant &value, int ro
const QMetaProperty property = AudioObject::staticMetaObject.property(propertyIndex);
if (!property.isWritable())
return false;
return property.write(m_objects.at(index.row()), value);
return property.write(object, value);
}
Qt::ItemFlags AudioModel::flags(const QModelIndex &index) const
@ -88,6 +87,14 @@ AudioObject *AudioModel::defaultNode() const
return m_backend->defaultNode(m_kind);
}
AudioObject *AudioModel::objectAt(const QModelIndex &index) const
{
if (!index.isValid() || index.model() != this || index.column() != 0
|| index.row() < 0 || index.row() >= m_objects.size())
return nullptr;
return m_objects.at(index.row());
}
void AudioModel::addObject(AudioObject *object)
{
if (object->kind() != m_kind)

@ -31,6 +31,7 @@ Q_SIGNALS:
void defaultNodeChanged();
private:
AudioObject *objectAt(const QModelIndex &index) const;
void addObject(AudioObject *object);
void removeObject(AudioObject *object);

@ -26,6 +26,17 @@ QVector<T> arrayValues(const spa_pod *pod, quint32 type)
return QVector<T>(values, values + count);
}
QVariantList intValues(const spa_pod *object, quint32 key)
{
QVariantList values;
const spa_pod_prop *prop = spa_pod_find_prop(object, nullptr, key);
if (prop) {
for (int value : arrayValues<int>(&prop->value, SPA_TYPE_Int))
values.append(value);
}
return values;
}
int availability(quint32 value)
{
if (value == SPA_PARAM_AVAILABILITY_yes)
@ -153,11 +164,16 @@ quint32 AudioObject::cardIndex() const
return ok ? id : InvalidId;
}
const AudioObject *AudioObject::cardObject() const
{
return m_backend ? m_backend->object(cardIndex()) : nullptr;
}
// The card's bus, such as "pci", "usb" or "bluetooth"; empty for virtual
// nodes and for SoC codecs, which have no bus.
QString AudioObject::bus() const
{
const AudioObject *card = m_backend ? m_backend->object(cardIndex()) : nullptr;
const AudioObject *card = cardObject();
return card ? card->m_properties.value("device.bus").toString() : QString();
}
@ -170,7 +186,7 @@ int AudioObject::profileDevice() const
QVariantList AudioObject::ports() const
{
const AudioObject *card = m_backend ? m_backend->object(cardIndex()) : nullptr;
const AudioObject *card = cardObject();
if (!card || (m_kind != Sink && m_kind != Source))
return {};
QVariantList result;
@ -191,7 +207,7 @@ QVariantList AudioObject::ports() const
int AudioObject::activePortIndex() const
{
const AudioObject *card = m_backend ? m_backend->object(cardIndex()) : nullptr;
const AudioObject *card = cardObject();
if (!card)
return -1;
const int active = card->m_activeRoutes.value(profileDevice(), -1);
@ -388,22 +404,11 @@ int AudioObject::updateRoute(const spa_pod *param, bool current)
SPA_POD_SIZE(&props->value)));
}
} else {
QVariantList devices;
QVariantList profiles;
const spa_pod_prop *prop = spa_pod_find_prop(param, nullptr, SPA_PARAM_ROUTE_devices);
if (prop) {
for (int value : arrayValues<int>(&prop->value, SPA_TYPE_Int))
devices.append(value);
}
prop = spa_pod_find_prop(param, nullptr, SPA_PARAM_ROUTE_profiles);
if (prop) {
for (int value : arrayValues<int>(&prop->value, SPA_TYPE_Int))
profiles.append(value);
}
updateChoice(m_routes, {{"index", index}, {"name", QString::fromUtf8(name)},
{"description", QString::fromUtf8(description)}, {"priority", priority},
{"direction", direction}, {"availability", availability(available)},
{"devices", devices}, {"profiles", profiles}});
{"devices", intValues(param, SPA_PARAM_ROUTE_devices)},
{"profiles", intValues(param, SPA_PARAM_ROUTE_profiles)}});
}
Q_EMIT changed();
return current ? device : -1;

@ -101,6 +101,7 @@ private:
// Returns the device of a current route, -1 otherwise.
int updateRoute(const spa_pod *param, bool current);
int profileDevice() const;
const AudioObject *cardObject() const;
Backend *m_backend;
quint32 m_id;

@ -131,11 +131,13 @@ const pw_device_events Backend::deviceEvents = [] {
binding->object->updateProfile(param, id == SPA_PARAM_Profile);
else if (id == SPA_PARAM_EnumRoute || id == SPA_PARAM_Route) {
const int device = binding->object->updateRoute(param, id == SPA_PARAM_Route);
if (device < 0)
return;
// Only that device's nodes: the card's other routes may still hold
// the previous volume of a node that has already changed.
for (AudioObject *node : binding->backend->objects()) {
if (device >= 0 && node->kind() != AudioObject::Card
&& node->cardIndex() == binding->object->index() && node->profileDevice() == device)
if (node->kind() != AudioObject::Card && node->cardIndex() == binding->object->index()
&& node->profileDevice() == device)
binding->backend->updateRouteVolume(node);
}
}
@ -226,8 +228,7 @@ void Backend::connectToDaemon()
PW_KEY_APP_ID, "org.cutefish.Audio", nullptr), 0);
}
if (!m_core) {
disconnectFromDaemon();
m_retry.start();
fail();
return;
}
pw_core_add_listener(m_core, &m_coreListener, &coreEvents, this);
@ -266,13 +267,7 @@ void Backend::disconnectFromDaemon()
const auto ids = m_bindings.keys();
for (quint32 id : ids)
removeGlobal(id);
if (m_metadata) {
spa_hook_remove(&m_metadataListener);
pw_proxy_destroy(reinterpret_cast<pw_proxy *>(m_metadata));
m_metadata = nullptr;
}
m_metadataId = AudioObject::InvalidId;
m_metadataPermissions = 0;
releaseMetadata();
if (m_registry) {
spa_hook_remove(&m_registryListener);
pw_proxy_destroy(reinterpret_cast<pw_proxy *>(m_registry));
@ -302,6 +297,17 @@ void Backend::disconnectFromDaemon()
updateDefaults();
}
void Backend::releaseMetadata()
{
if (m_metadata) {
spa_hook_remove(&m_metadataListener);
pw_proxy_destroy(reinterpret_cast<pw_proxy *>(m_metadata));
m_metadata = nullptr;
}
m_metadataId = AudioObject::InvalidId;
m_metadataPermissions = 0;
}
void Backend::addGlobal(quint32 id, quint32 permissions, const char *type,
quint32 version, const spa_dict *properties)
{
@ -390,10 +396,7 @@ void Backend::addGlobal(quint32 id, quint32 permissions, const char *type,
void Backend::removeGlobal(quint32 id)
{
if (id == m_metadataId && m_metadata) {
spa_hook_remove(&m_metadataListener);
pw_proxy_destroy(reinterpret_cast<pw_proxy *>(m_metadata));
m_metadata = nullptr;
m_metadataId = AudioObject::InvalidId;
releaseMetadata();
m_defaultSink.clear();
m_defaultSource.clear();
updateDefaults();
@ -470,8 +473,9 @@ void Backend::setNodeParam(AudioObject *object, const spa_pod *param)
if (!binding || binding->object != object || !object->m_writable || object->kind() == AudioObject::Card)
return;
Binding *card = m_bindings.value(object->cardIndex());
if (card && card->object->m_routeProperties.contains(object->profileDevice())
&& card->object->m_activeRoutes.contains(object->profileDevice())) {
const int device = object->profileDevice();
if (card && card->object->m_routeProperties.contains(device)
&& card->object->m_activeRoutes.contains(device)) {
if (!(card->permissions & PW_PERM_W)) {
checkResult(-EACCES, "Set device volume");
return;
@ -480,8 +484,8 @@ void Backend::setNodeParam(AudioObject *object, const spa_pod *param)
spa_pod_builder builder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
const auto *route = static_cast<spa_pod *>(spa_pod_builder_add_object(&builder,
SPA_TYPE_OBJECT_ParamRoute, SPA_PARAM_Route,
SPA_PARAM_ROUTE_index, SPA_POD_Int(card->object->m_activeRoutes.value(object->profileDevice())),
SPA_PARAM_ROUTE_device, SPA_POD_Int(object->profileDevice()),
SPA_PARAM_ROUTE_index, SPA_POD_Int(card->object->m_activeRoutes.value(device)),
SPA_PARAM_ROUTE_device, SPA_POD_Int(device),
SPA_PARAM_ROUTE_props, SPA_POD_Pod(param),
SPA_PARAM_ROUTE_save, SPA_POD_Bool(true)));
checkResult(pw_device_set_param(reinterpret_cast<pw_device *>(card->proxy), SPA_PARAM_Route, 0, route), "Set device volume");

@ -56,6 +56,7 @@ private:
void connectToDaemon();
void disconnectFromDaemon();
void releaseMetadata();
void fail();
void iterate();
void addGlobal(quint32 id, quint32 permissions, const char *type,

@ -5,7 +5,6 @@
#include "teststream.h"
#include <QAbstractItemModelTester>
#include <QDir>
#include <QFile>
#include <QProcess>
#include <QQmlComponent>

@ -16,14 +16,9 @@ const pw_stream_events VolumeMonitor::streamEvents = [] {
events.state_changed = [](void *data, pw_stream_state, pw_stream_state state, const char *) {
auto *self = static_cast<VolumeMonitor *>(data);
const bool available = state == PW_STREAM_STATE_STREAMING;
if (self->m_available != available) {
self->m_available = available;
Q_EMIT self->availableChanged();
}
if (!available && self->m_volume != 0.0) {
self->m_volume = 0.0;
Q_EMIT self->volumeChanged();
}
self->updateAvailable(available);
if (!available)
self->updateVolume(0.0);
};
return events;
}();
@ -110,13 +105,25 @@ void VolumeMonitor::process()
// Rise at once but fall at 30 dB/s, as a level meter does; raw per-buffer
// peaks jump by tens of dB and make the meter flicker.
const double fall = std::pow(10.0, -30.0 / 20.0 * frames / 48000.0);
const double volume = std::clamp(std::max(peak, m_volume * fall), 0.0, 1.0);
updateVolume(std::clamp(std::max(peak, m_volume * fall), 0.0, 1.0));
}
void VolumeMonitor::updateVolume(double volume)
{
if (m_volume != volume) {
m_volume = volume;
Q_EMIT volumeChanged();
}
}
void VolumeMonitor::updateAvailable(bool available)
{
if (m_available != available) {
m_available = available;
Q_EMIT availableChanged();
}
}
void VolumeMonitor::stop()
{
if (m_stream) {
@ -124,14 +131,8 @@ void VolumeMonitor::stop()
pw_stream_destroy(m_stream);
m_stream = nullptr;
}
if (m_available) {
m_available = false;
Q_EMIT availableChanged();
}
if (m_volume != 0.0) {
m_volume = 0.0;
Q_EMIT volumeChanged();
}
updateAvailable(false);
updateVolume(0.0);
}
}

@ -31,6 +31,8 @@ private:
static const pw_stream_events streamEvents;
void stop();
void process();
void updateVolume(double volume);
void updateAvailable(bool available);
QPointer<AudioObject> m_target;
pw_stream *m_stream = nullptr;

@ -36,7 +36,6 @@ target_link_libraries(cutefish-framework-bluetooth PUBLIC
)
cutefish_install_qml_module(cutefish-framework-bluetooth)
install(FILES DevicesModel.qml DESTINATION ${INSTALL_QMLDIR}/Cutefish/Bluetooth)
if(BUILD_TESTING)
add_subdirectory(tests)

@ -57,7 +57,7 @@ bool BluetoothAgent::handleMessage(const QDBusMessage &message, const QDBusConne
}
if (message.member() == QStringLiteral("Cancel")
|| message.member() == QStringLiteral("Release")) {
m_manager->finishPrompt(QStringLiteral("org.bluez.Error.Canceled"));
m_manager->finishPrompt();
if (message.member() == QStringLiteral("Release")) {
m_manager->m_agentReady = false;
emit m_manager->changed();

@ -1,64 +1,6 @@
#include "bluetoothdbustypes.h"
#include <QDBusArgument>
#include <QDBusMetaType>
namespace {
QDBusArgument &operator<<(QDBusArgument &argument, const BluetoothInterfaces &interfaces)
{
argument.beginMap(QMetaType::fromType<QString>(), QMetaType::fromType<QVariantMap>());
for (auto it = interfaces.cbegin(); it != interfaces.cend(); ++it) {
argument.beginMapEntry();
argument << it.key() << it.value();
argument.endMapEntry();
}
argument.endMap();
return argument;
}
const QDBusArgument &operator>>(const QDBusArgument &argument, BluetoothInterfaces &interfaces)
{
argument.beginMap();
while (!argument.atEnd()) {
QString name;
QVariantMap properties;
argument.beginMapEntry();
argument >> name >> properties;
argument.endMapEntry();
interfaces.insert(name, properties);
}
argument.endMap();
return argument;
}
QDBusArgument &operator<<(QDBusArgument &argument, const BluetoothObjects &objects)
{
argument.beginMap(QMetaType::fromType<QDBusObjectPath>(),
QMetaType::fromType<BluetoothInterfaces>());
for (auto it = objects.cbegin(); it != objects.cend(); ++it) {
argument.beginMapEntry();
argument << it.key() << it.value();
argument.endMapEntry();
}
argument.endMap();
return argument;
}
const QDBusArgument &operator>>(const QDBusArgument &argument, BluetoothObjects &objects)
{
argument.beginMap();
while (!argument.atEnd()) {
QDBusObjectPath path;
BluetoothInterfaces interfaces;
argument.beginMapEntry();
argument >> path >> interfaces;
argument.endMapEntry();
objects.insert(path, interfaces);
}
argument.endMap();
return argument;
}
} // namespace
void registerBluetoothDBusTypes()
{
qDBusRegisterMetaType<BluetoothInterfaces>();

@ -15,7 +15,9 @@ namespace BluetoothDBus {
inline const QString adapterInterface = QStringLiteral("org.bluez.Adapter1");
inline const QString deviceInterface = QStringLiteral("org.bluez.Device1");
inline const QString propertiesInterface = QStringLiteral("org.freedesktop.DBus.Properties");
inline const QString objectManagerInterface = QStringLiteral("org.freedesktop.DBus.ObjectManager");
inline const QString agentPath = QStringLiteral("/org/cutefish/BluetoothAgent");
inline const QString agentManagerPath = QStringLiteral("/org/bluez");
inline const QString agentManagerInterface = QStringLiteral("org.bluez.AgentManager1");
} // namespace BluetoothDBus

@ -117,14 +117,14 @@ void BluetoothDevicesModel::rebuild()
const int oldCount = m_devices.size();
for (int row = m_devices.size() - 1; row >= 0; --row) {
if (!m_devices.at(row) || !devices.contains(m_devices.at(row)))
removeRow(row);
removeDevice(row);
}
for (int row = 0; row < devices.size(); ++row) {
if (row < m_devices.size() && m_devices.at(row) == devices.at(row))
continue;
const int from = m_devices.indexOf(devices.at(row), row);
if (from < 0) {
insertRow(row, devices.at(row));
insertDevice(row, devices.at(row));
} else {
beginMoveRows({}, from, from, {}, row);
m_devices.move(from, row);
@ -135,7 +135,7 @@ void BluetoothDevicesModel::rebuild()
emit countChanged();
}
void BluetoothDevicesModel::insertRow(int row, BluetoothObject *device)
void BluetoothDevicesModel::insertDevice(int row, BluetoothObject *device)
{
beginInsertRows({}, row, row);
m_devices.insert(row, device);
@ -147,7 +147,7 @@ void BluetoothDevicesModel::insertRow(int row, BluetoothObject *device)
});
}
void BluetoothDevicesModel::removeRow(int row)
void BluetoothDevicesModel::removeDevice(int row)
{
beginRemoveRows({}, row, row);
const QPointer<BluetoothObject> device = m_devices.takeAt(row);

@ -45,8 +45,8 @@ signals:
private:
bool accepts(const BluetoothObject *device) const;
void rebuild();
void insertRow(int row, BluetoothObject *device);
void removeRow(int row);
void insertDevice(int row, BluetoothObject *device);
void removeDevice(int row);
QPointer<BluetoothManager> m_manager;
QList<QPointer<BluetoothObject>> m_devices;
Filter m_filter = AllDevices;

@ -3,7 +3,6 @@
#include "bluetoothrfkill.h"
#include <QDBusArgument>
#include <QDBusConnectionInterface>
#include <QDBusPendingCallWatcher>
#include <QDBusPendingReply>
#include <QDBusServiceWatcher>
@ -63,12 +62,12 @@ BluetoothManager::BluetoothManager(const QDBusConnection &bus, bool monitorRfkil
BluetoothManager::~BluetoothManager()
{
finishPrompt(QStringLiteral("org.bluez.Error.Canceled"));
finishPrompt();
for (const auto &path : std::as_const(m_pairingOperations))
m_bus.asyncCall(QDBusMessage::createMethodCall(m_owner, path, deviceInterface,
QStringLiteral("CancelPairing")));
if (m_agentReady) {
auto message = QDBusMessage::createMethodCall(m_owner, QStringLiteral("/org/bluez"),
auto message = QDBusMessage::createMethodCall(m_owner, agentManagerPath,
agentManagerInterface,
QStringLiteral("UnregisterAgent"));
message << QVariant::fromValue(QDBusObjectPath(agentPath));
@ -126,6 +125,8 @@ void BluetoothManager::ownerChanged(const QString &, const QString &, const QStr
reset();
if (!newOwner.isEmpty())
load(newOwner);
else
setReady();
}
void BluetoothManager::reset()
@ -133,19 +134,17 @@ void BluetoothManager::reset()
++m_generation;
m_retryTimer.stop();
if (!m_owner.isEmpty()) {
m_bus.disconnect(m_owner, QStringLiteral("/"),
QStringLiteral("org.freedesktop.DBus.ObjectManager"),
m_bus.disconnect(m_owner, QStringLiteral("/"), objectManagerInterface,
QStringLiteral("InterfacesAdded"), this,
SLOT(interfacesAdded(QDBusObjectPath, BluetoothInterfaces)));
m_bus.disconnect(m_owner, QStringLiteral("/"),
QStringLiteral("org.freedesktop.DBus.ObjectManager"),
m_bus.disconnect(m_owner, QStringLiteral("/"), objectManagerInterface,
QStringLiteral("InterfacesRemoved"), this,
SLOT(interfacesRemoved(QDBusObjectPath, QStringList)));
m_bus.disconnect(m_owner, QString(), propertiesInterface,
QStringLiteral("PropertiesChanged"), this,
SLOT(propertiesChanged(QString, QVariantMap, QStringList, QDBusMessage)));
}
finishPrompt(QStringLiteral("org.bluez.Error.Canceled"));
finishPrompt();
m_owner.clear();
m_operational = false;
m_loading = false;
@ -172,15 +171,15 @@ void BluetoothManager::load(const QString &owner)
return;
m_owner = owner;
m_loading = true;
m_bus.connect(owner, QStringLiteral("/"), QStringLiteral("org.freedesktop.DBus.ObjectManager"),
m_bus.connect(owner, QStringLiteral("/"), objectManagerInterface,
QStringLiteral("InterfacesAdded"), this,
SLOT(interfacesAdded(QDBusObjectPath, BluetoothInterfaces)));
m_bus.connect(owner, QStringLiteral("/"), QStringLiteral("org.freedesktop.DBus.ObjectManager"),
m_bus.connect(owner, QStringLiteral("/"), objectManagerInterface,
QStringLiteral("InterfacesRemoved"), this,
SLOT(interfacesRemoved(QDBusObjectPath, QStringList)));
m_bus.connect(owner, QString(), propertiesInterface, QStringLiteral("PropertiesChanged"), this,
SLOT(propertiesChanged(QString, QVariantMap, QStringList, QDBusMessage)));
call(QStringLiteral("/"), QStringLiteral("org.freedesktop.DBus.ObjectManager"),
call(QStringLiteral("/"), objectManagerInterface,
QStringLiteral("GetManagedObjects"), {}, [this](const QDBusMessage &reply) {
if (reply.type() == QDBusMessage::ErrorMessage) {
reset();
@ -244,7 +243,7 @@ void BluetoothManager::interfacesRemoved(const QDBusObjectPath &path, const QStr
return;
if (interfaces.contains(deviceInterface) || interfaces.contains(adapterInterface)) {
if (m_pairingPath == path.path())
finishPrompt(QStringLiteral("org.bluez.Error.Canceled"));
finishPrompt();
const bool adapter = entry->isAdapter();
m_objects.remove(path.path());
m_discoveryOwned.remove(path.path());
@ -425,8 +424,8 @@ void BluetoothManager::call(const QString &path, const QString &interface, const
});
}
void BluetoothManager::setProperty(BluetoothObject *entry, const QString &property,
const QVariant &value)
void BluetoothManager::setObjectProperty(BluetoothObject *entry, const QString &property,
const QVariant &value)
{
if (!entry || object(entry->path()) != entry)
return;
@ -444,13 +443,14 @@ void BluetoothManager::operate(BluetoothObject *entry, const QString &method,
return;
}
const bool cancel = method == QStringLiteral("CancelPairing");
const bool pair = method == QStringLiteral("Pair");
if (entry->busy() && !cancel) {
deviceFailure(method, entry->name(), tr("The device is busy. Try again in a moment."));
return;
}
// Without an agent of its own the caller pairs through BlueZ's default one,
// which the shell keeps registered.
if (method == QStringLiteral("Pair") && m_agentEnabled && !m_agentReady) {
if (pair && m_agentEnabled && !m_agentReady) {
deviceFailure(method, entry->name(),
tr("Bluetooth isn't ready yet. Try again in a moment."));
return;
@ -462,20 +462,18 @@ void BluetoothManager::operate(BluetoothObject *entry, const QString &method,
QPointer<BluetoothObject> guard(entry);
const QString path = entry->path();
const QString name = entry->name();
if (method == QStringLiteral("Pair"))
if (pair)
m_pairingOperations.insert(path);
call(
remove ? entry->adapterPath() : path, remove ? adapterInterface : deviceInterface,
method, remove ? QVariantList {QVariant::fromValue(QDBusObjectPath(path))} : arguments,
[this, guard, path, name, method, cancel](const QDBusMessage &reply) {
if (method == QStringLiteral("Pair"))
[this, guard, path, name, method, cancel, pair](const QDBusMessage &reply) {
if (pair)
m_pairingOperations.remove(path);
if (guard && !cancel)
guard->setBusy(false);
if (method == QStringLiteral("Pair") || cancel) {
if (m_pairingPath == path)
finishPrompt(QStringLiteral("org.bluez.Error.Canceled"));
}
if ((pair || cancel) && m_pairingPath == path)
finishPrompt();
if (reply.type() == QDBusMessage::ErrorMessage && !cancel) {
qCWarning(lcBluetooth) << method << "failed:" << reply.errorName()
<< reply.errorMessage();
@ -492,8 +490,7 @@ void BluetoothManager::operate(BluetoothObject *entry, const QString &method,
if (!quiet.contains(reply.errorName()) && !alreadyConnected)
deviceFailure(method, guard ? guard->name() : name, describe(reply));
}
if (!guard || reply.type() == QDBusMessage::ErrorMessage
|| method != QStringLiteral("Pair"))
if (!guard || reply.type() == QDBusMessage::ErrorMessage || !pair)
return;
// Trust is granted only after a successful user-initiated pairing.
guard->setBusy(true);
@ -508,19 +505,17 @@ void BluetoothManager::operate(BluetoothObject *entry, const QString &method,
operate(guard, QStringLiteral("Connect"));
});
},
Errors::Ignore, method == QStringLiteral("Pair") ? 180000 : 60000);
Errors::Ignore, pair ? 180000 : 60000);
}
void BluetoothManager::setDiscoveryRequested(bool requested)
{
m_discoveryFailed.clear();
m_stopRetries.clear();
if (m_discoveryRequested == requested) {
syncDiscovery();
return;
if (m_discoveryRequested != requested) {
m_discoveryRequested = requested;
emit changed();
}
m_discoveryRequested = requested;
emit changed();
syncDiscovery();
}
@ -613,8 +608,7 @@ void BluetoothManager::setAgentEnabled(bool enabled)
operate(device, QStringLiteral("CancelPairing"));
}
if (m_agentReady)
call(QStringLiteral("/org/bluez"), agentManagerInterface,
QStringLiteral("UnregisterAgent"),
call(agentManagerPath, agentManagerInterface, QStringLiteral("UnregisterAgent"),
{QVariant::fromValue(QDBusObjectPath(agentPath))});
m_agentReady = false;
m_bus.unregisterObject(agentPath);
@ -627,22 +621,20 @@ void BluetoothManager::registerAgent()
if (!m_agentEnabled || !m_operational || m_agentReady || m_registeringAgent)
return;
m_registeringAgent = true;
call(QStringLiteral("/org/bluez"), agentManagerInterface, QStringLiteral("RegisterAgent"),
call(agentManagerPath, agentManagerInterface, QStringLiteral("RegisterAgent"),
{QVariant::fromValue(QDBusObjectPath(agentPath)), QStringLiteral("KeyboardDisplay")},
[this](const QDBusMessage &reply) {
m_registeringAgent = false;
if (reply.type() == QDBusMessage::ErrorMessage)
return;
if (!m_agentEnabled) {
call(QStringLiteral("/org/bluez"), agentManagerInterface,
QStringLiteral("UnregisterAgent"),
call(agentManagerPath, agentManagerInterface, QStringLiteral("UnregisterAgent"),
{QVariant::fromValue(QDBusObjectPath(agentPath))});
return;
}
m_agentReady = true;
emit changed();
call(QStringLiteral("/org/bluez"), agentManagerInterface,
QStringLiteral("RequestDefaultAgent"),
call(agentManagerPath, agentManagerInterface, QStringLiteral("RequestDefaultAgent"),
{QVariant::fromValue(QDBusObjectPath(agentPath))});
});
}
@ -712,9 +704,7 @@ void BluetoothManager::respondPairing(bool accept, const QString &code)
void BluetoothManager::finishPrompt(const QString &error)
{
if (m_hasRequest)
m_bus.send(m_request.createErrorReply(
error.isEmpty() ? QStringLiteral("org.bluez.Error.Canceled") : error,
tr("Pairing request dismissed.")));
m_bus.send(m_request.createErrorReply(error, tr("Pairing request dismissed.")));
m_hasRequest = false;
m_request = QDBusMessage();
m_promptTimer.stop();
@ -729,7 +719,7 @@ void BluetoothManager::finishPrompt(const QString &error)
void BluetoothManager::cancelPairing()
{
const QString path = m_pairingPath;
finishPrompt(QStringLiteral("org.bluez.Error.Canceled"));
finishPrompt();
if (auto *device = object(path))
operate(device, QStringLiteral("CancelPairing"));
}
@ -794,6 +784,8 @@ QString BluetoothManager::describe(const QDBusMessage &reply) const
return tr("The device has no features this computer can use.");
if (reason == QLatin1String("adapter-not-powered"))
return tr("Bluetooth is turned off.");
if (name == QLatin1String("Blocked") && hardwareBlocked())
return tr("Bluetooth is blocked by a hardware switch.");
if (name == QLatin1String("NotReady"))
return tr("Bluetooth isn't ready yet. Try again in a moment.");
if (name == QLatin1String("InProgress") || reason == QLatin1String("busy")

@ -80,7 +80,7 @@ public:
Q_INVOKABLE void cancelPairing();
Q_INVOKABLE void refresh();
BluetoothObject *object(const QString &path) const;
void setProperty(BluetoothObject *object, const QString &property, const QVariant &value);
void setObjectProperty(BluetoothObject *object, const QString &property, const QVariant &value);
void operate(BluetoothObject *object, const QString &method,
const QVariantList &arguments = {});
void discovery(BluetoothObject *adapter, bool start);
@ -118,7 +118,7 @@ private:
void setReady();
void syncDiscovery();
void registerAgent();
void finishPrompt(const QString &error = QString());
void finishPrompt(const QString &error = QStringLiteral("org.bluez.Error.Canceled"));
void prompt(const QDBusMessage &message);
void deviceFailure(const QString &method, const QString &device, const QString &body);
QString describe(const QDBusMessage &reply) const;

@ -122,7 +122,7 @@ bool BluetoothObject::hasBluezInterface() const
void BluetoothObject::setValue(const QString &property, const QVariant &value)
{
m_manager->setProperty(this, property, value);
m_manager->setObjectProperty(this, property, value);
}
void BluetoothObject::setName(const QString &name)

@ -11,6 +11,22 @@
#include "linux-dmabuf-v1-client-protocol.h"
#include "xdg-output-unstable-v1-client-protocol.h"
namespace {
// Only a release tells the compositor; a plain destroy leaves the server
// object until the connection closes.
void releaseOutput(const CaptureClient::Output &output)
{
if (output.xdgOutput)
zxdg_output_v1_destroy(output.xdgOutput);
if (wl_output_get_version(output.output) >= WL_OUTPUT_RELEASE_SINCE_VERSION)
wl_output_release(output.output);
else
wl_output_destroy(output.output);
}
} // namespace
struct CaptureClientListeners {
static void global(void *data, wl_registry *registry, uint32_t name, const char *interface,
uint32_t version)
@ -69,12 +85,7 @@ struct CaptureClientListeners {
});
if (it == self->m_outputs.end())
return;
if ((*it)->xdgOutput)
zxdg_output_v1_destroy((*it)->xdgOutput);
if (wl_output_get_version((*it)->output) >= WL_OUTPUT_RELEASE_SINCE_VERSION)
wl_output_release((*it)->output);
else
wl_output_destroy((*it)->output);
releaseOutput(**it);
self->m_outputs.erase(it);
}
@ -108,7 +119,6 @@ struct CaptureClientListeners {
static void handleClosed(void *data, ext_foreign_toplevel_handle_v1 *handle)
{
auto *toplevel = static_cast<CaptureClient::Toplevel *>(data);
toplevel->identifier.clear();
ext_foreign_toplevel_handle_v1_destroy(handle);
toplevel->handle = nullptr;
}
@ -183,16 +193,8 @@ CaptureClient::~CaptureClient()
}
if (m_toplevelList)
ext_foreign_toplevel_list_v1_destroy(m_toplevelList);
for (auto &output : m_outputs) {
if (output->xdgOutput)
zxdg_output_v1_destroy(output->xdgOutput);
// Only a release tells the compositor; a plain destroy leaves the
// server object until the connection closes.
if (wl_output_get_version(output->output) >= WL_OUTPUT_RELEASE_SINCE_VERSION)
wl_output_release(output->output);
else
wl_output_destroy(output->output);
}
for (const auto &output : m_outputs)
releaseOutput(*output);
if (m_copyManager)
ext_image_copy_capture_manager_v1_destroy(m_copyManager);
if (m_dmabuf)

@ -1,7 +1,6 @@
#ifndef CUTEFISH_CAPTURECLIENT_H
#define CUTEFISH_CAPTURECLIENT_H
#include <QHash>
#include <QList>
#include <QObject>
#include <QRect>

@ -102,6 +102,8 @@ struct CaptureSessionListeners {
static void sessionStopped(void *data, ext_image_copy_capture_session_v1 *)
{
auto *self = static_cast<CaptureSession *>(data);
if (self->m_stopped)
return;
self->m_stopped = true;
emit self->stopped();
}
@ -154,8 +156,10 @@ struct CaptureSessionListeners {
self->freeBuffer(id);
qCDebug(cfCapture) << "frame failed, reason" << reason;
if (reason == EXT_IMAGE_COPY_CAPTURE_FRAME_V1_FAILURE_REASON_STOPPED) {
self->m_stopped = true;
emit self->stopped();
if (!self->m_stopped) {
self->m_stopped = true;
emit self->stopped();
}
} else {
// Unknown failures are retryable, and a buffer that no longer fits
// is replaced once the new constraints arrive.
@ -239,18 +243,15 @@ bool CaptureSession::capture()
if (m_stopped || m_frame || m_size.isEmpty())
return false;
int id = -1;
for (const auto &[candidate, buffer] : m_buffers) {
if (!buffer->held && !buffer->retired) {
id = candidate;
break;
}
}
if (id < 0)
const auto it = std::find_if(m_buffers.begin(), m_buffers.end(),
[](const auto &entry) {
return !entry.second->held && !entry.second->retired;
});
if (it == m_buffers.end())
return false;
Buffer *buffer = m_buffers.at(id).get();
m_frameBuffer = id;
Buffer *buffer = it->second.get();
m_frameBuffer = it->first;
m_frameTransform = 0;
m_framePresentedNs = 0;
m_frame = ext_image_copy_capture_session_v1_create_frame(m_session);

@ -19,7 +19,7 @@ struct ext_image_copy_capture_session_v1;
struct ext_image_copy_capture_frame_v1;
struct wl_buffer;
// One ext_image_copy_capture_v1 session into a pool of shared memory buffers.
// One ext_image_copy_capture_v1 session into a pool of shared memory or DMA-BUF buffers.
// A buffer handed out stays the consumer's until releaseBuffer().
class CaptureSession : public QObject
{

@ -3,7 +3,7 @@
include(CMakeFindDependencyMacro)
find_dependency(Qt6 COMPONENTS Core Gui DBus Qml)
# Without COMPONENTS every installed library is imported.
# Without COMPONENTS only Application, Appearance and IO are imported.
set(_cutefish_framework_components ${CutefishFramework_FIND_COMPONENTS})
if(NOT _cutefish_framework_components)
set(_cutefish_framework_components Application Appearance IO)

@ -1,13 +1,5 @@
qt_policy(SET QTP0001 NEW)
if(NOT DEFINED INSTALL_QMLDIR)
find_program(QT_PATHS_EXECUTABLE NAMES qtpaths6 REQUIRED)
execute_process(COMMAND ${QT_PATHS_EXECUTABLE} -query QT_INSTALL_QML
OUTPUT_VARIABLE INSTALL_QMLDIR
OUTPUT_STRIP_TRAILING_WHITESPACE
)
endif()
function(cutefish_install_qml_module target)
qt_query_qml_module("${target}"
TARGET_PATH module_path

@ -63,17 +63,22 @@ MprisController::MprisController(const QString &service, const QDBusConnection &
connect(m_mprisPlayerInterface, &MprisPlayerInterface::canPauseChanged, this, &MprisController::canPauseChanged);
connect(m_mprisPlayerInterface, &MprisPlayerInterface::canPlayChanged, this, &MprisController::canPlayChanged);
connect(m_mprisPlayerInterface, &MprisPlayerInterface::canSeekChanged, this, &MprisController::canSeekChanged);
connect(m_mprisPlayerInterface, &MprisPlayerInterface::canSeekChanged, this, &MprisController::canSeekChanged);
connect(m_mprisPlayerInterface, &MprisPlayerInterface::loopStatusChanged, this, &MprisController::loopStatusChanged);
connect(m_mprisPlayerInterface, &MprisPlayerInterface::maximumRateChanged, this, &MprisController::maximumRateChanged);
connect(m_mprisPlayerInterface, &MprisPlayerInterface::metadataChanged, this, &MprisController::metadataChanged);
connect(m_mprisPlayerInterface, &MprisPlayerInterface::minimumRateChanged, this, &MprisController::minimumRateChanged);
connect(m_mprisPlayerInterface, &MprisPlayerInterface::playbackStatusChanged, this, &MprisController::playbackStatusChanged);
connect(m_mprisPlayerInterface, &MprisPlayerInterface::positionChanged, this, &MprisController::onPositionChanged);
// An unchanged or failed Position reply emits no positionChanged; re-arm here.
connect(m_mprisPlayerInterface, &MprisPlayerInterface::asyncPropertyFinished, this, [this](const QString &propertyName) {
if (propertyName == QLatin1String("Position")) {
m_requestedPosition = false;
}
});
connect(m_mprisPlayerInterface, &MprisPlayerInterface::rateChanged, this, &MprisController::rateChanged);
connect(m_mprisPlayerInterface, &MprisPlayerInterface::shuffleChanged, this, &MprisController::shuffleChanged);
connect(m_mprisPlayerInterface, &MprisPlayerInterface::volumeChanged, this, &MprisController::volumeChanged);
connect(m_mprisPlayerInterface, &MprisPlayerInterface::seeked, this, &MprisController::seeked);
connect(m_mprisPlayerInterface, &MprisPlayerInterface::Seeked, this, &MprisController::seeked);
m_mprisPlayerInterface->setUseCache(true);
// This will initialize the properties, if needed
@ -305,7 +310,7 @@ bool MprisController::setPosition(const QString &aTrackId, qlonglong position)
}
QVariant length = m_mprisPlayerInterface->metadata()[Mpris::metadataToString(Mpris::Length)];
if (!length.isValid()) {
if (length.isValid()) {
qlonglong reportedLength = length.toLongLong();
if (position < 0 || position > reportedLength) {
@ -651,6 +656,8 @@ void MprisController::onAsyncGetAllPlayerPropertiesFinished()
void MprisController::onCanControlChanged()
{
Q_EMIT canControlChanged();
// On first reception, we are using a "GetAll" so we can skip this
if (m_canControlReceived) {
// I could disconnect and re-connect the signals so I avoid

@ -306,7 +306,7 @@ Q_SIGNALS: // SIGNALS
void rateChanged(double rate);
void shuffleChanged(bool shuffle);
void volumeChanged(double volume);
void seeked(qlonglong Position);
void Seeked(qlonglong Position);
private Q_SLOTS:
void onPropertyChanged(const QString &propertyName, const QVariant &value);

@ -416,7 +416,7 @@ void MprisManager::onServiceAppeared(const QString &service)
connect(controller.data(), SIGNAL(playbackStatusChanged()), m_playbackStatusMapper, SLOT(map()));
m_playbackStatusMapper->setMapping(controller.data(), controller->service());
// One mapper for every controller: connected once, not per player.
connect(m_playbackStatusMapper, SIGNAL(mapped(QString)), this, SLOT(onAvailableControllerPlaybackStatusChanged(QString)), Qt::UniqueConnection);
connect(m_playbackStatusMapper, &QSignalMapper::mappedString, this, &MprisManager::onAvailableControllerPlaybackStatusChanged, Qt::UniqueConnection);
}
if (m_currentController.isNull()) {
@ -434,7 +434,7 @@ void MprisManager::onServiceAppeared(const QString &service)
m_availableControllers.prepend(controller);
setCurrentController(controller);
} else {
m_availableControllers.insert(1, controller);
m_availableControllers.insert(qMin<qsizetype>(1, m_availableControllers.size()), controller);
if (controller->playbackStatus() == Mpris::Playing) {
m_otherPlayingControllers.prepend(controller);
}
@ -471,7 +471,9 @@ void MprisManager::onServiceVanished(const QString &service)
void MprisManager::onAvailableControllerPlaybackStatusChanged(const QString &service)
{
QSharedPointer<MprisController> controller = availableController(service);
Q_ASSERT(!controller.isNull());
if (controller.isNull()) {
return;
}
if (m_currentController == controller) {
if (m_currentController->playbackStatus() == Mpris::Playing) {
@ -480,7 +482,11 @@ void MprisManager::onAvailableControllerPlaybackStatusChanged(const QString &ser
if (!m_otherPlayingControllers.isEmpty()) {
QSharedPointer<MprisController> currentController = m_otherPlayingControllers.takeFirst();
m_availableControllers.move(m_availableControllers.indexOf(currentController), 0);
const qsizetype index = m_availableControllers.indexOf(currentController);
if (index >= 0) {
m_availableControllers.move(index, 0);
}
setCurrentController(currentController);
}
} else {
@ -493,7 +499,7 @@ void MprisManager::onAvailableControllerPlaybackStatusChanged(const QString &ser
&& m_currentController->playbackStatus() != Mpris::Playing) {
setCurrentController(controller);
} else {
m_availableControllers.move(m_availableControllers.indexOf(controller), 1);
m_availableControllers.move(m_availableControllers.indexOf(controller), qMin<qsizetype>(1, m_availableControllers.size() - 1));
m_otherPlayingControllers.removeAll(controller); // Just in case, shouldn't be needed
m_otherPlayingControllers.prepend(controller);
}
@ -583,6 +589,7 @@ void MprisManager::setCurrentController(QSharedPointer<MprisController> controll
connect(m_currentController.data(), &MprisController::metadataChanged, this, &MprisManager::metadataChanged);
connect(m_currentController.data(), &MprisController::minimumRateChanged, this, &MprisManager::minimumRateChanged);
connect(m_currentController.data(), &MprisController::playbackStatusChanged, this, &MprisManager::playbackStatusChanged);
connect(m_currentController.data(), &MprisController::positionChanged, this, &MprisManager::positionChanged);
connect(m_currentController.data(), &MprisController::rateChanged, this, &MprisManager::rateChanged);
connect(m_currentController.data(), &MprisController::shuffleChanged, this, &MprisManager::shuffleChanged);
connect(m_currentController.data(), &MprisController::volumeChanged, this, &MprisManager::volumeChanged);
@ -594,6 +601,31 @@ void MprisManager::setCurrentController(QSharedPointer<MprisController> controll
}
Q_EMIT currentServiceChanged();
// The forwarded properties read the new controller; notify them all.
Q_EMIT canQuitChanged();
Q_EMIT canRaiseChanged();
Q_EMIT canSetFullscreenChanged();
Q_EMIT desktopEntryChanged();
Q_EMIT fullscreenChanged();
Q_EMIT hasTrackListChanged();
Q_EMIT identityChanged();
Q_EMIT supportedUriSchemesChanged();
Q_EMIT supportedMimeTypesChanged();
Q_EMIT canControlChanged();
Q_EMIT canGoNextChanged();
Q_EMIT canGoPreviousChanged();
Q_EMIT canPauseChanged();
Q_EMIT canPlayChanged();
Q_EMIT canSeekChanged();
Q_EMIT loopStatusChanged();
Q_EMIT maximumRateChanged();
Q_EMIT metadataChanged();
Q_EMIT minimumRateChanged();
Q_EMIT playbackStatusChanged();
Q_EMIT rateChanged();
Q_EMIT shuffleChanged();
Q_EMIT volumeChanged();
}
bool MprisManager::checkController(const char *callerName) const

@ -246,10 +246,12 @@ void MprisPlayerAdaptor::OpenUri(const QString &Uri)
QUrl url(Uri, QUrl::StrictMode);
if (!url.isValid()) {
player->sendErrorReply(QDBusError::InvalidArgs, QStringLiteral("Wanted to open an url but the url is invalid."));
return;
}
if (!player->supportedUriSchemes().contains(url.scheme())) {
player->sendErrorReply(QDBusError::NotSupported, QStringLiteral("Wanted to open an url but the scheme is not supported."));
return;
}
QMimeDatabase db;

@ -103,7 +103,7 @@ void MprisPlayerInterface::onPropertyChanged(const QString &propertyName, const
Q_EMIT loopStatusChanged(m_loopStatus);
}
} else if (propertyName == QStringLiteral("MaximumRate")) {
bool maximumRate = value.toDouble();
double maximumRate = value.toDouble();
if (m_maximumRate != maximumRate) {
m_maximumRate = maximumRate;
Q_EMIT maximumRateChanged(m_maximumRate);

@ -92,16 +92,16 @@ void MprisRootInterface::onPropertyChanged(const QString &propertyName, const QV
Q_EMIT identityChanged(m_identity);
}
} else if (propertyName == QStringLiteral("SupportedMimeTypes")) {
QStringList supportedUriSchemes = value.toStringList();
if (m_supportedUriSchemes != supportedUriSchemes) {
m_supportedUriSchemes = supportedUriSchemes;
Q_EMIT supportedMimeTypesChanged(m_supportedUriSchemes);
}
} else if (propertyName == QStringLiteral("SupportedUriSchemes")) {
QStringList supportedMimeTypes = value.toStringList();
if (m_supportedMimeTypes != supportedMimeTypes) {
m_supportedMimeTypes = supportedMimeTypes;
Q_EMIT supportedUriSchemesChanged(m_supportedMimeTypes);
Q_EMIT supportedMimeTypesChanged(m_supportedMimeTypes);
}
} else if (propertyName == QStringLiteral("SupportedUriSchemes")) {
QStringList supportedUriSchemes = value.toStringList();
if (m_supportedUriSchemes != supportedUriSchemes) {
m_supportedUriSchemes = supportedUriSchemes;
Q_EMIT supportedUriSchemesChanged(m_supportedUriSchemes);
}
} else {
qWarning() << Q_FUNC_INFO

@ -95,13 +95,13 @@ bool AppletProxyModel::lessThan(const QModelIndex &left, const QModelIndex &righ
return QString::localeAwareCompare(value(left, "name").toString(), value(right, "name").toString()) > 0;
// Sorted descending: whatever compares greater comes first.
const bool leftConnected = value(left, "connectionState").toInt() == NetworkModel::Activated;
const bool rightConnected = value(right, "connectionState").toInt() == NetworkModel::Activated;
const int leftState = value(left, "connectionState").toInt();
const int rightState = value(right, "connectionState").toInt();
const bool leftConnected = leftState == NetworkModel::Activated;
const bool rightConnected = rightState == NetworkModel::Activated;
if (leftConnected != rightConnected)
return rightConnected;
const int leftState = value(left, "connectionState").toInt();
const int rightState = value(right, "connectionState").toInt();
if (leftState != rightState)
return leftState > rightState;

@ -459,8 +459,6 @@ void Handler::importVpn(const QUrl &file)
// NetworkManager's own readers, the ones nmcli and both big desktops use; whichever
// reads the file first wins.
QStringList types;
QStringList plugins = VpnUtils::installedPlugins();
const QString suffix = QFileInfo(path).suffix().toLower();
const QString likely = suffix == QLatin1String("ovpn") || suffix == QLatin1String("conf") ? QStringLiteral("openvpn")
@ -468,14 +466,13 @@ void Handler::importVpn(const QUrl &file)
: QString();
if (plugins.removeAll(likely))
plugins.prepend(likely);
types += plugins;
if (types.isEmpty()) {
if (plugins.isEmpty()) {
Q_EMIT vpnAddFailed(tr("No VPN plugin can read this file. Install the one for your VPN, "
"for example network-manager-openvpn for OpenVPN."));
return;
}
importVpnAs(path, types, QString());
importVpnAs(path, plugins, QString());
}
void Handler::importVpnAs(const QString &file, QStringList types, const QString &error)

@ -119,17 +119,6 @@ QVariant NetworkClient::setting(const QString &connection, const QString &settin
return plain(m_settings.value(connection).value(setting).value(key));
}
QString NetworkClient::connectionByUuid(const QString &uuid) const
{
if (uuid.isEmpty())
return {};
for (auto it = m_settings.cbegin(); it != m_settings.cend(); ++it) {
if (it->value(QStringLiteral("connection")).value(QStringLiteral("uuid")).toString() == uuid)
return it.key();
}
return {};
}
QString NetworkClient::hotspotConnection() const
{
for (const QString &connection : connections()) {
@ -233,7 +222,6 @@ void NetworkClient::reset()
const QStringList paths = m_objects.keys();
m_objects.clear();
m_settings.clear();
m_unreadable.clear();
m_settingsRequests.clear();
m_permissions.clear();
@ -344,7 +332,6 @@ void NetworkClient::interfacesRemoved(const QDBusObjectPath &path, const QString
if (interfaces.contains(connectionInterface)) {
m_settings.remove(objectPath);
m_unreadable.remove(objectPath);
m_settingsRequests.remove(objectPath);
}
@ -406,13 +393,10 @@ void NetworkClient::fetchSettings(const QString &path)
m_settingsRequests.remove(path);
// Another user's private profile: it exists, but its settings are not ours to read.
if (reply.type() == QDBusMessage::ReplyMessage) {
if (reply.type() == QDBusMessage::ReplyMessage)
m_settings.insert(path, qdbus_cast<NMVariantMapMap>(reply.arguments().value(0)));
m_unreadable.remove(path);
} else {
else
m_settings.remove(path);
m_unreadable.insert(path);
}
updateReady();
scheduleChanged();
});

@ -6,7 +6,6 @@
#include <QDBusMessage>
#include <QHash>
#include <QObject>
#include <QSet>
#include <QTimer>
#include <functional>
@ -50,7 +49,6 @@ public:
// As NetworkManager sent them, so an Update hands back the D-Bus types it was given.
NMVariantMapMap settings(const QString &connection) const;
QVariant setting(const QString &connection, const QString &setting, const QString &key) const;
QString connectionByUuid(const QString &uuid) const;
// The profile that shares this machine's connection over Wi-Fi.
QString hotspotConnection() const;
@ -105,7 +103,6 @@ private:
bool m_ready = false;
QHash<QString, QHash<QString, QVariantMap>> m_objects;
QHash<QString, NMVariantMapMap> m_settings;
QSet<QString> m_unreadable;
// The newest GetSettings per profile; an older reply is stale.
QHash<QString, quint64> m_settingsRequests;
quint64 m_permissionsRequest = 0;

@ -6,33 +6,6 @@
namespace {
QDBusArgument &operator<<(QDBusArgument &argument, const NMObjects &objects)
{
argument.beginMap(QMetaType::fromType<QDBusObjectPath>(), QMetaType::fromType<NMVariantMapMap>());
for (auto it = objects.cbegin(); it != objects.cend(); ++it) {
argument.beginMapEntry();
argument << it.key() << it.value();
argument.endMapEntry();
}
argument.endMap();
return argument;
}
const QDBusArgument &operator>>(const QDBusArgument &argument, NMObjects &objects)
{
argument.beginMap();
while (!argument.atEnd()) {
QDBusObjectPath path;
NMVariantMapMap interfaces;
argument.beginMapEntry();
argument >> path >> interfaces;
argument.endMapEntry();
objects.insert(path, interfaces);
}
argument.endMap();
return argument;
}
QVariant read(const QDBusArgument &argument)
{
switch (argument.currentType()) {

@ -34,7 +34,6 @@ inline const QString connectionInterface =
QStringLiteral("org.freedesktop.NetworkManager.Settings.Connection");
inline const QString activeInterface = QStringLiteral("org.freedesktop.NetworkManager.Connection.Active");
inline const QString deviceInterface = QStringLiteral("org.freedesktop.NetworkManager.Device");
inline const QString wiredInterface = QStringLiteral("org.freedesktop.NetworkManager.Device.Wired");
inline const QString wirelessInterface = QStringLiteral("org.freedesktop.NetworkManager.Device.Wireless");
inline const QString statisticsInterface =
QStringLiteral("org.freedesktop.NetworkManager.Device.Statistics");
@ -42,7 +41,6 @@ inline const QString accessPointInterface = QStringLiteral("org.freedesktop.Netw
inline const QString ip4ConfigInterface = QStringLiteral("org.freedesktop.NetworkManager.IP4Config");
inline const QString ip6ConfigInterface = QStringLiteral("org.freedesktop.NetworkManager.IP6Config");
inline const QString agentManagerInterface = QStringLiteral("org.freedesktop.NetworkManager.AgentManager");
inline const QString secretAgentInterface = QStringLiteral("org.freedesktop.NetworkManager.SecretAgent");
inline const QString propertiesInterface = QStringLiteral("org.freedesktop.DBus.Properties");
inline const QString objectManagerInterface = QStringLiteral("org.freedesktop.DBus.ObjectManager");

@ -93,6 +93,11 @@ QDateTime lastUsed(const NetworkClient *client, const QString &profile)
return seconds ? QDateTime::fromSecsSinceEpoch(qint64(seconds)) : QDateTime();
}
QString profileOf(const NetworkClient *client, const QString &active)
{
return NetworkClient::path(client->property(active, activeInterface, QStringLiteral("Connection")));
}
// The profiles NetworkManager would activate on the device, and the one it has active.
QStringList profilesFor(const NetworkClient *client, const QString &device, const QString &type)
{
@ -100,7 +105,7 @@ QStringList profilesFor(const NetworkClient *client, const QString &device, cons
QStringList candidates = client->property(device, deviceInterface, QStringLiteral("AvailableConnections")).toStringList();
const QString active = client->activeConnection(device);
if (!active.isEmpty())
candidates << NetworkClient::path(client->property(active, activeInterface, QStringLiteral("Connection")));
candidates << profileOf(client, active);
for (const QString &profile : std::as_const(candidates)) {
if (profiles.contains(profile) || !client->readable(profile) || isPort(client, profile)
@ -133,8 +138,7 @@ NetworkModel::NetworkModel(QObject *parent)
connect(m_client, &NetworkClient::changed, this, &NetworkModel::rebuild);
connect(m_client, &NetworkClient::activeStateChanged, this,
[this](const QString &active, uint state, uint reason) {
const QString profile = NetworkClient::path(
m_client->property(active, activeInterface, QStringLiteral("Connection")));
const QString profile = profileOf(m_client, active);
if (!VpnUtils::isVpn(m_client, profile))
return;
if (state == ActiveActivating) {
@ -348,7 +352,7 @@ void NetworkModel::rebuild()
void NetworkModel::addWired(const QString &device, QList<Item> &items) const
{
const QString active = m_client->activeConnection(device);
const QString activeProfile = NetworkClient::path(m_client->property(active, activeInterface, QStringLiteral("Connection")));
const QString activeProfile = profileOf(m_client, active);
for (const QString &profile : profilesFor(m_client, device, QStringLiteral("802-3-ethernet"))) {
Item item;
@ -392,7 +396,7 @@ void NetworkModel::addWireless(const QString &device, QList<Item> &items) const
const uint capabilities = m_client->property(device, wirelessInterface, QStringLiteral("WirelessCapabilities")).toUInt();
const QString activeAp = NetworkClient::path(m_client->property(device, wirelessInterface, QStringLiteral("ActiveAccessPoint")));
const QString active = m_client->activeConnection(device);
const QString activeProfile = NetworkClient::path(m_client->property(active, activeInterface, QStringLiteral("Connection")));
const QString activeProfile = profileOf(m_client, active);
const int activeState = active.isEmpty() ? int(Deactivated)
: m_client->property(active, activeInterface, QStringLiteral("State")).toInt();
@ -439,7 +443,7 @@ void NetworkModel::addWireless(const QString &device, QList<Item> &items) const
network.security = security;
}
QList<QString> unmatched;
bool activeUnlisted = false;
for (const QString &profile : profilesFor(m_client, device, wirelessSetting)) {
const QString mode = m_client->setting(profile, wirelessSetting, QStringLiteral("mode")).toString();
if (!mode.isEmpty() && mode != QLatin1String("infrastructure"))
@ -464,7 +468,7 @@ void NetworkModel::addWireless(const QString &device, QList<Item> &items) const
index = sameName;
if (index < 0) {
if (isActive)
unmatched << profile;
activeUnlisted = true;
continue;
}
@ -516,13 +520,13 @@ void NetworkModel::addWireless(const QString &device, QList<Item> &items) const
}
// Connected to a network no scan has listed, a hidden one for instance.
for (const QString &profile : std::as_const(unmatched)) {
if (activeUnlisted) {
Network network;
network.ssid = WifiUtils::ssidKey(m_client->setting(profile, wirelessSetting, QStringLiteral("ssid")).toByteArray());
network.profile = profile;
network.timestamp = lastUsed(m_client, profile);
network.ssid = WifiUtils::ssidKey(m_client->setting(activeProfile, wirelessSetting, QStringLiteral("ssid")).toByteArray());
network.profile = activeProfile;
network.timestamp = lastUsed(m_client, activeProfile);
network.accessPoint = activeAp;
switch (profileClass(m_client, profile)) {
switch (profileClass(m_client, activeProfile)) {
case SecurityClass::Open:
network.security = Enums::NoneSecurity;
break;
@ -538,7 +542,7 @@ void NetworkModel::addWireless(const QString &device, QList<Item> &items) const
}
if (!activeAp.isEmpty())
network.strength = ap(activeAp, "Strength").toInt();
addItem(network, device + QLatin1Char('|') + profile);
addItem(network, device + QLatin1Char('|') + activeProfile);
}
}
@ -587,13 +591,19 @@ void NetworkModel::addVpns(QList<Item> &items) const
void NetworkModel::fillDetails(Item &item) const
{
// Addresses are those of the device the network is up on.
// Addresses are those of the device the network is up on; a VPN's come from its
// active connection, as its row has no device.
if (item.connectionState != Activated)
return;
const QString device = item.devicePath;
const QString ip4 = NetworkClient::path(m_client->property(device, deviceInterface, QStringLiteral("Ip4Config")));
const QString ip6 = NetworkClient::path(m_client->property(device, deviceInterface, QStringLiteral("Ip6Config")));
const QString vpn = item.type == Enums::Vpn ? VpnUtils::activeConnection(m_client, item.connectionPath) : QString();
const auto config = [&](const QString &name) {
return NetworkClient::path(vpn.isEmpty() ? m_client->property(device, deviceInterface, name)
: m_client->property(vpn, activeInterface, name));
};
const QString ip4 = config(QStringLiteral("Ip4Config"));
const QString ip6 = config(QStringLiteral("Ip6Config"));
item.ipV4Address = firstAddress(m_client->property(ip4, ip4ConfigInterface, QStringLiteral("AddressData")).toList(), false);
item.ipV6Address = firstAddress(m_client->property(ip6, ip6ConfigInterface, QStringLiteral("AddressData")).toList(), true);

@ -608,13 +608,24 @@ void NetworkSecretAgent::finish()
Q_EMIT requestChanged();
}
void NetworkSecretAgent::dismiss(const std::function<bool(const Request &)> &matches)
void NetworkSecretAgent::dismissStalePrompts()
{
if (!m_client->ready())
return;
// A prompt for a network that went out of range, a device that went away, or a device
// that started connecting elsewhere is out of date.
bool headChanged = false;
for (int i = m_requests.size() - 1; i >= 0; --i) {
const Request &request = m_requests.at(i);
if (request.accessPointPath.isEmpty() || !matches(request))
if (request.accessPointPath.isEmpty())
continue;
if (m_client->has(request.accessPointPath, accessPointInterface)
&& m_client->has(request.devicePath, deviceInterface)) {
const QString active = m_client->activeConnection(request.devicePath);
if (active.isEmpty() || active == request.deviceActive)
continue;
}
m_requests.removeAt(i);
headChanged |= i == 0;
}
@ -623,20 +634,4 @@ void NetworkSecretAgent::dismiss(const std::function<bool(const Request &)> &mat
Q_EMIT requestChanged();
}
void NetworkSecretAgent::dismissStalePrompts()
{
if (!m_client->ready())
return;
// A prompt for a network that went out of range, a device that went away, or a device
// that started connecting elsewhere is out of date.
dismiss([this](const Request &request) {
if (!m_client->has(request.accessPointPath, accessPointInterface)
|| !m_client->has(request.devicePath, deviceInterface))
return true;
const QString active = m_client->activeConnection(request.devicePath);
return !active.isEmpty() && active != request.deviceActive;
});
}
#include "secretagent.moc"

@ -7,7 +7,6 @@
#include <QDBusMessage>
#include <QList>
#include <QPointer>
#include <functional>
#include <QtQml/qqmlregistration.h>
@ -109,7 +108,6 @@ private:
void fail(const Request &request, const QString &error, const QString &text);
void ask(const Request &request);
void finish();
void dismiss(const std::function<bool(const Request &)> &matches);
void dismissStalePrompts();
// Owned by the application, which may tear it down first.

@ -158,16 +158,16 @@ void SecretStore::save(const QString &uuid, const QString &connectionName,
const QString &settingName, const QString &key, const QString &value,
std::function<void()> done)
{
const auto finish = [done](const QString &session, SecretStore *store) {
const auto finish = [this, done](const QString &session) {
if (!session.isEmpty())
store->closeSession(session);
closeSession(session);
if (done)
done();
};
openSession([=, this](const QString &session) {
if (session.isEmpty()) {
finish(session, this);
finish(session);
return;
}
@ -176,13 +176,13 @@ void SecretStore::save(const QString &uuid, const QString &connectionName,
const auto collection = reply.arguments().value(0).value<QDBusObjectPath>();
if (reply.type() != QDBusMessage::ReplyMessage || collection.path() == noPrompt) {
qWarning() << "Secret Service has no default collection to store" << key << "in";
finish(session, this);
finish(session);
return;
}
unlock({collection}, [=, this](const Paths &unlocked) {
if (unlocked.isEmpty()) {
finish(session, this);
finish(session);
return;
}
@ -211,10 +211,10 @@ void SecretStore::save(const QString &uuid, const QString &connectionName,
if (reply.type() == QDBusMessage::ReplyMessage
&& promptPath.path() != noPrompt)
prompt(promptPath, [=, this](bool, const QVariant &) {
finish(session, this);
finish(session);
});
else
finish(session, this);
finish(session);
});
});
});

@ -5,7 +5,6 @@
#include <QDBusArgument>
#include <QDBusConnection>
#include <QDBusMessage>
#include <QDBusMetaType>
#include <QDBusObjectPath>
#include <QDBusVariant>
#include <QDBusVirtualObject>
@ -50,7 +49,6 @@ public:
: m_bus(bus)
{
registerNetworkDBusTypes();
qDBusRegisterMetaType<QList<QVariantMap>>();
objects[FakeNM::managerPath][FakeNM::manager] = {
{QStringLiteral("Version"), QStringLiteral("1.52.1")},
{QStringLiteral("State"), 70u},

@ -150,11 +150,7 @@ private slots:
handler.addAndActivateConnection(wifiDevice, office, QStringLiteral("wrong pass"));
QTRY_COMPARE(nm->count(QStringLiteral("AddAndActivateConnection")), 1);
QTRY_COMPARE(NetworkClient::instance()->activeConnections().size(), 2);
QString active;
for (const QString &path : NetworkClient::instance()->activeConnections()) {
if (path != wiredActive)
active = path;
}
const QString active = otherActive();
QVERIFY(!active.isEmpty());
// NM_ACTIVE_CONNECTION_STATE_DEACTIVATED for NO_SECRETS.
@ -229,11 +225,7 @@ private slots:
QTRY_VERIFY(handler.hotspotActive());
// The next start updates that profile rather than adding another.
QString active;
for (const QString &path : NetworkClient::instance()->activeConnections()) {
if (path != wiredActive)
active = path;
}
const QString active = otherActive();
nm->remove(active);
QTRY_VERIFY(!handler.hotspotActive());
configuration.setHotspotName(QStringLiteral("Desk 2"));
@ -254,11 +246,7 @@ private slots:
QSignalSpy failed(&handler, &Handler::hotspotFailed);
handler.createHotspot();
QTRY_COMPARE(NetworkClient::instance()->activeConnections().size(), 2);
QString active;
for (const QString &path : NetworkClient::instance()->activeConnections()) {
if (path != wiredActive)
active = path;
}
const QString active = otherActive();
// NM_ACTIVE_CONNECTION_STATE_DEACTIVATED for DEVICE_DISCONNECTED.
QTest::qWait(100);
@ -484,6 +472,16 @@ private:
return {};
}
static QString otherActive()
{
QString active;
for (const QString &path : NetworkClient::instance()->activeConnections()) {
if (path != wiredActive)
active = path;
}
return active;
}
std::unique_ptr<QDBusConnection> server;
std::unique_ptr<FakeNetworkManager> nm;
};

@ -127,6 +127,17 @@ private slots:
QCOMPARE(peers.first().value(QStringLiteral("allowed-ips")).toStringList(),
(QStringList {QStringLiteral("0.0.0.0/0"), QStringLiteral("::/0")}));
const NMVariantMapMap ipv6Server = VpnUtils::manualProfile({{QStringLiteral("kind"), QStringLiteral("wireguard")},
{QStringLiteral("server"), QStringLiteral("2001:db8::1")},
{QStringLiteral("privateKey"), QStringLiteral("yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=")},
{QStringLiteral("publicKey"), QStringLiteral("xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=")},
{QStringLiteral("address"), QStringLiteral("10.0.0.2/24")}},
&error);
QVERIFY2(!ipv6Server.isEmpty(), qPrintable(error));
QCOMPARE(ipv6Server.value(QStringLiteral("wireguard")).value(QStringLiteral("peers")).value<QList<QVariantMap>>()
.first().value(QStringLiteral("endpoint")).toString(),
QStringLiteral("[2001:db8::1]:51820"));
QVERIFY(VpnUtils::manualProfile({{QStringLiteral("kind"), QStringLiteral("pptp")},
{QStringLiteral("server"), QStringLiteral("x")}},
&error)

@ -63,10 +63,6 @@ private slots:
QVERIFY(!WifiUtils::pskValid(QStringLiteral("1234567")));
QVERIFY(WifiUtils::pskValid(QString(64, QLatin1Char('a'))));
QVERIFY(!WifiUtils::pskValid(QString(64, QLatin1Char('g'))));
QVERIFY(WifiUtils::wepKeyValid(QStringLiteral("abcde")));
QVERIFY(WifiUtils::wepKeyValid(QStringLiteral("0123456789")));
QVERIFY(!WifiUtils::wepKeyValid(QStringLiteral("012345678g")));
QVERIFY(!WifiUtils::wepKeyValid(QStringLiteral("abcdef")));
QCOMPARE(WifiUtils::generatedPassword().size(), 12);
QVERIFY(WifiUtils::pskValid(WifiUtils::generatedPassword()));
}

@ -1,7 +1,6 @@
#include "vpnutils.h"
#include "networkclient.h"
#include <QDBusArgument>
#include <QDateTime>
#include <QDir>
#include <QFile>
@ -377,16 +376,16 @@ NMVariantMapMap wireGuardProfile(const QByteArray &content, const QString &name,
uint fwmark = 0;
uint mtu = 0;
bool peerRoutes = true;
QVariantList addresses[2];
QVariantList dns[2];
QList<QVariantMap> addresses[2];
QList<uint> dns4;
QList<QByteArray> dns6;
QStringList search;
QList<QVariantMap> peers;
// Like `wg setconf`: whitespace anywhere is dropped and '#' starts a comment.
const QStringList lines = QString::fromUtf8(content).split(QLatin1Char('\n'));
for (int number = 1; number <= lines.size(); ++number) {
QString line = lines.at(number - 1);
line = line.left(line.indexOf(QLatin1Char('#')) < 0 ? line.size() : line.indexOf(QLatin1Char('#')));
QString line = lines.at(number - 1).section(QLatin1Char('#'), 0, 0);
line.remove(QRegularExpression(QStringLiteral("\\s")));
if (line.isEmpty())
continue;
@ -434,9 +433,9 @@ NMVariantMapMap wireGuardProfile(const QByteArray &content, const QString &name,
if (address.isNull())
search << word;
else if (address.protocol() == QAbstractSocket::IPv4Protocol)
dns[0] << qToBigEndian(address.toIPv4Address());
dns4 << qToBigEndian(address.toIPv4Address());
else
dns[1] << QByteArray(reinterpret_cast<const char *>(address.toIPv6Address().c), 16);
dns6 << QByteArray(reinterpret_cast<const char *>(address.toIPv6Address().c), 16);
}
} else if (section == Interface && field.compare(QLatin1String("MTU"), Qt::CaseInsensitive) == 0) {
mtu = value.toUInt(&ok);
@ -515,24 +514,12 @@ NMVariantMapMap wireGuardProfile(const QByteArray &content, const QString &name,
QVariantMap ip;
ip.insert(QStringLiteral("method"), addresses[family].isEmpty() ? QStringLiteral("disabled") : QStringLiteral("manual"));
if (!addresses[family].isEmpty()) {
QList<QVariantMap> data;
for (const QVariant &address : std::as_const(addresses[family]))
data << address.toMap();
ip.insert(QStringLiteral("address-data"), QVariant::fromValue(data));
ip.insert(QStringLiteral("address-data"), QVariant::fromValue(addresses[family]));
// DNS needs an address of its family; without search domains, the server
// answers for every name, as wg-quick has it.
if (!dns[family].isEmpty()) {
if (family == 0) {
QList<uint> servers;
for (const QVariant &server : std::as_const(dns[0]))
servers << server.toUInt();
ip.insert(QStringLiteral("dns"), QVariant::fromValue(servers));
} else {
QList<QByteArray> servers;
for (const QVariant &server : std::as_const(dns[1]))
servers << server.toByteArray();
ip.insert(QStringLiteral("dns"), QVariant::fromValue(servers));
}
const bool hasDns = family == 0 ? !dns4.isEmpty() : !dns6.isEmpty();
if (hasDns) {
ip.insert(QStringLiteral("dns"), family == 0 ? QVariant::fromValue(dns4) : QVariant::fromValue(dns6));
ip.insert(QStringLiteral("dns-search"), search.isEmpty() ? QStringList {QStringLiteral("~")} : search);
}
}
@ -577,6 +564,10 @@ NMVariantMapMap manualProfile(const QVariantMap &form, QString *error)
if (kind == QLatin1String("wireguard")) {
// Written out as a wg-quick file, so the form is checked the way imports are.
QString endpoint = server;
// NetworkManager takes a bare IPv6 address's last group for the port, so it needs brackets.
if (QHostAddress(server).protocol() == QAbstractSocket::IPv6Protocol
&& QHostAddress(server.section(QLatin1Char(':'), 0, -2)).isNull())
endpoint = QStringLiteral("[%1]").arg(server);
if (!endpoint.contains(QLatin1Char(':')) || endpoint.endsWith(QLatin1Char(']')))
endpoint += QStringLiteral(":51820");
QString config = QStringLiteral("[Interface]\nPrivateKey = %1\nAddress = %2\n")

@ -322,24 +322,6 @@ bool pskValid(const QString &psk)
return bytes.size() >= 8 && bytes.size() <= 63;
}
bool wepKeyValid(const QString &key)
{
if (key.size() == 10 || key.size() == 26) {
for (const QChar c : key) {
if (c.unicode() > 0x7f || !std::isxdigit(c.unicode()))
return false;
}
return true;
}
if (key.size() != 5 && key.size() != 13)
return false;
for (const QChar c : key) {
if (c.unicode() < 0x20 || c.unicode() > 0x7e)
return false;
}
return true;
}
QString generatedPassword()
{
// nmcli's alphabet, without the characters that read like others.

@ -31,7 +31,6 @@ CUTEFISH_NETWORK_EXPORT int strengthStep(int strength);
CUTEFISH_NETWORK_EXPORT QString strengthIcon(int strength);
CUTEFISH_NETWORK_EXPORT bool pskValid(const QString &psk);
CUTEFISH_NETWORK_EXPORT bool wepKeyValid(const QString &key);
CUTEFISH_NETWORK_EXPORT QString generatedPassword();
} // namespace WifiUtils

@ -4,7 +4,6 @@
#include "batterystatus.h"
#include <QDBusConnection>
#include <QDBusConnectionInterface>
#include <QSignalSpy>
#include <QTest>

@ -75,7 +75,6 @@ if(BUILD_TESTING)
find_package(Qt6 REQUIRED COMPONENTS Test)
add_executable(display-outputmodel-test tests/outputmodel_test.cpp)
target_link_libraries(display-outputmodel-test PRIVATE cutefish-framework-screen Qt6::Test)
target_include_directories(display-outputmodel-test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
add_test(NAME display-outputmodel-test COMMAND display-outputmodel-test)
add_executable(display-client-test tests/displayclient_test.cpp)
target_link_libraries(display-client-test PRIVATE Cutefish::Display Qt6::Test)
@ -89,7 +88,6 @@ if(BUILD_TESTING)
target_compile_definitions(display-screen-integration-test PRIVATE
SCREEN_PROBE_PATH="$<TARGET_FILE:display-wayland-screen-probe>")
target_link_libraries(display-screen-integration-test PRIVATE cutefish-framework-screen Qt6::Test)
target_include_directories(display-screen-integration-test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
add_test(NAME display-screen-integration-test
COMMAND dbus-run-session -- $<TARGET_FILE:display-screen-integration-test>)
set_tests_properties(display-screen-integration-test PROPERTIES

@ -65,7 +65,7 @@ void DisplayClient::connectInterface()
m_state = {};
m_loaded = false;
emit stateChanged();
if (m_applyPending || !m_operationToken.isEmpty() || m_completion)
if (operationPending())
finishOperation(m_operationToken,
{false, false, 1, tr("The display service restarted."), {}});
delete m_interface;
@ -124,7 +124,7 @@ void DisplayClient::refresh()
m_state = {};
emit stateChanged();
emit availabilityChanged();
if (m_applyPending || !m_operationToken.isEmpty() || m_completion)
if (operationPending())
finishOperation(m_operationToken, {false, false, 1, reply.error().message(), {}});
return;
}
@ -143,7 +143,7 @@ QString DisplayClient::apply(const QList<ipc::DisplayRequest> &displays, const Q
const QList<ipc::Placement> &placements,
const ipc::MirrorGroups &mirrorGroups)
{
if (!available() || m_applyPending || !m_operationToken.isEmpty() || m_completion)
if (!available() || operationPending())
return {};
m_applyPending = true;
m_operationToken.clear();
@ -166,10 +166,8 @@ QString DisplayClient::apply(const QList<ipc::DisplayRequest> &displays, const Q
} else {
m_operationToken = reply.value();
const auto result = m_earlyResults.constFind(m_operationToken);
if (result != m_earlyResults.cend()) {
const auto completed = *result;
queueCompletion(m_operationToken, completed);
}
if (result != m_earlyResults.cend())
queueCompletion(m_operationToken, *result);
m_earlyResults.clear();
}
});
@ -188,6 +186,11 @@ void DisplayClient::finishOperation(const QString &token, const OperationResult
result.message, result.affected);
}
bool DisplayClient::operationPending() const
{
return m_applyPending || !m_operationToken.isEmpty() || m_completion;
}
void DisplayClient::queueCompletion(const QString &token, const OperationResult &result)
{
m_operationToken = token;

@ -85,6 +85,9 @@ private:
bool sendRequest(const QDBusPendingCall &call, Completion done);
void failRequests();
// An apply waits for its reply, its OperationFinished, or the state that shows it.
bool operationPending() const;
struct OperationResult {
bool succeeded;
bool degraded;

@ -97,8 +97,6 @@ QVariant DisplayOutputModel::data(const QModelIndex &index, int role) const
return {};
const Entry &e = m_entries.at(index.row());
const auto *mode = selectedMode(e);
const auto resolutions = resolutionModes(e);
const auto refreshes = refreshModes(e);
switch (role) {
case Qt::DisplayRole:
return name(e);
@ -122,15 +120,18 @@ QVariant DisplayOutputModel::data(const QModelIndex &index, int role) const
return e.rotation == 0 ? 1 : e.rotation == 1 ? 2 : e.rotation == 2 ? 4 : 8;
case ScaleRole:
return e.scale120 / 120.0;
case ResolutionIndexRole:
case ResolutionIndexRole: {
const auto resolutions = resolutionModes(e);
for (int i = 0; i < resolutions.size(); ++i) {
const auto &m = e.physical.modes.at(resolutions.at(i));
if (mode && m.width == mode->width && m.height == mode->height)
return i;
}
return 0;
case RecommendedResolutionIndexRole:
}
case RecommendedResolutionIndexRole: {
// The display's preferred mode, the one the compositor starts it in.
const auto resolutions = resolutionModes(e);
for (int i = 0; i < resolutions.size(); ++i) {
const auto &m = e.physical.modes.at(resolutions.at(i));
for (const auto &preferred : e.physical.modes) {
@ -140,9 +141,11 @@ QVariant DisplayOutputModel::data(const QModelIndex &index, int role) const
}
}
return -1;
}
case RecommendedScaleRole:
return e.physical.recommendedScale120 / 120.0;
case ResolutionsRole: {
const auto resolutions = resolutionModes(e);
QStringList v;
for (int i : resolutions) {
const auto &m = e.physical.modes.at(i);
@ -151,8 +154,9 @@ QVariant DisplayOutputModel::data(const QModelIndex &index, int role) const
return v;
}
case RefreshRateIndexRole:
return refreshes.indexOf(e.modeIndex);
return refreshModes(e).indexOf(e.modeIndex);
case RefreshRatesRole: {
const auto refreshes = refreshModes(e);
QStringList v;
for (int i : refreshes)
v.append(QStringLiteral("%1 Hz").arg(e.physical.modes.at(i).refreshMilliHz / 1000.0, 0,
@ -266,12 +270,13 @@ bool DisplayOutputModel::setData(const QModelIndex &index, const QVariant &value
if (selected < 0 || selected >= choices.size())
return false;
const auto &t = e.physical.modes.at(choices.at(selected));
const auto *current = selectedMode(e);
const int currentRefresh = current ? current->refreshMilliHz : 0;
int best = choices.at(selected), difference = INT_MAX;
for (int i = 0; i < e.physical.modes.size(); ++i) {
const auto &c = e.physical.modes.at(i);
if (c.width == t.width && c.height == t.height) {
const int d = qAbs(c.refreshMilliHz
- (selectedMode(e) ? selectedMode(e)->refreshMilliHz : 0));
const int d = qAbs(c.refreshMilliHz - currentRefresh);
if (d < difference) {
best = i;
difference = d;
@ -313,6 +318,18 @@ bool DisplayOutputModel::setData(const QModelIndex &index, const QVariant &value
}
m_layoutDirty |= role == PositionRole || role == NormalizedPositionRole || role == EnabledRole
|| role == ReplicationSourceIndexRole;
if (role == ReplicationSourceIndexRole && e.primary && !e.mirrorSource.isEmpty()) {
// The compositor takes the primary only from a mirror source.
for (auto &candidate : m_entries) {
if (candidate.physical.sessionId == e.mirrorSource && candidate.enabled
&& candidate.mirrorSource.isEmpty()) {
e.primary = false;
candidate.primary = true;
emit dataChanged(this->index(0), this->index(m_entries.size() - 1), {PrimaryRole});
break;
}
}
}
if (role == EnabledRole && !e.enabled && e.primary) {
e.primary = false;
for (auto &candidate : m_entries) {
@ -377,12 +394,12 @@ void DisplayOutputModel::setState(const DisplayClient::State &state)
}
}
for (const auto &physical : state.physical) {
int row = 0;
while (row < m_entries.size() && m_entries[row].physical.sessionId != physical.sessionId)
++row;
Entry entry;
for (const auto &previous : m_entries)
if (previous.physical.sessionId == physical.sessionId) {
entry = previous;
break;
}
if (row < m_entries.size())
entry = m_entries[row];
entry.physical = physical;
entry.enabled = false;
entry.primary = false;
@ -405,9 +422,6 @@ void DisplayOutputModel::setState(const DisplayClient::State &state)
entry.mirrorSource = logical.destinations.first();
break;
}
int row = 0;
while (row < m_entries.size() && m_entries[row].physical.sessionId != physical.sessionId)
++row;
if (row == m_entries.size()) {
beginInsertRows({}, row, row);
m_entries.append(entry);

@ -22,7 +22,7 @@ DisplayScreen::DisplayScreen(QObject *parent)
[this](const QString &, bool succeeded, bool degraded, uint, const QString &message,
const QStringList &) {
m_busy = false;
m_error = succeeded ? (degraded ? message : QString()) : message;
m_error = succeeded && !degraded ? QString() : message;
m_outputModel->setState(m_client.state());
emit stateChanged();
});

@ -1,9 +1,7 @@
#include <memory>
#include <optional>
#include <utility>
#include <QDBusConnection>
#include <QDBusConnectionInterface>
#include <QSignalSpy>
#include <QTest>
#include <QTimer>

@ -3,6 +3,31 @@
#include "outputmodel.h"
// Displays side by side from x = 0, each its own logical display, display 0 primary.
static DisplayClient::State extendedDisplays(int count)
{
DisplayClient::State state;
for (int i = 0; i < count; ++i) {
cutefish::display::ipc::PhysicalDisplay physical;
physical.sessionId = QString::number(i);
cutefish::display::ipc::Mode mode;
mode.width = 1920;
mode.height = 1080;
mode.refreshMilliHz = 60000;
mode.current = true;
physical.modes.append(mode);
state.physical.append(physical);
cutefish::display::ipc::LogicalDisplay logical;
logical.id = physical.sessionId;
logical.destinations = {physical.sessionId};
logical.primary = i == 0;
logical.scale120 = 120;
logical.x = i * 1920;
state.logical.append(logical);
}
return state;
}
class OutputModelTest : public QObject
{
Q_OBJECT
@ -47,6 +72,50 @@ private slots:
QCOMPARE(model.data(selected, DisplayOutputModel::IdRole).toString(), QString("1"));
}
void mirroringThePrimaryHandsItToTheSource()
{
DisplayOutputModel model;
model.setState(extendedDisplays(3));
QSignalSpy changed(&model, &QAbstractItemModel::dataChanged);
// DisplayScreen applies on dirtyChanged, so the handoff has to come before it.
QString applied;
connect(&model, &DisplayOutputModel::dirtyChanged, [&] {
applied = model.primary();
});
QVERIFY(model.setData(model.index(0), 1, DisplayOutputModel::ReplicationSourceIndexRole));
QCOMPARE(applied, QString("1"));
QCOMPARE(model.primary(), QString("1"));
QVERIFY(!model.data(model.index(0), DisplayOutputModel::PrimaryRole).toBool());
QVERIFY(model.data(model.index(1), DisplayOutputModel::PrimaryRole).toBool());
bool primaryChanged = false;
for (const auto &arguments : changed) {
const auto roles = arguments.at(2).value<QList<int>>();
primaryChanged |= roles.contains(DisplayOutputModel::PrimaryRole);
}
QVERIFY(primaryChanged);
const auto placements = model.placements();
QCOMPARE(placements.size(), 1);
QCOMPARE(placements[0].displayId, QString("2"));
QCOMPARE(placements[0].parentId, QString("1"));
}
void mirroringThePrimaryNeedsAnExtendedSource()
{
// Display 1 is off in the first state and mirrors display 2 in the second.
auto off = extendedDisplays(3);
off.logical.removeAt(1);
auto mirroring = off;
mirroring.logical[1].destinations.append("1");
for (const auto &state : {off, mirroring}) {
DisplayOutputModel model;
model.setState(state);
QVERIFY(model.setData(model.index(0), 1,
DisplayOutputModel::ReplicationSourceIndexRole));
QCOMPARE(model.primary(), QString("0"));
QVERIFY(model.data(model.index(0), DisplayOutputModel::PrimaryRole).toBool());
}
}
void selectedResolutionAndRefreshStayConsistent()
{
DisplayClient::State state;

@ -1,4 +1,3 @@
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>

Loading…
Cancel
Save