diff --git a/rpm/nemo-qml-plugin-systemsettings.spec b/rpm/nemo-qml-plugin-systemsettings.spec index aa37c89..15b0ecc 100644 --- a/rpm/nemo-qml-plugin-systemsettings.spec +++ b/rpm/nemo-qml-plugin-systemsettings.spec @@ -12,6 +12,7 @@ Requires: connman Requires: mce >= 1.83.0 Requires: libsailfishkeyprovider >= 0.0.14 Requires: connman-qt5 >= 1.2.21 +Requires: user-managerd Requires(post): coreutils BuildRequires: pkgconfig(Qt5Qml) BuildRequires: pkgconfig(Qt5SystemInfo) @@ -33,6 +34,7 @@ BuildRequires: pkgconfig(packagekitqt5) BuildRequires: pkgconfig(glib-2.0) BuildRequires: pkgconfig(sailfishaccesscontrol) BuildRequires: pkgconfig(libsystemd) +BuildRequires: pkgconfig(sailfishusermanager) %description %{summary}. diff --git a/src/logging.cpp b/src/logging.cpp index e98d39f..5d0df08 100644 --- a/src/logging.cpp +++ b/src/logging.cpp @@ -34,3 +34,4 @@ Q_LOGGING_CATEGORY(lcVpnLog, "org.sailfishos.settings.vpn", QtWarningMsg) Q_LOGGING_CATEGORY(lcDeveloperModeLog, "org.sailfishos.settings.developermode", QtWarningMsg) Q_LOGGING_CATEGORY(lcMemoryCardLog, "org.sailfishos.settings.memorycard", QtWarningMsg) +Q_LOGGING_CATEGORY(lcUsersLog, "org.sailfishos.settings.users", QtWarningMsg) diff --git a/src/logging_p.h b/src/logging_p.h index 5f60411..f937e9c 100644 --- a/src/logging_p.h +++ b/src/logging_p.h @@ -37,5 +37,6 @@ Q_DECLARE_LOGGING_CATEGORY(lcVpnLog) Q_DECLARE_LOGGING_CATEGORY(lcDeveloperModeLog) Q_DECLARE_LOGGING_CATEGORY(lcMemoryCardLog) +Q_DECLARE_LOGGING_CATEGORY(lcUsersLog) #endif diff --git a/src/plugin/plugin.cpp b/src/plugin/plugin.cpp index 8ffdebd..7bc62e6 100644 --- a/src/plugin/plugin.cpp +++ b/src/plugin/plugin.cpp @@ -52,6 +52,7 @@ #include "deviceinfo.h" #include "nfcsettings.h" #include "userinfo.h" +#include "usermodel.h" template static QObject *api_factory(QQmlEngine *, QJSEngine *) @@ -93,6 +94,7 @@ class SystemSettingsPlugin : public QQmlExtensionPlugin qmlRegisterType(uri, 1, 0, "DeviceInfo"); qmlRegisterType(uri, 1, 0, "NfcSettings"); qmlRegisterType(uri, 1, 0, "UserInfo"); + qmlRegisterType(uri, 1, 0, "UserModel"); } }; diff --git a/src/src.pro b/src/src.pro index a07fb07..ee18be2 100644 --- a/src/src.pro +++ b/src/src.pro @@ -7,7 +7,7 @@ QT -= gui CONFIG += c++11 hide_symbols link_pkgconfig PKGCONFIG += profile mlite5 mce timed-qt5 libshadowutils blkid libcrypto nemomodels-qt5 libsailfishkeyprovider connman-qt5 glib-2.0 -PKGCONFIG += ssu-sysinfo nemodbus packagekitqt5 libsystemd +PKGCONFIG += ssu-sysinfo nemodbus packagekitqt5 libsystemd sailfishusermanager system(qdbusxml2cpp -p mceiface.h:mceiface.cpp mce.xml) @@ -38,7 +38,8 @@ SOURCES += \ udisks2blockdevices.cpp \ udisks2job.cpp \ udisks2monitor.cpp \ - userinfo.cpp + userinfo.cpp \ + usermodel.cpp PUBLIC_HEADERS = \ languagemodel.h \ @@ -62,7 +63,8 @@ PUBLIC_HEADERS = \ deviceinfo.h \ locationsettings.h \ timezoneinfo.h \ - userinfo.h + userinfo.h \ + usermodel.h HEADERS += \ $$PUBLIC_HEADERS \ diff --git a/src/userinfo.h b/src/userinfo.h index e3efe18..f25a771 100644 --- a/src/userinfo.h +++ b/src/userinfo.h @@ -39,6 +39,7 @@ #include "systemsettingsglobal.h" class UserInfoPrivate; +class UserModel; class SYSTEMSETTINGS_EXPORT UserInfo: public QObject { @@ -51,6 +52,8 @@ class SYSTEMSETTINGS_EXPORT UserInfo: public QObject Q_PROPERTY(int uid READ uid NOTIFY uidChanged) Q_PROPERTY(bool current READ current CONSTANT) + friend class UserModel; + public: enum UserType { User = 0, diff --git a/src/usermodel.cpp b/src/usermodel.cpp new file mode 100644 index 0000000..1838d0b --- /dev/null +++ b/src/usermodel.cpp @@ -0,0 +1,356 @@ +/* + * Copyright (C) 2020 Open Mobile Platform LLC. + * + * You may use this file under the terms of the BSD license as follows: + * + * "Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Nemo Mobile nor the names of its contributors + * may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." + */ + +#include "usermodel.h" +#include "logging_p.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { +const auto UserManagerService = QStringLiteral(SAILFISH_USERMANAGER_DBUS_INTERFACE); +const auto UserManagerPath = QStringLiteral(SAILFISH_USERMANAGER_DBUS_OBJECT_PATH); +const auto UserManagerInterface = QStringLiteral(SAILFISH_USERMANAGER_DBUS_INTERFACE); +} + +UserModel::UserModel(QObject *parent) + : QAbstractListModel(parent) + , m_dBusInterface(nullptr) + , m_dBusWatcher(new QDBusServiceWatcher(UserManagerService, QDBusConnection::systemBus(), + QDBusServiceWatcher::WatchForRegistration | QDBusServiceWatcher::WatchForUnregistration, this)) +{ + qDBusRegisterMetaType(); + connect(m_dBusWatcher, &QDBusServiceWatcher::serviceRegistered, + this, &UserModel::createInterface); + connect(m_dBusWatcher, &QDBusServiceWatcher::serviceUnregistered, + this, &UserModel::destroyInterface); + if (QDBusConnection::systemBus().interface()->isServiceRegistered(UserManagerService)) + createInterface(); + struct group *grp = getgrnam("users"); + for (int i = 0; grp->gr_mem[i] != nullptr; ++i) { + UserInfo user(QString(grp->gr_mem[i])); + if (user.isValid()) { // Skip invalid users here + m_users.append(user); + m_uidsToRows.insert(user.uid(), m_users.count()-1); + } + } + // grp must not be free'd +} + +UserModel::~UserModel() +{ +} + +bool UserModel::placeholder() +{ + // Placeholder is always last and the only item that can be invalid + if (m_users.count() == 0) + return false; + return !m_users.last().isValid(); +} + +void UserModel::setPlaceholder(bool value) +{ + if (placeholder() == value) + return; + + if (value) { + auto row = m_users.count(); + beginInsertRows(QModelIndex(), row, row); + m_users.append(UserInfo::placeholder()); + endInsertRows(); + } else { + auto row = m_users.count()-1; + beginRemoveRows(QModelIndex(), row, row); + m_users.remove(row); + endRemoveRows(); + } + emit placeholderChanged(); +} + +QHash UserModel::roleNames() const +{ + static const QHash roles = { + { Qt::DisplayRole, "displayName" }, + { UsernameRole, "username" }, + { NameRole, "name" }, + { TypeRole, "type" }, + { UidRole, "uid" }, + { CurrentRole, "current" }, + { PlaceholderRole, "placeholder" }, + }; + return roles; +} + +int UserModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return m_users.count(); +} + +QVariant UserModel::data(const QModelIndex &index, int role) const +{ + if (index.row() < 0 || index.row() >= m_users.count() || index.column() != 0) + return QVariant(); + + const UserInfo &user = m_users.at(index.row()); + switch (role) { + case Qt::DisplayRole: + return (user.name().isEmpty()) ? user.username() : user.name(); + case UsernameRole: + return user.username(); + case NameRole: + return user.name(); + case TypeRole: + return user.type(); + case UidRole: + return user.uid(); + case CurrentRole: + return user.current(); + case PlaceholderRole: + return !user.isValid(); + default: + return QVariant(); + } +} + +bool UserModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (index.row() < 0 || index.row() >= m_users.count() || index.column() != 0) + return false; + + UserInfo &user = m_users[index.row()]; + switch (role) { + case NameRole: { + QString name = value.toString(); + if (name.isEmpty() || name == user.name()) + return false; + user.setName(name); + if (user.isValid()) { + createInterface(); + auto call = m_dBusInterface->asyncCall(QStringLiteral("modifyUser"), (uint)user.uid(), name); + auto *watcher = new QDBusPendingCallWatcher(call, this); + connect(watcher, &QDBusPendingCallWatcher::finished, + this, std::bind(&UserModel::userModifyFinished, this, std::placeholders::_1, index.row())); + } + emit dataChanged(index, index, QVector() << role); + return true; + } + case Qt::DisplayRole: + case UsernameRole: + case TypeRole: + case UidRole: + case CurrentRole: + case PlaceholderRole: + default: + return false; + } +} + +QModelIndex UserModel::index(int row, int column, const QModelIndex &parent) const +{ + Q_UNUSED(parent); + if (row < 0 || row >= m_users.count() || column != 0) + return QModelIndex(); + + // create index + return createIndex(row, 0, row); +} + +/* + * Creates new user from a placeholder user. + * + * Does nothing if there is no placeholder or user's name is not set. + */ +void UserModel::createUser() +{ + if (!placeholder()) + return; + + auto user = m_users.last(); + if (user.name().isEmpty()) + return; + + createInterface(); + auto call = m_dBusInterface->asyncCall(QStringLiteral("addUser"), user.name()); + auto *watcher = new QDBusPendingCallWatcher(call, this); + connect(watcher, &QDBusPendingCallWatcher::finished, + this, std::bind(&UserModel::userAddFinished, this, std::placeholders::_1, m_users.count()-1)); +} + +void UserModel::removeUser(int row) +{ + if (row < 0 || row >= m_users.count()) + return; + + auto user = m_users.at(row); + if (!user.isValid()) + return; + + createInterface(); + auto call = m_dBusInterface->asyncCall(QStringLiteral("removeUser"), (uint)user.uid()); + auto *watcher = new QDBusPendingCallWatcher(call, this); + connect(watcher, &QDBusPendingCallWatcher::finished, + this, std::bind(&UserModel::userRemoveFinished, this, std::placeholders::_1, row)); +} + +void UserModel::reset(int row) +{ + if (row < 0 || row >= m_users.count()) + return; + + m_users[row].reset(); + auto idx = index(row, 0); + emit dataChanged(idx, idx, QVector()); +} + +UserInfo * UserModel::getCurrentUser() const +{ + return new UserInfo(); +} + +void UserModel::userAdded(const SailfishUserManagerEntry &entry) +{ + if (m_uidsToRows.contains(entry.uid)) + return; + + // Not found already, appending + auto user = UserInfo(entry.uid); + if (user.isValid()) { + int row = placeholder() ? m_users.count()-1 : m_users.count(); + beginInsertRows(QModelIndex(), row, row); + m_users.insert(row, user); + m_uidsToRows.insert(entry.uid, row); + endInsertRows(); + } +} + +void UserModel::userModified(uint uid, const QString &newName) +{ + if (!m_uidsToRows.contains(uid)) + return; + + int row = m_uidsToRows.value(uid); + UserInfo &user = m_users[row]; + if (user.name() != newName) { + user.setName(newName); + auto idx = index(row, 0); + dataChanged(idx, idx, QVector() << NameRole); + } +} + +void UserModel::userRemoved(uint uid) +{ + if (!m_uidsToRows.contains(uid)) + return; + + int row = m_uidsToRows.value(uid); + beginRemoveRows(QModelIndex(), row, row); + m_users.remove(row); + m_uidsToRows.remove(uid); + endRemoveRows(); +} + +void UserModel::userAddFinished(QDBusPendingCallWatcher *call, int row) +{ + QDBusPendingReply reply = *call; + if (reply.isError()) { + emit userAddFailed(); + qWarning() << "Adding user with usermanager failed:" << reply.error(); + } else { + uint uid = reply.value(); + // Check that this was not just added to the list by userAdded + if (!m_uidsToRows.contains(uid)) { + beginInsertRows(QModelIndex(), row, row); + m_users.insert(row, UserInfo(uid)); + m_uidsToRows.insert(uid, row); + endInsertRows(); + } + // Reset placeholder + reset(m_users.count()-1); + } + call->deleteLater(); +} + +void UserModel::userModifyFinished(QDBusPendingCallWatcher *call, int row) +{ + QDBusPendingReply reply = *call; + if (reply.isError()) { + emit userModifyFailed(row, m_users.at(row).name()); + qWarning() << "Modifying user with usermanager failed:" << reply.error(); + reset(row); + } // else awesome! (data was changed already) + call->deleteLater(); +} + +void UserModel::userRemoveFinished(QDBusPendingCallWatcher *call, int row) +{ + QDBusPendingReply reply = *call; + if (reply.isError()) { + emit userRemoveFailed(row, m_users.at(row).name()); + qWarning() << "Removing user with usermanager failed:" << reply.error(); + } // else awesome! (waiting for signal to alter data) + call->deleteLater(); +} + +void UserModel::createInterface() +{ + if (!m_dBusInterface) { + qCDebug(lcUsersLog) << "Creating interface to user-managerd"; + m_dBusInterface = new QDBusInterface(UserManagerService, UserManagerPath, UserManagerInterface, + QDBusConnection::systemBus(), this); + connect(m_dBusInterface, SIGNAL(userAdded(const SailfishUserManagerEntry &)), + this, SLOT(userAdded(const SailfishUserManagerEntry &)), Qt::QueuedConnection); + connect(m_dBusInterface, SIGNAL(userModified(uint, const QString &)), + this, SLOT(userModified(uint, const QString &))); + connect(m_dBusInterface, SIGNAL(userRemoved(uint)), + this, SLOT(userRemoved(uint))); + } +} + +void UserModel::destroyInterface() { + if (m_dBusInterface) { + qCDebug(lcUsersLog) << "Destroying interface to user-managerd"; + disconnect(m_dBusInterface, 0, this, 0); + m_dBusInterface->deleteLater(); + m_dBusInterface = nullptr; + } +} diff --git a/src/usermodel.h b/src/usermodel.h new file mode 100644 index 0000000..d6604c7 --- /dev/null +++ b/src/usermodel.h @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2020 Open Mobile Platform LLC. + * + * You may use this file under the terms of the BSD license as follows: + * + * "Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Nemo Mobile nor the names of its contributors + * may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." + */ + +#ifndef USERMODEL_H +#define USERMODEL_H + +#include +#include +#include + +#include "systemsettingsglobal.h" +#include "userinfo.h" + +class QDBusInterface; +class QDBusPendingCallWatcher; +class QDBusServiceWatcher; +struct SailfishUserManagerEntry; + +class SYSTEMSETTINGS_EXPORT UserModel: public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(bool placeholder READ placeholder WRITE setPlaceholder NOTIFY placeholderChanged) + +public: + enum Roles { + UsernameRole = Qt::UserRole, + NameRole, + TypeRole, + UidRole, + CurrentRole, + PlaceholderRole, + }; + Q_ENUM(Roles) + + enum UserType { + User = 0, + DeviceOwner = 1, + }; + Q_ENUM(UserType) + + explicit UserModel(QObject *parent = 0); + ~UserModel(); + + bool placeholder(); + void setPlaceholder(bool value); + + QHash roleNames() const; + int rowCount(const QModelIndex &parent = QModelIndex()) const; + QVariant data(const QModelIndex &index, int role) const; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole); + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const; + + Q_INVOKABLE void createUser(); + Q_INVOKABLE void removeUser(int row); + Q_INVOKABLE void reset(int row); + Q_INVOKABLE UserInfo * getCurrentUser() const; + +signals: + void placeholderChanged(); + void userAddFailed(); + void userModifyFailed(int row, const QString &name); + void userRemoveFailed(int row, const QString &name); + +private slots: + void userAdded(const SailfishUserManagerEntry &entry); + void userModified(uint uid, const QString &newName); + void userRemoved(uint uid); + void userAddFinished(QDBusPendingCallWatcher *call, int row); + void userModifyFinished(QDBusPendingCallWatcher *call, int row); + void userRemoveFinished(QDBusPendingCallWatcher *call, int row); + void createInterface(); + void destroyInterface(); + +private: + QVector m_users; + QHash m_uidsToRows; + QDBusInterface *m_dBusInterface; + QDBusServiceWatcher *m_dBusWatcher; +}; +#endif /* USERMODEL_H */