feat: add desktopfilegenerator and method addUserApplication

1. change type of ActionName to 'a{sa{ss}}'
2. refactor the method of serialization

Signed-off-by: ComixHe <heyuming@deepin.org>
This commit is contained in:
ComixHe 2023-10-16 14:39:20 +08:00 committed by Comix
parent b71ceb5fc1
commit 1f73eea404
15 changed files with 458 additions and 41 deletions

View File

@ -44,7 +44,7 @@
<property name="Terminal" type="b" access="read">
<annotation
name="org.freedesktop.DBus.Description"
value="Indicate this application should launch by DEFAULT terminal or not."
value="Indicate this application should launch by terminal or not."
/>
</property>
@ -56,10 +56,10 @@
/>
</property>
<property name="ActionName" type="a{ss}" access="read">
<property name="ActionName" type="a{sa{ss}}" access="read">
<annotation
name="org.freedesktop.DBus.Description"
value="The type of ActionName is a Map, where the key represents the locale and the value is the corresponding content."
value="The type of ActionName is a Map, first key represents action, second key represents locale and the value is the corresponding content."
/>
<annotation name="org.qtproject.QtDBus.QtTypeName" value="PropMap"/>
</property>
@ -69,7 +69,7 @@
name="org.freedesktop.DBus.Description"
value="The type of IconName is a Map, where the key represents the action and the value is the corresponding content."
/>
<annotation name="org.qtproject.QtDBus.QtTypeName" value="PropMap"/>
<annotation name="org.qtproject.QtDBus.QtTypeName" value="QStringMap"/>
</property>
<property name="Name" type="a{ss}" access="read">
@ -77,7 +77,7 @@
name="org.freedesktop.DBus.Description"
value="The meaning of this property's type is same as which in ActionName."
/>
<annotation name="org.qtproject.QtDBus.QtTypeName" value="PropMap"/>
<annotation name="org.qtproject.QtDBus.QtTypeName" value="QStringMap"/>
</property>
<property name="GenericName" type="a{ss}" access="read">
@ -85,7 +85,7 @@
name="org.freedesktop.DBus.Description"
value="The meaning of this property's type is same as which in ActionName."
/>
<annotation name="org.qtproject.QtDBus.QtTypeName" value="PropMap"/>
<annotation name="org.qtproject.QtDBus.QtTypeName" value="QStringMap"/>
</property>
<method name="Launch">

View File

@ -30,5 +30,20 @@
1. You should use pidfd_open(2) to get a pidfd."
/>
</method>
<method name="addUserApplication">
<arg type="a{sv}" name="desktop_file" direction="in"/>
<arg type="s" name="name" direction="in"/>
<arg type="s" name="app_id" direction="out" />
<annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="QVariantMap" />
<annotation
name="org.freedesktop.DBus.Description"
value="Desktop-entry-spec: https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html,
type of `v` is depends on the property which you want to set.
examples:
{'Name':{'en_US':'example','default':'测试'},{'custom':10}} : a{sa{sv}} // Name=测试 Name[en_US]=example custom=10
{'custom':20,'Name':'example'} : a{sv} // custom=20 Name=example
"
/>
</method>
</interface>
</node>

View File

@ -21,6 +21,7 @@ constexpr auto DDEApplicationManager1JobManager1ObjectPath = u8"/org/desktopspec
constexpr auto DDEApplicationManager1MimeManager1ObjectPath = u8"/org/desktopspec/ApplicationManager1/MimeManager1";
constexpr auto DesktopFileEntryKey = u8"Desktop Entry";
constexpr auto DesktopFileActionKey = u8"Desktop Action ";
constexpr auto DesktopFileDefaultKeyLocale = "default";
constexpr auto ApplicationManagerServerDBusName =
#ifdef DDE_AM_USE_DEBUG_DBUS_NAME
@ -52,7 +53,7 @@ constexpr auto AppExecOption = u8"appExec";
constexpr auto STORAGE_VERSION = 0;
constexpr auto ApplicationPropertiesGroup = u8"Application Properties";
constexpr auto LastLaunchedTime = u8"LastLaunchedTime";
constexpr auto ScaleFactor=u8"ScaleFactor";
constexpr auto ScaleFactor = u8"ScaleFactor";
constexpr auto ApplicationManagerHookDir = u8"/deepin/dde-application-manager/hooks.d";

