mirror of https://github.com/cutefishos/settings
feat(privacy): list and remove apps allowed to control the computer
parent
32e5fef464
commit
7d08ca2e45
@ -0,0 +1,77 @@
|
||||
// SPDX-FileCopyrightText: 2026 Reion Wong <reionwong@gmail.com>
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
#include "pages/privacy/computercontrol.h"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
using namespace cutefish::base;
|
||||
|
||||
namespace settings {
|
||||
|
||||
static constexpr const char* kService = "org.cutefish.ComputerControl1";
|
||||
static constexpr const char* kPath = "/org/cutefish/ComputerControl1";
|
||||
|
||||
static std::vector<ComputerControl::App> appsOf(const dbus::Reply& reply)
|
||||
{
|
||||
std::vector<ComputerControl::App> apps;
|
||||
for (const dbus::VariantMap& entry : reply.value<std::vector<dbus::VariantMap>>().value_or(
|
||||
std::vector<dbus::VariantMap>())) {
|
||||
ComputerControl::App app;
|
||||
if (const auto it = entry.find("path"); it != entry.end())
|
||||
app.path = it->second.value<std::string>();
|
||||
if (const auto it = entry.find("name"); it != entry.end())
|
||||
app.name = it->second.value<std::string>();
|
||||
if (const auto it = entry.find("app_id"); it != entry.end())
|
||||
app.appId = it->second.value<std::string>();
|
||||
if (!app.path.empty())
|
||||
apps.push_back(std::move(app));
|
||||
}
|
||||
return apps;
|
||||
}
|
||||
|
||||
ComputerControl::ComputerControl()
|
||||
: m_control(dbus::Connection::session(), kService, kPath, kService)
|
||||
{
|
||||
dbus::Connection* bus = m_control.connection();
|
||||
if (!bus)
|
||||
return;
|
||||
// Read at once, so the page has the list for its first frame.
|
||||
if (const dbus::Reply reply = m_control.call("allowedApps"); !reply.isError()) {
|
||||
m_apps = appsOf(reply);
|
||||
m_available = true;
|
||||
}
|
||||
m_changedMatch = m_control.onSignal("changed", [this](const dbus::Message&) { reload(); });
|
||||
m_watch = bus->watchName(kService, [this](const std::string& owner) {
|
||||
if (!owner.empty()) {
|
||||
reload();
|
||||
} else if (m_available) {
|
||||
m_available = false;
|
||||
m_apps.clear();
|
||||
changed.emit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void ComputerControl::reload()
|
||||
{
|
||||
m_reload = m_control.callAsync("allowedApps", [this](const dbus::Reply& reply) {
|
||||
if (reply.isError())
|
||||
return;
|
||||
m_apps = appsOf(reply);
|
||||
m_available = true;
|
||||
changed.emit();
|
||||
});
|
||||
}
|
||||
|
||||
void ComputerControl::forget(const std::string& path)
|
||||
{
|
||||
if (!m_available)
|
||||
return;
|
||||
m_forget = m_control.callAsync("forget", [](const dbus::Reply& reply) {
|
||||
if (reply.isError())
|
||||
fprintf(stderr, "Cannot forget the app: %s\n", reply.error().message.c_str());
|
||||
}, path);
|
||||
}
|
||||
|
||||
} // namespace settings
|
||||
@ -0,0 +1,47 @@
|
||||
// SPDX-FileCopyrightText: 2026 Reion Wong <reionwong@gmail.com>
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "base/dbus.h"
|
||||
#include "base/signal.h"
|
||||
|
||||
namespace settings {
|
||||
|
||||
// The compositor's org.cutefish.ComputerControl1: the apps whose agents may
|
||||
// control the computer without asking.
|
||||
class ComputerControl
|
||||
{
|
||||
public:
|
||||
struct App
|
||||
{
|
||||
std::string path;
|
||||
// The agent's name for itself, and its application's desktop entry.
|
||||
std::string name;
|
||||
std::string appId;
|
||||
};
|
||||
|
||||
ComputerControl();
|
||||
|
||||
bool isAvailable() const { return m_available; }
|
||||
const std::vector<App>& allowedApps() const { return m_apps; }
|
||||
void forget(const std::string& path);
|
||||
|
||||
cutefish::base::Signal<> changed;
|
||||
|
||||
private:
|
||||
void reload();
|
||||
|
||||
cutefish::base::dbus::Proxy m_control;
|
||||
cutefish::base::dbus::NameWatch m_watch;
|
||||
cutefish::base::dbus::Match m_changedMatch;
|
||||
cutefish::base::dbus::PendingCall m_reload;
|
||||
cutefish::base::dbus::PendingCall m_forget;
|
||||
std::vector<App> m_apps;
|
||||
bool m_available = false;
|
||||
};
|
||||
|
||||
} // namespace settings
|
||||
@ -0,0 +1,150 @@
|
||||
// SPDX-FileCopyrightText: 2026 Reion Wong <reionwong@gmail.com>
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "base/application_registry.h"
|
||||
#include "base/desktop_entry.h"
|
||||
#include "base/i18n.h"
|
||||
#include "base/signal.h"
|
||||
#include "pages/privacy/computercontrol.h"
|
||||
#include "ui/button.h"
|
||||
#include "ui/cell_layout.h"
|
||||
#include "ui/icon.h"
|
||||
#include "ui/label.h"
|
||||
#include "ui/theme.h"
|
||||
#include "widgets/card.h"
|
||||
#include "widgets/listrow.h"
|
||||
#include "widgets/metrics.h"
|
||||
#include "widgets/sections.h"
|
||||
#include "window/page.h"
|
||||
#include "window/pagelist.h"
|
||||
|
||||
using namespace cutefish::ui;
|
||||
using cutefish::base::ApplicationRegistry;
|
||||
using cutefish::i18n::tr;
|
||||
|
||||
namespace settings {
|
||||
|
||||
namespace {
|
||||
|
||||
// A label in the theme's secondary text colour.
|
||||
class SecondaryLabel : public Label
|
||||
{
|
||||
public:
|
||||
using Label::Label;
|
||||
|
||||
protected:
|
||||
void layout() override
|
||||
{
|
||||
setColor(Theme::current().disabledText);
|
||||
Label::layout();
|
||||
}
|
||||
};
|
||||
|
||||
// An app allowed to control the computer: its name and icon over the program
|
||||
// trusted, and Remove.
|
||||
class AllowedAppRow : public ListRow
|
||||
{
|
||||
public:
|
||||
AllowedAppRow(const ComputerControl::App& app, std::function<void()> remove)
|
||||
{
|
||||
setInteractive(false);
|
||||
const auto entry = app.appId.empty() ? nullptr : ApplicationRegistry::instance().byId(app.appId);
|
||||
std::string title = entry && !entry->name.empty() ? entry->name : app.name;
|
||||
if (title.empty())
|
||||
title = app.path;
|
||||
setLeading<ThemeIcon>(entry && !entry->icon.empty() ? entry->icon : std::string("computer"), 24);
|
||||
ColumnLayout* texts = addContent<ColumnLayout>();
|
||||
texts->setSpacing(metrics::textSpacing);
|
||||
texts->setLayoutHints({.fillWidth = true, .verticalAlignment = Alignment::Center});
|
||||
Label* name = texts->addChild<Label>(title);
|
||||
name->setElide(Label::Elide::Right);
|
||||
name->setLayoutHints({.fillWidth = true});
|
||||
SecondaryLabel* path = texts->addChild<SecondaryLabel>(app.path);
|
||||
path->setElide(Label::Elide::Middle);
|
||||
path->setLayoutHints({.fillWidth = true});
|
||||
path->setVisible(title != app.path);
|
||||
Button* button = addContent<Button>(tr("Remove"));
|
||||
button->onClicked = std::move(remove);
|
||||
}
|
||||
};
|
||||
|
||||
class PrivacyPage : public Page
|
||||
{
|
||||
public:
|
||||
explicit PrivacyPage(SettingsWindow& window)
|
||||
: Page(window, tr("Privacy"))
|
||||
{
|
||||
Section* section = content()->addChild<Section>(tr("Computer control"));
|
||||
m_unavailable = section->addChild<Card>();
|
||||
m_unavailableLabel = m_unavailable->addChild<Label>(
|
||||
tr("Computer control is managed by the Cutefish compositor, which is not running."));
|
||||
m_unavailableLabel->setWrap(true);
|
||||
m_unavailableLabel->setLayoutHints({.fillWidth = true});
|
||||
|
||||
m_empty = section->addChild<Card>();
|
||||
m_emptyLabel = m_empty->addChild<Label>(tr("No apps are allowed to control this computer without asking."));
|
||||
m_emptyLabel->setWrap(true);
|
||||
m_emptyLabel->setLayoutHints({.fillWidth = true});
|
||||
|
||||
m_list = section->addChild<Card>(Card::Style::List);
|
||||
|
||||
m_note = content()->addChild<Footnote>(
|
||||
tr("An app you always allow can let its agent see the screen and use the pointer and keyboard "
|
||||
"without asking first. The Stop button in the status bar still shows while it is in control."));
|
||||
m_subscription = m_control.changed.connect([this] { update(); });
|
||||
// Names and icons come once the registry has read the desktop entries.
|
||||
m_registry = ApplicationRegistry::instance().changed.connect([this] { update(); });
|
||||
update();
|
||||
}
|
||||
|
||||
protected:
|
||||
void layout() override
|
||||
{
|
||||
m_unavailableLabel->setColor(Theme::current().disabledText);
|
||||
m_emptyLabel->setColor(Theme::current().disabledText);
|
||||
Page::layout();
|
||||
}
|
||||
|
||||
private:
|
||||
void update()
|
||||
{
|
||||
const bool available = m_control.isAvailable();
|
||||
const std::vector<ComputerControl::App>& apps = m_control.allowedApps();
|
||||
m_unavailable->setVisible(!available);
|
||||
m_empty->setVisible(available && apps.empty());
|
||||
m_list->setVisible(available && !apps.empty());
|
||||
m_note->setVisible(available);
|
||||
|
||||
// The rows go only once the list changed, after a Remove returned.
|
||||
for (ListRow* row : m_rows)
|
||||
m_list->takeChild(row);
|
||||
m_rows.clear();
|
||||
for (const ComputerControl::App& app : apps) {
|
||||
const std::string path = app.path;
|
||||
m_rows.push_back(m_list->addChild<AllowedAppRow>(app, [this, path] { m_control.forget(path); }));
|
||||
}
|
||||
}
|
||||
|
||||
ComputerControl m_control;
|
||||
cutefish::base::Subscription m_subscription;
|
||||
cutefish::base::Subscription m_registry;
|
||||
Card* m_unavailable;
|
||||
Label* m_unavailableLabel;
|
||||
Card* m_empty;
|
||||
Label* m_emptyLabel;
|
||||
Card* m_list;
|
||||
Footnote* m_note;
|
||||
std::vector<ListRow*> m_rows;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
static const PageRegistration registration("privacy", [](SettingsWindow& window) -> std::unique_ptr<Page> {
|
||||
return std::make_unique<PrivacyPage>(window);
|
||||
});
|
||||
|
||||
} // namespace settings
|
||||
Loading…
Reference in New Issue