From a0a1c20e10c38e398e9dd09a0485979e7ea820b9 Mon Sep 17 00:00:00 2001 From: Jonas Lochmann Date: Mon, 1 Jun 2020 02:00:00 +0200 Subject: [PATCH] Add option to restrict viewing child users --- .../30.json | 1012 +++++++++++++++++ .../io/timelimit/android/data/Migrations.kt | 6 + .../io/timelimit/android/data/RoomDatabase.kt | 5 +- .../io/timelimit/android/data/model/User.kt | 21 +- .../timelimit/android/logic/AppSetupLogic.kt | 6 +- .../timelimit/android/sync/actions/Actions.kt | 28 + .../timelimit/android/sync/actions/Parser.kt | 1 + .../sync/actions/dispatch/ParentAction.kt | 12 +- .../advanced/ManageChildAdvancedFragment.kt | 11 + .../limituserviewing/LimitUserViewingView.kt | 69 ++ .../ui/overview/overview/OverviewFragment.kt | 18 +- .../layout/fragment_manage_child_advanced.xml | 3 + .../res/layout/limit_user_viewing_view.xml | 48 + .../values-de/strings-limit-user-viewing.xml | 24 + .../res/values/strings-limit-user-viewing.xml | 23 + 15 files changed, 1274 insertions(+), 13 deletions(-) create mode 100644 app/schemas/io.timelimit.android.data.RoomDatabase/30.json create mode 100644 app/src/main/java/io/timelimit/android/ui/manage/child/advanced/limituserviewing/LimitUserViewingView.kt create mode 100644 app/src/main/res/layout/limit_user_viewing_view.xml create mode 100644 app/src/main/res/values-de/strings-limit-user-viewing.xml create mode 100644 app/src/main/res/values/strings-limit-user-viewing.xml diff --git a/app/schemas/io.timelimit.android.data.RoomDatabase/30.json b/app/schemas/io.timelimit.android.data.RoomDatabase/30.json new file mode 100644 index 0000000..cd3a89f --- /dev/null +++ b/app/schemas/io.timelimit.android.data.RoomDatabase/30.json @@ -0,0 +1,1012 @@ +{ + "formatVersion": 1, + "database": { + "version": 30, + "identityHash": "92a484644a90519ea45035b41069c9b4", + "entities": [ + { + "tableName": "user", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `password` TEXT NOT NULL, `second_password_salt` TEXT NOT NULL, `type` TEXT NOT NULL, `timezone` TEXT NOT NULL, `disable_limits_until` INTEGER NOT NULL, `mail` TEXT NOT NULL, `current_device` TEXT NOT NULL, `category_for_not_assigned_apps` TEXT NOT NULL, `relax_primary_device` INTEGER NOT NULL, `mail_notification_flags` INTEGER NOT NULL, `blocked_times` TEXT NOT NULL, `flags` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "secondPasswordSalt", + "columnName": "second_password_salt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timeZone", + "columnName": "timezone", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "disableLimitsUntil", + "columnName": "disable_limits_until", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mail", + "columnName": "mail", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currentDevice", + "columnName": "current_device", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "categoryForNotAssignedApps", + "columnName": "category_for_not_assigned_apps", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "relaxPrimaryDevice", + "columnName": "relax_primary_device", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mailNotificationFlags", + "columnName": "mail_notification_flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "blockedTimes", + "columnName": "blocked_times", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "device", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `model` TEXT NOT NULL, `added_at` INTEGER NOT NULL, `current_user_id` TEXT NOT NULL, `apps_version` TEXT NOT NULL, `network_time` TEXT NOT NULL, `current_protection_level` TEXT NOT NULL, `highest_permission_level` TEXT NOT NULL, `current_usage_stats_permission` TEXT NOT NULL, `highest_usage_stats_permission` TEXT NOT NULL, `current_notification_access_permission` TEXT NOT NULL, `highest_notification_access_permission` TEXT NOT NULL, `current_app_version` INTEGER NOT NULL, `highest_app_version` INTEGER NOT NULL, `tried_disabling_device_admin` INTEGER NOT NULL, `did_reboot` INTEGER NOT NULL, `had_manipulation` INTEGER NOT NULL, `had_manipulation_flags` INTEGER NOT NULL, `did_report_uninstall` INTEGER NOT NULL, `is_user_kept_signed_in` INTEGER NOT NULL, `show_device_connected` INTEGER NOT NULL, `default_user` TEXT NOT NULL, `default_user_timeout` INTEGER NOT NULL, `consider_reboot_manipulation` INTEGER NOT NULL, `current_overlay_permission` TEXT NOT NULL, `highest_overlay_permission` TEXT NOT NULL, `current_accessibility_service_permission` INTEGER NOT NULL, `was_accessibility_service_permission` INTEGER NOT NULL, `enable_activity_level_blocking` INTEGER NOT NULL, `q_or_later` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "model", + "columnName": "model", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "addedAt", + "columnName": "added_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentUserId", + "columnName": "current_user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "installedAppsVersion", + "columnName": "apps_version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "networkTime", + "columnName": "network_time", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currentProtectionLevel", + "columnName": "current_protection_level", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "highestProtectionLevel", + "columnName": "highest_permission_level", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currentUsageStatsPermission", + "columnName": "current_usage_stats_permission", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "highestUsageStatsPermission", + "columnName": "highest_usage_stats_permission", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currentNotificationAccessPermission", + "columnName": "current_notification_access_permission", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "highestNotificationAccessPermission", + "columnName": "highest_notification_access_permission", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currentAppVersion", + "columnName": "current_app_version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "highestAppVersion", + "columnName": "highest_app_version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "manipulationTriedDisablingDeviceAdmin", + "columnName": "tried_disabling_device_admin", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "manipulationDidReboot", + "columnName": "did_reboot", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hadManipulation", + "columnName": "had_manipulation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hadManipulationFlags", + "columnName": "had_manipulation_flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "didReportUninstall", + "columnName": "did_report_uninstall", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isUserKeptSignedIn", + "columnName": "is_user_kept_signed_in", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "showDeviceConnected", + "columnName": "show_device_connected", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultUser", + "columnName": "default_user", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "defaultUserTimeout", + "columnName": "default_user_timeout", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "considerRebootManipulation", + "columnName": "consider_reboot_manipulation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentOverlayPermission", + "columnName": "current_overlay_permission", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "highestOverlayPermission", + "columnName": "highest_overlay_permission", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessibilityServiceEnabled", + "columnName": "current_accessibility_service_permission", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wasAccessibilityServiceEnabled", + "columnName": "was_accessibility_service_permission", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enableActivityLevelBlocking", + "columnName": "enable_activity_level_blocking", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "qOrLater", + "columnName": "q_or_later", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "app", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`device_id` TEXT NOT NULL, `package_name` TEXT NOT NULL, `title` TEXT NOT NULL, `launchable` INTEGER NOT NULL, `recommendation` TEXT NOT NULL, PRIMARY KEY(`device_id`, `package_name`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "device_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "package_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isLaunchable", + "columnName": "launchable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recommendation", + "columnName": "recommendation", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "device_id", + "package_name" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_app_device_id", + "unique": false, + "columnNames": [ + "device_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_app_device_id` ON `${TABLE_NAME}` (`device_id`)" + }, + { + "name": "index_app_package_name", + "unique": false, + "columnNames": [ + "package_name" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_app_package_name` ON `${TABLE_NAME}` (`package_name`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "category_app", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`category_id` TEXT NOT NULL, `package_name` TEXT NOT NULL, PRIMARY KEY(`category_id`, `package_name`))", + "fields": [ + { + "fieldPath": "categoryId", + "columnName": "category_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "package_name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "category_id", + "package_name" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_category_app_category_id", + "unique": false, + "columnNames": [ + "category_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_category_app_category_id` ON `${TABLE_NAME}` (`category_id`)" + }, + { + "name": "index_category_app_package_name", + "unique": false, + "columnNames": [ + "package_name" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_category_app_package_name` ON `${TABLE_NAME}` (`package_name`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "category", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `child_id` TEXT NOT NULL, `title` TEXT NOT NULL, `blocked_times` TEXT NOT NULL, `extra_time` INTEGER NOT NULL, `extra_time_day` INTEGER NOT NULL, `temporarily_blocked` INTEGER NOT NULL, `temporarily_blocked_end_time` INTEGER NOT NULL, `base_version` TEXT NOT NULL, `apps_version` TEXT NOT NULL, `rules_version` TEXT NOT NULL, `usedtimes_version` TEXT NOT NULL, `parent_category_id` TEXT NOT NULL, `block_all_notifications` INTEGER NOT NULL, `time_warnings` INTEGER NOT NULL, `min_battery_charging` INTEGER NOT NULL, `min_battery_mobile` INTEGER NOT NULL, `sort` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "childId", + "columnName": "child_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "blockedMinutesInWeek", + "columnName": "blocked_times", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "extraTimeInMillis", + "columnName": "extra_time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "extraTimeDay", + "columnName": "extra_time_day", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "temporarilyBlocked", + "columnName": "temporarily_blocked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "temporarilyBlockedEndTime", + "columnName": "temporarily_blocked_end_time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseVersion", + "columnName": "base_version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "assignedAppsVersion", + "columnName": "apps_version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timeLimitRulesVersion", + "columnName": "rules_version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usedTimesVersion", + "columnName": "usedtimes_version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentCategoryId", + "columnName": "parent_category_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "blockAllNotifications", + "columnName": "block_all_notifications", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timeWarnings", + "columnName": "time_warnings", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "minBatteryLevelWhileCharging", + "columnName": "min_battery_charging", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "minBatteryLevelMobile", + "columnName": "min_battery_mobile", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "used_time", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`day_of_epoch` INTEGER NOT NULL, `used_time` INTEGER NOT NULL, `category_id` TEXT NOT NULL, `start_time_of_day` INTEGER NOT NULL, `end_time_of_day` INTEGER NOT NULL, PRIMARY KEY(`category_id`, `day_of_epoch`, `start_time_of_day`, `end_time_of_day`))", + "fields": [ + { + "fieldPath": "dayOfEpoch", + "columnName": "day_of_epoch", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "usedMillis", + "columnName": "used_time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "categoryId", + "columnName": "category_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "startTimeOfDay", + "columnName": "start_time_of_day", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "endTimeOfDay", + "columnName": "end_time_of_day", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "category_id", + "day_of_epoch", + "start_time_of_day", + "end_time_of_day" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "time_limit_rule", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `category_id` TEXT NOT NULL, `apply_to_extra_time_usage` INTEGER NOT NULL, `day_mask` INTEGER NOT NULL, `max_time` INTEGER NOT NULL, `start_minute_of_day` INTEGER NOT NULL, `end_minute_of_day` INTEGER NOT NULL, `session_duration_milliseconds` INTEGER NOT NULL, `session_pause_milliseconds` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "categoryId", + "columnName": "category_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "applyToExtraTimeUsage", + "columnName": "apply_to_extra_time_usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dayMask", + "columnName": "day_mask", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "maximumTimeInMillis", + "columnName": "max_time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "startMinuteOfDay", + "columnName": "start_minute_of_day", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "endMinuteOfDay", + "columnName": "end_minute_of_day", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sessionDurationMilliseconds", + "columnName": "session_duration_milliseconds", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sessionPauseMilliseconds", + "columnName": "session_pause_milliseconds", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "config", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "temporarily_allowed_app", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`device_id` TEXT NOT NULL, `package_name` TEXT NOT NULL, PRIMARY KEY(`device_id`, `package_name`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "device_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "package_name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "device_id", + "package_name" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "pending_sync_action", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sequence_number` INTEGER NOT NULL, `action` TEXT NOT NULL, `integrity` TEXT NOT NULL, `scheduled_for_upload` INTEGER NOT NULL, `type` TEXT NOT NULL, `user_id` TEXT NOT NULL, PRIMARY KEY(`sequence_number`))", + "fields": [ + { + "fieldPath": "sequenceNumber", + "columnName": "sequence_number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "encodedAction", + "columnName": "action", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "integrity", + "columnName": "integrity", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scheduledForUpload", + "columnName": "scheduled_for_upload", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sequence_number" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_pending_sync_action_scheduled_for_upload", + "unique": false, + "columnNames": [ + "scheduled_for_upload" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_pending_sync_action_scheduled_for_upload` ON `${TABLE_NAME}` (`scheduled_for_upload`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "app_activity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`device_id` TEXT NOT NULL, `app_package_name` TEXT NOT NULL, `activity_class_name` TEXT NOT NULL, `activity_title` TEXT NOT NULL, PRIMARY KEY(`device_id`, `app_package_name`, `activity_class_name`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "device_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "appPackageName", + "columnName": "app_package_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activityClassName", + "columnName": "activity_class_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "activity_title", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "device_id", + "app_package_name", + "activity_class_name" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "notification", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL, `id` TEXT NOT NULL, `first_notify_time` INTEGER NOT NULL, `dismissed` INTEGER NOT NULL, PRIMARY KEY(`type`, `id`))", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "firstNotifyTime", + "columnName": "first_notify_time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDismissed", + "columnName": "dismissed", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "type", + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "allowed_contact", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `phone` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phone", + "columnName": "phone", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "user_key", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `key` BLOB NOT NULL, `last_use` INTEGER NOT NULL, PRIMARY KEY(`user_id`), FOREIGN KEY(`user_id`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "publicKey", + "columnName": "key", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "lastUse", + "columnName": "last_use", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "user_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_user_key_key", + "unique": true, + "columnNames": [ + "key" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_key_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "user_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "session_duration", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`category_id` TEXT NOT NULL, `max_session_duration` INTEGER NOT NULL, `session_pause_duration` INTEGER NOT NULL, `start_minute_of_day` INTEGER NOT NULL, `end_minute_of_day` INTEGER NOT NULL, `last_usage` INTEGER NOT NULL, `last_session_duration` INTEGER NOT NULL, PRIMARY KEY(`category_id`, `max_session_duration`, `session_pause_duration`, `start_minute_of_day`, `end_minute_of_day`), FOREIGN KEY(`category_id`) REFERENCES `category`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "categoryId", + "columnName": "category_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "maxSessionDuration", + "columnName": "max_session_duration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sessionPauseDuration", + "columnName": "session_pause_duration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "startMinuteOfDay", + "columnName": "start_minute_of_day", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "endMinuteOfDay", + "columnName": "end_minute_of_day", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUsage", + "columnName": "last_usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastSessionDuration", + "columnName": "last_session_duration", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "category_id", + "max_session_duration", + "session_pause_duration", + "start_minute_of_day", + "end_minute_of_day" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "session_duration_index_category_id", + "unique": false, + "columnNames": [ + "category_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `session_duration_index_category_id` ON `${TABLE_NAME}` (`category_id`)" + } + ], + "foreignKeys": [ + { + "table": "category", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "category_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '92a484644a90519ea45035b41069c9b4')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/data/Migrations.kt b/app/src/main/java/io/timelimit/android/data/Migrations.kt index 8193370..4d31514 100644 --- a/app/src/main/java/io/timelimit/android/data/Migrations.kt +++ b/app/src/main/java/io/timelimit/android/data/Migrations.kt @@ -218,4 +218,10 @@ object DatabaseMigrations { database.execSQL("CREATE INDEX IF NOT EXISTS `session_duration_index_category_id` ON `session_duration` (`category_id`)") } } + + val MIGRATE_TO_V30 = object: Migration(29, 30) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE `user` ADD COLUMN `flags` INTEGER NOT NULL DEFAULT 0") + } + } } diff --git a/app/src/main/java/io/timelimit/android/data/RoomDatabase.kt b/app/src/main/java/io/timelimit/android/data/RoomDatabase.kt index 34a0b57..86b3464 100644 --- a/app/src/main/java/io/timelimit/android/data/RoomDatabase.kt +++ b/app/src/main/java/io/timelimit/android/data/RoomDatabase.kt @@ -37,7 +37,7 @@ import io.timelimit.android.data.model.* AllowedContact::class, UserKey::class, SessionDuration::class -], version = 29) +], version = 30) abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database { companion object { private val lock = Object() @@ -100,7 +100,8 @@ abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database DatabaseMigrations.MIGRATE_TO_V26, DatabaseMigrations.MIGRATE_TO_V27, DatabaseMigrations.MIGRATE_TO_V28, - DatabaseMigrations.MIGRATE_TO_V29 + DatabaseMigrations.MIGRATE_TO_V29, + DatabaseMigrations.MIGRATE_TO_V30 ) .build() } diff --git a/app/src/main/java/io/timelimit/android/data/model/User.kt b/app/src/main/java/io/timelimit/android/data/model/User.kt index e06e018..d666223 100644 --- a/app/src/main/java/io/timelimit/android/data/model/User.kt +++ b/app/src/main/java/io/timelimit/android/data/model/User.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 Jonas Lochmann + * TimeLimit Copyright 2019 - 2020 Jonas Lochmann * * 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 @@ -62,7 +62,9 @@ data class User( @ColumnInfo(name = "mail_notification_flags") val mailNotificationFlags: Int, @ColumnInfo(name = "blocked_times") - val blockedTimes: ImmutableBitmask + val blockedTimes: ImmutableBitmask, + @ColumnInfo(name = "flags") + val flags: Long ): JsonSerializable { companion object { private const val ID = "id" @@ -78,6 +80,7 @@ data class User( private const val RELAX_PRIMARY_DEVICE = "relaxPrimaryDevice" private const val MAIL_NOTIFICATION_FLAGS = "mailNotificationFlags" private const val BLOCKED_TIMES = "blockedTimes" + private const val FLAGS = "flags" fun parse(reader: JsonReader): User { var id: String? = null @@ -93,6 +96,7 @@ data class User( var relaxPrimaryDevice = false var mailNotificationFlags = 0 var blockedTimes = ImmutableBitmask(BitSet()) + var flags = 0L reader.beginObject() while (reader.hasNext()) { @@ -110,6 +114,7 @@ data class User( RELAX_PRIMARY_DEVICE -> relaxPrimaryDevice = reader.nextBoolean() MAIL_NOTIFICATION_FLAGS -> mailNotificationFlags = reader.nextInt() BLOCKED_TIMES -> blockedTimes = ImmutableBitmaskJson.parse(reader.nextString(), Category.BLOCKED_MINUTES_IN_WEEK_LENGTH) + FLAGS -> flags = reader.nextLong() else -> reader.skipValue() } } @@ -128,7 +133,8 @@ data class User( categoryForNotAssignedApps = categoryForNotAssignedApps, relaxPrimaryDevice = relaxPrimaryDevice, mailNotificationFlags = mailNotificationFlags, - blockedTimes = blockedTimes + blockedTimes = blockedTimes, + flags = flags ) } @@ -159,6 +165,9 @@ data class User( } } + val restrictViewingToParents: Boolean + get() = flags and UserFlags.RESTRICT_VIEWING_TO_PARENTS == UserFlags.RESTRICT_VIEWING_TO_PARENTS + override fun serialize(writer: JsonWriter) { writer.beginObject() @@ -175,6 +184,7 @@ data class User( writer.name(RELAX_PRIMARY_DEVICE).value(relaxPrimaryDevice) writer.name(MAIL_NOTIFICATION_FLAGS).value(mailNotificationFlags) writer.name(BLOCKED_TIMES).value(ImmutableBitmaskJson.serialize(blockedTimes)) + writer.name(FLAGS).value(flags) writer.endObject() } @@ -207,3 +217,8 @@ class UserTypeConverter { @TypeConverter fun toString(value: UserType) = UserTypeJson.serialize(value) } + +object UserFlags { + const val RESTRICT_VIEWING_TO_PARENTS = 1L + const val ALL_FLAGS = RESTRICT_VIEWING_TO_PARENTS +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/logic/AppSetupLogic.kt b/app/src/main/java/io/timelimit/android/logic/AppSetupLogic.kt index 4821fa0..e724faa 100644 --- a/app/src/main/java/io/timelimit/android/logic/AppSetupLogic.kt +++ b/app/src/main/java/io/timelimit/android/logic/AppSetupLogic.kt @@ -128,7 +128,8 @@ class AppSetupLogic(private val appLogic: AppLogic) { categoryForNotAssignedApps = "", relaxPrimaryDevice = false, mailNotificationFlags = 0, - blockedTimes = ImmutableBitmask(BitSet()) + blockedTimes = ImmutableBitmask(BitSet()), + flags = 0 ) appLogic.database.user().addUserSync(child) @@ -150,7 +151,8 @@ class AppSetupLogic(private val appLogic: AppLogic) { categoryForNotAssignedApps = "", relaxPrimaryDevice = false, mailNotificationFlags = 0, - blockedTimes = ImmutableBitmask(BitSet()) + blockedTimes = ImmutableBitmask(BitSet()), + flags = 0 ) appLogic.database.user().addUserSync(parent) diff --git a/app/src/main/java/io/timelimit/android/sync/actions/Actions.kt b/app/src/main/java/io/timelimit/android/sync/actions/Actions.kt index 0342c0f..16b3da6 100644 --- a/app/src/main/java/io/timelimit/android/sync/actions/Actions.kt +++ b/app/src/main/java/io/timelimit/android/sync/actions/Actions.kt @@ -1826,6 +1826,34 @@ data class ResetParentBlockedTimesAction(val parentId: String): ParentAction() { } } +data class UpdateUserFlagsAction(val userId: String, val modifiedBits: Long, val newValues: Long): ParentAction() { + companion object { + private const val TYPE_VALUE = "UPDATE_USER_FLAGS" + private const val USER_ID = "userId" + private const val MODIFIED_BITS = "modified" + private const val NEW_VALUES = "values" + } + + init { + IdGenerator.assertIdValid(userId) + + if (modifiedBits or UserFlags.ALL_FLAGS != UserFlags.ALL_FLAGS || modifiedBits or newValues != modifiedBits) { + throw IllegalArgumentException() + } + } + + override fun serialize(writer: JsonWriter) { + writer.beginObject() + + writer.name(TYPE).value(TYPE_VALUE) + writer.name(USER_ID).value(userId) + writer.name(MODIFIED_BITS).value(modifiedBits) + writer.name(NEW_VALUES).value(newValues) + + writer.endObject() + } +} + // child actions object ChildSignInAction: ChildAction() { private const val TYPE_VALUE = "CHILD_SIGN_IN" diff --git a/app/src/main/java/io/timelimit/android/sync/actions/Parser.kt b/app/src/main/java/io/timelimit/android/sync/actions/Parser.kt index 64bc5a6..d4717d8 100644 --- a/app/src/main/java/io/timelimit/android/sync/actions/Parser.kt +++ b/app/src/main/java/io/timelimit/android/sync/actions/Parser.kt @@ -68,6 +68,7 @@ object ActionParser { // UpdateCategoryTimeWarningsAction // UpdateCategoryBatteryLimit // UpdateCategorySorting + // UpdateUserFlagsAction else -> throw IllegalStateException() } } diff --git a/app/src/main/java/io/timelimit/android/sync/actions/dispatch/ParentAction.kt b/app/src/main/java/io/timelimit/android/sync/actions/dispatch/ParentAction.kt index 2824457..65060e6 100644 --- a/app/src/main/java/io/timelimit/android/sync/actions/dispatch/ParentAction.kt +++ b/app/src/main/java/io/timelimit/android/sync/actions/dispatch/ParentAction.kt @@ -176,7 +176,8 @@ object LocalDatabaseParentActionDispatcher { categoryForNotAssignedApps = "", relaxPrimaryDevice = false, mailNotificationFlags = 0, - blockedTimes = ImmutableBitmask(BitSet()) + blockedTimes = ImmutableBitmask(BitSet()), + flags = 0 )) } is UpdateCategoryBlockedTimesAction -> { @@ -586,6 +587,15 @@ object LocalDatabaseParentActionDispatcher { database.category().updateCategorySorting(categoryId, index) } } + is UpdateUserFlagsAction -> { + val user = database.user().getUserByIdSync(action.userId)!! + + val updatedUser = user.copy( + flags = (user.flags and action.modifiedBits.inv()) or action.newValues + ) + + database.user().updateUserSync(updatedUser) + } }.let { } } } diff --git a/app/src/main/java/io/timelimit/android/ui/manage/child/advanced/ManageChildAdvancedFragment.kt b/app/src/main/java/io/timelimit/android/ui/manage/child/advanced/ManageChildAdvancedFragment.kt index 17917ef..bfb8079 100644 --- a/app/src/main/java/io/timelimit/android/ui/manage/child/advanced/ManageChildAdvancedFragment.kt +++ b/app/src/main/java/io/timelimit/android/ui/manage/child/advanced/ManageChildAdvancedFragment.kt @@ -34,11 +34,13 @@ import io.timelimit.android.ui.help.HelpDialogFragment import io.timelimit.android.ui.main.ActivityViewModel import io.timelimit.android.ui.main.getActivityViewModel import io.timelimit.android.ui.manage.child.ManageChildFragmentArgs +import io.timelimit.android.ui.manage.child.advanced.limituserviewing.LimitUserViewingView import io.timelimit.android.ui.manage.child.advanced.manageblocktemporarily.ManageBlockTemporarilyView import io.timelimit.android.ui.manage.child.advanced.managedisabletimelimits.ManageDisableTimelimitsViewHelper import io.timelimit.android.ui.manage.child.advanced.password.ManageChildPassword import io.timelimit.android.ui.manage.child.advanced.timezone.UserTimezoneView import io.timelimit.android.ui.manage.child.primarydevice.PrimaryDeviceView +import kotlin.concurrent.fixedRateTimer class ManageChildAdvancedFragment : Fragment() { companion object { @@ -149,6 +151,15 @@ class ManageChildAdvancedFragment : Fragment() { fragmentManager = fragmentManager!! ) + LimitUserViewingView.bind( + view = binding.limitViewing, + auth = auth, + lifecycleOwner = viewLifecycleOwner, + fragmentManager = parentFragmentManager, + userEntry = childEntry, + userId = params.childId + ) + return binding.root } } diff --git a/app/src/main/java/io/timelimit/android/ui/manage/child/advanced/limituserviewing/LimitUserViewingView.kt b/app/src/main/java/io/timelimit/android/ui/manage/child/advanced/limituserviewing/LimitUserViewingView.kt new file mode 100644 index 0000000..3f6c730 --- /dev/null +++ b/app/src/main/java/io/timelimit/android/ui/manage/child/advanced/limituserviewing/LimitUserViewingView.kt @@ -0,0 +1,69 @@ +/* + * TimeLimit Copyright 2019 - 2020 Jonas Lochmann + * + * 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 version 3 of the License. + * + * 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 . + */ + +package io.timelimit.android.ui.manage.child.advanced.limituserviewing + +import androidx.fragment.app.FragmentManager +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import io.timelimit.android.R +import io.timelimit.android.data.model.User +import io.timelimit.android.data.model.UserFlags +import io.timelimit.android.databinding.LimitUserViewingViewBinding +import io.timelimit.android.livedata.ignoreUnchanged +import io.timelimit.android.livedata.map +import io.timelimit.android.sync.actions.UpdateUserFlagsAction +import io.timelimit.android.ui.help.HelpDialogFragment +import io.timelimit.android.ui.main.ActivityViewModel + +object LimitUserViewingView { + fun bind( + view: LimitUserViewingViewBinding, + auth: ActivityViewModel, + lifecycleOwner: LifecycleOwner, + fragmentManager: FragmentManager, + userEntry: LiveData, + userId: String + ) { + view.titleView.setOnClickListener { + HelpDialogFragment.newInstance( + title = R.string.limit_user_viewing_title, + text = R.string.limit_user_viewing_help + ).show(fragmentManager) + } + + userEntry.map { it?.restrictViewingToParents ?: false }.ignoreUnchanged().observe(lifecycleOwner, Observer { checked -> + view.enableSwitch.setOnCheckedChangeListener { buttonView, isChecked -> /* ignore */ } + view.enableSwitch.isChecked = checked + view.enableSwitch.setOnCheckedChangeListener { buttonView, isChecked -> + if (isChecked != checked) { + if ( + !auth.tryDispatchParentAction( + UpdateUserFlagsAction( + userId = userId, + modifiedBits = UserFlags.RESTRICT_VIEWING_TO_PARENTS, + newValues = if (isChecked) UserFlags.RESTRICT_VIEWING_TO_PARENTS else 0 + ) + ) + ) { + view.enableSwitch.isChecked = checked + } + } + } + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/overview/overview/OverviewFragment.kt b/app/src/main/java/io/timelimit/android/ui/overview/overview/OverviewFragment.kt index ef15500..aa81d13 100644 --- a/app/src/main/java/io/timelimit/android/ui/overview/overview/OverviewFragment.kt +++ b/app/src/main/java/io/timelimit/android/ui/overview/overview/OverviewFragment.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 Jonas Lochmann + * TimeLimit Copyright 2019 - 2020 Jonas Lochmann * * 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 @@ -69,10 +69,18 @@ class OverviewFragment : CoroutineFragment(), CanNotAddDevicesInLocalModeDialogF } override fun onUserClicked(user: User) { - when (user.type) { - UserType.Child -> handlers.openManageChildScreen(childId = user.id) - UserType.Parent -> handlers.openManageParentScreen(parentId = user.id) - }.let { } + if ( + user.restrictViewingToParents && + logic.deviceUserId.value != user.id && + !auth.requestAuthenticationOrReturnTrue() + ) { + // do "nothing"/ request authentication + } else { + when (user.type) { + UserType.Child -> handlers.openManageChildScreen(childId = user.id) + UserType.Parent -> handlers.openManageParentScreen(parentId = user.id) + }.let { } + } } override fun onAddDeviceClicked() { diff --git a/app/src/main/res/layout/fragment_manage_child_advanced.xml b/app/src/main/res/layout/fragment_manage_child_advanced.xml index 330aab8..baaf276 100644 --- a/app/src/main/res/layout/fragment_manage_child_advanced.xml +++ b/app/src/main/res/layout/fragment_manage_child_advanced.xml @@ -98,6 +98,9 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings-limit-user-viewing.xml b/app/src/main/res/values-de/strings-limit-user-viewing.xml new file mode 100644 index 0000000..e8f7daf --- /dev/null +++ b/app/src/main/res/values-de/strings-limit-user-viewing.xml @@ -0,0 +1,24 @@ + + + + Nutzersichtbarkeit einschränken + Nur dem Benutzer selbst und den Eltern erlauben, die Benutzerdetails anzuzeigen + Damit kann verhindert werden, dass andere Kinder der Familie + die Einstellungen und Nutzungsdauern dieses Kindes sehen können. Das ist nicht als + Sicherheitsfunktion gedacht und leicht umgehbar, z.B. mit der Nutzung einer älteren TimeLimit- + Version. + + \ No newline at end of file diff --git a/app/src/main/res/values/strings-limit-user-viewing.xml b/app/src/main/res/values/strings-limit-user-viewing.xml new file mode 100644 index 0000000..6cff4d0 --- /dev/null +++ b/app/src/main/res/values/strings-limit-user-viewing.xml @@ -0,0 +1,23 @@ + + + + Restrict viewing the user + Only allow parents and the user itself to view its details + This allows preventing other child users to + view the configuration and used times of this child user. This is not intended as security feature + and it is not one - using an older version is enough to circumvent this. + + \ No newline at end of file