View File

@ -8,6 +8,7 @@
#include "systemdsignaldispatcher.h"
#include "propertiesForwarder.h"
#include "applicationHooks.h"
#include "desktopfilegenerator.h"
#include <QFile>
#include <QDBusMessage>
#include <unistd.h>
@ -548,3 +549,67 @@ ApplicationManager1Service::findApplicationsByIds(const QStringList &appIds) con
return ret;
}
QString ApplicationManager1Service::addUserApplication(const QVariantMap &desktop_file, const QString &name) noexcept
{
if (name.isEmpty()) {
sendErrorReply(QDBusError::Failed, "file name is empty.");
return {};
}
QDir xdgDataHome{getXDGDataHome() + "/applications"};
const auto &filePath = xdgDataHome.filePath(name);
if (QFileInfo info{filePath}; info.exists() and info.isFile()) {
sendErrorReply(QDBusError::Failed, QString{"file already exists:%1"}.arg(info.absoluteFilePath()));
return {};
}
QFile file{filePath};
if (!file.open(QFile::NewOnly | QFile::WriteOnly | QFile::Text)) {
sendErrorReply(QDBusError::Failed, file.errorString());
return {};
}
QString errMsg;
auto fileContent = DesktopFileGenerator::generate(desktop_file, errMsg);
if (fileContent.isEmpty() or !errMsg.isEmpty()) {
file.remove();
sendErrorReply(QDBusError::Failed, errMsg);
return {};
}
auto writeContent = fileContent.toLocal8Bit();
if (file.write(writeContent) != writeContent.size()) {
file.remove();
sendErrorReply(QDBusError::Failed, "incomplete file content.this file will be removed.");
return {};
}
file.flush();
ParserError err{ParserError::NoError};
auto ret = DesktopFile::searchDesktopFileByPath(filePath, err);
if (err != ParserError::NoError) {
file.remove();
qDebug() << "add user's application failed:" << err;
sendErrorReply(QDBusError::Failed, "search failed.");
return {};
}
if (!ret) {
file.remove();
sendErrorReply(QDBusError::InternalError);
return {};
}
auto desktopSource = std::move(ret).value();
auto appId = desktopSource.desktopId();
if (!addApplication(std::move(desktopSource))) {
file.remove();
sendErrorReply(QDBusError::Failed, "add application to ApplicationManager failed.");
return {};
}
return appId;
}

View File

@ -20,7 +20,7 @@
class ApplicationService;
class ApplicationManager1Service final : public QObject
class ApplicationManager1Service final : public QObject, public QDBusContext
{
Q_OBJECT
public:
@ -54,6 +54,7 @@ public Q_SLOTS:
QDBusObjectPath &instance,
ObjectInterfaceMap &application_instance_info) const noexcept;
void ReloadApplications();
QString addUserApplication(const QVariantMap &desktop_file, const QString &name) noexcept;
[[nodiscard]] ObjectMap GetManagedObjects() const;
Q_SIGNALS:

View File

