mirror of
https://codeberg.org/timelimit/timelimit-android.git
synced 2025-10-06 03:50:23 +02:00
Add support for end times for temporarily blocking
This commit is contained in:
parent
a81bc687fb
commit
1a695cfc96
26 changed files with 1555 additions and 112 deletions
822
app/schemas/io.timelimit.android.data.RoomDatabase/24.json
Normal file
822
app/schemas/io.timelimit.android.data.RoomDatabase/24.json
Normal file
|
@ -0,0 +1,822 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 24,
|
||||
"identityHash": "e65897d4948b6cf1c46d4b7e53583ad1",
|
||||
"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 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, `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, 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": "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
|
||||
}
|
||||
],
|
||||
"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 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": []
|
||||
}
|
||||
],
|
||||
"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, 'e65897d4948b6cf1c46d4b7e53583ad1')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -166,4 +166,10 @@ object DatabaseMigrations {
|
|||
database.execSQL("ALTER TABLE `category` ADD COLUMN `min_battery_mobile` INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V24 = object: Migration(23, 24) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE `category` ADD COLUMN `temporarily_blocked_end_time` INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ import io.timelimit.android.data.model.*
|
|||
AppActivity::class,
|
||||
Notification::class,
|
||||
AllowedContact::class
|
||||
], version = 23)
|
||||
], version = 24)
|
||||
abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database {
|
||||
companion object {
|
||||
private val lock = Object()
|
||||
|
@ -92,7 +92,8 @@ abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database
|
|||
DatabaseMigrations.MIGRATE_TO_V20,
|
||||
DatabaseMigrations.MIGRATE_TO_V21,
|
||||
DatabaseMigrations.MIGRATE_TO_V22,
|
||||
DatabaseMigrations.MIGRATE_TO_V23
|
||||
DatabaseMigrations.MIGRATE_TO_V23,
|
||||
DatabaseMigrations.MIGRATE_TO_V24
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
|
|
@ -65,8 +65,8 @@ abstract class CategoryDao {
|
|||
@Query("UPDATE category SET blocked_times = :blockedMinutesInWeek WHERE id = :categoryId")
|
||||
abstract fun updateCategoryBlockedTimes(categoryId: String, blockedMinutesInWeek: ImmutableBitmask)
|
||||
|
||||
@Query("UPDATE category SET temporarily_blocked = :blocked WHERE id = :categoryId")
|
||||
abstract fun updateCategoryTemporarilyBlocked(categoryId: String, blocked: Boolean)
|
||||
@Query("UPDATE category SET temporarily_blocked = :blocked, temporarily_blocked_end_time = :endTime WHERE id = :categoryId")
|
||||
abstract fun updateCategoryTemporarilyBlocked(categoryId: String, blocked: Boolean, endTime: Long)
|
||||
|
||||
@Query("SELECT id, base_version, apps_version, rules_version, usedtimes_version FROM category")
|
||||
abstract fun getCategoriesWithVersionNumbers(): LiveData<List<CategoryWithVersionNumbers>>
|
||||
|
@ -89,7 +89,7 @@ abstract class CategoryDao {
|
|||
@Query("SELECT * FROM category LIMIT :pageSize OFFSET :offset")
|
||||
abstract fun getCategoryPageSync(offset: Int, pageSize: Int): List<Category>
|
||||
|
||||
@Query("SELECT id, child_id, temporarily_blocked FROM category")
|
||||
@Query("SELECT id, child_id, temporarily_blocked, temporarily_blocked_end_time FROM category")
|
||||
abstract fun getAllCategoriesShortInfo(): LiveData<List<CategoryShortInfo>>
|
||||
|
||||
@Query("UPDATE category SET parent_category_id = :parentCategoryId WHERE id = :categoryId")
|
||||
|
@ -118,5 +118,7 @@ data class CategoryShortInfo(
|
|||
@ColumnInfo(name = "id")
|
||||
val categoryId: String,
|
||||
@ColumnInfo(name = "temporarily_blocked")
|
||||
val temporarilyBlocked: Boolean
|
||||
val temporarilyBlocked: Boolean,
|
||||
@ColumnInfo(name = "temporarily_blocked_end_time")
|
||||
val temporarilyBlockedEndTime: Long
|
||||
)
|
||||
|
|
|
@ -44,6 +44,8 @@ data class Category(
|
|||
val extraTimeInMillis: Long,
|
||||
@ColumnInfo(name = "temporarily_blocked")
|
||||
val temporarilyBlocked: Boolean,
|
||||
@ColumnInfo(name = "temporarily_blocked_end_time")
|
||||
val temporarilyBlockedEndTime: Long,
|
||||
@ColumnInfo(name = "base_version")
|
||||
val baseVersion: String,
|
||||
@ColumnInfo(name = "apps_version")
|
||||
|
@ -73,6 +75,7 @@ data class Category(
|
|||
private const val BLOCKED_MINUTES_IN_WEEK = "b"
|
||||
private const val EXTRA_TIME_IN_MILLIS = "et"
|
||||
private const val TEMPORARILY_BLOCKED = "tb"
|
||||
private const val TEMPORARILY_BLOCKED_NED_TIME = "tbet"
|
||||
private const val BASE_VERSION = "vb"
|
||||
private const val ASSIGNED_APPS_VERSION = "va"
|
||||
private const val RULES_VERSION = "vr"
|
||||
|
@ -90,6 +93,7 @@ data class Category(
|
|||
var blockedMinutesInWeek: ImmutableBitmask? = null
|
||||
var extraTimeInMillis: Long? = null
|
||||
var temporarilyBlocked: Boolean? = null
|
||||
var temporarilyBlockedEndTime: Long = 0
|
||||
var baseVersion: String? = null
|
||||
var assignedAppsVersion: String? = null
|
||||
var timeLimitRulesVersion: String? = null
|
||||
|
@ -111,6 +115,7 @@ data class Category(
|
|||
BLOCKED_MINUTES_IN_WEEK -> blockedMinutesInWeek = ImmutableBitmaskJson.parse(reader.nextString(), BLOCKED_MINUTES_IN_WEEK_LENGTH)
|
||||
EXTRA_TIME_IN_MILLIS -> extraTimeInMillis = reader.nextLong()
|
||||
TEMPORARILY_BLOCKED -> temporarilyBlocked = reader.nextBoolean()
|
||||
TEMPORARILY_BLOCKED_NED_TIME -> temporarilyBlockedEndTime = reader.nextLong()
|
||||
BASE_VERSION -> baseVersion = reader.nextString()
|
||||
ASSIGNED_APPS_VERSION -> assignedAppsVersion = reader.nextString()
|
||||
RULES_VERSION -> timeLimitRulesVersion = reader.nextString()
|
||||
|
@ -133,6 +138,7 @@ data class Category(
|
|||
blockedMinutesInWeek = blockedMinutesInWeek!!,
|
||||
extraTimeInMillis = extraTimeInMillis!!,
|
||||
temporarilyBlocked = temporarilyBlocked!!,
|
||||
temporarilyBlockedEndTime = temporarilyBlockedEndTime,
|
||||
baseVersion = baseVersion!!,
|
||||
assignedAppsVersion = assignedAppsVersion!!,
|
||||
timeLimitRulesVersion = timeLimitRulesVersion!!,
|
||||
|
@ -176,6 +182,7 @@ data class Category(
|
|||
writer.name(BLOCKED_MINUTES_IN_WEEK).value(ImmutableBitmaskJson.serialize(blockedMinutesInWeek))
|
||||
writer.name(EXTRA_TIME_IN_MILLIS).value(extraTimeInMillis)
|
||||
writer.name(TEMPORARILY_BLOCKED).value(temporarilyBlocked)
|
||||
writer.name(TEMPORARILY_BLOCKED_NED_TIME).value(temporarilyBlockedEndTime)
|
||||
writer.name(BASE_VERSION).value(baseVersion)
|
||||
writer.name(ASSIGNED_APPS_VERSION).value(assignedAppsVersion)
|
||||
writer.name(RULES_VERSION).value(timeLimitRulesVersion)
|
||||
|
|
|
@ -173,6 +173,7 @@ class AppSetupLogic(private val appLogic: AppLogic) {
|
|||
blockedMinutesInWeek = ImmutableBitmask((BitSet())),
|
||||
extraTimeInMillis = 0,
|
||||
temporarilyBlocked = false,
|
||||
temporarilyBlockedEndTime = 0,
|
||||
baseVersion = "",
|
||||
assignedAppsVersion = "",
|
||||
timeLimitRulesVersion = "",
|
||||
|
@ -191,6 +192,7 @@ class AppSetupLogic(private val appLogic: AppLogic) {
|
|||
blockedMinutesInWeek = defaultCategories.allowedGamesBlockedTimes,
|
||||
extraTimeInMillis = 0,
|
||||
temporarilyBlocked = false,
|
||||
temporarilyBlockedEndTime = 0,
|
||||
baseVersion = "",
|
||||
assignedAppsVersion = "",
|
||||
timeLimitRulesVersion = "",
|
||||
|
|
|
@ -93,7 +93,12 @@ object BackgroundTaskRestrictionLogic {
|
|||
result.status = BackgroundTaskLogicAppStatus.ShouldBlock
|
||||
|
||||
return
|
||||
} else if (category.temporarilyBlocked or (parentCategory?.temporarilyBlocked == true)) {
|
||||
} else if (
|
||||
(category.temporarilyBlocked && (
|
||||
(!shouldTrustTimeTemporarily) || (category.temporarilyBlockedEndTime == 0L) || (category.temporarilyBlockedEndTime > nowTimestamp))) or
|
||||
(parentCategory?.temporarilyBlocked == true && (
|
||||
(!shouldTrustTimeTemporarily) || (parentCategory.temporarilyBlockedEndTime == 0L) || (parentCategory.temporarilyBlockedEndTime > nowTimestamp)))
|
||||
) {
|
||||
result.status = BackgroundTaskLogicAppStatus.ShouldBlock
|
||||
|
||||
return
|
||||
|
|
|
@ -256,9 +256,25 @@ class BlockingReasonUtil(private val appLogic: AppLogic) {
|
|||
}
|
||||
|
||||
if (category.temporarilyBlocked) {
|
||||
if (category.temporarilyBlockedEndTime == 0L) {
|
||||
return liveDataFromValue(BlockingReason.TemporarilyBlocked)
|
||||
} else {
|
||||
return getTemporarilyTrustedTimeInMillis().switchMap { time ->
|
||||
if (time == null) {
|
||||
liveDataFromValue(BlockingReason.MissingNetworkTime)
|
||||
} else if (time < category.temporarilyBlockedEndTime) {
|
||||
liveDataFromValue(BlockingReason.TemporarilyBlocked)
|
||||
} else {
|
||||
getBlockingReasonStep4Point8(category, child, timeZone, isParentCategory, blockingLevel)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return getBlockingReasonStep4Point8(category, child, timeZone, isParentCategory, blockingLevel)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBlockingReasonStep4Point8(category: Category, child: User, timeZone: TimeZone, isParentCategory: Boolean, blockingLevel: BlockingLevel): LiveData<BlockingReason> {
|
||||
val areLimitsDisabled: LiveData<Boolean>
|
||||
|
||||
if (child.disableLimitsUntil == 0L) {
|
||||
|
|
|
@ -132,14 +132,7 @@ class CategoriesBlockingReasonUtil(private val appLogic: AppLogic) {
|
|||
): LiveData<BlockingReason> {
|
||||
return category.switchMap { category ->
|
||||
val batteryOk = batteryLevel.map { it.isCategoryAllowed(category) }.ignoreUnchanged()
|
||||
|
||||
batteryOk.switchMap { ok ->
|
||||
if (!ok) {
|
||||
liveDataFromValue(BlockingReason.BatteryLimit)
|
||||
} else if (category.temporarilyBlocked) {
|
||||
liveDataFromValue(BlockingReason.TemporarilyBlocked)
|
||||
} else {
|
||||
areLimitsTemporarilyDisabled.switchMap { areLimitsTemporarilyDisabled ->
|
||||
val elseCase = areLimitsTemporarilyDisabled.switchMap { areLimitsTemporarilyDisabled ->
|
||||
if (areLimitsTemporarilyDisabled) {
|
||||
liveDataFromValue(BlockingReason.None)
|
||||
} else {
|
||||
|
@ -159,6 +152,27 @@ class CategoriesBlockingReasonUtil(private val appLogic: AppLogic) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
batteryOk.switchMap { ok ->
|
||||
if (!ok) {
|
||||
liveDataFromValue(BlockingReason.BatteryLimit)
|
||||
} else if (category.temporarilyBlocked) {
|
||||
if (category.temporarilyBlockedEndTime == 0L) {
|
||||
liveDataFromValue(BlockingReason.TemporarilyBlocked)
|
||||
} else {
|
||||
temporarilyTrustedTimeInMillis.switchMap { timeInMillis ->
|
||||
if (timeInMillis == null) {
|
||||
liveDataFromValue(BlockingReason.MissingNetworkTime)
|
||||
} else if (timeInMillis < category.temporarilyBlockedEndTime) {
|
||||
liveDataFromValue(BlockingReason.TemporarilyBlocked)
|
||||
} else {
|
||||
elseCase
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
elseCase
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -303,6 +303,7 @@ object ApplyServerDataStatus {
|
|||
blockedMinutesInWeek = newCategory.blockedMinutesInWeek,
|
||||
extraTimeInMillis = newCategory.extraTimeInMillis,
|
||||
temporarilyBlocked = newCategory.temporarilyBlocked,
|
||||
temporarilyBlockedEndTime = newCategory.temporarilyBlockedEndTime,
|
||||
blockAllNotifications = newCategory.blockAllNotifications,
|
||||
baseVersion = newCategory.baseDataVersion,
|
||||
assignedAppsVersion = "",
|
||||
|
@ -320,6 +321,7 @@ object ApplyServerDataStatus {
|
|||
blockedMinutesInWeek = newCategory.blockedMinutesInWeek,
|
||||
extraTimeInMillis = newCategory.extraTimeInMillis,
|
||||
temporarilyBlocked = newCategory.temporarilyBlocked,
|
||||
temporarilyBlockedEndTime = newCategory.temporarilyBlockedEndTime,
|
||||
blockAllNotifications = newCategory.blockAllNotifications,
|
||||
baseVersion = newCategory.baseDataVersion,
|
||||
parentCategoryId = newCategory.parentCategoryId,
|
||||
|
|
|
@ -644,20 +644,26 @@ data class IncrementCategoryExtraTimeAction(val categoryId: String, val addedExt
|
|||
writer.endObject()
|
||||
}
|
||||
}
|
||||
data class UpdateCategoryTemporarilyBlockedAction(val categoryId: String, val blocked: Boolean): ParentAction() {
|
||||
data class UpdateCategoryTemporarilyBlockedAction(val categoryId: String, val blocked: Boolean, val endTime: Long?): ParentAction() {
|
||||
companion object {
|
||||
const val TYPE_VALUE = "UPDATE_CATEGORY_TEMPORARILY_BLOCKED"
|
||||
private const val CATEGORY_ID = "categoryId"
|
||||
private const val BLOCKED = "blocked"
|
||||
private const val END_TIME = "endTime"
|
||||
|
||||
fun parse(action: JSONObject) = UpdateCategoryTemporarilyBlockedAction(
|
||||
categoryId = action.getString(CATEGORY_ID),
|
||||
blocked = action.getBoolean(BLOCKED)
|
||||
blocked = action.getBoolean(BLOCKED),
|
||||
endTime = if (action.has(END_TIME)) action.getLong(END_TIME) else null
|
||||
)
|
||||
}
|
||||
|
||||
init {
|
||||
IdGenerator.assertIdValid(categoryId)
|
||||
|
||||
if (endTime != null && (!blocked)) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
|
||||
override fun serialize(writer: JsonWriter) {
|
||||
|
@ -667,6 +673,10 @@ data class UpdateCategoryTemporarilyBlockedAction(val categoryId: String, val bl
|
|||
writer.name(CATEGORY_ID).value(categoryId)
|
||||
writer.name(BLOCKED).value(blocked)
|
||||
|
||||
if (endTime != null) {
|
||||
writer.name(END_TIME).value(endTime)
|
||||
}
|
||||
|
||||
writer.endObject()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -74,6 +74,7 @@ object LocalDatabaseParentActionDispatcher {
|
|||
blockedMinutesInWeek = ImmutableBitmask(BitSet()),
|
||||
extraTimeInMillis = 0,
|
||||
temporarilyBlocked = false,
|
||||
temporarilyBlockedEndTime = 0,
|
||||
baseVersion = "",
|
||||
assignedAppsVersion = "",
|
||||
timeLimitRulesVersion = "",
|
||||
|
@ -135,7 +136,11 @@ object LocalDatabaseParentActionDispatcher {
|
|||
is UpdateCategoryTemporarilyBlockedAction -> {
|
||||
DatabaseValidation.assertCategoryExists(database, action.categoryId)
|
||||
|
||||
database.category().updateCategoryTemporarilyBlocked(action.categoryId, action.blocked)
|
||||
database.category().updateCategoryTemporarilyBlocked(
|
||||
categoryId = action.categoryId,
|
||||
blocked = action.blocked,
|
||||
endTime = if (action.blocked) action.endTime ?: 0 else 0
|
||||
)
|
||||
}
|
||||
is DeleteTimeLimitRuleAction -> {
|
||||
DatabaseValidation.assertTimelimitRuleExists(database, action.ruleId)
|
||||
|
|
|
@ -336,6 +336,7 @@ data class ServerUpdatedCategoryBaseData(
|
|||
val blockedMinutesInWeek: ImmutableBitmask,
|
||||
val extraTimeInMillis: Long,
|
||||
val temporarilyBlocked: Boolean,
|
||||
val temporarilyBlockedEndTime: Long,
|
||||
val baseDataVersion: String,
|
||||
val parentCategoryId: String,
|
||||
val blockAllNotifications: Boolean,
|
||||
|
@ -350,6 +351,7 @@ data class ServerUpdatedCategoryBaseData(
|
|||
private const val BLOCKED_MINUTES_IN_WEEK = "blockedTimes"
|
||||
private const val EXTRA_TIME_IN_MILLIS = "extraTime"
|
||||
private const val TEMPORARILY_BLOCKED = "tempBlocked"
|
||||
private const val TEMPORARILY_BLOCKED_END_TIME = "tempBlockTime"
|
||||
private const val BASE_DATA_VERSION = "version"
|
||||
private const val PARENT_CATEGORY_ID = "parentCategoryId"
|
||||
private const val BLOCK_ALL_NOTIFICATIONS = "blockAllNotifications"
|
||||
|
@ -364,6 +366,7 @@ data class ServerUpdatedCategoryBaseData(
|
|||
var blockedMinutesInWeek: ImmutableBitmask? = null
|
||||
var extraTimeInMillis: Long? = null
|
||||
var temporarilyBlocked: Boolean? = null
|
||||
var temporarilyBlockedEndTime: Long = 0
|
||||
var baseDataVersion: String? = null
|
||||
var parentCategoryId: String? = null
|
||||
// added later -> default values
|
||||
|
@ -381,6 +384,7 @@ data class ServerUpdatedCategoryBaseData(
|
|||
BLOCKED_MINUTES_IN_WEEK -> blockedMinutesInWeek = ImmutableBitmaskJson.parse(reader.nextString(), Category.BLOCKED_MINUTES_IN_WEEK_LENGTH)
|
||||
EXTRA_TIME_IN_MILLIS -> extraTimeInMillis = reader.nextLong()
|
||||
TEMPORARILY_BLOCKED -> temporarilyBlocked = reader.nextBoolean()
|
||||
TEMPORARILY_BLOCKED_END_TIME -> temporarilyBlockedEndTime = reader.nextLong()
|
||||
BASE_DATA_VERSION -> baseDataVersion = reader.nextString()
|
||||
PARENT_CATEGORY_ID -> parentCategoryId = reader.nextString()
|
||||
BLOCK_ALL_NOTIFICATIONS -> blockAllNotifications = reader.nextBoolean()
|
||||
|
@ -399,6 +403,7 @@ data class ServerUpdatedCategoryBaseData(
|
|||
blockedMinutesInWeek = blockedMinutesInWeek!!,
|
||||
extraTimeInMillis = extraTimeInMillis!!,
|
||||
temporarilyBlocked = temporarilyBlocked!!,
|
||||
temporarilyBlockedEndTime = temporarilyBlockedEndTime,
|
||||
baseDataVersion = baseDataVersion!!,
|
||||
parentCategoryId = parentCategoryId!!,
|
||||
blockAllNotifications = blockAllNotifications,
|
||||
|
|
|
@ -308,13 +308,6 @@ class LockFragment : Fragment() {
|
|||
binding.manageDisableTimeLimits.disableTimeLimitsUntilString = it
|
||||
})
|
||||
|
||||
// bind disable temporarily blocking
|
||||
categories
|
||||
.map { it != null && it.second.filter { category -> category.temporarilyBlocked }.size > 1 }
|
||||
.observe(this, Observer {
|
||||
binding.areMultipleCategoriesBlocked = it!!
|
||||
})
|
||||
|
||||
binding.handlers = object: Handlers {
|
||||
override fun openMainApp() {
|
||||
startActivity(Intent(context, MainActivity::class.java))
|
||||
|
@ -380,7 +373,8 @@ class LockFragment : Fragment() {
|
|||
auth.tryDispatchParentAction(
|
||||
UpdateCategoryTemporarilyBlockedAction(
|
||||
categoryId = categoryId,
|
||||
blocked = false
|
||||
blocked = false,
|
||||
endTime = null
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -397,7 +391,8 @@ class LockFragment : Fragment() {
|
|||
auth.tryDispatchParentAction(
|
||||
UpdateCategoryTemporarilyBlockedAction(
|
||||
categoryId = category.id,
|
||||
blocked = false
|
||||
blocked = false,
|
||||
endTime = null
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -19,7 +19,6 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.CheckBox
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
|
@ -31,16 +30,15 @@ import io.timelimit.android.livedata.map
|
|||
import io.timelimit.android.livedata.mergeLiveData
|
||||
import io.timelimit.android.logic.AppLogic
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
import io.timelimit.android.sync.actions.UpdateCategoryTemporarilyBlockedAction
|
||||
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.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 io.timelimit.android.ui.payment.RequiresPurchaseDialogFragment
|
||||
|
||||
class ManageChildAdvancedFragment : Fragment() {
|
||||
companion object {
|
||||
|
@ -59,6 +57,9 @@ class ManageChildAdvancedFragment : Fragment() {
|
|||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val binding = FragmentManageChildAdvancedBinding.inflate(layoutInflater, container, false)
|
||||
|
||||
val categories = logic.database.category().getCategoriesByChildId(params.childId)
|
||||
val shouldProvideFullVersionFunctions = logic.fullVersion.shouldProvideFullVersionFunctions
|
||||
|
||||
run {
|
||||
// blocked categories
|
||||
|
||||
|
@ -69,47 +70,15 @@ class ManageChildAdvancedFragment : Fragment() {
|
|||
).show(fragmentManager!!)
|
||||
}
|
||||
|
||||
val categoriesLive = logic.database.category().getCategoriesByChildId(params.childId)
|
||||
|
||||
mergeLiveData(categoriesLive, logic.fullVersion.shouldProvideFullVersionFunctions).observe(this, Observer {
|
||||
(categories, hasFullVersion) ->
|
||||
|
||||
binding.blockedCategoriesCheckboxContainer.removeAllViews()
|
||||
|
||||
categories?.forEach {
|
||||
category ->
|
||||
|
||||
val checkbox = CheckBox(context)
|
||||
|
||||
checkbox.isChecked = category.temporarilyBlocked
|
||||
checkbox.text = category.title
|
||||
|
||||
checkbox.setOnCheckedChangeListener { _, isChecked ->
|
||||
if (isChecked != category.temporarilyBlocked) {
|
||||
if (isChecked) {
|
||||
if (hasFullVersion != true) {
|
||||
checkbox.isChecked = false
|
||||
|
||||
RequiresPurchaseDialogFragment().show(fragmentManager!!)
|
||||
|
||||
return@setOnCheckedChangeListener
|
||||
}
|
||||
}
|
||||
|
||||
if (!auth.tryDispatchParentAction(
|
||||
UpdateCategoryTemporarilyBlockedAction(
|
||||
categoryId = category.id,
|
||||
blocked = !category.temporarilyBlocked
|
||||
ManageBlockTemporarilyView.bind(
|
||||
lifecycleOwner = this,
|
||||
fragmentManager = fragmentManager!!,
|
||||
categories = categories,
|
||||
shouldProvideFullVersionFunctions = shouldProvideFullVersionFunctions,
|
||||
container = binding.blockedCategoriesCheckboxContainer,
|
||||
auth = auth,
|
||||
childId = params.childId
|
||||
)
|
||||
)) {
|
||||
checkbox.isChecked = category.temporarilyBlocked
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.blockedCategoriesCheckboxContainer.addView(checkbox)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
run {
|
||||
|
|
|
@ -0,0 +1,204 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.ui.manage.child.advanced.manageblocktemporarily
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.data.RoomDatabase
|
||||
import io.timelimit.android.data.model.UserType
|
||||
import io.timelimit.android.databinding.BlockTemporarilyDialogBinding
|
||||
import io.timelimit.android.extensions.showSafe
|
||||
import io.timelimit.android.logic.RealTime
|
||||
import io.timelimit.android.sync.actions.UpdateCategoryTemporarilyBlockedAction
|
||||
import io.timelimit.android.ui.main.ActivityViewModel
|
||||
import io.timelimit.android.ui.main.ActivityViewModelHolder
|
||||
import io.timelimit.android.ui.manage.child.advanced.managedisabletimelimits.*
|
||||
import org.threeten.bp.Instant
|
||||
import org.threeten.bp.LocalDate
|
||||
import org.threeten.bp.LocalDateTime
|
||||
import org.threeten.bp.ZoneId
|
||||
|
||||
class BlockTemporarilyDialogFragment: DialogFragment() {
|
||||
companion object {
|
||||
private const val DIALOG_TAG = "BlockTemporarilyDialogFragment"
|
||||
private const val EXTRA_CATEGORY_ID = "categoryId"
|
||||
private const val EXTRA_CHILD_ID = "childId"
|
||||
|
||||
private const val STATE_PAGE = "page"
|
||||
|
||||
private const val PAGE_LIST = 0
|
||||
private const val PAGE_TIME = 1
|
||||
private const val PAGE_DATE = 2
|
||||
|
||||
fun newInstance(childId: String, categoryId: String) = BlockTemporarilyDialogFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putString(EXTRA_CHILD_ID, childId)
|
||||
putString(EXTRA_CATEGORY_ID, categoryId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val auth: ActivityViewModel by lazy { (activity as ActivityViewModelHolder).getActivityViewModel() }
|
||||
|
||||
lateinit var binding: BlockTemporarilyDialogBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
auth.authenticatedUser.observe(this, Observer {
|
||||
if (it?.second?.type != UserType.Parent) {
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?) = object: BottomSheetDialog(context!!, theme) {
|
||||
override fun onBackPressed() {
|
||||
if (binding.flipper.displayedChild == PAGE_LIST) {
|
||||
super.onBackPressed()
|
||||
} else {
|
||||
binding.flipper.setInAnimation(context, R.anim.wizard_close_step_in)
|
||||
binding.flipper.setOutAnimation(context, R.anim.wizard_close_step_out)
|
||||
binding.flipper.displayedChild = PAGE_LIST
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
binding = BlockTemporarilyDialogBinding.inflate(inflater, container, false)
|
||||
val database = RoomDatabase.with(context!!)
|
||||
val categoryId = arguments!!.getString(EXTRA_CATEGORY_ID)!!
|
||||
val childId = arguments!!.getString(EXTRA_CHILD_ID)!!
|
||||
|
||||
fun now() = RealTime.newInstance().apply {
|
||||
auth.logic.realTimeLogic.getRealTime(this)
|
||||
}.timeInMillis
|
||||
|
||||
fun applyTimestamp(timestamp: Long) {
|
||||
val now = now()
|
||||
|
||||
if (timestamp > now) {
|
||||
auth.tryDispatchParentAction(
|
||||
UpdateCategoryTemporarilyBlockedAction(
|
||||
categoryId = categoryId,
|
||||
blocked = true,
|
||||
endTime = timestamp
|
||||
)
|
||||
)
|
||||
|
||||
dismiss()
|
||||
} else {
|
||||
Toast.makeText(context!!, R.string.manage_disable_time_limits_toast_time_in_past, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
binding.flipper.displayedChild = savedInstanceState.getInt(STATE_PAGE)
|
||||
}
|
||||
|
||||
val endOptionAdapter = BlockTemporarilyEndTimeAdapter()
|
||||
|
||||
database.category().getCategoriesByChildId(childId).observe(this, Observer { categories ->
|
||||
val now = now()
|
||||
|
||||
val endTimes = categories
|
||||
.filter { it.temporarilyBlocked && it.temporarilyBlockedEndTime != 0L }
|
||||
.map { it.temporarilyBlockedEndTime }
|
||||
.filter { it > now }
|
||||
.distinct()
|
||||
.sorted()
|
||||
|
||||
endOptionAdapter.items = endTimes.map {
|
||||
DisableTimelimitsDurationItemFixedEndTime(timestamp = it)
|
||||
} + DisableTimelimitsDuration.items
|
||||
})
|
||||
|
||||
database.user().getChildUserByIdLive(childId).observe(this, Observer { child ->
|
||||
if (child == null) {
|
||||
dismiss()
|
||||
return@Observer
|
||||
}
|
||||
|
||||
endOptionAdapter.listener = object: BlockTemporarilyEndTimeAdapterListener {
|
||||
override fun onItemClicked(item: DisableTimelimitsOption) {
|
||||
when (item) {
|
||||
is DisableTimelimitsUntilTimeOption -> {
|
||||
binding.flipper.setInAnimation(context!!, R.anim.wizard_open_step_in)
|
||||
binding.flipper.setOutAnimation(context!!, R.anim.wizard_open_step_out)
|
||||
binding.flipper.displayedChild = PAGE_TIME
|
||||
}
|
||||
is DisableTimelimitsUntilDateOption -> {
|
||||
binding.flipper.setInAnimation(context!!, R.anim.wizard_open_step_in)
|
||||
binding.flipper.setOutAnimation(context!!, R.anim.wizard_open_step_out)
|
||||
binding.flipper.displayedChild = PAGE_DATE
|
||||
}
|
||||
is DisableTimelimitsDurationItem -> {
|
||||
applyTimestamp(item.getTime(
|
||||
currentTimestamp = now(),
|
||||
timezone = child.timeZone
|
||||
)
|
||||
)
|
||||
}
|
||||
}.let {/* require handling all paths */}
|
||||
}
|
||||
}
|
||||
|
||||
binding.calendarConfirmButton.setOnClickListener {
|
||||
applyTimestamp(
|
||||
LocalDate.of(binding.calendar.year, binding.calendar.month + 1, binding.calendar.dayOfMonth)
|
||||
.atStartOfDay(ZoneId.of(child.timeZone))
|
||||
.toEpochSecond() * 1000
|
||||
)
|
||||
}
|
||||
|
||||
binding.timeConfirmButton.setOnClickListener {
|
||||
applyTimestamp(
|
||||
LocalDateTime.ofInstant(
|
||||
Instant.ofEpochMilli(now()),
|
||||
ZoneId.of(child.timeZone)
|
||||
)
|
||||
.toLocalDate()
|
||||
.atStartOfDay(ZoneId.of(child.timeZone))
|
||||
.plusHours(binding.time.currentHour.toLong())
|
||||
.plusMinutes(binding.time.currentMinute.toLong())
|
||||
.toEpochSecond() * 1000
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
binding.endTimeOptionList.layoutManager = LinearLayoutManager(context)
|
||||
binding.endTimeOptionList.adapter = endOptionAdapter
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
|
||||
outState.putInt(STATE_PAGE, binding.flipper.displayedChild)
|
||||
}
|
||||
|
||||
fun show(fragmentManager: FragmentManager) = showSafe(fragmentManager, DIALOG_TAG)
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.ui.manage.child.advanced.manageblocktemporarily
|
||||
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.timelimit.android.ui.list.TextViewHolder
|
||||
import io.timelimit.android.ui.manage.child.advanced.managedisabletimelimits.DisableTimelimitsOption
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
class BlockTemporarilyEndTimeAdapter: RecyclerView.Adapter<TextViewHolder>() {
|
||||
var items: List<DisableTimelimitsOption> by Delegates.observable(emptyList()) { _, _, _ -> notifyDataSetChanged() }
|
||||
var listener: BlockTemporarilyEndTimeAdapterListener? = null
|
||||
|
||||
init {
|
||||
setHasStableIds(true)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = items.size
|
||||
override fun getItemId(position: Int): Long = items[position].hashCode().toLong()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TextViewHolder = TextViewHolder(parent)
|
||||
|
||||
override fun onBindViewHolder(holder: TextViewHolder, position: Int) {
|
||||
val item = items[position]
|
||||
|
||||
holder.textView.setText(item.getLabel(holder.textView.context))
|
||||
holder.textView.setOnClickListener { listener?.onItemClicked(item) }
|
||||
}
|
||||
}
|
||||
|
||||
interface BlockTemporarilyEndTimeAdapterListener {
|
||||
fun onItemClicked(item: DisableTimelimitsOption)
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.ui.manage.child.advanced.manageblocktemporarily
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import io.timelimit.android.data.model.Category
|
||||
import io.timelimit.android.livedata.*
|
||||
import io.timelimit.android.logic.RealTimeLogic
|
||||
|
||||
data class ManageBlockTemporarilyItem(
|
||||
val categoryId: String,
|
||||
val categoryTitle: String,
|
||||
val checked: Boolean,
|
||||
val endTime: Long
|
||||
)
|
||||
|
||||
object ManageBlockTemporarilyItems {
|
||||
fun build(
|
||||
categories: LiveData<List<Category>>,
|
||||
realTimeLogic: RealTimeLogic
|
||||
): LiveData<List<ManageBlockTemporarilyItem>> {
|
||||
val time = liveDataFromFunction { realTimeLogic.getCurrentTimeInMillis() }
|
||||
|
||||
return categories.map { categories ->
|
||||
categories.map { category ->
|
||||
ManageBlockTemporarilyItem(
|
||||
categoryId = category.id,
|
||||
categoryTitle = category.title,
|
||||
endTime = category.temporarilyBlockedEndTime,
|
||||
checked = category.temporarilyBlocked
|
||||
)
|
||||
}
|
||||
}.switchMap { items ->
|
||||
val hasEndtimes = items.find { it.endTime != 0L } != null
|
||||
|
||||
if (hasEndtimes) {
|
||||
time.map { time ->
|
||||
items.map { item ->
|
||||
if (item.endTime == 0L || item.endTime >= time) {
|
||||
item
|
||||
} else {
|
||||
item.copy(checked = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
liveDataFromValue(items)
|
||||
}
|
||||
}.ignoreUnchanged()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.ui.manage.child.advanced.manageblocktemporarily
|
||||
|
||||
import android.text.format.DateUtils
|
||||
import android.widget.CheckBox
|
||||
import android.widget.LinearLayout
|
||||
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.Category
|
||||
import io.timelimit.android.livedata.mergeLiveData
|
||||
import io.timelimit.android.sync.actions.UpdateCategoryTemporarilyBlockedAction
|
||||
import io.timelimit.android.ui.main.ActivityViewModel
|
||||
import io.timelimit.android.ui.payment.RequiresPurchaseDialogFragment
|
||||
|
||||
object ManageBlockTemporarilyView {
|
||||
fun bind(
|
||||
categories: LiveData<List<Category>>,
|
||||
shouldProvideFullVersionFunctions: LiveData<Boolean>,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
container: LinearLayout,
|
||||
fragmentManager: FragmentManager,
|
||||
auth: ActivityViewModel,
|
||||
childId: String
|
||||
) {
|
||||
val context = container.context
|
||||
val items = ManageBlockTemporarilyItems.build(
|
||||
categories = categories,
|
||||
realTimeLogic = auth.logic.realTimeLogic
|
||||
)
|
||||
|
||||
mergeLiveData(items, shouldProvideFullVersionFunctions).observe(lifecycleOwner, Observer {
|
||||
(categories, hasFullVersion) ->
|
||||
|
||||
container.removeAllViews()
|
||||
|
||||
categories?.forEach {
|
||||
category ->
|
||||
|
||||
val checkbox = CheckBox(context)
|
||||
val showEndTime = category.checked && category.endTime != 0L
|
||||
|
||||
checkbox.isChecked = category.checked
|
||||
checkbox.text = if (showEndTime)
|
||||
context.getString(
|
||||
R.string.manage_child_block_temporarily_until,
|
||||
category.categoryTitle,
|
||||
DateUtils.formatDateTime(
|
||||
context,
|
||||
category.endTime,
|
||||
DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_TIME or
|
||||
DateUtils.FORMAT_SHOW_YEAR or DateUtils.FORMAT_SHOW_WEEKDAY
|
||||
)
|
||||
)
|
||||
else
|
||||
category.categoryTitle
|
||||
|
||||
checkbox.setOnLongClickListener {
|
||||
if (hasFullVersion != true) {
|
||||
checkbox.isChecked = false
|
||||
|
||||
RequiresPurchaseDialogFragment().show(fragmentManager)
|
||||
} else if (auth.requestAuthenticationOrReturnTrue()) {
|
||||
BlockTemporarilyDialogFragment.newInstance(
|
||||
childId = childId,
|
||||
categoryId = category.categoryId
|
||||
).show(fragmentManager)
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
checkbox.setOnCheckedChangeListener { _, isChecked ->
|
||||
if (isChecked != category.checked) {
|
||||
if (isChecked) {
|
||||
if (hasFullVersion != true) {
|
||||
checkbox.isChecked = false
|
||||
|
||||
RequiresPurchaseDialogFragment().show(fragmentManager)
|
||||
|
||||
return@setOnCheckedChangeListener
|
||||
}
|
||||
}
|
||||
|
||||
if (!auth.tryDispatchParentAction(
|
||||
UpdateCategoryTemporarilyBlockedAction(
|
||||
categoryId = category.categoryId,
|
||||
blocked = !category.checked,
|
||||
endTime = null
|
||||
)
|
||||
)) {
|
||||
checkbox.isChecked = category.checked
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
container.addView(checkbox)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.ui.manage.child.advanced.managedisabletimelimits
|
||||
|
||||
import android.content.Context
|
||||
import android.text.format.DateUtils
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.date.DateInTimezone
|
||||
import org.threeten.bp.LocalDate
|
||||
import org.threeten.bp.ZoneId
|
||||
import java.util.*
|
||||
|
||||
sealed class DisableTimelimitsOption {
|
||||
abstract fun getLabel(context: Context): String
|
||||
}
|
||||
|
||||
object DisableTimelimitsUntilTimeOption: DisableTimelimitsOption() {
|
||||
override fun getLabel(context: Context): String = context.getString(R.string.manage_disable_time_limits_btn_time)
|
||||
}
|
||||
|
||||
object DisableTimelimitsUntilDateOption: DisableTimelimitsOption() {
|
||||
override fun getLabel(context: Context): String = context.getString(R.string.manage_disable_time_limits_btn_date)
|
||||
}
|
||||
|
||||
abstract class DisableTimelimitsDurationItem: DisableTimelimitsOption() {
|
||||
abstract fun getTime(currentTimestamp: Long, timezone: String): Long
|
||||
}
|
||||
|
||||
class DisableTimelimitsDurationItemFixed(val label: Int, val duration: Long): DisableTimelimitsDurationItem() {
|
||||
override fun getLabel(context: Context) = context.getString(label)
|
||||
override fun getTime(currentTimestamp: Long, timezone: String): Long = currentTimestamp + duration
|
||||
}
|
||||
|
||||
class DisableTimelimitsDurationItemFixedEndTime(val timestamp: Long): DisableTimelimitsDurationItem() {
|
||||
override fun getLabel(context: Context): String = context.getString(
|
||||
R.string.manage_child_block_temporarily_dialog_until,
|
||||
DateUtils.formatDateTime(
|
||||
context,
|
||||
timestamp,
|
||||
DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_TIME or
|
||||
DateUtils.FORMAT_SHOW_YEAR or DateUtils.FORMAT_SHOW_WEEKDAY
|
||||
)
|
||||
)
|
||||
|
||||
override fun getTime(currentTimestamp: Long, timezone: String): Long = timestamp
|
||||
}
|
||||
|
||||
class DisableTimelimitsDurationItemDays(val label: Int, val dayCounter: Int): DisableTimelimitsDurationItem() {
|
||||
override fun getLabel(context: Context) = context.getString(label)
|
||||
override fun getTime(currentTimestamp: Long, timezone: String): Long {
|
||||
return LocalDate.ofEpochDay(DateInTimezone.newInstance(currentTimestamp, TimeZone.getTimeZone(timezone)).dayOfEpoch.toLong())
|
||||
.plusDays(dayCounter.toLong())
|
||||
.atStartOfDay(ZoneId.of(timezone))
|
||||
.toEpochSecond() * 1000
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: this is not used at the disable time limit view yet ...
|
||||
object DisableTimelimitsDuration {
|
||||
val items = listOf(
|
||||
DisableTimelimitsDurationItemFixed(R.string.manage_disable_time_limits_btn_10_min, 1000 * 60 * 10),
|
||||
DisableTimelimitsDurationItemFixed(R.string.manage_disable_time_limits_btn_30_min, 1000 * 60 * 30),
|
||||
DisableTimelimitsDurationItemFixed(R.string.manage_disable_time_limits_btn_1_hour, 1000 * 60 * 60 * 1),
|
||||
DisableTimelimitsDurationItemFixed(R.string.manage_disable_time_limits_btn_2_hour, 1000 * 60 * 60 * 2),
|
||||
DisableTimelimitsDurationItemFixed(R.string.manage_disable_time_limits_btn_4_hour, 1000 * 60 * 60 * 4),
|
||||
DisableTimelimitsDurationItemDays(R.string.manage_disable_time_limits_btn_today, 1),
|
||||
DisableTimelimitsUntilTimeOption,
|
||||
DisableTimelimitsUntilDateOption
|
||||
)
|
||||
}
|
|
@ -139,7 +139,8 @@ data class OfflineModeStatus(
|
|||
apply(
|
||||
UpdateCategoryTemporarilyBlockedAction(
|
||||
categoryId = category.id,
|
||||
blocked = true
|
||||
blocked = true,
|
||||
endTime = category.temporarilyBlockedEndTime
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 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
|
||||
|
@ -42,15 +42,23 @@ class OverviewFragmentModel(application: Application): AndroidViewModel(applicat
|
|||
}
|
||||
}.ignoreUnchanged()
|
||||
private val userEntries = usersWithTemporarilyDisabledLimits.switchMap { users ->
|
||||
categoryEntries.map { categories ->
|
||||
categoryEntries.switchMap { categories ->
|
||||
liveDataFromFunction (5000) { logic.realTimeLogic.getCurrentTimeInMillis() }.map { now ->
|
||||
users.map { user ->
|
||||
OverviewFragmentItemUser(
|
||||
user = user.first,
|
||||
limitsTemporarilyDisabled = user.second,
|
||||
temporarilyBlocked = categories.find { category -> category.childId == user.first.id && category.temporarilyBlocked } != null
|
||||
temporarilyBlocked = categories.find { category ->
|
||||
category.childId == user.first.id &&
|
||||
category.temporarilyBlocked && (
|
||||
category.temporarilyBlockedEndTime == 0L ||
|
||||
category.temporarilyBlockedEndTime > now
|
||||
)
|
||||
} != null
|
||||
)
|
||||
}
|
||||
}
|
||||
}.ignoreUnchanged()
|
||||
}
|
||||
|
||||
private val ownDeviceId = logic.deviceId
|
||||
|
|
67
app/src/main/res/layout/block_temporarily_dialog.xml
Normal file
67
app/src/main/res/layout/block_temporarily_dialog.xml
Normal file
|
@ -0,0 +1,67 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 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 <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<io.timelimit.android.ui.view.SafeViewFlipper
|
||||
android:measureAllChildren="false"
|
||||
android:id="@+id/flipper"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/end_time_option_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TimePicker
|
||||
android:id="@+id/time"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<Button
|
||||
android:text="@string/generic_ok"
|
||||
android:id="@+id/time_confirm_button"
|
||||
android:layout_gravity="end"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<DatePicker
|
||||
android:id="@+id/calendar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<Button
|
||||
android:text="@string/generic_ok"
|
||||
android:id="@+id/calendar_confirm_button"
|
||||
android:layout_gravity="end"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</io.timelimit.android.ui.view.SafeViewFlipper>
|
||||
</layout>
|
|
@ -47,10 +47,6 @@
|
|||
name="currentTime"
|
||||
type="String" />
|
||||
|
||||
<variable
|
||||
name="areMultipleCategoriesBlocked"
|
||||
type="Boolean" />
|
||||
|
||||
<variable
|
||||
name="blockedKindLabel"
|
||||
type="String" />
|
||||
|
@ -437,7 +433,6 @@
|
|||
android:layout_height="wrap_content" />
|
||||
|
||||
<Button
|
||||
android:enabled="@{safeUnbox(areMultipleCategoriesBlocked)}"
|
||||
android:onClick="@{() -> handlers.disableTemporarilyLockForAllCategories()}"
|
||||
android:text="@string/lock_disable_temporarily_all_categories"
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -445,14 +440,6 @@
|
|||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:visibility="@{safeUnbox(areMultipleCategoriesBlocked) ? View.GONE : View.VISIBLE}"
|
||||
tools:text="@string/lock_disable_temporarily_all_categories_disabled"
|
||||
android:text="@{@string/lock_disable_temporarily_all_categories_disabled(appCategoryTitle)}"
|
||||
android:textAppearance="?android:textAppearanceSmall"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
TimeLimit Copyright <C> 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.
|
||||
|
@ -19,9 +19,11 @@
|
|||
<string name="manage_child_block_temporarily_title">Vorübergehend Sperren</string>
|
||||
<string name="manage_child_block_temporarily_text">
|
||||
Wählen Sie Kategorien, die vorübergehend gesperrt werden sollen.
|
||||
Vorübergehend bedeutet nicht, dass die Sperre automatisch aufgehoben wird.
|
||||
Wenn Sie eine regelmäßige Sperrung benötigen, verwenden Sie die Sperrzeiten.
|
||||
Sie können die Sperre automatisch aufheben lassen, indem Sie lange auf eine Kategorie tippen
|
||||
und dann eine Endzeit auswählen.
|
||||
</string>
|
||||
<string name="manage_child_block_temporarily_until">%1$s (bis %2$s)</string>
|
||||
<string name="manage_child_block_temporarily_dialog_until">bis %s</string>
|
||||
|
||||
<string name="manage_child_category_no_time_limits">Keine Zeitbegrenzung</string>
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
TimeLimit Copyright <C> 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.
|
||||
|
@ -19,9 +19,11 @@
|
|||
<string name="manage_child_block_temporarily_title">block temporarily</string>
|
||||
<string name="manage_child_block_temporarily_text">
|
||||
Select categories which should be blocked temporarily.
|
||||
Temporarily does not mean that the blocking is disabled automatically.
|
||||
If you need a recurring blocking, use blocked time areas.
|
||||
You can disable the blocking automatically by holding a category
|
||||
and selecting an end time.
|
||||
</string>
|
||||
<string name="manage_child_block_temporarily_until">%1$s (until %2$s)</string>
|
||||
<string name="manage_child_block_temporarily_dialog_until">until %s</string>
|
||||
|
||||
<string name="manage_child_category_no_time_limits">no time limit</string>
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue