diff --git a/app/schemas/io.timelimit.android.data.RoomDatabase/12.json b/app/schemas/io.timelimit.android.data.RoomDatabase/12.json
index 238cb09..d38e412 100644
--- a/app/schemas/io.timelimit.android.data.RoomDatabase/12.json
+++ b/app/schemas/io.timelimit.android.data.RoomDatabase/12.json
@@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 12,
- "identityHash": "6634817cfeb16e6c23aced572b2a391e",
+ "identityHash": "289393ddc0b615d627ac88802fbfb977",
"entities": [
{
"tableName": "user",
@@ -92,7 +92,7 @@
},
{
"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, `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, PRIMARY KEY(`id`))",
+ "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, `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, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
@@ -237,6 +237,18 @@
"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
}
],
"primaryKey": {
@@ -356,7 +368,7 @@
},
{
"tableName": "category",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `child_id` TEXT NOT NULL, `title` TEXT NOT NULL, `blocked_times` TEXT NOT NULL, `extra_time` INTEGER NOT NULL, `temporarily_blocked` INTEGER NOT NULL, `block_all_notifications` 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, PRIMARY KEY(`id`))",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `child_id` TEXT NOT NULL, `title` TEXT NOT NULL, `blocked_times` TEXT NOT NULL, `extra_time` INTEGER NOT NULL, `temporarily_blocked` INTEGER NOT NULL, `base_version` TEXT NOT NULL, `apps_version` TEXT NOT NULL, `rules_version` TEXT NOT NULL, `usedtimes_version` TEXT NOT NULL, `parent_category_id` TEXT NOT NULL, `block_all_notifications` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
@@ -394,12 +406,6 @@
"affinity": "INTEGER",
"notNull": true
},
- {
- "fieldPath": "blockAllNotifications",
- "columnName": "block_all_notifications",
- "affinity": "INTEGER",
- "notNull": true
- },
{
"fieldPath": "baseVersion",
"columnName": "base_version",
@@ -429,6 +435,12 @@
"columnName": "parent_category_id",
"affinity": "TEXT",
"notNull": true
+ },
+ {
+ "fieldPath": "blockAllNotifications",
+ "columnName": "block_all_notifications",
+ "affinity": "INTEGER",
+ "notNull": true
}
],
"primaryKey": {
@@ -632,7 +644,7 @@
],
"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, \"6634817cfeb16e6c23aced572b2a391e\")"
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"289393ddc0b615d627ac88802fbfb977\")"
]
}
}
\ No newline at end of file
diff --git a/app/schemas/io.timelimit.android.data.RoomDatabase/13.json b/app/schemas/io.timelimit.android.data.RoomDatabase/13.json
new file mode 100644
index 0000000..b6d4a17
--- /dev/null
+++ b/app/schemas/io.timelimit.android.data.RoomDatabase/13.json
@@ -0,0 +1,650 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 13,
+ "identityHash": "289393ddc0b615d627ac88802fbfb977",
+ "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, 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
+ }
+ ],
+ "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, `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, 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": "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
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "app",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`device_id` TEXT NOT NULL, `package_name` TEXT NOT NULL, `title` TEXT NOT NULL, `launchable` INTEGER NOT NULL, `recommendation` TEXT NOT NULL, PRIMARY KEY(`device_id`, `package_name`))",
+ "fields": [
+ {
+ "fieldPath": "deviceId",
+ "columnName": "device_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "packageName",
+ "columnName": "package_name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isLaunchable",
+ "columnName": "launchable",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "recommendation",
+ "columnName": "recommendation",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "device_id",
+ "package_name"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_app_device_id",
+ "unique": false,
+ "columnNames": [
+ "device_id"
+ ],
+ "createSql": "CREATE INDEX `index_app_device_id` ON `${TABLE_NAME}` (`device_id`)"
+ },
+ {
+ "name": "index_app_package_name",
+ "unique": false,
+ "columnNames": [
+ "package_name"
+ ],
+ "createSql": "CREATE INDEX `index_app_package_name` ON `${TABLE_NAME}` (`package_name`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "category_app",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`category_id` TEXT NOT NULL, `package_name` TEXT NOT NULL, PRIMARY KEY(`category_id`, `package_name`))",
+ "fields": [
+ {
+ "fieldPath": "categoryId",
+ "columnName": "category_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "packageName",
+ "columnName": "package_name",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "category_id",
+ "package_name"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_category_app_category_id",
+ "unique": false,
+ "columnNames": [
+ "category_id"
+ ],
+ "createSql": "CREATE INDEX `index_category_app_category_id` ON `${TABLE_NAME}` (`category_id`)"
+ },
+ {
+ "name": "index_category_app_package_name",
+ "unique": false,
+ "columnNames": [
+ "package_name"
+ ],
+ "createSql": "CREATE INDEX `index_category_app_package_name` ON `${TABLE_NAME}` (`package_name`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "category",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `child_id` TEXT NOT NULL, `title` TEXT NOT NULL, `blocked_times` TEXT NOT NULL, `extra_time` INTEGER NOT NULL, `temporarily_blocked` INTEGER NOT NULL, `base_version` TEXT NOT NULL, `apps_version` TEXT NOT NULL, `rules_version` TEXT NOT NULL, `usedtimes_version` TEXT NOT NULL, `parent_category_id` TEXT NOT NULL, `block_all_notifications` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "childId",
+ "columnName": "child_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "blockedMinutesInWeek",
+ "columnName": "blocked_times",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "extraTimeInMillis",
+ "columnName": "extra_time",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "temporarilyBlocked",
+ "columnName": "temporarily_blocked",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "baseVersion",
+ "columnName": "base_version",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "assignedAppsVersion",
+ "columnName": "apps_version",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timeLimitRulesVersion",
+ "columnName": "rules_version",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "usedTimesVersion",
+ "columnName": "usedtimes_version",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "parentCategoryId",
+ "columnName": "parent_category_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "blockAllNotifications",
+ "columnName": "block_all_notifications",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "used_time",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`day_of_epoch` INTEGER NOT NULL, `used_time` INTEGER NOT NULL, `category_id` TEXT NOT NULL, PRIMARY KEY(`category_id`, `day_of_epoch`))",
+ "fields": [
+ {
+ "fieldPath": "dayOfEpoch",
+ "columnName": "day_of_epoch",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "usedMillis",
+ "columnName": "used_time",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "categoryId",
+ "columnName": "category_id",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "category_id",
+ "day_of_epoch"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "time_limit_rule",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `category_id` TEXT NOT NULL, `apply_to_extra_time_usage` INTEGER NOT NULL, `day_mask` INTEGER NOT NULL, `max_time` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "categoryId",
+ "columnName": "category_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "applyToExtraTimeUsage",
+ "columnName": "apply_to_extra_time_usage",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "dayMask",
+ "columnName": "day_mask",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "maximumTimeInMillis",
+ "columnName": "max_time",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "config",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "key",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "temporarily_allowed_app",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`device_id` TEXT NOT NULL, `package_name` TEXT NOT NULL, PRIMARY KEY(`device_id`, `package_name`))",
+ "fields": [
+ {
+ "fieldPath": "deviceId",
+ "columnName": "device_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "packageName",
+ "columnName": "package_name",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "device_id",
+ "package_name"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "pending_sync_action",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sequence_number` INTEGER NOT NULL, `action` TEXT NOT NULL, `integrity` TEXT NOT NULL, `scheduled_for_upload` INTEGER NOT NULL, `type` TEXT NOT NULL, `user_id` TEXT NOT NULL, PRIMARY KEY(`sequence_number`))",
+ "fields": [
+ {
+ "fieldPath": "sequenceNumber",
+ "columnName": "sequence_number",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "encodedAction",
+ "columnName": "action",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "integrity",
+ "columnName": "integrity",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "scheduledForUpload",
+ "columnName": "scheduled_for_upload",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "sequence_number"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_pending_sync_action_scheduled_for_upload",
+ "unique": false,
+ "columnNames": [
+ "scheduled_for_upload"
+ ],
+ "createSql": "CREATE INDEX `index_pending_sync_action_scheduled_for_upload` ON `${TABLE_NAME}` (`scheduled_for_upload`)"
+ }
+ ],
+ "foreignKeys": []
+ }
+ ],
+ "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, \"289393ddc0b615d627ac88802fbfb977\")"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f5e1abf..2f215ac 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -20,7 +20,6 @@
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 c30346a..3e518ce 100644
--- a/app/src/main/java/io/timelimit/android/data/Migrations.kt
+++ b/app/src/main/java/io/timelimit/android/data/Migrations.kt
@@ -91,4 +91,11 @@ object DatabaseMigrations {
database.execSQL("ALTER TABLE `category` ADD COLUMN `block_all_notifications` INTEGER NOT NULL DEFAULT 0")
}
}
+
+ val MIGRATE_TO_V13 = object: Migration(12, 13) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL("ALTER TABLE `device` ADD COLUMN `current_overlay_permission` TEXT NOT NULL DEFAULT \"not granted\"")
+ database.execSQL("ALTER TABLE `device` ADD COLUMN `highest_overlay_permission` TEXT NOT NULL DEFAULT \"not granted\"")
+ }
+ }
}
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 1239b61..b4ca418 100644
--- a/app/src/main/java/io/timelimit/android/data/RoomDatabase.kt
+++ b/app/src/main/java/io/timelimit/android/data/RoomDatabase.kt
@@ -32,7 +32,7 @@ import io.timelimit.android.data.model.*
ConfigurationItem::class,
TemporarilyAllowedApp::class,
PendingSyncAction::class
-], version = 12)
+], version = 13)
abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database {
companion object {
private val lock = Object()
@@ -78,7 +78,8 @@ abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database
DatabaseMigrations.MIGRATE_TO_V9,
DatabaseMigrations.MIGRATE_TO_V10,
DatabaseMigrations.MIGRATE_TO_V11,
- DatabaseMigrations.MIGRATE_TO_V12
+ DatabaseMigrations.MIGRATE_TO_V12,
+ DatabaseMigrations.MIGRATE_TO_V13
)
.build()
}
diff --git a/app/src/main/java/io/timelimit/android/data/model/Device.kt b/app/src/main/java/io/timelimit/android/data/model/Device.kt
index f772894..76c80dc 100644
--- a/app/src/main/java/io/timelimit/android/data/model/Device.kt
+++ b/app/src/main/java/io/timelimit/android/data/model/Device.kt
@@ -78,7 +78,11 @@ data class Device(
@ColumnInfo(name = "default_user_timeout")
val defaultUserTimeout: Int,
@ColumnInfo(name = "consider_reboot_manipulation")
- val considerRebootManipulation: Boolean
+ val considerRebootManipulation: Boolean,
+ @ColumnInfo(name = "current_overlay_permission")
+ val currentOverlayPermission: RuntimePermissionStatus,
+ @ColumnInfo(name = "highest_overlay_permission")
+ val highestOverlayPermission: RuntimePermissionStatus
): JsonSerializable {
companion object {
private const val ID = "id"
@@ -105,6 +109,8 @@ data class Device(
private const val DEFAULT_USER = "du"
private const val DEFAULT_USER_TIMEOUT = "dut"
private const val CONSIDER_REBOOT_A_MANIPULATION = "cram"
+ private const val CURRENT_OVERLAY_PERMISSION = "cop"
+ private const val HIGHEST_OVERLAY_PERMISSION = "hop"
fun parse(reader: JsonReader): Device {
var id: String? = null
@@ -131,6 +137,8 @@ data class Device(
var defaultUser = ""
var defaultUserTimeout = 0
var considerRebootManipulation = false
+ var currentOverlayPermission = RuntimePermissionStatus.NotGranted
+ var highestOverlayPermission = RuntimePermissionStatus.NotGranted
reader.beginObject()
@@ -160,6 +168,8 @@ data class Device(
DEFAULT_USER -> defaultUser = reader.nextString()
DEFAULT_USER_TIMEOUT -> defaultUserTimeout = reader.nextInt()
CONSIDER_REBOOT_A_MANIPULATION -> considerRebootManipulation = reader.nextBoolean()
+ CURRENT_OVERLAY_PERMISSION -> currentOverlayPermission = RuntimePermissionStatusUtil.parse(reader.nextString())
+ HIGHEST_OVERLAY_PERMISSION -> highestOverlayPermission = RuntimePermissionStatusUtil.parse(reader.nextString())
else -> reader.skipValue()
}
}
@@ -190,7 +200,9 @@ data class Device(
showDeviceConnected = showDeviceConnected,
defaultUser = defaultUser,
defaultUserTimeout = defaultUserTimeout,
- considerRebootManipulation = considerRebootManipulation
+ considerRebootManipulation = considerRebootManipulation,
+ currentOverlayPermission = currentOverlayPermission,
+ highestOverlayPermission = highestOverlayPermission
)
}
}
@@ -254,6 +266,8 @@ data class Device(
writer.name(DEFAULT_USER).value(defaultUser)
writer.name(DEFAULT_USER_TIMEOUT).value(defaultUserTimeout)
writer.name(CONSIDER_REBOOT_A_MANIPULATION).value(considerRebootManipulation)
+ writer.name(CURRENT_OVERLAY_PERMISSION).value(RuntimePermissionStatusUtil.serialize(currentOverlayPermission))
+ writer.name(HIGHEST_OVERLAY_PERMISSION).value(RuntimePermissionStatusUtil.serialize(highestOverlayPermission))
writer.endObject()
}
@@ -266,6 +280,8 @@ data class Device(
val manipulationOfNotificationAccess = currentNotificationAccessPermission != highestNotificationAccessPermission
@Transient
val manipulationOfAppVersion = currentAppVersion != highestAppVersion
+ @Transient
+ val manipulationOfOverlayPermission = currentOverlayPermission != highestOverlayPermission
@Transient
val hasActiveManipulationWarning = manipulationOfProtectionLevel ||
@@ -273,7 +289,8 @@ data class Device(
manipulationOfNotificationAccess ||
manipulationOfAppVersion ||
manipulationTriedDisablingDeviceAdmin ||
- manipulationDidReboot
+ manipulationDidReboot ||
+ manipulationOfOverlayPermission
@Transient
val hasAnyManipulation = hasActiveManipulationWarning || hadManipulation
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 fabf82c..103131a 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
@@ -31,12 +31,14 @@ abstract class PlatformIntegration(
abstract fun getForegroundAppPermissionStatus(): RuntimePermissionStatus
abstract fun getDrawOverOtherAppsPermissionStatus(): RuntimePermissionStatus
abstract fun getNotificationAccessPermissionStatus(): NewPermissionStatus
+ abstract fun getOverlayPermissionStatus(): RuntimePermissionStatus
abstract fun disableDeviceAdmin()
abstract fun trySetLockScreenPassword(password: String): Boolean
// this must have a fallback if the permission is not granted
abstract fun showOverlayMessage(text: String)
abstract fun showAppLockScreen(currentPackageName: String)
+ abstract fun setShowBlockingOverlay(show: Boolean)
// this should throw an SecurityException if the permission is missing
abstract suspend fun getForegroundAppPackageName(): String?
abstract fun setAppStatusMessage(message: AppStatusMessage?)
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 6101edf..c49b994 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
@@ -17,6 +17,7 @@ package io.timelimit.android.integration.platform.android
import android.annotation.TargetApi
import android.app.ActivityManager
+import android.app.Application
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.admin.DevicePolicyManager
@@ -73,6 +74,7 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio
private val activityManager = this.context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
private val notificationManager = this.context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
private val deviceAdmin = ComponentName(context.applicationContext, AdminReceiver::class.java)
+ private val overlay = OverlayUtil(context as Application)
init {
AppsChangeListener.registerBroadcastReceiver(this.context, object : BroadcastReceiver() {
@@ -136,6 +138,8 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio
}
}
+ override fun getOverlayPermissionStatus(): RuntimePermissionStatus = overlay.getOverlayPermissionStatus()
+
override fun trySetLockScreenPassword(password: String): Boolean {
if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "set password")
@@ -186,6 +190,14 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio
LockActivity.start(context, currentPackageName)
}
+ override fun setShowBlockingOverlay(show: Boolean) {
+ if (show) {
+ overlay.show()
+ } else {
+ overlay.hide()
+ }
+ }
+
override fun isScreenOn(): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
return powerManager.isInteractive
diff --git a/app/src/main/java/io/timelimit/android/integration/platform/android/OverlayUtil.kt b/app/src/main/java/io/timelimit/android/integration/platform/android/OverlayUtil.kt
new file mode 100644
index 0000000..7c2f707
--- /dev/null
+++ b/app/src/main/java/io/timelimit/android/integration/platform/android/OverlayUtil.kt
@@ -0,0 +1,81 @@
+/*
+ * TimeLimit Copyright 2019 Jonas Lochmann
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package io.timelimit.android.integration.platform.android
+
+import android.app.Application
+import android.content.Context
+import android.view.WindowManager
+import android.graphics.PixelFormat
+import android.os.Build
+import android.provider.Settings
+import android.view.LayoutInflater
+import io.timelimit.android.async.Threads
+import io.timelimit.android.databinding.BlockingOverlayBinding
+import io.timelimit.android.integration.platform.RuntimePermissionStatus
+
+class OverlayUtil(private var application: Application) {
+ private val windowManager = application.getSystemService(Context.WINDOW_SERVICE) as WindowManager
+ private var currentView: BlockingOverlayBinding? = null
+
+ fun show() {
+ if (currentView != null) {
+ return
+ }
+
+ if (getOverlayPermissionStatus() == RuntimePermissionStatus.NotGranted) {
+ return
+ }
+
+ val view = BlockingOverlayBinding.inflate(LayoutInflater.from(application))
+
+ val params = WindowManager.LayoutParams(
+ WindowManager.LayoutParams.MATCH_PARENT,
+ WindowManager.LayoutParams.MATCH_PARENT,
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
+ WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
+ else
+ WindowManager.LayoutParams.TYPE_SYSTEM_ALERT,
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
+ PixelFormat.TRANSLUCENT
+ )
+
+ windowManager.addView(view.root, params)
+ currentView = view
+
+ Threads.mainThreadHandler.postDelayed({
+ view.showWarningMessage = true
+ }, 2000)
+ }
+
+ fun hide() {
+ if (currentView == null) {
+ return
+ }
+
+ windowManager.removeView(currentView!!.root)
+ currentView = null
+ }
+
+ fun isOverlayShown() = currentView?.root?.isShown ?: false
+
+ fun getOverlayPermissionStatus() = /*if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
+ if (Settings.canDrawOverlays(application))
+ RuntimePermissionStatus.Granted
+ else
+ */RuntimePermissionStatus.NotGranted/*
+ else
+ RuntimePermissionStatus.NotRequired*/
+}
\ No newline at end of file
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 215b1f7..979d615 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
@@ -61,6 +61,10 @@ class DummyIntegration(
return notificationAccess
}
+ override fun getOverlayPermissionStatus(): RuntimePermissionStatus {
+ return RuntimePermissionStatus.NotRequired
+ }
+
override fun trySetLockScreenPassword(password: String): Boolean {
return false // it failed
}
@@ -72,6 +76,10 @@ class DummyIntegration(
launchLockScreenForPackage = currentPackageName
}
+ override fun setShowBlockingOverlay(show: Boolean) {
+ // ignore
+ }
+
fun getAndResetShowAppLockScreen(): String? {
try {
return launchLockScreenForPackage
diff --git a/app/src/main/java/io/timelimit/android/logic/AppSetupLogic.kt b/app/src/main/java/io/timelimit/android/logic/AppSetupLogic.kt
index 7db456d..b4b3f6b 100644
--- a/app/src/main/java/io/timelimit/android/logic/AppSetupLogic.kt
+++ b/app/src/main/java/io/timelimit/android/logic/AppSetupLogic.kt
@@ -95,7 +95,9 @@ class AppSetupLogic(private val appLogic: AppLogic) {
showDeviceConnected = false,
defaultUser = "",
defaultUserTimeout = 0,
- considerRebootManipulation = false
+ considerRebootManipulation = false,
+ currentOverlayPermission = RuntimePermissionStatus.NotGranted,
+ highestOverlayPermission = RuntimePermissionStatus.NotGranted
)
appLogic.database.device().addDeviceSync(device)
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 ac5dcfb..0ed98e7 100644
--- a/app/src/main/java/io/timelimit/android/logic/BackgroundTaskLogic.kt
+++ b/app/src/main/java/io/timelimit/android/logic/BackgroundTaskLogic.kt
@@ -35,7 +35,6 @@ import io.timelimit.android.integration.platform.android.AndroidIntegrationApps
import io.timelimit.android.livedata.*
import io.timelimit.android.sync.actions.UpdateDeviceStatusAction
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
-import io.timelimit.android.ui.IsAppInForeground
import io.timelimit.android.util.TimeTextUtil
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@@ -144,6 +143,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
usedTimeUpdateHelper?.commit(appLogic)
liveDataCaches.removeAllItems()
appLogic.platformIntegration.setAppStatusMessage(null)
+ appLogic.platformIntegration.setShowBlockingOverlay(false)
appLogic.enable.waitUntilValueMatches { it == true }
continue
@@ -165,12 +165,14 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
text = appLogic.context.getString(R.string.background_logic_timeout_text)
)
)
+ appLogic.platformIntegration.setShowBlockingOverlay(false)
liveDataCaches.reportLoopDone()
appLogic.timeApi.sleep(BACKGROUND_SERVICE_INTERVAL)
} else {
liveDataCaches.removeAllItems()
appLogic.platformIntegration.setAppStatusMessage(null)
+ appLogic.platformIntegration.setShowBlockingOverlay(false)
val isChildSignedIn = deviceUserEntryLive.read().map { it != null && it.type == UserType.Child }
@@ -238,12 +240,14 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
appTitleCache.query(foregroundAppPackageName),
appLogic.context.getString(R.string.background_logic_whitelisted)
))
+ appLogic.platformIntegration.setShowBlockingOverlay(false)
} else if (foregroundAppPackageName != null && temporarilyAllowedApps.contains(foregroundAppPackageName)) {
usedTimeUpdateHelper?.commit(appLogic)
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
appTitleCache.query(foregroundAppPackageName),
appLogic.context.getString(R.string.background_logic_temporarily_allowed)
))
+ appLogic.platformIntegration.setShowBlockingOverlay(false)
} else if (foregroundAppPackageName != null) {
val appCategory = appCategories.get(Pair(foregroundAppPackageName, categories.map { it.id })).waitForNullableValue()
val category = categories.find { it.id == appCategory?.categoryId }
@@ -259,6 +263,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
))
appLogic.platformIntegration.setSuspendedApps(listOf(foregroundAppPackageName), true)
appLogic.platformIntegration.showAppLockScreen(foregroundAppPackageName)
+ appLogic.platformIntegration.setShowBlockingOverlay(true)
} else if (category.temporarilyBlocked or (parentCategory?.temporarilyBlocked == true)) {
usedTimeUpdateHelper?.commit(appLogic)
@@ -267,6 +272,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
text = appLogic.context.getString(R.string.background_logic_opening_lockscreen)
))
appLogic.platformIntegration.showAppLockScreen(foregroundAppPackageName)
+ appLogic.platformIntegration.setShowBlockingOverlay(true)
} else {
// disable time limits temporarily feature
if (realTime.shouldTrustTimeTemporarily && nowTimestamp < deviceUserEntry.disableLimitsUntil) {
@@ -274,6 +280,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
title = appTitleCache.query(foregroundAppPackageName),
text = appLogic.context.getString(R.string.background_logic_limits_disabled)
))
+ appLogic.platformIntegration.setShowBlockingOverlay(false)
} else if (
// check blocked time areas
// directly blocked
@@ -295,6 +302,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
text = appLogic.context.getString(R.string.background_logic_opening_lockscreen)
))
appLogic.platformIntegration.showAppLockScreen(foregroundAppPackageName)
+ appLogic.platformIntegration.setShowBlockingOverlay(true)
} else {
// check time limits
val rules = timeLimitRules.get(category.id).waitForNonNullValue()
@@ -310,6 +318,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
category.title + " - " + appTitleCache.query(foregroundAppPackageName),
appLogic.context.getString(R.string.background_logic_no_timelimit)
))
+ appLogic.platformIntegration.setShowBlockingOverlay(false)
} else {
val isCurrentDevice = isThisDeviceTheCurrentDeviceLive.read().waitForNonNullValue()
@@ -321,6 +330,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
text = appLogic.context.getString(R.string.background_logic_opening_lockscreen)
))
appLogic.platformIntegration.showAppLockScreen(foregroundAppPackageName)
+ appLogic.platformIntegration.setShowBlockingOverlay(true)
} else if (realTime.shouldTrustTimeTemporarily) {
val usedTimes = usedTimesOfCategoryAndWeekByFirstDayOfWeek.get(Pair(category.id, nowDate.dayOfEpoch - nowDate.dayOfWeek)).waitForNonNullValue()
val parentUsedTimes = parentCategory?.let {
@@ -387,6 +397,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
category.title + " - " + appTitleCache.query(foregroundAppPackageName),
appLogic.context.getString(R.string.background_logic_no_timelimit)
))
+ appLogic.platformIntegration.setShowBlockingOverlay(false)
} else {
// time limited
if (remaining.includingExtraTime > 0) {
@@ -397,6 +408,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
category.title + " - " + appTitleCache.query(foregroundAppPackageName),
appLogic.context.getString(R.string.background_logic_using_extra_time, TimeTextUtil.remaining(remaining.includingExtraTime.toInt(), appLogic.context))
))
+ appLogic.platformIntegration.setShowBlockingOverlay(false)
if (isScreenOn) {
newUsedTimeItemBatchUpdateHelper.addUsedTime(
@@ -412,6 +424,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
category.title + " - " + appTitleCache.query(foregroundAppPackageName),
TimeTextUtil.remaining(remaining.default.toInt(), appLogic.context)
))
+ appLogic.platformIntegration.setShowBlockingOverlay(false)
if (isScreenOn) {
newUsedTimeItemBatchUpdateHelper.addUsedTime(
@@ -431,6 +444,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
text = appLogic.context.getString(R.string.background_logic_opening_lockscreen)
))
appLogic.platformIntegration.showAppLockScreen(foregroundAppPackageName)
+ appLogic.platformIntegration.setShowBlockingOverlay(true)
}
}
} else {
@@ -443,6 +457,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
text = appLogic.context.getString(R.string.background_logic_opening_lockscreen)
))
appLogic.platformIntegration.showAppLockScreen(foregroundAppPackageName)
+ appLogic.platformIntegration.setShowBlockingOverlay(true)
}
}
}
@@ -452,6 +467,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
appLogic.context.getString(R.string.background_logic_idle_title),
appLogic.context.getString(R.string.background_logic_idle_text)
))
+ appLogic.platformIntegration.setShowBlockingOverlay(false)
}
} catch (ex: SecurityException) {
// this is handled by an other main loop (with a delay)
@@ -460,6 +476,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
appLogic.context.getString(R.string.background_logic_error),
appLogic.context.getString(R.string.background_logic_error_permission)
))
+ appLogic.platformIntegration.setShowBlockingOverlay(false)
} catch (ex: Exception) {
if (BuildConfig.DEBUG) {
Log.w(LOG_TAG, "exception during running main loop", ex)
@@ -469,6 +486,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
appLogic.context.getString(R.string.background_logic_error),
appLogic.context.getString(R.string.background_logic_error_internal)
))
+ appLogic.platformIntegration.setShowBlockingOverlay(false)
}
liveDataCaches.reportLoopDone()
@@ -542,6 +560,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
val protectionLevel = appLogic.platformIntegration.getCurrentProtectionLevel()
val usageStatsPermission = appLogic.platformIntegration.getForegroundAppPermissionStatus()
val notificationAccess = appLogic.platformIntegration.getNotificationAccessPermissionStatus()
+ val overlayPermission = appLogic.platformIntegration.getOverlayPermissionStatus()
var changes = UpdateDeviceStatusAction.empty
@@ -567,6 +586,12 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
)
}
+ if (overlayPermission != deviceEntry.currentOverlayPermission) {
+ changes = changes.copy(
+ newOverlayPermission = overlayPermission
+ )
+ }
+
if (changes != UpdateDeviceStatusAction.empty) {
ApplyActionUtil.applyAppLogicAction(
action = changes,
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 0f0fcc1..6565d1b 100644
--- a/app/src/main/java/io/timelimit/android/sync/ApplyServerDataStatus.kt
+++ b/app/src/main/java/io/timelimit/android/sync/ApplyServerDataStatus.kt
@@ -154,7 +154,9 @@ object ApplyServerDataStatus {
showDeviceConnected = newDevice.showDeviceConnected,
defaultUser = newDevice.defaultUser,
defaultUserTimeout = newDevice.defaultUserTimeout,
- considerRebootManipulation = newDevice.considerRebootManipulation
+ considerRebootManipulation = newDevice.considerRebootManipulation,
+ currentOverlayPermission = newDevice.currentOverlayPermission,
+ highestOverlayPermission = newDevice.highestOverlayPermission
))
} else {
// eventually update old entry
@@ -181,7 +183,9 @@ object ApplyServerDataStatus {
showDeviceConnected = newDevice.showDeviceConnected,
defaultUser = newDevice.defaultUser,
defaultUserTimeout = newDevice.defaultUserTimeout,
- considerRebootManipulation = newDevice.considerRebootManipulation
+ considerRebootManipulation = newDevice.considerRebootManipulation,
+ currentOverlayPermission = newDevice.currentOverlayPermission,
+ highestOverlayPermission = newDevice.highestOverlayPermission
)
if (updatedDeviceEntry != oldDeviceEntry) {
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 57d6639..0c5c255 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
@@ -563,6 +563,7 @@ data class UpdateDeviceStatusAction(
val newProtectionLevel: ProtectionLevel?,
val newUsageStatsPermissionStatus: RuntimePermissionStatus?,
val newNotificationAccessPermission: NewPermissionStatus?,
+ val newOverlayPermission: RuntimePermissionStatus?,
val newAppVersion: Int?,
val didReboot: Boolean
): AppLogicAction() {
@@ -571,6 +572,7 @@ data class UpdateDeviceStatusAction(
private const val NEW_PROTECTION_LEVEL = "protectionLevel"
private const val NEW_USAGE_STATS_PERMISSION_STATUS = "usageStats"
private const val NEW_NOTIFICATION_ACCESS_PERMISSION = "notificationAccess"
+ private const val NEW_OVERLAY_PERMISSION = "overlayPermission"
private const val NEW_APP_VERSION = "appVersion"
private const val DID_REBOOT = "didReboot"
@@ -578,6 +580,7 @@ data class UpdateDeviceStatusAction(
newProtectionLevel = null,
newUsageStatsPermissionStatus = null,
newNotificationAccessPermission = null,
+ newOverlayPermission = null,
newAppVersion = null,
didReboot = false
)
@@ -611,6 +614,12 @@ data class UpdateDeviceStatusAction(
.value(NewPermissionStatusUtil.serialize(newNotificationAccessPermission))
}
+ if (newOverlayPermission != null) {
+ writer
+ .name(NEW_OVERLAY_PERMISSION)
+ .value(RuntimePermissionStatusUtil.serialize(newOverlayPermission))
+ }
+
if (newAppVersion != null) {
writer.name(NEW_APP_VERSION)
writer.value(newAppVersion)
@@ -631,6 +640,7 @@ data class IgnoreManipulationAction(
val ignoreAppDowngrade: Boolean,
val ignoreNotificationAccessManipulation: Boolean,
val ignoreUsageStatsAccessManipulation: Boolean,
+ val ignoreOverlayPermissionManipulation: Boolean,
val ignoreReboot: Boolean,
val ignoreHadManipulation: Boolean
): ParentAction() {
@@ -642,6 +652,7 @@ data class IgnoreManipulationAction(
private const val IGNORE_APP_DOWNGRADE = "downgrade"
private const val IGNORE_NOTIFICATION_ACCESS = "notification"
private const val IGNORE_USAGE_STATS_ACCESS = "usageStats"
+ private const val IGNORE_OVERLAY_PERMISSION_MANIPULATION = "overlay"
private const val IGNORE_HAD_MANIPULATION = "hadManipulation"
private const val IGNORE_REBOOT = "reboot"
}
@@ -655,6 +666,7 @@ data class IgnoreManipulationAction(
(!ignoreAppDowngrade) &&
(!ignoreNotificationAccessManipulation) &&
(!ignoreUsageStatsAccessManipulation) &&
+ (!ignoreOverlayPermissionManipulation) &&
(!ignoreReboot) &&
(!ignoreHadManipulation)
@@ -668,6 +680,7 @@ data class IgnoreManipulationAction(
writer.name(IGNORE_APP_DOWNGRADE).value(ignoreAppDowngrade)
writer.name(IGNORE_NOTIFICATION_ACCESS).value(ignoreNotificationAccessManipulation)
writer.name(IGNORE_USAGE_STATS_ACCESS).value(ignoreUsageStatsAccessManipulation)
+ writer.name(IGNORE_OVERLAY_PERMISSION_MANIPULATION).value(ignoreOverlayPermissionManipulation)
writer.name(IGNORE_HAD_MANIPULATION).value(ignoreHadManipulation)
writer.name(IGNORE_REBOOT).value(ignoreReboot)
diff --git a/app/src/main/java/io/timelimit/android/sync/actions/dispatch/AppLogicAction.kt b/app/src/main/java/io/timelimit/android/sync/actions/dispatch/AppLogicAction.kt
index 33d8351..f42f909 100644
--- a/app/src/main/java/io/timelimit/android/sync/actions/dispatch/AppLogicAction.kt
+++ b/app/src/main/java/io/timelimit/android/sync/actions/dispatch/AppLogicAction.kt
@@ -150,6 +150,24 @@ object LocalDatabaseAppLogicActionDispatcher {
}
}
+ if (action.newOverlayPermission != null) {
+ if (device.currentOverlayPermission != action.newOverlayPermission) {
+ device = device.copy(
+ currentOverlayPermission = action.newOverlayPermission
+ )
+
+ if (RuntimePermissionStatusUtil.toInt(action.newOverlayPermission) > RuntimePermissionStatusUtil.toInt(device.highestOverlayPermission)) {
+ device = device.copy(
+ highestOverlayPermission = action.newOverlayPermission
+ )
+ }
+
+ if (device.currentOverlayPermission != device.highestOverlayPermission) {
+ device = device.copy(hadManipulation = true)
+ }
+ }
+ }
+
if (action.newAppVersion != null) {
if (device.currentAppVersion != action.newAppVersion) {
device = device.copy(
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 8380de1..5f452f0 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
@@ -290,6 +290,10 @@ object LocalDatabaseParentActionDispatcher {
deviceEntry = deviceEntry.copy(highestUsageStatsPermission = deviceEntry.currentUsageStatsPermission)
}
+ if (action.ignoreOverlayPermissionManipulation) {
+ deviceEntry = deviceEntry.copy(highestOverlayPermission = deviceEntry.currentOverlayPermission)
+ }
+
if (action.ignoreReboot) {
deviceEntry = deviceEntry.copy(manipulationDidReboot = false)
}
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 494483a..17317cf 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
@@ -178,7 +178,9 @@ data class ServerDeviceData(
val showDeviceConnected: Boolean,
val defaultUser: String,
val defaultUserTimeout: Int,
- val considerRebootManipulation: Boolean
+ val considerRebootManipulation: Boolean,
+ val currentOverlayPermission: RuntimePermissionStatus,
+ val highestOverlayPermission: RuntimePermissionStatus
) {
companion object {
private const val DEVICE_ID = "deviceId"
@@ -204,6 +206,8 @@ data class ServerDeviceData(
private const val DEFAULT_USER = "defUser"
private const val DEFAULT_USER_TIMEOUT = "defUserTimeout"
private const val CONSIDER_REBOOT_MANIPULATION = "rebootIsManipulation"
+ private const val CURRENT_OVERLAY_PERMISSION = "cOverlay"
+ private const val HIGHEST_OVERLAY_PERMISSION = "hOverlay"
fun parse(reader: JsonReader): ServerDeviceData {
var deviceId: String? = null
@@ -229,6 +233,8 @@ data class ServerDeviceData(
var defaultUser: String? = null
var defaultUserTimeout: Int? = null
var considerRebootManipulation: Boolean? = null
+ var currentOverlayPermission: RuntimePermissionStatus? = null
+ var highestOverlayPermission: RuntimePermissionStatus? = null
reader.beginObject()
while (reader.hasNext()) {
@@ -256,6 +262,8 @@ data class ServerDeviceData(
DEFAULT_USER -> defaultUser = reader.nextString()
DEFAULT_USER_TIMEOUT -> defaultUserTimeout = reader.nextInt()
CONSIDER_REBOOT_MANIPULATION -> considerRebootManipulation = reader.nextBoolean()
+ CURRENT_OVERLAY_PERMISSION -> currentOverlayPermission = RuntimePermissionStatusUtil.parse(reader.nextString())
+ HIGHEST_OVERLAY_PERMISSION -> highestOverlayPermission = RuntimePermissionStatusUtil.parse(reader.nextString())
else -> reader.skipValue()
}
}
@@ -284,7 +292,9 @@ data class ServerDeviceData(
showDeviceConnected = showDeviceConnected!!,
defaultUser = defaultUser!!,
defaultUserTimeout = defaultUserTimeout!!,
- considerRebootManipulation = considerRebootManipulation!!
+ considerRebootManipulation = considerRebootManipulation!!,
+ currentOverlayPermission = currentOverlayPermission!!,
+ highestOverlayPermission = highestOverlayPermission!!
)
}
diff --git a/app/src/main/java/io/timelimit/android/ui/manage/device/manage/ManageDeviceFragment.kt b/app/src/main/java/io/timelimit/android/ui/manage/device/manage/ManageDeviceFragment.kt
index 8ffb3fd..db26c4f 100644
--- a/app/src/main/java/io/timelimit/android/ui/manage/device/manage/ManageDeviceFragment.kt
+++ b/app/src/main/java/io/timelimit/android/ui/manage/device/manage/ManageDeviceFragment.kt
@@ -18,6 +18,7 @@ package io.timelimit.android.ui.manage.device.manage
import android.app.admin.DevicePolicyManager
import android.content.ComponentName
import android.content.Intent
+import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
@@ -186,6 +187,14 @@ class ManageDeviceFragment : Fragment(), FragmentWithCustomTitle {
}
}
+ override fun openDrawOverOtherAppsScreen() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ startActivity(
+ Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + context!!.packageName))
+ )
+ }
+ }
+
override fun manageDeviceAdmin() {
if (binding.isThisDevice == true) {
val protectionLevel = logic.platformIntegration.getCurrentProtectionLevel()
@@ -269,6 +278,7 @@ class ManageDeviceFragment : Fragment(), FragmentWithCustomTitle {
binding.usageStatsAccess = device.currentUsageStatsPermission
binding.notificationAccessPermission = device.currentNotificationAccessPermission
binding.protectionLevel = device.currentProtectionLevel
+ binding.overlayPermission = device.currentOverlayPermission
binding.didAppDowngrade = device.currentAppVersion < device.highestAppVersion
}
})
@@ -354,6 +364,7 @@ interface ManageDeviceFragmentHandlers {
fun changeNetworkTimeVerification(newValue: NetworkTime)
fun openUsageStatsSettings()
fun openNotificationAccessSettings()
+ fun openDrawOverOtherAppsScreen()
fun manageDeviceAdmin()
fun editDeviceTitle()
fun showAuthenticationScreen()
diff --git a/app/src/main/java/io/timelimit/android/ui/manage/device/manage/ManageDeviceManipulation.kt b/app/src/main/java/io/timelimit/android/ui/manage/device/manage/ManageDeviceManipulation.kt
index 97fafd2..9478685 100644
--- a/app/src/main/java/io/timelimit/android/ui/manage/device/manage/ManageDeviceManipulation.kt
+++ b/app/src/main/java/io/timelimit/android/ui/manage/device/manage/ManageDeviceManipulation.kt
@@ -41,6 +41,7 @@ object ManageDeviceManipulation {
binding.hasManipulatedDeviceAdmin = device?.manipulationOfProtectionLevel ?: false
binding.hasManipulatedUsageStatsAccess = device?.manipulationOfUsageStats ?: false
binding.hasManipulatedNotificationAccess = device?.manipulationOfNotificationAccess ?: false
+ binding.hasManipulatedOverlayPermission = device?.manipulationOfOverlayPermission ?: false
binding.hasManipulationReboot = device?.manipulationDidReboot ?: false
binding.hasHadManipulation = (device?.hadManipulation ?: false) and (! (device?.hasActiveManipulationWarning ?: false))
binding.hasAnyManipulation = device?.hasAnyManipulation ?: false
@@ -62,6 +63,7 @@ object ManageDeviceManipulation {
binding.deviceAdminDisabledCheckbox,
binding.usageAccessCheckbox,
binding.notificationAccessCheckbox,
+ binding.overlayPermissionCheckbox,
binding.rebootCheckbox,
binding.hadManipulationCheckbox
)
@@ -80,6 +82,7 @@ object ManageDeviceManipulation {
ignoreNotificationAccessManipulation = binding.notificationAccessCheckbox.isChecked && binding.hasManipulatedNotificationAccess == true,
ignoreDeviceAdminManipulationAttempt = binding.deviceAdminDisableAttemptCheckbox.isChecked && binding.hasTriedManipulatingDeviceAdmin == true,
ignoreDeviceAdminManipulation = binding.deviceAdminDisabledCheckbox.isChecked && binding.hasManipulatedDeviceAdmin == true,
+ ignoreOverlayPermissionManipulation = binding.overlayPermissionCheckbox.isChecked && binding.hasManipulatedOverlayPermission == true,
ignoreAppDowngrade = binding.appVersionCheckbox.isChecked && binding.hasManipulatedAppVersion == true,
ignoreReboot = binding.rebootCheckbox.isChecked && binding.hasManipulationReboot == true,
ignoreHadManipulation = binding.hadManipulationCheckbox.isChecked || (
diff --git a/app/src/main/java/io/timelimit/android/ui/setup/SetupDevicePermissionsFragment.kt b/app/src/main/java/io/timelimit/android/ui/setup/SetupDevicePermissionsFragment.kt
index 20cb56f..219d479 100644
--- a/app/src/main/java/io/timelimit/android/ui/setup/SetupDevicePermissionsFragment.kt
+++ b/app/src/main/java/io/timelimit/android/ui/setup/SetupDevicePermissionsFragment.kt
@@ -35,6 +35,8 @@ import io.timelimit.android.integration.platform.android.AdminReceiver
import io.timelimit.android.logic.AppLogic
import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.ui.manage.device.manage.InformAboutDeviceOwnerDialogFragment
+import android.net.Uri
+
class SetupDevicePermissionsFragment : Fragment() {
private val logic: AppLogic by lazy { DefaultAppLogic.with(context!!) }
@@ -93,6 +95,14 @@ class SetupDevicePermissionsFragment : Fragment() {
}
}
+ override fun openDrawOverOtherAppsScreen() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ startActivity(
+ Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + context!!.packageName))
+ )
+ }
+ }
+
override fun gotoNextStep() {
navigation.safeNavigate(
SetupDevicePermissionsFragmentDirections
@@ -113,6 +123,7 @@ class SetupDevicePermissionsFragment : Fragment() {
binding.notificationAccessPermission = platform.getNotificationAccessPermissionStatus()
binding.protectionLevel = platform.getCurrentProtectionLevel()
binding.usageStatsAccess = platform.getForegroundAppPermissionStatus()
+ binding.overlayPermission = platform.getOverlayPermissionStatus()
}
override fun onResume() {
@@ -126,5 +137,6 @@ interface SetupDevicePermissionsHandlers {
fun manageDeviceAdmin()
fun openUsageStatsSettings()
fun openNotificationAccessSettings()
+ fun openDrawOverOtherAppsScreen()
fun gotoNextStep()
}
diff --git a/app/src/main/res/layout/blocking_overlay.xml b/app/src/main/res/layout/blocking_overlay.xml
new file mode 100644
index 0000000..0bb1108
--- /dev/null
+++ b/app/src/main/res/layout/blocking_overlay.xml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_manage_device.xml b/app/src/main/res/layout/fragment_manage_device.xml
index 67f8c4e..3e4e55f 100644
--- a/app/src/main/res/layout/fragment_manage_device.xml
+++ b/app/src/main/res/layout/fragment_manage_device.xml
@@ -54,6 +54,10 @@
name="protectionLevel"
type="io.timelimit.android.integration.platform.ProtectionLevel" />
+
+
@@ -390,6 +394,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -229,6 +233,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -107,6 +111,13 @@
android:layout_width="match_parent"
android:layout_height="wrap_content" />
+
+
Netzwerkzeit erforderlich
nicht als aktuelles Gerät gewählt
alle Benachrichtigungen werden blockiert
+
+
+ Öffnen des Sperrbildschirms fehlgeschlagen.
+ Versuche es, den Home- oder Anwendungslisten-Button zu verwenden, um diesen Bildschirm zu verlassen.
+
diff --git a/app/src/main/res/values-de/strings-manage-device-manipulation.xml b/app/src/main/res/values-de/strings-manage-device-manipulation.xml
index 5498ce4..3eb12fa 100644
--- a/app/src/main/res/values-de/strings-manage-device-manipulation.xml
+++ b/app/src/main/res/values-de/strings-manage-device-manipulation.xml
@@ -22,6 +22,7 @@
Geräteadministrator-Deaktivierung wurde versucht
Nutzungsdatenzugriff
Benachrichtigungszugriff
+ Berechtigung zum Anzeigen über anderen Apps
ältere App-Version installiert
Gerät wurde neu gestartet
Es gab eine Manipulation, die wieder beendet wurde
diff --git a/app/src/main/res/values-de/strings-manage-device.xml b/app/src/main/res/values-de/strings-manage-device.xml
index 182e518..6c9851d 100644
--- a/app/src/main/res/values-de/strings-manage-device.xml
+++ b/app/src/main/res/values-de/strings-manage-device.xml
@@ -47,6 +47,11 @@
deren Benachrichtigungen zu sperren.
+ Über anderen Apps anzeigen
+
+ Das Anzeigen über anderen Apps kann das Sperren in einigen Fällen verbessern.
+
+
Geräte-Administrator
Die Geräte-Administrator-Berechtigung wurde nicht gewährt. TimeLimit kann jederzeit über die Systemeinstellungen beendet werden.
diff --git a/app/src/main/res/values/strings-lock.xml b/app/src/main/res/values/strings-lock.xml
index 25e9ec2..d4a5304 100644
--- a/app/src/main/res/values/strings-lock.xml
+++ b/app/src/main/res/values/strings-lock.xml
@@ -87,4 +87,9 @@
missing network time
device must be the current device
all notifications are blocked
+
+
+ Failed to open the lock screen.
+ You can try using the home or overview button to leave this screen.
+
diff --git a/app/src/main/res/values/strings-manage-device-manipulation.xml b/app/src/main/res/values/strings-manage-device-manipulation.xml
index b1a8a88..2b8d368 100644
--- a/app/src/main/res/values/strings-manage-device-manipulation.xml
+++ b/app/src/main/res/values/strings-manage-device-manipulation.xml
@@ -22,6 +22,7 @@
disabling devcie admin was attempted
usage stats access
notification access
+ draw over other Apps permission
older App version installed
device was rebooted
there was a manipulation which was stopped again
diff --git a/app/src/main/res/values/strings-manage-device.xml b/app/src/main/res/values/strings-manage-device.xml
index 1aaf939..68568ed 100644
--- a/app/src/main/res/values/strings-manage-device.xml
+++ b/app/src/main/res/values/strings-manage-device.xml
@@ -47,6 +47,11 @@
Notifications and their contents are not saved.
+ Draw over other Apps
+
+ Enabling drawing on top of other Apps can improve blocking in some cases.
+
+
Device admin
The device admin permission was not granted.