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") 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, AppActivity::class,
Notification::class, Notification::class,
AllowedContact::class AllowedContact::class
], version = 23) ], version = 24)
abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database { abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database {
companion object { companion object {
private val lock = 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_V20,
DatabaseMigrations.MIGRATE_TO_V21, DatabaseMigrations.MIGRATE_TO_V21,
DatabaseMigrations.MIGRATE_TO_V22, DatabaseMigrations.MIGRATE_TO_V22,
DatabaseMigrations.MIGRATE_TO_V23 DatabaseMigrations.MIGRATE_TO_V23,
DatabaseMigrations.MIGRATE_TO_V24
) )
.build() .build()
} }

View file

@ -65,8 +65,8 @@ abstract class CategoryDao {
@Query("UPDATE category SET blocked_times = :blockedMinutesInWeek WHERE id = :categoryId") @Query("UPDATE category SET blocked_times = :blockedMinutesInWeek WHERE id = :categoryId")
abstract fun updateCategoryBlockedTimes(categoryId: String, blockedMinutesInWeek: ImmutableBitmask) abstract fun updateCategoryBlockedTimes(categoryId: String, blockedMinutesInWeek: ImmutableBitmask)
@Query("UPDATE category SET temporarily_blocked = :blocked WHERE id = :categoryId") @Query("UPDATE category SET temporarily_blocked = :blocked, temporarily_blocked_end_time = :endTime WHERE id = :categoryId")
abstract fun updateCategoryTemporarilyBlocked(categoryId: String, blocked: Boolean) abstract fun updateCategoryTemporarilyBlocked(categoryId: String, blocked: Boolean, endTime: Long)
@Query("SELECT id, base_version, apps_version, rules_version, usedtimes_version FROM category") @Query("SELECT id, base_version, apps_version, rules_version, usedtimes_version FROM category")
abstract fun getCategoriesWithVersionNumbers(): LiveData<List<CategoryWithVersionNumbers>> abstract fun getCategoriesWithVersionNumbers(): LiveData<List<CategoryWithVersionNumbers>>
@ -89,7 +89,7 @@ abstract class CategoryDao {
@Query("SELECT * FROM category LIMIT :pageSize OFFSET :offset") @Query("SELECT * FROM category LIMIT :pageSize OFFSET :offset")
abstract fun getCategoryPageSync(offset: Int, pageSize: Int): List<Category> 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>> abstract fun getAllCategoriesShortInfo(): LiveData<List<CategoryShortInfo>>
@Query("UPDATE category SET parent_category_id = :parentCategoryId WHERE id = :categoryId") @Query("UPDATE category SET parent_category_id = :parentCategoryId WHERE id = :categoryId")
@ -118,5 +118,7 @@ data class CategoryShortInfo(
@ColumnInfo(name = "id") @ColumnInfo(name = "id")
val categoryId: String, val categoryId: String,
@ColumnInfo(name = "temporarily_blocked") @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, val extraTimeInMillis: Long,
@ColumnInfo(name = "temporarily_blocked") @ColumnInfo(name = "temporarily_blocked")
val temporarilyBlocked: Boolean, val temporarilyBlocked: Boolean,
@ColumnInfo(name = "temporarily_blocked_end_time")
val temporarilyBlockedEndTime: Long,
@ColumnInfo(name = "base_version") @ColumnInfo(name = "base_version")
val baseVersion: String, val baseVersion: String,
@ColumnInfo(name = "apps_version") @ColumnInfo(name = "apps_version")
@ -73,6 +75,7 @@ data class Category(
private const val BLOCKED_MINUTES_IN_WEEK = "b" private const val BLOCKED_MINUTES_IN_WEEK = "b"
private const val EXTRA_TIME_IN_MILLIS = "et" private const val EXTRA_TIME_IN_MILLIS = "et"
private const val TEMPORARILY_BLOCKED = "tb" private const val TEMPORARILY_BLOCKED = "tb"
private const val TEMPORARILY_BLOCKED_NED_TIME = "tbet"
private const val BASE_VERSION = "vb" private const val BASE_VERSION = "vb"
private const val ASSIGNED_APPS_VERSION = "va" private const val ASSIGNED_APPS_VERSION = "va"
private const val RULES_VERSION = "vr" private const val RULES_VERSION = "vr"
@ -90,6 +93,7 @@ data class Category(
var blockedMinutesInWeek: ImmutableBitmask? = null var blockedMinutesInWeek: ImmutableBitmask? = null
var extraTimeInMillis: Long? = null var extraTimeInMillis: Long? = null
var temporarilyBlocked: Boolean? = null var temporarilyBlocked: Boolean? = null
var temporarilyBlockedEndTime: Long = 0
var baseVersion: String? = null var baseVersion: String? = null
var assignedAppsVersion: String? = null var assignedAppsVersion: String? = null
var timeLimitRulesVersion: 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) BLOCKED_MINUTES_IN_WEEK -> blockedMinutesInWeek = ImmutableBitmaskJson.parse(reader.nextString(), BLOCKED_MINUTES_IN_WEEK_LENGTH)
EXTRA_TIME_IN_MILLIS -> extraTimeInMillis = reader.nextLong() EXTRA_TIME_IN_MILLIS -> extraTimeInMillis = reader.nextLong()
TEMPORARILY_BLOCKED -> temporarilyBlocked = reader.nextBoolean() TEMPORARILY_BLOCKED -> temporarilyBlocked = reader.nextBoolean()
TEMPORARILY_BLOCKED_NED_TIME -> temporarilyBlockedEndTime = reader.nextLong()
BASE_VERSION -> baseVersion = reader.nextString() BASE_VERSION -> baseVersion = reader.nextString()
ASSIGNED_APPS_VERSION -> assignedAppsVersion = reader.nextString() ASSIGNED_APPS_VERSION -> assignedAppsVersion = reader.nextString()
RULES_VERSION -> timeLimitRulesVersion = reader.nextString() RULES_VERSION -> timeLimitRulesVersion = reader.nextString()
@ -133,6 +138,7 @@ data class Category(
blockedMinutesInWeek = blockedMinutesInWeek!!, blockedMinutesInWeek = blockedMinutesInWeek!!,
extraTimeInMillis = extraTimeInMillis!!, extraTimeInMillis = extraTimeInMillis!!,
temporarilyBlocked = temporarilyBlocked!!, temporarilyBlocked = temporarilyBlocked!!,
temporarilyBlockedEndTime = temporarilyBlockedEndTime,
baseVersion = baseVersion!!, baseVersion = baseVersion!!,
assignedAppsVersion = assignedAppsVersion!!, assignedAppsVersion = assignedAppsVersion!!,
timeLimitRulesVersion = timeLimitRulesVersion!!, timeLimitRulesVersion = timeLimitRulesVersion!!,
@ -176,6 +182,7 @@ data class Category(
writer.name(BLOCKED_MINUTES_IN_WEEK).value(ImmutableBitmaskJson.serialize(blockedMinutesInWeek)) writer.name(BLOCKED_MINUTES_IN_WEEK).value(ImmutableBitmaskJson.serialize(blockedMinutesInWeek))
writer.name(EXTRA_TIME_IN_MILLIS).value(extraTimeInMillis) writer.name(EXTRA_TIME_IN_MILLIS).value(extraTimeInMillis)
writer.name(TEMPORARILY_BLOCKED).value(temporarilyBlocked) writer.name(TEMPORARILY_BLOCKED).value(temporarilyBlocked)
writer.name(TEMPORARILY_BLOCKED_NED_TIME).value(temporarilyBlockedEndTime)
writer.name(BASE_VERSION).value(baseVersion) writer.name(BASE_VERSION).value(baseVersion)
writer.name(ASSIGNED_APPS_VERSION).value(assignedAppsVersion) writer.name(ASSIGNED_APPS_VERSION).value(assignedAppsVersion)
writer.name(RULES_VERSION).value(timeLimitRulesVersion) writer.name(RULES_VERSION).value(timeLimitRulesVersion)

View file

@ -173,6 +173,7 @@ class AppSetupLogic(private val appLogic: AppLogic) {
blockedMinutesInWeek = ImmutableBitmask((BitSet())), blockedMinutesInWeek = ImmutableBitmask((BitSet())),
extraTimeInMillis = 0, extraTimeInMillis = 0,
temporarilyBlocked = false, temporarilyBlocked = false,
temporarilyBlockedEndTime = 0,
baseVersion = "", baseVersion = "",
assignedAppsVersion = "", assignedAppsVersion = "",
timeLimitRulesVersion = "", timeLimitRulesVersion = "",
@ -191,6 +192,7 @@ class AppSetupLogic(private val appLogic: AppLogic) {
blockedMinutesInWeek = defaultCategories.allowedGamesBlockedTimes, blockedMinutesInWeek = defaultCategories.allowedGamesBlockedTimes,
extraTimeInMillis = 0, extraTimeInMillis = 0,
temporarilyBlocked = false, temporarilyBlocked = false,
temporarilyBlockedEndTime = 0,
baseVersion = "", baseVersion = "",
assignedAppsVersion = "", assignedAppsVersion = "",
timeLimitRulesVersion = "", timeLimitRulesVersion = "",

View file

@ -93,7 +93,12 @@ object BackgroundTaskRestrictionLogic {
result.status = BackgroundTaskLogicAppStatus.ShouldBlock result.status = BackgroundTaskLogicAppStatus.ShouldBlock
return 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 result.status = BackgroundTaskLogicAppStatus.ShouldBlock
return return

View file

@ -256,9 +256,25 @@ class BlockingReasonUtil(private val appLogic: AppLogic) {
} }
if (category.temporarilyBlocked) { if (category.temporarilyBlocked) {
if (category.temporarilyBlockedEndTime == 0L) {
return liveDataFromValue(BlockingReason.TemporarilyBlocked) 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> val areLimitsDisabled: LiveData<Boolean>
if (child.disableLimitsUntil == 0L) { if (child.disableLimitsUntil == 0L) {

View file

@ -132,14 +132,7 @@ class CategoriesBlockingReasonUtil(private val appLogic: AppLogic) {
): LiveData<BlockingReason> { ): LiveData<BlockingReason> {
return category.switchMap { category -> return category.switchMap { category ->
val batteryOk = batteryLevel.map { it.isCategoryAllowed(category) }.ignoreUnchanged() val batteryOk = batteryLevel.map { it.isCategoryAllowed(category) }.ignoreUnchanged()
val elseCase = areLimitsTemporarilyDisabled.switchMap { areLimitsTemporarilyDisabled ->
batteryOk.switchMap { ok ->
if (!ok) {
liveDataFromValue(BlockingReason.BatteryLimit)
} else if (category.temporarilyBlocked) {
liveDataFromValue(BlockingReason.TemporarilyBlocked)
} else {
areLimitsTemporarilyDisabled.switchMap { areLimitsTemporarilyDisabled ->
if (areLimitsTemporarilyDisabled) { if (areLimitsTemporarilyDisabled) {
liveDataFromValue(BlockingReason.None) liveDataFromValue(BlockingReason.None)
} else { } 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
} }
} }
} }

View file

@ -303,6 +303,7 @@ object ApplyServerDataStatus {
blockedMinutesInWeek = newCategory.blockedMinutesInWeek, blockedMinutesInWeek = newCategory.blockedMinutesInWeek,
extraTimeInMillis = newCategory.extraTimeInMillis, extraTimeInMillis = newCategory.extraTimeInMillis,
temporarilyBlocked = newCategory.temporarilyBlocked, temporarilyBlocked = newCategory.temporarilyBlocked,
temporarilyBlockedEndTime = newCategory.temporarilyBlockedEndTime,
blockAllNotifications = newCategory.blockAllNotifications, blockAllNotifications = newCategory.blockAllNotifications,
baseVersion = newCategory.baseDataVersion, baseVersion = newCategory.baseDataVersion,
assignedAppsVersion = "", assignedAppsVersion = "",
@ -320,6 +321,7 @@ object ApplyServerDataStatus {
blockedMinutesInWeek = newCategory.blockedMinutesInWeek, blockedMinutesInWeek = newCategory.blockedMinutesInWeek,
extraTimeInMillis = newCategory.extraTimeInMillis, extraTimeInMillis = newCategory.extraTimeInMillis,
temporarilyBlocked = newCategory.temporarilyBlocked, temporarilyBlocked = newCategory.temporarilyBlocked,
temporarilyBlockedEndTime = newCategory.temporarilyBlockedEndTime,
blockAllNotifications = newCategory.blockAllNotifications, blockAllNotifications = newCategory.blockAllNotifications,
baseVersion = newCategory.baseDataVersion, baseVersion = newCategory.baseDataVersion,
parentCategoryId = newCategory.parentCategoryId, parentCategoryId = newCategory.parentCategoryId,

View file

@ -644,20 +644,26 @@ data class IncrementCategoryExtraTimeAction(val categoryId: String, val addedExt
writer.endObject() 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 { companion object {
const val TYPE_VALUE = "UPDATE_CATEGORY_TEMPORARILY_BLOCKED" const val TYPE_VALUE = "UPDATE_CATEGORY_TEMPORARILY_BLOCKED"
private const val CATEGORY_ID = "categoryId" private const val CATEGORY_ID = "categoryId"
private const val BLOCKED = "blocked" private const val BLOCKED = "blocked"
private const val END_TIME = "endTime"
fun parse(action: JSONObject) = UpdateCategoryTemporarilyBlockedAction( fun parse(action: JSONObject) = UpdateCategoryTemporarilyBlockedAction(
categoryId = action.getString(CATEGORY_ID), 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 { init {
IdGenerator.assertIdValid(categoryId) IdGenerator.assertIdValid(categoryId)
if (endTime != null && (!blocked)) {
throw IllegalArgumentException()
}
} }
override fun serialize(writer: JsonWriter) { 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(CATEGORY_ID).value(categoryId)
writer.name(BLOCKED).value(blocked) writer.name(BLOCKED).value(blocked)
if (endTime != null) {
writer.name(END_TIME).value(endTime)
}
writer.endObject() writer.endObject()
} }
} }

View file

@ -74,6 +74,7 @@ object LocalDatabaseParentActionDispatcher {
blockedMinutesInWeek = ImmutableBitmask(BitSet()), blockedMinutesInWeek = ImmutableBitmask(BitSet()),
extraTimeInMillis = 0, extraTimeInMillis = 0,
temporarilyBlocked = false, temporarilyBlocked = false,
temporarilyBlockedEndTime = 0,
baseVersion = "", baseVersion = "",
assignedAppsVersion = "", assignedAppsVersion = "",
timeLimitRulesVersion = "", timeLimitRulesVersion = "",
@ -135,7 +136,11 @@ object LocalDatabaseParentActionDispatcher {
is UpdateCategoryTemporarilyBlockedAction -> { is UpdateCategoryTemporarilyBlockedAction -> {
DatabaseValidation.assertCategoryExists(database, action.categoryId) 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 -> { is DeleteTimeLimitRuleAction -> {
DatabaseValidation.assertTimelimitRuleExists(database, action.ruleId) DatabaseValidation.assertTimelimitRuleExists(database, action.ruleId)

View file

@ -336,6 +336,7 @@ data class ServerUpdatedCategoryBaseData(
val blockedMinutesInWeek: ImmutableBitmask, val blockedMinutesInWeek: ImmutableBitmask,
val extraTimeInMillis: Long, val extraTimeInMillis: Long,
val temporarilyBlocked: Boolean, val temporarilyBlocked: Boolean,
val temporarilyBlockedEndTime: Long,
val baseDataVersion: String, val baseDataVersion: String,
val parentCategoryId: String, val parentCategoryId: String,
val blockAllNotifications: Boolean, val blockAllNotifications: Boolean,
@ -350,6 +351,7 @@ data class ServerUpdatedCategoryBaseData(
private const val BLOCKED_MINUTES_IN_WEEK = "blockedTimes" private const val BLOCKED_MINUTES_IN_WEEK = "blockedTimes"
private const val EXTRA_TIME_IN_MILLIS = "extraTime" private const val EXTRA_TIME_IN_MILLIS = "extraTime"
private const val TEMPORARILY_BLOCKED = "tempBlocked" private const val TEMPORARILY_BLOCKED = "tempBlocked"
private const val TEMPORARILY_BLOCKED_END_TIME = "tempBlockTime"
private const val BASE_DATA_VERSION = "version" private const val BASE_DATA_VERSION = "version"
private const val PARENT_CATEGORY_ID = "parentCategoryId" private const val PARENT_CATEGORY_ID = "parentCategoryId"
private const val BLOCK_ALL_NOTIFICATIONS = "blockAllNotifications" private const val BLOCK_ALL_NOTIFICATIONS = "blockAllNotifications"
@ -364,6 +366,7 @@ data class ServerUpdatedCategoryBaseData(
var blockedMinutesInWeek: ImmutableBitmask? = null var blockedMinutesInWeek: ImmutableBitmask? = null
var extraTimeInMillis: Long? = null var extraTimeInMillis: Long? = null
var temporarilyBlocked: Boolean? = null var temporarilyBlocked: Boolean? = null
var temporarilyBlockedEndTime: Long = 0
var baseDataVersion: String? = null var baseDataVersion: String? = null
var parentCategoryId: String? = null var parentCategoryId: String? = null
// added later -> default values // 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) BLOCKED_MINUTES_IN_WEEK -> blockedMinutesInWeek = ImmutableBitmaskJson.parse(reader.nextString(), Category.BLOCKED_MINUTES_IN_WEEK_LENGTH)
EXTRA_TIME_IN_MILLIS -> extraTimeInMillis = reader.nextLong() EXTRA_TIME_IN_MILLIS -> extraTimeInMillis = reader.nextLong()
TEMPORARILY_BLOCKED -> temporarilyBlocked = reader.nextBoolean() TEMPORARILY_BLOCKED -> temporarilyBlocked = reader.nextBoolean()
TEMPORARILY_BLOCKED_END_TIME -> temporarilyBlockedEndTime = reader.nextLong()
BASE_DATA_VERSION -> baseDataVersion = reader.nextString() BASE_DATA_VERSION -> baseDataVersion = reader.nextString()
PARENT_CATEGORY_ID -> parentCategoryId = reader.nextString() PARENT_CATEGORY_ID -> parentCategoryId = reader.nextString()
BLOCK_ALL_NOTIFICATIONS -> blockAllNotifications = reader.nextBoolean() BLOCK_ALL_NOTIFICATIONS -> blockAllNotifications = reader.nextBoolean()
@ -399,6 +403,7 @@ data class ServerUpdatedCategoryBaseData(
blockedMinutesInWeek = blockedMinutesInWeek!!, blockedMinutesInWeek = blockedMinutesInWeek!!,
extraTimeInMillis = extraTimeInMillis!!, extraTimeInMillis = extraTimeInMillis!!,
temporarilyBlocked = temporarilyBlocked!!, temporarilyBlocked = temporarilyBlocked!!,
temporarilyBlockedEndTime = temporarilyBlockedEndTime,
baseDataVersion = baseDataVersion!!, baseDataVersion = baseDataVersion!!,
parentCategoryId = parentCategoryId!!, parentCategoryId = parentCategoryId!!,
blockAllNotifications = blockAllNotifications, blockAllNotifications = blockAllNotifications,

View file

@ -308,13 +308,6 @@ class LockFragment : Fragment() {
binding.manageDisableTimeLimits.disableTimeLimitsUntilString = it 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 { binding.handlers = object: Handlers {
override fun openMainApp() { override fun openMainApp() {
startActivity(Intent(context, MainActivity::class.java)) startActivity(Intent(context, MainActivity::class.java))
@ -380,7 +373,8 @@ class LockFragment : Fragment() {
auth.tryDispatchParentAction( auth.tryDispatchParentAction(
UpdateCategoryTemporarilyBlockedAction( UpdateCategoryTemporarilyBlockedAction(
categoryId = categoryId, categoryId = categoryId,
blocked = false blocked = false,
endTime = null
) )
) )
} }
@ -397,7 +391,8 @@ class LockFragment : Fragment() {
auth.tryDispatchParentAction( auth.tryDispatchParentAction(
UpdateCategoryTemporarilyBlockedAction( UpdateCategoryTemporarilyBlockedAction(
categoryId = category.id, 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.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.CheckBox
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
@ -31,16 +30,15 @@ import io.timelimit.android.livedata.map
import io.timelimit.android.livedata.mergeLiveData import io.timelimit.android.livedata.mergeLiveData
import io.timelimit.android.logic.AppLogic import io.timelimit.android.logic.AppLogic
import io.timelimit.android.logic.DefaultAppLogic 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.help.HelpDialogFragment
import io.timelimit.android.ui.main.ActivityViewModel import io.timelimit.android.ui.main.ActivityViewModel
import io.timelimit.android.ui.main.getActivityViewModel import io.timelimit.android.ui.main.getActivityViewModel
import io.timelimit.android.ui.manage.child.ManageChildFragmentArgs 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.managedisabletimelimits.ManageDisableTimelimitsViewHelper
import io.timelimit.android.ui.manage.child.advanced.password.ManageChildPassword 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.advanced.timezone.UserTimezoneView
import io.timelimit.android.ui.manage.child.primarydevice.PrimaryDeviceView import io.timelimit.android.ui.manage.child.primarydevice.PrimaryDeviceView
import io.timelimit.android.ui.payment.RequiresPurchaseDialogFragment
class ManageChildAdvancedFragment : Fragment() { class ManageChildAdvancedFragment : Fragment() {
companion object { companion object {
@ -59,6 +57,9 @@ class ManageChildAdvancedFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val binding = FragmentManageChildAdvancedBinding.inflate(layoutInflater, container, false) val binding = FragmentManageChildAdvancedBinding.inflate(layoutInflater, container, false)
val categories = logic.database.category().getCategoriesByChildId(params.childId)
val shouldProvideFullVersionFunctions = logic.fullVersion.shouldProvideFullVersionFunctions
run { run {
// blocked categories // blocked categories
@ -69,47 +70,15 @@ class ManageChildAdvancedFragment : Fragment() {
).show(fragmentManager!!) ).show(fragmentManager!!)
} }
val categoriesLive = logic.database.category().getCategoriesByChildId(params.childId) ManageBlockTemporarilyView.bind(
lifecycleOwner = this,
mergeLiveData(categoriesLive, logic.fullVersion.shouldProvideFullVersionFunctions).observe(this, Observer { fragmentManager = fragmentManager!!,
(categories, hasFullVersion) -> categories = categories,
shouldProvideFullVersionFunctions = shouldProvideFullVersionFunctions,
binding.blockedCategoriesCheckboxContainer.removeAllViews() container = binding.blockedCategoriesCheckboxContainer,
auth = auth,
categories?.forEach { childId = params.childId
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)
}
})
} }
run { 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( apply(
UpdateCategoryTemporarilyBlockedAction( UpdateCategoryTemporarilyBlockedAction(
categoryId = category.id, 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 * 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 * it under the terms of the GNU General Public License as published by
@ -42,15 +42,23 @@ class OverviewFragmentModel(application: Application): AndroidViewModel(applicat
} }
}.ignoreUnchanged() }.ignoreUnchanged()
private val userEntries = usersWithTemporarilyDisabledLimits.switchMap { users -> private val userEntries = usersWithTemporarilyDisabledLimits.switchMap { users ->
categoryEntries.map { categories -> categoryEntries.switchMap { categories ->
liveDataFromFunction (5000) { logic.realTimeLogic.getCurrentTimeInMillis() }.map { now ->
users.map { user -> users.map { user ->
OverviewFragmentItemUser( OverviewFragmentItemUser(
user = user.first, user = user.first,
limitsTemporarilyDisabled = user.second, 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 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" name="currentTime"
type="String" /> type="String" />
<variable
name="areMultipleCategoriesBlocked"
type="Boolean" />
<variable <variable
name="blockedKindLabel" name="blockedKindLabel"
type="String" /> type="String" />
@ -437,7 +433,6 @@
android:layout_height="wrap_content" /> android:layout_height="wrap_content" />
<Button <Button
android:enabled="@{safeUnbox(areMultipleCategoriesBlocked)}"
android:onClick="@{() -> handlers.disableTemporarilyLockForAllCategories()}" android:onClick="@{() -> handlers.disableTemporarilyLockForAllCategories()}"
android:text="@string/lock_disable_temporarily_all_categories" android:text="@string/lock_disable_temporarily_all_categories"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -445,14 +440,6 @@
</LinearLayout> </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> </LinearLayout>
</androidx.cardview.widget.CardView> </androidx.cardview.widget.CardView>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?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 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 it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License. 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_title">Vorübergehend Sperren</string>
<string name="manage_child_block_temporarily_text"> <string name="manage_child_block_temporarily_text">
Wählen Sie Kategorien, die vorübergehend gesperrt werden sollen. Wählen Sie Kategorien, die vorübergehend gesperrt werden sollen.
Vorübergehend bedeutet nicht, dass die Sperre automatisch aufgehoben wird. Sie können die Sperre automatisch aufheben lassen, indem Sie lange auf eine Kategorie tippen
Wenn Sie eine regelmäßige Sperrung benötigen, verwenden Sie die Sperrzeiten. und dann eine Endzeit auswählen.
</string> </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> <string name="manage_child_category_no_time_limits">Keine Zeitbegrenzung</string>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?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 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 it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License. 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_title">block temporarily</string>
<string name="manage_child_block_temporarily_text"> <string name="manage_child_block_temporarily_text">
Select categories which should be blocked temporarily. Select categories which should be blocked temporarily.
Temporarily does not mean that the blocking is disabled automatically. You can disable the blocking automatically by holding a category
If you need a recurring blocking, use blocked time areas. and selecting an end time.
</string> </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> <string name="manage_child_category_no_time_limits">no time limit</string>