@ -384,59 +384,64 @@ QStringList ApplicationService::categories() const noexcept
PropMap ApplicationService::actionName() const noexcept
{
PropMap ret;
auto actionList = actions();
const auto &actionList = actions();
for (auto &action : actionList) {
action.prepend(DesktopFileActionKey);
auto value = m_entry->value(action, "Name");
for (const auto &action : actionList) {
auto rawActionKey = DesktopFileActionKey % action;
auto value = m_entry->value(rawActionKey, "Name");
if (!value.has_value()) {
continue;
}
ret.insert(action, std::move(value).value());
ret.insert(action, std::move(value).value().value<QStringMap>());
}
return ret;
}
PropMap ApplicationService::name() const noexcept
QStringMap ApplicationService::name() const noexcept
{
PropMap ret;
auto value = m_entry->value(DesktopFileEntryKey, "Name");
if (!value) {
return ret;
return {};
}
ret.insert(QString{"Name"}, {std::move(value).value()});
return ret;
if (!value->canConvert<QStringMap>()) {
return {};
}
return value->value<QStringMap>();
}
PropMap ApplicationService::genericName() const noexcept
QStringMap ApplicationService::genericName() const noexcept
{
PropMap ret;
auto value = m_entry->value(DesktopFileEntryKey, "GenericName");
if (!value) {
return ret;
return {};
}
ret.insert(QString{"GenericName"}, {std::move(value).value()});
return ret;
if (!value->canConvert<QStringMap>()) {
return {};
}
return value->value<QStringMap>();
}
PropMap ApplicationService::icons() const noexcept
QStringMap ApplicationService::icons() const noexcept
{
PropMap ret;
QStringMap ret;
auto actionList = actions();
for (const auto &action : actionList) {
auto value = m_entry->value(QString{action}.prepend(DesktopFileActionKey), "Icon");
const auto &actionKey = QString{action}.prepend(DesktopFileActionKey);
auto value = m_entry->value(actionKey, "Icon");
if (!value.has_value()) {
continue;
}
ret.insert(action, {std::move(value).value()});
ret.insert(actionKey, value->value<QString>());
}
auto mainIcon = m_entry->value(DesktopFileEntryKey, "Icon");
if (mainIcon.has_value()) {
ret.insert(defaultKeyStr, {std::move(mainIcon).value()});
ret.insert(DesktopFileEntryKey, mainIcon->value<QString>());
}
return ret;

View File

@ -50,14 +50,14 @@ public:
Q_PROPERTY(QString ID READ id CONSTANT)
[[nodiscard]] QString id() const noexcept;
Q_PROPERTY(PropMap Name READ name NOTIFY nameChanged)
[[nodiscard]] PropMap name() const noexcept;
Q_PROPERTY(QStringMap Name READ name NOTIFY nameChanged)
[[nodiscard]] QStringMap name() const noexcept;
Q_PROPERTY(PropMap GenericName READ genericName NOTIFY genericNameChanged)
[[nodiscard]] PropMap genericName() const noexcept;
Q_PROPERTY(QStringMap GenericName READ genericName NOTIFY genericNameChanged)
[[nodiscard]] QStringMap genericName() const noexcept;
Q_PROPERTY(PropMap Icons READ icons NOTIFY iconsChanged)
[[nodiscard]] PropMap icons() const noexcept;
Q_PROPERTY(QStringMap Icons READ icons NOTIFY iconsChanged)
[[nodiscard]] QStringMap icons() const noexcept;
Q_PROPERTY(qulonglong LastLaunchedTime READ lastLaunchedTime NOTIFY lastLaunchedTimeChanged)
[[nodiscard]] qulonglong lastLaunchedTime() const noexcept;

View File

@ -285,7 +285,7 @@ QString toString(const DesktopEntry::Value &value) noexcept
QString str;
if (value.canConvert<QStringMap>()) { // get default locale
str = value.value<QStringMap>()[defaultKeyStr];
str = value.value<QStringMap>()[DesktopFileDefaultKeyLocale];
} else {
str = value.toString();
}
@ -312,7 +312,7 @@ QString toLocaleString(const QStringMap &localeMap, const QLocale &locale) noexc
}
}
return toString(localeMap[defaultKeyStr]);
return toString(localeMap[DesktopFileDefaultKeyLocale]);
}
QString toIconString(const DesktopEntry::Value &value) noexcept

View File

@ -15,8 +15,6 @@
#include "iniParser.h"
#include "global.h"
constexpr static auto defaultKeyStr = "default";
enum class EntryContext { Unknown, EntryOuter, Entry, Done };
enum class EntryValueType { String, LocaleString, Boolean, IconString };
@ -126,6 +124,9 @@ private:
[[nodiscard]] bool checkMainEntryValidation() const noexcept;
QMap<QString, QMap<QString, Value>> m_entryMap;
bool m_parsed{false};
public:
using container_type = decltype(m_entryMap);
};
bool operator==(const DesktopEntry &lhs, const DesktopEntry &rhs);

View File

