diff --git a/app/schemas/io.timelimit.android.data.RoomDatabase/32.json b/app/schemas/io.timelimit.android.data.RoomDatabase/32.json
new file mode 100644
index 0000000..7119915
--- /dev/null
+++ b/app/schemas/io.timelimit.android.data.RoomDatabase/32.json
@@ -0,0 +1,1115 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 32,
+ "identityHash": "9935dfc30c9a5e09b8d71f388d526eb1",
+ "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, `flags` INTEGER 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
+ },
+ {
+ "fieldPath": "flags",
+ "columnName": "flags",
+ "affinity": "INTEGER",
+ "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, `extra_time_day` 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, `sort` 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": "extraTimeDay",
+ "columnName": "extra_time_day",
+ "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
+ },
+ {
+ "fieldPath": "sort",
+ "columnName": "sort",
+ "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, `start_time_of_day` INTEGER NOT NULL, `end_time_of_day` INTEGER NOT NULL, PRIMARY KEY(`category_id`, `day_of_epoch`, `start_time_of_day`, `end_time_of_day`))",
+ "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
+ },
+ {
+ "fieldPath": "startTimeOfDay",
+ "columnName": "start_time_of_day",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "endTimeOfDay",
+ "columnName": "end_time_of_day",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "category_id",
+ "day_of_epoch",
+ "start_time_of_day",
+ "end_time_of_day"
+ ],
+ "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, `start_minute_of_day` INTEGER NOT NULL, `end_minute_of_day` INTEGER NOT NULL, `session_duration_milliseconds` INTEGER NOT NULL, `session_pause_milliseconds` 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
+ },
+ {
+ "fieldPath": "startMinuteOfDay",
+ "columnName": "start_minute_of_day",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "endMinuteOfDay",
+ "columnName": "end_minute_of_day",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sessionDurationMilliseconds",
+ "columnName": "session_duration_milliseconds",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sessionPauseMilliseconds",
+ "columnName": "session_pause_milliseconds",
+ "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": []
+ },
+ {
+ "tableName": "user_key",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `key` BLOB NOT NULL, `last_use` INTEGER NOT NULL, PRIMARY KEY(`user_id`), FOREIGN KEY(`user_id`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "publicKey",
+ "columnName": "key",
+ "affinity": "BLOB",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastUse",
+ "columnName": "last_use",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_user_key_key",
+ "unique": true,
+ "columnNames": [
+ "key"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_key_key` ON `${TABLE_NAME}` (`key`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "user",
+ "onDelete": "CASCADE",
+ "onUpdate": "CASCADE",
+ "columns": [
+ "user_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "session_duration",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`category_id` TEXT NOT NULL, `max_session_duration` INTEGER NOT NULL, `session_pause_duration` INTEGER NOT NULL, `start_minute_of_day` INTEGER NOT NULL, `end_minute_of_day` INTEGER NOT NULL, `last_usage` INTEGER NOT NULL, `last_session_duration` INTEGER NOT NULL, PRIMARY KEY(`category_id`, `max_session_duration`, `session_pause_duration`, `start_minute_of_day`, `end_minute_of_day`), FOREIGN KEY(`category_id`) REFERENCES `category`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "categoryId",
+ "columnName": "category_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "maxSessionDuration",
+ "columnName": "max_session_duration",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sessionPauseDuration",
+ "columnName": "session_pause_duration",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "startMinuteOfDay",
+ "columnName": "start_minute_of_day",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "endMinuteOfDay",
+ "columnName": "end_minute_of_day",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastUsage",
+ "columnName": "last_usage",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastSessionDuration",
+ "columnName": "last_session_duration",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "category_id",
+ "max_session_duration",
+ "session_pause_duration",
+ "start_minute_of_day",
+ "end_minute_of_day"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "session_duration_index_category_id",
+ "unique": false,
+ "columnNames": [
+ "category_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `session_duration_index_category_id` ON `${TABLE_NAME}` (`category_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "category",
+ "onDelete": "CASCADE",
+ "onUpdate": "CASCADE",
+ "columns": [
+ "category_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "user_limit_login_category",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `category_id` TEXT NOT NULL, PRIMARY KEY(`user_id`), FOREIGN KEY(`user_id`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`category_id`) REFERENCES `category`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "categoryId",
+ "columnName": "category_id",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "user_limit_login_category_index_category_id",
+ "unique": false,
+ "columnNames": [
+ "category_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `user_limit_login_category_index_category_id` ON `${TABLE_NAME}` (`category_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "user",
+ "onDelete": "CASCADE",
+ "onUpdate": "CASCADE",
+ "columns": [
+ "user_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ },
+ {
+ "table": "category",
+ "onDelete": "CASCADE",
+ "onUpdate": "CASCADE",
+ "columns": [
+ "category_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "category_network_id",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`category_id` TEXT NOT NULL, `network_item_id` TEXT NOT NULL, `hashed_network_id` TEXT NOT NULL, PRIMARY KEY(`category_id`, `network_item_id`), FOREIGN KEY(`category_id`) REFERENCES `category`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "categoryId",
+ "columnName": "category_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "networkItemId",
+ "columnName": "network_item_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hashedNetworkId",
+ "columnName": "hashed_network_id",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "category_id",
+ "network_item_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": [
+ {
+ "table": "category",
+ "onDelete": "CASCADE",
+ "onUpdate": "CASCADE",
+ "columns": [
+ "category_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ }
+ ],
+ "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, '9935dfc30c9a5e09b8d71f388d526eb1')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index e7bc317..6962ca5 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -33,6 +33,9 @@
+
+
+
diff --git a/app/src/main/java/io/timelimit/android/crypto/Sha512.kt b/app/src/main/java/io/timelimit/android/crypto/Sha512.kt
index d44087d..1caa8d4 100644
--- a/app/src/main/java/io/timelimit/android/crypto/Sha512.kt
+++ b/app/src/main/java/io/timelimit/android/crypto/Sha512.kt
@@ -1,5 +1,5 @@
/*
- * TimeLimit Copyright 2019 Jonas Lochmann
+ * TimeLimit Copyright 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
@@ -24,5 +24,5 @@ object Sha512 {
return HexString.toHex(hashSync(data.toByteArray(charset("UTF-8"))))
}
- fun hashSync(data: ByteArray) = messageDigest.digest(data)
+ fun hashSync(data: ByteArray): ByteArray = messageDigest.digest(data)
}
diff --git a/app/src/main/java/io/timelimit/android/data/Database.kt b/app/src/main/java/io/timelimit/android/data/Database.kt
index 2f9feac..e698665 100644
--- a/app/src/main/java/io/timelimit/android/data/Database.kt
+++ b/app/src/main/java/io/timelimit/android/data/Database.kt
@@ -38,6 +38,7 @@ interface Database {
fun sessionDuration(): SessionDurationDao
fun derivedDataDao(): DerivedDataDao
fun userLimitLoginCategoryDao(): UserLimitLoginCategoryDao
+ fun categoryNetworkId(): CategoryNetworkIdDao
fun runInTransaction(block: () -> T): T
fun runInUnobservedTransaction(block: () -> T): T
diff --git a/app/src/main/java/io/timelimit/android/data/Migrations.kt b/app/src/main/java/io/timelimit/android/data/Migrations.kt
index f17fda2..a0c2b5d 100644
--- a/app/src/main/java/io/timelimit/android/data/Migrations.kt
+++ b/app/src/main/java/io/timelimit/android/data/Migrations.kt
@@ -231,4 +231,10 @@ object DatabaseMigrations {
database.execSQL("CREATE INDEX IF NOT EXISTS `user_limit_login_category_index_category_id` ON `user_limit_login_category` (`category_id`)")
}
}
+
+ val MIGRATE_TO_V32 = object: Migration(31, 32) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL("CREATE TABLE IF NOT EXISTS `category_network_id` (`category_id` TEXT NOT NULL, `network_item_id` TEXT NOT NULL, `hashed_network_id` TEXT NOT NULL, PRIMARY KEY(`category_id`, `network_item_id`), FOREIGN KEY(`category_id`) REFERENCES `category`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )")
+ }
+ }
}
diff --git a/app/src/main/java/io/timelimit/android/data/RoomDatabase.kt b/app/src/main/java/io/timelimit/android/data/RoomDatabase.kt
index 7250969..c43f32a 100644
--- a/app/src/main/java/io/timelimit/android/data/RoomDatabase.kt
+++ b/app/src/main/java/io/timelimit/android/data/RoomDatabase.kt
@@ -47,8 +47,9 @@ import java.util.concurrent.TimeUnit
AllowedContact::class,
UserKey::class,
SessionDuration::class,
- UserLimitLoginCategory::class
-], version = 31)
+ UserLimitLoginCategory::class,
+ CategoryNetworkId::class
+], version = 32)
abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database {
companion object {
private val lock = Object()
@@ -113,7 +114,8 @@ abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database
DatabaseMigrations.MIGRATE_TO_V28,
DatabaseMigrations.MIGRATE_TO_V29,
DatabaseMigrations.MIGRATE_TO_V30,
- DatabaseMigrations.MIGRATE_TO_V31
+ DatabaseMigrations.MIGRATE_TO_V31,
+ DatabaseMigrations.MIGRATE_TO_V32
)
.setQueryExecutor(Threads.database)
.build()
diff --git a/app/src/main/java/io/timelimit/android/data/backup/DatabaseBackupLowlevel.kt b/app/src/main/java/io/timelimit/android/data/backup/DatabaseBackupLowlevel.kt
index 7687d12..ac65ad2 100644
--- a/app/src/main/java/io/timelimit/android/data/backup/DatabaseBackupLowlevel.kt
+++ b/app/src/main/java/io/timelimit/android/data/backup/DatabaseBackupLowlevel.kt
@@ -43,6 +43,7 @@ object DatabaseBackupLowlevel {
private const val USER_KEY = "userKey"
private const val SESSION_DURATION = "sessionDuration"
private const val USER_LIMIT_LOGIN_CATEGORY = "userLimitLoginCategory"
+ private const val CATEGORY_NETWORK_ID = "categoryNetworkId"
fun outputAsBackupJson(database: Database, outputStream: OutputStream) {
val writer = JsonWriter(OutputStreamWriter(outputStream, Charsets.UTF_8))
@@ -90,6 +91,7 @@ object DatabaseBackupLowlevel {
handleCollection(USER_KEY) { offset, pageSize -> database.userKey().getUserKeyPageSync(offset, pageSize) }
handleCollection(SESSION_DURATION) { offset, pageSize -> database.sessionDuration().getSessionDurationPageSync(offset, pageSize) }
handleCollection(USER_LIMIT_LOGIN_CATEGORY) { offset, pageSize -> database.userLimitLoginCategoryDao().getAllowedContactPageSync(offset, pageSize) }
+ handleCollection(CATEGORY_NETWORK_ID) { offset, pageSize -> database.categoryNetworkId().getPageSync(offset, pageSize) }
writer.endObject().flush()
}
@@ -98,6 +100,7 @@ object DatabaseBackupLowlevel {
val reader = JsonReader(InputStreamReader(inputStream, Charsets.UTF_8))
var userLoginLimitCategories = emptyList()
+ var categoryNetworkId = emptyList()
database.runInTransaction {
database.deleteAllData()
@@ -251,12 +254,26 @@ object DatabaseBackupLowlevel {
reader.endArray()
}
+ CATEGORY_NETWORK_ID -> {
+ reader.beginArray()
+
+ mutableListOf().let { list ->
+ while (reader.hasNext()) {
+ list.add(CategoryNetworkId.parse(reader))
+ }
+
+ categoryNetworkId = list
+ }
+
+ reader.endArray()
+ }
else -> reader.skipValue()
}
}
reader.endObject()
- database.userLimitLoginCategoryDao().addItemsSync(userLoginLimitCategories)
+ if (userLoginLimitCategories.isNotEmpty()) { database.userLimitLoginCategoryDao().addItemsSync(userLoginLimitCategories) }
+ if (categoryNetworkId.isNotEmpty()) { database.categoryNetworkId().insertItemsSync(categoryNetworkId) }
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/io/timelimit/android/data/dao/CategoryNetworkIdDao.kt b/app/src/main/java/io/timelimit/android/data/dao/CategoryNetworkIdDao.kt
new file mode 100644
index 0000000..56ef5e5
--- /dev/null
+++ b/app/src/main/java/io/timelimit/android/data/dao/CategoryNetworkIdDao.kt
@@ -0,0 +1,49 @@
+/*
+ * TimeLimit Copyright 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 .
+ */
+package io.timelimit.android.data.dao
+
+import androidx.lifecycle.LiveData
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.Query
+import io.timelimit.android.data.model.CategoryNetworkId
+
+@Dao
+interface CategoryNetworkIdDao {
+ @Query("SELECT * FROM category_network_id LIMIT :pageSize OFFSET :offset")
+ fun getPageSync(offset: Int, pageSize: Int): List
+
+ @Query("SELECT * FROM category_network_id WHERE category_id = :categoryId")
+ fun getByCategoryIdLive(categoryId: String): LiveData>
+
+ @Query("SELECT * FROM category_network_id WHERE category_id = :categoryId")
+ fun getByCategoryIdSync(categoryId: String): List
+
+ @Query("SELECT * FROM category_network_id WHERE category_id = :categoryId AND network_item_id = :itemId")
+ fun getByCategoryIdAndItemIdSync(categoryId: String, itemId: String): CategoryNetworkId?
+
+ @Query("SELECT COUNT(*) FROM category_network_id WHERE category_id = :categoryId")
+ fun countByCategoryIdSync(categoryId: String): Long
+
+ @Insert
+ fun insertItemsSync(items: List)
+
+ @Insert
+ fun insertItemSync(item: CategoryNetworkId)
+
+ @Query("DELETE FROM category_network_id WHERE category_id = :categoryId")
+ fun deleteByCategoryId(categoryId: String)
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/timelimit/android/data/invalidation/Tables.kt b/app/src/main/java/io/timelimit/android/data/invalidation/Tables.kt
index 1aba83a..d6fc2d9 100644
--- a/app/src/main/java/io/timelimit/android/data/invalidation/Tables.kt
+++ b/app/src/main/java/io/timelimit/android/data/invalidation/Tables.kt
@@ -32,7 +32,8 @@ enum class Table {
UsedTimeItem,
User,
UserKey,
- UserLimitLoginCategory
+ UserLimitLoginCategory,
+ CategoryNetworkId
}
object TableNames {
@@ -52,6 +53,7 @@ object TableNames {
const val USER = "user"
const val USER_KEY = "user_key"
const val USER_LIMIT_LOGIN_CATEGORY = "user_limit_login_category"
+ const val CATEGORY_NETWORK_ID = "category_network_id"
}
object TableUtil {
@@ -72,6 +74,7 @@ object TableUtil {
Table.User -> TableNames.USER
Table.UserKey -> TableNames.USER_KEY
Table.UserLimitLoginCategory -> TableNames.USER_LIMIT_LOGIN_CATEGORY
+ Table.CategoryNetworkId -> TableNames.CATEGORY_NETWORK_ID
}
fun toEnum(value: String): Table = when (value) {
@@ -91,6 +94,7 @@ object TableUtil {
TableNames.USER -> Table.User
TableNames.USER_KEY -> Table.UserKey
TableNames.USER_LIMIT_LOGIN_CATEGORY -> Table.UserLimitLoginCategory
+ TableNames.CATEGORY_NETWORK_ID -> Table.CategoryNetworkId
else -> throw IllegalArgumentException()
}
}
\ No newline at end of file
diff --git a/app/src/main/java/io/timelimit/android/data/model/CategoryNetworkId.kt b/app/src/main/java/io/timelimit/android/data/model/CategoryNetworkId.kt
new file mode 100644
index 0000000..3254173
--- /dev/null
+++ b/app/src/main/java/io/timelimit/android/data/model/CategoryNetworkId.kt
@@ -0,0 +1,98 @@
+/*
+ * TimeLimit Copyright 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 .
+ */
+package io.timelimit.android.data.model
+
+import android.util.JsonReader
+import android.util.JsonWriter
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.ForeignKey
+import io.timelimit.android.crypto.HexString
+import io.timelimit.android.crypto.Sha512
+import io.timelimit.android.data.IdGenerator
+import io.timelimit.android.data.JsonSerializable
+
+@Entity(
+ tableName = "category_network_id",
+ primaryKeys = ["category_id", "network_item_id"],
+ foreignKeys = [
+ ForeignKey(
+ entity = Category::class,
+ parentColumns = ["id"],
+ childColumns = ["category_id"],
+ onUpdate = ForeignKey.CASCADE,
+ onDelete = ForeignKey.CASCADE
+ )
+ ]
+)
+data class CategoryNetworkId(
+ @ColumnInfo(name = "category_id")
+ val categoryId: String,
+ @ColumnInfo(name = "network_item_id")
+ val networkItemId: String,
+ @ColumnInfo(name = "hashed_network_id")
+ val hashedNetworkId: String
+): JsonSerializable {
+ companion object {
+ private const val CATEGORY_ID = "categoryId"
+ private const val NETWORK_ITEM_ID = "networkItemId"
+ private const val HASHED_NETWORK_ID = "hashedNetworkId"
+ const val ANONYMIZED_NETWORK_ID_LENGTH = 8
+ const val MAX_ITEMS = 8
+
+ fun anonymizeNetworkId(itemId: String, networkId: String) = Sha512.hashSync(itemId + networkId).substring(0, ANONYMIZED_NETWORK_ID_LENGTH)
+
+ fun parse(reader: JsonReader): CategoryNetworkId {
+ var categoryId: String? = null
+ var networkItemId: String? = null
+ var hashedNetworkId: String? = null
+
+ reader.beginObject()
+ while (reader.hasNext()) {
+ when (reader.nextName()) {
+ CATEGORY_ID -> categoryId = reader.nextString()
+ NETWORK_ITEM_ID -> networkItemId = reader.nextString()
+ HASHED_NETWORK_ID -> hashedNetworkId = reader.nextString()
+ else -> reader.skipValue()
+ }
+ }
+ reader.endObject()
+
+ return CategoryNetworkId(
+ categoryId = categoryId!!,
+ networkItemId = networkItemId!!,
+ hashedNetworkId = hashedNetworkId!!
+ )
+ }
+ }
+
+ init {
+ IdGenerator.assertIdValid(categoryId)
+ IdGenerator.assertIdValid(networkItemId)
+ if (hashedNetworkId.length != ANONYMIZED_NETWORK_ID_LENGTH) throw IllegalArgumentException()
+ HexString.assertIsHexString(hashedNetworkId)
+ }
+
+ override fun serialize(writer: JsonWriter) {
+ writer.beginObject()
+
+ writer.name(CATEGORY_ID).value(categoryId)
+ writer.name(NETWORK_ITEM_ID).value(networkItemId)
+ writer.name(HASHED_NETWORK_ID).value(hashedNetworkId)
+
+ writer.endObject()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/timelimit/android/data/model/derived/CategoryRelatedData.kt b/app/src/main/java/io/timelimit/android/data/model/derived/CategoryRelatedData.kt
index bb6eeb6..b8ea9df 100644
--- a/app/src/main/java/io/timelimit/android/data/model/derived/CategoryRelatedData.kt
+++ b/app/src/main/java/io/timelimit/android/data/model/derived/CategoryRelatedData.kt
@@ -17,28 +17,28 @@
package io.timelimit.android.data.model.derived
import io.timelimit.android.data.Database
-import io.timelimit.android.data.model.Category
-import io.timelimit.android.data.model.SessionDuration
-import io.timelimit.android.data.model.TimeLimitRule
-import io.timelimit.android.data.model.UsedTimeItem
+import io.timelimit.android.data.model.*
data class CategoryRelatedData(
val category: Category,
val rules: List,
val usedTimes: List,
- val durations: List
+ val durations: List,
+ val networks: List
) {
companion object {
fun load(category: Category, database: Database): CategoryRelatedData = database.runInUnobservedTransaction {
val rules = database.timeLimitRules().getTimeLimitRulesByCategorySync(category.id)
val usedTimes = database.usedTimes().getUsedTimeItemsByCategoryId(category.id)
val durations = database.sessionDuration().getSessionDurationItemsByCategoryIdSync(category.id)
+ val networks = database.categoryNetworkId().getByCategoryIdSync(category.id)
CategoryRelatedData(
category = category,
rules = rules,
usedTimes = usedTimes,
- durations = durations
+ durations = durations,
+ networks = networks
)
}
}
@@ -48,6 +48,7 @@ data class CategoryRelatedData(
updateRules: Boolean,
updateTimes: Boolean,
updateDurations: Boolean,
+ updateNetworks: Boolean,
database: Database
): CategoryRelatedData = database.runInUnobservedTransaction {
if (category.id != this.category.id) {
@@ -57,15 +58,17 @@ data class CategoryRelatedData(
val rules = if (updateRules) database.timeLimitRules().getTimeLimitRulesByCategorySync(category.id) else rules
val usedTimes = if (updateTimes) database.usedTimes().getUsedTimeItemsByCategoryId(category.id) else usedTimes
val durations = if (updateDurations) database.sessionDuration().getSessionDurationItemsByCategoryIdSync(category.id) else durations
+ val networks = if (updateNetworks) database.categoryNetworkId().getByCategoryIdSync(category.id) else networks
- if (category == this.category && rules == this.rules && usedTimes == this.usedTimes && durations == this.durations) {
+ if (category == this.category && rules == this.rules && usedTimes == this.usedTimes && durations == this.durations && networks == this.networks) {
this
} else {
CategoryRelatedData(
category = category,
rules = rules,
usedTimes = usedTimes,
- durations = durations
+ durations = durations,
+ networks = networks
)
}
}
diff --git a/app/src/main/java/io/timelimit/android/data/model/derived/UserRelatedData.kt b/app/src/main/java/io/timelimit/android/data/model/derived/UserRelatedData.kt
index ce47508..f4c6c93 100644
--- a/app/src/main/java/io/timelimit/android/data/model/derived/UserRelatedData.kt
+++ b/app/src/main/java/io/timelimit/android/data/model/derived/UserRelatedData.kt
@@ -38,7 +38,8 @@ data class UserRelatedData(
private val relatedTables = arrayOf(
Table.User, Table.Category, Table.TimeLimitRule,
- Table.UsedTimeItem, Table.SessionDuration, Table.CategoryApp
+ Table.UsedTimeItem, Table.SessionDuration, Table.CategoryApp,
+ Table.CategoryNetworkId
)
fun load(user: User, database: Database): UserRelatedData = database.runInUnobservedTransaction {
@@ -82,9 +83,10 @@ data class UserRelatedData(
private var usedTimesInvalidated = false
private var sessionDurationsInvalidated = false
private var categoryAppsInvalidated = false
+ private var categoryNetworksInvalidated = false
private val invalidated
- get() = userInvalidated || categoriesInvalidated || rulesInvalidated || usedTimesInvalidated || sessionDurationsInvalidated || categoryAppsInvalidated
+ get() = userInvalidated || categoriesInvalidated || rulesInvalidated || usedTimesInvalidated || sessionDurationsInvalidated || categoryAppsInvalidated || categoryNetworksInvalidated
override fun onInvalidated(tables: Set) {
tables.forEach {
@@ -95,6 +97,7 @@ data class UserRelatedData(
Table.UsedTimeItem -> usedTimesInvalidated = true
Table.SessionDuration -> sessionDurationsInvalidated = true
Table.CategoryApp -> categoryAppsInvalidated = true
+ Table.CategoryNetworkId -> categoryNetworksInvalidated = true
else -> {/* do nothing */}
}
}
@@ -117,20 +120,22 @@ data class UserRelatedData(
database = database,
updateDurations = sessionDurationsInvalidated,
updateRules = rulesInvalidated,
- updateTimes = usedTimesInvalidated
+ updateTimes = usedTimesInvalidated,
+ updateNetworks = categoryNetworksInvalidated
) ?: CategoryRelatedData.load(
category = category,
database = database
)
}
- } else if (sessionDurationsInvalidated || rulesInvalidated || usedTimesInvalidated) {
+ } else if (sessionDurationsInvalidated || rulesInvalidated || usedTimesInvalidated || categoryNetworksInvalidated) {
categories.map {
it.update(
category = it.category,
database = database,
updateDurations = sessionDurationsInvalidated,
updateRules = rulesInvalidated,
- updateTimes = usedTimesInvalidated
+ updateTimes = usedTimesInvalidated,
+ updateNetworks = categoryNetworksInvalidated
)
}
} else {
diff --git a/app/src/main/java/io/timelimit/android/integration/platform/PlatformIntegration.kt b/app/src/main/java/io/timelimit/android/integration/platform/PlatformIntegration.kt
index f12ffb3..32abd84 100644
--- a/app/src/main/java/io/timelimit/android/integration/platform/PlatformIntegration.kt
+++ b/app/src/main/java/io/timelimit/android/integration/platform/PlatformIntegration.kt
@@ -74,6 +74,8 @@ abstract class PlatformIntegration(
abstract fun restartApp()
+ abstract fun getCurrentNetworkId(): NetworkId
+
var installedAppsChangeListener: Runnable? = null
var systemClockChangeListener: Runnable? = null
}
@@ -216,4 +218,12 @@ data class BatteryStatus(
throw IllegalArgumentException()
}
}
-}
\ No newline at end of file
+}
+
+sealed class NetworkId {
+ object NoNetworkConnected : NetworkId()
+ object MissingPermission: NetworkId()
+ data class Network(val id: String): NetworkId()
+}
+
+fun NetworkId.getNetworkIdOrNull(): String? = if (this is NetworkId.Network) this.id else null
\ No newline at end of file
diff --git a/app/src/main/java/io/timelimit/android/integration/platform/android/AndroidIntegration.kt b/app/src/main/java/io/timelimit/android/integration/platform/android/AndroidIntegration.kt
index 9cbfe83..be9965e 100644
--- a/app/src/main/java/io/timelimit/android/integration/platform/android/AndroidIntegration.kt
+++ b/app/src/main/java/io/timelimit/android/integration/platform/android/AndroidIntegration.kt
@@ -83,6 +83,7 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio
private val deviceAdmin = ComponentName(context.applicationContext, AdminReceiver::class.java)
private val overlay = OverlayUtil(context as Application)
private val battery = BatteryStatusUtil(context)
+ private val connectedNetwork = ConnectedNetworkUtil(context)
init {
AppsChangeListener.registerBroadcastReceiver(this.context, object : BroadcastReceiver() {
@@ -520,4 +521,6 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio
}
}
}
+
+ override fun getCurrentNetworkId(): NetworkId = connectedNetwork.getNetworkId()
}
diff --git a/app/src/main/java/io/timelimit/android/integration/platform/android/ConnectedNetworkUtil.kt b/app/src/main/java/io/timelimit/android/integration/platform/android/ConnectedNetworkUtil.kt
new file mode 100644
index 0000000..95a96b7
--- /dev/null
+++ b/app/src/main/java/io/timelimit/android/integration/platform/android/ConnectedNetworkUtil.kt
@@ -0,0 +1,41 @@
+/*
+ * TimeLimit Copyright 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 .
+ */
+package io.timelimit.android.integration.platform.android
+
+import android.content.Context
+import android.net.wifi.WifiInfo
+import android.net.wifi.WifiManager
+import io.timelimit.android.crypto.Sha512
+import io.timelimit.android.integration.platform.NetworkId
+
+class ConnectedNetworkUtil (context: Context) {
+ private val workContext = context.applicationContext
+ private val wifiManager = workContext.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
+
+ fun getNetworkId(): NetworkId {
+ val info: WifiInfo? = wifiManager.connectionInfo
+
+ info ?: return NetworkId.NoNetworkConnected
+
+ val ssid: String? = info.ssid
+ val bssid: String? = info.bssid
+
+ if (ssid == null || bssid == null) return NetworkId.NoNetworkConnected
+ if (ssid == WifiManager.UNKNOWN_SSID) return NetworkId.MissingPermission
+
+ return NetworkId.Network(Sha512.hashSync(ssid + bssid).substring(0, 16))
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/timelimit/android/integration/platform/android/NotificationListener.kt b/app/src/main/java/io/timelimit/android/integration/platform/android/NotificationListener.kt
index f5e6c92..8a0df12 100644
--- a/app/src/main/java/io/timelimit/android/integration/platform/android/NotificationListener.kt
+++ b/app/src/main/java/io/timelimit/android/integration/platform/android/NotificationListener.kt
@@ -31,6 +31,8 @@ import io.timelimit.android.async.Threads
import io.timelimit.android.coroutines.executeAndWait
import io.timelimit.android.coroutines.runAsync
import io.timelimit.android.data.model.UserType
+import io.timelimit.android.integration.platform.getNetworkIdOrNull
+import io.timelimit.android.livedata.waitForNonNullValue
import io.timelimit.android.logic.*
import io.timelimit.android.logic.blockingreason.AppBaseHandling
import io.timelimit.android.logic.blockingreason.CategoryItselfHandling
@@ -117,6 +119,7 @@ class NotificationListener: NotificationListenerService() {
BlockingReason.NotificationsAreBlocked -> getString(R.string.lock_reason_short_notification_blocking)
BlockingReason.BatteryLimit -> getString(R.string.lock_reason_short_battery_limit)
BlockingReason.SessionDurationLimit -> getString(R.string.lock_reason_short_session_duration)
+ BlockingReason.MissingRequiredNetwork -> getString(R.string.lock_reason_short_missing_required_network)
BlockingReason.None -> throw IllegalStateException()
}
)
@@ -165,12 +168,16 @@ class NotificationListener: NotificationListenerService() {
val time = RealTime.newInstance()
val battery = appLogic.platformIntegration.getBatteryStatus()
val allowNotificationFilter = deviceAndUserRelatedData.deviceRelatedData.isConnectedAndHasPremium || deviceAndUserRelatedData.deviceRelatedData.isLocalMode
+ val networkId = if (appHandling.needsNetworkId) appLogic.platformIntegration.getCurrentNetworkId().getNetworkIdOrNull() else null
+ val hasPremiumOrLocalMode = appLogic.fullVersion.shouldProvideFullVersionFunctions.waitForNonNullValue()
appLogic.realTimeLogic.getRealTime(time)
val categoryHandlings = appHandling.categoryIds.map { categoryId ->
+ val categoryRelatedData = deviceAndUserRelatedData.userRelatedData.categoryById[categoryId]!!
+
CategoryItselfHandling.calculate(
- categoryRelatedData = deviceAndUserRelatedData.userRelatedData.categoryById[categoryId]!!,
+ categoryRelatedData = categoryRelatedData,
user = deviceAndUserRelatedData.userRelatedData,
assumeCurrentDevice = CurrentDeviceLogic.handleDeviceAsCurrentDevice(
device = deviceAndUserRelatedData.deviceRelatedData,
@@ -178,7 +185,9 @@ class NotificationListener: NotificationListenerService() {
),
batteryStatus = battery,
shouldTrustTimeTemporarily = time.shouldTrustTimeTemporarily,
- timeInMillis = time.timeInMillis
+ timeInMillis = time.timeInMillis,
+ currentNetworkId = networkId,
+ hasPremiumOrLocalMode = hasPremiumOrLocalMode
)
}
diff --git a/app/src/main/java/io/timelimit/android/integration/platform/dummy/DummyIntegration.kt b/app/src/main/java/io/timelimit/android/integration/platform/dummy/DummyIntegration.kt
index e6fecd8..741dd68 100644
--- a/app/src/main/java/io/timelimit/android/integration/platform/dummy/DummyIntegration.kt
+++ b/app/src/main/java/io/timelimit/android/integration/platform/dummy/DummyIntegration.kt
@@ -175,4 +175,6 @@ class DummyIntegration(
override fun setForceNetworkTime(enable: Boolean) = Unit
override fun restartApp() = Unit
+
+ override fun getCurrentNetworkId(): NetworkId = NetworkId.NoNetworkConnected
}
diff --git a/app/src/main/java/io/timelimit/android/logic/BackgroundTaskLogic.kt b/app/src/main/java/io/timelimit/android/logic/BackgroundTaskLogic.kt
index fbc8fc1..bb36846 100644
--- a/app/src/main/java/io/timelimit/android/logic/BackgroundTaskLogic.kt
+++ b/app/src/main/java/io/timelimit/android/logic/BackgroundTaskLogic.kt
@@ -31,9 +31,11 @@ import io.timelimit.android.date.DateInTimezone
import io.timelimit.android.integration.platform.AppStatusMessage
import io.timelimit.android.integration.platform.ProtectionLevel
import io.timelimit.android.integration.platform.android.AccessibilityService
+import io.timelimit.android.integration.platform.getNetworkIdOrNull
import io.timelimit.android.livedata.*
import io.timelimit.android.logic.blockingreason.AppBaseHandling
import io.timelimit.android.logic.blockingreason.CategoryHandlingCache
+import io.timelimit.android.logic.blockingreason.needsNetworkId
import io.timelimit.android.sync.actions.UpdateDeviceStatusAction
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
import io.timelimit.android.ui.IsAppInForeground
@@ -266,16 +268,6 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
}
}
- fun reportStatusToCategoryHandlingCache(userRelatedData: UserRelatedData) {
- categoryHandlingCache.reportStatus(
- user = userRelatedData,
- timeInMillis = nowTimestamp,
- shouldTrustTimeTemporarily = realTime.shouldTrustTimeTemporarily,
- assumeCurrentDevice = CurrentDeviceLogic.handleDeviceAsCurrentDevice(deviceRelatedData, userRelatedData),
- batteryStatus = batteryStatus
- )
- }; reportStatusToCategoryHandlingCache(userRelatedData)
-
val foregroundApps = appLogic.platformIntegration.getForegroundApps(
appLogic.getForegroundAppQueryInterval(),
appLogic.getEnableMultiAppDetection()
@@ -304,6 +296,21 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
pauseCounting = false
)
+ val needsNetworkId = foregroundAppWithBaseHandlings.find { it.second.needsNetworkId() } != null || backgroundAppBaseHandling.needsNetworkId()
+ val networkId: String? = if (needsNetworkId) appLogic.platformIntegration.getCurrentNetworkId().getNetworkIdOrNull() else null
+
+ fun reportStatusToCategoryHandlingCache(userRelatedData: UserRelatedData) {
+ categoryHandlingCache.reportStatus(
+ user = userRelatedData,
+ timeInMillis = nowTimestamp,
+ shouldTrustTimeTemporarily = realTime.shouldTrustTimeTemporarily,
+ assumeCurrentDevice = CurrentDeviceLogic.handleDeviceAsCurrentDevice(deviceRelatedData, userRelatedData),
+ batteryStatus = batteryStatus,
+ currentNetworkId = networkId,
+ hasPremiumOrLocalMode = deviceRelatedData.isLocalMode || deviceRelatedData.isConnectedAndHasPremium
+ )
+ }; reportStatusToCategoryHandlingCache(userRelatedData)
+
// check if should be blocked
val blockedForegroundApp = foregroundAppWithBaseHandlings.find { (_, foregroundAppBaseHandling) ->
foregroundAppBaseHandling is AppBaseHandling.BlockDueToNoCategory ||
diff --git a/app/src/main/java/io/timelimit/android/logic/BlockingReason.kt b/app/src/main/java/io/timelimit/android/logic/BlockingReason.kt
index 81c65aa..89d40e2 100644
--- a/app/src/main/java/io/timelimit/android/logic/BlockingReason.kt
+++ b/app/src/main/java/io/timelimit/android/logic/BlockingReason.kt
@@ -32,7 +32,8 @@ enum class BlockingReason {
RequiresCurrentDevice,
NotificationsAreBlocked,
BatteryLimit,
- SessionDurationLimit
+ SessionDurationLimit,
+ MissingRequiredNetwork
}
enum class BlockingLevel {
diff --git a/app/src/main/java/io/timelimit/android/logic/SuspendAppsLogic.kt b/app/src/main/java/io/timelimit/android/logic/SuspendAppsLogic.kt
index a3b9dbd..bdb1eb0 100644
--- a/app/src/main/java/io/timelimit/android/logic/SuspendAppsLogic.kt
+++ b/app/src/main/java/io/timelimit/android/logic/SuspendAppsLogic.kt
@@ -112,7 +112,9 @@ class SuspendAppsLogic(private val appLogic: AppLogic): Observer {
assumeCurrentDevice = CurrentDeviceLogic.handleDeviceAsCurrentDevice(
device = userAndDeviceRelatedData.deviceRelatedData,
user = userRelatedData
- )
+ ),
+ currentNetworkId = null, // not relevant/ not suspending Apps if there is no matching network
+ hasPremiumOrLocalMode = userAndDeviceRelatedData.deviceRelatedData.isLocalMode || userAndDeviceRelatedData.deviceRelatedData.isConnectedAndHasPremium
)
val defaultCategory = userRelatedData.user.categoryForNotAssignedApps
diff --git a/app/src/main/java/io/timelimit/android/logic/blockingreason/AppBaseHandling.kt b/app/src/main/java/io/timelimit/android/logic/blockingreason/AppBaseHandling.kt
index 7e51044..23f37db 100644
--- a/app/src/main/java/io/timelimit/android/logic/blockingreason/AppBaseHandling.kt
+++ b/app/src/main/java/io/timelimit/android/logic/blockingreason/AppBaseHandling.kt
@@ -32,7 +32,8 @@ sealed class AppBaseHandling {
data class UseCategories(
val categoryIds: Set,
val shouldCount: Boolean,
- val level: BlockingLevel
+ val level: BlockingLevel,
+ val needsNetworkId: Boolean
): AppBaseHandling() {
init {
if (categoryIds.isEmpty()) {
@@ -85,14 +86,19 @@ sealed class AppBaseHandling {
if (startCategory == null) {
return BlockDueToNoCategory
} else {
+ val categoryIds = userRelatedData.getCategoryWithParentCategories(startCategoryId = startCategory.category.id)
+
return UseCategories(
- categoryIds = userRelatedData.getCategoryWithParentCategories(startCategoryId = startCategory.category.id),
+ categoryIds = categoryIds,
shouldCount = !pauseCounting,
level = when (appCategory?.specifiesActivity) {
null -> BlockingLevel.Activity // occurs when using a default category
true -> BlockingLevel.Activity
false -> BlockingLevel.App
- }
+ },
+ needsNetworkId = categoryIds.find { categoryId ->
+ userRelatedData.categoryById[categoryId]!!.networks.isNotEmpty()
+ } != null
)
}
} else {
@@ -112,4 +118,6 @@ sealed class AppBaseHandling {
return result
}
}
-}
\ No newline at end of file
+}
+
+fun AppBaseHandling.needsNetworkId(): Boolean = if (this is AppBaseHandling.UseCategories) this.needsNetworkId else false
\ No newline at end of file
diff --git a/app/src/main/java/io/timelimit/android/logic/blockingreason/CategoryHandlingCache.kt b/app/src/main/java/io/timelimit/android/logic/blockingreason/CategoryHandlingCache.kt
index 404db67..c547d78 100644
--- a/app/src/main/java/io/timelimit/android/logic/blockingreason/CategoryHandlingCache.kt
+++ b/app/src/main/java/io/timelimit/android/logic/blockingreason/CategoryHandlingCache.kt
@@ -27,19 +27,25 @@ class CategoryHandlingCache {
private var shouldTrustTimeTemporarily: Boolean = false
private var timeInMillis: Long = 0
private var assumeCurrentDevice: Boolean = false
+ private var currentNetworkId: String? = null
+ private var hasPremiumOrLocalMode: Boolean = false
fun reportStatus(
user: UserRelatedData,
batteryStatus: BatteryStatus,
shouldTrustTimeTemporarily: Boolean,
timeInMillis: Long,
- assumeCurrentDevice: Boolean
+ assumeCurrentDevice: Boolean,
+ currentNetworkId: String?,
+ hasPremiumOrLocalMode: Boolean
) {
this.user = user
this.batteryStatus = batteryStatus
this.shouldTrustTimeTemporarily = shouldTrustTimeTemporarily
this.timeInMillis = timeInMillis
this.assumeCurrentDevice = assumeCurrentDevice
+ this.currentNetworkId = currentNetworkId
+ this.hasPremiumOrLocalMode = hasPremiumOrLocalMode
val iterator = cachedItems.iterator()
@@ -54,7 +60,9 @@ class CategoryHandlingCache {
batteryStatus = batteryStatus,
assumeCurrentDevice = assumeCurrentDevice,
shouldTrustTimeTemporarily = shouldTrustTimeTemporarily,
- timeInMillis = timeInMillis
+ timeInMillis = timeInMillis,
+ currentNetworkId = currentNetworkId,
+ hasPremiumOrLocalMode = hasPremiumOrLocalMode
)
) {
iterator.remove()
@@ -76,6 +84,8 @@ class CategoryHandlingCache {
batteryStatus = batteryStatus,
assumeCurrentDevice = assumeCurrentDevice,
shouldTrustTimeTemporarily = shouldTrustTimeTemporarily,
- timeInMillis = timeInMillis
+ timeInMillis = timeInMillis,
+ currentNetworkId = currentNetworkId,
+ hasPremiumOrLocalMode = hasPremiumOrLocalMode
)
}
\ No newline at end of file
diff --git a/app/src/main/java/io/timelimit/android/logic/blockingreason/CategoryItselfHandling.kt b/app/src/main/java/io/timelimit/android/logic/blockingreason/CategoryItselfHandling.kt
index c976709..fbba930 100644
--- a/app/src/main/java/io/timelimit/android/logic/blockingreason/CategoryItselfHandling.kt
+++ b/app/src/main/java/io/timelimit/android/logic/blockingreason/CategoryItselfHandling.kt
@@ -15,6 +15,7 @@
*/
package io.timelimit.android.logic.blockingreason
+import io.timelimit.android.data.model.CategoryNetworkId
import io.timelimit.android.data.model.derived.CategoryRelatedData
import io.timelimit.android.data.model.derived.UserRelatedData
import io.timelimit.android.date.DateInTimezone
@@ -37,6 +38,7 @@ data class CategoryItselfHandling (
val areLimitsTemporarilyDisabled: Boolean,
val okByBattery: Boolean,
val okByTempBlocking: Boolean,
+ val okByNetworkId: Boolean,
val okByBlockedTimeAreas: Boolean,
val okByTimeLimitRules: Boolean,
val okBySessionDurationLimits: Boolean,
@@ -50,11 +52,14 @@ data class CategoryItselfHandling (
val dependsOnBatteryCharging: Boolean,
val dependsOnMinBatteryLevel: Int,
val dependsOnMaxBatteryLevel: Int,
+ val dependsOnNetworkId: Boolean,
val createdWithCategoryRelatedData: CategoryRelatedData,
val createdWithUserRelatedData: UserRelatedData,
val createdWithBatteryStatus: BatteryStatus,
val createdWithTemporarilyTrustTime: Boolean,
- val createdWithAssumeCurrentDevice: Boolean
+ val createdWithAssumeCurrentDevice: Boolean,
+ val createdWithNetworkId: String?,
+ val createdWithHasPremiumOrLocalMode: Boolean
) {
companion object {
fun calculate(
@@ -63,7 +68,9 @@ data class CategoryItselfHandling (
batteryStatus: BatteryStatus,
shouldTrustTimeTemporarily: Boolean,
timeInMillis: Long,
- assumeCurrentDevice: Boolean
+ assumeCurrentDevice: Boolean,
+ currentNetworkId: String?,
+ hasPremiumOrLocalMode: Boolean
): CategoryItselfHandling {
val dependsOnMinTime = timeInMillis
val dateInTimezone = DateInTimezone.newInstance(timeInMillis, user.timeZone)
@@ -88,6 +95,14 @@ data class CategoryItselfHandling (
val dependsOnMaxTimeByTemporarilyDisabledLimits = if (areLimitsTemporarilyDisabled) user.user.disableLimitsUntil else Long.MAX_VALUE
// ignore it for this case: val requiresTrustedTimeForTempLimitsDisabled = user.user.disableLimitsUntil != 0L
+ val dependsOnNetworkId = categoryRelatedData.networks.isNotEmpty()
+ val okByNetworkId = if (categoryRelatedData.networks.isEmpty() || areLimitsTemporarilyDisabled || !hasPremiumOrLocalMode)
+ true
+ else if (currentNetworkId == null)
+ false
+ else
+ categoryRelatedData.networks.find { CategoryNetworkId.anonymizeNetworkId(itemId = it.networkItemId, networkId = currentNetworkId) == it.hashedNetworkId } != null
+
val missingNetworkTimeForBlockedTimeAreas = !categoryRelatedData.category.blockedMinutesInWeek.dataNotToModify.isEmpty
val okByBlockedTimeAreas = areLimitsTemporarilyDisabled || !categoryRelatedData.category.blockedMinutesInWeek.read(minuteInWeek)
val dependsOnMaxMinuteOfWeekByBlockedTimeAreas = categoryRelatedData.category.blockedMinutesInWeek.let { blockedTimeAreas ->
@@ -195,7 +210,7 @@ data class CategoryItselfHandling (
else
emptySet()
- val blockAllNotifications = categoryRelatedData.category.blockAllNotifications
+ val blockAllNotifications = categoryRelatedData.category.blockAllNotifications &&hasPremiumOrLocalMode
return CategoryItselfHandling(
shouldCountTime = shouldCountTime,
@@ -205,6 +220,7 @@ data class CategoryItselfHandling (
areLimitsTemporarilyDisabled = areLimitsTemporarilyDisabled,
okByBattery = okByBattery,
okByTempBlocking = okByTempBlocking,
+ okByNetworkId = okByNetworkId,
okByBlockedTimeAreas = okByBlockedTimeAreas,
okByTimeLimitRules = okByTimeLimitRules,
okBySessionDurationLimits = okBySessionDurationLimits,
@@ -219,22 +235,27 @@ data class CategoryItselfHandling (
dependsOnBatteryCharging = dependsOnBatteryCharging,
dependsOnMinBatteryLevel = dependsOnMinBatteryLevel,
dependsOnMaxBatteryLevel = dependsOnMaxBatteryLevel,
+ dependsOnNetworkId = dependsOnNetworkId,
createdWithCategoryRelatedData = categoryRelatedData,
createdWithBatteryStatus = batteryStatus,
createdWithTemporarilyTrustTime = shouldTrustTimeTemporarily,
createdWithAssumeCurrentDevice = assumeCurrentDevice,
- createdWithUserRelatedData = user
+ createdWithUserRelatedData = user,
+ createdWithNetworkId = currentNetworkId,
+ createdWithHasPremiumOrLocalMode = hasPremiumOrLocalMode
)
}
}
val okBasic = okByBattery && okByTempBlocking && okByBlockedTimeAreas && okByTimeLimitRules && okBySessionDurationLimits && !missingNetworkTime
- val okAll = okBasic && okByCurrentDevice
+ val okAll = okBasic && okByCurrentDevice && okByNetworkId
val shouldBlockActivities = !okAll
val activityBlockingReason: BlockingReason = if (!okByBattery)
BlockingReason.BatteryLimit
else if (!okByTempBlocking)
BlockingReason.TemporarilyBlocked
+ else if (!okByNetworkId)
+ BlockingReason.MissingRequiredNetwork
else if (!okByBlockedTimeAreas)
BlockingReason.BlockedAtThisTime
else if (!okByTimeLimitRules)
@@ -278,7 +299,9 @@ data class CategoryItselfHandling (
batteryStatus: BatteryStatus,
shouldTrustTimeTemporarily: Boolean,
timeInMillis: Long,
- assumeCurrentDevice: Boolean
+ assumeCurrentDevice: Boolean,
+ currentNetworkId: String?,
+ hasPremiumOrLocalMode: Boolean
): Boolean {
if (
categoryRelatedData != createdWithCategoryRelatedData || user != createdWithUserRelatedData ||
@@ -299,6 +322,14 @@ data class CategoryItselfHandling (
return false
}
+ if (dependsOnNetworkId && currentNetworkId != createdWithNetworkId) {
+ return false
+ }
+
+ if (hasPremiumOrLocalMode != createdWithHasPremiumOrLocalMode) {
+ return false
+ }
+
return true
}
}
diff --git a/app/src/main/java/io/timelimit/android/sync/ApplyServerDataStatus.kt b/app/src/main/java/io/timelimit/android/sync/ApplyServerDataStatus.kt
index a2cc419..3ea5d6e 100644
--- a/app/src/main/java/io/timelimit/android/sync/ApplyServerDataStatus.kt
+++ b/app/src/main/java/io/timelimit/android/sync/ApplyServerDataStatus.kt
@@ -354,6 +354,21 @@ object ApplyServerDataStatus {
database.category().updateCategorySync(updatedCategory)
}
}
+
+ // apply networks
+ database.categoryNetworkId().deleteByCategoryId(newCategory.categoryId)
+
+ if (newCategory.networks.isNotEmpty()) {
+ database.categoryNetworkId().insertItemsSync(
+ newCategory.networks.map { network ->
+ CategoryNetworkId(
+ categoryId = newCategory.categoryId,
+ networkItemId = network.itemId,
+ hashedNetworkId = network.hashedNetworkId
+ )
+ }
+ )
+ }
}
}
}
diff --git a/app/src/main/java/io/timelimit/android/sync/actions/Actions.kt b/app/src/main/java/io/timelimit/android/sync/actions/Actions.kt
index 772aed7..af27006 100644
--- a/app/src/main/java/io/timelimit/android/sync/actions/Actions.kt
+++ b/app/src/main/java/io/timelimit/android/sync/actions/Actions.kt
@@ -965,6 +965,50 @@ data class UpdateCategorySortingAction(val categoryIds: List): ParentAct
writer.endObject()
}
}
+
+data class AddCategoryNetworkId(val categoryId: String, val itemId: String, val hashedNetworkId: String): ParentAction() {
+ companion object {
+ private const val TYPE_VALUE = "ADD_CATEGORY_NETWORK_ID"
+ private const val CATEGORY_ID = "categoryId"
+ private const val ITEM_ID = "itemId"
+ private const val HASHED_NETWORK_ID = "hashedNetworkId"
+ }
+
+ init {
+ IdGenerator.assertIdValid(categoryId)
+ IdGenerator.assertIdValid(itemId)
+ HexString.assertIsHexString(hashedNetworkId)
+ if (hashedNetworkId.length != CategoryNetworkId.ANONYMIZED_NETWORK_ID_LENGTH) throw IllegalArgumentException()
+ }
+
+ override fun serialize(writer: JsonWriter) {
+ writer.beginObject()
+
+ writer.name(TYPE).value(TYPE_VALUE)
+ writer.name(CATEGORY_ID).value(categoryId)
+ writer.name(ITEM_ID).value(itemId)
+ writer.name(HASHED_NETWORK_ID).value(hashedNetworkId)
+
+ writer.endObject()
+ }
+}
+
+data class ResetCategoryNetworkIds(val categoryId: String): ParentAction() {
+ companion object {
+ private const val TYPE_VALUE = "RESET_CATEGORY_NETWORK_IDS"
+ private const val CATEGORY_ID = "categoryId"
+ }
+
+ override fun serialize(writer: JsonWriter) {
+ writer.beginObject()
+
+ writer.name(TYPE).value(TYPE_VALUE)
+ writer.name(CATEGORY_ID).value(categoryId)
+
+ writer.endObject()
+ }
+}
+
// DeviceDao
data class UpdateDeviceStatusAction(
diff --git a/app/src/main/java/io/timelimit/android/sync/actions/Parser.kt b/app/src/main/java/io/timelimit/android/sync/actions/Parser.kt
index 7dd6f8f..cba0771 100644
--- a/app/src/main/java/io/timelimit/android/sync/actions/Parser.kt
+++ b/app/src/main/java/io/timelimit/android/sync/actions/Parser.kt
@@ -70,6 +70,8 @@ object ActionParser {
// UpdateCategorySorting
// UpdateUserFlagsAction
// UpdateUserLimitLoginCategory
+ // AddCategoryNetworkId
+ // ResetCategoryNetworkIds
else -> throw IllegalStateException()
}
}
diff --git a/app/src/main/java/io/timelimit/android/sync/actions/dispatch/ParentAction.kt b/app/src/main/java/io/timelimit/android/sync/actions/dispatch/ParentAction.kt
index d42ea54..5c42d3d 100644
--- a/app/src/main/java/io/timelimit/android/sync/actions/dispatch/ParentAction.kt
+++ b/app/src/main/java/io/timelimit/android/sync/actions/dispatch/ParentAction.kt
@@ -729,6 +729,30 @@ object LocalDatabaseParentActionDispatcher {
)
) }
}
+ is AddCategoryNetworkId -> {
+ DatabaseValidation.assertCategoryExists(database, action.categoryId)
+
+ val count = database.categoryNetworkId().countByCategoryIdSync(action.categoryId)
+
+ if (count + 1 > CategoryNetworkId.MAX_ITEMS) {
+ throw IllegalArgumentException()
+ }
+
+ val oldItem = database.categoryNetworkId().getByCategoryIdAndItemIdSync(categoryId = action.categoryId, itemId = action.itemId)
+
+ if (oldItem != null) {
+ throw IllegalArgumentException("id already used")
+ }
+
+ database.categoryNetworkId().insertItemSync(CategoryNetworkId(
+ categoryId = action.categoryId,
+ networkItemId = action.itemId,
+ hashedNetworkId = action.hashedNetworkId
+ ))
+ }
+ is ResetCategoryNetworkIds -> {
+ database.categoryNetworkId().deleteByCategoryId(categoryId = action.categoryId)
+ }
}.let { }
}
}
diff --git a/app/src/main/java/io/timelimit/android/sync/network/ServerDataStatus.kt b/app/src/main/java/io/timelimit/android/sync/network/ServerDataStatus.kt
index 8dd9ebf..27a7acd 100644
--- a/app/src/main/java/io/timelimit/android/sync/network/ServerDataStatus.kt
+++ b/app/src/main/java/io/timelimit/android/sync/network/ServerDataStatus.kt
@@ -17,6 +17,8 @@ package io.timelimit.android.sync.network
import android.util.JsonReader
import android.util.JsonToken
+import io.timelimit.android.crypto.HexString
+import io.timelimit.android.data.IdGenerator
import io.timelimit.android.data.customtypes.ImmutableBitmask
import io.timelimit.android.data.customtypes.ImmutableBitmaskJson
import io.timelimit.android.data.model.*
@@ -443,7 +445,8 @@ data class ServerUpdatedCategoryBaseData(
val timeWarnings: Int,
val minBatteryLevelCharging: Int,
val minBatteryLevelMobile: Int,
- val sort: Int
+ val sort: Int,
+ val networks: List
) {
companion object {
private const val CATEGORY_ID = "categoryId"
@@ -461,6 +464,7 @@ data class ServerUpdatedCategoryBaseData(
private const val MIN_BATTERY_LEVEL_MOBILE = "mblMobile"
private const val MIN_BATTERY_LEVEL_CHARGING = "mblCharging"
private const val SORT = "sort"
+ private const val NETWORKS = "networks"
fun parse(reader: JsonReader): ServerUpdatedCategoryBaseData {
var categoryId: String? = null
@@ -479,6 +483,7 @@ data class ServerUpdatedCategoryBaseData(
var minBatteryLevelCharging = 0
var minBatteryLevelMobile = 0
var sort = 0
+ var networks: List = emptyList()
reader.beginObject()
while (reader.hasNext()) {
@@ -498,6 +503,7 @@ data class ServerUpdatedCategoryBaseData(
MIN_BATTERY_LEVEL_CHARGING -> minBatteryLevelCharging = reader.nextInt()
MIN_BATTERY_LEVEL_MOBILE -> minBatteryLevelMobile = reader.nextInt()
SORT -> sort = reader.nextInt()
+ NETWORKS -> networks = ServerCategoryNetworkId.parseList(reader)
else -> reader.skipValue()
}
}
@@ -518,7 +524,8 @@ data class ServerUpdatedCategoryBaseData(
timeWarnings = timeWarnings,
minBatteryLevelCharging = minBatteryLevelCharging,
minBatteryLevelMobile = minBatteryLevelMobile,
- sort = sort
+ sort = sort,
+ networks = networks
)
}
@@ -526,6 +533,41 @@ data class ServerUpdatedCategoryBaseData(
}
}
+data class ServerCategoryNetworkId(val itemId: String, val hashedNetworkId: String) {
+ companion object {
+ private const val ITEM_ID = "itemId"
+ private const val HASHED_NETWORK_ID = "hashedNetworkId"
+
+ fun parse(reader: JsonReader): ServerCategoryNetworkId {
+ var itemId: String? = null
+ var hashedNetworkId: String? = null
+
+ reader.beginObject()
+ while (reader.hasNext()) {
+ when (reader.nextName()) {
+ ITEM_ID -> itemId = reader.nextString()
+ HASHED_NETWORK_ID -> hashedNetworkId = reader.nextString()
+ else -> reader.skipValue()
+ }
+ }
+ reader.endObject()
+
+ return ServerCategoryNetworkId(
+ itemId = itemId!!,
+ hashedNetworkId = hashedNetworkId!!
+ )
+ }
+
+ fun parseList(reader: JsonReader) = parseJsonArray(reader) { parse(reader) }
+ }
+
+ init {
+ IdGenerator.assertIdValid(itemId)
+ HexString.assertIsHexString(hashedNetworkId)
+ if (hashedNetworkId.length != CategoryNetworkId.ANONYMIZED_NETWORK_ID_LENGTH) throw IllegalArgumentException()
+ }
+}
+
data class ServerUpdatedCategoryAssignedApps(
val categoryId: String,
val assignedApps: List,
diff --git a/app/src/main/java/io/timelimit/android/ui/diagnose/DiagnoseConnectionFragment.kt b/app/src/main/java/io/timelimit/android/ui/diagnose/DiagnoseConnectionFragment.kt
index 0acfdc2..e4067f8 100644
--- a/app/src/main/java/io/timelimit/android/ui/diagnose/DiagnoseConnectionFragment.kt
+++ b/app/src/main/java/io/timelimit/android/ui/diagnose/DiagnoseConnectionFragment.kt
@@ -25,6 +25,8 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import io.timelimit.android.R
import io.timelimit.android.databinding.FragmentDiagnoseConnectionBinding
+import io.timelimit.android.integration.platform.NetworkId
+import io.timelimit.android.livedata.liveDataFromFunction
import io.timelimit.android.livedata.liveDataFromValue
import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.sync.websocket.NetworkStatus
@@ -35,14 +37,14 @@ class DiagnoseConnectionFragment : Fragment(), FragmentWithCustomTitle {
val binding = FragmentDiagnoseConnectionBinding.inflate(inflater, container, false)
val logic = DefaultAppLogic.with(context!!)
- logic.networkStatus.observe(this, Observer {
+ logic.networkStatus.observe(viewLifecycleOwner, Observer {
binding.generalStatus = getString(when (it!!) {
NetworkStatus.Online -> R.string.diagnose_connection_yes
NetworkStatus.Offline -> R.string.diagnose_connection_no
})
})
- logic.isConnected.observe(this, Observer {
+ logic.isConnected.observe(viewLifecycleOwner, Observer {
binding.ownServerStatus = getString(if (it == true)
R.string.diagnose_connection_yes
else
@@ -50,6 +52,14 @@ class DiagnoseConnectionFragment : Fragment(), FragmentWithCustomTitle {
)
})
+ liveDataFromFunction { logic.platformIntegration.getCurrentNetworkId() }.observe(viewLifecycleOwner, Observer {
+ binding.networkId = when (it) {
+ NetworkId.MissingPermission -> "missing permission"
+ NetworkId.NoNetworkConnected -> "no network connected"
+ is NetworkId.Network -> it.id
+ }
+ })
+
return binding.root
}
diff --git a/app/src/main/java/io/timelimit/android/ui/lock/LockFragment.kt b/app/src/main/java/io/timelimit/android/ui/lock/LockFragment.kt
index 4659e84..2cbae74 100644
--- a/app/src/main/java/io/timelimit/android/ui/lock/LockFragment.kt
+++ b/app/src/main/java/io/timelimit/android/ui/lock/LockFragment.kt
@@ -15,15 +15,19 @@
*/
package io.timelimit.android.ui.lock
+import android.Manifest
import android.content.Intent
+import android.content.pm.PackageManager
import android.database.sqlite.SQLiteConstraintException
import android.os.Bundle
import android.text.format.DateUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import io.timelimit.android.R
import io.timelimit.android.async.Threads
@@ -36,10 +40,13 @@ import io.timelimit.android.databinding.LockFragmentBinding
import io.timelimit.android.databinding.LockFragmentCategoryButtonBinding
import io.timelimit.android.date.DateInTimezone
import io.timelimit.android.integration.platform.BatteryStatus
+import io.timelimit.android.integration.platform.NetworkId
+import io.timelimit.android.integration.platform.getNetworkIdOrNull
import io.timelimit.android.livedata.*
import io.timelimit.android.logic.*
import io.timelimit.android.logic.blockingreason.AppBaseHandling
import io.timelimit.android.logic.blockingreason.CategoryHandlingCache
+import io.timelimit.android.logic.blockingreason.needsNetworkId
import io.timelimit.android.sync.actions.AddCategoryAppsAction
import io.timelimit.android.sync.actions.IncrementCategoryExtraTimeAction
import io.timelimit.android.sync.actions.UpdateCategoryTemporarilyBlockedAction
@@ -50,6 +57,7 @@ import io.timelimit.android.ui.help.HelpDialogFragment
import io.timelimit.android.ui.main.ActivityViewModel
import io.timelimit.android.ui.main.AuthenticationFab
import io.timelimit.android.ui.main.getActivityViewModel
+import io.timelimit.android.ui.manage.category.settings.networks.RequestWifiPermission
import io.timelimit.android.ui.manage.child.ManageChildFragmentArgs
import io.timelimit.android.ui.manage.child.advanced.managedisabletimelimits.ManageDisableTimelimitsViewHelper
import io.timelimit.android.ui.manage.child.category.create.CreateCategoryDialogFragment
@@ -63,6 +71,7 @@ class LockFragment : Fragment() {
private const val EXTRA_PACKAGE_NAME = "pkg"
private const val EXTRA_ACTIVITY = "activitiy"
private const val STATUS_DID_OPEN_SET_CURRENT_DEVICE_SCREEN = "didOpenSetCurrentDeviceScreen"
+ private const val LOCATION_REQUEST_CODE = 1
fun newInstance(packageName: String, activity: String?): LockFragment {
val result = LockFragment()
@@ -96,6 +105,12 @@ class LockFragment : Fragment() {
private val batteryStatus: LiveData by lazy {
logic.platformIntegration.getBatteryStatusLive()
}
+ private val needsNetworkIdLive = MutableLiveData().apply { value = false }
+ private val realNetworkIdLive: LiveData by lazy { liveDataFromFunction { logic.platformIntegration.getCurrentNetworkId() } }
+ private val networkIdLive: LiveData by lazy { needsNetworkIdLive.switchMap { needsNetworkId ->
+ if (needsNetworkId) realNetworkIdLive as LiveData else liveDataFromValue(null as NetworkId?)
+ } }
+ private val hasPremiumOrLocalMode: LiveData by lazy { logic.fullVersion.shouldProvideFullVersionFunctions }
private lateinit var binding: LockFragmentBinding
private val handlingCache = CategoryHandlingCache()
private val realTime = RealTime.newInstance()
@@ -115,6 +130,8 @@ class LockFragment : Fragment() {
private fun update() {
val deviceAndUserRelatedData = deviceAndUserRelatedData.value ?: return
val batteryStatus = batteryStatus.value ?: return
+ val hasPremiumOrLocalMode = hasPremiumOrLocalMode.value ?: return
+ val networkId = networkIdLive.value
logic.realTimeLogic.getRealTime(realTime)
@@ -125,14 +142,6 @@ class LockFragment : Fragment() {
return
}
- handlingCache.reportStatus(
- user = deviceAndUserRelatedData.userRelatedData,
- assumeCurrentDevice = CurrentDeviceLogic.handleDeviceAsCurrentDevice(deviceAndUserRelatedData.deviceRelatedData, deviceAndUserRelatedData.userRelatedData),
- batteryStatus = batteryStatus,
- timeInMillis = realTime.timeInMillis,
- shouldTrustTimeTemporarily = realTime.shouldTrustTimeTemporarily
- )
-
val appBaseHandling = AppBaseHandling.calculate(
foregroundAppPackageName = packageName,
foregroundAppActivityName = activityName,
@@ -142,6 +151,24 @@ class LockFragment : Fragment() {
pauseCounting = false
)
+ val needsNetworkId = appBaseHandling.needsNetworkId()
+
+ if (needsNetworkId != needsNetworkIdLive.value) {
+ needsNetworkIdLive.value = needsNetworkId
+ }
+
+ if (needsNetworkId && networkId == null) return
+
+ handlingCache.reportStatus(
+ user = deviceAndUserRelatedData.userRelatedData,
+ assumeCurrentDevice = CurrentDeviceLogic.handleDeviceAsCurrentDevice(deviceAndUserRelatedData.deviceRelatedData, deviceAndUserRelatedData.userRelatedData),
+ batteryStatus = batteryStatus,
+ timeInMillis = realTime.timeInMillis,
+ shouldTrustTimeTemporarily = realTime.shouldTrustTimeTemporarily,
+ currentNetworkId = networkId?.getNetworkIdOrNull(),
+ hasPremiumOrLocalMode = hasPremiumOrLocalMode
+ )
+
binding.activityName = if (deviceAndUserRelatedData.deviceRelatedData.deviceEntry.enableActivityLevelBlocking)
activityName?.removePrefix(packageName)
else
@@ -278,6 +305,10 @@ class LockFragment : Fragment() {
}
override fun setThisDeviceAsCurrentDevice() = this@LockFragment.setThisDeviceAsCurrentDevice()
+
+ override fun requestLocationPermission() {
+ RequestWifiPermission.doRequest(this@LockFragment, LOCATION_REQUEST_CODE)
+ }
}
}
@@ -373,6 +404,12 @@ class LockFragment : Fragment() {
}
}
+ private fun initGrantPermissionView() {
+ networkIdLive.observe(viewLifecycleOwner, Observer {
+ binding.missingNetworkIdPermission = it is NetworkId.MissingPermission
+ })
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -411,6 +448,8 @@ class LockFragment : Fragment() {
deviceAndUserRelatedData.observe(viewLifecycleOwner, Observer { update() })
batteryStatus.observe(viewLifecycleOwner, Observer { update() })
+ networkIdLive.observe(viewLifecycleOwner, Observer { update() })
+ hasPremiumOrLocalMode.observe(viewLifecycleOwner, Observer { update() })
binding.packageName = packageName
@@ -418,6 +457,7 @@ class LockFragment : Fragment() {
binding.appIcon.setImageDrawable(logic.platformIntegration.getAppIcon(packageName))
initExtraTimeView()
+ initGrantPermissionView()
return binding.root
}
@@ -441,6 +481,12 @@ class LockFragment : Fragment() {
unscheduleUpdate()
}
+
+ override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
+ if (grantResults.find { it != PackageManager.PERMISSION_GRANTED } != null) {
+ Toast.makeText(context!!, R.string.generic_runtime_permission_rejected, Toast.LENGTH_LONG).show()
+ }
+ }
}
interface Handlers {
@@ -452,4 +498,5 @@ interface Handlers {
fun disableTemporarilyLockForAllCategories()
fun showAuthenticationScreen()
fun setThisDeviceAsCurrentDevice()
+ fun requestLocationPermission()
}
diff --git a/app/src/main/java/io/timelimit/android/ui/login/AllowUserLoginStatus.kt b/app/src/main/java/io/timelimit/android/ui/login/AllowUserLoginStatus.kt
index b6662eb..15e80e6 100644
--- a/app/src/main/java/io/timelimit/android/ui/login/AllowUserLoginStatus.kt
+++ b/app/src/main/java/io/timelimit/android/ui/login/AllowUserLoginStatus.kt
@@ -73,7 +73,9 @@ object AllowUserLoginStatusUtil {
assumeCurrentDevice = true,
timeInMillis = time.timeInMillis,
batteryStatus = batteryStatus,
- shouldTrustTimeTemporarily = time.shouldTrustTimeTemporarily
+ shouldTrustTimeTemporarily = time.shouldTrustTimeTemporarily,
+ currentNetworkId = null, // only checks shouldBlockAtSystemLevel which ignores the network id
+ hasPremiumOrLocalMode = data.deviceRelatedData.isLocalMode || data.deviceRelatedData.isConnectedAndHasPremium
)
val categoryIds = data.limitLoginCategoryUserRelatedData.getCategoryWithParentCategories(data.loginRelatedData.limitLoginCategory.categoryId)
diff --git a/app/src/main/java/io/timelimit/android/ui/login/NewLoginFragment.kt b/app/src/main/java/io/timelimit/android/ui/login/NewLoginFragment.kt
index f2afb4c..62ad1d9 100644
--- a/app/src/main/java/io/timelimit/android/ui/login/NewLoginFragment.kt
+++ b/app/src/main/java/io/timelimit/android/ui/login/NewLoginFragment.kt
@@ -329,6 +329,7 @@ class NewLoginFragment: DialogFragment() {
BlockingReason.NotificationsAreBlocked -> getString(R.string.lock_reason_short_notification_blocking)
BlockingReason.BatteryLimit -> getString(R.string.lock_reason_short_battery_limit)
BlockingReason.SessionDurationLimit -> getString(R.string.lock_reason_short_session_duration)
+ BlockingReason.MissingRequiredNetwork -> getString(R.string.lock_reason_short_missing_required_network)
BlockingReason.NotPartOfAnCategory -> "???"
BlockingReason.None -> "???"
}
diff --git a/app/src/main/java/io/timelimit/android/ui/manage/category/settings/CategorySettingsFragment.kt b/app/src/main/java/io/timelimit/android/ui/manage/category/settings/CategorySettingsFragment.kt
index a53f21b..308e448 100644
--- a/app/src/main/java/io/timelimit/android/ui/manage/category/settings/CategorySettingsFragment.kt
+++ b/app/src/main/java/io/timelimit/android/ui/manage/category/settings/CategorySettingsFragment.kt
@@ -15,10 +15,12 @@
*/
package io.timelimit.android.ui.manage.category.settings
+import android.content.pm.PackageManager
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import com.google.android.material.snackbar.Snackbar
@@ -36,11 +38,14 @@ 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.settings.addusedtime.AddUsedTimeDialogFragment
+import io.timelimit.android.ui.manage.category.settings.networks.ManageCategoryNetworksView
import io.timelimit.android.ui.payment.RequiresPurchaseDialogFragment
import io.timelimit.android.ui.view.SelectTimeSpanViewListener
class CategorySettingsFragment : Fragment() {
companion object {
+ private const val PERMISSION_REQUEST_CODE = 1
+
fun newInstance(params: ManageCategoryFragmentArgs): CategorySettingsFragment {
val result = CategorySettingsFragment()
result.arguments = params.toBundle()
@@ -117,6 +122,16 @@ class CategorySettingsFragment : Fragment() {
fragmentManager = parentFragmentManager
)
+ ManageCategoryNetworksView.bind(
+ view = binding.networks,
+ auth = auth,
+ lifecycleOwner = viewLifecycleOwner,
+ fragmentManager = parentFragmentManager,
+ fragment = this,
+ permissionRequestCode = PERMISSION_REQUEST_CODE,
+ categoryId = params.categoryId
+ )
+
binding.btnDeleteCategory.setOnClickListener { deleteCategory() }
binding.editCategoryTitleGo.setOnClickListener { renameCategory() }
binding.addUsedTimeBtn.setOnClickListener { addUsedTime() }
@@ -231,4 +246,12 @@ class CategorySettingsFragment : Fragment() {
).show(parentFragmentManager)
}
}
+
+ override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
+ if (requestCode == PERMISSION_REQUEST_CODE) {
+ if (grantResults.find { it != PackageManager.PERMISSION_GRANTED } != null) {
+ Toast.makeText(context!!, R.string.generic_runtime_permission_rejected, Toast.LENGTH_LONG).show()
+ }
+ }
+ }
}
diff --git a/app/src/main/java/io/timelimit/android/ui/manage/category/settings/networks/ManageCategoryNetworksView.kt b/app/src/main/java/io/timelimit/android/ui/manage/category/settings/networks/ManageCategoryNetworksView.kt
new file mode 100644
index 0000000..eab8f71
--- /dev/null
+++ b/app/src/main/java/io/timelimit/android/ui/manage/category/settings/networks/ManageCategoryNetworksView.kt
@@ -0,0 +1,155 @@
+/*
+ * TimeLimit Copyright 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 .
+ */
+package io.timelimit.android.ui.manage.category.settings.networks
+
+import android.Manifest
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentManager
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.Observer
+import com.google.android.material.snackbar.Snackbar
+import io.timelimit.android.R
+import io.timelimit.android.data.IdGenerator
+import io.timelimit.android.data.model.CategoryNetworkId
+import io.timelimit.android.databinding.ManageCategoryNetworksViewBinding
+import io.timelimit.android.integration.platform.NetworkId
+import io.timelimit.android.livedata.liveDataFromFunction
+import io.timelimit.android.livedata.map
+import io.timelimit.android.livedata.switchMap
+import io.timelimit.android.sync.actions.AddCategoryNetworkId
+import io.timelimit.android.sync.actions.ResetCategoryNetworkIds
+import io.timelimit.android.ui.help.HelpDialogFragment
+import io.timelimit.android.ui.main.ActivityViewModel
+import io.timelimit.android.ui.payment.RequiresPurchaseDialogFragment
+
+object ManageCategoryNetworksView {
+ fun bind(
+ view: ManageCategoryNetworksViewBinding,
+ auth: ActivityViewModel,
+ lifecycleOwner: LifecycleOwner,
+ fragmentManager: FragmentManager,
+ categoryId: String,
+ fragment: Fragment,
+ permissionRequestCode: Int
+ ) {
+ fun networkId(): NetworkId = auth.logic.platformIntegration.getCurrentNetworkId()
+
+ val context = view.root.context
+ val networkIdLive = liveDataFromFunction { networkId() }
+ val networksLive = auth.database.categoryNetworkId().getByCategoryIdLive(categoryId)
+ val isFullVersionLive = auth.logic.fullVersion.shouldProvideFullVersionFunctions
+
+ view.titleView.setOnClickListener {
+ HelpDialogFragment.newInstance(
+ title = R.string.category_networks_title,
+ text = R.string.category_networks_help
+ ).show(fragmentManager)
+ }
+
+ networksLive.switchMap { networks ->
+ networkIdLive.map { networkId ->
+ networks to networkId
+ }
+ }.observe(lifecycleOwner, Observer { (networks, networkId) ->
+ view.showRemoveNetworksButton = networks.isNotEmpty()
+
+ view.addedNetworksText = if (networks.isEmpty())
+ context.getString(R.string.category_networks_empty)
+ else
+ context.getString(
+ R.string.category_networks_not_empty,
+ context.resources.getQuantityString(R.plurals.category_networks_counter, networks.size, networks.size)
+ )
+
+ view.status = when (networkId) {
+ NetworkId.MissingPermission -> NetworkStatus.MissingPermission
+ NetworkId.NoNetworkConnected -> NetworkStatus.NoneConnected
+ is NetworkId.Network -> {
+ val hasItem = networks.find {item ->
+ CategoryNetworkId.anonymizeNetworkId(networkId = networkId.id, itemId = item.networkItemId) == item.hashedNetworkId
+ } != null
+
+ if (hasItem)
+ NetworkStatus.ConnectedAndAdded
+ else if (networks.size + 1 > CategoryNetworkId.MAX_ITEMS)
+ NetworkStatus.ConnectedNotAddedButFull
+ else
+ NetworkStatus.ConnectedButNotAdded
+ }
+ }
+ })
+
+ view.removeBtn.setOnClickListener {
+ val oldList = networksLive.value ?: return@setOnClickListener
+
+ if (
+ auth.tryDispatchParentAction(
+ ResetCategoryNetworkIds(categoryId = categoryId)
+ )
+ ) {
+ Snackbar.make(view.root, R.string.category_networks_toast_all_removed, Snackbar.LENGTH_LONG)
+ .setAction(R.string.generic_undo) {
+ val isEmpty = networksLive.value?.isEmpty() ?: false
+
+ if (isEmpty) {
+ auth.tryDispatchParentActions(
+ oldList.map { item ->
+ AddCategoryNetworkId(
+ categoryId = item.categoryId,
+ itemId = item.networkItemId,
+ hashedNetworkId = item.hashedNetworkId
+ )
+ }
+ )
+ }
+ }.show()
+ }
+ }
+
+ view.grantPermissionButton.setOnClickListener {
+ RequestWifiPermission.doRequest(fragment, permissionRequestCode)
+ }
+
+ isFullVersionLive.observe(lifecycleOwner, Observer { isFullVersion ->
+ view.addNetworkButton.setOnClickListener {
+ if (isFullVersion) {
+ val itemId = IdGenerator.generateId()
+ val networkId = networkId()
+
+ if (!(networkId is NetworkId.Network)) return@setOnClickListener
+
+ auth.tryDispatchParentAction(
+ AddCategoryNetworkId(
+ categoryId = categoryId,
+ itemId = itemId,
+ hashedNetworkId = CategoryNetworkId.anonymizeNetworkId(itemId = itemId, networkId = networkId.id)
+ )
+ )
+ } else {
+ RequiresPurchaseDialogFragment().show(fragmentManager)
+ }
+ }
+ })
+ }
+
+ enum class NetworkStatus {
+ MissingPermission,
+ NoneConnected,
+ ConnectedButNotAdded,
+ ConnectedNotAddedButFull,
+ ConnectedAndAdded
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/timelimit/android/ui/manage/category/settings/networks/RequestWifiPermission.kt b/app/src/main/java/io/timelimit/android/ui/manage/category/settings/networks/RequestWifiPermission.kt
new file mode 100644
index 0000000..44be1f7
--- /dev/null
+++ b/app/src/main/java/io/timelimit/android/ui/manage/category/settings/networks/RequestWifiPermission.kt
@@ -0,0 +1,53 @@
+/*
+ * TimeLimit Copyright 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 .
+ */
+package io.timelimit.android.ui.manage.category.settings.networks
+
+import android.Manifest
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.location.LocationManager
+import android.os.Build
+import android.provider.Settings
+import android.widget.Toast
+import androidx.core.content.ContextCompat
+import androidx.fragment.app.Fragment
+import io.timelimit.android.R
+
+object RequestWifiPermission {
+ private fun isLocationEnabled(context: Context): Boolean = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P)
+ Settings.Secure.getInt(context.contentResolver, Settings.Secure.LOCATION_MODE) == Settings.Secure.LOCATION_MODE_OFF
+ else {
+ val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
+
+ locationManager.isLocationEnabled
+ }
+
+ fun doRequest(fragment: Fragment, permissionRequestCode: Int) {
+ if (ContextCompat.checkSelfPermission(fragment.requireContext(), Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
+ fragment.requestPermissions(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), permissionRequestCode)
+ } else if (!isLocationEnabled(fragment.requireContext())) {
+ Toast.makeText(fragment.requireContext(), R.string.category_networks_toast_enable_location_service, Toast.LENGTH_SHORT).show()
+
+ fragment.startActivity(
+ Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ )
+ } else {
+ Toast.makeText(fragment.requireContext(), R.string.error_general, Toast.LENGTH_SHORT).show()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/timelimit/android/ui/widget/TimesWidgetItems.kt b/app/src/main/java/io/timelimit/android/ui/widget/TimesWidgetItems.kt
index 8c80097..dc78ac8 100644
--- a/app/src/main/java/io/timelimit/android/ui/widget/TimesWidgetItems.kt
+++ b/app/src/main/java/io/timelimit/android/ui/widget/TimesWidgetItems.kt
@@ -82,7 +82,9 @@ object TimesWidgetItems {
timeInMillis = realTime.timeInMillis,
batteryStatus = logic.platformIntegration.getBatteryStatus(),
shouldTrustTimeTemporarily = realTime.shouldTrustTimeTemporarily,
- assumeCurrentDevice = true
+ assumeCurrentDevice = true,
+ currentNetworkId = null, // not relevant here
+ hasPremiumOrLocalMode = false // not relevant here
)
var maxTime = Long.MAX_VALUE
diff --git a/app/src/main/res/drawable/ic_baseline_wifi_lock_24.xml b/app/src/main/res/drawable/ic_baseline_wifi_lock_24.xml
new file mode 100644
index 0000000..fce0db2
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_wifi_lock_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/layout/fragment_category_settings.xml b/app/src/main/res/layout/fragment_category_settings.xml
index 52a9d7a..15ba4fb 100644
--- a/app/src/main/res/layout/fragment_category_settings.xml
+++ b/app/src/main/res/layout/fragment_category_settings.xml
@@ -95,6 +95,9 @@
+
+
2019 Jonas Lochmann
+ TimeLimit Copyright 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.
@@ -25,45 +25,82 @@
+
+
-
-
+
+
-
-
+ android:layout_height="wrap_content">
-
+
+
+
+
+
+
+
+
+
+
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:padding="8dp">
-
+
-
-
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/lock_fragment.xml b/app/src/main/res/layout/lock_fragment.xml
index 77caced..d5879ef 100644
--- a/app/src/main/res/layout/lock_fragment.xml
+++ b/app/src/main/res/layout/lock_fragment.xml
@@ -51,6 +51,10 @@
name="blockedKindLabel"
type="String" />
+
+
@@ -281,11 +285,11 @@
tools:ignore="UnusedAttribute"
android:drawablePadding="16dp"
android:drawableTint="?colorOnSurface"
- android:drawableStart="@drawable/ic_pause_circle_outline_black_24dp"
- android:visibility="@{reason == BlockingReason.SessionDurationLimit ? View.VISIBLE : View.GONE}"
+ android:drawableStart="@drawable/ic_baseline_wifi_lock_24"
+ android:visibility="@{reason == BlockingReason.MissingRequiredNetwork ? View.VISIBLE : View.GONE}"
android:textAppearance="?android:textAppearanceMedium"
- android:text="@{@string/lock_reason_session_duration(blockedKindLabel)}"
- tools:text="@string/lock_reason_session_duration"
+ android:text="@{@string/lock_reason_missing_required_network(blockedKindLabel)}"
+ tools:text="@string/lock_reason_missing_required_network"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
@@ -566,6 +570,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-de/string-generic.xml b/app/src/main/res/values-de/string-generic.xml
index 4be0cfa..e79af01 100644
--- a/app/src/main/res/values-de/string-generic.xml
+++ b/app/src/main/res/values-de/string-generic.xml
@@ -32,4 +32,5 @@
Se können diesen Hinweis entfernen, indem Sie ihn zur Seite wischen
+ Berechtigung abgelehnt; Sie können die Berechtigungen in den Systemeinstellungen verwalten
diff --git a/app/src/main/res/values-de/strings-diagnose.xml b/app/src/main/res/values-de/strings-diagnose.xml
index 6f1e390..b2625c2 100644
--- a/app/src/main/res/values-de/strings-diagnose.xml
+++ b/app/src/main/res/values-de/strings-diagnose.xml
@@ -34,6 +34,7 @@
TimeLimit-Server: %s
verbunden
nicht verbunden
+ Netzwerk-ID
Synchronisation
Es gibt Nichts das synchronisiert werden müsste
diff --git a/app/src/main/res/values-de/strings-lock.xml b/app/src/main/res/values-de/strings-lock.xml
index 22cd8ef..2c6f6f2 100644
--- a/app/src/main/res/values-de/strings-lock.xml
+++ b/app/src/main/res/values-de/strings-lock.xml
@@ -50,6 +50,9 @@
Systemzeit manuell bestätigen
Diese Zeit ist richtig
+ Berechtigung erteilen
+ TimeLimit ermöglichen, das verbundene Netzwerk zu erkennen
+
Die App wurde zu keiner Kategorie zugeordnet. Das bedeutet, dass es keine Einschränkungs-Einstellungen gibt. Und da ist es die einfachste Lösung, die App zu sperren.
@@ -84,6 +87,10 @@
Für diese %s gibt es eine Sitzungsdauerbegrenzung.
Nach dem Ablauf der Pausenzeit wird die Sperre aufgehoben.
+
+ Diese %s darf nur in bestimmten Netzwerken verwendet werden,
+ aber es wurde keine Verbindung zu einem entsprechenden Netzwerk gefunden.
+
keine Kategorie
vorübergehend gesperrt
@@ -94,6 +101,7 @@
alle Benachrichtigungen werden blockiert
Akkulimit unterschritten
Sitzungsdauergrenze erreicht
+ kein erlaubtes Netzwerk
Öffnen des Sperrbildschirms fehlgeschlagen.
diff --git a/app/src/main/res/values-de/strings-manage-category-networks.xml b/app/src/main/res/values-de/strings-manage-category-networks.xml
new file mode 100644
index 0000000..1963807
--- /dev/null
+++ b/app/src/main/res/values-de/strings-manage-category-networks.xml
@@ -0,0 +1,47 @@
+
+
+
+ Netzwerke
+ Hier können WLAN-Drahtlosnetzwerke hinzugefügt werden.
+ Diese Kategorie wird gesperrt, wenn Netzwerke hinzugefügt wurden und es keine
+ Verbindung zu einem der angegebenen Netzwerke gibt.
+ Ein Netzwerk wird durch einen Access Point identifiziert. Im Fall von mehreren Access Point/ Repeatern
+ müssen alle einzeln hinzugefügt werden.
+ Das vorübergehende Deaktivieren von Zeitbegrenzungen deaktiviert auch die Beschränkungen durch diese Funktion.
+
+ Sie haben keine Netzwerke hinzugefügt. Diese Kategorie wird
+ nicht abhängig vom Netzwerk gesperrt.
+
+ Es wurden %s hinzugefügt. Diese Kategorie kann nur in den
+ angegebenen Netzwerken verwendet werden.
+
+ - %d Netzwerk
+ - %d Netzwerke
+
+ Die Berechtigung zum Abrufen vom aktuellen Netzwerk fehlt
+ Sie sind mit keinem WLAN verbunden
+ Das aktuelle WLAN wurde nicht hinzugefügt
+ Das aktuelle WLAN wurde nicht hinzugefügt,
+ aber es können keine weiteren WLANs hinzugefügt werden
+
+ Das aktuelle WLAN wurde hinzugefügt
+ Netzwerk hinzufügen
+ Berechtigung erteilen
+ Alle Netzwerke entfernen
+ alle Netzwerke entfernt
+
+ Bitte den Standortzugriff aktivieren
+
\ No newline at end of file
diff --git a/app/src/main/res/values/string-generic.xml b/app/src/main/res/values/string-generic.xml
index a85c974..36d1933 100644
--- a/app/src/main/res/values/string-generic.xml
+++ b/app/src/main/res/values/string-generic.xml
@@ -32,4 +32,5 @@
Swipe to the side to remove this message
+ Permission rejected; You can manage permissions in the system settings
diff --git a/app/src/main/res/values/strings-diagnose.xml b/app/src/main/res/values/strings-diagnose.xml
index a193c25..1715452 100644
--- a/app/src/main/res/values/strings-diagnose.xml
+++ b/app/src/main/res/values/strings-diagnose.xml
@@ -34,6 +34,7 @@
TimeLimit Server: %s
connected
not connected
+ Network ID
Synchronization
There is nothing which needs to be synchronized
diff --git a/app/src/main/res/values/strings-lock.xml b/app/src/main/res/values/strings-lock.xml
index 67884ca..ecc48f1 100644
--- a/app/src/main/res/values/strings-lock.xml
+++ b/app/src/main/res/values/strings-lock.xml
@@ -52,6 +52,9 @@
Confirm time manually
This time is correct
+ Grant permission
+ Allow TimeLimit to see the connected network
+
This App was not assigned to any category.
Due to that, there are no restriction settings.
@@ -89,6 +92,9 @@
and this limit was reached.
It will be unlocked after the break duration.
+
+ This %s is only allowed in some networks, but no connection to such network was found.
+
no category
temporarily blocked
@@ -99,6 +105,7 @@
all notifications are blocked
battery limit reached
session duration limit reached
+ no allowed network
Failed to open the lock screen.
diff --git a/app/src/main/res/values/strings-manage-category-networks.xml b/app/src/main/res/values/strings-manage-category-networks.xml
new file mode 100644
index 0000000..32a9d57
--- /dev/null
+++ b/app/src/main/res/values/strings-manage-category-networks.xml
@@ -0,0 +1,43 @@
+
+
+
+ Networks
+ Here you can add WiFi networks. This category will be blocked
+ if there are WiFi networks added and there is no connection to a specified network.
+ A network is identified by an access point. In case of multiple access points/ repeaters,
+ you have to add all of them one by one.
+ Disabling the time limits temporarily additionally disables limitations caused by this feature.
+
+ You did not add any networks. This category is not limited to specific networks.
+ You added %s. This category is limited to the specified networks.
+
+ - %d network
+ - %d networks
+
+ The permission to get the connected network is missing
+ You are not connected to any WiFi network
+ You are connected to a network which was not added
+ You are connected to a network which was not added,
+ but you can not add more networks to this category
+
+ You are connected to a network which was added
+ Add network
+ Grant permission
+ Remove all networks
+ All networks removed
+
+ Please enable location access
+
\ No newline at end of file