diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f2740ec..61ed3c7 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -5,7 +5,7 @@ before_script: libxml2-devel dbus-daemon glib2-devel dbus-devel gobject-introspection-devel gettext-devel polkit-devel polkit-gnome git - lcov pam-devel + lcov pam-devel gtk3-devel accountsservice-devel flatpak-devel - export LANG=C.UTF-8 stages: diff --git a/malcontent-control/application.c b/malcontent-control/application.c new file mode 100644 index 0000000..d2b69a4 --- /dev/null +++ b/malcontent-control/application.c @@ -0,0 +1,243 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright © 2019 Endless Mobile, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * + * Authors: + * - Philip Withnall + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include + +#include "application.h" +#include "user-controls.h" +#include "user-selector.h" + + +static void user_selector_notify_user_cb (GObject *obj, + GParamSpec *pspec, + gpointer user_data); +static void user_manager_notify_is_loaded_cb (GObject *obj, + GParamSpec *pspec, + gpointer user_data); + + +/** + * MctApplication: + * + * #MctApplication is a top-level object representing the parental controls + * application. + * + * Since: 0.5.0 + */ +struct _MctApplication +{ + GtkApplication parent_instance; + + ActUserManager *user_manager; /* (owned) */ + + MctUserSelector *user_selector; + MctUserControls *user_controls; + GtkStack *main_stack; + GtkLabel *error_title; + GtkLabel *error_message; +}; + +G_DEFINE_TYPE (MctApplication, mct_application, GTK_TYPE_APPLICATION) + +static void +mct_application_init (MctApplication *self) +{ + /* Nothing to do here. */ +} + +static void +mct_application_constructed (GObject *object) +{ + GApplication *application = G_APPLICATION (object); + + g_application_set_application_id (application, "org.freedesktop.MalcontentControl"); + + /* Localisation */ + bindtextdomain ("malcontent", PACKAGE_LOCALE_DIR); + bind_textdomain_codeset ("malcontent", "UTF-8"); + textdomain ("malcontent"); + + g_set_application_name (_("Parental Controls")); + gtk_window_set_default_icon_name ("org.freedesktop.MalcontentControl"); + + G_OBJECT_CLASS (mct_application_parent_class)->constructed (object); +} + +static void +mct_application_dispose (GObject *object) +{ + MctApplication *self = MCT_APPLICATION (object); + + if (self->user_manager != NULL) + { + g_signal_handlers_disconnect_by_func (self->user_manager, + user_manager_notify_is_loaded_cb, self); + g_clear_object (&self->user_manager); + } + + G_OBJECT_CLASS (mct_application_parent_class)->dispose (object); +} + +static GtkWindow * +mct_application_get_main_window (MctApplication *self) +{ + return gtk_application_get_active_window (GTK_APPLICATION (self)); +} + +static void +mct_application_activate (GApplication *application) +{ + MctApplication *self = MCT_APPLICATION (application); + GtkWindow *window = NULL; + + window = mct_application_get_main_window (self); + + if (window == NULL) + { + g_autoptr(GtkBuilder) builder = NULL; + g_autoptr(GError) local_error = NULL; + + /* Ensure the types used in the UI are registered. */ + g_type_ensure (MCT_TYPE_USER_CONTROLS); + g_type_ensure (MCT_TYPE_USER_SELECTOR); + + builder = gtk_builder_new (); + + g_assert (self->user_manager == NULL); + self->user_manager = g_object_ref (act_user_manager_get_default ()); + + gtk_builder_set_translation_domain (builder, "malcontent"); + gtk_builder_expose_object (builder, "user_manager", G_OBJECT (self->user_manager)); + + gtk_builder_add_from_resource (builder, "/org/freedesktop/MalcontentControl/ui/main.ui", &local_error); + g_assert (local_error == NULL); + + /* Set up the main window. */ + window = GTK_WINDOW (gtk_builder_get_object (builder, "main_window")); + gtk_window_set_application (window, GTK_APPLICATION (application)); + + self->main_stack = GTK_STACK (gtk_builder_get_object (builder, "main_stack")); + self->user_selector = MCT_USER_SELECTOR (gtk_builder_get_object (builder, "user_selector")); + self->user_controls = MCT_USER_CONTROLS (gtk_builder_get_object (builder, "user_controls")); + self->error_title = GTK_LABEL (gtk_builder_get_object (builder, "error_title")); + self->error_message = GTK_LABEL (gtk_builder_get_object (builder, "error_message")); + + /* Connect signals. */ + g_signal_connect (self->user_selector, "notify::user", + G_CALLBACK (user_selector_notify_user_cb), + self); + g_signal_connect (self->user_manager, "notify::is-loaded", + G_CALLBACK (user_manager_notify_is_loaded_cb), self); + + /* Work out whether to show the loading page or the main page, and show + * the controls for the initially selected user. */ + user_selector_notify_user_cb (G_OBJECT (self->user_selector), NULL, self); + user_manager_notify_is_loaded_cb (G_OBJECT (self->user_manager), NULL, self); + + gtk_widget_show (GTK_WIDGET (window)); + } + + /* Bring the window to the front. */ + gtk_window_present (window); +} + +static void +mct_application_class_init (MctApplicationClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GApplicationClass *application_class = G_APPLICATION_CLASS (klass); + + object_class->constructed = mct_application_constructed; + object_class->dispose = mct_application_dispose; + + application_class->activate = mct_application_activate; +} + +static void +user_selector_notify_user_cb (GObject *obj, + GParamSpec *pspec, + gpointer user_data) +{ + MctUserSelector *selector = MCT_USER_SELECTOR (obj); + MctApplication *self = MCT_APPLICATION (user_data); + ActUser *user; + + user = mct_user_selector_get_user (selector); + + mct_user_controls_set_user (self->user_controls, user); +} + +static void +user_manager_notify_is_loaded_cb (GObject *obj, + GParamSpec *pspec, + gpointer user_data) +{ + MctApplication *self = MCT_APPLICATION (user_data); + ActUserManager *user_manager = ACT_USER_MANAGER (obj); + gboolean is_loaded; + const gchar *new_page_name; + + /* The implementation of #ActUserManager guarantees that once is-loaded is + * true, it is never reset to false. */ + g_object_get (user_manager, "is-loaded", &is_loaded, NULL); + + /* Handle any loading errors. */ + if (is_loaded && act_user_manager_no_service (user_manager)) + { + gtk_label_set_label (self->error_title, + _("Failed to load user data from the system")); + gtk_label_set_label (self->error_message, + _("Please make sure that the AccountsService is installed and enabled.")); + + new_page_name = "error"; + } + else if (is_loaded) + { + new_page_name = "controls"; + } + else + { + new_page_name = "loading"; + } + + gtk_stack_set_visible_child_name (self->main_stack, new_page_name); +} + +/** + * mct_application_new: + * + * Create a new #MctApplication. + * + * Returns: (transfer full): a new #MctApplication + * Since: 0.5.0 + */ +MctApplication * +mct_application_new (void) +{ + return g_object_new (MCT_TYPE_APPLICATION, NULL); +} diff --git a/malcontent-control/application.h b/malcontent-control/application.h new file mode 100644 index 0000000..8c0f39a --- /dev/null +++ b/malcontent-control/application.h @@ -0,0 +1,37 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright © 2019 Endless Mobile, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * + * Authors: + * - Philip Withnall + */ + +#pragma once + +#include +#include +#include +#include + + +G_BEGIN_DECLS + +#define MCT_TYPE_APPLICATION mct_application_get_type () +G_DECLARE_FINAL_TYPE (MctApplication, mct_application, MCT, APPLICATION, GtkApplication) + +MctApplication *mct_application_new (void); + +G_END_DECLS diff --git a/malcontent-control/carousel.c b/malcontent-control/carousel.c new file mode 100644 index 0000000..2a10ee4 --- /dev/null +++ b/malcontent-control/carousel.c @@ -0,0 +1,461 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright © 2016 Red Hat, Inc. + * Copyright © 2019 Endless Mobile, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * + * Authors: + * - Felipe Borges + * - Philip Withnall + */ + +#include +#include + +#include "carousel.h" + + +#define ARROW_SIZE 20 + +struct _MctCarouselItem { + GtkRadioButton parent; + + gint page; +}; + +G_DEFINE_TYPE (MctCarouselItem, mct_carousel_item, GTK_TYPE_RADIO_BUTTON) + +GtkWidget * +mct_carousel_item_new (void) +{ + return g_object_new (MCT_TYPE_CAROUSEL_ITEM, NULL); +} + +static void +mct_carousel_item_class_init (MctCarouselItemClass *klass) +{ +} + +static void +mct_carousel_item_init (MctCarouselItem *self) +{ + gtk_toggle_button_set_mode (GTK_TOGGLE_BUTTON (self), FALSE); + gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (self)), + "carousel-item"); +} + +struct _MctCarousel { + GtkRevealer parent; + + GList *children; + gint visible_page; + MctCarouselItem *selected_item; + GtkWidget *last_box; + GtkWidget *arrow; + gint arrow_start_x; + + /* Widgets */ + GtkStack *stack; + GtkWidget *go_back_button; + GtkWidget *go_next_button; + + GtkStyleProvider *provider; +}; + +G_DEFINE_TYPE (MctCarousel, mct_carousel, GTK_TYPE_REVEALER) + +enum { + ITEM_ACTIVATED, + NUM_SIGNALS +}; + +static guint signals[NUM_SIGNALS] = { 0, }; + +#define ITEMS_PER_PAGE 3 + +static gint +mct_carousel_item_get_x (MctCarouselItem *item, + MctCarousel *carousel) +{ + GtkWidget *widget, *parent; + gint width; + gint dest_x; + + parent = GTK_WIDGET (carousel->stack); + widget = GTK_WIDGET (item); + + width = gtk_widget_get_allocated_width (widget); + gtk_widget_translate_coordinates (widget, + parent, + width / 2, + 0, + &dest_x, + NULL); + + return CLAMP (dest_x - ARROW_SIZE, + 0, + gtk_widget_get_allocated_width (parent)); +} + +static void +mct_carousel_move_arrow (MctCarousel *self) +{ + GtkStyleContext *context; + gchar *css; + gint end_x; + GtkSettings *settings; + gboolean animations; + + if (!self->selected_item) + return; + + end_x = mct_carousel_item_get_x (self->selected_item, self); + + context = gtk_widget_get_style_context (self->arrow); + if (self->provider) + gtk_style_context_remove_provider (context, self->provider); + g_clear_object (&self->provider); + + settings = gtk_widget_get_settings (GTK_WIDGET (self)); + g_object_get (settings, "gtk-enable-animations", &animations, NULL); + + /* Animate the arrow movement if animations are enabled. Otherwise, + * jump the arrow to the right location instantly. */ + if (animations) + { + css = g_strdup_printf ("@keyframes arrow_keyframes-%d-%d {\n" + " from { margin-left: %dpx; }\n" + " to { margin-left: %dpx; }\n" + "}\n" + "* {\n" + " animation-name: arrow_keyframes-%d-%d;\n" + "}\n", + self->arrow_start_x, end_x, + self->arrow_start_x, end_x, + self->arrow_start_x, end_x); + } + else + { + css = g_strdup_printf ("* { margin-left: %dpx }", end_x); + } + + self->provider = GTK_STYLE_PROVIDER (gtk_css_provider_new ()); + gtk_css_provider_load_from_data (GTK_CSS_PROVIDER (self->provider), css, -1, NULL); + gtk_style_context_add_provider (context, self->provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + + g_free (css); +} + +static gint +get_last_page_number (MctCarousel *self) +{ + if (g_list_length (self->children) == 0) + return 0; + + return ((g_list_length (self->children) - 1) / ITEMS_PER_PAGE); +} + +static void +update_buttons_visibility (MctCarousel *self) +{ + gtk_widget_set_visible (self->go_back_button, (self->visible_page > 0)); + gtk_widget_set_visible (self->go_next_button, (self->visible_page < get_last_page_number (self))); +} + +/** + * mct_carousel_find_item: + * @carousel: an MctCarousel instance + * @data: user data passed to the comparation function + * @func: the function to call for each element. + * It should return 0 when the desired element is found + * + * Finds an MctCarousel item using the supplied function to find the + * desired element. + * Ideally useful for matching a model object and its correspondent + * widget. + * + * Returns: the found MctCarouselItem, or %NULL if it is not found + */ +MctCarouselItem * +mct_carousel_find_item (MctCarousel *self, + gconstpointer data, + GCompareFunc func) +{ + GList *list; + + list = self->children; + while (list != NULL) + { + if (!func (list->data, data)) + return list->data; + list = list->next; + } + + return NULL; +} + +static void +on_item_toggled (MctCarouselItem *item, + GdkEvent *event, + gpointer user_data) +{ + MctCarousel *self = MCT_CAROUSEL (user_data); + + mct_carousel_select_item (self, item); +} + +void +mct_carousel_select_item (MctCarousel *self, + MctCarouselItem *item) +{ + gchar *page_name; + gboolean page_changed = TRUE; + + /* Select first user if none is specified */ + if (item == NULL) + { + if (self->children != NULL) + item = self->children->data; + else + return; + } + + if (self->selected_item != NULL) + { + page_changed = (self->selected_item->page != item->page); + self->arrow_start_x = mct_carousel_item_get_x (self->selected_item, self); + } + + self->selected_item = item; + self->visible_page = item->page; + g_signal_emit (self, signals[ITEM_ACTIVATED], 0, item); + + if (!page_changed) + { + mct_carousel_move_arrow (self); + return; + } + + page_name = g_strdup_printf ("%d", self->visible_page); + gtk_stack_set_visible_child_name (self->stack, page_name); + + g_free (page_name); + + update_buttons_visibility (self); + + /* mct_carousel_move_arrow is called from on_transition_running */ +} + +static void +mct_carousel_select_item_at_index (MctCarousel *self, + gint index) +{ + GList *l = NULL; + + l = g_list_nth (self->children, index); + mct_carousel_select_item (self, l->data); +} + +static void +mct_carousel_goto_previous_page (GtkWidget *button, + gpointer user_data) +{ + MctCarousel *self = MCT_CAROUSEL (user_data); + + self->visible_page--; + if (self->visible_page < 0) + self->visible_page = 0; + + /* Select first item of the page */ + mct_carousel_select_item_at_index (self, self->visible_page * ITEMS_PER_PAGE); +} + +static void +mct_carousel_goto_next_page (GtkWidget *button, + gpointer user_data) +{ + MctCarousel *self = MCT_CAROUSEL (user_data); + gint last_page; + + last_page = get_last_page_number (self); + + self->visible_page++; + if (self->visible_page > last_page) + self->visible_page = last_page; + + /* Select first item of the page */ + mct_carousel_select_item_at_index (self, self->visible_page * ITEMS_PER_PAGE); +} + +static void +mct_carousel_add (GtkContainer *container, + GtkWidget *widget) +{ + MctCarousel *self = MCT_CAROUSEL (container); + gboolean last_box_is_full; + + if (!MCT_IS_CAROUSEL_ITEM (widget)) + { + GTK_CONTAINER_CLASS (mct_carousel_parent_class)->add (container, widget); + return; + } + + gtk_style_context_add_class (gtk_widget_get_style_context (widget), "menu"); + gtk_button_set_relief (GTK_BUTTON (widget), GTK_RELIEF_NONE); + + self->children = g_list_append (self->children, widget); + MCT_CAROUSEL_ITEM (widget)->page = get_last_page_number (self); + if (self->selected_item != NULL) + gtk_radio_button_join_group (GTK_RADIO_BUTTON (widget), GTK_RADIO_BUTTON (self->selected_item)); + g_signal_connect (widget, "button-press-event", G_CALLBACK (on_item_toggled), self); + + last_box_is_full = ((g_list_length (self->children) - 1) % ITEMS_PER_PAGE == 0); + if (last_box_is_full) + { + g_autofree gchar *page = NULL; + + page = g_strdup_printf ("%d", MCT_CAROUSEL_ITEM (widget)->page); + self->last_box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); + gtk_widget_show (self->last_box); + gtk_widget_set_valign (self->last_box, GTK_ALIGN_CENTER); + gtk_stack_add_named (self->stack, self->last_box, page); + } + + gtk_widget_show_all (widget); + gtk_box_pack_start (GTK_BOX (self->last_box), widget, TRUE, FALSE, 10); + + update_buttons_visibility (self); +} + +static void +destroy_widget_cb (GtkWidget *widget, + gpointer user_data) +{ + gtk_widget_destroy (widget); +} + +void +mct_carousel_purge_items (MctCarousel *self) +{ + gtk_container_forall (GTK_CONTAINER (self->stack), + destroy_widget_cb, + NULL); + + g_list_free (self->children); + self->children = NULL; + self->visible_page = 0; + self->selected_item = NULL; +} + +MctCarousel * +mct_carousel_new (void) +{ + return g_object_new (MCT_TYPE_CAROUSEL, NULL); +} + +static void +mct_carousel_dispose (GObject *object) +{ + MctCarousel *self = MCT_CAROUSEL (object); + + g_clear_object (&self->provider); + if (self->children != NULL) + { + g_list_free (self->children); + self->children = NULL; + } + + G_OBJECT_CLASS (mct_carousel_parent_class)->dispose (object); +} + +static void +mct_carousel_class_init (MctCarouselClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *wclass = GTK_WIDGET_CLASS (klass); + GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass); + + gtk_widget_class_set_template_from_resource (wclass, + "/org/freedesktop/MalcontentControl/ui/carousel.ui"); + + gtk_widget_class_bind_template_child (wclass, MctCarousel, stack); + gtk_widget_class_bind_template_child (wclass, MctCarousel, go_back_button); + gtk_widget_class_bind_template_child (wclass, MctCarousel, go_next_button); + gtk_widget_class_bind_template_child (wclass, MctCarousel, arrow); + + gtk_widget_class_bind_template_callback (wclass, mct_carousel_goto_previous_page); + gtk_widget_class_bind_template_callback (wclass, mct_carousel_goto_next_page); + + object_class->dispose = mct_carousel_dispose; + + container_class->add = mct_carousel_add; + + signals[ITEM_ACTIVATED] = + g_signal_new ("item-activated", + MCT_TYPE_CAROUSEL, + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, 1, + MCT_TYPE_CAROUSEL_ITEM); +} + +static void +on_size_allocate (MctCarousel *self) +{ + if (self->selected_item == NULL) + return; + + if (gtk_stack_get_transition_running (self->stack)) + return; + + self->arrow_start_x = mct_carousel_item_get_x (self->selected_item, self); + mct_carousel_move_arrow (self); +} + +static void +on_transition_running (MctCarousel *self) +{ + if (!gtk_stack_get_transition_running (self->stack)) + mct_carousel_move_arrow (self); +} + +static void +mct_carousel_init (MctCarousel *self) +{ + GtkStyleProvider *provider; + + gtk_widget_init_template (GTK_WIDGET (self)); + + provider = GTK_STYLE_PROVIDER (gtk_css_provider_new ()); + gtk_css_provider_load_from_resource (GTK_CSS_PROVIDER (provider), + "/org/freedesktop/MalcontentControl/ui/carousel.css"); + + gtk_style_context_add_provider_for_screen (gdk_screen_get_default (), + provider, + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + + g_object_unref (provider); + + g_signal_connect_swapped (self->stack, "size-allocate", G_CALLBACK (on_size_allocate), self); + g_signal_connect_swapped (self->stack, "notify::transition-running", G_CALLBACK (on_transition_running), self); +} + +guint +mct_carousel_get_item_count (MctCarousel *self) +{ + return g_list_length (self->children); +} diff --git a/malcontent-control/carousel.css b/malcontent-control/carousel.css new file mode 100644 index 0000000..738562c --- /dev/null +++ b/malcontent-control/carousel.css @@ -0,0 +1,30 @@ +.carousel-arrow-container { + border-bottom: 1px solid @borders; +} + +.carousel-arrow, +.carousel-inner-arrow { + border-width: 20px; /* ARROW_SIZE */ + border-style: solid; + border-color: transparent; +} + +.carousel-arrow { + border-bottom-color: @borders; + margin-bottom: -1px; + animation-duration: 200ms; + animation-timing-function: ease-in-out; + animation-fill-mode: forwards; +} + +.carousel-inner-arrow { + border-bottom-color: @theme_bg_color; + margin-bottom: -2px; +} + +.carousel-item { + background: transparent; + box-shadow: none; + border: none; + color: @theme_fg_color; +} diff --git a/malcontent-control/carousel.h b/malcontent-control/carousel.h new file mode 100644 index 0000000..db18e9e --- /dev/null +++ b/malcontent-control/carousel.h @@ -0,0 +1,52 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright © 2016 Red Hat, Inc. + * Copyright © 2019 Endless Mobile, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * + * Authors: + * - Felipe Borges + * - Philip Withnall + */ + +#pragma once + +#include + + +G_BEGIN_DECLS + +#define MCT_TYPE_CAROUSEL_ITEM (mct_carousel_item_get_type ()) +G_DECLARE_FINAL_TYPE (MctCarouselItem, mct_carousel_item, MCT, CAROUSEL_ITEM, GtkRadioButton) + +#define MCT_TYPE_CAROUSEL (mct_carousel_get_type ()) +G_DECLARE_FINAL_TYPE (MctCarousel, mct_carousel, MCT, CAROUSEL, GtkRevealer) + +GtkWidget *mct_carousel_item_new (void); + +MctCarousel *mct_carousel_new (void); + +void mct_carousel_purge_items (MctCarousel *self); + +MctCarouselItem *mct_carousel_find_item (MctCarousel *self, + gconstpointer data, + GCompareFunc func); + +void mct_carousel_select_item (MctCarousel *self, + MctCarouselItem *item); + +guint mct_carousel_get_item_count (MctCarousel *self); + +G_END_DECLS diff --git a/malcontent-control/carousel.ui b/malcontent-control/carousel.ui new file mode 100644 index 0000000..41db014 --- /dev/null +++ b/malcontent-control/carousel.ui @@ -0,0 +1,130 @@ + + + + + + + diff --git a/malcontent-control/gs-content-rating.c b/malcontent-control/gs-content-rating.c new file mode 100644 index 0000000..0e584be --- /dev/null +++ b/malcontent-control/gs-content-rating.c @@ -0,0 +1,968 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * + * Copyright (C) 2015-2016 Richard Hughes + * + * Licensed under the GNU General Public License Version 2 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" + +#include +#include + +#include "gs-content-rating.h" + +const gchar * +gs_content_rating_system_to_str (GsContentRatingSystem system) +{ + if (system == GS_CONTENT_RATING_SYSTEM_INCAA) + return "INCAA"; + if (system == GS_CONTENT_RATING_SYSTEM_ACB) + return "ACB"; + if (system == GS_CONTENT_RATING_SYSTEM_DJCTQ) + return "DJCTQ"; + if (system == GS_CONTENT_RATING_SYSTEM_GSRR) + return "GSRR"; + if (system == GS_CONTENT_RATING_SYSTEM_PEGI) + return "PEGI"; + if (system == GS_CONTENT_RATING_SYSTEM_KAVI) + return "KAVI"; + if (system == GS_CONTENT_RATING_SYSTEM_USK) + return "USK"; + if (system == GS_CONTENT_RATING_SYSTEM_ESRA) + return "ESRA"; + if (system == GS_CONTENT_RATING_SYSTEM_CERO) + return "CERO"; + if (system == GS_CONTENT_RATING_SYSTEM_OFLCNZ) + return "OFLCNZ"; + if (system == GS_CONTENT_RATING_SYSTEM_RUSSIA) + return "RUSSIA"; + if (system == GS_CONTENT_RATING_SYSTEM_MDA) + return "MDA"; + if (system == GS_CONTENT_RATING_SYSTEM_GRAC) + return "GRAC"; + if (system == GS_CONTENT_RATING_SYSTEM_ESRB) + return "ESRB"; + if (system == GS_CONTENT_RATING_SYSTEM_IARC) + return "IARC"; + return NULL; +} + +const gchar * +gs_content_rating_key_value_to_str (const gchar *id, MctAppFilterOarsValue value) +{ + guint i; + const struct { + const gchar *id; + MctAppFilterOarsValue value; + const gchar *desc; + } tab[] = { + { "violence-cartoon", MCT_APP_FILTER_OARS_VALUE_NONE, + /* TRANSLATORS: content rating description */ + _("No cartoon violence") }, + { "violence-cartoon", MCT_APP_FILTER_OARS_VALUE_MILD, + /* TRANSLATORS: content rating description */ + _("Cartoon characters in unsafe situations") }, + { "violence-cartoon", MCT_APP_FILTER_OARS_VALUE_MODERATE, + /* TRANSLATORS: content rating description */ + _("Cartoon characters in aggressive conflict") }, + { "violence-cartoon", MCT_APP_FILTER_OARS_VALUE_INTENSE, + /* TRANSLATORS: content rating description */ + _("Graphic violence involving cartoon characters") }, + { "violence-fantasy", MCT_APP_FILTER_OARS_VALUE_NONE, + /* TRANSLATORS: content rating description */ + _("No fantasy violence") }, + { "violence-fantasy", MCT_APP_FILTER_OARS_VALUE_MILD, + /* TRANSLATORS: content rating description */ + _("Characters in unsafe situations easily distinguishable from reality") }, + { "violence-fantasy", MCT_APP_FILTER_OARS_VALUE_MODERATE, + /* TRANSLATORS: content rating description */ + _("Characters in aggressive conflict easily distinguishable from reality") }, + { "violence-fantasy", MCT_APP_FILTER_OARS_VALUE_INTENSE, + /* TRANSLATORS: content rating description */ + _("Graphic violence easily distinguishable from reality") }, + { "violence-realistic", MCT_APP_FILTER_OARS_VALUE_NONE, + /* TRANSLATORS: content rating description */ + _("No realistic violence") }, + { "violence-realistic", MCT_APP_FILTER_OARS_VALUE_MILD, + /* TRANSLATORS: content rating description */ + _("Mildly realistic characters in unsafe situations") }, + { "violence-realistic", MCT_APP_FILTER_OARS_VALUE_MODERATE, + /* TRANSLATORS: content rating description */ + _("Depictions of realistic characters in aggressive conflict") }, + { "violence-realistic", MCT_APP_FILTER_OARS_VALUE_INTENSE, + /* TRANSLATORS: content rating description */ + _("Graphic violence involving realistic characters") }, + { "violence-bloodshed", MCT_APP_FILTER_OARS_VALUE_NONE, + /* TRANSLATORS: content rating description */ + _("No bloodshed") }, + { "violence-bloodshed", MCT_APP_FILTER_OARS_VALUE_MILD, + /* TRANSLATORS: content rating description */ + _("Unrealistic bloodshed") }, + { "violence-bloodshed", MCT_APP_FILTER_OARS_VALUE_MODERATE, + /* TRANSLATORS: content rating description */ + _("Realistic bloodshed") }, + { "violence-bloodshed", MCT_APP_FILTER_OARS_VALUE_INTENSE, + /* TRANSLATORS: content rating description */ + _("Depictions of bloodshed and the mutilation of body parts") }, + { "violence-sexual", MCT_APP_FILTER_OARS_VALUE_NONE, + /* TRANSLATORS: content rating description */ + _("No sexual violence") }, + { "violence-sexual", MCT_APP_FILTER_OARS_VALUE_INTENSE, + /* TRANSLATORS: content rating description */ + _("Rape or other violent sexual behavior") }, + { "drugs-alcohol", MCT_APP_FILTER_OARS_VALUE_NONE, + /* TRANSLATORS: content rating description */ + _("No references to alcohol") }, + { "drugs-alcohol", MCT_APP_FILTER_OARS_VALUE_MILD, + /* TRANSLATORS: content rating description */ + _("References to alcoholic beverages") }, + { "drugs-alcohol", MCT_APP_FILTER_OARS_VALUE_MODERATE, + /* TRANSLATORS: content rating description */ + _("Use of alcoholic beverages") }, + { "drugs-narcotics", MCT_APP_FILTER_OARS_VALUE_NONE, + /* TRANSLATORS: content rating description */ + _("No references to illicit drugs") }, + { "drugs-narcotics", MCT_APP_FILTER_OARS_VALUE_MILD, + /* TRANSLATORS: content rating description */ + _("References to illicit drugs") }, + { "drugs-narcotics", MCT_APP_FILTER_OARS_VALUE_MODERATE, + /* TRANSLATORS: content rating description */ + _("Use of illicit drugs") }, + { "drugs-tobacco", MCT_APP_FILTER_OARS_VALUE_MILD, + /* TRANSLATORS: content rating description */ + _("References to tobacco products") }, + { "drugs-tobacco", MCT_APP_FILTER_OARS_VALUE_MODERATE, + /* TRANSLATORS: content rating description */ + _("Use of tobacco products") }, + { "sex-nudity", MCT_APP_FILTER_OARS_VALUE_NONE, + /* TRANSLATORS: content rating description */ + _("No nudity of any sort") }, + { "sex-nudity", MCT_APP_FILTER_OARS_VALUE_MILD, + /* TRANSLATORS: content rating description */ + _("Brief artistic nudity") }, + { "sex-nudity", MCT_APP_FILTER_OARS_VALUE_MODERATE, + /* TRANSLATORS: content rating description */ + _("Prolonged nudity") }, + { "sex-themes", MCT_APP_FILTER_OARS_VALUE_NONE, + /* TRANSLATORS: content rating description */ + _("No references or depictions of sexual nature") }, + { "sex-themes", MCT_APP_FILTER_OARS_VALUE_MILD, + /* TRANSLATORS: content rating description */ + _("Provocative references or depictions") }, + { "sex-themes", MCT_APP_FILTER_OARS_VALUE_MODERATE, + /* TRANSLATORS: content rating description */ + _("Sexual references or depictions") }, + { "sex-themes", MCT_APP_FILTER_OARS_VALUE_INTENSE, + /* TRANSLATORS: content rating description */ + _("Graphic sexual behavior") }, + { "language-profanity", MCT_APP_FILTER_OARS_VALUE_NONE, + /* TRANSLATORS: content rating description */ + _("No profanity of any kind") }, + { "language-profanity", MCT_APP_FILTER_OARS_VALUE_MILD, + /* TRANSLATORS: content rating description */ + _("Mild or infrequent use of profanity") }, + { "language-profanity", MCT_APP_FILTER_OARS_VALUE_MODERATE, + /* TRANSLATORS: content rating description */ + _("Moderate use of profanity") }, + { "language-profanity", MCT_APP_FILTER_OARS_VALUE_INTENSE, + /* TRANSLATORS: content rating description */ + _("Strong or frequent use of profanity") }, + { "language-humor", MCT_APP_FILTER_OARS_VALUE_NONE, + /* TRANSLATORS: content rating description */ + _("No inappropriate humor") }, + { "language-humor", MCT_APP_FILTER_OARS_VALUE_MILD, + /* TRANSLATORS: content rating description */ + _("Slapstick humor") }, + { "language-humor", MCT_APP_FILTER_OARS_VALUE_MODERATE, + /* TRANSLATORS: content rating description */ + _("Vulgar or bathroom humor") }, + { "language-humor", MCT_APP_FILTER_OARS_VALUE_INTENSE, + /* TRANSLATORS: content rating description */ + _("Mature or sexual humor") }, + { "language-discrimination", MCT_APP_FILTER_OARS_VALUE_NONE, + /* TRANSLATORS: content rating description */ + _("No discriminatory language of any kind") }, + { "language-discrimination", MCT_APP_FILTER_OARS_VALUE_MILD, + /* TRANSLATORS: content rating description */ + _("Negativity towards a specific group of people") }, + { "language-discrimination", MCT_APP_FILTER_OARS_VALUE_MODERATE, + /* TRANSLATORS: content rating description */ + _("Discrimination designed to cause emotional harm") }, + { "language-discrimination", MCT_APP_FILTER_OARS_VALUE_INTENSE, + /* TRANSLATORS: content rating description */ + _("Explicit discrimination based on gender, sexuality, race or religion") }, + { "money-advertising", MCT_APP_FILTER_OARS_VALUE_NONE, + /* TRANSLATORS: content rating description */ + _("No advertising of any kind") }, + { "money-advertising", MCT_APP_FILTER_OARS_VALUE_MILD, + /* TRANSLATORS: content rating description */ + _("Product placement") }, + { "money-advertising", MCT_APP_FILTER_OARS_VALUE_MODERATE, + /* TRANSLATORS: content rating description */ + _("Explicit references to specific brands or trademarked products") }, + { "money-advertising", MCT_APP_FILTER_OARS_VALUE_INTENSE, + /* TRANSLATORS: content rating description */ + _("Users are encouraged to purchase specific real-world items") }, + { "money-gambling", MCT_APP_FILTER_OARS_VALUE_NONE, + /* TRANSLATORS: content rating description */ + _("No gambling of any kind") }, + { "money-gambling", MCT_APP_FILTER_OARS_VALUE_MILD, + /* TRANSLATORS: content rating description */ + _("Gambling on random events using tokens or credits") }, + { "money-gambling", MCT_APP_FILTER_OARS_VALUE_MODERATE, + /* TRANSLATORS: content rating description */ + _("Gambling using “play” money") }, + { "money-gambling", MCT_APP_FILTER_OARS_VALUE_INTENSE, + /* TRANSLATORS: content rating description */ + _("Gambling using real money") }, + { "money-purchasing", MCT_APP_FILTER_OARS_VALUE_NONE, + /* TRANSLATORS: content rating description */ + _("No ability to spend money") }, + { "money-purchasing", MCT_APP_FILTER_OARS_VALUE_MILD, /* v1.1 */ + /* TRANSLATORS: content rating description */ + _("Users are encouraged to donate real money") }, + { "money-purchasing", MCT_APP_FILTER_OARS_VALUE_INTENSE, + /* TRANSLATORS: content rating description */ + _("Ability to spend real money in-game") }, + { "social-chat", MCT_APP_FILTER_OARS_VALUE_NONE, + /* TRANSLATORS: content rating description */ + _("No way to chat with other users") }, + { "social-chat", MCT_APP_FILTER_OARS_VALUE_MILD, + /* TRANSLATORS: content rating description */ + _("User-to-user game interactions without chat functionality") }, + { "social-chat", MCT_APP_FILTER_OARS_VALUE_MODERATE, + /* TRANSLATORS: content rating description */ + _("Moderated chat functionality between users") }, + { "social-chat", MCT_APP_FILTER_OARS_VALUE_INTENSE, + /* TRANSLATORS: content rating description */ + _("Uncontrolled chat functionality between users") }, + { "social-audio", MCT_APP_FILTER_OARS_VALUE_NONE, + /* TRANSLATORS: content rating description */ + _("No way to talk with other users") }, + { "social-audio", MCT_APP_FILTER_OARS_VALUE_INTENSE, + /* TRANSLATORS: content rating description */ + _("Uncontrolled audio or video chat functionality between users") }, + { "social-contacts", MCT_APP_FILTER_OARS_VALUE_NONE, + /* TRANSLATORS: content rating description */ + _("No sharing of social network usernames or email addresses") }, + { "social-contacts", MCT_APP_FILTER_OARS_VALUE_INTENSE, + /* TRANSLATORS: content rating description */ + _("Sharing social network usernames or email addresses") }, + { "social-info", MCT_APP_FILTER_OARS_VALUE_NONE, + /* TRANSLATORS: content rating description */ + _("No sharing of user information with 3rd parties") }, + { "social-info", MCT_APP_FILTER_OARS_VALUE_MILD, /* v1.1 */ + /* TRANSLATORS: content rating description */ + _("Checking for the latest application version") }, + { "social-info", MCT_APP_FILTER_OARS_VALUE_MODERATE, /* v1.1 */ + /* TRANSLATORS: content rating description */ + _("Sharing diagnostic data that does not let others identify the user") }, + { "social-info", MCT_APP_FILTER_OARS_VALUE_INTENSE, + /* TRANSLATORS: content rating description */ + _("Sharing information that lets others identify the user") }, + { "social-location", MCT_APP_FILTER_OARS_VALUE_NONE, + /* TRANSLATORS: content rating description */ + _("No sharing of physical location to other users") }, + { "social-location", MCT_APP_FILTER_OARS_VALUE_INTENSE, + /* TRANSLATORS: content rating description */ + _("Sharing physical location to other users") }, + + /* v1.1 */ + { "sex-homosexuality", MCT_APP_FILTER_OARS_VALUE_NONE, + /* TRANSLATORS: content rating description */ + _("No references to homosexuality") }, + { "sex-homosexuality", MCT_APP_FILTER_OARS_VALUE_MILD, + /* TRANSLATORS: content rating description */ + _("Indirect references to homosexuality") }, + { "sex-homosexuality", MCT_APP_FILTER_OARS_VALUE_MODERATE, + /* TRANSLATORS: content rating description */ + _("Kissing between people of the same gender") }, + { "sex-homosexuality", MCT_APP_FILTER_OARS_VALUE_INTENSE, + /* TRANSLATORS: content rating description */ + _("Graphic sexual behavior between people of the same gender") }, + { "sex-prostitution", MCT_APP_FILTER_OARS_VALUE_NONE, + /* TRANSLATORS: content rating description */ + _("No references to prostitution") }, + { "sex-prostitution", MCT_APP_FILTER_OARS_VALUE_MILD, + /* TRANSLATORS: content rating description */ + _("Indirect references to prostitution") }, + { "sex-prostitution", MCT_APP_FILTER_OARS_VALUE_MODERATE, + /* TRANSLATORS: content rating description */ + _("Direct references to prostitution") }, + { "sex-prostitution", MCT_APP_FILTER_OARS_VALUE_INTENSE, + /* TRANSLATORS: content rating description */ + _("Graphic depictions of the act of prostitution") }, + { "sex-adultery", MCT_APP_FILTER_OARS_VALUE_NONE, + /* TRANSLATORS: content rating description */ + _("No references to adultery") }, + { "sex-adultery", MCT_APP_FILTER_OARS_VALUE_MILD, + /* TRANSLATORS: content rating description */ + _("Indirect references to adultery") }, + { "sex-adultery", MCT_APP_FILTER_OARS_VALUE_MODERATE, + /* TRANSLATORS: content rating description */ + _("Direct references to adultery") }, + { "sex-adultery", MCT_APP_FILTER_OARS_VALUE_INTENSE, + /* TRANSLATORS: content rating description */ + _("Graphic depictions of the act of adultery") }, + { "sex-appearance", MCT_APP_FILTER_OARS_VALUE_NONE, + /* TRANSLATORS: content rating description */ + _("No sexualized characters") }, + { "sex-appearance", MCT_APP_FILTER_OARS_VALUE_MODERATE, + /* TRANSLATORS: content rating description */ + _("Scantily clad human characters") }, + { "sex-appearance", MCT_APP_FILTER_OARS_VALUE_INTENSE, + /* TRANSLATORS: content rating description */ + _("Overtly sexualized human characters") }, + { "violence-worship", MCT_APP_FILTER_OARS_VALUE_NONE, + /* TRANSLATORS: content rating description */ + _("No references to desecration") }, + { "violence-worship", MCT_APP_FILTER_OARS_VALUE_MILD, + /* TRANSLATORS: content rating description */ + _("Depictions or references to historical desecration") }, + { "violence-worship", MCT_APP_FILTER_OARS_VALUE_MODERATE, + /* TRANSLATORS: content rating description */ + _("Depictions of modern-day human desecration") }, + { "violence-worship", MCT_APP_FILTER_OARS_VALUE_INTENSE, + /* TRANSLATORS: content rating description */ + _("Graphic depictions of modern-day desecration") }, + { "violence-desecration", MCT_APP_FILTER_OARS_VALUE_NONE, + /* TRANSLATORS: content rating description */ + _("No visible dead human remains") }, + { "violence-desecration", MCT_APP_FILTER_OARS_VALUE_MILD, + /* TRANSLATORS: content rating description */ + _("Visible dead human remains") }, + { "violence-desecration", MCT_APP_FILTER_OARS_VALUE_MODERATE, + /* TRANSLATORS: content rating description */ + _("Dead human remains that are exposed to the elements") }, + { "violence-desecration", MCT_APP_FILTER_OARS_VALUE_INTENSE, + /* TRANSLATORS: content rating description */ + _("Graphic depictions of desecration of human bodies") }, + { "violence-slavery", MCT_APP_FILTER_OARS_VALUE_NONE, + /* TRANSLATORS: content rating description */ + _("No references to slavery") }, + { "violence-slavery", MCT_APP_FILTER_OARS_VALUE_MILD, + /* TRANSLATORS: content rating description */ + _("Depictions or references to historical slavery") }, + { "violence-slavery", MCT_APP_FILTER_OARS_VALUE_MODERATE, + /* TRANSLATORS: content rating description */ + _("Depictions of modern-day slavery") }, + { "violence-slavery", MCT_APP_FILTER_OARS_VALUE_INTENSE, + /* TRANSLATORS: content rating description */ + _("Graphic depictions of modern-day slavery") }, + { NULL, 0, NULL } }; + for (i = 0; tab[i].id != NULL; i++) { + if (g_strcmp0 (tab[i].id, id) == 0 && tab[i].value == value) + return tab[i].desc; + } + return NULL; +} + +/* data obtained from https://en.wikipedia.org/wiki/Video_game_rating_system */ +const gchar * +gs_utils_content_rating_age_to_str (GsContentRatingSystem system, guint age) +{ + if (system == GS_CONTENT_RATING_SYSTEM_INCAA) { + if (age >= 18) + return "+18"; + if (age >= 13) + return "+13"; + return "ATP"; + } + if (system == GS_CONTENT_RATING_SYSTEM_ACB) { + if (age >= 18) + return "R18+"; + if (age >= 15) + return "MA15+"; + return "PG"; + } + if (system == GS_CONTENT_RATING_SYSTEM_DJCTQ) { + if (age >= 18) + return "18"; + if (age >= 16) + return "16"; + if (age >= 14) + return "14"; + if (age >= 12) + return "12"; + if (age >= 10) + return "10"; + return "L"; + } + if (system == GS_CONTENT_RATING_SYSTEM_GSRR) { + if (age >= 18) + return "限制"; + if (age >= 15) + return "輔15"; + if (age >= 12) + return "輔12"; + if (age >= 6) + return "保護"; + return "普通"; + } + if (system == GS_CONTENT_RATING_SYSTEM_PEGI) { + if (age >= 18) + return "18"; + if (age >= 16) + return "16"; + if (age >= 12) + return "12"; + if (age >= 7) + return "7"; + if (age >= 3) + return "3"; + return NULL; + } + if (system == GS_CONTENT_RATING_SYSTEM_KAVI) { + if (age >= 18) + return "18+"; + if (age >= 16) + return "16+"; + if (age >= 12) + return "12+"; + if (age >= 7) + return "7+"; + if (age >= 3) + return "3+"; + return NULL; + } + if (system == GS_CONTENT_RATING_SYSTEM_USK) { + if (age >= 18) + return "18"; + if (age >= 16) + return "16"; + if (age >= 12) + return "12"; + if (age >= 6) + return "6"; + return "0"; + } + /* Reference: http://www.esra.org.ir/ */ + if (system == GS_CONTENT_RATING_SYSTEM_ESRA) { + if (age >= 18) + return "+18"; + if (age >= 15) + return "+15"; + if (age >= 12) + return "+12"; + if (age >= 7) + return "+7"; + if (age >= 3) + return "+3"; + return NULL; + } + if (system == GS_CONTENT_RATING_SYSTEM_CERO) { + if (age >= 18) + return "Z"; + if (age >= 17) + return "D"; + if (age >= 15) + return "C"; + if (age >= 12) + return "B"; + return "A"; + } + if (system == GS_CONTENT_RATING_SYSTEM_OFLCNZ) { + if (age >= 18) + return "R18"; + if (age >= 16) + return "R16"; + if (age >= 15) + return "R15"; + if (age >= 13) + return "R13"; + return "G"; + } + if (system == GS_CONTENT_RATING_SYSTEM_RUSSIA) { + if (age >= 18) + return "18+"; + if (age >= 16) + return "16+"; + if (age >= 12) + return "12+"; + if (age >= 6) + return "6+"; + return "0+"; + } + if (system == GS_CONTENT_RATING_SYSTEM_MDA) { + if (age >= 18) + return "M18"; + if (age >= 16) + return "ADV"; + return "General"; + } + if (system == GS_CONTENT_RATING_SYSTEM_GRAC) { + if (age >= 18) + return "18"; + if (age >= 15) + return "15"; + if (age >= 12) + return "12"; + return "ALL"; + } + if (system == GS_CONTENT_RATING_SYSTEM_ESRB) { + if (age >= 18) + return "Adults Only"; + if (age >= 17) + return "Mature"; + if (age >= 13) + return "Teen"; + if (age >= 10) + return "Everyone 10+"; + if (age >= 6) + return "Everyone"; + return "Early Childhood"; + } + /* IARC = everything else */ + if (age >= 18) + return "18+"; + if (age >= 16) + return "16+"; + if (age >= 12) + return "12+"; + if (age >= 7) + return "7+"; + if (age >= 3) + return "3+"; + return NULL; +} + +/* + * parse_locale: + * @locale: (transfer full): a locale to parse + * @language_out: (out) (optional) (nullable): return location for the parsed + * language, or %NULL to ignore + * @territory_out: (out) (optional) (nullable): return location for the parsed + * territory, or %NULL to ignore + * @codeset_out: (out) (optional) (nullable): return location for the parsed + * codeset, or %NULL to ignore + * @modifier_out: (out) (optional) (nullable): return location for the parsed + * modifier, or %NULL to ignore + * + * Parse @locale as a locale string of the form + * `language[_territory][.codeset][@modifier]` — see `man 3 setlocale` for + * details. + * + * On success, %TRUE will be returned, and the components of the locale will be + * returned in the given addresses, with each component not including any + * separators. Otherwise, %FALSE will be returned and the components will be set + * to %NULL. + * + * @locale is modified, and any returned non-%NULL pointers will point inside + * it. + * + * Returns: %TRUE on success, %FALSE otherwise + */ +static gboolean +parse_locale (gchar *locale /* (transfer full) */, + const gchar **language_out, + const gchar **territory_out, + const gchar **codeset_out, + const gchar **modifier_out) +{ + gchar *separator; + const gchar *language = NULL, *territory = NULL, *codeset = NULL, *modifier = NULL; + + separator = strrchr (locale, '@'); + if (separator != NULL) { + modifier = separator + 1; + *separator = '\0'; + } + + separator = strrchr (locale, '.'); + if (separator != NULL) { + codeset = separator + 1; + *separator = '\0'; + } + + separator = strrchr (locale, '_'); + if (separator != NULL) { + territory = separator + 1; + *separator = '\0'; + } + + language = locale; + + /* Parse failure? */ + if (*language == '\0') { + language = NULL; + territory = NULL; + codeset = NULL; + modifier = NULL; + } + + if (language_out != NULL) + *language_out = language; + if (territory_out != NULL) + *territory_out = territory; + if (codeset_out != NULL) + *codeset_out = codeset; + if (modifier_out != NULL) + *modifier_out = modifier; + + return (language != NULL); +} + +/* data obtained from https://en.wikipedia.org/wiki/Video_game_rating_system */ +GsContentRatingSystem +gs_utils_content_rating_system_from_locale (const gchar *locale) +{ + g_autofree gchar *locale_copy = g_strdup (locale); + const gchar *language, *territory; + + /* Default to IARC for locales which can’t be parsed. */ + if (!parse_locale (locale_copy, &language, &territory, NULL, NULL)) + return GS_CONTENT_RATING_SYSTEM_IARC; + + /* Argentina */ + if (g_strcmp0 (language, "ar") == 0) + return GS_CONTENT_RATING_SYSTEM_INCAA; + + /* Australia */ + if (g_strcmp0 (language, "au") == 0) + return GS_CONTENT_RATING_SYSTEM_ACB; + + /* Brazil */ + if (g_strcmp0 (language, "pt") == 0 && + g_strcmp0 (territory, "BR") == 0) + return GS_CONTENT_RATING_SYSTEM_DJCTQ; + + /* Taiwan */ + if (g_strcmp0 (language, "zh") == 0 && + g_strcmp0 (territory, "TW") == 0) + return GS_CONTENT_RATING_SYSTEM_GSRR; + + /* Europe (but not Finland or Germany), India, Israel, + * Pakistan, Quebec, South Africa */ + if ((g_strcmp0 (language, "en") == 0 && + g_strcmp0 (territory, "GB") == 0) || + g_strcmp0 (language, "gb") == 0 || + g_strcmp0 (language, "al") == 0 || + g_strcmp0 (language, "ad") == 0 || + g_strcmp0 (language, "am") == 0 || + g_strcmp0 (language, "at") == 0 || + g_strcmp0 (language, "az") == 0 || + g_strcmp0 (language, "by") == 0 || + g_strcmp0 (language, "be") == 0 || + g_strcmp0 (language, "ba") == 0 || + g_strcmp0 (language, "bg") == 0 || + g_strcmp0 (language, "hr") == 0 || + g_strcmp0 (language, "cy") == 0 || + g_strcmp0 (language, "cz") == 0 || + g_strcmp0 (language, "dk") == 0 || + g_strcmp0 (language, "ee") == 0 || + g_strcmp0 (language, "fr") == 0 || + g_strcmp0 (language, "ge") == 0 || + g_strcmp0 (language, "gr") == 0 || + g_strcmp0 (language, "hu") == 0 || + g_strcmp0 (language, "is") == 0 || + g_strcmp0 (language, "it") == 0 || + g_strcmp0 (language, "kz") == 0 || + g_strcmp0 (language, "xk") == 0 || + g_strcmp0 (language, "lv") == 0 || + g_strcmp0 (language, "fl") == 0 || + g_strcmp0 (language, "lu") == 0 || + g_strcmp0 (language, "lt") == 0 || + g_strcmp0 (language, "mk") == 0 || + g_strcmp0 (language, "mt") == 0 || + g_strcmp0 (language, "md") == 0 || + g_strcmp0 (language, "mc") == 0 || + g_strcmp0 (language, "me") == 0 || + g_strcmp0 (language, "nl") == 0 || + g_strcmp0 (language, "no") == 0 || + g_strcmp0 (language, "pl") == 0 || + g_strcmp0 (language, "pt") == 0 || + g_strcmp0 (language, "ro") == 0 || + g_strcmp0 (language, "sm") == 0 || + g_strcmp0 (language, "rs") == 0 || + g_strcmp0 (language, "sk") == 0 || + g_strcmp0 (language, "si") == 0 || + g_strcmp0 (language, "es") == 0 || + g_strcmp0 (language, "se") == 0 || + g_strcmp0 (language, "ch") == 0 || + g_strcmp0 (language, "tr") == 0 || + g_strcmp0 (language, "ua") == 0 || + g_strcmp0 (language, "va") == 0 || + g_strcmp0 (language, "in") == 0 || + g_strcmp0 (language, "il") == 0 || + g_strcmp0 (language, "pk") == 0 || + g_strcmp0 (language, "za") == 0) + return GS_CONTENT_RATING_SYSTEM_PEGI; + + /* Finland */ + if (g_strcmp0 (language, "fi") == 0) + return GS_CONTENT_RATING_SYSTEM_KAVI; + + /* Germany */ + if (g_strcmp0 (language, "de") == 0) + return GS_CONTENT_RATING_SYSTEM_USK; + + /* Iran */ + if (g_strcmp0 (language, "ir") == 0) + return GS_CONTENT_RATING_SYSTEM_ESRA; + + /* Japan */ + if (g_strcmp0 (language, "jp") == 0) + return GS_CONTENT_RATING_SYSTEM_CERO; + + /* New Zealand */ + if (g_strcmp0 (language, "nz") == 0) + return GS_CONTENT_RATING_SYSTEM_OFLCNZ; + + /* Russia: Content rating law */ + if (g_strcmp0 (language, "ru") == 0) + return GS_CONTENT_RATING_SYSTEM_RUSSIA; + + /* Singapore */ + if (g_strcmp0 (language, "sg") == 0) + return GS_CONTENT_RATING_SYSTEM_MDA; + + /* South Korea */ + if (g_strcmp0 (language, "kr") == 0) + return GS_CONTENT_RATING_SYSTEM_GRAC; + + /* USA, Canada, Mexico */ + if ((g_strcmp0 (language, "en") == 0 && + g_strcmp0 (territory, "US") == 0) || + g_strcmp0 (language, "us") == 0 || + g_strcmp0 (language, "ca") == 0 || + g_strcmp0 (language, "mx") == 0) + return GS_CONTENT_RATING_SYSTEM_ESRB; + + /* everything else is IARC */ + return GS_CONTENT_RATING_SYSTEM_IARC; +} + +static const gchar *content_rating_strings[GS_CONTENT_RATING_SYSTEM_LAST][7] = { + { "3+", "7+", "12+", "16+", "18+", NULL }, /* GS_CONTENT_RATING_SYSTEM_UNKNOWN */ + { "ATP", "+13", "+18", NULL }, /* GS_CONTENT_RATING_SYSTEM_INCAA */ + { "PG", "MA15+", "R18+", NULL }, /* GS_CONTENT_RATING_SYSTEM_ACB */ + { "L", "10", "12", "14", "16", "18", NULL }, /* GS_CONTENT_RATING_SYSTEM_DJCTQ */ + { "普通", "保護", "輔12", "輔15", "限制", NULL }, /* GS_CONTENT_RATING_SYSTEM_GSRR */ + { "3", "7", "12", "16", "18", NULL }, /* GS_CONTENT_RATING_SYSTEM_PEGI */ + { "3+", "7+", "12+", "16+", "18+", NULL }, /* GS_CONTENT_RATING_SYSTEM_KAVI */ + { "0", "6", "12", "16", "18", NULL}, /* GS_CONTENT_RATING_SYSTEM_USK */ + { "+3", "+7", "+12", "+15", "+18", NULL }, /* GS_CONTENT_RATING_SYSTEM_ESRA */ + { "A", "B", "C", "D", "Z", NULL }, /* GS_CONTENT_RATING_SYSTEM_CERO */ + { "G", "R13", "R15", "R16", "R18", NULL }, /* GS_CONTENT_RATING_SYSTEM_OFLCNZ */ + { "0+", "6+", "12+", "16+", "18+", NULL }, /* GS_CONTENT_RATING_SYSTEM_RUSSIA */ + { "General", "ADV", "M18", NULL }, /* GS_CONTENT_RATING_SYSTEM_MDA */ + { "ALL", "12", "15", "18", NULL }, /* GS_CONTENT_RATING_SYSTEM_GRAC */ + { "Early Childhood", "Everyone", "Everyone 10+", "Teen", "Mature", "Adults Only", NULL }, /* GS_CONTENT_RATING_SYSTEM_ESRB */ + { "3+", "7+", "12+", "16+", "18+", NULL }, /* GS_CONTENT_RATING_SYSTEM_IARC */ +}; + +const gchar * const * +gs_utils_content_rating_get_values (GsContentRatingSystem system) +{ + g_assert (system < GS_CONTENT_RATING_SYSTEM_LAST); + return content_rating_strings[system]; +} + +static guint content_rating_ages[GS_CONTENT_RATING_SYSTEM_LAST][7] = { + { 3, 7, 12, 16, 18 }, /* GS_CONTENT_RATING_SYSTEM_UNKNOWN */ + { 0, 13, 18 }, /* GS_CONTENT_RATING_SYSTEM_INCAA */ + { 0, 15, 18 }, /* GS_CONTENT_RATING_SYSTEM_ACB */ + { 0, 10, 12, 14, 16, 18 }, /* GS_CONTENT_RATING_SYSTEM_DJCTQ */ + { 0, 6, 12, 15, 18 }, /* GS_CONTENT_RATING_SYSTEM_GSRR */ + { 3, 7, 12, 16, 18 }, /* GS_CONTENT_RATING_SYSTEM_PEGI */ + { 3, 7, 12, 16, 18 }, /* GS_CONTENT_RATING_SYSTEM_KAVI */ + { 0, 6, 12, 16, 18 }, /* GS_CONTENT_RATING_SYSTEM_USK */ + { 3, 7, 12, 15, 18 }, /* GS_CONTENT_RATING_SYSTEM_ESRA */ + { 0, 12, 15, 17, 18 }, /* GS_CONTENT_RATING_SYSTEM_CERO */ + { 0, 13, 15, 16, 18 }, /* GS_CONTENT_RATING_SYSTEM_OFLCNZ */ + { 0, 6, 12, 16, 18 }, /* GS_CONTENT_RATING_SYSTEM_RUSSIA */ + { 0, 16, 18 }, /* GS_CONTENT_RATING_SYSTEM_MDA */ + { 0, 12, 15, 18 }, /* GS_CONTENT_RATING_SYSTEM_GRAC */ + { 0, 6, 10, 13, 17, 18 }, /* GS_CONTENT_RATING_SYSTEM_ESRB */ + { 3, 7, 12, 16, 18 }, /* GS_CONTENT_RATING_SYSTEM_IARC */ +}; + +const guint * +gs_utils_content_rating_get_ages (GsContentRatingSystem system) +{ + g_assert (system < GS_CONTENT_RATING_SYSTEM_LAST); + return content_rating_ages[system]; +} + +const struct { + const gchar *id; + MctAppFilterOarsValue value; + guint csm_age; +} id_to_csm_age[] = { +/* v1.0 */ +{ "violence-cartoon", MCT_APP_FILTER_OARS_VALUE_NONE, 0 }, +{ "violence-cartoon", MCT_APP_FILTER_OARS_VALUE_MILD, 3 }, +{ "violence-cartoon", MCT_APP_FILTER_OARS_VALUE_MODERATE, 4 }, +{ "violence-cartoon", MCT_APP_FILTER_OARS_VALUE_INTENSE, 6 }, +{ "violence-fantasy", MCT_APP_FILTER_OARS_VALUE_NONE, 0 }, +{ "violence-fantasy", MCT_APP_FILTER_OARS_VALUE_MILD, 3 }, +{ "violence-fantasy", MCT_APP_FILTER_OARS_VALUE_MODERATE, 7 }, +{ "violence-fantasy", MCT_APP_FILTER_OARS_VALUE_INTENSE, 8 }, +{ "violence-realistic", MCT_APP_FILTER_OARS_VALUE_NONE, 0 }, +{ "violence-realistic", MCT_APP_FILTER_OARS_VALUE_MILD, 4 }, +{ "violence-realistic", MCT_APP_FILTER_OARS_VALUE_MODERATE, 9 }, +{ "violence-realistic", MCT_APP_FILTER_OARS_VALUE_INTENSE, 14 }, +{ "violence-bloodshed", MCT_APP_FILTER_OARS_VALUE_NONE, 0 }, +{ "violence-bloodshed", MCT_APP_FILTER_OARS_VALUE_MILD, 9 }, +{ "violence-bloodshed", MCT_APP_FILTER_OARS_VALUE_MODERATE, 11 }, +{ "violence-bloodshed", MCT_APP_FILTER_OARS_VALUE_INTENSE, 18 }, +{ "violence-sexual", MCT_APP_FILTER_OARS_VALUE_NONE, 0 }, +{ "violence-sexual", MCT_APP_FILTER_OARS_VALUE_INTENSE, 18 }, +{ "drugs-alcohol", MCT_APP_FILTER_OARS_VALUE_NONE, 0 }, +{ "drugs-alcohol", MCT_APP_FILTER_OARS_VALUE_MILD, 11 }, +{ "drugs-alcohol", MCT_APP_FILTER_OARS_VALUE_MODERATE, 13 }, +{ "drugs-narcotics", MCT_APP_FILTER_OARS_VALUE_NONE, 0 }, +{ "drugs-narcotics", MCT_APP_FILTER_OARS_VALUE_MILD, 12 }, +{ "drugs-narcotics", MCT_APP_FILTER_OARS_VALUE_MODERATE, 14 }, +{ "drugs-tobacco", MCT_APP_FILTER_OARS_VALUE_NONE, 0 }, +{ "drugs-tobacco", MCT_APP_FILTER_OARS_VALUE_MILD, 10 }, +{ "drugs-tobacco", MCT_APP_FILTER_OARS_VALUE_MODERATE, 13 }, +{ "sex-nudity", MCT_APP_FILTER_OARS_VALUE_NONE, 0 }, +{ "sex-nudity", MCT_APP_FILTER_OARS_VALUE_MILD, 12 }, +{ "sex-nudity", MCT_APP_FILTER_OARS_VALUE_MODERATE, 14 }, +{ "sex-themes", MCT_APP_FILTER_OARS_VALUE_NONE, 0 }, +{ "sex-themes", MCT_APP_FILTER_OARS_VALUE_MILD, 13 }, +{ "sex-themes", MCT_APP_FILTER_OARS_VALUE_MODERATE, 14 }, +{ "sex-themes", MCT_APP_FILTER_OARS_VALUE_INTENSE, 15 }, +{ "language-profanity", MCT_APP_FILTER_OARS_VALUE_NONE, 0 }, +{ "language-profanity", MCT_APP_FILTER_OARS_VALUE_MILD, 8 }, +{ "language-profanity", MCT_APP_FILTER_OARS_VALUE_MODERATE, 11 }, +{ "language-profanity", MCT_APP_FILTER_OARS_VALUE_INTENSE, 14 }, +{ "language-humor", MCT_APP_FILTER_OARS_VALUE_NONE, 0 }, +{ "language-humor", MCT_APP_FILTER_OARS_VALUE_MILD, 3 }, +{ "language-humor", MCT_APP_FILTER_OARS_VALUE_MODERATE, 8 }, +{ "language-humor", MCT_APP_FILTER_OARS_VALUE_INTENSE, 14 }, +{ "language-discrimination", MCT_APP_FILTER_OARS_VALUE_NONE, 0 }, +{ "language-discrimination", MCT_APP_FILTER_OARS_VALUE_MILD, 9 }, +{ "language-discrimination", MCT_APP_FILTER_OARS_VALUE_MODERATE,10 }, +{ "language-discrimination", MCT_APP_FILTER_OARS_VALUE_INTENSE, 11 }, +{ "money-advertising", MCT_APP_FILTER_OARS_VALUE_NONE, 0 }, +{ "money-advertising", MCT_APP_FILTER_OARS_VALUE_MILD, 7 }, +{ "money-advertising", MCT_APP_FILTER_OARS_VALUE_MODERATE, 8 }, +{ "money-advertising", MCT_APP_FILTER_OARS_VALUE_INTENSE, 10 }, +{ "money-gambling", MCT_APP_FILTER_OARS_VALUE_NONE, 0 }, +{ "money-gambling", MCT_APP_FILTER_OARS_VALUE_MILD, 7 }, +{ "money-gambling", MCT_APP_FILTER_OARS_VALUE_MODERATE, 10 }, +{ "money-gambling", MCT_APP_FILTER_OARS_VALUE_INTENSE, 18 }, +{ "money-purchasing", MCT_APP_FILTER_OARS_VALUE_NONE, 0 }, +{ "money-purchasing", MCT_APP_FILTER_OARS_VALUE_INTENSE, 15 }, +{ "social-chat", MCT_APP_FILTER_OARS_VALUE_NONE, 0 }, +{ "social-chat", MCT_APP_FILTER_OARS_VALUE_MILD, 4 }, +{ "social-chat", MCT_APP_FILTER_OARS_VALUE_MODERATE, 10 }, +{ "social-chat", MCT_APP_FILTER_OARS_VALUE_INTENSE, 13 }, +{ "social-audio", MCT_APP_FILTER_OARS_VALUE_NONE, 0 }, +{ "social-audio", MCT_APP_FILTER_OARS_VALUE_INTENSE, 15 }, +{ "social-contacts", MCT_APP_FILTER_OARS_VALUE_NONE, 0 }, +{ "social-contacts", MCT_APP_FILTER_OARS_VALUE_INTENSE, 12 }, +{ "social-info", MCT_APP_FILTER_OARS_VALUE_NONE, 0 }, +{ "social-info", MCT_APP_FILTER_OARS_VALUE_INTENSE, 13 }, +{ "social-location", MCT_APP_FILTER_OARS_VALUE_NONE, 0 }, +{ "social-location", MCT_APP_FILTER_OARS_VALUE_INTENSE, 13 }, +/* v1.1 additions */ +{ "social-info", MCT_APP_FILTER_OARS_VALUE_MILD, 0 }, +{ "social-info", MCT_APP_FILTER_OARS_VALUE_MODERATE, 13 }, +{ "money-purchasing", MCT_APP_FILTER_OARS_VALUE_MILD, 12 }, +{ "social-chat", MCT_APP_FILTER_OARS_VALUE_MODERATE, 14 }, +{ "sex-homosexuality", MCT_APP_FILTER_OARS_VALUE_NONE, 0 }, +{ "sex-homosexuality", MCT_APP_FILTER_OARS_VALUE_MILD, 10 }, +{ "sex-homosexuality", MCT_APP_FILTER_OARS_VALUE_MODERATE, 13 }, +{ "sex-homosexuality", MCT_APP_FILTER_OARS_VALUE_INTENSE, 18 }, +{ "sex-prostitution", MCT_APP_FILTER_OARS_VALUE_NONE, 0 }, +{ "sex-prostitution", MCT_APP_FILTER_OARS_VALUE_MILD, 12 }, +{ "sex-prostitution", MCT_APP_FILTER_OARS_VALUE_MODERATE, 14 }, +{ "sex-prostitution", MCT_APP_FILTER_OARS_VALUE_INTENSE, 18 }, +{ "sex-adultery", MCT_APP_FILTER_OARS_VALUE_NONE, 0 }, +{ "sex-adultery", MCT_APP_FILTER_OARS_VALUE_MILD, 8 }, +{ "sex-adultery", MCT_APP_FILTER_OARS_VALUE_MODERATE, 10 }, +{ "sex-adultery", MCT_APP_FILTER_OARS_VALUE_INTENSE, 18 }, +{ "sex-appearance", MCT_APP_FILTER_OARS_VALUE_NONE, 0 }, +{ "sex-appearance", MCT_APP_FILTER_OARS_VALUE_MODERATE, 10 }, +{ "sex-appearance", MCT_APP_FILTER_OARS_VALUE_INTENSE, 15 }, +{ "violence-worship", MCT_APP_FILTER_OARS_VALUE_NONE, 0 }, +{ "violence-worship", MCT_APP_FILTER_OARS_VALUE_MILD, 13 }, +{ "violence-worship", MCT_APP_FILTER_OARS_VALUE_MODERATE, 15 }, +{ "violence-worship", MCT_APP_FILTER_OARS_VALUE_INTENSE, 18 }, +{ "violence-desecration", MCT_APP_FILTER_OARS_VALUE_NONE, 0 }, +{ "violence-desecration", MCT_APP_FILTER_OARS_VALUE_MILD, 13 }, +{ "violence-desecration", MCT_APP_FILTER_OARS_VALUE_MODERATE, 15 }, +{ "violence-desecration", MCT_APP_FILTER_OARS_VALUE_INTENSE, 18 }, +{ "violence-slavery", MCT_APP_FILTER_OARS_VALUE_NONE, 0 }, +{ "violence-slavery", MCT_APP_FILTER_OARS_VALUE_MILD, 13 }, +{ "violence-slavery", MCT_APP_FILTER_OARS_VALUE_MODERATE, 15 }, +{ "violence-slavery", MCT_APP_FILTER_OARS_VALUE_INTENSE, 18 }, + +/* EOS customisation to add at least one CSM ↔ OARS mapping for ages 16 and 17, + * as these are used in many locale-specific ratings systems. Without them, + * mapping (e.g.) OFLCNZ R16 → CSM 16 → OARS → CSM gives CSM 15, which then maps + * back to OFLCNZ R15, which is not what we want. The addition of these two + * mappings should not expose younger users to content they would not have seen + * with the default upstream mappings; it instead slightly raises the age at + * which users are allowed to see intense content in these two categories. + * + * See https://phabricator.endlessm.com/T23897#666769. */ +{ "drugs-alcohol", MCT_APP_FILTER_OARS_VALUE_INTENSE, 16 }, +{ "drugs-narcotics", MCT_APP_FILTER_OARS_VALUE_INTENSE, 17 }, +{ NULL, 0, 0 } }; + +/** + * as_content_rating_id_value_to_csm_age: + * @id: the subsection ID e.g. "violence-cartoon" + * @value: the #AsContentRatingValue, e.g. %MCT_APP_FILTER_OARS_VALUE_INTENSE + * + * Gets the Common Sense Media approved age for a specific rating level. + * + * Returns: The age in years, or 0 for no details. + * + * Since: 0.5.12 + **/ +guint +as_content_rating_id_value_to_csm_age (const gchar *id, MctAppFilterOarsValue value) +{ + guint i; + for (i = 0; id_to_csm_age[i].id != NULL; i++) { + if (value == id_to_csm_age[i].value && + g_strcmp0 (id, id_to_csm_age[i].id) == 0) + return id_to_csm_age[i].csm_age; + } + return 0; +} + +/** + * as_content_rating_id_csm_age_to_value: + * @id: the subsection ID e.g. "violence-cartoon" + * @age: the age + * + * Gets the #MctAppFilterOarsValue for a given age. + * + * Returns: the #MctAppFilterOarsValue + **/ +MctAppFilterOarsValue +as_content_rating_id_csm_age_to_value (const gchar *id, guint age) +{ + MctAppFilterOarsValue value; + guint i; + + value = MCT_APP_FILTER_OARS_VALUE_UNKNOWN; + + for (i = 0; id_to_csm_age[i].id != NULL; i++) { + if (age >= id_to_csm_age[i].csm_age && + g_strcmp0 (id, id_to_csm_age[i].id) == 0) + value = MAX (value, id_to_csm_age[i].value); + } + return value; +} diff --git a/malcontent-control/gs-content-rating.h b/malcontent-control/gs-content-rating.h new file mode 100644 index 0000000..6a111ae --- /dev/null +++ b/malcontent-control/gs-content-rating.h @@ -0,0 +1,61 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * + * Copyright (C) 2015-2016 Richard Hughes + * + * Licensed under the GNU General Public License Version 2 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +G_BEGIN_DECLS + +#include +#include + +typedef enum { + GS_CONTENT_RATING_SYSTEM_UNKNOWN, + GS_CONTENT_RATING_SYSTEM_INCAA, + GS_CONTENT_RATING_SYSTEM_ACB, + GS_CONTENT_RATING_SYSTEM_DJCTQ, + GS_CONTENT_RATING_SYSTEM_GSRR, + GS_CONTENT_RATING_SYSTEM_PEGI, + GS_CONTENT_RATING_SYSTEM_KAVI, + GS_CONTENT_RATING_SYSTEM_USK, + GS_CONTENT_RATING_SYSTEM_ESRA, + GS_CONTENT_RATING_SYSTEM_CERO, + GS_CONTENT_RATING_SYSTEM_OFLCNZ, + GS_CONTENT_RATING_SYSTEM_RUSSIA, + GS_CONTENT_RATING_SYSTEM_MDA, + GS_CONTENT_RATING_SYSTEM_GRAC, + GS_CONTENT_RATING_SYSTEM_ESRB, + GS_CONTENT_RATING_SYSTEM_IARC, + /*< private >*/ + GS_CONTENT_RATING_SYSTEM_LAST +} GsContentRatingSystem; + +const gchar *gs_utils_content_rating_age_to_str (GsContentRatingSystem system, + guint age); +GsContentRatingSystem gs_utils_content_rating_system_from_locale (const gchar *locale); +const gchar *gs_content_rating_key_value_to_str (const gchar *id, + MctAppFilterOarsValue value); +const gchar *gs_content_rating_system_to_str (GsContentRatingSystem system); +const gchar * const *gs_utils_content_rating_get_values (GsContentRatingSystem system); +const guint *gs_utils_content_rating_get_ages (GsContentRatingSystem system); +guint as_content_rating_id_value_to_csm_age (const gchar *id, MctAppFilterOarsValue value); +MctAppFilterOarsValue as_content_rating_id_csm_age_to_value (const gchar *id, guint age); + +G_END_DECLS diff --git a/malcontent-control/main.c b/malcontent-control/main.c new file mode 100644 index 0000000..cce6f70 --- /dev/null +++ b/malcontent-control/main.c @@ -0,0 +1,37 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright © 2019 Endless Mobile, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * + * Authors: + * - Philip Withnall + */ + +#include +#include +#include + +#include "application.h" + + +int +main (int argc, + char *argv[]) +{ + g_autoptr(MctApplication) app = NULL; + + app = mct_application_new (); + return g_application_run (G_APPLICATION (app), argc, argv); +} diff --git a/malcontent-control/main.ui b/malcontent-control/main.ui new file mode 100644 index 0000000..6d3d026 --- /dev/null +++ b/malcontent-control/main.ui @@ -0,0 +1,118 @@ + + + + + + 500 + 700 + + + True + + + True + vertical + 0 + + + True + user_manager + + + + + + False + False + + + + + True + never + 450 + + + True + 12 + + + + + True + + + + + controls + + + + + + True + vertical + True + True + + + True + Loading… + + + + + + static + + + + + + + loading + + + + + + True + vertical + True + True + + + True + vertical + 12 + + + True + + + + + + + + + True + + + + + + + + alert + + + + + error + + + + + + diff --git a/malcontent-control/malcontent-control.gresource.xml b/malcontent-control/malcontent-control.gresource.xml new file mode 100644 index 0000000..516a033 --- /dev/null +++ b/malcontent-control/malcontent-control.gresource.xml @@ -0,0 +1,11 @@ + + + + + carousel.css + carousel.ui + main.ui + user-controls.ui + user-selector.ui + + diff --git a/malcontent-control/meson.build b/malcontent-control/meson.build new file mode 100644 index 0000000..4e66a6e --- /dev/null +++ b/malcontent-control/meson.build @@ -0,0 +1,103 @@ +application_id = 'org.freedesktop.MalcontentControl' + +if not cc.has_function('atexit') + error('atexit() needed for generated GResource files') +endif + +resources = gnome.compile_resources( + 'resources', + 'malcontent-control.gresource.xml', + source_dir: meson.source_root(), +) + +malcontent_control = executable('malcontent-control', + [ + 'application.c', + 'application.h', + 'carousel.c', + 'carousel.h', + 'gs-content-rating.c', + 'gs-content-rating.h', + 'main.c', + 'user-controls.c', + 'user-controls.h', + 'user-image.c', + 'user-image.h', + 'user-selector.c', + 'user-selector.h', + ] + resources, + dependencies: [ + dependency('accountsservice'), + dependency('gio-2.0', version: '>= 2.44'), + dependency('glib-2.0', version: '>= 2.54.2'), + dependency('gobject-2.0', version: '>= 2.54'), + dependency('gtk+-3.0'), + dependency('flatpak'), + libmalcontent_dep, + ], + include_directories: root_inc, + install: true, +) + +desktop_file = i18n.merge_file('desktop-file', + type: 'desktop', + input: '@0@.desktop.in'.format(application_id), + output: '@0@.desktop'.format(application_id), + po_dir: join_paths(meson.source_root(), 'po'), + install: true, + install_dir: join_paths(get_option('datadir'), 'applications'), +) + +desktop_file_validate = find_program('desktop-file-validate', required: false) +if desktop_file_validate.found() + test( + 'validate-desktop', + desktop_file_validate, + args: [ + desktop_file.full_path(), + ], + suite: ['malcontent-control'], + ) +endif + +appdata_file = i18n.merge_file('appdata-file', + input: '@0@.appdata.xml.in'.format(application_id), + output: '@0@.appdata.xml'.format(application_id), + po_dir: join_paths(meson.source_root(), 'po'), + install: true, + install_dir: join_paths(get_option('datadir'), 'metainfo'), +) + +appstream_util = find_program('appstream-util', required: false) +if appstream_util.found() + test( + 'validate-appdata', appstream_util, + args: [ + 'validate-relax', '--nonet', appdata_file.full_path(), + ], + suite: ['malcontent-control'], + ) +endif + +xmllint = find_program('xmllint', required: false) +if xmllint.found() + gtk_prefix = dependency('gtk+-3.0').get_pkgconfig_variable('prefix') + test( + 'validate-ui', xmllint, + args: [ + '--nonet', '--noblanks', '--noout', + '--relaxng', join_paths(gtk_prefix, 'share', 'gtk-3.0', 'gtkbuilder.rng'), + files( + 'carousel.ui', + 'main.ui', + 'user-controls.ui', + 'user-selector.ui', + ), + ], + suite: ['malcontent-control'], + ) +endif + +# FIXME: Add icons and tests +#subdir('icons') +#subdir('tests') diff --git a/malcontent-control/org.freedesktop.MalcontentControl.appdata.xml.in b/malcontent-control/org.freedesktop.MalcontentControl.appdata.xml.in new file mode 100644 index 0000000..5c91ab2 --- /dev/null +++ b/malcontent-control/org.freedesktop.MalcontentControl.appdata.xml.in @@ -0,0 +1,56 @@ + + + + org.freedesktop.MalcontentControl + CC-BY-SA-3.0 + GPL-2.0+ + + + Parental Controls + + + Set parental controls and monitor usage by users + + + +