@ -0,0 +1,183 @@
// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: LGPL-3.0-or-later
#include "desktopfilegenerator.h"
#include "desktopfileparser.h"
bool DesktopFileGenerator::checkValidation(const QVariantMap &desktopFile, QString &err) noexcept
{
if (!desktopFile.contains("Type") or !desktopFile.contains("Name")) {
err = "required key doesn't exists.";
return false;
}
auto type = qdbus_cast<QString>(desktopFile["Type"]);
if (type.isEmpty()) {
err = "Type's type is invalid";
return false;
}
if (type == "Link" and !desktopFile.contains("URL")) {
err = "URL must be set when Type is 'Link'";
return false;
}
return true;
}
int DesktopFileGenerator::processMainGroupLocaleEntry(DesktopEntry::container_type::iterator mainEntry,
const QString &key,
const QVariant &value) noexcept
{
if (key == "ActionName") {
return 1;
}
if (key == "Name") {
const auto &nameMap = qdbus_cast<QStringMap>(value);
if (nameMap.isEmpty()) {
qDebug() << "Name's type mismatch:" << nameMap;
return -1;
}
mainEntry->insert("Name", QVariant::fromValue(nameMap));
return 1;
}
if (key == "Icon") {
const auto &iconMap = qdbus_cast<QStringMap>(value);
if (auto icon = iconMap.constFind(DesktopFileDefaultKeyLocale); icon != iconMap.cend() and !icon->isEmpty()) {
mainEntry->insert("Icon", *icon);
}
return 1;
}
if (key == "Exec") {
const auto &execMap = qdbus_cast<QStringMap>(value);
if (auto exec = execMap.constFind(DesktopFileDefaultKeyLocale); exec != execMap.cend() and !exec->isEmpty()) {
mainEntry->insert("Exec", *exec);
}
return 1;
}
return 0;
}
bool DesktopFileGenerator::processMainGroup(DesktopEntry::container_type &content, const QVariantMap &rawValue) noexcept
{
auto mainEntry = content.insert(DesktopFileEntryKey, {});
for (auto it = rawValue.constKeyValueBegin(); it != rawValue.constKeyValueEnd(); ++it) {
const auto &[key, value] = *it;
if (mainEntry->contains(key)) {
qDebug() << "duplicate key:" << key << ",skip";
return false;
}
auto ret = processMainGroupLocaleEntry(mainEntry, key, value);
if (ret == 1) {
continue;
}
if (ret == -1) {
return false;
}
mainEntry->insert(key, value);
}
mainEntry->insert("X-Deepin-CreateBy", QString{"dde-application-manager"});
return true;
}
bool DesktopFileGenerator::processActionGroup(QStringList actions,
DesktopEntry::container_type &content,
const QVariantMap &rawValue) noexcept
{
actions.removeDuplicates();
if (actions.isEmpty()) {
qDebug() << "empty actions";
return false;
}
auto nameMap = qdbus_cast<QVariantMap>(rawValue["ActionName"]);
if (nameMap.isEmpty()) {
qDebug() << "ActionName's type mismatch.";
return false;
}
QStringMap iconMap;
if (auto actionIcon = rawValue.constFind("Icon"); actionIcon != rawValue.cend()) {
iconMap = qdbus_cast<QStringMap>(*actionIcon);
if (iconMap.isEmpty()) {
qDebug() << "Icon's type mismatch.";
return false;
}
}
QStringMap execMap;
if (auto actionExec = rawValue.constFind("Exec"); actionExec != rawValue.cend()) {
execMap = qdbus_cast<QStringMap>(*actionExec);
if (execMap.isEmpty()) {
qDebug() << "Exec's type mismatch:" << actionExec->typeName();
return false;
}
}
for (const auto &action : actions) {
if (action.isEmpty()) {
qDebug() << "action's content is empty. skip";
continue;
}
if (!nameMap.contains(action)) {
qDebug() << "couldn't find actionName, current action:" << action;
return false;
}
auto actionGroup = content.insert(DesktopFileActionKey % action, {});
auto curVal = qdbus_cast<QStringMap>(nameMap[action]);
if (curVal.isEmpty()) {
qDebug() << "inner type of actionName is mismatched";
return false;
}
actionGroup->insert("Name", QVariant::fromValue(curVal));
if (auto actionIcon = iconMap.constFind(action); actionIcon != iconMap.cend() and !actionIcon->isEmpty()) {
actionGroup->insert("Icon", iconMap[action]);
}
if (auto actionExec = execMap.constFind(action); actionExec != execMap.cend() and !actionExec->isEmpty()) {
actionGroup->insert("Exec", execMap[action]);
}
};
return true;
}
QString DesktopFileGenerator::generate(const QVariantMap &desktopFile, QString &err) noexcept
{
DesktopEntry::container_type content{};
if (auto actions = desktopFile.find("Actions"); actions != desktopFile.end()) {
if (!desktopFile.contains("ActionName")) {
err = "'ActionName' doesn't exists";
return {};
}
if (!processActionGroup(actions->toStringList(), content, desktopFile)) {
err = "please check action group";
return {};
}
}
if (!processMainGroup(content, desktopFile)) {
err = "please check main group.";
return {};
}
auto fileContent = toString(content);
if (fileContent.isEmpty()) {
err = "couldn't convert to desktop file.";
return {};
}
return fileContent;
}

