diff --git a/app/schemas/io.timelimit.android.data.RoomDatabase/42.json b/app/schemas/io.timelimit.android.data.RoomDatabase/42.json new file mode 100644 index 0000000..b5c6b45 --- /dev/null +++ b/app/schemas/io.timelimit.android.data.RoomDatabase/42.json @@ -0,0 +1,1270 @@ +{ + "formatVersion": 1, + "database": { + "version": 42, + "identityHash": "dd1ce868d6b12f7939d7cd738d6c4715", + "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": "obsoleteBlockedTimes", + "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, `manipulation_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": "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 + }, + { + "fieldPath": "manipulationFlags", + "columnName": "manipulation_flags", + "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" + ], + "orders": [], + "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" + ], + "orders": [], + "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": "appSpecifierString", + "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" + ], + "orders": [], + "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" + ], + "orders": [], + "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, `tasks_version` TEXT NOT NULL DEFAULT '', `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, `disable_limits_until` INTEGER NOT NULL, `flags` INTEGER NOT NULL DEFAULT 0, `block_notification_delay` INTEGER NOT NULL DEFAULT 0, 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": "tasksVersion", + "columnName": "tasks_version", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "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 + }, + { + "fieldPath": "disableLimitsUntil", + "columnName": "disable_limits_until", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "blockNotificationDelay", + "columnName": "block_notification_delay", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "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, `per_day` 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 + }, + { + "fieldPath": "perDay", + "columnName": "per_day", + "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" + ], + "orders": [], + "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" + ], + "orders": [], + "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" + ], + "orders": [], + "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, `pre_block_duration` INTEGER NOT NULL DEFAULT 0, 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 + }, + { + "fieldPath": "preBlockDuration", + "columnName": "pre_block_duration", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "user_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "user_limit_login_category_index_category_id", + "unique": false, + "columnNames": [ + "category_id" + ], + "orders": [], + "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" + ] + } + ] + }, + { + "tableName": "child_task", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`task_id` TEXT NOT NULL, `category_id` TEXT NOT NULL, `task_title` TEXT NOT NULL, `extra_time_duration` INTEGER NOT NULL, `pending_request` INTEGER NOT NULL, `last_grant_timestamp` INTEGER NOT NULL, PRIMARY KEY(`task_id`), FOREIGN KEY(`category_id`) REFERENCES `category`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "taskId", + "columnName": "task_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "categoryId", + "columnName": "category_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "taskTitle", + "columnName": "task_title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "extraTimeDuration", + "columnName": "extra_time_duration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pendingRequest", + "columnName": "pending_request", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastGrantTimestamp", + "columnName": "last_grant_timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "task_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "category", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "category_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "category_time_warning", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`category_id` TEXT NOT NULL, `minutes` INTEGER NOT NULL, PRIMARY KEY(`category_id`, `minutes`), FOREIGN KEY(`category_id`) REFERENCES `category`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "categoryId", + "columnName": "category_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "minutes", + "columnName": "minutes", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "category_id", + "minutes" + ], + "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, 'dd1ce868d6b12f7939d7cd738d6c4715')" + ] + } +} \ No newline at end of file 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 be0d621..19dec21 100644 --- a/app/src/main/java/io/timelimit/android/data/Migrations.kt +++ b/app/src/main/java/io/timelimit/android/data/Migrations.kt @@ -293,6 +293,12 @@ object DatabaseMigrations { } } + private val MIGRATE_TO_V42 = object: Migration(41, 42) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE device ADD COLUMN manipulation_flags INTEGER NOT NULL DEFAULT 0") + } + } + val ALL = arrayOf( MIGRATE_TO_V2, MIGRATE_TO_V3, @@ -333,6 +339,7 @@ object DatabaseMigrations { MIGRATE_TO_V38, MIGRATE_TO_V39, MIGRATE_TO_V40, - MIGRATE_TO_V41 + MIGRATE_TO_V41, + MIGRATE_TO_V42 ) } 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 4c6dd13..17f9ae4 100644 --- a/app/src/main/java/io/timelimit/android/data/RoomDatabase.kt +++ b/app/src/main/java/io/timelimit/android/data/RoomDatabase.kt @@ -53,7 +53,7 @@ import java.util.concurrent.TimeUnit CategoryNetworkId::class, ChildTask::class, CategoryTimeWarning::class -], version = 41) +], version = 42) abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database { companion object { private val lock = Object() 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 ff887dc..ed135db 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 @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 Jonas Lochmann + * TimeLimit Copyright 2019 - 2022 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 @@ -92,7 +92,9 @@ data class Device( @ColumnInfo(name = "enable_activity_level_blocking") val enableActivityLevelBlocking: Boolean, @ColumnInfo(name = "q_or_later") - val qOrLater: Boolean + val qOrLater: Boolean, + @ColumnInfo(name = "manipulation_flags") + val manipulationFlags: Long ): JsonSerializable { companion object { private const val ID = "id" @@ -126,6 +128,7 @@ data class Device( private const val WAS_ACCESSIBILITY_SERVICE_ENABLED = "wase" private const val ENABLE_ACTIVITY_LEVEL_BLOCKING = "ealb" private const val Q_OR_LATER = "qol" + private const val MANIPULATION_FLAGS = "mf" fun parse(reader: JsonReader): Device { var id: String? = null @@ -159,6 +162,7 @@ data class Device( var wasAccessibilityServiceEnabled = false var enableActivityLevelBlocking = false var qOrLater = false + var manipulationFlags = 0L reader.beginObject() @@ -195,6 +199,7 @@ data class Device( WAS_ACCESSIBILITY_SERVICE_ENABLED -> wasAccessibilityServiceEnabled = reader.nextBoolean() ENABLE_ACTIVITY_LEVEL_BLOCKING -> enableActivityLevelBlocking = reader.nextBoolean() Q_OR_LATER -> qOrLater = reader.nextBoolean() + MANIPULATION_FLAGS -> manipulationFlags = reader.nextLong() else -> reader.skipValue() } } @@ -232,7 +237,8 @@ data class Device( accessibilityServiceEnabled = accessibilityServiceEnabled, wasAccessibilityServiceEnabled = wasAccessibilityServiceEnabled, enableActivityLevelBlocking = enableActivityLevelBlocking, - qOrLater = qOrLater + qOrLater = qOrLater, + manipulationFlags = manipulationFlags ) } } @@ -303,6 +309,7 @@ data class Device( writer.name(WAS_ACCESSIBILITY_SERVICE_ENABLED).value(wasAccessibilityServiceEnabled) writer.name(ENABLE_ACTIVITY_LEVEL_BLOCKING).value(enableActivityLevelBlocking) writer.name(Q_OR_LATER).value(qOrLater) + writer.name(MANIPULATION_FLAGS).value(manipulationFlags) writer.endObject() } @@ -331,7 +338,7 @@ data class Device( manipulationOfAccessibilityService @Transient - val hasAnyManipulation = hasActiveManipulationWarning || hadManipulation + val hasAnyManipulation = hasActiveManipulationWarning || hadManipulation || manipulationFlags != 0L @Transient val missingPermissionAtQOrLater = qOrLater && @@ -381,4 +388,8 @@ object HadManipulationFlag { const val APP_VERSION = 1L shl 3 const val OVERLAY_PERMISSION = 1L shl 4 const val ACCESSIBILITY_SERVICE = 1L shl 5 +} + +object ManipulationFlag { + const val USED_FGS_KILLER = 1L shl 0 } \ No newline at end of file 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 ae249e6..4fcf58f 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 @@ -89,7 +89,7 @@ abstract class PlatformIntegration( confirmationLevel: SystemPermissionConfirmationLevel = SystemPermissionConfirmationLevel.None ): Boolean - abstract fun getExitLog(): List + abstract fun getExitLog(length: Int): List var installedAppsChangeListener: Runnable? = null var systemClockChangeListener: Runnable? = null 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 a3047be..88451c1 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 @@ -815,9 +815,9 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio } } - override fun getExitLog(): List { + override fun getExitLog(length: Int): List { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - activityManager.getHistoricalProcessExitReasons(context.packageName, 0, 0) + activityManager.getHistoricalProcessExitReasons(context.packageName, 0, length) .map { ExitLogItem.fromApplicationExitInfo(it) } } else emptyList() } 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 4fc14a6..0221871 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 @@ -190,5 +190,5 @@ class DummyIntegration( confirmationLevel: SystemPermissionConfirmationLevel ): Boolean = false - override fun getExitLog(): List = emptyList() + override fun getExitLog(length: Int): List = emptyList() } 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 3e58fc7..34dfed9 100644 --- a/app/src/main/java/io/timelimit/android/logic/AppSetupLogic.kt +++ b/app/src/main/java/io/timelimit/android/logic/AppSetupLogic.kt @@ -109,7 +109,8 @@ class AppSetupLogic(private val appLogic: AppLogic) { accessibilityServiceEnabled = false, wasAccessibilityServiceEnabled = false, enableActivityLevelBlocking = false, - qOrLater = AndroidVersion.qOrLater + qOrLater = AndroidVersion.qOrLater, + manipulationFlags = 0 ) 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 226a362..935c15f 100644 --- a/app/src/main/java/io/timelimit/android/logic/BackgroundTaskLogic.kt +++ b/app/src/main/java/io/timelimit/android/logic/BackgroundTaskLogic.kt @@ -26,16 +26,14 @@ import io.timelimit.android.coroutines.runAsync import io.timelimit.android.coroutines.runAsyncExpectForever import io.timelimit.android.data.backup.DatabaseBackup import io.timelimit.android.data.model.ExperimentalFlags +import io.timelimit.android.data.model.ManipulationFlag import io.timelimit.android.data.model.UserType import io.timelimit.android.data.model.derived.UserRelatedData import io.timelimit.android.date.DateInTimezone import io.timelimit.android.date.getMinuteOfWeek import io.timelimit.android.extensions.MinuteOfDay import io.timelimit.android.extensions.nextBlockedMinuteOfWeek -import io.timelimit.android.integration.platform.AppStatusMessage -import io.timelimit.android.integration.platform.ForegroundApp -import io.timelimit.android.integration.platform.NetworkId -import io.timelimit.android.integration.platform.ProtectionLevel +import io.timelimit.android.integration.platform.* import io.timelimit.android.integration.platform.android.AccessibilityService import io.timelimit.android.livedata.* import io.timelimit.android.logic.blockingreason.AppBaseHandling @@ -76,6 +74,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) { runAsyncExpectForever { syncDeviceStatusLoop() } runAsyncExpectForever { backupDatabaseLoop() } runAsyncExpectForever { annoyUserOnManipulationLoop() } + runAsync { checkForceKilled() } runAsync { // this is effective after an reboot @@ -924,6 +923,33 @@ class BackgroundTaskLogic(val appLogic: AppLogic) { } } + private suspend fun checkForceKilled() { + appLogic.platformIntegration.getExitLog(1).singleOrNull()?.let { item -> + if ( + item.reason == ExitReason.UserRequest && + item.description != null && + item.description.startsWith("fully stop ") && + item.description.endsWith("by user request") + ) { + appLogic.isInitialized.waitUntilValueMatches { it == true } + + try { + ApplyActionUtil.applyAppLogicAction( + action = UpdateDeviceStatusAction.empty.copy( + addedManipulationFlags = ManipulationFlag.USED_FGS_KILLER + ), + appLogic = appLogic, + ignoreIfDeviceIsNotConfigured = true + ) + } catch (ex: Exception) { + if (BuildConfig.DEBUG) { + Log.w(LOG_TAG, "could not save a forced kill notification", ex) + } + } + } + } + } + private val syncDeviceStatusLock = Mutex() fun reportDeviceReboot() { 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 70873fe..ae54fdf 100644 --- a/app/src/main/java/io/timelimit/android/sync/ApplyServerDataStatus.kt +++ b/app/src/main/java/io/timelimit/android/sync/ApplyServerDataStatus.kt @@ -188,7 +188,8 @@ object ApplyServerDataStatus { accessibilityServiceEnabled = newDevice.accessibilityServiceEnabled, wasAccessibilityServiceEnabled = newDevice.wasAccessibilityServiceEnabled, enableActivityLevelBlocking = newDevice.enableActivityLevelBlocking, - qOrLater = newDevice.qOrLater + qOrLater = newDevice.qOrLater, + manipulationFlags = newDevice.manipulationFlags )) } else { // eventually update old entry @@ -222,7 +223,8 @@ object ApplyServerDataStatus { accessibilityServiceEnabled = newDevice.accessibilityServiceEnabled, wasAccessibilityServiceEnabled = newDevice.wasAccessibilityServiceEnabled, enableActivityLevelBlocking = newDevice.enableActivityLevelBlocking, - qOrLater = newDevice.qOrLater + qOrLater = newDevice.qOrLater, + manipulationFlags = newDevice.manipulationFlags ) 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 fe873a4..9fd493b 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 @@ -1081,7 +1081,8 @@ data class UpdateDeviceStatusAction( val newAccessibilityServiceEnabled: Boolean?, val newAppVersion: Int?, val didReboot: Boolean, - val isQOrLaterNow: Boolean + val isQOrLaterNow: Boolean, + val addedManipulationFlags: Long ): AppLogicAction() { companion object { const val TYPE_VALUE = "UPDATE_DEVICE_STATUS" @@ -1093,6 +1094,7 @@ data class UpdateDeviceStatusAction( private const val NEW_APP_VERSION = "appVersion" private const val DID_REBOOT = "didReboot" private const val IS_Q_OR_LATER_NOW = "isQOrLaterNow" + private const val ADDED_MANIPULATION_FLAGS = "addedManipulationFlags" val empty = UpdateDeviceStatusAction( newProtectionLevel = null, @@ -1102,7 +1104,8 @@ data class UpdateDeviceStatusAction( newAccessibilityServiceEnabled = null, newAppVersion = null, didReboot = false, - isQOrLaterNow = false + isQOrLaterNow = false, + addedManipulationFlags = 0L ) } @@ -1159,6 +1162,10 @@ data class UpdateDeviceStatusAction( writer.name(IS_Q_OR_LATER_NOW).value(true) } + if (addedManipulationFlags != 0L) { + writer.name(ADDED_MANIPULATION_FLAGS).value(addedManipulationFlags) + } + writer.endObject() } } @@ -1174,7 +1181,8 @@ data class IgnoreManipulationAction( val ignoreAccessibilityServiceManipulation: Boolean, val ignoreReboot: Boolean, val ignoreHadManipulation: Boolean, - val ignoreHadManipulationFlags: Long + val ignoreHadManipulationFlags: Long, + val ignoreManipulationFlags: Long ): ParentAction() { companion object { const val TYPE_VALUE = "IGNORE_MANIPULATION" @@ -1189,6 +1197,7 @@ data class IgnoreManipulationAction( private const val IGNORE_HAD_MANIPULATION = "hadManipulation" private const val IGNORE_REBOOT = "reboot" private const val IGNORE_HAD_MANIPULATION_FLAGS = "ignoreHadManipulationFlags" + private const val IGNORE_MANIPULATION_FLAGS = "ignoreManipulationFlags" } init { @@ -1204,7 +1213,8 @@ data class IgnoreManipulationAction( (!ignoreAccessibilityServiceManipulation) && (!ignoreReboot) && (!ignoreHadManipulation) && - (ignoreHadManipulationFlags == 0L) + (ignoreHadManipulationFlags == 0L) && + (ignoreManipulationFlags == 0L) override fun serialize(writer: JsonWriter) { writer.beginObject() @@ -1222,6 +1232,8 @@ data class IgnoreManipulationAction( writer.name(IGNORE_REBOOT).value(ignoreReboot) writer.name(IGNORE_HAD_MANIPULATION_FLAGS).value(ignoreHadManipulationFlags) + if (ignoreManipulationFlags != 0L) writer.name(IGNORE_MANIPULATION_FLAGS).value(ignoreManipulationFlags) + writer.endObject() } } 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 43d90b3..e3231b0 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 @@ -305,6 +305,10 @@ object LocalDatabaseAppLogicActionDispatcher { device = device.copy(qOrLater = true) } + if (action.addedManipulationFlags != 0L) { + device = device.copy(manipulationFlags = device.manipulationFlags or action.addedManipulationFlags) + } + database.device().updateDeviceEntry(device) null 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 2a11948..d48733f 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 @@ -465,6 +465,12 @@ object LocalDatabaseParentActionDispatcher { } } + if (action.ignoreManipulationFlags != 0L) { + deviceEntry = deviceEntry.copy( + manipulationFlags = deviceEntry.manipulationFlags and (action.ignoreManipulationFlags.inv()) + ) + } + database.device().updateDeviceEntry(deviceEntry) } is SetKeepSignedInAction -> { 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 fa2295e..5969633 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 @@ -298,7 +298,8 @@ data class ServerDeviceData( val accessibilityServiceEnabled: Boolean, val wasAccessibilityServiceEnabled: Boolean, val enableActivityLevelBlocking: Boolean, - val qOrLater: Boolean + val qOrLater: Boolean, + val manipulationFlags: Long ) { companion object { private const val DEVICE_ID = "deviceId" @@ -331,6 +332,7 @@ data class ServerDeviceData( private const val WAS_ACCESSIBILITY_SERVICE_ENABLED = "wasAsEnabled" private const val ENABLE_ACTIVITY_LEVEL_BLOCKING = "activityLevelBlocking" private const val Q_OR_LATER = "qOrLater" + private const val MANIPULATION_FLAGS = "mFlags" fun parse(reader: JsonReader): ServerDeviceData { var deviceId: String? = null @@ -363,6 +365,7 @@ data class ServerDeviceData( var wasAccessibilityServiceEnabled: Boolean? = null var enableActivityLevelBlocking = false var qOrLater = false + var manipulationFlags = 0L reader.beginObject() while (reader.hasNext()) { @@ -397,6 +400,7 @@ data class ServerDeviceData( WAS_ACCESSIBILITY_SERVICE_ENABLED -> wasAccessibilityServiceEnabled = reader.nextBoolean() ENABLE_ACTIVITY_LEVEL_BLOCKING -> enableActivityLevelBlocking = reader.nextBoolean() Q_OR_LATER -> qOrLater = reader.nextBoolean() + MANIPULATION_FLAGS -> manipulationFlags = reader.nextLong() else -> reader.skipValue() } } @@ -432,7 +436,8 @@ data class ServerDeviceData( accessibilityServiceEnabled = accessibilityServiceEnabled!!, wasAccessibilityServiceEnabled = wasAccessibilityServiceEnabled!!, enableActivityLevelBlocking = enableActivityLevelBlocking, - qOrLater = qOrLater + qOrLater = qOrLater, + manipulationFlags = manipulationFlags ) } diff --git a/app/src/main/java/io/timelimit/android/ui/diagnose/exitreason/DiagnoseExitReasonFragment.kt b/app/src/main/java/io/timelimit/android/ui/diagnose/exitreason/DiagnoseExitReasonFragment.kt index d8ef0ce..066bca5 100644 --- a/app/src/main/java/io/timelimit/android/ui/diagnose/exitreason/DiagnoseExitReasonFragment.kt +++ b/app/src/main/java/io/timelimit/android/ui/diagnose/exitreason/DiagnoseExitReasonFragment.kt @@ -31,7 +31,7 @@ import io.timelimit.android.ui.main.FragmentWithCustomTitle class DiagnoseExitReasonFragment: Fragment(), FragmentWithCustomTitle { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val binding = DiagnoseExitReasonFragmentBinding.inflate(inflater, container, false) - val data = DefaultAppLogic.with(requireContext()).platformIntegration.getExitLog() + val data = DefaultAppLogic.with(requireContext()).platformIntegration.getExitLog(0) val recycler = binding.recycler val adapter = DiagnoseExitReasonAdapter() 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 e18db4b..0beedba 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 @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 Jonas Lochmann + * TimeLimit Copyright 2019 - 2022 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 @@ -25,6 +25,7 @@ import com.google.android.material.snackbar.Snackbar import io.timelimit.android.R import io.timelimit.android.data.model.Device import io.timelimit.android.data.model.HadManipulationFlag +import io.timelimit.android.data.model.ManipulationFlag import io.timelimit.android.databinding.ManageDeviceManipulationViewBinding import io.timelimit.android.livedata.map import io.timelimit.android.sync.actions.IgnoreManipulationAction @@ -62,7 +63,7 @@ object ManageDeviceManipulation { entries.forEach { warning -> container.addView( createCheckbox().apply { - setText(ManipulationWarningTypeLabel.getLabel(warning)) + setText(warning.labelResourceId) isChecked = selection.contains(warning) setOnCheckedChangeListener { _, newIsChecked -> @@ -128,66 +129,80 @@ data class ManipulationWarnings(val current: List, val fun isFlagSet(flag: Long) = (manipulationFlags and flag) == flag if (device.manipulationTriedDisablingDeviceAdmin) { - current.add(ManipulationWarningType.TriedDisablingDeviceAdmin) + current.add(ManipulationWarningType.Classic(ClassicManipulationWarningType.TriedDisablingDeviceAdmin)) } if (device.manipulationOfAppVersion) { - current.add(ManipulationWarningType.AppDowngrade) + current.add(ManipulationWarningType.Classic(ClassicManipulationWarningType.AppDowngrade)) } if (isFlagSet(HadManipulationFlag.APP_VERSION)) { - past.add(ManipulationWarningType.AppDowngrade) + past.add(ManipulationWarningType.Classic(ClassicManipulationWarningType.AppDowngrade)) } if (device.manipulationOfProtectionLevel) { - current.add(ManipulationWarningType.DeviceAdmin) + current.add(ManipulationWarningType.Classic(ClassicManipulationWarningType.DeviceAdmin)) } if (isFlagSet(HadManipulationFlag.PROTECTION_LEVEL)) { - past.add(ManipulationWarningType.DeviceAdmin) + past.add(ManipulationWarningType.Classic(ClassicManipulationWarningType.DeviceAdmin)) } if (device.manipulationOfUsageStats) { - current.add(ManipulationWarningType.UsageStats) + current.add(ManipulationWarningType.Classic(ClassicManipulationWarningType.UsageStats)) } if (isFlagSet(HadManipulationFlag.USAGE_STATS_ACCESS)) { - past.add(ManipulationWarningType.UsageStats) + past.add(ManipulationWarningType.Classic(ClassicManipulationWarningType.UsageStats)) } if (device.manipulationOfNotificationAccess) { - current.add(ManipulationWarningType.NotificationAccess) + current.add(ManipulationWarningType.Classic(ClassicManipulationWarningType.NotificationAccess)) } if (isFlagSet(HadManipulationFlag.NOTIFICATION_ACCESS)) { - past.add(ManipulationWarningType.NotificationAccess) + past.add(ManipulationWarningType.Classic(ClassicManipulationWarningType.NotificationAccess)) } if (device.manipulationOfOverlayPermission) { - current.add(ManipulationWarningType.OverlayPermission) + current.add(ManipulationWarningType.Classic(ClassicManipulationWarningType.OverlayPermission)) } if (isFlagSet(HadManipulationFlag.OVERLAY_PERMISSION)) { - past.add(ManipulationWarningType.OverlayPermission) + past.add(ManipulationWarningType.Classic(ClassicManipulationWarningType.OverlayPermission)) } if (device.wasAccessibilityServiceEnabled) { if (!device.accessibilityServiceEnabled) { - current.add(ManipulationWarningType.AccessibilityService) + current.add(ManipulationWarningType.Classic(ClassicManipulationWarningType.AccessibilityService)) } } if (isFlagSet(HadManipulationFlag.ACCESSIBILITY_SERVICE)) { - past.add(ManipulationWarningType.AccessibilityService) + past.add(ManipulationWarningType.Classic(ClassicManipulationWarningType.AccessibilityService)) } if (device.manipulationDidReboot) { - current.add(ManipulationWarningType.DidReboot) + current.add(ManipulationWarningType.Classic(ClassicManipulationWarningType.DidReboot)) } if (device.hadManipulation) { if (past.isEmpty()) { - past.add(ManipulationWarningType.Unknown) + past.add(ManipulationWarningType.Classic(ClassicManipulationWarningType.Unknown)) + } + } + + if (device.manipulationFlags != 0L) { + var remainingFlags = device.manipulationFlags + + if (remainingFlags and ManipulationFlag.USED_FGS_KILLER == ManipulationFlag.USED_FGS_KILLER) { + past.add(ManipulationWarningType.Flag(ManipulationFlag.USED_FGS_KILLER, R.string.manage_device_manipulation_fgs_killer)) + + remainingFlags = remainingFlags.and(ManipulationFlag.USED_FGS_KILLER.inv()) + } + + if (remainingFlags != 0L) { + past.add(ManipulationWarningType.Flag(remainingFlags, R.string.manage_device_manipulation_unknown)) } } return ManipulationWarnings( - current = current, - past = past + current = current, + past = past ) } } @@ -197,38 +212,69 @@ data class ManipulationWarnings(val current: List, val fun buildIgnoreAction(deviceId: String): IgnoreManipulationAction { var ignoreHadManipulationFlags = 0L + var ignoreManipulationFlags = 0L past.forEach { type -> - ignoreHadManipulationFlags = ignoreHadManipulationFlags or when(type) { - ManipulationWarningType.TriedDisablingDeviceAdmin -> throw IllegalArgumentException() - ManipulationWarningType.AppDowngrade -> HadManipulationFlag.APP_VERSION - ManipulationWarningType.DeviceAdmin -> HadManipulationFlag.PROTECTION_LEVEL - ManipulationWarningType.UsageStats -> HadManipulationFlag.USAGE_STATS_ACCESS - ManipulationWarningType.NotificationAccess -> HadManipulationFlag.NOTIFICATION_ACCESS - ManipulationWarningType.OverlayPermission -> HadManipulationFlag.OVERLAY_PERMISSION - ManipulationWarningType.AccessibilityService -> HadManipulationFlag.ACCESSIBILITY_SERVICE - ManipulationWarningType.DidReboot -> throw IllegalArgumentException() - ManipulationWarningType.Unknown -> 0L // handled at an other location - } + when (type) { + is ManipulationWarningType.Classic -> { + ignoreHadManipulationFlags = ignoreHadManipulationFlags or when(type.type) { + ClassicManipulationWarningType.TriedDisablingDeviceAdmin -> throw IllegalArgumentException() + ClassicManipulationWarningType.AppDowngrade -> HadManipulationFlag.APP_VERSION + ClassicManipulationWarningType.DeviceAdmin -> HadManipulationFlag.PROTECTION_LEVEL + ClassicManipulationWarningType.UsageStats -> HadManipulationFlag.USAGE_STATS_ACCESS + ClassicManipulationWarningType.NotificationAccess -> HadManipulationFlag.NOTIFICATION_ACCESS + ClassicManipulationWarningType.OverlayPermission -> HadManipulationFlag.OVERLAY_PERMISSION + ClassicManipulationWarningType.AccessibilityService -> HadManipulationFlag.ACCESSIBILITY_SERVICE + ClassicManipulationWarningType.DidReboot -> throw IllegalArgumentException() + ClassicManipulationWarningType.Unknown -> 0L // handled at an other location + } + } + is ManipulationWarningType.Flag -> { + ignoreManipulationFlags = ignoreManipulationFlags or type.mask + } + }.let {/* require handling all cases */} } + val currentClassic = current.filterIsInstance().map { it.type } + return IgnoreManipulationAction( - deviceId = deviceId, - ignoreUsageStatsAccessManipulation = current.contains(ManipulationWarningType.UsageStats), - ignoreNotificationAccessManipulation = current.contains(ManipulationWarningType.NotificationAccess), - ignoreDeviceAdminManipulationAttempt = current.contains(ManipulationWarningType.TriedDisablingDeviceAdmin), - ignoreDeviceAdminManipulation = current.contains(ManipulationWarningType.DeviceAdmin), - ignoreOverlayPermissionManipulation = current.contains(ManipulationWarningType.OverlayPermission), - ignoreAccessibilityServiceManipulation = current.contains(ManipulationWarningType.AccessibilityService), - ignoreAppDowngrade = current.contains(ManipulationWarningType.AppDowngrade), - ignoreReboot = current.contains(ManipulationWarningType.DidReboot), - ignoreHadManipulation = past.contains(ManipulationWarningType.Unknown), - ignoreHadManipulationFlags = ignoreHadManipulationFlags + deviceId = deviceId, + ignoreUsageStatsAccessManipulation = currentClassic.contains(ClassicManipulationWarningType.UsageStats), + ignoreNotificationAccessManipulation = currentClassic.contains(ClassicManipulationWarningType.NotificationAccess), + ignoreDeviceAdminManipulationAttempt = currentClassic.contains(ClassicManipulationWarningType.TriedDisablingDeviceAdmin), + ignoreDeviceAdminManipulation = currentClassic.contains(ClassicManipulationWarningType.DeviceAdmin), + ignoreOverlayPermissionManipulation = currentClassic.contains(ClassicManipulationWarningType.OverlayPermission), + ignoreAccessibilityServiceManipulation = currentClassic.contains(ClassicManipulationWarningType.AccessibilityService), + ignoreAppDowngrade = currentClassic.contains(ClassicManipulationWarningType.AppDowngrade), + ignoreReboot = currentClassic.contains(ClassicManipulationWarningType.DidReboot), + ignoreHadManipulation = currentClassic.contains(ClassicManipulationWarningType.Unknown), + ignoreHadManipulationFlags = ignoreHadManipulationFlags, + ignoreManipulationFlags = ignoreManipulationFlags ) } } -enum class ManipulationWarningType { +sealed class ManipulationWarningType { + abstract val labelResourceId: Int + + data class Classic(val type: ClassicManipulationWarningType): ManipulationWarningType() { + override val labelResourceId = when (type) { + ClassicManipulationWarningType.TriedDisablingDeviceAdmin -> R.string.manage_device_manipulation_device_admin_disable_attempt + ClassicManipulationWarningType.AppDowngrade -> R.string.manage_device_manipulation_app_version + ClassicManipulationWarningType.DeviceAdmin -> R.string.manage_device_manipulation_device_admin_disabled + ClassicManipulationWarningType.UsageStats -> R.string.manage_device_manipulation_usage_stats_access + ClassicManipulationWarningType.NotificationAccess -> R.string.manage_device_manipulation_notification_access + ClassicManipulationWarningType.OverlayPermission -> R.string.manage_device_manipulation_overlay_permission + ClassicManipulationWarningType.AccessibilityService -> R.string.manage_device_manipulation_accessibility_service + ClassicManipulationWarningType.DidReboot -> R.string.manage_device_manipulation_reboot + ClassicManipulationWarningType.Unknown -> R.string.manage_device_manipulation_existed + } + } + + data class Flag(val mask: Long, override val labelResourceId: Int): ManipulationWarningType() +} + +enum class ClassicManipulationWarningType { TriedDisablingDeviceAdmin, AppDowngrade, DeviceAdmin, @@ -240,20 +286,6 @@ enum class ManipulationWarningType { Unknown } -object ManipulationWarningTypeLabel { - fun getLabel(type: ManipulationWarningType) = when (type) { - ManipulationWarningType.TriedDisablingDeviceAdmin -> R.string.manage_device_manipulation_device_admin_disable_attempt - ManipulationWarningType.AppDowngrade -> R.string.manage_device_manipulation_app_version - ManipulationWarningType.DeviceAdmin -> R.string.manage_device_manipulation_device_admin_disabled - ManipulationWarningType.UsageStats -> R.string.manage_device_manipulation_usage_stats_access - ManipulationWarningType.NotificationAccess -> R.string.manage_device_manipulation_notification_access - ManipulationWarningType.OverlayPermission -> R.string.manage_device_manipulation_overlay_permission - ManipulationWarningType.AccessibilityService -> R.string.manage_device_manipulation_accessibility_service - ManipulationWarningType.DidReboot -> R.string.manage_device_manipulation_reboot - ManipulationWarningType.Unknown -> R.string.manage_device_manipulation_existed - } -} - class ManageDeviceManipulationStatus { val selectedCurrent = mutableListOf() val selectedPast = mutableListOf() diff --git a/app/src/main/java/io/timelimit/android/ui/manipulation/AnnoyActivity.kt b/app/src/main/java/io/timelimit/android/ui/manipulation/AnnoyActivity.kt index 33d9074..d81acdf 100644 --- a/app/src/main/java/io/timelimit/android/ui/manipulation/AnnoyActivity.kt +++ b/app/src/main/java/io/timelimit/android/ui/manipulation/AnnoyActivity.kt @@ -35,7 +35,6 @@ import io.timelimit.android.ui.backdoor.BackdoorDialogFragment import io.timelimit.android.ui.login.NewLoginFragment import io.timelimit.android.ui.main.ActivityViewModel import io.timelimit.android.ui.main.ActivityViewModelHolder -import io.timelimit.android.ui.manage.device.manage.ManipulationWarningTypeLabel import io.timelimit.android.ui.manage.device.manage.ManipulationWarnings import io.timelimit.android.util.TimeTextUtil @@ -95,7 +94,7 @@ class AnnoyActivity : AppCompatActivity(), ActivityViewModelHolder { logic.deviceEntry.map { val reasonItems = (it?.let { ManipulationWarnings.getFromDevice(it) } ?: ManipulationWarnings.empty) .current - .map { getString(ManipulationWarningTypeLabel.getLabel(it)) } + .map { getString(it.labelResourceId) } if (reasonItems.isEmpty()) { null diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 6ebc0ad..702eb0c 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -862,6 +862,8 @@ ältere App-Version installiert Gerät wurde neu gestartet Es gab eine Manipulation, die wieder beendet wurde + unbekannte Manipulation + mittels Task-Manager beendet Warnungen bestätigen und ausblenden Sie müssen zuerst Warnungen wählen, die Sie ignorieren möchten diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b9af35e..c6e2350 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -907,6 +907,8 @@ older App version installed device was rebooted there was a manipulation which was stopped again + unknown Manipulation + killed using task manager Confirm and hide warnings You have to select warnings to ignore first