+ Manage users’ parental controls restrictions, controlling how long they + can use the computer for, what software they can install, and what + installed software they can run. +

+
+ + + malcontent-control + + org.freedesktop.MalcontentControl.desktop + https://gitlab.freedesktop.org/pwithnall/malcontent + https://gitlab.freedesktop.org/pwithnall/malcontent/issues + http://www.gnome.org/friends/ + https://wiki.gnome.org/TranslationProject/LocalisationGuide + philip_at_tecnocode.co.uk + GNOME + The GNOME Project + + AppMenu + HighContrast + ModernToolkit + + malcontent + + + +
    +
  • Maintenance release of underlying parental controls library
  • +
+
+
+
+ +
diff --git a/malcontent-control/org.freedesktop.MalcontentControl.desktop.in b/malcontent-control/org.freedesktop.MalcontentControl.desktop.in new file mode 100644 index 0000000..d36ad39 --- /dev/null +++ b/malcontent-control/org.freedesktop.MalcontentControl.desktop.in @@ -0,0 +1,12 @@ +[Desktop Entry] +Name=Parental Controls +Comment=Set parental controls and monitor usage by users +Exec=malcontent-control +# Translators: Do NOT translate or transliterate this text (this is an icon file name)! +Icon=org.freedesktop.MalcontentControl +Terminal=false +Type=Application +Categories=GTK;GNOME;System; +StartupNotify=true +# Translators: Search terms to find this application. Do NOT translate or localise the semicolons! The list MUST also end with a semicolon! +Keywords=parental controls;screen time;app restrictions;web browser restrictions;oars;usage;usage limit;kid;child; diff --git a/malcontent-control/user-controls.c b/malcontent-control/user-controls.c new file mode 100644 index 0000000..5cf6cae --- /dev/null +++ b/malcontent-control/user-controls.c @@ -0,0 +1,1178 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright © 2018, 2019, 2020 Endless Mobile, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * + * Authors: + * - Georges Basile Stavracas Neto + * - Philip Withnall + */ + +#include +#include +#include +#include +#include +#include + +#include "gs-content-rating.h" +#include "user-controls.h" + + +#define WEB_BROWSERS_CONTENT_TYPE "x-scheme-handler/http" + +/* The value which we store as an age to indicate that OARS filtering is disabled. */ +static const guint32 oars_disabled_age = (guint32) -1; + +struct _MctUserControls +{ + GtkGrid parent_instance; + + GMenu *age_menu; + GtkSwitch *allow_system_installation_switch; + GtkSwitch *allow_user_installation_switch; + GtkSwitch *allow_web_browsers_switch; + GtkListBox *listbox; + GtkButton *restriction_button; + GtkPopover *restriction_popover; + + FlatpakInstallation *system_installation; /* (owned) */ + FlatpakInstallation *user_installation; /* (owned) */ + + GSimpleActionGroup *action_group; /* (owned) */ + + ActUser *user; /* (owned) (nullable) */ + + GPermission *permission; /* (owned) (nullable) */ + gulong permission_allowed_id; + + GAppInfoMonitor *app_info_monitor; /* (owned) */ + + GHashTable *blacklisted_apps; /* (owned) */ + GListStore *apps; /* (owned) */ + + GCancellable *cancellable; /* (owned) */ + MctManager *manager; /* (owned) */ + MctAppFilter *filter; /* (owned) */ + guint selected_age; /* @oars_disabled_age to disable OARS */ + + guint blacklist_apps_source_id; +}; + +static gboolean blacklist_apps_cb (gpointer data); +static void app_info_changed_cb (GAppInfoMonitor *monitor, + gpointer user_data); + +static gint compare_app_info_cb (gconstpointer a, + gconstpointer b, + gpointer user_data); + +static void on_allow_installation_switch_active_changed_cb (GtkSwitch *s, + GParamSpec *pspec, + MctUserControls *self); + +static void on_allow_web_browsers_switch_active_changed_cb (GtkSwitch *s, + GParamSpec *pspec, + MctUserControls *self); + +static void on_set_age_action_activated (GSimpleAction *action, + GVariant *param, + gpointer user_data); + +static void on_permission_allowed_cb (GObject *obj, + GParamSpec *pspec, + gpointer user_data); + +G_DEFINE_TYPE (MctUserControls, mct_user_controls, GTK_TYPE_GRID) + +enum +{ + PROP_USER = 1, + PROP_PERMISSION, + N_PROPS +}; + +static GParamSpec *properties [N_PROPS]; + +static const GActionEntry actions[] = { + { "set-age", on_set_age_action_activated, "u", NULL, NULL, { 0, }} +}; + +/* FIXME: Factor this out and rely on code from libappstream-glib or gnome-software + * to do it. See: https://phabricator.endlessm.com/T24986 */ +static const gchar * const oars_categories[] = +{ + "violence-cartoon", + "violence-fantasy", + "violence-realistic", + "violence-bloodshed", + "violence-sexual", + "violence-desecration", + "violence-slavery", + "violence-worship", + "drugs-alcohol", + "drugs-narcotics", + "drugs-tobacco", + "sex-nudity", + "sex-themes", + "sex-homosexuality", + "sex-prostitution", + "sex-adultery", + "sex-appearance", + "language-profanity", + "language-humor", + "language-discrimination", + "social-chat", + "social-info", + "social-audio", + "social-location", + "social-contacts", + "money-purchasing", + "money-gambling", + NULL +}; + +/* Auxiliary methods */ + +static gint +app_compare_id_length_cb (gconstpointer a, + gconstpointer b) +{ + GAppInfo *info_a = (GAppInfo *) a, *info_b = (GAppInfo *) b; + const gchar *id_a, *id_b; + + id_a = g_app_info_get_id (info_a); + id_b = g_app_info_get_id (info_b); + + if (id_a == NULL && id_b == NULL) + return 0; + else if (id_a == NULL) + return -1; + else if (id_b == NULL) + return 1; + + return strlen (id_a) - strlen (id_b); +} + +static void +reload_apps (MctUserControls *self) +{ + GList *iter, *apps; + g_autoptr(GHashTable) seen_flatpak_ids = NULL; + g_autoptr(GHashTable) seen_executables = NULL; + + apps = g_app_info_get_all (); + + /* Sort the apps by increasing length of #GAppInfo ID. When coupled with the + * deduplication of flatpak IDs and executable paths, below, this should ensure that we + * pick the ‘base’ app out of any set with matching prefixes and identical app IDs (in + * case of flatpak apps) or executables (for non-flatpak apps), and show only that. + * + * This is designed to avoid listing all the components of LibreOffice for example, + * which all share an app ID and hence have the same entry in the parental controls + * app filter. */ + apps = g_list_sort (apps, app_compare_id_length_cb); + seen_flatpak_ids = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + seen_executables = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + + g_list_store_remove_all (self->apps); + + for (iter = apps; iter; iter = iter->next) + { + GAppInfo *app; + const gchar *app_name; + const gchar * const *supported_types; + + app = iter->data; + app_name = g_app_info_get_name (app); + + supported_types = g_app_info_get_supported_types (app); + + if (!G_IS_DESKTOP_APP_INFO (app) || + !g_app_info_should_show (app) || + app_name[0] == '\0' || + /* Endless' link apps have the "eos-link" prefix, and should be ignored too */ + g_str_has_prefix (g_app_info_get_id (app), "eos-link") || + /* FIXME: Only list flatpak apps and apps with X-Parental-Controls + * key set for now; we really need a system-wide MAC to be able to + * reliably support blacklisting system programs. See + * https://phabricator.endlessm.com/T25080. */ + (!g_desktop_app_info_has_key (G_DESKTOP_APP_INFO (app), "X-Flatpak") && + !g_desktop_app_info_has_key (G_DESKTOP_APP_INFO (app), "X-Parental-Controls")) || + /* Web browsers are special cased */ + (supported_types && g_strv_contains (supported_types, WEB_BROWSERS_CONTENT_TYPE))) + { + continue; + } + + if (g_desktop_app_info_has_key (G_DESKTOP_APP_INFO (app), "X-Flatpak")) + { + g_autofree gchar *flatpak_id = NULL; + + flatpak_id = g_desktop_app_info_get_string (G_DESKTOP_APP_INFO (app), "X-Flatpak"); + g_debug ("Processing app ‘%s’ (Exec=%s, X-Flatpak=%s)", + g_app_info_get_id (app), + g_app_info_get_executable (app), + flatpak_id); + + /* Have we seen this flatpak ID before? */ + if (!g_hash_table_add (seen_flatpak_ids, g_steal_pointer (&flatpak_id))) + { + g_debug (" → Skipping ‘%s’ due to seeing its flatpak ID already", + g_app_info_get_id (app)); + continue; + } + } + else if (g_desktop_app_info_has_key (G_DESKTOP_APP_INFO (app), "X-Parental-Controls")) + { + g_autofree gchar *parental_controls_type = NULL; + g_autofree gchar *executable = NULL; + + parental_controls_type = g_desktop_app_info_get_string (G_DESKTOP_APP_INFO (app), + "X-Parental-Controls"); + /* Ignore X-Parental-Controls=none */ + if (g_strcmp0 (parental_controls_type, "none") == 0) + continue; + + executable = g_strdup (g_app_info_get_executable (app)); + g_debug ("Processing app ‘%s’ (Exec=%s, X-Parental-Controls=%s)", + g_app_info_get_id (app), + executable, + parental_controls_type); + + /* Have we seen this executable before? */ + if (!g_hash_table_add (seen_executables, g_steal_pointer (&executable))) + { + g_debug (" → Skipping ‘%s’ due to seeing its executable already", + g_app_info_get_id (app)); + continue; + } + } + + g_list_store_insert_sorted (self->apps, + app, + compare_app_info_cb, + self); + } + + g_list_free_full (apps, g_object_unref); +} + +static void +app_info_changed_cb (GAppInfoMonitor *monitor, + gpointer user_data) +{ + MctUserControls *self = MCT_USER_CONTROLS (user_data); + + reload_apps (self); +} + +static GsContentRatingSystem +get_content_rating_system (ActUser *user) +{ + const gchar *user_language; + + user_language = act_user_get_language (user); + + return gs_utils_content_rating_system_from_locale (user_language); +} + +static void +schedule_update_blacklisted_apps (MctUserControls *self) +{ + if (self->blacklist_apps_source_id > 0) + return; + + /* Use a timeout to batch multiple quick changes into a single + * update. 1 second is an arbitrary sufficiently small number */ + self->blacklist_apps_source_id = g_timeout_add_seconds (1, blacklist_apps_cb, self); +} + +static void +flush_update_blacklisted_apps (MctUserControls *self) +{ + if (self->blacklist_apps_source_id > 0) + { + /* Remove the timer and forcefully call the timer callback. */ + g_source_remove (self->blacklist_apps_source_id); + self->blacklist_apps_source_id = 0; + + blacklist_apps_cb (self); + } +} + +static void +update_app_filter (MctUserControls *self) +{ + g_autoptr(GError) error = NULL; + + g_clear_pointer (&self->filter, mct_app_filter_unref); + + /* FIXME: make it asynchronous */ + self->filter = mct_manager_get_app_filter (self->manager, + act_user_get_uid (self->user), + MCT_MANAGER_GET_VALUE_FLAGS_NONE, + self->cancellable, + &error); + + if (error) + { + /* It's expected that a non-admin user can't read another user's parental + * controls info unless the panel has been unlocked; ignore such an + * error. + */ + if (act_user_get_uid (self->user) != getuid () && + self->permission != NULL && + !g_permission_get_allowed (self->permission) && + g_error_matches (error, MCT_MANAGER_ERROR, MCT_MANAGER_ERROR_PERMISSION_DENIED)) + { + g_clear_error (&error); + g_debug ("Not enough permissions to retrieve app filter for user '%s'", + act_user_get_user_name (self->user)); + } + else + { + g_warning ("Error retrieving app filter for user '%s': %s", + act_user_get_user_name (self->user), + error->message); + } + + return; + } + + g_debug ("Retrieved new app filter for user '%s'", act_user_get_user_name (self->user)); +} + +static void +update_categories_from_language (MctUserControls *self) +{ + GsContentRatingSystem rating_system; + const gchar * const * entries; + const gchar *rating_system_str; + const guint *ages; + gsize i; + g_autofree gchar *disabled_action = NULL; + + rating_system = get_content_rating_system (self->user); + rating_system_str = gs_content_rating_system_to_str (rating_system); + + g_debug ("Using rating system %s", rating_system_str); + + entries = gs_utils_content_rating_get_values (rating_system); + ages = gs_utils_content_rating_get_ages (rating_system); + + /* Fill in the age menu */ + g_menu_remove_all (self->age_menu); + + disabled_action = g_strdup_printf ("permissions.set-age(uint32 %u)", oars_disabled_age); + g_menu_append (self->age_menu, _("No Restriction"), disabled_action); + + for (i = 0; entries[i] != NULL; i++) + { + g_autofree gchar *action = g_strdup_printf ("permissions.set-age(uint32 %u)", ages[i]); + + /* Prevent the unlikely case that one of the real ages is the same as our + * special ‘disabled’ value. */ + g_assert (ages[i] != oars_disabled_age); + + g_menu_append (self->age_menu, entries[i], action); + } +} + +/* Returns a human-readable but untranslated string, not suitable + * to be shown in any UI */ +static const gchar * +oars_value_to_string (MctAppFilterOarsValue oars_value) +{ + switch (oars_value) + { + case MCT_APP_FILTER_OARS_VALUE_UNKNOWN: + return "unknown"; + case MCT_APP_FILTER_OARS_VALUE_NONE: + return "none"; + case MCT_APP_FILTER_OARS_VALUE_MILD: + return "mild"; + case MCT_APP_FILTER_OARS_VALUE_MODERATE: + return "moderate"; + case MCT_APP_FILTER_OARS_VALUE_INTENSE: + return "intense"; + default: + return ""; + } +} + +static void +update_oars_level (MctUserControls *self) +{ + GsContentRatingSystem rating_system; + const gchar *rating_age_category; + guint maximum_age; + gsize i; + gboolean all_categories_unset; + + g_assert (self->filter != NULL); + + maximum_age = 0; + all_categories_unset = TRUE; + + for (i = 0; oars_categories[i] != NULL; i++) + { + MctAppFilterOarsValue oars_value; + guint age; + + oars_value = mct_app_filter_get_oars_value (self->filter, oars_categories[i]); + all_categories_unset &= (oars_value == MCT_APP_FILTER_OARS_VALUE_UNKNOWN); + age = as_content_rating_id_value_to_csm_age (oars_categories[i], oars_value); + + g_debug ("OARS value for '%s': %s", oars_categories[i], oars_value_to_string (oars_value)); + + if (age > maximum_age) + maximum_age = age; + } + + g_debug ("Effective age for this user: %u; %s", maximum_age, + all_categories_unset ? "all categories unset" : "some categories set"); + + rating_system = get_content_rating_system (self->user); + rating_age_category = gs_utils_content_rating_age_to_str (rating_system, maximum_age); + + /* Unrestricted? */ + if (rating_age_category == NULL || all_categories_unset) + rating_age_category = _("No Restriction"); + + gtk_button_set_label (self->restriction_button, rating_age_category); +} + +static void +update_allow_app_installation (MctUserControls *self) +{ + gboolean allow_system_installation; + gboolean allow_user_installation; + gboolean non_admin_user = TRUE; + + if (act_user_get_account_type (self->user) == ACT_USER_ACCOUNT_TYPE_ADMINISTRATOR) + non_admin_user = FALSE; + + /* Admins are always allowed to install apps for all users. This behaviour is governed + * by flatpak polkit rules. Hence, these hide these defunct switches for admins. */ + gtk_widget_set_visible (GTK_WIDGET (self->allow_system_installation_switch), non_admin_user); + gtk_widget_set_visible (GTK_WIDGET (self->allow_user_installation_switch), non_admin_user); + + /* If user is admin, we are done here, bail out. */ + if (!non_admin_user) + { + g_debug ("User %s is administrator, hiding app installation controls", + act_user_get_user_name (self->user)); + return; + } + + allow_system_installation = mct_app_filter_is_system_installation_allowed (self->filter); + allow_user_installation = mct_app_filter_is_user_installation_allowed (self->filter); + + /* While the underlying permissions storage allows the system and user settings + * to be stored completely independently, force the system setting to OFF if + * the user setting is OFF in the UI. This keeps the policy in use for most + * people simpler. */ + if (!allow_user_installation) + allow_system_installation = FALSE; + + g_signal_handlers_block_by_func (self->allow_system_installation_switch, + on_allow_installation_switch_active_changed_cb, + self); + + g_signal_handlers_block_by_func (self->allow_user_installation_switch, + on_allow_installation_switch_active_changed_cb, + self); + + gtk_switch_set_active (self->allow_system_installation_switch, allow_system_installation); + gtk_switch_set_active (self->allow_user_installation_switch, allow_user_installation); + + g_debug ("Allow system installation: %s", allow_system_installation ? "yes" : "no"); + g_debug ("Allow user installation: %s", allow_user_installation ? "yes" : "no"); + + g_signal_handlers_unblock_by_func (self->allow_system_installation_switch, + on_allow_installation_switch_active_changed_cb, + self); + + g_signal_handlers_unblock_by_func (self->allow_user_installation_switch, + on_allow_installation_switch_active_changed_cb, + self); +} + +static void +update_allow_web_browsers (MctUserControls *self) +{ + gboolean allow_web_browsers; + + allow_web_browsers = mct_app_filter_is_content_type_allowed (self->filter, + WEB_BROWSERS_CONTENT_TYPE); + + g_signal_handlers_block_by_func (self->allow_web_browsers_switch, + on_allow_web_browsers_switch_active_changed_cb, + self); + + gtk_switch_set_active (self->allow_web_browsers_switch, allow_web_browsers); + + g_debug ("Allow web browsers: %s", allow_web_browsers ? "yes" : "no"); + + g_signal_handlers_unblock_by_func (self->allow_web_browsers_switch, + on_allow_web_browsers_switch_active_changed_cb, + self); +} + +static void +setup_parental_control_settings (MctUserControls *self) +{ + gboolean is_authorized; + + gtk_widget_set_visible (GTK_WIDGET (self), self->filter != NULL); + + if (!self->filter) + return; + + /* We only want to make the controls sensitive if we have permission to save + * changes (@is_authorized). */ + if (self->permission != NULL) + is_authorized = g_permission_get_allowed (G_PERMISSION (self->permission)); + else + is_authorized = TRUE; + + gtk_widget_set_sensitive (GTK_WIDGET (self), is_authorized); + + g_hash_table_remove_all (self->blacklisted_apps); + + update_oars_level (self); + update_categories_from_language (self); + update_allow_app_installation (self); + update_allow_web_browsers (self); + reload_apps (self); +} + +/* Will return %NULL if @flatpak_id is not installed. */ +static gchar * +get_flatpak_ref_for_app_id (MctUserControls *self, + const gchar *flatpak_id) +{ + g_autoptr(FlatpakInstalledRef) ref = NULL; + g_autoptr(GError) error = NULL; + + g_assert (self->system_installation != NULL); + g_assert (self->user_installation != NULL); + + ref = flatpak_installation_get_current_installed_app (self->user_installation, + flatpak_id, + self->cancellable, + &error); + + if (error && + !g_error_matches (error, FLATPAK_ERROR, FLATPAK_ERROR_NOT_INSTALLED)) + { + g_warning ("Error searching for Flatpak ref: %s", error->message); + return NULL; + } + + g_clear_error (&error); + + if (!ref || !flatpak_installed_ref_get_is_current (ref)) + { + ref = flatpak_installation_get_current_installed_app (self->system_installation, + flatpak_id, + self->cancellable, + &error); + if (error) + { + if (!g_error_matches (error, FLATPAK_ERROR, FLATPAK_ERROR_NOT_INSTALLED)) + g_warning ("Error searching for Flatpak ref: %s", error->message); + return NULL; + } + } + + return flatpak_ref_format_ref (FLATPAK_REF (ref)); +} + +/* Callbacks */ + +static gboolean +blacklist_apps_cb (gpointer data) +{ + g_auto(MctAppFilterBuilder) builder = MCT_APP_FILTER_BUILDER_INIT (); + g_autoptr(MctAppFilter) new_filter = NULL; + g_autoptr(GError) error = NULL; + MctUserControls *self = data; + GDesktopAppInfo *app; + GHashTableIter iter; + gboolean allow_web_browsers; + gsize i; + + self->blacklist_apps_source_id = 0; + + g_debug ("Building parental controls settings…"); + + /* Blacklist */ + + g_debug ("\t → Blacklisting apps"); + + g_hash_table_iter_init (&iter, self->blacklisted_apps); + while (g_hash_table_iter_next (&iter, (gpointer) &app, NULL)) + { + g_autofree gchar *flatpak_id = NULL; + + flatpak_id = g_desktop_app_info_get_string (app, "X-Flatpak"); + if (flatpak_id) + flatpak_id = g_strstrip (flatpak_id); + + if (flatpak_id) + { + g_autofree gchar *flatpak_ref = get_flatpak_ref_for_app_id (self, flatpak_id); + + if (!flatpak_ref) + { + g_warning ("Skipping blacklisting Flatpak ID ‘%s’ due to it not being installed", flatpak_id); + continue; + } + + g_debug ("\t\t → Blacklisting Flatpak ref: %s", flatpak_ref); + mct_app_filter_builder_blacklist_flatpak_ref (&builder, flatpak_ref); + } + else + { + const gchar *executable = g_app_info_get_executable (G_APP_INFO (app)); + g_autofree gchar *path = g_find_program_in_path (executable); + + if (!path) + { + g_warning ("Skipping blacklisting executable ‘%s’ due to it not being found", executable); + continue; + } + + g_debug ("\t\t → Blacklisting path: %s", path); + mct_app_filter_builder_blacklist_path (&builder, path); + } + } + + /* Maturity level */ + + g_debug ("\t → Maturity level"); + + if (self->selected_age == oars_disabled_age) + g_debug ("\t\t → Disabled"); + + for (i = 0; self->selected_age != oars_disabled_age && oars_categories[i] != NULL; i++) + { + MctAppFilterOarsValue oars_value; + const gchar *oars_category; + + oars_category = oars_categories[i]; + oars_value = as_content_rating_id_csm_age_to_value (oars_category, self->selected_age); + + g_debug ("\t\t → %s: %s", oars_category, oars_value_to_string (oars_value)); + + mct_app_filter_builder_set_oars_value (&builder, oars_category, oars_value); + } + + /* Web browsers */ + allow_web_browsers = gtk_switch_get_active (self->allow_web_browsers_switch); + + g_debug ("\t → %s web browsers", allow_web_browsers ? "Enabling" : "Disabling"); + + if (!allow_web_browsers) + mct_app_filter_builder_blacklist_content_type (&builder, WEB_BROWSERS_CONTENT_TYPE); + + /* App installation */ + if (act_user_get_account_type (self->user) != ACT_USER_ACCOUNT_TYPE_ADMINISTRATOR) + { + gboolean allow_system_installation; + gboolean allow_user_installation; + + allow_system_installation = gtk_switch_get_active (self->allow_system_installation_switch); + allow_user_installation = gtk_switch_get_active (self->allow_user_installation_switch); + + g_debug ("\t → %s system installation", allow_system_installation ? "Enabling" : "Disabling"); + g_debug ("\t → %s user installation", allow_user_installation ? "Enabling" : "Disabling"); + + mct_app_filter_builder_set_allow_user_installation (&builder, allow_user_installation); + mct_app_filter_builder_set_allow_system_installation (&builder, allow_system_installation); + } + + new_filter = mct_app_filter_builder_end (&builder); + + /* FIXME: should become asynchronous */ + mct_manager_set_app_filter (self->manager, + act_user_get_uid (self->user), + new_filter, + MCT_MANAGER_SET_VALUE_FLAGS_INTERACTIVE, + self->cancellable, + &error); + + if (error) + { + g_warning ("Error updating app filter: %s", error->message); + setup_parental_control_settings (self); + } + + return G_SOURCE_REMOVE; +} + +static void +on_allow_installation_switch_active_changed_cb (GtkSwitch *s, + GParamSpec *pspec, + MctUserControls *self) +{ + /* See the comment about policy in update_allow_app_installation(). */ + if (s == self->allow_user_installation_switch && + !gtk_switch_get_active (s) && + gtk_switch_get_active (self->allow_system_installation_switch)) + { + g_signal_handlers_block_by_func (self->allow_system_installation_switch, + on_allow_installation_switch_active_changed_cb, + self); + gtk_switch_set_active (self->allow_system_installation_switch, FALSE); + g_signal_handlers_unblock_by_func (self->allow_system_installation_switch, + on_allow_installation_switch_active_changed_cb, + self); + } + + /* Save the changes. */ + schedule_update_blacklisted_apps (self); +} + +static void +on_allow_web_browsers_switch_active_changed_cb (GtkSwitch *s, + GParamSpec *pspec, + MctUserControls *self) +{ + /* Save the changes. */ + schedule_update_blacklisted_apps (self); +} + +static void +on_switch_active_changed_cb (GtkSwitch *s, + GParamSpec *pspec, + MctUserControls *self) +{ + GAppInfo *app; + gboolean allowed; + + app = g_object_get_data (G_OBJECT (s), "GAppInfo"); + allowed = gtk_switch_get_active (s); + + if (allowed) + { + gboolean removed; + + g_debug ("Removing '%s' from blacklisted apps", g_app_info_get_id (app)); + + removed = g_hash_table_remove (self->blacklisted_apps, app); + g_assert (removed); + } + else + { + gboolean added; + + g_debug ("Blacklisting '%s'", g_app_info_get_id (app)); + + added = g_hash_table_add (self->blacklisted_apps, g_object_ref (app)); + g_assert (added); + } + + schedule_update_blacklisted_apps (self); +} + +static GtkWidget * +create_row_for_app_cb (gpointer item, + gpointer user_data) +{ + g_autoptr(GIcon) icon = NULL; + MctUserControls *self; + GtkWidget *box, *w; + GAppInfo *app; + gboolean allowed; + const gchar *app_name; + gint size; + + self = MCT_USER_CONTROLS (user_data); + app = item; + app_name = g_app_info_get_name (app); + + g_assert (G_IS_DESKTOP_APP_INFO (app)); + + icon = g_app_info_get_icon (app); + if (icon == NULL) + icon = g_themed_icon_new ("application-x-executable"); + else + g_object_ref (icon); + + box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 12); + gtk_container_set_border_width (GTK_CONTAINER (box), 12); + gtk_widget_set_margin_end (box, 12); + + /* Icon */ + w = gtk_image_new_from_gicon (icon, GTK_ICON_SIZE_DIALOG); + gtk_icon_size_lookup (GTK_ICON_SIZE_DND, &size, NULL); + gtk_image_set_pixel_size (GTK_IMAGE (w), size); + gtk_container_add (GTK_CONTAINER (box), w); + + /* App name label */ + w = g_object_new (GTK_TYPE_LABEL, + "label", app_name, + "hexpand", TRUE, + "xalign", 0.0, + NULL); + gtk_container_add (GTK_CONTAINER (box), w); + + /* Switch */ + w = g_object_new (GTK_TYPE_SWITCH, + "valign", GTK_ALIGN_CENTER, + NULL); + gtk_container_add (GTK_CONTAINER (box), w); + + gtk_widget_show_all (box); + + /* Fetch status from AccountService */ + allowed = mct_app_filter_is_appinfo_allowed (self->filter, app); + + gtk_switch_set_active (GTK_SWITCH (w), allowed); + g_object_set_data_full (G_OBJECT (w), "GAppInfo", g_object_ref (app), g_object_unref); + + if (allowed) + g_hash_table_remove (self->blacklisted_apps, app); + else if (!allowed) + g_hash_table_add (self->blacklisted_apps, g_object_ref (app)); + + g_signal_connect (w, "notify::active", G_CALLBACK (on_switch_active_changed_cb), self); + + return box; +} + +static gint +compare_app_info_cb (gconstpointer a, + gconstpointer b, + gpointer user_data) +{ + GAppInfo *app_a = (GAppInfo*) a; + GAppInfo *app_b = (GAppInfo*) b; + + return g_utf8_collate (g_app_info_get_display_name (app_a), + g_app_info_get_display_name (app_b)); +} + +static void +on_set_age_action_activated (GSimpleAction *action, + GVariant *param, + gpointer user_data) +{ + GsContentRatingSystem rating_system; + MctUserControls *self; + const gchar * const * entries; + const guint *ages; + guint age; + guint i; + + self = MCT_USER_CONTROLS (user_data); + age = g_variant_get_uint32 (param); + + rating_system = get_content_rating_system (self->user); + entries = gs_utils_content_rating_get_values (rating_system); + ages = gs_utils_content_rating_get_ages (rating_system); + + /* Update the button */ + if (age == oars_disabled_age) + gtk_button_set_label (self->restriction_button, _("No Restriction")); + + for (i = 0; age != oars_disabled_age && entries[i] != NULL; i++) + { + if (ages[i] == age) + { + gtk_button_set_label (self->restriction_button, entries[i]); + break; + } + } + + g_assert (age == oars_disabled_age || entries[i] != NULL); + + if (age == oars_disabled_age) + g_debug ("Selected to disable OARS"); + else + g_debug ("Selected OARS age: %u", age); + + self->selected_age = age; + + schedule_update_blacklisted_apps (self); +} + +/* GObject overrides */ + +static void +mct_user_controls_finalize (GObject *object) +{ + MctUserControls *self = (MctUserControls *)object; + + g_assert (self->blacklist_apps_source_id == 0); + + g_cancellable_cancel (self->cancellable); + g_clear_object (&self->action_group); + g_clear_object (&self->apps); + g_clear_object (&self->cancellable); + g_clear_object (&self->system_installation); + g_clear_object (&self->user); + g_clear_object (&self->user_installation); + + if (self->permission != NULL && self->permission_allowed_id != 0) + { + g_signal_handler_disconnect (self->permission, self->permission_allowed_id); + self->permission_allowed_id = 0; + } + g_clear_object (&self->permission); + + g_clear_pointer (&self->blacklisted_apps, g_hash_table_unref); + g_clear_pointer (&self->filter, mct_app_filter_unref); + g_clear_object (&self->manager); + g_clear_object (&self->app_info_monitor); + + G_OBJECT_CLASS (mct_user_controls_parent_class)->finalize (object); +} + + +static void +mct_user_controls_dispose (GObject *object) +{ + MctUserControls *self = (MctUserControls *)object; + + flush_update_blacklisted_apps (self); + + G_OBJECT_CLASS (mct_user_controls_parent_class)->dispose (object); +} + +static void +mct_user_controls_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + MctUserControls *self = MCT_USER_CONTROLS (object); + + switch (prop_id) + { + case PROP_USER: + g_value_set_object (value, self->user); + break; + + case PROP_PERMISSION: + g_value_set_object (value, self->permission); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +mct_user_controls_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + MctUserControls *self = MCT_USER_CONTROLS (object); + + switch (prop_id) + { + case PROP_USER: + mct_user_controls_set_user (self, g_value_get_object (value)); + break; + + case PROP_PERMISSION: + mct_user_controls_set_permission (self, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +mct_user_controls_class_init (MctUserControlsClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = mct_user_controls_finalize; + object_class->dispose = mct_user_controls_dispose; + object_class->get_property = mct_user_controls_get_property; + object_class->set_property = mct_user_controls_set_property; + + properties[PROP_USER] = g_param_spec_object ("user", + "User", + "User", + ACT_TYPE_USER, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS | + G_PARAM_EXPLICIT_NOTIFY); + + properties[PROP_PERMISSION] = g_param_spec_object ("permission", + "Permission", + "Permission to change parental controls", + G_TYPE_PERMISSION, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS | + G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/freedesktop/MalcontentControl/ui/user-controls.ui"); + + gtk_widget_class_bind_template_child (widget_class, MctUserControls, age_menu); + gtk_widget_class_bind_template_child (widget_class, MctUserControls, allow_system_installation_switch); + gtk_widget_class_bind_template_child (widget_class, MctUserControls, allow_user_installation_switch); + gtk_widget_class_bind_template_child (widget_class, MctUserControls, allow_web_browsers_switch); + gtk_widget_class_bind_template_child (widget_class, MctUserControls, restriction_button); + gtk_widget_class_bind_template_child (widget_class, MctUserControls, restriction_popover); + gtk_widget_class_bind_template_child (widget_class, MctUserControls, listbox); + + gtk_widget_class_bind_template_callback (widget_class, on_allow_installation_switch_active_changed_cb); + gtk_widget_class_bind_template_callback (widget_class, on_allow_web_browsers_switch_active_changed_cb); +} + +static void +mct_user_controls_init (MctUserControls *self) +{ + g_autoptr(GDBusConnection) system_bus = NULL; + g_autoptr(GError) error = NULL; + + gtk_widget_init_template (GTK_WIDGET (self)); + + self->selected_age = (guint) -1; + self->system_installation = flatpak_installation_new_system (NULL, NULL); + self->user_installation = flatpak_installation_new_user (NULL, NULL); + + self->cancellable = g_cancellable_new (); + + /* FIXME: should become asynchronous */ + system_bus = g_bus_get_sync (G_BUS_TYPE_SYSTEM, self->cancellable, &error); + if (system_bus == NULL) + { + g_warning ("Error getting system bus while setting up app permissions: %s", error->message); + return; + } + + self->manager = mct_manager_new (system_bus); + + self->action_group = g_simple_action_group_new (); + g_action_map_add_action_entries (G_ACTION_MAP (self->action_group), + actions, + G_N_ELEMENTS (actions), + self); + + gtk_widget_insert_action_group (GTK_WIDGET (self), + "permissions", + G_ACTION_GROUP (self->action_group)); + + gtk_popover_bind_model (self->restriction_popover, G_MENU_MODEL (self->age_menu), NULL); + self->blacklisted_apps = g_hash_table_new_full (g_direct_hash, g_direct_equal, g_object_unref, NULL); + + self->apps = g_list_store_new (G_TYPE_APP_INFO); + + self->app_info_monitor = g_app_info_monitor_get (); + g_signal_connect_object (self->app_info_monitor, "changed", + (GCallback) app_info_changed_cb, self, 0); + + gtk_list_box_bind_model (self->listbox, + G_LIST_MODEL (self->apps), + create_row_for_app_cb, + self, + NULL); + + g_object_bind_property (self->allow_user_installation_switch, "active", + self->allow_system_installation_switch, "sensitive", + G_BINDING_DEFAULT); +} + +ActUser * +mct_user_controls_get_user (MctUserControls *self) +{ + g_return_val_if_fail (MCT_IS_USER_CONTROLS (self), NULL); + + return self->user; +} + +void +mct_user_controls_set_user (MctUserControls *self, + ActUser *user) +{ + g_return_if_fail (MCT_IS_USER_CONTROLS (self)); + g_return_if_fail (user == NULL || ACT_IS_USER (user)); + + /* If we have pending unsaved changes from the previous user, force them to be + * saved first. */ + flush_update_blacklisted_apps (self); + + if (g_set_object (&self->user, user)) + { + update_app_filter (self); + setup_parental_control_settings (self); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_USER]); + } +} + +static void +on_permission_allowed_cb (GObject *obj, + GParamSpec *pspec, + gpointer user_data) +{ + MctUserControls *self = MCT_USER_CONTROLS (user_data); + + setup_parental_control_settings (self); +} + +GPermission * /* (nullable) */ +mct_user_controls_get_permission (MctUserControls *self) +{ + g_return_val_if_fail (MCT_IS_USER_CONTROLS (self), NULL); + + return self->permission; +} + +void +mct_user_controls_set_permission (MctUserControls *self, + GPermission *permission /* (nullable) */) +{ + g_return_if_fail (MCT_IS_USER_CONTROLS (self)); + g_return_if_fail (permission == NULL || G_IS_PERMISSION (permission)); + + if (self->permission == permission) + return; + + if (self->permission != NULL && self->permission_allowed_id != 0) + { + g_signal_handler_disconnect (self->permission, self->permission_allowed_id); + self->permission_allowed_id = 0; + } + + g_clear_object (&self->permission); + + if (permission != NULL) + { + self->permission = g_object_ref (permission); + self->permission_allowed_id = g_signal_connect (self->permission, + "notify::allowed", + (GCallback) on_permission_allowed_cb, + self); + } + + /* Handle changes. */ + setup_parental_control_settings (self); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_PERMISSION]); +} diff --git a/malcontent-control/user-controls.h b/malcontent-control/user-controls.h new file mode 100644 index 0000000..a18d1c9 --- /dev/null +++ b/malcontent-control/user-controls.h @@ -0,0 +1,42 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright © 2018, 2019, 2020 Endless Mobile, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * + * Authors: + * - Georges Basile Stavracas Neto + * - Philip Withnall + */ + +#pragma once + +#include +#include + + +G_BEGIN_DECLS + +#define MCT_TYPE_USER_CONTROLS (mct_user_controls_get_type()) +G_DECLARE_FINAL_TYPE (MctUserControls, mct_user_controls, MCT, USER_CONTROLS, GtkGrid) + +ActUser *mct_user_controls_get_user (MctUserControls *self); +void mct_user_controls_set_user (MctUserControls *self, + ActUser *user); + +GPermission *mct_user_controls_get_permission (MctUserControls *self); +void mct_user_controls_set_permission (MctUserControls *self, + GPermission *permission); + +G_END_DECLS diff --git a/malcontent-control/user-controls.ui b/malcontent-control/user-controls.ui new file mode 100644 index 0000000..880c207 --- /dev/null +++ b/malcontent-control/user-controls.ui @@ -0,0 +1,327 @@ + + + + + + + + + + + + + + + + horizontal + + + + + + + + horizontal + + + + + + + + diff --git a/malcontent-control/user-image.c b/malcontent-control/user-image.c new file mode 100644 index 0000000..851ffb6 --- /dev/null +++ b/malcontent-control/user-image.c @@ -0,0 +1,174 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright © 2015 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * + * Authors: + * - Ondrej Holy + */ + +#include +#include +#include + +#include "user-image.h" + + +struct _MctUserImage +{ + GtkImage parent_instance; + + ActUser *user; +}; + +G_DEFINE_TYPE (MctUserImage, mct_user_image, GTK_TYPE_IMAGE) + +static GdkPixbuf * +round_image (GdkPixbuf *pixbuf) +{ + GdkPixbuf *dest = NULL; + cairo_surface_t *surface; + cairo_t *cr; + gint size; + + size = gdk_pixbuf_get_width (pixbuf); + surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, size, size); + cr = cairo_create (surface); + + /* Clip a circle */ + cairo_arc (cr, size / 2, size / 2, size / 2, 0, 2 * G_PI); + cairo_clip (cr); + cairo_new_path (cr); + + gdk_cairo_set_source_pixbuf (cr, pixbuf, 0, 0); + cairo_paint (cr); + + dest = gdk_pixbuf_get_from_surface (surface, 0, 0, size, size); + cairo_surface_destroy (surface); + cairo_destroy (cr); + + return dest; +} + +static cairo_surface_t * +render_user_icon (ActUser *user, + gint icon_size, + gint scale) +{ + g_autoptr(GdkPixbuf) source_pixbuf = NULL; + GdkPixbuf *pixbuf = NULL; + GError *error; + const gchar *icon_file; + cairo_surface_t *surface = NULL; + + g_return_val_if_fail (ACT_IS_USER (user), NULL); + g_return_val_if_fail (icon_size > 12, NULL); + + icon_file = act_user_get_icon_file (user); + pixbuf = NULL; + if (icon_file) + { + source_pixbuf = gdk_pixbuf_new_from_file_at_size (icon_file, + icon_size * scale, + icon_size * scale, + NULL); + if (source_pixbuf) + pixbuf = round_image (source_pixbuf); + } + + if (pixbuf != NULL) + goto out; + + error = NULL; + pixbuf = gtk_icon_theme_load_icon (gtk_icon_theme_get_default (), + "avatar-default", + icon_size * scale, + GTK_ICON_LOOKUP_FORCE_SIZE, + &error); + if (error) + { + g_warning ("%s", error->message); + g_error_free (error); + } + +out: + + if (pixbuf != NULL) + { + surface = gdk_cairo_surface_create_from_pixbuf (pixbuf, scale, NULL); + g_object_unref (pixbuf); + } + + return surface; +} + +static void +render_image (MctUserImage *image) +{ + cairo_surface_t *surface; + gint scale, pixel_size; + + if (image->user == NULL) + return; + + pixel_size = gtk_image_get_pixel_size (GTK_IMAGE (image)); + scale = gtk_widget_get_scale_factor (GTK_WIDGET (image)); + surface = render_user_icon (image->user, + pixel_size > 0 ? pixel_size : 48, + scale); + gtk_image_set_from_surface (GTK_IMAGE (image), surface); + cairo_surface_destroy (surface); +} + +void +mct_user_image_set_user (MctUserImage *image, + ActUser *user) +{ + g_clear_object (&image->user); + image->user = g_object_ref (user); + + render_image (image); +} + +static void +mct_user_image_finalize (GObject *object) +{ + MctUserImage *image = MCT_USER_IMAGE (object); + + g_clear_object (&image->user); + + G_OBJECT_CLASS (mct_user_image_parent_class)->finalize (object); +} + +static void +mct_user_image_class_init (MctUserImageClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + + object_class->finalize = mct_user_image_finalize; +} + +static void +mct_user_image_init (MctUserImage *image) +{ + g_signal_connect_swapped (image, "notify::scale-factor", G_CALLBACK (render_image), image); + g_signal_connect_swapped (image, "notify::pixel-size", G_CALLBACK (render_image), image); +} + +GtkWidget * +mct_user_image_new (void) +{ + return g_object_new (MCT_TYPE_USER_IMAGE, NULL); +} diff --git a/malcontent-control/user-image.h b/malcontent-control/user-image.h new file mode 100644 index 0000000..4126073 --- /dev/null +++ b/malcontent-control/user-image.h @@ -0,0 +1,37 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright © 2015 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * + * Authors: + * - Ondrej Holy + */ + +#pragma once + +#include +#include + + +G_BEGIN_DECLS + +#define MCT_TYPE_USER_IMAGE (mct_user_image_get_type ()) +G_DECLARE_FINAL_TYPE (MctUserImage, mct_user_image, MCT, USER_IMAGE, GtkImage) + +GtkWidget *mct_user_image_new (void); +void mct_user_image_set_user (MctUserImage *image, + ActUser *user); + +G_END_DECLS diff --git a/malcontent-control/user-selector.c b/malcontent-control/user-selector.c new file mode 100644 index 0000000..046fc93 --- /dev/null +++ b/malcontent-control/user-selector.c @@ -0,0 +1,470 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright © 2020 Endless Mobile, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * + * Authors: + * - Philip Withnall + */ + +#include +#include +#include +#include +#include +#include + +#include "carousel.h" +#include "user-image.h" +#include "user-selector.h" + + +static void reload_users (MctUserSelector *self, + ActUser *selected_user); +static void notify_is_loaded_cb (GObject *obj, + GParamSpec *pspec, + gpointer user_data); +static void user_added_cb (ActUserManager *user_manager, + ActUser *user, + gpointer user_data); +static void user_changed_or_removed_cb (ActUserManager *user_manager, + ActUser *user, + gpointer user_data); +static void carousel_item_activated (MctCarousel *carousel, + MctCarouselItem *item, + gpointer user_data); + + +/** + * MctUserSelector: + * + * The user selector is a widget which lists available user accounts and allows + * the user to select one. + * + * Since: 0.5.0 + */ +struct _MctUserSelector +{ + GtkBox parent_instance; + + MctCarousel *carousel; + + ActUserManager *user_manager; /* (owned) */ + ActUser *user; /* (owned) */ +}; + +G_DEFINE_TYPE (MctUserSelector, mct_user_selector, GTK_TYPE_BOX) + +typedef enum +{ + PROP_USER = 1, + PROP_USER_MANAGER, +} MctUserSelectorProperty; + +static GParamSpec *properties[PROP_USER_MANAGER + 1]; + +static void +mct_user_selector_constructed (GObject *obj) +{ + MctUserSelector *self = MCT_USER_SELECTOR (obj); + + g_assert (self->user_manager != NULL); + + g_signal_connect (self->user_manager, "user-changed", + G_CALLBACK (user_changed_or_removed_cb), self); + g_signal_connect (self->user_manager, "user-is-logged-in-changed", + G_CALLBACK (user_changed_or_removed_cb), self); + g_signal_connect (self->user_manager, "user-added", + G_CALLBACK (user_added_cb), self); + g_signal_connect (self->user_manager, "user-removed", + G_CALLBACK (user_changed_or_removed_cb), self); + g_signal_connect (self->user_manager, "notify::is-loaded", + G_CALLBACK (notify_is_loaded_cb), self); + + /* Start loading the user accounts. */ + notify_is_loaded_cb (G_OBJECT (self->user_manager), NULL, self); + + G_OBJECT_CLASS (mct_user_selector_parent_class)->constructed (obj); +} + +static void +mct_user_selector_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + MctUserSelector *self = MCT_USER_SELECTOR (object); + + switch ((MctUserSelectorProperty) prop_id) + { + case PROP_USER: + g_value_set_object (value, self->user); + break; + + case PROP_USER_MANAGER: + g_value_set_object (value, self->user_manager); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +mct_user_selector_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + MctUserSelector *self = MCT_USER_SELECTOR (object); + + switch ((MctUserSelectorProperty) prop_id) + { + case PROP_USER: + /* Currently read only */ + g_assert_not_reached (); + break; + + case PROP_USER_MANAGER: + g_assert (self->user_manager == NULL); + self->user_manager = g_value_dup_object (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +mct_user_selector_dispose (GObject *object) +{ + MctUserSelector *self = (MctUserSelector *)object; + + g_clear_object (&self->user); + + if (self->user_manager != NULL) + { + g_signal_handlers_disconnect_by_func (self->user_manager, notify_is_loaded_cb, self); + g_signal_handlers_disconnect_by_func (self->user_manager, user_changed_or_removed_cb, self); + g_signal_handlers_disconnect_by_func (self->user_manager, user_added_cb, self); + + g_clear_object (&self->user_manager); + } + + G_OBJECT_CLASS (mct_user_selector_parent_class)->dispose (object); +} + +static void +mct_user_selector_class_init (MctUserSelectorClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->constructed = mct_user_selector_constructed; + object_class->get_property = mct_user_selector_get_property; + object_class->set_property = mct_user_selector_set_property; + object_class->dispose = mct_user_selector_dispose; + + /** + * MctUserSelector:user: (nullable) + * + * The currently selected user account, or %NULL if no user is selected. + * Currently read only but may become writable in future. + * + * Since: 0.5.0 + */ + properties[PROP_USER] = + g_param_spec_object ("user", + "User", + "The currently selected user account, or %NULL if no user is selected.", + ACT_TYPE_USER, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS | + G_PARAM_EXPLICIT_NOTIFY); + + /** + * MctUserSelector:user-manager: (not nullable) + * + * The user manager providing the data for the widget. + * + * Since: 0.5.0 + */ + properties[PROP_USER_MANAGER] = + g_param_spec_object ("user-manager", + "User Manager", + "The user manager providing the data for the widget.", + ACT_TYPE_USER_MANAGER, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS | + G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, G_N_ELEMENTS (properties), properties); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/freedesktop/MalcontentControl/ui/user-selector.ui"); + + gtk_widget_class_bind_template_child (widget_class, MctUserSelector, carousel); + + gtk_widget_class_bind_template_callback (widget_class, carousel_item_activated); +} + +static void +mct_user_selector_init (MctUserSelector *self) +{ + /* Ensure the types used in the UI are registered. */ + g_type_ensure (MCT_TYPE_CAROUSEL); + + gtk_widget_init_template (GTK_WIDGET (self)); +} + +static void +notify_is_loaded_cb (GObject *obj, + GParamSpec *pspec, + gpointer user_data) +{ + MctUserSelector *self = MCT_USER_SELECTOR (user_data); + gboolean is_loaded; + + /* The implementation of #ActUserManager guarantees that once is-loaded is + * true, it is never reset to false. */ + g_object_get (self->user_manager, "is-loaded", &is_loaded, NULL); + if (is_loaded) + reload_users (self, NULL); +} + +static const gchar * +get_real_or_user_name (ActUser *user) +{ + const gchar *name; + + name = act_user_get_real_name (user); + if (name == NULL) + name = act_user_get_user_name (user); + + return name; +} + +static void +carousel_item_activated (MctCarousel *carousel, + MctCarouselItem *item, + gpointer user_data) +{ + MctUserSelector *self = MCT_USER_SELECTOR (user_data); + uid_t uid; + ActUser *user = NULL; + + g_clear_object (&self->user); + + uid = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (item), "uid")); + user = act_user_manager_get_user_by_id (self->user_manager, uid); + + if (g_set_object (&self->user, user)) + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_USER]); +} + +static gint +sort_users (gconstpointer a, + gconstpointer b) +{ + ActUser *ua, *ub; + gint result; + + ua = ACT_USER (a); + ub = ACT_USER (b); + + /* Make sure the current user is shown first */ + if (act_user_get_uid (ua) == getuid ()) + { + result = G_MININT32; + } + else if (act_user_get_uid (ub) == getuid ()) + { + result = G_MAXINT32; + } + else + { + g_autofree gchar *name1 = NULL, *name2 = NULL; + + name1 = g_utf8_collate_key (get_real_or_user_name (ua), -1); + name2 = g_utf8_collate_key (get_real_or_user_name (ub), -1); + + result = strcmp (name1, name2); + } + + return result; +} + +static gint +user_compare (gconstpointer i, + gconstpointer u) +{ + MctCarouselItem *item; + ActUser *user; + gint uid_a, uid_b; + gint result; + + item = (MctCarouselItem *) i; + user = ACT_USER (u); + + uid_a = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (item), "uid")); + uid_b = act_user_get_uid (user); + + result = uid_a - uid_b; + + return result; +} + +static void +reload_users (MctUserSelector *self, + ActUser *selected_user) +{ + ActUser *user; + g_autoptr(GSList) list = NULL; + GSList *l; + MctCarouselItem *item = NULL; + GtkSettings *settings; + gboolean animations; + + settings = gtk_widget_get_settings (GTK_WIDGET (self->carousel)); + + g_object_get (settings, "gtk-enable-animations", &animations, NULL); + g_object_set (settings, "gtk-enable-animations", FALSE, NULL); + + mct_carousel_purge_items (self->carousel); + + list = act_user_manager_list_users (self->user_manager); + g_debug ("Got %u users", g_slist_length (list)); + + list = g_slist_sort (list, (GCompareFunc) sort_users); + for (l = list; l; l = l->next) + { + user = l->data; + g_debug ("Adding user %s", get_real_or_user_name (user)); + user_added_cb (self->user_manager, user, self); + } + + if (selected_user) + item = mct_carousel_find_item (self->carousel, selected_user, user_compare); + mct_carousel_select_item (self->carousel, item); + + g_object_set (settings, "gtk-enable-animations", animations, NULL); + + gtk_revealer_set_reveal_child (GTK_REVEALER (self->carousel), TRUE); +} + +static GtkWidget * +create_carousel_entry (MctUserSelector *self, + ActUser *user) +{ + GtkWidget *box, *widget; + gchar *label; + + box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); + + widget = mct_user_image_new (); + mct_user_image_set_user (MCT_USER_IMAGE (widget), user); + gtk_box_pack_start (GTK_BOX (box), widget, FALSE, FALSE, 0); + + label = g_strdup_printf ("%s", + get_real_or_user_name (user)); + widget = gtk_label_new (label); + gtk_label_set_use_markup (GTK_LABEL (widget), TRUE); + gtk_label_set_ellipsize (GTK_LABEL (widget), PANGO_ELLIPSIZE_END); + gtk_widget_set_margin_top (widget, 5); + gtk_box_pack_start (GTK_BOX (box), widget, FALSE, FALSE, 0); + g_free (label); + + if (act_user_get_uid (user) == getuid ()) + label = g_strdup_printf ("%s", _("Your account")); + else + label = g_strdup (" "); + + widget = gtk_label_new (label); + gtk_label_set_use_markup (GTK_LABEL (widget), TRUE); + g_free (label); + + gtk_box_pack_start (GTK_BOX (box), widget, FALSE, FALSE, 0); + gtk_style_context_add_class (gtk_widget_get_style_context (widget), + "dim-label"); + + return box; +} + +static void +user_added_cb (ActUserManager *user_manager, + ActUser *user, + gpointer user_data) +{ + MctUserSelector *self = MCT_USER_SELECTOR (user_data); + GtkWidget *item, *widget; + + if (act_user_is_system_account (user)) + return; + + g_debug ("User added: %u %s", (guint) act_user_get_uid (user), get_real_or_user_name (user)); + + widget = create_carousel_entry (self, user); + item = mct_carousel_item_new (); + gtk_container_add (GTK_CONTAINER (item), widget); + + g_object_set_data (G_OBJECT (item), "uid", GINT_TO_POINTER (act_user_get_uid (user))); + gtk_container_add (GTK_CONTAINER (self->carousel), item); +} + +static void +user_changed_or_removed_cb (ActUserManager *user_manager, + ActUser *user, + gpointer user_data) +{ + MctUserSelector *self = MCT_USER_SELECTOR (user_data); + + reload_users (self, self->user); +} + +/** + * mct_user_selector_new: + * @user_manager: (transfer none): an #ActUserManager to provide the user data + * + * Create a new #MctUserSelector widget. + * + * Returns: (transfer full): a new user selector + * Since: 0.5.0 + */ +MctUserSelector * +mct_user_selector_new (ActUserManager *user_manager) +{ + g_return_val_if_fail (ACT_IS_USER_MANAGER (user_manager), NULL); + + return g_object_new (MCT_TYPE_USER_SELECTOR, + "user-manager", user_manager, + NULL); +} + +/** + * mct_user_selector_get_user: + * @self: an #MctUserSelector + * + * Get the currently selected user, or %NULL if no user is selected. + * + * Returns: (transfer none) (nullable): the currently selected user + * Since: 0.5.0 + */ +ActUser * +mct_user_selector_get_user (MctUserSelector *self) +{ + g_return_val_if_fail (MCT_IS_USER_SELECTOR (self), NULL); + + return self->user; +} diff --git a/malcontent-control/user-selector.h b/malcontent-control/user-selector.h new file mode 100644 index 0000000..ec30929 --- /dev/null +++ b/malcontent-control/user-selector.h @@ -0,0 +1,37 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright © 2020 Endless Mobile, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * + * Authors: + * - Philip Withnall + */ + +#pragma once + +#include +#include + + +G_BEGIN_DECLS + +#define MCT_TYPE_USER_SELECTOR (mct_user_selector_get_type ()) +G_DECLARE_FINAL_TYPE (MctUserSelector, mct_user_selector, MCT, USER_SELECTOR, GtkBox) + +MctUserSelector *mct_user_selector_new (ActUserManager *user_manager); + +ActUser *mct_user_selector_get_user (MctUserSelector *self); + +G_END_DECLS diff --git a/malcontent-control/user-selector.ui b/malcontent-control/user-selector.ui new file mode 100644 index 0000000..088d72f --- /dev/null +++ b/malcontent-control/user-selector.ui @@ -0,0 +1,14 @@ + + + + + + diff --git a/meson.build b/meson.build index 3907058..ea9fea2 100644 --- a/meson.build +++ b/meson.build @@ -48,6 +48,7 @@ libglib_testing_dep = dependency( config_h = configuration_data() config_h.set_quoted('GETTEXT_PACKAGE', meson.project_name()) +config_h.set_quoted('PACKAGE_LOCALE_DIR', join_paths(get_option('localedir'), meson.project_name())) configure_file( output: 'config.h', configuration: config_h, @@ -126,6 +127,8 @@ test_env = [ ] subdir('accounts-service') -subdir('malcontent-client') subdir('libmalcontent') +subdir('malcontent-client') +subdir('malcontent-control') subdir('pam') +subdir('po') diff --git a/po/.gitignore b/po/.gitignore new file mode 100644 index 0000000..9a72ba1 --- /dev/null +++ b/po/.gitignore @@ -0,0 +1 @@ +malcontent.pot diff --git a/po/POTFILES.in b/po/POTFILES.in index fd575ff..f5380f0 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -1,5 +1,12 @@ # List of source files containing translatable strings. # Please keep this file sorted alphabetically. -accounts-service/com.endlessm.ParentalControls.policy +accounts-service/com.endlessm.ParentalControls.policy.in libmalcontent/manager.c +malcontent-control/application.c +malcontent-control/gs-content-rating.c +malcontent-control/main.ui +malcontent-control/org.freedesktop.MalcontentControl.appdata.xml.in +malcontent-control/org.freedesktop.MalcontentControl.desktop.in +malcontent-control/user-controls.c +malcontent-control/user-controls.ui pam/pam_malcontent.c