Add time lock for parent users

This allows restricting the times at which a parent can sign in
This commit is contained in:
Jonas Lochmann 2019-08-19 00:00:00 +00:00
parent eac11f92e7
commit 24710aa21e
No known key found for this signature in database
GPG key ID: 8B8C9AEE10FA5B36
31 changed files with 1747 additions and 217 deletions

View file

@ -0,0 +1,803 @@
{
"formatVersion": 1,
"database": {
"version": 22,
"identityHash": "016e1fa3596a77d5e527c3b3999f9a66",
"entities": [
{
"tableName": "user",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `password` TEXT NOT NULL, `second_password_salt` TEXT NOT NULL, `type` TEXT NOT NULL, `timezone` TEXT NOT NULL, `disable_limits_until` INTEGER NOT NULL, `mail` TEXT NOT NULL, `current_device` TEXT NOT NULL, `category_for_not_assigned_apps` TEXT NOT NULL, `relax_primary_device` INTEGER NOT NULL, `mail_notification_flags` INTEGER NOT NULL, `blocked_times` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "password",
"columnName": "password",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "secondPasswordSalt",
"columnName": "second_password_salt",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "timeZone",
"columnName": "timezone",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "disableLimitsUntil",
"columnName": "disable_limits_until",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mail",
"columnName": "mail",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "currentDevice",
"columnName": "current_device",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "categoryForNotAssignedApps",
"columnName": "category_for_not_assigned_apps",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "relaxPrimaryDevice",
"columnName": "relax_primary_device",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mailNotificationFlags",
"columnName": "mail_notification_flags",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "blockedTimes",
"columnName": "blocked_times",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "device",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `model` TEXT NOT NULL, `added_at` INTEGER NOT NULL, `current_user_id` TEXT NOT NULL, `apps_version` TEXT NOT NULL, `network_time` TEXT NOT NULL, `current_protection_level` TEXT NOT NULL, `highest_permission_level` TEXT NOT NULL, `current_usage_stats_permission` TEXT NOT NULL, `highest_usage_stats_permission` TEXT NOT NULL, `current_notification_access_permission` TEXT NOT NULL, `highest_notification_access_permission` TEXT NOT NULL, `current_app_version` INTEGER NOT NULL, `highest_app_version` INTEGER NOT NULL, `tried_disabling_device_admin` INTEGER NOT NULL, `did_reboot` INTEGER NOT NULL, `had_manipulation` INTEGER NOT NULL, `had_manipulation_flags` INTEGER NOT NULL, `did_report_uninstall` INTEGER NOT NULL, `is_user_kept_signed_in` INTEGER NOT NULL, `show_device_connected` INTEGER NOT NULL, `default_user` TEXT NOT NULL, `default_user_timeout` INTEGER NOT NULL, `consider_reboot_manipulation` INTEGER NOT NULL, `current_overlay_permission` TEXT NOT NULL, `highest_overlay_permission` TEXT NOT NULL, `current_accessibility_service_permission` INTEGER NOT NULL, `was_accessibility_service_permission` INTEGER NOT NULL, `enable_activity_level_blocking` INTEGER NOT NULL, `q_or_later` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "model",
"columnName": "model",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "addedAt",
"columnName": "added_at",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "currentUserId",
"columnName": "current_user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "installedAppsVersion",
"columnName": "apps_version",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "networkTime",
"columnName": "network_time",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "currentProtectionLevel",
"columnName": "current_protection_level",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "highestProtectionLevel",
"columnName": "highest_permission_level",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "currentUsageStatsPermission",
"columnName": "current_usage_stats_permission",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "highestUsageStatsPermission",
"columnName": "highest_usage_stats_permission",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "currentNotificationAccessPermission",
"columnName": "current_notification_access_permission",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "highestNotificationAccessPermission",
"columnName": "highest_notification_access_permission",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "currentAppVersion",
"columnName": "current_app_version",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "highestAppVersion",
"columnName": "highest_app_version",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "manipulationTriedDisablingDeviceAdmin",
"columnName": "tried_disabling_device_admin",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "manipulationDidReboot",
"columnName": "did_reboot",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hadManipulation",
"columnName": "had_manipulation",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hadManipulationFlags",
"columnName": "had_manipulation_flags",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "didReportUninstall",
"columnName": "did_report_uninstall",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isUserKeptSignedIn",
"columnName": "is_user_kept_signed_in",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "showDeviceConnected",
"columnName": "show_device_connected",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "defaultUser",
"columnName": "default_user",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "defaultUserTimeout",
"columnName": "default_user_timeout",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "considerRebootManipulation",
"columnName": "consider_reboot_manipulation",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "currentOverlayPermission",
"columnName": "current_overlay_permission",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "highestOverlayPermission",
"columnName": "highest_overlay_permission",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accessibilityServiceEnabled",
"columnName": "current_accessibility_service_permission",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "wasAccessibilityServiceEnabled",
"columnName": "was_accessibility_service_permission",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enableActivityLevelBlocking",
"columnName": "enable_activity_level_blocking",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "qOrLater",
"columnName": "q_or_later",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "app",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`device_id` TEXT NOT NULL, `package_name` TEXT NOT NULL, `title` TEXT NOT NULL, `launchable` INTEGER NOT NULL, `recommendation` TEXT NOT NULL, PRIMARY KEY(`device_id`, `package_name`))",
"fields": [
{
"fieldPath": "deviceId",
"columnName": "device_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "packageName",
"columnName": "package_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isLaunchable",
"columnName": "launchable",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "recommendation",
"columnName": "recommendation",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"device_id",
"package_name"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_app_device_id",
"unique": false,
"columnNames": [
"device_id"
],
"createSql": "CREATE INDEX `index_app_device_id` ON `${TABLE_NAME}` (`device_id`)"
},
{
"name": "index_app_package_name",
"unique": false,
"columnNames": [
"package_name"
],
"createSql": "CREATE INDEX `index_app_package_name` ON `${TABLE_NAME}` (`package_name`)"
}
],
"foreignKeys": []
},
{
"tableName": "category_app",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`category_id` TEXT NOT NULL, `package_name` TEXT NOT NULL, PRIMARY KEY(`category_id`, `package_name`))",
"fields": [
{
"fieldPath": "categoryId",
"columnName": "category_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "packageName",
"columnName": "package_name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"category_id",
"package_name"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_category_app_category_id",
"unique": false,
"columnNames": [
"category_id"
],
"createSql": "CREATE INDEX `index_category_app_category_id` ON `${TABLE_NAME}` (`category_id`)"
},
{
"name": "index_category_app_package_name",
"unique": false,
"columnNames": [
"package_name"
],
"createSql": "CREATE INDEX `index_category_app_package_name` ON `${TABLE_NAME}` (`package_name`)"
}
],
"foreignKeys": []
},
{
"tableName": "category",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `child_id` TEXT NOT NULL, `title` TEXT NOT NULL, `blocked_times` TEXT NOT NULL, `extra_time` INTEGER NOT NULL, `temporarily_blocked` INTEGER NOT NULL, `base_version` TEXT NOT NULL, `apps_version` TEXT NOT NULL, `rules_version` TEXT NOT NULL, `usedtimes_version` TEXT NOT NULL, `parent_category_id` TEXT NOT NULL, `block_all_notifications` INTEGER NOT NULL, `time_warnings` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "childId",
"columnName": "child_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "blockedMinutesInWeek",
"columnName": "blocked_times",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "extraTimeInMillis",
"columnName": "extra_time",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "temporarilyBlocked",
"columnName": "temporarily_blocked",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "baseVersion",
"columnName": "base_version",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "assignedAppsVersion",
"columnName": "apps_version",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "timeLimitRulesVersion",
"columnName": "rules_version",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "usedTimesVersion",
"columnName": "usedtimes_version",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "parentCategoryId",
"columnName": "parent_category_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "blockAllNotifications",
"columnName": "block_all_notifications",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "timeWarnings",
"columnName": "time_warnings",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "used_time",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`day_of_epoch` INTEGER NOT NULL, `used_time` INTEGER NOT NULL, `category_id` TEXT NOT NULL, PRIMARY KEY(`category_id`, `day_of_epoch`))",
"fields": [
{
"fieldPath": "dayOfEpoch",
"columnName": "day_of_epoch",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "usedMillis",
"columnName": "used_time",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "categoryId",
"columnName": "category_id",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"category_id",
"day_of_epoch"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "time_limit_rule",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `category_id` TEXT NOT NULL, `apply_to_extra_time_usage` INTEGER NOT NULL, `day_mask` INTEGER NOT NULL, `max_time` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "categoryId",
"columnName": "category_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "applyToExtraTimeUsage",
"columnName": "apply_to_extra_time_usage",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dayMask",
"columnName": "day_mask",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "maximumTimeInMillis",
"columnName": "max_time",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "config",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "key",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "value",
"columnName": "value",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "temporarily_allowed_app",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`device_id` TEXT NOT NULL, `package_name` TEXT NOT NULL, PRIMARY KEY(`device_id`, `package_name`))",
"fields": [
{
"fieldPath": "deviceId",
"columnName": "device_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "packageName",
"columnName": "package_name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"device_id",
"package_name"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "pending_sync_action",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sequence_number` INTEGER NOT NULL, `action` TEXT NOT NULL, `integrity` TEXT NOT NULL, `scheduled_for_upload` INTEGER NOT NULL, `type` TEXT NOT NULL, `user_id` TEXT NOT NULL, PRIMARY KEY(`sequence_number`))",
"fields": [
{
"fieldPath": "sequenceNumber",
"columnName": "sequence_number",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "encodedAction",
"columnName": "action",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "integrity",
"columnName": "integrity",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "scheduledForUpload",
"columnName": "scheduled_for_upload",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"sequence_number"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_pending_sync_action_scheduled_for_upload",
"unique": false,
"columnNames": [
"scheduled_for_upload"
],
"createSql": "CREATE INDEX `index_pending_sync_action_scheduled_for_upload` ON `${TABLE_NAME}` (`scheduled_for_upload`)"
}
],
"foreignKeys": []
},
{
"tableName": "app_activity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`device_id` TEXT NOT NULL, `app_package_name` TEXT NOT NULL, `activity_class_name` TEXT NOT NULL, `activity_title` TEXT NOT NULL, PRIMARY KEY(`device_id`, `app_package_name`, `activity_class_name`))",
"fields": [
{
"fieldPath": "deviceId",
"columnName": "device_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "appPackageName",
"columnName": "app_package_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "activityClassName",
"columnName": "activity_class_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "activity_title",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"device_id",
"app_package_name",
"activity_class_name"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "notification",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL, `id` TEXT NOT NULL, `first_notify_time` INTEGER NOT NULL, `dismissed` INTEGER NOT NULL, PRIMARY KEY(`type`, `id`))",
"fields": [
{
"fieldPath": "type",
"columnName": "type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "firstNotifyTime",
"columnName": "first_notify_time",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isDismissed",
"columnName": "dismissed",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"type",
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "allowed_contact",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `phone` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "phone",
"columnName": "phone",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"016e1fa3596a77d5e527c3b3999f9a66\")"
]
}
}

View file

@ -153,4 +153,10 @@ object DatabaseMigrations {
database.execSQL("ALTER TABLE `device` ADD COLUMN `had_manipulation_flags` INTEGER NOT NULL DEFAULT 0")
}
}
val MIGRATE_TO_V22 = object: Migration(21, 22) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `user` ADD COLUMN `blocked_times` TEXT NOT NULL DEFAULT \"\"")
}
}
}

View file

@ -35,7 +35,7 @@ import io.timelimit.android.data.model.*
AppActivity::class,
Notification::class,
AllowedContact::class
], version = 21)
], version = 22)
abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database {
companion object {
private val lock = Object()
@ -90,7 +90,8 @@ abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database
DatabaseMigrations.MIGRATE_TO_V18,
DatabaseMigrations.MIGRATE_TO_V19,
DatabaseMigrations.MIGRATE_TO_V20,
DatabaseMigrations.MIGRATE_TO_V21
DatabaseMigrations.MIGRATE_TO_V21,
DatabaseMigrations.MIGRATE_TO_V22
)
.build()
}

View file

@ -26,6 +26,7 @@ import io.timelimit.android.data.JsonSerializable
import io.timelimit.android.data.customtypes.ImmutableBitmask
import io.timelimit.android.data.customtypes.ImmutableBitmaskAdapter
import io.timelimit.android.data.customtypes.ImmutableBitmaskJson
import java.util.*
@Entity(tableName = "category")
@TypeConverters(ImmutableBitmaskAdapter::class)
@ -177,4 +178,24 @@ object CategoryTimeWarnings {
)
val durations = durationToBitIndex.keys
}
fun ImmutableBitmask.withConfigCopiedToOtherDates(sourceDay: Int, targetDays: Set<Int>): ImmutableBitmask {
val result = dataNotToModify.clone() as BitSet
val configForSelectedDay = result.get(
sourceDay * Category.MINUTES_PER_DAY,
(sourceDay + 1) * Category.MINUTES_PER_DAY
)
// update all days
targetDays.forEach { day ->
val startWriteIndex = day * Category.MINUTES_PER_DAY
for (i in 0..(Category.MINUTES_PER_DAY - 1)) {
result[startWriteIndex + i] = configForSelectedDay[i]
}
}
return ImmutableBitmask(result)
}

View file

@ -20,10 +20,17 @@ import android.util.JsonWriter
import androidx.room.*
import io.timelimit.android.data.IdGenerator
import io.timelimit.android.data.JsonSerializable
import io.timelimit.android.data.customtypes.ImmutableBitmask
import io.timelimit.android.data.customtypes.ImmutableBitmaskAdapter
import io.timelimit.android.data.customtypes.ImmutableBitmaskJson
import io.timelimit.android.util.parseJsonArray
import java.util.*
@Entity(tableName = "user")
@TypeConverters(UserTypeConverter::class)
@TypeConverters(
UserTypeConverter::class,
ImmutableBitmaskAdapter::class
)
data class User(
@PrimaryKey
@ColumnInfo(name = "id")
@ -53,7 +60,9 @@ data class User(
@ColumnInfo(name = "relax_primary_device")
val relaxPrimaryDevice: Boolean,
@ColumnInfo(name = "mail_notification_flags")
val mailNotificationFlags: Int
val mailNotificationFlags: Int,
@ColumnInfo(name = "blocked_times")
val blockedTimes: ImmutableBitmask
): JsonSerializable {
companion object {
private const val ID = "id"
@ -68,6 +77,7 @@ data class User(
private const val CATEGORY_FOR_NOT_ASSIGNED_APPS = "categoryForNotAssignedApps"
private const val RELAX_PRIMARY_DEVICE = "relaxPrimaryDevice"
private const val MAIL_NOTIFICATION_FLAGS = "mailNotificationFlags"
private const val BLOCKED_TIMES = "blockedTimes"
fun parse(reader: JsonReader): User {
var id: String? = null
@ -82,6 +92,7 @@ data class User(
var categoryForNotAssignedApps = ""
var relaxPrimaryDevice = false
var mailNotificationFlags = 0
var blockedTimes = ImmutableBitmask(BitSet())
reader.beginObject()
while (reader.hasNext()) {
@ -98,6 +109,7 @@ data class User(
CATEGORY_FOR_NOT_ASSIGNED_APPS -> categoryForNotAssignedApps = reader.nextString()
RELAX_PRIMARY_DEVICE -> relaxPrimaryDevice = reader.nextBoolean()
MAIL_NOTIFICATION_FLAGS -> mailNotificationFlags = reader.nextInt()
BLOCKED_TIMES -> blockedTimes = ImmutableBitmaskJson.parse(reader.nextString(), Category.BLOCKED_MINUTES_IN_WEEK_LENGTH)
else -> reader.skipValue()
}
}
@ -115,7 +127,8 @@ data class User(
currentDevice = currentDevice!!,
categoryForNotAssignedApps = categoryForNotAssignedApps,
relaxPrimaryDevice = relaxPrimaryDevice,
mailNotificationFlags = mailNotificationFlags
mailNotificationFlags = mailNotificationFlags,
blockedTimes = blockedTimes
)
}
@ -161,6 +174,7 @@ data class User(
writer.name(CATEGORY_FOR_NOT_ASSIGNED_APPS).value(categoryForNotAssignedApps)
writer.name(RELAX_PRIMARY_DEVICE).value(relaxPrimaryDevice)
writer.name(MAIL_NOTIFICATION_FLAGS).value(mailNotificationFlags)
writer.name(BLOCKED_TIMES).value(ImmutableBitmaskJson.serialize(blockedTimes))
writer.endObject()
}

View file

@ -127,7 +127,8 @@ class AppSetupLogic(private val appLogic: AppLogic) {
currentDevice = "",
categoryForNotAssignedApps = "",
relaxPrimaryDevice = false,
mailNotificationFlags = 0
mailNotificationFlags = 0,
blockedTimes = ImmutableBitmask(BitSet())
)
appLogic.database.user().addUserSync(child)
@ -148,7 +149,8 @@ class AppSetupLogic(private val appLogic: AppLogic) {
currentDevice = "",
categoryForNotAssignedApps = "",
relaxPrimaryDevice = false,
mailNotificationFlags = 0
mailNotificationFlags = 0,
blockedTimes = ImmutableBitmask(BitSet())
)
appLogic.database.user().addUserSync(parent)

View file

@ -278,7 +278,7 @@ class BlockingReasonUtil(private val appLogic: AppLogic) {
Log.d(LOG_TAG, "step 5")
}
return Transformations.switchMap(getTrustedMinuteOfWeekLive(appLogic.timeApi, timeZone)) {
return Transformations.switchMap(getTrustedMinuteOfWeekLive(timeZone)) {
trustedMinuteOfWeek ->
if (category.blockedMinutesInWeek.dataNotToModify.isEmpty) {
@ -298,7 +298,7 @@ class BlockingReasonUtil(private val appLogic: AppLogic) {
Log.d(LOG_TAG, "step 6")
}
return getTrustedDateLive(appLogic.timeApi, timeZone).switchMap {
return getTrustedDateLive(timeZone).switchMap {
nowTrustedDate ->
appLogic.database.timeLimitRules().getTimeLimitRulesByCategory(category.id).switchMap {
@ -371,7 +371,7 @@ class BlockingReasonUtil(private val appLogic: AppLogic) {
}
}
private fun getTrustedMinuteOfWeekLive(api: TimeApi, timeZone: TimeZone): LiveData<Int?> {
fun getTrustedMinuteOfWeekLive(timeZone: TimeZone): LiveData<Int?> {
val realTime = RealTime.newInstance()
return object: LiveData<Int?>() {
@ -395,11 +395,11 @@ class BlockingReasonUtil(private val appLogic: AppLogic) {
}
fun scheduleUpdate() {
api.runDelayed(scheduledUpdateRunnable, 1000L /* every second */)
appLogic.timeApi.runDelayed(scheduledUpdateRunnable, 1000L /* every second */)
}
fun cancelScheduledUpdate() {
api.cancelScheduledAction(scheduledUpdateRunnable)
appLogic.timeApi.cancelScheduledAction(scheduledUpdateRunnable)
}
override fun onActive() {
@ -417,7 +417,7 @@ class BlockingReasonUtil(private val appLogic: AppLogic) {
}.ignoreUnchanged()
}
private fun getTrustedDateLive(api: TimeApi, timeZone: TimeZone): LiveData<DateInTimezone?> {
private fun getTrustedDateLive(timeZone: TimeZone): LiveData<DateInTimezone?> {
val realTime = RealTime.newInstance()
return object: LiveData<DateInTimezone?>() {
@ -441,11 +441,11 @@ class BlockingReasonUtil(private val appLogic: AppLogic) {
}
fun scheduleUpdate() {
api.runDelayed(scheduledUpdateRunnable, 1000L /* every second */)
appLogic.timeApi.runDelayed(scheduledUpdateRunnable, 1000L /* every second */)
}
fun cancelScheduledUpdate() {
api.cancelScheduledAction(scheduledUpdateRunnable)
appLogic.timeApi.cancelScheduledAction(scheduledUpdateRunnable)
}
override fun onActive() {

View file

@ -1475,6 +1475,48 @@ data class RenameChildAction(val childId: String, val newName: String): ParentAc
}
}
data class UpdateParentBlockedTimesAction(val parentId: String, val blockedTimes: ImmutableBitmask): ParentAction() {
companion object {
const val TYPE_VALUE = "UPDATE_PARENT_BLOCKED_TIMES"
private const val PARENT_ID = "parentId"
private const val BLOCKED_TIMES = "times"
}
init {
IdGenerator.assertIdValid(parentId)
}
override fun serialize(writer: JsonWriter) {
writer.beginObject()
writer.name(TYPE).value(TYPE_VALUE)
writer.name(PARENT_ID).value(parentId)
writer.name(BLOCKED_TIMES).value(ImmutableBitmaskJson.serialize(blockedTimes))
writer.endObject()
}
}
data class ResetParentBlockedTimesAction(val parentId: String): ParentAction() {
companion object {
const val TYPE_VALUE = "RESET_PARENT_BLOCKED_TIMES"
private const val PARENT_ID = "parentId"
}
init {
IdGenerator.assertIdValid(parentId)
}
override fun serialize(writer: JsonWriter) {
writer.beginObject()
writer.name(TYPE).value(TYPE_VALUE)
writer.name(PARENT_ID).value(parentId)
writer.endObject()
}
}
// child actions
object ChildSignInAction: ChildAction() {
private const val TYPE_VALUE = "CHILD_SIGN_IN"

View file

@ -153,7 +153,8 @@ object LocalDatabaseParentActionDispatcher {
currentDevice = "",
categoryForNotAssignedApps = "",
relaxPrimaryDevice = false,
mailNotificationFlags = 0
mailNotificationFlags = 0,
blockedTimes = ImmutableBitmask(BitSet())
))
}
is UpdateCategoryBlockedTimesAction -> {
@ -512,6 +513,32 @@ object LocalDatabaseParentActionDispatcher {
null
}
is UpdateParentBlockedTimesAction -> {
val userEntry = database.user().getUserByIdSync(action.parentId)
if (userEntry?.type != UserType.Parent) {
throw IllegalArgumentException("no valid parent id")
}
database.user().updateUserSync(
userEntry.copy(
blockedTimes = action.blockedTimes
)
)
}
is ResetParentBlockedTimesAction -> {
val userEntry = database.user().getUserByIdSync(action.parentId)
if (userEntry?.type != UserType.Parent) {
throw IllegalArgumentException("no valid parent id")
}
database.user().updateUserSync(
userEntry.copy(
blockedTimes = ImmutableBitmask(BitSet())
)
)
}
}.let { }
database.setTransactionSuccessful()

View file

@ -28,6 +28,7 @@ import io.timelimit.android.crypto.PasswordHashing
import io.timelimit.android.data.model.User
import io.timelimit.android.data.model.UserType
import io.timelimit.android.livedata.*
import io.timelimit.android.logic.BlockingReasonUtil
import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.sync.actions.ChildSignInAction
import io.timelimit.android.sync.actions.SetDeviceUserAction
@ -38,10 +39,12 @@ import io.timelimit.android.ui.main.ActivityViewModel
import io.timelimit.android.ui.main.AuthenticatedUser
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.util.*
class LoginDialogFragmentModel(application: Application): AndroidViewModel(application) {
val selectedUserId = MutableLiveData<String?>().apply { value = null }
private val logic = DefaultAppLogic.with(application)
private val blockingReasonUtil = BlockingReasonUtil(logic)
private val users = logic.database.user().getAllUsersLive()
private val isConnectedMode = logic.fullVersion.isLocalMode.invert()
private val selectedUser = users.switchMap { users ->
@ -49,6 +52,7 @@ class LoginDialogFragmentModel(application: Application): AndroidViewModel(appli
users.find { it.id == userId }
}
}
private val trustedTime = selectedUser.switchMap { blockingReasonUtil.getTrustedMinuteOfWeekLive(TimeZone.getTimeZone(it?.timeZone ?: "GMT")) }
private val currentDeviceUser = logic.deviceUserId
private val isCheckingPassword = MutableLiveData<Boolean>().apply { value = false }
private val wasPasswordWrong = MutableLiveData<Boolean>().apply { value = false }
@ -63,8 +67,7 @@ class LoginDialogFragmentModel(application: Application): AndroidViewModel(appli
when (selectedUser?.type) {
UserType.Parent -> {
val isAlreadyCurrentUser = currentDeviceUser.map { it == selectedUser.id }.ignoreUnchanged()
isConnectedMode.switchMap { isConnectedMode ->
val loginScreen = isConnectedMode.switchMap { isConnectedMode ->
isAlreadyCurrentUser.switchMap { isAlreadyCurrentUser ->
isCheckingPassword.switchMap { isCheckingPassword ->
wasPasswordWrong.map { wasPasswordWrong ->
@ -78,6 +81,26 @@ class LoginDialogFragmentModel(application: Application): AndroidViewModel(appli
}
}
}
if (selectedUser.blockedTimes.dataNotToModify.isEmpty) {
loginScreen
} else {
logic.fullVersion.shouldProvideFullVersionFunctions.switchMap { hasPremium ->
if (hasPremium) {
trustedTime.switchMap { time ->
if (time == null) {
liveDataFromValue(ParentUserLoginMissingTrustedTime as LoginDialogStatus)
} else if (selectedUser.blockedTimes.dataNotToModify[time]) {
liveDataFromValue(ParentUserLoginBlockedTime as LoginDialogStatus)
} else {
loginScreen
}
}
} else {
loginScreen
}
}
}
}
UserType.Child -> {
logic.fullVersion.shouldProvideFullVersionFunctions.switchMap { fullversion ->
@ -265,6 +288,8 @@ class LoginDialogFragmentModel(application: Application): AndroidViewModel(appli
sealed class LoginDialogStatus
data class UserListLoginDialogStatus(val usersToShow: List<User>): LoginDialogStatus()
object ParentUserLoginMissingTrustedTime: LoginDialogStatus()
object ParentUserLoginBlockedTime: LoginDialogStatus()
data class ParentUserLogin(
val isConnectedMode: Boolean,
val isAlreadyCurrentDeviceUser: Boolean,

View file

@ -44,6 +44,8 @@ class NewLoginFragment: DialogFragment() {
private const val CHILD_ALREADY_CURRENT_USER = 3
private const val CHILD_AUTH = 4
private const val CHILD_LOGIN_REQUIRES_PREMIUM = 5
private const val BLOCKED_LOGIN_TIME = 6
private const val UNVERIFIED_TIME = 7
}
private val model: LoginDialogFragmentModel by lazy {
@ -200,6 +202,24 @@ class NewLoginFragment: DialogFragment() {
null
}
ParentUserLoginMissingTrustedTime -> {
if (binding.switcher.displayedChild != UNVERIFIED_TIME) {
binding.switcher.setInAnimation(context!!, R.anim.wizard_open_step_in)
binding.switcher.setOutAnimation(context!!, R.anim.wizard_open_step_out)
binding.switcher.displayedChild = UNVERIFIED_TIME
}
null
}
ParentUserLoginBlockedTime -> {
if (binding.switcher.displayedChild != BLOCKED_LOGIN_TIME) {
binding.switcher.setInAnimation(context!!, R.anim.wizard_open_step_in)
binding.switcher.setOutAnimation(context!!, R.anim.wizard_open_step_out)
binding.switcher.displayedChild = BLOCKED_LOGIN_TIME
}
null
}
is CanNotSignInChildHasNoPassword -> {
if (binding.switcher.displayedChild != CHILD_MISSING_PASSWORD) {
binding.switcher.setInAnimation(context!!, R.anim.wizard_open_step_in)

View file

@ -19,29 +19,24 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.snackbar.Snackbar
import io.timelimit.android.R
import io.timelimit.android.data.Database
import io.timelimit.android.data.customtypes.ImmutableBitmask
import io.timelimit.android.data.model.Category
import io.timelimit.android.data.model.withConfigCopiedToOtherDates
import io.timelimit.android.livedata.map
import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.sync.actions.UpdateCategoryBlockedTimesAction
import io.timelimit.android.ui.main.ActivityViewModel
import io.timelimit.android.ui.main.getActivityViewModel
import io.timelimit.android.ui.manage.category.ManageCategoryFragmentArgs
import io.timelimit.android.ui.manage.category.blocked_times.copy.CopyBlockedTimeAreasDialogFragment
import kotlinx.android.synthetic.main.fragment_blocked_time_areas.*
import java.util.*
class BlockedTimeAreasFragment : Fragment() {
class BlockedTimeAreasFragment : Fragment(), CopyBlockedTimeAreasDialogFragmentListener {
companion object {
fun newInstance(params: ManageCategoryFragmentArgs): BlockedTimeAreasFragment {
val result = BlockedTimeAreasFragment()
@ -66,12 +61,12 @@ class BlockedTimeAreasFragment : Fragment() {
return inflater.inflate(R.layout.fragment_blocked_time_areas, container, false)
}
fun updateBlockedTimes(oldMask: BitSet, newMask: BitSet) {
fun updateBlockedTimes(oldMask: ImmutableBitmask, newMask: ImmutableBitmask) {
if (
auth.tryDispatchParentAction(
UpdateCategoryBlockedTimesAction(
categoryId = params.categoryId,
blockedTimes = ImmutableBitmask(newMask)
blockedTimes = newMask
)
)
) {
@ -80,7 +75,7 @@ class BlockedTimeAreasFragment : Fragment() {
auth.tryDispatchParentAction(
UpdateCategoryBlockedTimesAction(
categoryId = params.categoryId,
blockedTimes = ImmutableBitmask(oldMask)
blockedTimes = oldMask
)
)
}
@ -88,168 +83,33 @@ class BlockedTimeAreasFragment : Fragment() {
}
}
override fun onCopyBlockedTimeAreasConfirmed(sourceDay: Int, targetDays: Set<Int>) {
category.value?.blockedMinutesInWeek?.let { current ->
updateBlockedTimes(current, current.withConfigCopiedToOtherDates(sourceDay, targetDays))
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val layoutManager = GridLayoutManager(context, items.value!!.recommendColumns)
layoutManager.spanSizeLookup = SpanSizeLookup(items.value!!)
val adapter = Adapter(items.value!!)
items.observe(this, Observer {
layoutManager.spanCount = it.recommendColumns
layoutManager.spanSizeLookup = SpanSizeLookup(it)
adapter.items = it
})
recycler.adapter = adapter
recycler.layoutManager = layoutManager
category.observe(this, Observer { adapter.blockedTimeAreas = it?.blockedMinutesInWeek?.dataNotToModify })
btn_help.setOnClickListener {
BlockedTimeAreasHelpDialog().show(fragmentManager!!)
}
btn_copy_to_other_days.setOnClickListener {
if (auth.requestAuthenticationOrReturnTrue()) {
CopyBlockedTimeAreasDialogFragment.newInstance(params).apply {
setTargetFragment(this@BlockedTimeAreasFragment, 0)
}.show(fragmentManager!!)
CopyBlockedTimeAreasDialogFragment.newInstance(this@BlockedTimeAreasFragment).show(fragmentManager!!)
}
}
adapter.handlers = object: Handlers {
override fun onMinuteTileClick(time: MinuteTile) {
if (auth.requestAuthenticationOrReturnTrue()) {
val selectedMinuteOfWeek = adapter.selectedMinuteOfWeek
val blockedTimeAreas = adapter.blockedTimeAreas
if (blockedTimeAreas == null) {
// nothing to work with
} else if (selectedMinuteOfWeek == null) {
adapter.selectedMinuteOfWeek = time.minuteOfWeek
} else if (selectedMinuteOfWeek == time.minuteOfWeek) {
adapter.selectedMinuteOfWeek = null
val newBlockMask = blockedTimeAreas.clone() as BitSet
newBlockMask.set(
selectedMinuteOfWeek,
selectedMinuteOfWeek + items.value!!.minutesPerTile,
!newBlockMask[selectedMinuteOfWeek]
)
updateBlockedTimes(blockedTimeAreas, newBlockMask)
} else {
var times = selectedMinuteOfWeek to time.minuteOfWeek
adapter.selectedMinuteOfWeek = null
// sort selected times
if (times.first > times.second) {
times = times.second to times.first
}
// mark until the end
times = times.first to (times.second + items.value!!.minutesPerTile - 1)
// get majority of current value
var allowed = 0
var blocked = 0
for (i in times.first..times.second) {
if (blockedTimeAreas[i]) {
blocked++
} else {
allowed++
}
}
val isMajorityBlocked = blocked > allowed
val shouldBlock = !isMajorityBlocked
val newBlockMask = blockedTimeAreas.clone() as BitSet
newBlockMask.set(times.first, times.second + 1, shouldBlock)
updateBlockedTimes(blockedTimeAreas, newBlockMask)
}
}
}
}
run {
val spinnerAdapter = ArrayAdapter.createFromResource(context!!, R.array.days_of_week_array, android.R.layout.simple_spinner_item)
spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spinner_day.adapter = spinnerAdapter
spinner_day.onItemSelectedListener = object: AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
val selectedDay = items.value!!.getDayOfPosition(
layoutManager.findFirstVisibleItemPosition()
)
if (selectedDay != position) {
layoutManager.scrollToPositionWithOffset(
items.value!!.getPositionOfItem(
DayHeader(position)
),
0
)
}
}
override fun onNothingSelected(parent: AdapterView<*>?) {
// ignore
}
}
}
recycler.addOnScrollListener(object: RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
items.value?.let { items ->
try {
val selectedDay = items.getDayOfPosition(
layoutManager.findFirstVisibleItemPosition()
)
if (selectedDay != spinner_day.selectedItemPosition) {
spinner_day.setSelection(selectedDay, true)
}
} catch (ex: IllegalStateException) {
// ignore
}
}
}
}
})
// bind detailed mode
items.value = when (detailed_mode.isChecked) {
true -> MinuteOfWeekItems
false -> FifteenMinutesOfWeekItems
}
detailed_mode.setOnCheckedChangeListener { _, isChecked ->
val oldValue = items.value
val newValue = when (isChecked) {
true -> MinuteOfWeekItems
false -> FifteenMinutesOfWeekItems
}
if (oldValue != newValue) {
val currentlyVisiblePosition = layoutManager.findFirstVisibleItemPosition()
if (currentlyVisiblePosition == RecyclerView.NO_POSITION) {
items.value = newValue
} else {
val currentlyVisibleItem = oldValue!!.getItemAtPosition(currentlyVisiblePosition)
val newVisiblePosition = newValue.getPositionOfItem(currentlyVisibleItem)
items.value = newValue
layoutManager.scrollToPositionWithOffset(newVisiblePosition, 0)
}
}
}
BlockedTimeAreasLogic.init(
recycler = recycler,
daySpinner = spinner_day,
detailedModeCheckbox = detailed_mode,
requestAuthenticationOrReturnTrue = { auth.requestAuthenticationOrReturnTrue() },
updateBlockedTimes = { a, b -> updateBlockedTimes(a, b) },
currentData = category.map { it?.blockedMinutesInWeek },
lifecycleOwner = this
)
}
}

View file

@ -24,16 +24,34 @@ import androidx.fragment.app.FragmentManager
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import io.timelimit.android.R
import io.timelimit.android.extensions.showSafe
import kotlinx.android.synthetic.main.fragment_blocked_time_areas_help_dialog.*
class BlockedTimeAreasHelpDialog : BottomSheetDialogFragment() {
companion object {
private const val DIALOG_TAG = "r"
private const val FOR_USER = "forUser"
fun newInstance(forUser: Boolean) = BlockedTimeAreasHelpDialog().apply {
arguments = Bundle().apply {
putBoolean(FOR_USER, forUser)
}
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_blocked_time_areas_help_dialog, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val forUser = arguments?.getBoolean(FOR_USER, false)
if (forUser == true) {
text1.setText(R.string.manage_parent_blocked_times_description)
}
}
fun show(manager: FragmentManager) {
showSafe(manager, DIALOG_TAG)
}

View file

@ -0,0 +1,196 @@
/*
* TimeLimit Copyright <C> 2019 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.timelimit.android.ui.manage.category.blocked_times
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.CheckBox
import android.widget.Spinner
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import io.timelimit.android.R
import io.timelimit.android.data.customtypes.ImmutableBitmask
import java.util.*
object BlockedTimeAreasLogic {
fun init(
recycler: RecyclerView,
daySpinner: Spinner,
detailedModeCheckbox: CheckBox,
requestAuthenticationOrReturnTrue: () -> Boolean,
updateBlockedTimes: (ImmutableBitmask, ImmutableBitmask) -> Unit,
currentData: LiveData<ImmutableBitmask?>,
lifecycleOwner: LifecycleOwner
) {
val context = recycler.context!!
val items = MutableLiveData<BlockedTimeItems>().apply { value = FifteenMinutesOfWeekItems }
val layoutManager = GridLayoutManager(context, items.value!!.recommendColumns)
layoutManager.spanSizeLookup = SpanSizeLookup(items.value!!)
val adapter = Adapter(items.value!!)
items.observe(lifecycleOwner, Observer {
layoutManager.spanCount = it.recommendColumns
layoutManager.spanSizeLookup = SpanSizeLookup(it)
adapter.items = it
})
recycler.adapter = adapter
recycler.layoutManager = layoutManager
adapter.handlers = object: Handlers {
override fun onMinuteTileClick(time: MinuteTile) {
if (requestAuthenticationOrReturnTrue()) {
val selectedMinuteOfWeek = adapter.selectedMinuteOfWeek
val blockedTimeAreas = adapter.blockedTimeAreas
if (blockedTimeAreas == null) {
// nothing to work with
} else if (selectedMinuteOfWeek == null) {
adapter.selectedMinuteOfWeek = time.minuteOfWeek
} else if (selectedMinuteOfWeek == time.minuteOfWeek) {
adapter.selectedMinuteOfWeek = null
val newBlockMask = blockedTimeAreas.clone() as BitSet
newBlockMask.set(
selectedMinuteOfWeek,
selectedMinuteOfWeek + items.value!!.minutesPerTile,
!newBlockMask[selectedMinuteOfWeek]
)
updateBlockedTimes(ImmutableBitmask(blockedTimeAreas), ImmutableBitmask(newBlockMask))
} else {
var times = selectedMinuteOfWeek to time.minuteOfWeek
adapter.selectedMinuteOfWeek = null
// sort selected times
if (times.first > times.second) {
times = times.second to times.first
}
// mark until the end
times = times.first to (times.second + items.value!!.minutesPerTile - 1)
// get majority of current value
var allowed = 0
var blocked = 0
for (i in times.first..times.second) {
if (blockedTimeAreas[i]) {
blocked++
} else {
allowed++
}
}
val isMajorityBlocked = blocked > allowed
val shouldBlock = !isMajorityBlocked
val newBlockMask = blockedTimeAreas.clone() as BitSet
newBlockMask.set(times.first, times.second + 1, shouldBlock)
updateBlockedTimes(ImmutableBitmask(blockedTimeAreas), ImmutableBitmask(newBlockMask))
}
}
}
}
run {
val spinnerAdapter = ArrayAdapter.createFromResource(context, R.array.days_of_week_array, android.R.layout.simple_spinner_item)
spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
daySpinner.adapter = spinnerAdapter
daySpinner.onItemSelectedListener = object: AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
val selectedDay = items.value!!.getDayOfPosition(
layoutManager.findFirstVisibleItemPosition()
)
if (selectedDay != position) {
layoutManager.scrollToPositionWithOffset(
items.value!!.getPositionOfItem(
DayHeader(position)
),
0
)
}
}
override fun onNothingSelected(parent: AdapterView<*>?) {
// ignore
}
}
}
recycler.addOnScrollListener(object: RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
items.value?.let { items ->
try {
val selectedDay = items.getDayOfPosition(
layoutManager.findFirstVisibleItemPosition()
)
if (selectedDay != daySpinner.selectedItemPosition) {
daySpinner.setSelection(selectedDay, true)
}
} catch (ex: IllegalStateException) {
// ignore
}
}
}
}
})
// bind detailed mode
items.value = when (detailedModeCheckbox.isChecked) {
true -> MinuteOfWeekItems
false -> FifteenMinutesOfWeekItems
}
detailedModeCheckbox.setOnCheckedChangeListener { _, isChecked ->
val oldValue = items.value
val newValue = when (isChecked) {
true -> MinuteOfWeekItems
false -> FifteenMinutesOfWeekItems
}
if (oldValue != newValue) {
val currentlyVisiblePosition = layoutManager.findFirstVisibleItemPosition()
if (currentlyVisiblePosition == RecyclerView.NO_POSITION) {
items.value = newValue
} else {
val currentlyVisibleItem = oldValue!!.getItemAtPosition(currentlyVisiblePosition)
val newVisiblePosition = newValue.getPositionOfItem(currentlyVisibleItem)
items.value = newValue
layoutManager.scrollToPositionWithOffset(newVisiblePosition, 0)
}
}
}
// loading data
currentData.observe(lifecycleOwner, Observer { adapter.blockedTimeAreas = it?.dataNotToModify })
}
}

View file

@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.timelimit.android.ui.manage.category.blocked_times.copy
package io.timelimit.android.ui.manage.category.blocked_times
import android.os.Bundle
@ -21,38 +21,35 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CheckedTextView
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Observer
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import io.timelimit.android.R
import io.timelimit.android.coroutines.runAsync
import io.timelimit.android.data.model.Category
import io.timelimit.android.data.model.UserType
import io.timelimit.android.databinding.CopyBlockedTimeAreasDialogFragmentBinding
import io.timelimit.android.extensions.showSafe
import io.timelimit.android.livedata.waitForNullableValue
import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.ui.main.ActivityViewModel
import io.timelimit.android.ui.main.ActivityViewModelHolder
import io.timelimit.android.ui.manage.category.ManageCategoryFragmentArgs
import io.timelimit.android.ui.manage.category.blocked_times.BlockedTimeAreasFragment
import java.util.*
class CopyBlockedTimeAreasDialogFragment : BottomSheetDialogFragment() {
companion object {
private const val TAG = "cbtadf"
private const val SELECTED_START_DAY = "ssd"
fun newInstance(params: ManageCategoryFragmentArgs) = CopyBlockedTimeAreasDialogFragment().apply {
arguments = params.toBundle()
fun newInstance(target: Fragment) = CopyBlockedTimeAreasDialogFragment().apply {
setTargetFragment(target, 0)
}
}
val params: ManageCategoryFragmentArgs by lazy { ManageCategoryFragmentArgs.fromBundle(arguments!!) }
var selectedStartDayIndex = -1
val auth: ActivityViewModel by lazy {
(activity as ActivityViewModelHolder).getActivityViewModel()
}
val target: CopyBlockedTimeAreasDialogFragmentListener by lazy {
targetFragment as CopyBlockedTimeAreasDialogFragmentListener
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -126,35 +123,19 @@ class CopyBlockedTimeAreasDialogFragment : BottomSheetDialogFragment() {
bindSecondPage()
binding.saveButton.setOnClickListener {
val logic = DefaultAppLogic.with(context!!)
val targetDays = mutableSetOf<Int>()
runAsync {
val current = logic.database.category().getCategoryByChildIdAndId(params.childId, params.categoryId).waitForNullableValue()
?: return@runAsync
val newBlockedTimes = current.blockedMinutesInWeek.dataNotToModify.clone() as BitSet
val configForSelectedDay = newBlockedTimes.get(
selectedStartDayIndex * Category.MINUTES_PER_DAY,
(selectedStartDayIndex + 1) * Category.MINUTES_PER_DAY
)
// update all days
dayCheckboxes.forEachIndexed { day, checkBox ->
if (checkBox.isChecked) {
val startWriteIndex = day * Category.MINUTES_PER_DAY
for (i in 0..(Category.MINUTES_PER_DAY - 1)) {
newBlockedTimes[startWriteIndex + i] = configForSelectedDay[i]
}
}
dayCheckboxes.forEachIndexed { day, checkBox ->
if (checkBox.isChecked && day != selectedStartDayIndex) {
targetDays.add(day)
}
// apply
val target = targetFragment as BlockedTimeAreasFragment
target.updateBlockedTimes(current.blockedMinutesInWeek.dataNotToModify, newBlockedTimes)
}
target.onCopyBlockedTimeAreasConfirmed(
sourceDay = selectedStartDayIndex,
targetDays = targetDays
)
dismissAllowingStateLoss()
}
@ -168,3 +149,7 @@ class CopyBlockedTimeAreasDialogFragment : BottomSheetDialogFragment() {
fun show(fragmentManager: FragmentManager) = showSafe(fragmentManager, TAG)
}
interface CopyBlockedTimeAreasDialogFragmentListener {
fun onCopyBlockedTimeAreasConfirmed(sourceDay: Int, targetDays: Set<Int>)
}

View file

@ -135,6 +135,16 @@ class ManageParentFragment : Fragment(), FragmentWithCustomTitle {
R.id.manageParentFragment
)
}
override fun onManageBlockedTimesClicked() {
navigation.safeNavigate(
ManageParentFragmentDirections.
actionManageParentFragmentToManageParentBlockedTimesFragment(
params.parentId
),
R.id.manageParentFragment
)
}
}
return binding.root
@ -147,4 +157,5 @@ interface ManageParentFragmentHandlers {
fun onChangePasswordClicked()
fun onRestorePasswordClicked()
fun onLinkMailClicked()
fun onManageBlockedTimesClicked()
}

View file

@ -0,0 +1,173 @@
/*
* TimeLimit Copyright <C> 2019 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.timelimit.android.ui.manage.parent.blockedtimes
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.LiveData
import com.google.android.material.snackbar.Snackbar
import io.timelimit.android.R
import io.timelimit.android.data.customtypes.ImmutableBitmask
import io.timelimit.android.data.model.User
import io.timelimit.android.data.model.withConfigCopiedToOtherDates
import io.timelimit.android.databinding.ManageParentBlockedTimesFragmentBinding
import io.timelimit.android.livedata.liveDataFromValue
import io.timelimit.android.livedata.map
import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.sync.actions.UpdateParentBlockedTimesAction
import io.timelimit.android.ui.main.ActivityViewModel
import io.timelimit.android.ui.main.ActivityViewModelHolder
import io.timelimit.android.ui.main.AuthenticationFab
import io.timelimit.android.ui.main.FragmentWithCustomTitle
import io.timelimit.android.ui.manage.category.blocked_times.*
import kotlinx.android.synthetic.main.fragment_blocked_time_areas.*
import java.util.*
class ManageParentBlockedTimesFragment : Fragment(), FragmentWithCustomTitle, CopyBlockedTimeAreasDialogFragmentListener {
companion object {
private const val MINUTES_PER_DAY = 60 * 24
private const val MAX_BLOCKED_MINUTES_PER_DAY = 60 * 18 + 1
}
private val params: ManageParentBlockedTimesFragmentArgs by lazy {
ManageParentBlockedTimesFragmentArgs.fromBundle(arguments!!)
}
private val authActivity: ActivityViewModelHolder by lazy {
activity!! as ActivityViewModelHolder
}
private val auth: ActivityViewModel by lazy {
authActivity.getActivityViewModel()
}
private val parent: LiveData<User?> by lazy {
DefaultAppLogic.with(context!!).database.user().getParentUserByIdLive(params.parentUserId)
}
override fun getCustomTitle(): LiveData<String?> = parent.map { it?.name }
override fun onCopyBlockedTimeAreasConfirmed(sourceDay: Int, targetDays: Set<Int>) {
parent.value?.blockedTimes?.let { current ->
updateBlockedTimes(current, current.withConfigCopiedToOtherDates(sourceDay, targetDays))
}
}
private fun validateBlockedTimeAreas(newMask: BitSet): Boolean {
for (day in 0 until 7) {
var blocked = 0
for (minute in 0 until MINUTES_PER_DAY) {
if (newMask[day * MINUTES_PER_DAY + minute]) {
blocked++
}
}
if (blocked >= MAX_BLOCKED_MINUTES_PER_DAY) {
return false
}
}
return true
}
private fun updateBlockedTimes(oldMask: ImmutableBitmask, newMask: ImmutableBitmask) {
if (!validateBlockedTimeAreas(newMask.dataNotToModify)) {
Snackbar.make(coordinator, R.string.manage_parent_lockout_hour_rule, Snackbar.LENGTH_LONG).show()
return
}
if (
auth.tryDispatchParentAction(
UpdateParentBlockedTimesAction(
parentId = params.parentUserId,
blockedTimes = newMask
)
)
) {
Snackbar.make(coordinator, R.string.blocked_time_areas_snackbar_modified, Snackbar.LENGTH_SHORT)
.setAction(R.string.generic_undo) {
auth.tryDispatchParentAction(
UpdateParentBlockedTimesAction(
parentId = params.parentUserId,
blockedTimes = oldMask
)
)
}
.show()
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val binding = ManageParentBlockedTimesFragmentBinding.inflate(inflater, container, false)
// auth button
AuthenticationFab.manageAuthenticationFab(
fab = binding.fab,
fragment = this,
shouldHighlight = auth.shouldHighlightAuthenticationButton,
authenticatedUser = auth.authenticatedUser,
doesSupportAuth = liveDataFromValue(true)
)
binding.fab.setOnClickListener { authActivity.showAuthenticationScreen() }
// dispatching
fun requestAuthenticationOrReturnTrue(): Boolean {
if (!auth.requestAuthenticationOrReturnTrue()) {
return false
}
val authenticatedUser = auth.authenticatedUser.value?.second?.id ?: return false
val targetUser = params.parentUserId
if (authenticatedUser == targetUser) {
return true
} else {
TryResetParentBlockedTimesDialogFragment.newInstance(parentUserId = params.parentUserId).show(fragmentManager!!)
return false
}
}
// UI
binding.btnHelp.setOnClickListener {
BlockedTimeAreasHelpDialog.newInstance(forUser = true).show(fragmentManager!!)
}
binding.btnCopyToOtherDays.setOnClickListener {
if (requestAuthenticationOrReturnTrue()) {
CopyBlockedTimeAreasDialogFragment.newInstance(this@ManageParentBlockedTimesFragment).show(fragmentManager!!)
}
}
BlockedTimeAreasLogic.init(
recycler = binding.recycler,
daySpinner = binding.spinnerDay,
detailedModeCheckbox = binding.detailedMode,
requestAuthenticationOrReturnTrue = { requestAuthenticationOrReturnTrue() },
updateBlockedTimes = { a, b -> updateBlockedTimes(a, b) },
currentData = parent.map { it?.blockedTimes },
lifecycleOwner = this
)
return binding.root
}
}

View file

@ -0,0 +1,66 @@
/*
* TimeLimit Copyright <C> 2019 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.timelimit.android.ui.manage.parent.blockedtimes
import android.app.Dialog
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Observer
import io.timelimit.android.R
import io.timelimit.android.data.model.UserType
import io.timelimit.android.extensions.showSafe
import io.timelimit.android.sync.actions.ResetParentBlockedTimesAction
import io.timelimit.android.ui.main.ActivityViewModelHolder
class TryResetParentBlockedTimesDialogFragment: DialogFragment() {
companion object {
private const val DIALOG_TAG = "TryResetParentBlockedTimesDialogFragment"
private const val PARENT_USER_ID = "parentUserId"
fun newInstance(parentUserId: String) = TryResetParentBlockedTimesDialogFragment().apply {
arguments = Bundle().apply {
putString(PARENT_USER_ID, parentUserId)
}
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val parentUserId = arguments!!.getString(PARENT_USER_ID)!!
val auth = (activity!! as ActivityViewModelHolder).getActivityViewModel()
auth.authenticatedUser.observe(this, Observer {
if (it?.second?.type != UserType.Parent) {
dismissAllowingStateLoss()
}
})
return AlertDialog.Builder(context!!, theme)
.setMessage(R.string.manage_parent_blocked_times_info)
.setPositiveButton(R.string.manage_parent_blocked_action_reset) { _, _ ->
auth.tryDispatchParentAction(
ResetParentBlockedTimesAction(
parentId = parentUserId
)
)
}
.setNegativeButton(R.string.generic_cancel, null)
.create()
}
fun show(fragmentManager: FragmentManager) = showSafe(fragmentManager, DIALOG_TAG)
}

View file

@ -14,7 +14,7 @@
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:context="io.timelimit.android.ui.manage.category.blocked_times.copy.CopyBlockedTimeAreasDialogFragment">
tools:context="io.timelimit.android.ui.manage.category.blocked_times.CopyBlockedTimeAreasDialogFragment">
<data>
<variable

View file

@ -17,7 +17,6 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:id="@+id/coordinator"
tools:context="io.timelimit.android.ui.manage.category.blocked_times.BlockedTimeAreasFragment">

View file

@ -27,6 +27,7 @@
android:text="@string/generic_help" />
<TextView
android:id="@+id/text1"
android:paddingTop="8dp"
android:text="@string/blocked_time_areas_help_about"
android:textAppearance="?android:textAppearanceSmall"

View file

@ -157,6 +157,39 @@
<include android:id="@+id/manage_notifications"
layout="@layout/manage_parent_notifications" />
<androidx.cardview.widget.CardView
app:cardUseCompatPadding="true"
android:onClick="@{() -> handlers.onManageBlockedTimesClicked()}"
android:foreground="?selectableItemBackground"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:orientation="vertical"
android:padding="8dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:text="@string/manage_parent_blocked_times_title"
android:textAppearance="?android:textAppearanceLarge"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:text="@string/manage_parent_blocked_times_description"
android:textAppearance="?android:textAppearanceMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:textAppearance="?android:textAppearanceSmall"
android:text="@string/purchase_required_info_local_mode_free"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<include android:id="@+id/delete_parent"
layout="@layout/delete_parent_view" />

View file

@ -0,0 +1,89 @@
<!--
TimeLimit Copyright <C> 2019 Jonas Lochmann
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context="io.timelimit.android.ui.manage.parent.blockedtimes.ManageParentBlockedTimesFragment">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/coordinator">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
app:layout_scrollFlags="scroll|enterAlways|snap"
android:background="?android:windowBackground"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Spinner
android:layout_gravity="center_vertical"
android:layout_weight="2"
android:id="@+id/spinner_day"
android:layout_width="0dp"
android:layout_height="wrap_content" />
<Button
android:id="@+id/btn_help"
android:layout_weight="1"
android:text="@string/generic_help"
android:layout_width="0dp"
android:layout_height="wrap_content" />
</LinearLayout>
<Button
android:id="@+id/btn_copy_to_other_days"
android:text="@string/blocked_time_areas_copy_to_other_days"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<CheckBox
android:text="@string/blocked_time_areas_checkbox_detailed"
android:id="@+id/detailed_mode"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
app:fabSize="normal"
android:src="@drawable/ic_lock_open_white_24dp"
android:layout_margin="16dp"
android:layout_gravity="end|bottom"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

View file

@ -38,5 +38,9 @@
<include android:id="@+id/child_without_premium"
layout="@layout/new_login_fragment_child_without_premium" />
<include layout="@layout/new_login_fragment_blocked_time" />
<include layout="@layout/new_login_fragment_missing_trusted_time" />
</io.timelimit.android.ui.view.SafeViewFlipper>
</layout>

View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
TimeLimit Copyright <C> 2019 Jonas Lochmann
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<TextView
android:gravity="center_horizontal"
android:textAppearance="?android:textAppearanceMedium"
android:padding="8dp"
android:text="@string/login_blocked_time"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</layout>

View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
TimeLimit Copyright <C> 2019 Jonas Lochmann
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<TextView
android:gravity="center_horizontal"
android:textAppearance="?android:textAppearanceMedium"
android:padding="8dp"
android:text="@string/login_missing_trusted_time"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</layout>

View file

@ -259,6 +259,13 @@
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_manageParentFragment_to_manageParentBlockedTimesFragment"
app:destination="@id/manageParentBlockedTimesFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
</fragment>
<fragment
android:id="@+id/changeParentPasswordFragment"
@ -387,4 +394,13 @@
android:name="deviceId"
app:argType="string" />
</fragment>
<fragment
android:id="@+id/manageParentBlockedTimesFragment"
android:name="io.timelimit.android.ui.manage.parent.blockedtimes.ManageParentBlockedTimesFragment"
android:label="manage_parent_blocked_times_fragment"
tools:layout="@layout/manage_parent_blocked_times_fragment" >
<argument
android:name="parentUserId"
app:argType="string" />
</fragment>
</navigation>

View file

@ -27,4 +27,6 @@
<string name="login_child_already_current_user">Du bist schon der aktuelle Benutzer dieses Gerätes</string>
<string name="login_child_info">Dadurch wirst du als aktueller Nutzer dieses Gerätes festgelegt</string>
<string name="login_child_done_toast">Der Benutzer dieses Gerätes wurde geändert</string>
<string name="login_missing_trusted_time">Zum Anmelden mit diesem Benutzer wird die Uhrzeit benötigt</string>
<string name="login_blocked_time">Das Anmelden mit diesem Benutzer zu dieser Zeit wurde gesperrt</string>
</resources>

View file

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
TimeLimit Copyright <C> 2019 Jonas Lochmann
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<resources>
<string name="manage_parent_blocked_times_title">Zeitschloss</string>
<string name="manage_parent_blocked_times_description">
Hiermit können Zeiten festgelegt werden, an denen sich ein Elternteil nicht anmelden kann.
Das ist bei der Selbstbeschränkung relevant.
</string>
<string name="manage_parent_blocked_times_info">
Ein Elternteil kann nur für sich selbst Anmeldebeschränkungen festlegen.
Andere Elternteile können diese nur zurücksetzen.
</string>
<string name="manage_parent_blocked_action_reset">Einstellungen zurücksetzen</string>
<string name="manage_parent_lockout_hour_rule">Um ein Aussperren zu verhindern können maximal 18 Stunden je Tag blockiert werden</string>
</resources>

View file

@ -27,4 +27,6 @@
<string name="login_child_already_current_user">You are already the current user of this device</string>
<string name="login_child_info">This will set you as the current user of this device</string>
<string name="login_child_done_toast">The user of this device was changed</string>
<string name="login_missing_trusted_time">The current time is required to sign in with this user</string>
<string name="login_blocked_time">Signing in as this user at this time was blocked</string>
</resources>

View file

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
TimeLimit Copyright <C> 2019 Jonas Lochmann
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<resources>
<string name="manage_parent_blocked_times_title">Time lock</string>
<string name="manage_parent_blocked_times_description">
This allows to set times at which the parent can not sign in.
This is relevant for limiting ones own usage.
</string>
<string name="manage_parent_blocked_times_info">
A parent can only add limits for itself.
Other parents can only reset the limits.
</string>
<string name="manage_parent_blocked_action_reset">Reset limits</string>
<string name="manage_parent_lockout_hour_rule">To prevent losing access, you can only block 18 hours per day</string>
</resources>