View File

@ -0,0 +1,24 @@
// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: LGPL-3.0-or-later
#ifndef DESKTOPFILEGENERATOR_H
#define DESKTOPFILEGENERATOR_H
#include "desktopentry.h"
struct DesktopFileGenerator
{
static QString generate(const QVariantMap &desktopFile, QString &err) noexcept;
private:
static bool checkValidation(const QVariantMap &desktopFile, QString &err) noexcept;
static bool processMainGroup(DesktopEntry::container_type &content, const QVariantMap &rawValue) noexcept;
static bool
processActionGroup(QStringList actions, DesktopEntry::container_type &content, const QVariantMap &rawValue) noexcept;
static int processMainGroupLocaleEntry(DesktopEntry::container_type::iterator mainEntry,
const QString &key,
const QVariant &value) noexcept;
};
#endif

View File

@ -116,7 +116,7 @@ ParserError DesktopFileParser::addEntry(typename Groups::iterator &group) noexce
auto valueStr = line.sliced(splitCharIndex + 1).trimmed();
QString key{""};
QString localeStr{defaultKeyStr};
QString localeStr{DesktopFileDefaultKeyLocale};
// NOTE:
// We are process "localized keys" here, for usage check:
// https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#localized-keys
@ -146,7 +146,7 @@ ParserError DesktopFileParser::addEntry(typename Groups::iterator &group) noexce
return ParserError::NoError;
}
if (localeStr != defaultKeyStr and !isInvalidLocaleString(localeStr)) {
if (localeStr != DesktopFileDefaultKeyLocale and !isInvalidLocaleString(localeStr)) {
qWarning().noquote() << QString("invalid LOCALE (%2) for key \"%1\"").arg(key, localeStr);
return ParserError::NoError;
}
@ -180,3 +180,47 @@ ParserError DesktopFileParser::addEntry(typename Groups::iterator &group) noexce
return ParserError::NoError;
}
QString toString(const DesktopFileParser::Groups &map)
{
QString ret;
auto groupToString = [&ret, map](const QString &group) {
const auto &groupEntry = map[group];
ret.append('[' % group % "]\n");
for (auto entryIt = groupEntry.constKeyValueBegin(); entryIt != groupEntry.constKeyValueEnd(); ++entryIt) {
const auto &key = entryIt->first;
const auto &value = entryIt->second;
if (value.canConvert<QStringMap>()) {
const auto &rawMap = value.value<QStringMap>();
std::for_each(rawMap.constKeyValueBegin(), rawMap.constKeyValueEnd(), [key, &ret](const auto &inner) {
const auto &[locale, rawVal] = inner;
ret.append(key);
if (locale != DesktopFileDefaultKeyLocale) {
ret.append('[' % locale % ']');
}
ret.append('=' % rawVal % '\n');
});
} else if (value.canConvert<QStringList>()) {
const auto &rawVal = value.value<QStringList>();
auto str = rawVal.join(';');
ret.append(key % '=' % str % '\n');
} else if (value.canConvert<QString>()) {
const auto &rawVal = value.value<QString>();
ret.append(key % '=' % rawVal % '\n');
} else {
qWarning() << "value type mismatch:" << value;
}
}
ret.append('\n');
};
groupToString(DesktopFileEntryKey);
for (const auto &groupName : map.keys()) {
if (groupName == DesktopFileEntryKey) {
continue;
}
groupToString(groupName);
}
return ret;
}

