Add support for end times for temporarily blocking

This commit is contained in:
Jonas Lochmann 2020-01-27 01:00:00 +01:00
parent a81bc687fb
commit 1a695cfc96
No known key found for this signature in database
GPG key ID: 8B8C9AEE10FA5B36
26 changed files with 1555 additions and 112 deletions

View 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')"
]
}
}

View file

@ -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")
}
}
}

View file

@ -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()
}

View file

@ -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
)

View file

@ -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)

View file

@ -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 = "",

View file

@ -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

View file

@ -256,9 +256,25 @@ class BlockingReasonUtil(private val appLogic: AppLogic) {
}
if (category.temporarilyBlocked) {
return liveDataFromValue(BlockingReason.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) {

View file

@ -132,33 +132,47 @@ class CategoriesBlockingReasonUtil(private val appLogic: AppLogic) {
): LiveData<BlockingReason> {
return category.switchMap { category ->
val batteryOk = batteryLevel.map { it.isCategoryAllowed(category) }.ignoreUnchanged()
val elseCase = areLimitsTemporarilyDisabled.switchMap { areLimitsTemporarilyDisabled ->
if (areLimitsTemporarilyDisabled) {
liveDataFromValue(BlockingReason.None)
} else {
checkCategoryBlockedTimeAreas(
temporarilyTrustedMinuteOfWeek = temporarilyTrustedMinuteOfWeek,
blockedMinutesInWeek = category.blockedMinutesInWeek.dataNotToModify
).switchMap { blockedTimeAreasReason ->
if (blockedTimeAreasReason != BlockingReason.None) {
liveDataFromValue(blockedTimeAreasReason)
} else {
checkCategoryTimeLimitRules(
temporarilyTrustedDate = temporarilyTrustedDate,
category = category,
rules = appLogic.database.timeLimitRules().getTimeLimitRulesByCategory(category.id)
)
}
}
}
}
batteryOk.switchMap { ok ->
if (!ok) {
liveDataFromValue(BlockingReason.BatteryLimit)
} else if (category.temporarilyBlocked) {
liveDataFromValue(BlockingReason.TemporarilyBlocked)
} else {
areLimitsTemporarilyDisabled.switchMap { areLimitsTemporarilyDisabled ->
if (areLimitsTemporarilyDisabled) {
liveDataFromValue(BlockingReason.None)
} else {
checkCategoryBlockedTimeAreas(
temporarilyTrustedMinuteOfWeek = temporarilyTrustedMinuteOfWeek,
blockedMinutesInWeek = category.blockedMinutesInWeek.dataNotToModify
).switchMap { blockedTimeAreasReason ->
if (blockedTimeAreasReason != BlockingReason.None) {
liveDataFromValue(blockedTimeAreasReason)
} else {
checkCategoryTimeLimitRules(
temporarilyTrustedDate = temporarilyTrustedDate,
category = category,
rules = appLogic.database.timeLimitRules().getTimeLimitRulesByCategory(category.id)
)
}
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
}
}
}

View file

@ -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,

View file

@ -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()
}
}

View file

@ -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)

View file

@ -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,

View file

@ -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
)
)
}

View file

@ -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
)
)) {
checkbox.isChecked = category.temporarilyBlocked
}
}
}
binding.blockedCategoriesCheckboxContainer.addView(checkbox)
}
})
ManageBlockTemporarilyView.bind(
lifecycleOwner = this,
fragmentManager = fragmentManager!!,
categories = categories,
shouldProvideFullVersionFunctions = shouldProvideFullVersionFunctions,
container = binding.blockedCategoriesCheckboxContainer,
auth = auth,
childId = params.childId
)
}
run {

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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()
}
}

View file

@ -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)
}
})
}
}

View file

@ -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
)
}

View file

@ -139,7 +139,8 @@ data class OfflineModeStatus(
apply(
UpdateCategoryTemporarilyBlockedAction(
categoryId = category.id,
blocked = true
blocked = true,
endTime = category.temporarilyBlockedEndTime
)
)
}

View file

@ -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 ->
users.map { user ->
OverviewFragmentItemUser(
user = user.first,
limitsTemporarilyDisabled = user.second,
temporarilyBlocked = categories.find { category -> category.childId == user.first.id && category.temporarilyBlocked } != null
)
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 && (
category.temporarilyBlockedEndTime == 0L ||
category.temporarilyBlockedEndTime > now
)
} != null
)
}
}
}
}.ignoreUnchanged()
}
private val ownDeviceId = logic.deviceId

View 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>

View file

@ -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>

View file

@ -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>

View file

@ -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>