mirror of
https://codeberg.org/timelimit/timelimit-android.git
synced 2025-10-06 03:50:23 +02:00
Add time lock for parent users
This allows restricting the times at which a parent can sign in
This commit is contained in:
parent
eac11f92e7
commit
24710aa21e
31 changed files with 1747 additions and 217 deletions
803
app/schemas/io.timelimit.android.data.RoomDatabase/22.json
Normal file
803
app/schemas/io.timelimit.android.data.RoomDatabase/22.json
Normal file
|
@ -0,0 +1,803 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 22,
|
||||
"identityHash": "016e1fa3596a77d5e527c3b3999f9a66",
|
||||
"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, 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
|
||||
}
|
||||
],
|
||||
"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 `index_app_device_id` ON `${TABLE_NAME}` (`device_id`)"
|
||||
},
|
||||
{
|
||||
"name": "index_app_package_name",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"package_name"
|
||||
],
|
||||
"createSql": "CREATE INDEX `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 `index_category_app_category_id` ON `${TABLE_NAME}` (`category_id`)"
|
||||
},
|
||||
{
|
||||
"name": "index_category_app_package_name",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"package_name"
|
||||
],
|
||||
"createSql": "CREATE INDEX `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, `temporarily_blocked` 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, 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": "temporarilyBlocked",
|
||||
"columnName": "temporarily_blocked",
|
||||
"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
|
||||
}
|
||||
],
|
||||
"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, PRIMARY KEY(`category_id`, `day_of_epoch`))",
|
||||
"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
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"category_id",
|
||||
"day_of_epoch"
|
||||
],
|
||||
"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, 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
|
||||
}
|
||||
],
|
||||
"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 `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": []
|
||||
}
|
||||
],
|
||||
"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, \"016e1fa3596a77d5e527c3b3999f9a66\")"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -153,4 +153,10 @@ object DatabaseMigrations {
|
|||
database.execSQL("ALTER TABLE `device` ADD COLUMN `had_manipulation_flags` INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V22 = object: Migration(21, 22) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE `user` ADD COLUMN `blocked_times` TEXT NOT NULL DEFAULT \"\"")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ import io.timelimit.android.data.model.*
|
|||
AppActivity::class,
|
||||
Notification::class,
|
||||
AllowedContact::class
|
||||
], version = 21)
|
||||
], version = 22)
|
||||
abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database {
|
||||
companion object {
|
||||
private val lock = Object()
|
||||
|
@ -90,7 +90,8 @@ abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database
|
|||
DatabaseMigrations.MIGRATE_TO_V18,
|
||||
DatabaseMigrations.MIGRATE_TO_V19,
|
||||
DatabaseMigrations.MIGRATE_TO_V20,
|
||||
DatabaseMigrations.MIGRATE_TO_V21
|
||||
DatabaseMigrations.MIGRATE_TO_V21,
|
||||
DatabaseMigrations.MIGRATE_TO_V22
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ import io.timelimit.android.data.JsonSerializable
|
|||
import io.timelimit.android.data.customtypes.ImmutableBitmask
|
||||
import io.timelimit.android.data.customtypes.ImmutableBitmaskAdapter
|
||||
import io.timelimit.android.data.customtypes.ImmutableBitmaskJson
|
||||
import java.util.*
|
||||
|
||||
@Entity(tableName = "category")
|
||||
@TypeConverters(ImmutableBitmaskAdapter::class)
|
||||
|
@ -177,4 +178,24 @@ object CategoryTimeWarnings {
|
|||
)
|
||||
|
||||
val durations = durationToBitIndex.keys
|
||||
}
|
||||
|
||||
fun ImmutableBitmask.withConfigCopiedToOtherDates(sourceDay: Int, targetDays: Set<Int>): ImmutableBitmask {
|
||||
val result = dataNotToModify.clone() as BitSet
|
||||
|
||||
val configForSelectedDay = result.get(
|
||||
sourceDay * Category.MINUTES_PER_DAY,
|
||||
(sourceDay + 1) * Category.MINUTES_PER_DAY
|
||||
)
|
||||
|
||||
// update all days
|
||||
targetDays.forEach { day ->
|
||||
val startWriteIndex = day * Category.MINUTES_PER_DAY
|
||||
|
||||
for (i in 0..(Category.MINUTES_PER_DAY - 1)) {
|
||||
result[startWriteIndex + i] = configForSelectedDay[i]
|
||||
}
|
||||
}
|
||||
|
||||
return ImmutableBitmask(result)
|
||||
}
|
|
@ -20,10 +20,17 @@ import android.util.JsonWriter
|
|||
import androidx.room.*
|
||||
import io.timelimit.android.data.IdGenerator
|
||||
import io.timelimit.android.data.JsonSerializable
|
||||
import io.timelimit.android.data.customtypes.ImmutableBitmask
|
||||
import io.timelimit.android.data.customtypes.ImmutableBitmaskAdapter
|
||||
import io.timelimit.android.data.customtypes.ImmutableBitmaskJson
|
||||
import io.timelimit.android.util.parseJsonArray
|
||||
import java.util.*
|
||||
|
||||
@Entity(tableName = "user")
|
||||
@TypeConverters(UserTypeConverter::class)
|
||||
@TypeConverters(
|
||||
UserTypeConverter::class,
|
||||
ImmutableBitmaskAdapter::class
|
||||
)
|
||||
data class User(
|
||||
@PrimaryKey
|
||||
@ColumnInfo(name = "id")
|
||||
|
@ -53,7 +60,9 @@ data class User(
|
|||
@ColumnInfo(name = "relax_primary_device")
|
||||
val relaxPrimaryDevice: Boolean,
|
||||
@ColumnInfo(name = "mail_notification_flags")
|
||||
val mailNotificationFlags: Int
|
||||
val mailNotificationFlags: Int,
|
||||
@ColumnInfo(name = "blocked_times")
|
||||
val blockedTimes: ImmutableBitmask
|
||||
): JsonSerializable {
|
||||
companion object {
|
||||
private const val ID = "id"
|
||||
|
@ -68,6 +77,7 @@ data class User(
|
|||
private const val CATEGORY_FOR_NOT_ASSIGNED_APPS = "categoryForNotAssignedApps"
|
||||
private const val RELAX_PRIMARY_DEVICE = "relaxPrimaryDevice"
|
||||
private const val MAIL_NOTIFICATION_FLAGS = "mailNotificationFlags"
|
||||
private const val BLOCKED_TIMES = "blockedTimes"
|
||||
|
||||
fun parse(reader: JsonReader): User {
|
||||
var id: String? = null
|
||||
|
@ -82,6 +92,7 @@ data class User(
|
|||
var categoryForNotAssignedApps = ""
|
||||
var relaxPrimaryDevice = false
|
||||
var mailNotificationFlags = 0
|
||||
var blockedTimes = ImmutableBitmask(BitSet())
|
||||
|
||||
reader.beginObject()
|
||||
while (reader.hasNext()) {
|
||||
|
@ -98,6 +109,7 @@ data class User(
|
|||
CATEGORY_FOR_NOT_ASSIGNED_APPS -> categoryForNotAssignedApps = reader.nextString()
|
||||
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)
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
|
@ -115,7 +127,8 @@ data class User(
|
|||
currentDevice = currentDevice!!,
|
||||
categoryForNotAssignedApps = categoryForNotAssignedApps,
|
||||
relaxPrimaryDevice = relaxPrimaryDevice,
|
||||
mailNotificationFlags = mailNotificationFlags
|
||||
mailNotificationFlags = mailNotificationFlags,
|
||||
blockedTimes = blockedTimes
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -161,6 +174,7 @@ data class User(
|
|||
writer.name(CATEGORY_FOR_NOT_ASSIGNED_APPS).value(categoryForNotAssignedApps)
|
||||
writer.name(RELAX_PRIMARY_DEVICE).value(relaxPrimaryDevice)
|
||||
writer.name(MAIL_NOTIFICATION_FLAGS).value(mailNotificationFlags)
|
||||
writer.name(BLOCKED_TIMES).value(ImmutableBitmaskJson.serialize(blockedTimes))
|
||||
|
||||
writer.endObject()
|
||||
}
|
||||
|
|
|
@ -127,7 +127,8 @@ class AppSetupLogic(private val appLogic: AppLogic) {
|
|||
currentDevice = "",
|
||||
categoryForNotAssignedApps = "",
|
||||
relaxPrimaryDevice = false,
|
||||
mailNotificationFlags = 0
|
||||
mailNotificationFlags = 0,
|
||||
blockedTimes = ImmutableBitmask(BitSet())
|
||||
)
|
||||
|
||||
appLogic.database.user().addUserSync(child)
|
||||
|
@ -148,7 +149,8 @@ class AppSetupLogic(private val appLogic: AppLogic) {
|
|||
currentDevice = "",
|
||||
categoryForNotAssignedApps = "",
|
||||
relaxPrimaryDevice = false,
|
||||
mailNotificationFlags = 0
|
||||
mailNotificationFlags = 0,
|
||||
blockedTimes = ImmutableBitmask(BitSet())
|
||||
)
|
||||
|
||||
appLogic.database.user().addUserSync(parent)
|
||||
|
|
|
@ -278,7 +278,7 @@ class BlockingReasonUtil(private val appLogic: AppLogic) {
|
|||
Log.d(LOG_TAG, "step 5")
|
||||
}
|
||||
|
||||
return Transformations.switchMap(getTrustedMinuteOfWeekLive(appLogic.timeApi, timeZone)) {
|
||||
return Transformations.switchMap(getTrustedMinuteOfWeekLive(timeZone)) {
|
||||
trustedMinuteOfWeek ->
|
||||
|
||||
if (category.blockedMinutesInWeek.dataNotToModify.isEmpty) {
|
||||
|
@ -298,7 +298,7 @@ class BlockingReasonUtil(private val appLogic: AppLogic) {
|
|||
Log.d(LOG_TAG, "step 6")
|
||||
}
|
||||
|
||||
return getTrustedDateLive(appLogic.timeApi, timeZone).switchMap {
|
||||
return getTrustedDateLive(timeZone).switchMap {
|
||||
nowTrustedDate ->
|
||||
|
||||
appLogic.database.timeLimitRules().getTimeLimitRulesByCategory(category.id).switchMap {
|
||||
|
@ -371,7 +371,7 @@ class BlockingReasonUtil(private val appLogic: AppLogic) {
|
|||
}
|
||||
}
|
||||
|
||||
private fun getTrustedMinuteOfWeekLive(api: TimeApi, timeZone: TimeZone): LiveData<Int?> {
|
||||
fun getTrustedMinuteOfWeekLive(timeZone: TimeZone): LiveData<Int?> {
|
||||
val realTime = RealTime.newInstance()
|
||||
|
||||
return object: LiveData<Int?>() {
|
||||
|
@ -395,11 +395,11 @@ class BlockingReasonUtil(private val appLogic: AppLogic) {
|
|||
}
|
||||
|
||||
fun scheduleUpdate() {
|
||||
api.runDelayed(scheduledUpdateRunnable, 1000L /* every second */)
|
||||
appLogic.timeApi.runDelayed(scheduledUpdateRunnable, 1000L /* every second */)
|
||||
}
|
||||
|
||||
fun cancelScheduledUpdate() {
|
||||
api.cancelScheduledAction(scheduledUpdateRunnable)
|
||||
appLogic.timeApi.cancelScheduledAction(scheduledUpdateRunnable)
|
||||
}
|
||||
|
||||
override fun onActive() {
|
||||
|
@ -417,7 +417,7 @@ class BlockingReasonUtil(private val appLogic: AppLogic) {
|
|||
}.ignoreUnchanged()
|
||||
}
|
||||
|
||||
private fun getTrustedDateLive(api: TimeApi, timeZone: TimeZone): LiveData<DateInTimezone?> {
|
||||
private fun getTrustedDateLive(timeZone: TimeZone): LiveData<DateInTimezone?> {
|
||||
val realTime = RealTime.newInstance()
|
||||
|
||||
return object: LiveData<DateInTimezone?>() {
|
||||
|
@ -441,11 +441,11 @@ class BlockingReasonUtil(private val appLogic: AppLogic) {
|
|||
}
|
||||
|
||||
fun scheduleUpdate() {
|
||||
api.runDelayed(scheduledUpdateRunnable, 1000L /* every second */)
|
||||
appLogic.timeApi.runDelayed(scheduledUpdateRunnable, 1000L /* every second */)
|
||||
}
|
||||
|
||||
fun cancelScheduledUpdate() {
|
||||
api.cancelScheduledAction(scheduledUpdateRunnable)
|
||||
appLogic.timeApi.cancelScheduledAction(scheduledUpdateRunnable)
|
||||
}
|
||||
|
||||
override fun onActive() {
|
||||
|
|
|
@ -1475,6 +1475,48 @@ data class RenameChildAction(val childId: String, val newName: String): ParentAc
|
|||
}
|
||||
}
|
||||
|
||||
data class UpdateParentBlockedTimesAction(val parentId: String, val blockedTimes: ImmutableBitmask): ParentAction() {
|
||||
companion object {
|
||||
const val TYPE_VALUE = "UPDATE_PARENT_BLOCKED_TIMES"
|
||||
private const val PARENT_ID = "parentId"
|
||||
private const val BLOCKED_TIMES = "times"
|
||||
}
|
||||
|
||||
init {
|
||||
IdGenerator.assertIdValid(parentId)
|
||||
}
|
||||
|
||||
override fun serialize(writer: JsonWriter) {
|
||||
writer.beginObject()
|
||||
|
||||
writer.name(TYPE).value(TYPE_VALUE)
|
||||
writer.name(PARENT_ID).value(parentId)
|
||||
writer.name(BLOCKED_TIMES).value(ImmutableBitmaskJson.serialize(blockedTimes))
|
||||
|
||||
writer.endObject()
|
||||
}
|
||||
}
|
||||
|
||||
data class ResetParentBlockedTimesAction(val parentId: String): ParentAction() {
|
||||
companion object {
|
||||
const val TYPE_VALUE = "RESET_PARENT_BLOCKED_TIMES"
|
||||
private const val PARENT_ID = "parentId"
|
||||
}
|
||||
|
||||
init {
|
||||
IdGenerator.assertIdValid(parentId)
|
||||
}
|
||||
|
||||
override fun serialize(writer: JsonWriter) {
|
||||
writer.beginObject()
|
||||
|
||||
writer.name(TYPE).value(TYPE_VALUE)
|
||||
writer.name(PARENT_ID).value(parentId)
|
||||
|
||||
writer.endObject()
|
||||
}
|
||||
}
|
||||
|
||||
// child actions
|
||||
object ChildSignInAction: ChildAction() {
|
||||
private const val TYPE_VALUE = "CHILD_SIGN_IN"
|
||||
|
|
|
@ -153,7 +153,8 @@ object LocalDatabaseParentActionDispatcher {
|
|||
currentDevice = "",
|
||||
categoryForNotAssignedApps = "",
|
||||
relaxPrimaryDevice = false,
|
||||
mailNotificationFlags = 0
|
||||
mailNotificationFlags = 0,
|
||||
blockedTimes = ImmutableBitmask(BitSet())
|
||||
))
|
||||
}
|
||||
is UpdateCategoryBlockedTimesAction -> {
|
||||
|
@ -512,6 +513,32 @@ object LocalDatabaseParentActionDispatcher {
|
|||
|
||||
null
|
||||
}
|
||||
is UpdateParentBlockedTimesAction -> {
|
||||
val userEntry = database.user().getUserByIdSync(action.parentId)
|
||||
|
||||
if (userEntry?.type != UserType.Parent) {
|
||||
throw IllegalArgumentException("no valid parent id")
|
||||
}
|
||||
|
||||
database.user().updateUserSync(
|
||||
userEntry.copy(
|
||||
blockedTimes = action.blockedTimes
|
||||
)
|
||||
)
|
||||
}
|
||||
is ResetParentBlockedTimesAction -> {
|
||||
val userEntry = database.user().getUserByIdSync(action.parentId)
|
||||
|
||||
if (userEntry?.type != UserType.Parent) {
|
||||
throw IllegalArgumentException("no valid parent id")
|
||||
}
|
||||
|
||||
database.user().updateUserSync(
|
||||
userEntry.copy(
|
||||
blockedTimes = ImmutableBitmask(BitSet())
|
||||
)
|
||||
)
|
||||
}
|
||||
}.let { }
|
||||
|
||||
database.setTransactionSuccessful()
|
||||
|
|
|
@ -28,6 +28,7 @@ import io.timelimit.android.crypto.PasswordHashing
|
|||
import io.timelimit.android.data.model.User
|
||||
import io.timelimit.android.data.model.UserType
|
||||
import io.timelimit.android.livedata.*
|
||||
import io.timelimit.android.logic.BlockingReasonUtil
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
import io.timelimit.android.sync.actions.ChildSignInAction
|
||||
import io.timelimit.android.sync.actions.SetDeviceUserAction
|
||||
|
@ -38,10 +39,12 @@ import io.timelimit.android.ui.main.ActivityViewModel
|
|||
import io.timelimit.android.ui.main.AuthenticatedUser
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.util.*
|
||||
|
||||
class LoginDialogFragmentModel(application: Application): AndroidViewModel(application) {
|
||||
val selectedUserId = MutableLiveData<String?>().apply { value = null }
|
||||
private val logic = DefaultAppLogic.with(application)
|
||||
private val blockingReasonUtil = BlockingReasonUtil(logic)
|
||||
private val users = logic.database.user().getAllUsersLive()
|
||||
private val isConnectedMode = logic.fullVersion.isLocalMode.invert()
|
||||
private val selectedUser = users.switchMap { users ->
|
||||
|
@ -49,6 +52,7 @@ class LoginDialogFragmentModel(application: Application): AndroidViewModel(appli
|
|||
users.find { it.id == userId }
|
||||
}
|
||||
}
|
||||
private val trustedTime = selectedUser.switchMap { blockingReasonUtil.getTrustedMinuteOfWeekLive(TimeZone.getTimeZone(it?.timeZone ?: "GMT")) }
|
||||
private val currentDeviceUser = logic.deviceUserId
|
||||
private val isCheckingPassword = MutableLiveData<Boolean>().apply { value = false }
|
||||
private val wasPasswordWrong = MutableLiveData<Boolean>().apply { value = false }
|
||||
|
@ -63,8 +67,7 @@ class LoginDialogFragmentModel(application: Application): AndroidViewModel(appli
|
|||
when (selectedUser?.type) {
|
||||
UserType.Parent -> {
|
||||
val isAlreadyCurrentUser = currentDeviceUser.map { it == selectedUser.id }.ignoreUnchanged()
|
||||
|
||||
isConnectedMode.switchMap { isConnectedMode ->
|
||||
val loginScreen = isConnectedMode.switchMap { isConnectedMode ->
|
||||
isAlreadyCurrentUser.switchMap { isAlreadyCurrentUser ->
|
||||
isCheckingPassword.switchMap { isCheckingPassword ->
|
||||
wasPasswordWrong.map { wasPasswordWrong ->
|
||||
|
@ -78,6 +81,26 @@ class LoginDialogFragmentModel(application: Application): AndroidViewModel(appli
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedUser.blockedTimes.dataNotToModify.isEmpty) {
|
||||
loginScreen
|
||||
} else {
|
||||
logic.fullVersion.shouldProvideFullVersionFunctions.switchMap { hasPremium ->
|
||||
if (hasPremium) {
|
||||
trustedTime.switchMap { time ->
|
||||
if (time == null) {
|
||||
liveDataFromValue(ParentUserLoginMissingTrustedTime as LoginDialogStatus)
|
||||
} else if (selectedUser.blockedTimes.dataNotToModify[time]) {
|
||||
liveDataFromValue(ParentUserLoginBlockedTime as LoginDialogStatus)
|
||||
} else {
|
||||
loginScreen
|
||||
}
|
||||
}
|
||||
} else {
|
||||
loginScreen
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
UserType.Child -> {
|
||||
logic.fullVersion.shouldProvideFullVersionFunctions.switchMap { fullversion ->
|
||||
|
@ -265,6 +288,8 @@ class LoginDialogFragmentModel(application: Application): AndroidViewModel(appli
|
|||
|
||||
sealed class LoginDialogStatus
|
||||
data class UserListLoginDialogStatus(val usersToShow: List<User>): LoginDialogStatus()
|
||||
object ParentUserLoginMissingTrustedTime: LoginDialogStatus()
|
||||
object ParentUserLoginBlockedTime: LoginDialogStatus()
|
||||
data class ParentUserLogin(
|
||||
val isConnectedMode: Boolean,
|
||||
val isAlreadyCurrentDeviceUser: Boolean,
|
||||
|
|
|
@ -44,6 +44,8 @@ class NewLoginFragment: DialogFragment() {
|
|||
private const val CHILD_ALREADY_CURRENT_USER = 3
|
||||
private const val CHILD_AUTH = 4
|
||||
private const val CHILD_LOGIN_REQUIRES_PREMIUM = 5
|
||||
private const val BLOCKED_LOGIN_TIME = 6
|
||||
private const val UNVERIFIED_TIME = 7
|
||||
}
|
||||
|
||||
private val model: LoginDialogFragmentModel by lazy {
|
||||
|
@ -200,6 +202,24 @@ class NewLoginFragment: DialogFragment() {
|
|||
|
||||
null
|
||||
}
|
||||
ParentUserLoginMissingTrustedTime -> {
|
||||
if (binding.switcher.displayedChild != UNVERIFIED_TIME) {
|
||||
binding.switcher.setInAnimation(context!!, R.anim.wizard_open_step_in)
|
||||
binding.switcher.setOutAnimation(context!!, R.anim.wizard_open_step_out)
|
||||
binding.switcher.displayedChild = UNVERIFIED_TIME
|
||||
}
|
||||
|
||||
null
|
||||
}
|
||||
ParentUserLoginBlockedTime -> {
|
||||
if (binding.switcher.displayedChild != BLOCKED_LOGIN_TIME) {
|
||||
binding.switcher.setInAnimation(context!!, R.anim.wizard_open_step_in)
|
||||
binding.switcher.setOutAnimation(context!!, R.anim.wizard_open_step_out)
|
||||
binding.switcher.displayedChild = BLOCKED_LOGIN_TIME
|
||||
}
|
||||
|
||||
null
|
||||
}
|
||||
is CanNotSignInChildHasNoPassword -> {
|
||||
if (binding.switcher.displayedChild != CHILD_MISSING_PASSWORD) {
|
||||
binding.switcher.setInAnimation(context!!, R.anim.wizard_open_step_in)
|
||||
|
|
|
@ -19,29 +19,24 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.data.Database
|
||||
import io.timelimit.android.data.customtypes.ImmutableBitmask
|
||||
import io.timelimit.android.data.model.Category
|
||||
import io.timelimit.android.data.model.withConfigCopiedToOtherDates
|
||||
import io.timelimit.android.livedata.map
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
import io.timelimit.android.sync.actions.UpdateCategoryBlockedTimesAction
|
||||
import io.timelimit.android.ui.main.ActivityViewModel
|
||||
import io.timelimit.android.ui.main.getActivityViewModel
|
||||
import io.timelimit.android.ui.manage.category.ManageCategoryFragmentArgs
|
||||
import io.timelimit.android.ui.manage.category.blocked_times.copy.CopyBlockedTimeAreasDialogFragment
|
||||
import kotlinx.android.synthetic.main.fragment_blocked_time_areas.*
|
||||
import java.util.*
|
||||
|
||||
class BlockedTimeAreasFragment : Fragment() {
|
||||
class BlockedTimeAreasFragment : Fragment(), CopyBlockedTimeAreasDialogFragmentListener {
|
||||
companion object {
|
||||
fun newInstance(params: ManageCategoryFragmentArgs): BlockedTimeAreasFragment {
|
||||
val result = BlockedTimeAreasFragment()
|
||||
|
@ -66,12 +61,12 @@ class BlockedTimeAreasFragment : Fragment() {
|
|||
return inflater.inflate(R.layout.fragment_blocked_time_areas, container, false)
|
||||
}
|
||||
|
||||
fun updateBlockedTimes(oldMask: BitSet, newMask: BitSet) {
|
||||
fun updateBlockedTimes(oldMask: ImmutableBitmask, newMask: ImmutableBitmask) {
|
||||
if (
|
||||
auth.tryDispatchParentAction(
|
||||
UpdateCategoryBlockedTimesAction(
|
||||
categoryId = params.categoryId,
|
||||
blockedTimes = ImmutableBitmask(newMask)
|
||||
blockedTimes = newMask
|
||||
)
|
||||
)
|
||||
) {
|
||||
|
@ -80,7 +75,7 @@ class BlockedTimeAreasFragment : Fragment() {
|
|||
auth.tryDispatchParentAction(
|
||||
UpdateCategoryBlockedTimesAction(
|
||||
categoryId = params.categoryId,
|
||||
blockedTimes = ImmutableBitmask(oldMask)
|
||||
blockedTimes = oldMask
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -88,168 +83,33 @@ class BlockedTimeAreasFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onCopyBlockedTimeAreasConfirmed(sourceDay: Int, targetDays: Set<Int>) {
|
||||
category.value?.blockedMinutesInWeek?.let { current ->
|
||||
updateBlockedTimes(current, current.withConfigCopiedToOtherDates(sourceDay, targetDays))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val layoutManager = GridLayoutManager(context, items.value!!.recommendColumns)
|
||||
layoutManager.spanSizeLookup = SpanSizeLookup(items.value!!)
|
||||
|
||||
val adapter = Adapter(items.value!!)
|
||||
|
||||
items.observe(this, Observer {
|
||||
layoutManager.spanCount = it.recommendColumns
|
||||
layoutManager.spanSizeLookup = SpanSizeLookup(it)
|
||||
adapter.items = it
|
||||
})
|
||||
|
||||
recycler.adapter = adapter
|
||||
recycler.layoutManager = layoutManager
|
||||
|
||||
category.observe(this, Observer { adapter.blockedTimeAreas = it?.blockedMinutesInWeek?.dataNotToModify })
|
||||
|
||||
btn_help.setOnClickListener {
|
||||
BlockedTimeAreasHelpDialog().show(fragmentManager!!)
|
||||
}
|
||||
|
||||
btn_copy_to_other_days.setOnClickListener {
|
||||
if (auth.requestAuthenticationOrReturnTrue()) {
|
||||
CopyBlockedTimeAreasDialogFragment.newInstance(params).apply {
|
||||
setTargetFragment(this@BlockedTimeAreasFragment, 0)
|
||||
}.show(fragmentManager!!)
|
||||
CopyBlockedTimeAreasDialogFragment.newInstance(this@BlockedTimeAreasFragment).show(fragmentManager!!)
|
||||
}
|
||||
}
|
||||
|
||||
adapter.handlers = object: Handlers {
|
||||
override fun onMinuteTileClick(time: MinuteTile) {
|
||||
if (auth.requestAuthenticationOrReturnTrue()) {
|
||||
val selectedMinuteOfWeek = adapter.selectedMinuteOfWeek
|
||||
val blockedTimeAreas = adapter.blockedTimeAreas
|
||||
|
||||
if (blockedTimeAreas == null) {
|
||||
// nothing to work with
|
||||
} else if (selectedMinuteOfWeek == null) {
|
||||
adapter.selectedMinuteOfWeek = time.minuteOfWeek
|
||||
} else if (selectedMinuteOfWeek == time.minuteOfWeek) {
|
||||
adapter.selectedMinuteOfWeek = null
|
||||
|
||||
val newBlockMask = blockedTimeAreas.clone() as BitSet
|
||||
newBlockMask.set(
|
||||
selectedMinuteOfWeek,
|
||||
selectedMinuteOfWeek + items.value!!.minutesPerTile,
|
||||
!newBlockMask[selectedMinuteOfWeek]
|
||||
)
|
||||
|
||||
updateBlockedTimes(blockedTimeAreas, newBlockMask)
|
||||
} else {
|
||||
var times = selectedMinuteOfWeek to time.minuteOfWeek
|
||||
adapter.selectedMinuteOfWeek = null
|
||||
|
||||
// sort selected times
|
||||
if (times.first > times.second) {
|
||||
times = times.second to times.first
|
||||
}
|
||||
|
||||
// mark until the end
|
||||
times = times.first to (times.second + items.value!!.minutesPerTile - 1)
|
||||
|
||||
// get majority of current value
|
||||
var allowed = 0
|
||||
var blocked = 0
|
||||
|
||||
for (i in times.first..times.second) {
|
||||
if (blockedTimeAreas[i]) {
|
||||
blocked++
|
||||
} else {
|
||||
allowed++
|
||||
}
|
||||
}
|
||||
|
||||
val isMajorityBlocked = blocked > allowed
|
||||
val shouldBlock = !isMajorityBlocked
|
||||
|
||||
val newBlockMask = blockedTimeAreas.clone() as BitSet
|
||||
newBlockMask.set(times.first, times.second + 1, shouldBlock)
|
||||
|
||||
updateBlockedTimes(blockedTimeAreas, newBlockMask)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
run {
|
||||
val spinnerAdapter = ArrayAdapter.createFromResource(context!!, R.array.days_of_week_array, android.R.layout.simple_spinner_item)
|
||||
spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
spinner_day.adapter = spinnerAdapter
|
||||
spinner_day.onItemSelectedListener = object: AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||
val selectedDay = items.value!!.getDayOfPosition(
|
||||
layoutManager.findFirstVisibleItemPosition()
|
||||
)
|
||||
|
||||
if (selectedDay != position) {
|
||||
layoutManager.scrollToPositionWithOffset(
|
||||
items.value!!.getPositionOfItem(
|
||||
DayHeader(position)
|
||||
),
|
||||
0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
recycler.addOnScrollListener(object: RecyclerView.OnScrollListener() {
|
||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||
super.onScrollStateChanged(recyclerView, newState)
|
||||
|
||||
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
|
||||
items.value?.let { items ->
|
||||
try {
|
||||
val selectedDay = items.getDayOfPosition(
|
||||
layoutManager.findFirstVisibleItemPosition()
|
||||
)
|
||||
|
||||
if (selectedDay != spinner_day.selectedItemPosition) {
|
||||
spinner_day.setSelection(selectedDay, true)
|
||||
}
|
||||
} catch (ex: IllegalStateException) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// bind detailed mode
|
||||
items.value = when (detailed_mode.isChecked) {
|
||||
true -> MinuteOfWeekItems
|
||||
false -> FifteenMinutesOfWeekItems
|
||||
}
|
||||
|
||||
detailed_mode.setOnCheckedChangeListener { _, isChecked ->
|
||||
val oldValue = items.value
|
||||
val newValue = when (isChecked) {
|
||||
true -> MinuteOfWeekItems
|
||||
false -> FifteenMinutesOfWeekItems
|
||||
}
|
||||
|
||||
if (oldValue != newValue) {
|
||||
val currentlyVisiblePosition = layoutManager.findFirstVisibleItemPosition()
|
||||
|
||||
if (currentlyVisiblePosition == RecyclerView.NO_POSITION) {
|
||||
items.value = newValue
|
||||
} else {
|
||||
val currentlyVisibleItem = oldValue!!.getItemAtPosition(currentlyVisiblePosition)
|
||||
val newVisiblePosition = newValue.getPositionOfItem(currentlyVisibleItem)
|
||||
|
||||
items.value = newValue
|
||||
layoutManager.scrollToPositionWithOffset(newVisiblePosition, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
BlockedTimeAreasLogic.init(
|
||||
recycler = recycler,
|
||||
daySpinner = spinner_day,
|
||||
detailedModeCheckbox = detailed_mode,
|
||||
requestAuthenticationOrReturnTrue = { auth.requestAuthenticationOrReturnTrue() },
|
||||
updateBlockedTimes = { a, b -> updateBlockedTimes(a, b) },
|
||||
currentData = category.map { it?.blockedMinutesInWeek },
|
||||
lifecycleOwner = this
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,16 +24,34 @@ import androidx.fragment.app.FragmentManager
|
|||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.extensions.showSafe
|
||||
import kotlinx.android.synthetic.main.fragment_blocked_time_areas_help_dialog.*
|
||||
|
||||
class BlockedTimeAreasHelpDialog : BottomSheetDialogFragment() {
|
||||
companion object {
|
||||
private const val DIALOG_TAG = "r"
|
||||
private const val FOR_USER = "forUser"
|
||||
|
||||
fun newInstance(forUser: Boolean) = BlockedTimeAreasHelpDialog().apply {
|
||||
arguments = Bundle().apply {
|
||||
putBoolean(FOR_USER, forUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_blocked_time_areas_help_dialog, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val forUser = arguments?.getBoolean(FOR_USER, false)
|
||||
|
||||
if (forUser == true) {
|
||||
text1.setText(R.string.manage_parent_blocked_times_description)
|
||||
}
|
||||
}
|
||||
|
||||
fun show(manager: FragmentManager) {
|
||||
showSafe(manager, DIALOG_TAG)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,196 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.ui.manage.category.blocked_times
|
||||
|
||||
import android.view.View
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.CheckBox
|
||||
import android.widget.Spinner
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.data.customtypes.ImmutableBitmask
|
||||
import java.util.*
|
||||
|
||||
object BlockedTimeAreasLogic {
|
||||
fun init(
|
||||
recycler: RecyclerView,
|
||||
daySpinner: Spinner,
|
||||
detailedModeCheckbox: CheckBox,
|
||||
requestAuthenticationOrReturnTrue: () -> Boolean,
|
||||
updateBlockedTimes: (ImmutableBitmask, ImmutableBitmask) -> Unit,
|
||||
currentData: LiveData<ImmutableBitmask?>,
|
||||
lifecycleOwner: LifecycleOwner
|
||||
) {
|
||||
val context = recycler.context!!
|
||||
|
||||
val items = MutableLiveData<BlockedTimeItems>().apply { value = FifteenMinutesOfWeekItems }
|
||||
val layoutManager = GridLayoutManager(context, items.value!!.recommendColumns)
|
||||
layoutManager.spanSizeLookup = SpanSizeLookup(items.value!!)
|
||||
|
||||
val adapter = Adapter(items.value!!)
|
||||
|
||||
items.observe(lifecycleOwner, Observer {
|
||||
layoutManager.spanCount = it.recommendColumns
|
||||
layoutManager.spanSizeLookup = SpanSizeLookup(it)
|
||||
adapter.items = it
|
||||
})
|
||||
|
||||
recycler.adapter = adapter
|
||||
recycler.layoutManager = layoutManager
|
||||
|
||||
adapter.handlers = object: Handlers {
|
||||
override fun onMinuteTileClick(time: MinuteTile) {
|
||||
if (requestAuthenticationOrReturnTrue()) {
|
||||
val selectedMinuteOfWeek = adapter.selectedMinuteOfWeek
|
||||
val blockedTimeAreas = adapter.blockedTimeAreas
|
||||
|
||||
if (blockedTimeAreas == null) {
|
||||
// nothing to work with
|
||||
} else if (selectedMinuteOfWeek == null) {
|
||||
adapter.selectedMinuteOfWeek = time.minuteOfWeek
|
||||
} else if (selectedMinuteOfWeek == time.minuteOfWeek) {
|
||||
adapter.selectedMinuteOfWeek = null
|
||||
|
||||
val newBlockMask = blockedTimeAreas.clone() as BitSet
|
||||
newBlockMask.set(
|
||||
selectedMinuteOfWeek,
|
||||
selectedMinuteOfWeek + items.value!!.minutesPerTile,
|
||||
!newBlockMask[selectedMinuteOfWeek]
|
||||
)
|
||||
|
||||
updateBlockedTimes(ImmutableBitmask(blockedTimeAreas), ImmutableBitmask(newBlockMask))
|
||||
} else {
|
||||
var times = selectedMinuteOfWeek to time.minuteOfWeek
|
||||
adapter.selectedMinuteOfWeek = null
|
||||
|
||||
// sort selected times
|
||||
if (times.first > times.second) {
|
||||
times = times.second to times.first
|
||||
}
|
||||
|
||||
// mark until the end
|
||||
times = times.first to (times.second + items.value!!.minutesPerTile - 1)
|
||||
|
||||
// get majority of current value
|
||||
var allowed = 0
|
||||
var blocked = 0
|
||||
|
||||
for (i in times.first..times.second) {
|
||||
if (blockedTimeAreas[i]) {
|
||||
blocked++
|
||||
} else {
|
||||
allowed++
|
||||
}
|
||||
}
|
||||
|
||||
val isMajorityBlocked = blocked > allowed
|
||||
val shouldBlock = !isMajorityBlocked
|
||||
|
||||
val newBlockMask = blockedTimeAreas.clone() as BitSet
|
||||
newBlockMask.set(times.first, times.second + 1, shouldBlock)
|
||||
|
||||
updateBlockedTimes(ImmutableBitmask(blockedTimeAreas), ImmutableBitmask(newBlockMask))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
run {
|
||||
val spinnerAdapter = ArrayAdapter.createFromResource(context, R.array.days_of_week_array, android.R.layout.simple_spinner_item)
|
||||
spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
daySpinner.adapter = spinnerAdapter
|
||||
daySpinner.onItemSelectedListener = object: AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||
val selectedDay = items.value!!.getDayOfPosition(
|
||||
layoutManager.findFirstVisibleItemPosition()
|
||||
)
|
||||
|
||||
if (selectedDay != position) {
|
||||
layoutManager.scrollToPositionWithOffset(
|
||||
items.value!!.getPositionOfItem(
|
||||
DayHeader(position)
|
||||
),
|
||||
0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
recycler.addOnScrollListener(object: RecyclerView.OnScrollListener() {
|
||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||
super.onScrollStateChanged(recyclerView, newState)
|
||||
|
||||
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
|
||||
items.value?.let { items ->
|
||||
try {
|
||||
val selectedDay = items.getDayOfPosition(
|
||||
layoutManager.findFirstVisibleItemPosition()
|
||||
)
|
||||
|
||||
if (selectedDay != daySpinner.selectedItemPosition) {
|
||||
daySpinner.setSelection(selectedDay, true)
|
||||
}
|
||||
} catch (ex: IllegalStateException) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// bind detailed mode
|
||||
items.value = when (detailedModeCheckbox.isChecked) {
|
||||
true -> MinuteOfWeekItems
|
||||
false -> FifteenMinutesOfWeekItems
|
||||
}
|
||||
|
||||
detailedModeCheckbox.setOnCheckedChangeListener { _, isChecked ->
|
||||
val oldValue = items.value
|
||||
val newValue = when (isChecked) {
|
||||
true -> MinuteOfWeekItems
|
||||
false -> FifteenMinutesOfWeekItems
|
||||
}
|
||||
|
||||
if (oldValue != newValue) {
|
||||
val currentlyVisiblePosition = layoutManager.findFirstVisibleItemPosition()
|
||||
|
||||
if (currentlyVisiblePosition == RecyclerView.NO_POSITION) {
|
||||
items.value = newValue
|
||||
} else {
|
||||
val currentlyVisibleItem = oldValue!!.getItemAtPosition(currentlyVisiblePosition)
|
||||
val newVisiblePosition = newValue.getPositionOfItem(currentlyVisibleItem)
|
||||
|
||||
items.value = newValue
|
||||
layoutManager.scrollToPositionWithOffset(newVisiblePosition, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// loading data
|
||||
currentData.observe(lifecycleOwner, Observer { adapter.blockedTimeAreas = it?.dataNotToModify })
|
||||
}
|
||||
}
|
|
@ -13,7 +13,7 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.ui.manage.category.blocked_times.copy
|
||||
package io.timelimit.android.ui.manage.category.blocked_times
|
||||
|
||||
|
||||
import android.os.Bundle
|
||||
|
@ -21,38 +21,35 @@ import android.view.LayoutInflater
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.CheckedTextView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.Observer
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.coroutines.runAsync
|
||||
import io.timelimit.android.data.model.Category
|
||||
import io.timelimit.android.data.model.UserType
|
||||
import io.timelimit.android.databinding.CopyBlockedTimeAreasDialogFragmentBinding
|
||||
import io.timelimit.android.extensions.showSafe
|
||||
import io.timelimit.android.livedata.waitForNullableValue
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
import io.timelimit.android.ui.main.ActivityViewModel
|
||||
import io.timelimit.android.ui.main.ActivityViewModelHolder
|
||||
import io.timelimit.android.ui.manage.category.ManageCategoryFragmentArgs
|
||||
import io.timelimit.android.ui.manage.category.blocked_times.BlockedTimeAreasFragment
|
||||
import java.util.*
|
||||
|
||||
class CopyBlockedTimeAreasDialogFragment : BottomSheetDialogFragment() {
|
||||
companion object {
|
||||
private const val TAG = "cbtadf"
|
||||
private const val SELECTED_START_DAY = "ssd"
|
||||
|
||||
fun newInstance(params: ManageCategoryFragmentArgs) = CopyBlockedTimeAreasDialogFragment().apply {
|
||||
arguments = params.toBundle()
|
||||
fun newInstance(target: Fragment) = CopyBlockedTimeAreasDialogFragment().apply {
|
||||
setTargetFragment(target, 0)
|
||||
}
|
||||
}
|
||||
|
||||
val params: ManageCategoryFragmentArgs by lazy { ManageCategoryFragmentArgs.fromBundle(arguments!!) }
|
||||
var selectedStartDayIndex = -1
|
||||
val auth: ActivityViewModel by lazy {
|
||||
(activity as ActivityViewModelHolder).getActivityViewModel()
|
||||
}
|
||||
val target: CopyBlockedTimeAreasDialogFragmentListener by lazy {
|
||||
targetFragment as CopyBlockedTimeAreasDialogFragmentListener
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
@ -126,35 +123,19 @@ class CopyBlockedTimeAreasDialogFragment : BottomSheetDialogFragment() {
|
|||
bindSecondPage()
|
||||
|
||||
binding.saveButton.setOnClickListener {
|
||||
val logic = DefaultAppLogic.with(context!!)
|
||||
val targetDays = mutableSetOf<Int>()
|
||||
|
||||
runAsync {
|
||||
val current = logic.database.category().getCategoryByChildIdAndId(params.childId, params.categoryId).waitForNullableValue()
|
||||
?: return@runAsync
|
||||
|
||||
val newBlockedTimes = current.blockedMinutesInWeek.dataNotToModify.clone() as BitSet
|
||||
|
||||
val configForSelectedDay = newBlockedTimes.get(
|
||||
selectedStartDayIndex * Category.MINUTES_PER_DAY,
|
||||
(selectedStartDayIndex + 1) * Category.MINUTES_PER_DAY
|
||||
)
|
||||
|
||||
// update all days
|
||||
dayCheckboxes.forEachIndexed { day, checkBox ->
|
||||
if (checkBox.isChecked) {
|
||||
val startWriteIndex = day * Category.MINUTES_PER_DAY
|
||||
|
||||
for (i in 0..(Category.MINUTES_PER_DAY - 1)) {
|
||||
newBlockedTimes[startWriteIndex + i] = configForSelectedDay[i]
|
||||
}
|
||||
}
|
||||
dayCheckboxes.forEachIndexed { day, checkBox ->
|
||||
if (checkBox.isChecked && day != selectedStartDayIndex) {
|
||||
targetDays.add(day)
|
||||
}
|
||||
|
||||
// apply
|
||||
val target = targetFragment as BlockedTimeAreasFragment
|
||||
target.updateBlockedTimes(current.blockedMinutesInWeek.dataNotToModify, newBlockedTimes)
|
||||
}
|
||||
|
||||
target.onCopyBlockedTimeAreasConfirmed(
|
||||
sourceDay = selectedStartDayIndex,
|
||||
targetDays = targetDays
|
||||
)
|
||||
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
|
||||
|
@ -168,3 +149,7 @@ class CopyBlockedTimeAreasDialogFragment : BottomSheetDialogFragment() {
|
|||
|
||||
fun show(fragmentManager: FragmentManager) = showSafe(fragmentManager, TAG)
|
||||
}
|
||||
|
||||
interface CopyBlockedTimeAreasDialogFragmentListener {
|
||||
fun onCopyBlockedTimeAreasConfirmed(sourceDay: Int, targetDays: Set<Int>)
|
||||
}
|
|
@ -135,6 +135,16 @@ class ManageParentFragment : Fragment(), FragmentWithCustomTitle {
|
|||
R.id.manageParentFragment
|
||||
)
|
||||
}
|
||||
|
||||
override fun onManageBlockedTimesClicked() {
|
||||
navigation.safeNavigate(
|
||||
ManageParentFragmentDirections.
|
||||
actionManageParentFragmentToManageParentBlockedTimesFragment(
|
||||
params.parentId
|
||||
),
|
||||
R.id.manageParentFragment
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return binding.root
|
||||
|
@ -147,4 +157,5 @@ interface ManageParentFragmentHandlers {
|
|||
fun onChangePasswordClicked()
|
||||
fun onRestorePasswordClicked()
|
||||
fun onLinkMailClicked()
|
||||
fun onManageBlockedTimesClicked()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,173 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.ui.manage.parent.blockedtimes
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.LiveData
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.data.customtypes.ImmutableBitmask
|
||||
import io.timelimit.android.data.model.User
|
||||
import io.timelimit.android.data.model.withConfigCopiedToOtherDates
|
||||
import io.timelimit.android.databinding.ManageParentBlockedTimesFragmentBinding
|
||||
import io.timelimit.android.livedata.liveDataFromValue
|
||||
import io.timelimit.android.livedata.map
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
import io.timelimit.android.sync.actions.UpdateParentBlockedTimesAction
|
||||
import io.timelimit.android.ui.main.ActivityViewModel
|
||||
import io.timelimit.android.ui.main.ActivityViewModelHolder
|
||||
import io.timelimit.android.ui.main.AuthenticationFab
|
||||
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
||||
import io.timelimit.android.ui.manage.category.blocked_times.*
|
||||
import kotlinx.android.synthetic.main.fragment_blocked_time_areas.*
|
||||
import java.util.*
|
||||
|
||||
class ManageParentBlockedTimesFragment : Fragment(), FragmentWithCustomTitle, CopyBlockedTimeAreasDialogFragmentListener {
|
||||
companion object {
|
||||
private const val MINUTES_PER_DAY = 60 * 24
|
||||
private const val MAX_BLOCKED_MINUTES_PER_DAY = 60 * 18 + 1
|
||||
}
|
||||
|
||||
private val params: ManageParentBlockedTimesFragmentArgs by lazy {
|
||||
ManageParentBlockedTimesFragmentArgs.fromBundle(arguments!!)
|
||||
}
|
||||
|
||||
private val authActivity: ActivityViewModelHolder by lazy {
|
||||
activity!! as ActivityViewModelHolder
|
||||
}
|
||||
|
||||
private val auth: ActivityViewModel by lazy {
|
||||
authActivity.getActivityViewModel()
|
||||
}
|
||||
|
||||
private val parent: LiveData<User?> by lazy {
|
||||
DefaultAppLogic.with(context!!).database.user().getParentUserByIdLive(params.parentUserId)
|
||||
}
|
||||
|
||||
override fun getCustomTitle(): LiveData<String?> = parent.map { it?.name }
|
||||
|
||||
override fun onCopyBlockedTimeAreasConfirmed(sourceDay: Int, targetDays: Set<Int>) {
|
||||
parent.value?.blockedTimes?.let { current ->
|
||||
updateBlockedTimes(current, current.withConfigCopiedToOtherDates(sourceDay, targetDays))
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateBlockedTimeAreas(newMask: BitSet): Boolean {
|
||||
for (day in 0 until 7) {
|
||||
var blocked = 0
|
||||
|
||||
for (minute in 0 until MINUTES_PER_DAY) {
|
||||
if (newMask[day * MINUTES_PER_DAY + minute]) {
|
||||
blocked++
|
||||
}
|
||||
}
|
||||
|
||||
if (blocked >= MAX_BLOCKED_MINUTES_PER_DAY) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun updateBlockedTimes(oldMask: ImmutableBitmask, newMask: ImmutableBitmask) {
|
||||
if (!validateBlockedTimeAreas(newMask.dataNotToModify)) {
|
||||
Snackbar.make(coordinator, R.string.manage_parent_lockout_hour_rule, Snackbar.LENGTH_LONG).show()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
auth.tryDispatchParentAction(
|
||||
UpdateParentBlockedTimesAction(
|
||||
parentId = params.parentUserId,
|
||||
blockedTimes = newMask
|
||||
)
|
||||
)
|
||||
) {
|
||||
Snackbar.make(coordinator, R.string.blocked_time_areas_snackbar_modified, Snackbar.LENGTH_SHORT)
|
||||
.setAction(R.string.generic_undo) {
|
||||
auth.tryDispatchParentAction(
|
||||
UpdateParentBlockedTimesAction(
|
||||
parentId = params.parentUserId,
|
||||
blockedTimes = oldMask
|
||||
)
|
||||
)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val binding = ManageParentBlockedTimesFragmentBinding.inflate(inflater, container, false)
|
||||
|
||||
// auth button
|
||||
AuthenticationFab.manageAuthenticationFab(
|
||||
fab = binding.fab,
|
||||
fragment = this,
|
||||
shouldHighlight = auth.shouldHighlightAuthenticationButton,
|
||||
authenticatedUser = auth.authenticatedUser,
|
||||
doesSupportAuth = liveDataFromValue(true)
|
||||
)
|
||||
|
||||
binding.fab.setOnClickListener { authActivity.showAuthenticationScreen() }
|
||||
|
||||
// dispatching
|
||||
fun requestAuthenticationOrReturnTrue(): Boolean {
|
||||
if (!auth.requestAuthenticationOrReturnTrue()) {
|
||||
return false
|
||||
}
|
||||
|
||||
val authenticatedUser = auth.authenticatedUser.value?.second?.id ?: return false
|
||||
val targetUser = params.parentUserId
|
||||
|
||||
if (authenticatedUser == targetUser) {
|
||||
return true
|
||||
} else {
|
||||
TryResetParentBlockedTimesDialogFragment.newInstance(parentUserId = params.parentUserId).show(fragmentManager!!)
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// UI
|
||||
binding.btnHelp.setOnClickListener {
|
||||
BlockedTimeAreasHelpDialog.newInstance(forUser = true).show(fragmentManager!!)
|
||||
}
|
||||
|
||||
binding.btnCopyToOtherDays.setOnClickListener {
|
||||
if (requestAuthenticationOrReturnTrue()) {
|
||||
CopyBlockedTimeAreasDialogFragment.newInstance(this@ManageParentBlockedTimesFragment).show(fragmentManager!!)
|
||||
}
|
||||
}
|
||||
|
||||
BlockedTimeAreasLogic.init(
|
||||
recycler = binding.recycler,
|
||||
daySpinner = binding.spinnerDay,
|
||||
detailedModeCheckbox = binding.detailedMode,
|
||||
requestAuthenticationOrReturnTrue = { requestAuthenticationOrReturnTrue() },
|
||||
updateBlockedTimes = { a, b -> updateBlockedTimes(a, b) },
|
||||
currentData = parent.map { it?.blockedTimes },
|
||||
lifecycleOwner = this
|
||||
)
|
||||
|
||||
return binding.root
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.ui.manage.parent.blockedtimes
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.Observer
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.data.model.UserType
|
||||
import io.timelimit.android.extensions.showSafe
|
||||
import io.timelimit.android.sync.actions.ResetParentBlockedTimesAction
|
||||
import io.timelimit.android.ui.main.ActivityViewModelHolder
|
||||
|
||||
class TryResetParentBlockedTimesDialogFragment: DialogFragment() {
|
||||
companion object {
|
||||
private const val DIALOG_TAG = "TryResetParentBlockedTimesDialogFragment"
|
||||
private const val PARENT_USER_ID = "parentUserId"
|
||||
|
||||
fun newInstance(parentUserId: String) = TryResetParentBlockedTimesDialogFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putString(PARENT_USER_ID, parentUserId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val parentUserId = arguments!!.getString(PARENT_USER_ID)!!
|
||||
val auth = (activity!! as ActivityViewModelHolder).getActivityViewModel()
|
||||
|
||||
auth.authenticatedUser.observe(this, Observer {
|
||||
if (it?.second?.type != UserType.Parent) {
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
})
|
||||
|
||||
return AlertDialog.Builder(context!!, theme)
|
||||
.setMessage(R.string.manage_parent_blocked_times_info)
|
||||
.setPositiveButton(R.string.manage_parent_blocked_action_reset) { _, _ ->
|
||||
auth.tryDispatchParentAction(
|
||||
ResetParentBlockedTimesAction(
|
||||
parentId = parentUserId
|
||||
)
|
||||
)
|
||||
}
|
||||
.setNegativeButton(R.string.generic_cancel, null)
|
||||
.create()
|
||||
}
|
||||
|
||||
fun show(fragmentManager: FragmentManager) = showSafe(fragmentManager, DIALOG_TAG)
|
||||
}
|
|
@ -14,7 +14,7 @@
|
|||
-->
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context="io.timelimit.android.ui.manage.category.blocked_times.copy.CopyBlockedTimeAreasDialogFragment">
|
||||
tools:context="io.timelimit.android.ui.manage.category.blocked_times.CopyBlockedTimeAreasDialogFragment">
|
||||
|
||||
<data>
|
||||
<variable
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:id="@+id/coordinator"
|
||||
tools:context="io.timelimit.android.ui.manage.category.blocked_times.BlockedTimeAreasFragment">
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
android:text="@string/generic_help" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text1"
|
||||
android:paddingTop="8dp"
|
||||
android:text="@string/blocked_time_areas_help_about"
|
||||
android:textAppearance="?android:textAppearanceSmall"
|
||||
|
|
|
@ -157,6 +157,39 @@
|
|||
<include android:id="@+id/manage_notifications"
|
||||
layout="@layout/manage_parent_notifications" />
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
app:cardUseCompatPadding="true"
|
||||
android:onClick="@{() -> handlers.onManageBlockedTimesClicked()}"
|
||||
android:foreground="?selectableItemBackground"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:text="@string/manage_parent_blocked_times_title"
|
||||
android:textAppearance="?android:textAppearanceLarge"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:text="@string/manage_parent_blocked_times_description"
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:textAppearance="?android:textAppearanceSmall"
|
||||
android:text="@string/purchase_required_info_local_mode_free"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<include android:id="@+id/delete_parent"
|
||||
layout="@layout/delete_parent_view" />
|
||||
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
<!--
|
||||
TimeLimit Copyright <C> 2019 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 <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
tools:context="io.timelimit.android.ui.manage.parent.blockedtimes.ManageParentBlockedTimesFragment">
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/coordinator">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
app:layout_scrollFlags="scroll|enterAlways|snap"
|
||||
android:background="?android:windowBackground"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<Spinner
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_weight="2"
|
||||
android:id="@+id/spinner_day"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_help"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/generic_help"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_copy_to_other_days"
|
||||
android:text="@string/blocked_time_areas_copy_to_other_days"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<CheckBox
|
||||
android:text="@string/blocked_time_areas_checkbox_detailed"
|
||||
android:id="@+id/detailed_mode"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
android:id="@+id/recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fab"
|
||||
app:fabSize="normal"
|
||||
android:src="@drawable/ic_lock_open_white_24dp"
|
||||
android:layout_margin="16dp"
|
||||
android:layout_gravity="end|bottom"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
</layout>
|
|
@ -38,5 +38,9 @@
|
|||
<include android:id="@+id/child_without_premium"
|
||||
layout="@layout/new_login_fragment_child_without_premium" />
|
||||
|
||||
<include layout="@layout/new_login_fragment_blocked_time" />
|
||||
|
||||
<include layout="@layout/new_login_fragment_missing_trusted_time" />
|
||||
|
||||
</io.timelimit.android.ui.view.SafeViewFlipper>
|
||||
</layout>
|
||||
|
|
26
app/src/main/res/layout/new_login_fragment_blocked_time.xml
Normal file
26
app/src/main/res/layout/new_login_fragment_blocked_time.xml
Normal file
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 2019 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 <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<TextView
|
||||
android:gravity="center_horizontal"
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
android:padding="8dp"
|
||||
android:text="@string/login_blocked_time"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</layout>
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 2019 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 <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<TextView
|
||||
android:gravity="center_horizontal"
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
android:padding="8dp"
|
||||
android:text="@string/login_missing_trusted_time"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</layout>
|
|
@ -259,6 +259,13 @@
|
|||
app:exitAnim="@anim/nav_default_exit_anim"
|
||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
|
||||
<action
|
||||
android:id="@+id/action_manageParentFragment_to_manageParentBlockedTimesFragment"
|
||||
app:destination="@id/manageParentBlockedTimesFragment"
|
||||
app:enterAnim="@anim/nav_default_enter_anim"
|
||||
app:exitAnim="@anim/nav_default_exit_anim"
|
||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/changeParentPasswordFragment"
|
||||
|
@ -387,4 +394,13 @@
|
|||
android:name="deviceId"
|
||||
app:argType="string" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/manageParentBlockedTimesFragment"
|
||||
android:name="io.timelimit.android.ui.manage.parent.blockedtimes.ManageParentBlockedTimesFragment"
|
||||
android:label="manage_parent_blocked_times_fragment"
|
||||
tools:layout="@layout/manage_parent_blocked_times_fragment" >
|
||||
<argument
|
||||
android:name="parentUserId"
|
||||
app:argType="string" />
|
||||
</fragment>
|
||||
</navigation>
|
||||
|
|
|
@ -27,4 +27,6 @@
|
|||
<string name="login_child_already_current_user">Du bist schon der aktuelle Benutzer dieses Gerätes</string>
|
||||
<string name="login_child_info">Dadurch wirst du als aktueller Nutzer dieses Gerätes festgelegt</string>
|
||||
<string name="login_child_done_toast">Der Benutzer dieses Gerätes wurde geändert</string>
|
||||
<string name="login_missing_trusted_time">Zum Anmelden mit diesem Benutzer wird die Uhrzeit benötigt</string>
|
||||
<string name="login_blocked_time">Das Anmelden mit diesem Benutzer zu dieser Zeit wurde gesperrt</string>
|
||||
</resources>
|
|
@ -0,0 +1,31 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 2019 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 <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<resources>
|
||||
<string name="manage_parent_blocked_times_title">Zeitschloss</string>
|
||||
<string name="manage_parent_blocked_times_description">
|
||||
Hiermit können Zeiten festgelegt werden, an denen sich ein Elternteil nicht anmelden kann.
|
||||
Das ist bei der Selbstbeschränkung relevant.
|
||||
</string>
|
||||
|
||||
<string name="manage_parent_blocked_times_info">
|
||||
Ein Elternteil kann nur für sich selbst Anmeldebeschränkungen festlegen.
|
||||
Andere Elternteile können diese nur zurücksetzen.
|
||||
</string>
|
||||
|
||||
<string name="manage_parent_blocked_action_reset">Einstellungen zurücksetzen</string>
|
||||
|
||||
<string name="manage_parent_lockout_hour_rule">Um ein Aussperren zu verhindern können maximal 18 Stunden je Tag blockiert werden</string>
|
||||
</resources>
|
|
@ -27,4 +27,6 @@
|
|||
<string name="login_child_already_current_user">You are already the current user of this device</string>
|
||||
<string name="login_child_info">This will set you as the current user of this device</string>
|
||||
<string name="login_child_done_toast">The user of this device was changed</string>
|
||||
<string name="login_missing_trusted_time">The current time is required to sign in with this user</string>
|
||||
<string name="login_blocked_time">Signing in as this user at this time was blocked</string>
|
||||
</resources>
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 2019 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 <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<resources>
|
||||
<string name="manage_parent_blocked_times_title">Time lock</string>
|
||||
<string name="manage_parent_blocked_times_description">
|
||||
This allows to set times at which the parent can not sign in.
|
||||
This is relevant for limiting ones own usage.
|
||||
</string>
|
||||
|
||||
<string name="manage_parent_blocked_times_info">
|
||||
A parent can only add limits for itself.
|
||||
Other parents can only reset the limits.
|
||||
</string>
|
||||
|
||||
<string name="manage_parent_blocked_action_reset">Reset limits</string>
|
||||
|
||||
<string name="manage_parent_lockout_hour_rule">To prevent losing access, you can only block 18 hours per day</string>
|
||||
</resources>
|
Loading…
Add table
Add a link
Reference in a new issue