View File

@ -17,4 +17,6 @@ public:
ParserError addEntry(Groups::iterator &group) noexcept override;
};
QString toString(const DesktopFileParser::Groups &map);
#endif

View File

@ -30,7 +30,7 @@ Q_DECLARE_LOGGING_CATEGORY(DDEAMProf)
using ObjectInterfaceMap = QMap<QString, QVariantMap>;
using ObjectMap = QMap<QDBusObjectPath, ObjectInterfaceMap>;
using QStringMap = QMap<QString, QString>;
using PropMap = QVariantMap;
using PropMap = QMap<QString, QStringMap>;
Q_DECLARE_METATYPE(ObjectInterfaceMap)
Q_DECLARE_METATYPE(ObjectMap)
@ -44,6 +44,21 @@ struct SystemdUnitDBusMessage
QDBusObjectPath objectPath;
};
inline const QDBusArgument &operator>>(const QDBusArgument &argument, QStringMap &map)
{
argument.beginMap();
while (!argument.atEnd()) {
QString key;
QString value;
argument.beginMapEntry();
argument >> key >> value;
argument.endMapEntry();
map.insert(key, value);
}
argument.endMap();
return argument;
}
inline const QDBusArgument &operator>>(const QDBusArgument &argument, QList<SystemdUnitDBusMessage> &units)
{
argument.beginArray();

View File

@ -0,0 +1,61 @@
// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: LGPL-3.0-or-later
#include "desktopfilegenerator.h"
#include <gtest/gtest.h>
#include <QVariant>
TEST(DesktopFileGenerator, generate)
{
QVariantMap map;
map.insert("Type", QString{"Application"});
map.insert("Name",
QVariant::fromValue(QStringMap{{"default", "UserApp"}, {"zh_CN", "yonghuyingyong"}, {"en_US", "setApplication"}}));
map.insert("Actions", QStringList{"one", "two"});
map.insert("Exec",
QVariant::fromValue(QStringMap{
{"default", "/usr/bin/exec"}, {"one", "/usr/bin/exec --type=one"}, {"two", "/usr/bin/exec --type=two"}}));
map.insert("Icon", QVariant::fromValue(QStringMap{{"default", "default-icon"}, {"one", "one-icon"}, {"two", "two-icon"}}));
map.insert("ActionName",
QVariantMap{{"one", QVariant::fromValue(QStringMap{{"default", "oneName"}, {"zh_CN", "yi"}, {"en_US", "one"}})},
{"two", QVariant::fromValue(QStringMap{{"default", "twoname"}, {"zh_CN", "er"}, {"en_US", "two"}})}});
map.insert("Version", 1.0);
map.insert("Terminal", false);
map.insert("MimeType", QStringList{"text/html", "text/xml", "application/xhtml+xml"});
QString errMsg{"NO ERROR"};
auto content = DesktopFileGenerator::generate(map, errMsg);
EXPECT_EQ(errMsg.toStdString(), QString{"NO ERROR"}.toStdString());
QString expect{R"([Desktop Entry]
Actions=one;two
Exec=/usr/bin/exec
Icon=default-icon
MimeType=text/html;text/xml;application/xhtml+xml
Name=UserApp
Name[en_US]=setApplication
Name[zh_CN]=yonghuyingyong
Terminal=false
Type=Application
Version=1
X-Deepin-CreateBy=dde-application-manager
[Desktop Action one]
Exec=/usr/bin/exec --type=one
Icon=one-icon
Name=oneName
Name[en_US]=one
Name[zh_CN]=yi
[Desktop Action two]
Exec=/usr/bin/exec --type=two
Icon=two-icon
Name=twoname
Name[en_US]=two
Name[zh_CN]=er
)"};
EXPECT_EQ(expect.toStdString(), content.toStdString());
}