diff --git a/malcontent-control/carousel.c b/malcontent-control/carousel.c new file mode 100644 index 0000000..928028b --- /dev/null +++ b/malcontent-control/carousel.c @@ -0,0 +1,429 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright 2016 (c) 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 . + * + * Author: Felipe Borges + */ + +#include "cc-carousel.h" + +#include +#include + +#define ARROW_SIZE 20 + +struct _CcCarouselItem { + GtkRadioButton parent; + + gint page; +}; + +G_DEFINE_TYPE (CcCarouselItem, cc_carousel_item, GTK_TYPE_RADIO_BUTTON) + +GtkWidget * +cc_carousel_item_new (void) +{ + return g_object_new (CC_TYPE_CAROUSEL_ITEM, NULL); +} + +static void +cc_carousel_item_class_init (CcCarouselItemClass *klass) +{ +} + +static void +cc_carousel_item_init (CcCarouselItem *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 _CcCarousel { + GtkRevealer parent; + + GList *children; + gint visible_page; + CcCarouselItem *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 (CcCarousel, cc_carousel, GTK_TYPE_REVEALER) + +enum { + ITEM_ACTIVATED, + NUM_SIGNALS +}; + +static guint signals[NUM_SIGNALS] = { 0, }; + +#define ITEMS_PER_PAGE 3 + +static gint +cc_carousel_item_get_x (CcCarouselItem *item, + CcCarousel *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 +cc_carousel_move_arrow (CcCarousel *self) +{ + GtkStyleContext *context; + gchar *css; + gint end_x; + GtkSettings *settings; + gboolean animations; + + if (!self->selected_item) + return; + + end_x = cc_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 (CcCarousel *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 (CcCarousel *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))); +} + +/** + * cc_carousel_find_item: + * @carousel: an CcCarousel 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 CcCarousel item using the supplied function to find the + * desired element. + * Ideally useful for matching a model object and its correspondent + * widget. + * + * Returns: the found CcCarouselItem, or %NULL if it is not found + */ +CcCarouselItem * +cc_carousel_find_item (CcCarousel *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 (CcCarouselItem *item, + GdkEvent *event, + gpointer user_data) +{ + CcCarousel *self = CC_CAROUSEL (user_data); + + cc_carousel_select_item (self, item); +} + +void +cc_carousel_select_item (CcCarousel *self, + CcCarouselItem *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 = cc_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) + { + cc_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); + + /* cc_carousel_move_arrow is called from on_transition_running */ +} + +static void +cc_carousel_select_item_at_index (CcCarousel *self, + gint index) +{ + GList *l = NULL; + + l = g_list_nth (self->children, index); + cc_carousel_select_item (self, l->data); +} + +static void +cc_carousel_goto_previous_page (GtkWidget *button, + gpointer user_data) +{ + CcCarousel *self = CC_CAROUSEL (user_data); + + self->visible_page--; + if (self->visible_page < 0) + self->visible_page = 0; + + /* Select first item of the page */ + cc_carousel_select_item_at_index (self, self->visible_page * ITEMS_PER_PAGE); +} + +static void +cc_carousel_goto_next_page (GtkWidget *button, + gpointer user_data) +{ + CcCarousel *self = CC_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 */ + cc_carousel_select_item_at_index (self, self->visible_page * ITEMS_PER_PAGE); +} + +static void +cc_carousel_add (GtkContainer *container, + GtkWidget *widget) +{ + CcCarousel *self = CC_CAROUSEL (container); + gboolean last_box_is_full; + + if (!CC_IS_CAROUSEL_ITEM (widget)) { + GTK_CONTAINER_CLASS (cc_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); + CC_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) { + gchar *page; + + page = g_strdup_printf ("%d", CC_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); +} + +void +cc_carousel_purge_items (CcCarousel *self) +{ + gtk_container_forall (GTK_CONTAINER (self->stack), + (GtkCallback) gtk_widget_destroy, + NULL); + + g_list_free (self->children); + self->children = NULL; + self->visible_page = 0; + self->selected_item = NULL; +} + +CcCarousel * +cc_carousel_new (void) +{ + return g_object_new (CC_TYPE_CAROUSEL, NULL); +} + +static void +cc_carousel_class_init (CcCarouselClass *klass) +{ + GtkWidgetClass *wclass = GTK_WIDGET_CLASS (klass); + GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass); + + gtk_widget_class_set_template_from_resource (wclass, + "/org/gnome/control-center/user-accounts/cc-carousel.ui"); + + gtk_widget_class_bind_template_child (wclass, CcCarousel, stack); + gtk_widget_class_bind_template_child (wclass, CcCarousel, go_back_button); + gtk_widget_class_bind_template_child (wclass, CcCarousel, go_next_button); + gtk_widget_class_bind_template_child (wclass, CcCarousel, arrow); + + gtk_widget_class_bind_template_callback (wclass, cc_carousel_goto_previous_page); + gtk_widget_class_bind_template_callback (wclass, cc_carousel_goto_next_page); + + container_class->add = cc_carousel_add; + + signals[ITEM_ACTIVATED] = g_signal_new ("item-activated", + CC_TYPE_CAROUSEL, + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, 1, + CC_TYPE_CAROUSEL_ITEM); +} + +static void +on_size_allocate (CcCarousel *self) +{ + if (self->selected_item == NULL) + return; + + if (gtk_stack_get_transition_running (self->stack)) + return; + + self->arrow_start_x = cc_carousel_item_get_x (self->selected_item, self); + cc_carousel_move_arrow (self); +} + +static void +on_transition_running (CcCarousel *self) +{ + if (!gtk_stack_get_transition_running (self->stack)) + cc_carousel_move_arrow (self); +} + +static void +cc_carousel_init (CcCarousel *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/gnome/control-center/user-accounts/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 +cc_carousel_get_item_count (CcCarousel *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..8cd3f9a --- /dev/null +++ b/malcontent-control/carousel.h @@ -0,0 +1,50 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright 2016 (c) 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 . + * + * Author: Felipe Borges + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define CC_TYPE_CAROUSEL_ITEM (cc_carousel_item_get_type ()) + +G_DECLARE_FINAL_TYPE (CcCarouselItem, cc_carousel_item, CC, CAROUSEL_ITEM, GtkRadioButton) + +#define CC_TYPE_CAROUSEL (cc_carousel_get_type ()) + +G_DECLARE_FINAL_TYPE (CcCarousel, cc_carousel, CC, CAROUSEL, GtkRevealer) + +GtkWidget *cc_carousel_item_new (void); + +CcCarousel *cc_carousel_new (void); + +void cc_carousel_purge_items (CcCarousel *self); + +CcCarouselItem *cc_carousel_find_item (CcCarousel *self, + gconstpointer data, + GCompareFunc func); + +void cc_carousel_select_item (CcCarousel *self, + CcCarouselItem *item); + +guint cc_carousel_get_item_count (CcCarousel *self); + +G_END_DECLS diff --git a/malcontent-control/carousel.ui b/malcontent-control/carousel.ui new file mode 100644 index 0000000..77ba44b --- /dev/null +++ b/malcontent-control/carousel.ui @@ -0,0 +1,118 @@ + + + + + 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/user-controls.c b/malcontent-control/user-controls.c new file mode 100644 index 0000000..4fa2951 --- /dev/null +++ b/malcontent-control/user-controls.c @@ -0,0 +1,1156 @@ +/* cc-app-permissions.c + * + * Copyright 2018, 2019 Endless, 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 3 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 . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include +#include +#include +#include +#include +#include + +#include "gs-content-rating.h" + +#include "cc-app-permissions.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 _CcAppPermissions +{ + 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) */ + + 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, + CcAppPermissions *self); + +static void on_allow_web_browsers_switch_active_changed_cb (GtkSwitch *s, + GParamSpec *pspec, + CcAppPermissions *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 (CcAppPermissions, cc_app_permissions, 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 (CcAppPermissions *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) +{ + CcAppPermissions *self = CC_APP_PERMISSIONS (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 (CcAppPermissions *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 (CcAppPermissions *self) +{ + if (self->blacklist_apps_source_id > 0) + { + blacklist_apps_cb (self); + g_source_remove (self->blacklist_apps_source_id); + self->blacklist_apps_source_id = 0; + } +} + +static void +update_app_filter (CcAppPermissions *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_GET_APP_FILTER_FLAGS_NONE, + self->cancellable, + &error); + + if (error) + { + 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 (CcAppPermissions *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"; + } + return ""; +} + +static void +update_oars_level (CcAppPermissions *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 (CcAppPermissions *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 (CcAppPermissions *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 (CcAppPermissions *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 (CcAppPermissions *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; + CcAppPermissions *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_GET_APP_FILTER_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, + CcAppPermissions *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, + CcAppPermissions *self) +{ + /* Save the changes. */ + schedule_update_blacklisted_apps (self); +} + +static void +on_switch_active_changed_cb (GtkSwitch *s, + GParamSpec *pspec, + CcAppPermissions *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; + CcAppPermissions *self; + GtkWidget *box, *w; + GAppInfo *app; + gboolean allowed; + const gchar *app_name; + gint size; + + self = CC_APP_PERMISSIONS (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; + CcAppPermissions *self; + const gchar * const * entries; + const guint *ages; + guint age; + guint i; + + self = CC_APP_PERMISSIONS (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 +cc_app_permissions_finalize (GObject *object) +{ + CcAppPermissions *self = (CcAppPermissions *)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 (cc_app_permissions_parent_class)->finalize (object); +} + + +static void +cc_app_permissions_dispose (GObject *object) +{ + CcAppPermissions *self = (CcAppPermissions *)object; + + flush_update_blacklisted_apps (self); + + G_OBJECT_CLASS (cc_app_permissions_parent_class)->dispose (object); +} + +static void +cc_app_permissions_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + CcAppPermissions *self = CC_APP_PERMISSIONS (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 +cc_app_permissions_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + CcAppPermissions *self = CC_APP_PERMISSIONS (object); + + switch (prop_id) + { + case PROP_USER: + cc_app_permissions_set_user (self, g_value_get_object (value)); + break; + + case PROP_PERMISSION: + cc_app_permissions_set_permission (self, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +cc_app_permissions_class_init (CcAppPermissionsClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = cc_app_permissions_finalize; + object_class->dispose = cc_app_permissions_dispose; + object_class->get_property = cc_app_permissions_get_property; + object_class->set_property = cc_app_permissions_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/gnome/control-center/user-accounts/cc-app-permissions.ui"); + + gtk_widget_class_bind_template_child (widget_class, CcAppPermissions, age_menu); + gtk_widget_class_bind_template_child (widget_class, CcAppPermissions, allow_system_installation_switch); + gtk_widget_class_bind_template_child (widget_class, CcAppPermissions, allow_user_installation_switch); + gtk_widget_class_bind_template_child (widget_class, CcAppPermissions, allow_web_browsers_switch); + gtk_widget_class_bind_template_child (widget_class, CcAppPermissions, restriction_button); + gtk_widget_class_bind_template_child (widget_class, CcAppPermissions, restriction_popover); + gtk_widget_class_bind_template_child (widget_class, CcAppPermissions, 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 +cc_app_permissions_init (CcAppPermissions *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* +cc_app_permissions_get_user (CcAppPermissions *self) +{ + g_return_val_if_fail (CC_IS_APP_PERMISSIONS (self), NULL); + + return self->user; +} + +void +cc_app_permissions_set_user (CcAppPermissions *self, + ActUser *user) +{ + g_return_if_fail (CC_IS_APP_PERMISSIONS (self)); + g_return_if_fail (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) +{ + CcAppPermissions *self = CC_APP_PERMISSIONS (user_data); + + setup_parental_control_settings (self); +} + +GPermission * /* (nullable) */ +cc_app_permissions_get_permission (CcAppPermissions *self) +{ + g_return_val_if_fail (CC_IS_APP_PERMISSIONS (self), NULL); + + return self->permission; +} + +void +cc_app_permissions_set_permission (CcAppPermissions *self, + GPermission *permission /* (nullable) */) +{ + g_return_if_fail (CC_IS_APP_PERMISSIONS (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..8a1f24e --- /dev/null +++ b/malcontent-control/user-controls.h @@ -0,0 +1,41 @@ +/* cc-app-permissions.h + * + * Copyright 2018 Georges Basile Stavracas Neto + * + * 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 3 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 . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include +#include +#include + +G_BEGIN_DECLS + +#define CC_TYPE_APP_PERMISSIONS (cc_app_permissions_get_type()) + +G_DECLARE_FINAL_TYPE (CcAppPermissions, cc_app_permissions, CC, APP_PERMISSIONS, GtkGrid) + +ActUser* cc_app_permissions_get_user (CcAppPermissions *self); +void cc_app_permissions_set_user (CcAppPermissions *self, + ActUser *user); + +GPermission *cc_app_permissions_get_permission (CcAppPermissions *self); +void cc_app_permissions_set_permission (CcAppPermissions *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..8b9438c --- /dev/null +++ b/malcontent-control/user-controls.ui @@ -0,0 +1,296 @@ + + + + + + + + + horizontal + + + + + + + + horizontal + + + + + + + + + diff --git a/malcontent-control/user-image.c b/malcontent-control/user-image.c new file mode 100644 index 0000000..26a430d --- /dev/null +++ b/malcontent-control/user-image.c @@ -0,0 +1,142 @@ +/* + * 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. + * + * (C) Copyright 2015 Red Hat, Inc. + */ + +#include "cc-user-image.h" + +#include +#include +#include + +#include "user-utils.h" + +struct _CcUserImage { + GtkImage parent_instance; + + ActUser *user; +}; + +G_DEFINE_TYPE (CcUserImage, cc_user_image, GTK_TYPE_IMAGE) + +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 (CcUserImage *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 +cc_user_image_set_user (CcUserImage *image, + ActUser *user) +{ + g_clear_object (&image->user); + image->user = g_object_ref (user); + + render_image (image); +} + +static void +cc_user_image_finalize (GObject *object) +{ + CcUserImage *image = CC_USER_IMAGE (object); + + g_clear_object (&image->user); + + G_OBJECT_CLASS (cc_user_image_parent_class)->finalize (object); +} + +static void +cc_user_image_class_init (CcUserImageClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + + object_class->finalize = cc_user_image_finalize; +} + +static void +cc_user_image_init (CcUserImage *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 * +cc_user_image_new (void) +{ + return g_object_new (CC_TYPE_USER_IMAGE, NULL); +} diff --git a/malcontent-control/user-image.h b/malcontent-control/user-image.h new file mode 100644 index 0000000..a7f69a8 --- /dev/null +++ b/malcontent-control/user-image.h @@ -0,0 +1,32 @@ +/* + * 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. + * + * (C) Copyright 2015 Red Hat, Inc. + */ + +#pragma once + +#include +#include + +G_BEGIN_DECLS + +#define CC_TYPE_USER_IMAGE (cc_user_image_get_type ()) +G_DECLARE_FINAL_TYPE (CcUserImage, cc_user_image, CC, USER_IMAGE, GtkImage) + +GtkWidget *cc_user_image_new (void); +void cc_user_image_set_user (CcUserImage *image, ActUser *user); + +G_END_DECLS