diff --git a/app/schemas/io.timelimit.android.data.RoomDatabase/23.json b/app/schemas/io.timelimit.android.data.RoomDatabase/23.json new file mode 100644 index 0000000..57dddff --- /dev/null +++ b/app/schemas/io.timelimit.android.data.RoomDatabase/23.json @@ -0,0 +1,816 @@ +{ + "formatVersion": 1, + "database": { + "version": 23, + "identityHash": "1d39f67895d7506bbfac727ac075efb3", + "entities": [ + { + "tableName": "user", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `password` TEXT NOT NULL, `second_password_salt` TEXT NOT NULL, `type` TEXT NOT NULL, `timezone` TEXT NOT NULL, `disable_limits_until` INTEGER NOT NULL, `mail` TEXT NOT NULL, `current_device` TEXT NOT NULL, `category_for_not_assigned_apps` TEXT NOT NULL, `relax_primary_device` INTEGER NOT NULL, `mail_notification_flags` INTEGER NOT NULL, `blocked_times` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "secondPasswordSalt", + "columnName": "second_password_salt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timeZone", + "columnName": "timezone", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "disableLimitsUntil", + "columnName": "disable_limits_until", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mail", + "columnName": "mail", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currentDevice", + "columnName": "current_device", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "categoryForNotAssignedApps", + "columnName": "category_for_not_assigned_apps", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "relaxPrimaryDevice", + "columnName": "relax_primary_device", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mailNotificationFlags", + "columnName": "mail_notification_flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "blockedTimes", + "columnName": "blocked_times", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "device", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `model` TEXT NOT NULL, `added_at` INTEGER NOT NULL, `current_user_id` TEXT NOT NULL, `apps_version` TEXT NOT NULL, `network_time` TEXT NOT NULL, `current_protection_level` TEXT NOT NULL, `highest_permission_level` TEXT NOT NULL, `current_usage_stats_permission` TEXT NOT NULL, `highest_usage_stats_permission` TEXT NOT NULL, `current_notification_access_permission` TEXT NOT NULL, `highest_notification_access_permission` TEXT NOT NULL, `current_app_version` INTEGER NOT NULL, `highest_app_version` INTEGER NOT NULL, `tried_disabling_device_admin` INTEGER NOT NULL, `did_reboot` INTEGER NOT NULL, `had_manipulation` INTEGER NOT NULL, `had_manipulation_flags` INTEGER NOT NULL, `did_report_uninstall` INTEGER NOT NULL, `is_user_kept_signed_in` INTEGER NOT NULL, `show_device_connected` INTEGER NOT NULL, `default_user` TEXT NOT NULL, `default_user_timeout` INTEGER NOT NULL, `consider_reboot_manipulation` INTEGER NOT NULL, `current_overlay_permission` TEXT NOT NULL, `highest_overlay_permission` TEXT NOT NULL, `current_accessibility_service_permission` INTEGER NOT NULL, `was_accessibility_service_permission` INTEGER NOT NULL, `enable_activity_level_blocking` INTEGER NOT NULL, `q_or_later` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "model", + "columnName": "model", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "addedAt", + "columnName": "added_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentUserId", + "columnName": "current_user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "installedAppsVersion", + "columnName": "apps_version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "networkTime", + "columnName": "network_time", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currentProtectionLevel", + "columnName": "current_protection_level", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "highestProtectionLevel", + "columnName": "highest_permission_level", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currentUsageStatsPermission", + "columnName": "current_usage_stats_permission", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "highestUsageStatsPermission", + "columnName": "highest_usage_stats_permission", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currentNotificationAccessPermission", + "columnName": "current_notification_access_permission", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "highestNotificationAccessPermission", + "columnName": "highest_notification_access_permission", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currentAppVersion", + "columnName": "current_app_version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "highestAppVersion", + "columnName": "highest_app_version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "manipulationTriedDisablingDeviceAdmin", + "columnName": "tried_disabling_device_admin", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "manipulationDidReboot", + "columnName": "did_reboot", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hadManipulation", + "columnName": "had_manipulation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hadManipulationFlags", + "columnName": "had_manipulation_flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "didReportUninstall", + "columnName": "did_report_uninstall", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isUserKeptSignedIn", + "columnName": "is_user_kept_signed_in", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "showDeviceConnected", + "columnName": "show_device_connected", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultUser", + "columnName": "default_user", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "defaultUserTimeout", + "columnName": "default_user_timeout", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "considerRebootManipulation", + "columnName": "consider_reboot_manipulation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentOverlayPermission", + "columnName": "current_overlay_permission", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "highestOverlayPermission", + "columnName": "highest_overlay_permission", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessibilityServiceEnabled", + "columnName": "current_accessibility_service_permission", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wasAccessibilityServiceEnabled", + "columnName": "was_accessibility_service_permission", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enableActivityLevelBlocking", + "columnName": "enable_activity_level_blocking", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "qOrLater", + "columnName": "q_or_later", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "app", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`device_id` TEXT NOT NULL, `package_name` TEXT NOT NULL, `title` TEXT NOT NULL, `launchable` INTEGER NOT NULL, `recommendation` TEXT NOT NULL, PRIMARY KEY(`device_id`, `package_name`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "device_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "package_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isLaunchable", + "columnName": "launchable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recommendation", + "columnName": "recommendation", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "device_id", + "package_name" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_app_device_id", + "unique": false, + "columnNames": [ + "device_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_app_device_id` ON `${TABLE_NAME}` (`device_id`)" + }, + { + "name": "index_app_package_name", + "unique": false, + "columnNames": [ + "package_name" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_app_package_name` ON `${TABLE_NAME}` (`package_name`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "category_app", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`category_id` TEXT NOT NULL, `package_name` TEXT NOT NULL, PRIMARY KEY(`category_id`, `package_name`))", + "fields": [ + { + "fieldPath": "categoryId", + "columnName": "category_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "package_name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "category_id", + "package_name" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_category_app_category_id", + "unique": false, + "columnNames": [ + "category_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_category_app_category_id` ON `${TABLE_NAME}` (`category_id`)" + }, + { + "name": "index_category_app_package_name", + "unique": false, + "columnNames": [ + "package_name" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_category_app_package_name` ON `${TABLE_NAME}` (`package_name`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "category", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `child_id` TEXT NOT NULL, `title` TEXT NOT NULL, `blocked_times` TEXT NOT NULL, `extra_time` INTEGER NOT NULL, `temporarily_blocked` INTEGER NOT NULL, `base_version` TEXT NOT NULL, `apps_version` TEXT NOT NULL, `rules_version` TEXT NOT NULL, `usedtimes_version` TEXT NOT NULL, `parent_category_id` TEXT NOT NULL, `block_all_notifications` INTEGER NOT NULL, `time_warnings` INTEGER NOT NULL, `min_battery_charging` INTEGER NOT NULL, `min_battery_mobile` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "childId", + "columnName": "child_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "blockedMinutesInWeek", + "columnName": "blocked_times", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "extraTimeInMillis", + "columnName": "extra_time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "temporarilyBlocked", + "columnName": "temporarily_blocked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseVersion", + "columnName": "base_version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "assignedAppsVersion", + "columnName": "apps_version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timeLimitRulesVersion", + "columnName": "rules_version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usedTimesVersion", + "columnName": "usedtimes_version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentCategoryId", + "columnName": "parent_category_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "blockAllNotifications", + "columnName": "block_all_notifications", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timeWarnings", + "columnName": "time_warnings", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "minBatteryLevelWhileCharging", + "columnName": "min_battery_charging", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "minBatteryLevelMobile", + "columnName": "min_battery_mobile", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "used_time", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`day_of_epoch` INTEGER NOT NULL, `used_time` INTEGER NOT NULL, `category_id` TEXT NOT NULL, PRIMARY KEY(`category_id`, `day_of_epoch`))", + "fields": [ + { + "fieldPath": "dayOfEpoch", + "columnName": "day_of_epoch", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "usedMillis", + "columnName": "used_time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "categoryId", + "columnName": "category_id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "category_id", + "day_of_epoch" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "time_limit_rule", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `category_id` TEXT NOT NULL, `apply_to_extra_time_usage` INTEGER NOT NULL, `day_mask` INTEGER NOT NULL, `max_time` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "categoryId", + "columnName": "category_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "applyToExtraTimeUsage", + "columnName": "apply_to_extra_time_usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dayMask", + "columnName": "day_mask", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "maximumTimeInMillis", + "columnName": "max_time", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "config", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "temporarily_allowed_app", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`device_id` TEXT NOT NULL, `package_name` TEXT NOT NULL, PRIMARY KEY(`device_id`, `package_name`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "device_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "package_name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "device_id", + "package_name" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "pending_sync_action", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sequence_number` INTEGER NOT NULL, `action` TEXT NOT NULL, `integrity` TEXT NOT NULL, `scheduled_for_upload` INTEGER NOT NULL, `type` TEXT NOT NULL, `user_id` TEXT NOT NULL, PRIMARY KEY(`sequence_number`))", + "fields": [ + { + "fieldPath": "sequenceNumber", + "columnName": "sequence_number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "encodedAction", + "columnName": "action", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "integrity", + "columnName": "integrity", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scheduledForUpload", + "columnName": "scheduled_for_upload", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sequence_number" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_pending_sync_action_scheduled_for_upload", + "unique": false, + "columnNames": [ + "scheduled_for_upload" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_pending_sync_action_scheduled_for_upload` ON `${TABLE_NAME}` (`scheduled_for_upload`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "app_activity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`device_id` TEXT NOT NULL, `app_package_name` TEXT NOT NULL, `activity_class_name` TEXT NOT NULL, `activity_title` TEXT NOT NULL, PRIMARY KEY(`device_id`, `app_package_name`, `activity_class_name`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "device_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "appPackageName", + "columnName": "app_package_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activityClassName", + "columnName": "activity_class_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "activity_title", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "device_id", + "app_package_name", + "activity_class_name" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "notification", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL, `id` TEXT NOT NULL, `first_notify_time` INTEGER NOT NULL, `dismissed` INTEGER NOT NULL, PRIMARY KEY(`type`, `id`))", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "firstNotifyTime", + "columnName": "first_notify_time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDismissed", + "columnName": "dismissed", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "type", + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "allowed_contact", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `phone` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phone", + "columnName": "phone", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1d39f67895d7506bbfac727ac075efb3')" + ] + } +} \ 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 c2d3ee7..ac60003 100644 --- a/app/src/main/java/io/timelimit/android/data/Migrations.kt +++ b/app/src/main/java/io/timelimit/android/data/Migrations.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 Jonas Lochmann + * TimeLimit Copyright 2019 - 2020 Jonas Lochmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -159,4 +159,11 @@ object DatabaseMigrations { database.execSQL("ALTER TABLE `user` ADD COLUMN `blocked_times` TEXT NOT NULL DEFAULT \"\"") } } + + val MIGRATE_TO_V23 = object: Migration(22, 23) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE `category` ADD COLUMN `min_battery_charging` INTEGER NOT NULL DEFAULT 0") + database.execSQL("ALTER TABLE `category` ADD COLUMN `min_battery_mobile` INTEGER NOT NULL DEFAULT 0") + } + } } 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 c9b0ad6..744b561 100644 --- a/app/src/main/java/io/timelimit/android/data/RoomDatabase.kt +++ b/app/src/main/java/io/timelimit/android/data/RoomDatabase.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 Jonas Lochmann + * TimeLimit Copyright 2019 - 2020 Jonas Lochmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -35,7 +35,7 @@ import io.timelimit.android.data.model.* AppActivity::class, Notification::class, AllowedContact::class -], version = 22) +], version = 23) abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database { companion object { private val lock = Object() @@ -91,7 +91,8 @@ abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database DatabaseMigrations.MIGRATE_TO_V19, DatabaseMigrations.MIGRATE_TO_V20, DatabaseMigrations.MIGRATE_TO_V21, - DatabaseMigrations.MIGRATE_TO_V22 + DatabaseMigrations.MIGRATE_TO_V22, + DatabaseMigrations.MIGRATE_TO_V23 ) .build() } diff --git a/app/src/main/java/io/timelimit/android/data/model/Category.kt b/app/src/main/java/io/timelimit/android/data/model/Category.kt index 7257f0b..b0365eb 100644 --- a/app/src/main/java/io/timelimit/android/data/model/Category.kt +++ b/app/src/main/java/io/timelimit/android/data/model/Category.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 Jonas Lochmann + * TimeLimit Copyright 2019 - 2020 Jonas Lochmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -57,7 +57,11 @@ data class Category( @ColumnInfo(name = "block_all_notifications") val blockAllNotifications: Boolean, @ColumnInfo(name = "time_warnings") - val timeWarnings: Int + val timeWarnings: Int, + @ColumnInfo(name = "min_battery_charging") + val minBatteryLevelWhileCharging: Int, + @ColumnInfo(name = "min_battery_mobile") + val minBatteryLevelMobile: Int ): JsonSerializable { companion object { const val MINUTES_PER_DAY = 60 * 24 @@ -76,6 +80,8 @@ data class Category( private const val PARENT_CATEGORY_ID = "pc" private const val BlOCK_ALL_NOTIFICATIONS = "ban" private const val TIME_WARNINGS = "tw" + private const val MIN_BATTERY_CHARGING = "minBatteryCharging" + private const val MIN_BATTERY_MOBILE = "minBatteryMobile" fun parse(reader: JsonReader): Category { var id: String? = null @@ -92,6 +98,8 @@ data class Category( var parentCategoryId = "" var blockAllNotifications = false var timeWarnings = 0 + var minBatteryCharging = 0 + var minBatteryMobile = 0 reader.beginObject() @@ -110,6 +118,8 @@ data class Category( PARENT_CATEGORY_ID -> parentCategoryId = reader.nextString() BlOCK_ALL_NOTIFICATIONS -> blockAllNotifications = reader.nextBoolean() TIME_WARNINGS -> timeWarnings = reader.nextInt() + MIN_BATTERY_CHARGING -> minBatteryCharging = reader.nextInt() + MIN_BATTERY_MOBILE -> minBatteryMobile = reader.nextInt() else -> reader.skipValue() } } @@ -129,7 +139,9 @@ data class Category( usedTimesVersion = usedTimesVersion!!, parentCategoryId = parentCategoryId, blockAllNotifications = blockAllNotifications, - timeWarnings = timeWarnings + timeWarnings = timeWarnings, + minBatteryLevelWhileCharging = minBatteryCharging, + minBatteryLevelMobile = minBatteryMobile ) } } @@ -145,6 +157,14 @@ data class Category( if (title.isEmpty()) { throw IllegalArgumentException() } + + if (minBatteryLevelMobile < 0 || minBatteryLevelWhileCharging < 0) { + throw IllegalArgumentException() + } + + if (minBatteryLevelMobile > 100 || minBatteryLevelWhileCharging > 100) { + throw IllegalArgumentException() + } } override fun serialize(writer: JsonWriter) { @@ -163,6 +183,8 @@ data class Category( writer.name(PARENT_CATEGORY_ID).value(parentCategoryId) writer.name(BlOCK_ALL_NOTIFICATIONS).value(blockAllNotifications) writer.name(TIME_WARNINGS).value(timeWarnings) + writer.name(MIN_BATTERY_CHARGING).value(minBatteryLevelWhileCharging) + writer.name(MIN_BATTERY_MOBILE).value(minBatteryLevelMobile) writer.endObject() } 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 f724624..e151695 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 @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 Jonas Lochmann + * TimeLimit Copyright 2019 - 2020 Jonas Lochmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -17,6 +17,7 @@ package io.timelimit.android.integration.platform import android.graphics.drawable.Drawable import android.os.Parcelable +import androidx.lifecycle.LiveData import androidx.room.TypeConverter import io.timelimit.android.data.model.App import io.timelimit.android.data.model.AppActivity @@ -62,6 +63,9 @@ abstract class PlatformIntegration( // returns true on success abstract fun setLockTaskPackages(packageNames: List): Boolean + abstract fun getBatteryStatus(): BatteryStatus + abstract fun getBatteryStatusLive(): LiveData + var installedAppsChangeListener: Runnable? = null } @@ -193,3 +197,18 @@ data class AppStatusMessage( val subtext: String? = null, val showSwitchToDefaultUserOption: Boolean = false ): Parcelable + +data class BatteryStatus( + val charging: Boolean, + val level: Int +) { + companion object { + val dummy = BatteryStatus(false, 0) + } + + init { + if (level < 0 || level > 100) { + throw IllegalArgumentException() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/integration/platform/android/AndroidIntegration.kt b/app/src/main/java/io/timelimit/android/integration/platform/android/AndroidIntegration.kt index 655773a..a2c3b68 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 @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 Jonas Lochmann + * TimeLimit Copyright 2019 - 2020 Jonas Lochmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -38,6 +38,7 @@ import android.view.KeyEvent import android.widget.Toast import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat +import androidx.lifecycle.LiveData import io.timelimit.android.BuildConfig import io.timelimit.android.R import io.timelimit.android.coroutines.runAsyncExpectForever @@ -79,6 +80,7 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio 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) + private val battery = BatteryStatusUtil(context) init { AppsChangeListener.registerBroadcastReceiver(this.context, object : BroadcastReceiver() { @@ -442,4 +444,7 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio false } } + + override fun getBatteryStatus(): BatteryStatus = battery.status.value!! + override fun getBatteryStatusLive(): LiveData = battery.status } diff --git a/app/src/main/java/io/timelimit/android/integration/platform/android/BatteryStatusUtil.kt b/app/src/main/java/io/timelimit/android/integration/platform/android/BatteryStatusUtil.kt new file mode 100644 index 0000000..f45dedf --- /dev/null +++ b/app/src/main/java/io/timelimit/android/integration/platform/android/BatteryStatusUtil.kt @@ -0,0 +1,55 @@ +/* + * TimeLimit Copyright 2019 - 2020 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package io.timelimit.android.integration.platform.android + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.BatteryManager +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import io.timelimit.android.integration.platform.BatteryStatus + +class BatteryStatusUtil(context: Context) { + private val statusInternal = MutableLiveData().apply { value = BatteryStatus.dummy } + val status: LiveData = statusInternal + + init { + context.applicationContext.registerReceiver(object: BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val charging = run { + val status: Int = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1) + + status == BatteryManager.BATTERY_STATUS_CHARGING + || status == BatteryManager.BATTERY_STATUS_FULL + } + + val level = run { + val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) + val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1) + + (level * 100 / scale).coerceIn(0, 100) + } + + statusInternal.value = BatteryStatus( + charging = charging, + level = level + ) + } + }, IntentFilter(Intent.ACTION_BATTERY_CHANGED)) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/integration/platform/android/NotificationListener.kt b/app/src/main/java/io/timelimit/android/integration/platform/android/NotificationListener.kt index 5fef2d3..fadade8 100644 --- a/app/src/main/java/io/timelimit/android/integration/platform/android/NotificationListener.kt +++ b/app/src/main/java/io/timelimit/android/integration/platform/android/NotificationListener.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 Jonas Lochmann + * TimeLimit Copyright 2019 - 2020 Jonas Lochmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -112,6 +112,7 @@ class NotificationListener: NotificationListenerService() { BlockingReason.MissingNetworkTime -> getString(R.string.lock_reason_short_missing_network_time) BlockingReason.RequiresCurrentDevice -> getString(R.string.lock_reason_short_requires_current_device) BlockingReason.NotificationsAreBlocked -> getString(R.string.lock_reason_short_notification_blocking) + BlockingReason.BatteryLimit -> getString(R.string.lock_reason_short_battery_limit) BlockingReason.None -> throw IllegalStateException() } ) 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 118dd4c..ebff7cd 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 @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 Jonas Lochmann + * TimeLimit Copyright 2019 - 2020 Jonas Lochmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,6 +16,8 @@ package io.timelimit.android.integration.platform.dummy import android.graphics.drawable.Drawable +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import io.timelimit.android.data.model.App import io.timelimit.android.data.model.AppActivity import io.timelimit.android.integration.platform.* @@ -33,6 +35,7 @@ class DummyIntegration( var lastAppStatusMessage: AppStatusMessage? = null var launchLockScreenForPackage: String? = null var showRevokeTemporarilyAllowedNotification = false + val batteryStatus = MutableLiveData().apply { value = BatteryStatus(true, 100) } override fun getLocalApps(deviceId: String): Collection { return localApps.map{ it.copy(deviceId = deviceId) } @@ -159,4 +162,7 @@ class DummyIntegration( override fun setEnableSystemLockdown(enableLockdown: Boolean) = false override fun setLockTaskPackages(packageNames: List) = false + + override fun getBatteryStatus(): BatteryStatus = batteryStatus.value!! + override fun getBatteryStatusLive(): LiveData = batteryStatus } 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 485e2b1..4de1221 100644 --- a/app/src/main/java/io/timelimit/android/logic/AppSetupLogic.kt +++ b/app/src/main/java/io/timelimit/android/logic/AppSetupLogic.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 Jonas Lochmann + * TimeLimit Copyright 2019 - 2020 Jonas Lochmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -179,7 +179,9 @@ class AppSetupLogic(private val appLogic: AppLogic) { usedTimesVersion = "", parentCategoryId = "", blockAllNotifications = false, - timeWarnings = 0 + timeWarnings = 0, + minBatteryLevelWhileCharging = 0, + minBatteryLevelMobile = 0 )) appLogic.database.category().addCategory(Category( @@ -195,7 +197,9 @@ class AppSetupLogic(private val appLogic: AppLogic) { usedTimesVersion = "", parentCategoryId = "", blockAllNotifications = false, - timeWarnings = 0 + timeWarnings = 0, + minBatteryLevelWhileCharging = 0, + minBatteryLevelMobile = 0 )) // add default allowed apps 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 0a83b86..bcc19b4 100644 --- a/app/src/main/java/io/timelimit/android/logic/BackgroundTaskLogic.kt +++ b/app/src/main/java/io/timelimit/android/logic/BackgroundTaskLogic.kt @@ -36,6 +36,7 @@ import io.timelimit.android.integration.platform.ProtectionLevel import io.timelimit.android.integration.platform.android.AccessibilityService import io.timelimit.android.integration.platform.android.AndroidIntegrationApps import io.timelimit.android.livedata.* +import io.timelimit.android.logic.extension.isCategoryAllowed import io.timelimit.android.sync.actions.UpdateDeviceStatusAction import io.timelimit.android.sync.actions.apply.ApplyActionUtil import io.timelimit.android.ui.IsAppInForeground @@ -261,6 +262,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) { // get the current status val isScreenOn = appLogic.platformIntegration.isScreenOn() + val batteryStatus = appLogic.platformIntegration.getBatteryStatus() appLogic.defaultUserLogic.reportScreenOn(isScreenOn) @@ -335,6 +337,10 @@ class BackgroundTaskLogic(val appLogic: AppLogic) { if (category == null) { usedTimeUpdateHelper?.commit(appLogic) + openLockscreen(foregroundAppPackageName, foregroundAppActivityName) + } else if ((!batteryStatus.isCategoryAllowed(category)) || (!batteryStatus.isCategoryAllowed(parentCategory))) { + usedTimeUpdateHelper?.commit(appLogic) + openLockscreen(foregroundAppPackageName, foregroundAppActivityName) } else if (category.temporarilyBlocked or (parentCategory?.temporarilyBlocked == true)) { usedTimeUpdateHelper?.commit(appLogic) diff --git a/app/src/main/java/io/timelimit/android/logic/BlockingReason.kt b/app/src/main/java/io/timelimit/android/logic/BlockingReason.kt index 1f2dda5..1057545 100644 --- a/app/src/main/java/io/timelimit/android/logic/BlockingReason.kt +++ b/app/src/main/java/io/timelimit/android/logic/BlockingReason.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 Jonas Lochmann + * TimeLimit Copyright 2019 - 2020 Jonas Lochmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -26,6 +26,7 @@ import io.timelimit.android.date.getMinuteOfWeek import io.timelimit.android.integration.platform.android.AndroidIntegrationApps import io.timelimit.android.integration.time.TimeApi import io.timelimit.android.livedata.* +import io.timelimit.android.logic.extension.isCategoryAllowed import java.util.* enum class BlockingReason { @@ -37,7 +38,8 @@ enum class BlockingReason { TimeOverExtraTimeCanBeUsedLater, MissingNetworkTime, RequiresCurrentDevice, - NotificationsAreBlocked + NotificationsAreBlocked, + BatteryLimit } enum class BlockingLevel { @@ -74,6 +76,7 @@ class BlockingReasonUtil(private val appLogic: AppLogic) { } private val enableActivityLevelFiltering = appLogic.deviceEntry.map { it?.enableActivityLevelBlocking ?: false } + private val batteryLevel = appLogic.platformIntegration.getBatteryStatusLive() fun getBlockingReason(packageName: String, activityName: String?): LiveData { // check precondition that the app is running @@ -211,7 +214,7 @@ class BlockingReasonUtil(private val appLogic: AppLogic) { else liveDataFromValue(false) - val nextLevel = getBlockingReasonStep4Point7(category, child, timeZone, isParentCategory, blockingLevel) + val nextLevel = getBlockingReasonStep4Point6(category, child, timeZone, isParentCategory, blockingLevel) return shouldBlockNotifications.switchMap { blockNotifications -> nextLevel.map { blockingReason -> @@ -229,6 +232,24 @@ class BlockingReasonUtil(private val appLogic: AppLogic) { } } + private fun getBlockingReasonStep4Point6(category: Category, child: User, timeZone: TimeZone, isParentCategory: Boolean, blockingLevel: BlockingLevel): LiveData { + val next = getBlockingReasonStep4Point7(category, child, timeZone, isParentCategory, blockingLevel) + + return if (category.minBatteryLevelWhileCharging == 0 && category.minBatteryLevelMobile == 0) { + next + } else { + val batteryLevelOk = batteryLevel.map { it.isCategoryAllowed(category) }.ignoreUnchanged() + + batteryLevelOk.switchMap { ok -> + if (ok) { + next + } else { + liveDataFromValue(BlockingReason.BatteryLimit) + } + } + } + } + private fun getBlockingReasonStep4Point7(category: Category, child: User, timeZone: TimeZone, isParentCategory: Boolean, blockingLevel: BlockingLevel): LiveData { if (BuildConfig.DEBUG) { Log.d(LOG_TAG, "step 4.7") @@ -264,7 +285,7 @@ class BlockingReasonUtil(private val appLogic: AppLogic) { if (parentCategory == null) { liveDataFromValue(BlockingReason.None) } else { - getBlockingReasonStep4Point7(parentCategory, child, timeZone, true, blockingLevel) + getBlockingReasonStep4Point6(parentCategory, child, timeZone, true, blockingLevel) } } } else { diff --git a/app/src/main/java/io/timelimit/android/logic/CategoriesBlockingReasons.kt b/app/src/main/java/io/timelimit/android/logic/CategoriesBlockingReasons.kt index be7d5bf..51729fb 100644 --- a/app/src/main/java/io/timelimit/android/logic/CategoriesBlockingReasons.kt +++ b/app/src/main/java/io/timelimit/android/logic/CategoriesBlockingReasons.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 Jonas Lochmann + * TimeLimit Copyright 2019 - 2020 Jonas Lochmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -23,6 +23,7 @@ import io.timelimit.android.BuildConfig import io.timelimit.android.data.model.* import io.timelimit.android.date.DateInTimezone import io.timelimit.android.livedata.* +import io.timelimit.android.logic.extension.isCategoryAllowed import java.util.* class CategoriesBlockingReasonUtil(private val appLogic: AppLogic) { @@ -32,6 +33,7 @@ class CategoriesBlockingReasonUtil(private val appLogic: AppLogic) { private val blockingReason = BlockingReasonUtil(appLogic) private val temporarilyTrustedTimeInMillis = blockingReason.getTemporarilyTrustedTimeInMillis() + private val batteryLevel = appLogic.platformIntegration.getBatteryStatusLive() // NOTE: this ignores the current device rule fun getCategoryBlockingReasons( @@ -129,25 +131,31 @@ class CategoriesBlockingReasonUtil(private val appLogic: AppLogic) { areLimitsTemporarilyDisabled: LiveData ): LiveData { return category.switchMap { category -> - if (category.temporarilyBlocked) { - liveDataFromValue(BlockingReason.TemporarilyBlocked) - } else { - areLimitsTemporarilyDisabled.switchMap { areLimitsTemporarilyDisabled -> - if (areLimitsTemporarilyDisabled) { - liveDataFromValue(BlockingReason.None) - } else { - checkCategoryBlockedTimeAreas( - temporarilyTrustedMinuteOfWeek = temporarilyTrustedMinuteOfWeek, - blockedMinutesInWeek = category.blockedMinutesInWeek.dataNotToModify - ).switchMap { blockedTimeAreasReason -> - if (blockedTimeAreasReason != BlockingReason.None) { - liveDataFromValue(blockedTimeAreasReason) - } else { - checkCategoryTimeLimitRules( - temporarilyTrustedDate = temporarilyTrustedDate, - category = category, - rules = appLogic.database.timeLimitRules().getTimeLimitRulesByCategory(category.id) - ) + val batteryOk = batteryLevel.map { it.isCategoryAllowed(category) }.ignoreUnchanged() + + batteryOk.switchMap { ok -> + if (!ok) { + liveDataFromValue(BlockingReason.BatteryLimit) + } else if (category.temporarilyBlocked) { + liveDataFromValue(BlockingReason.TemporarilyBlocked) + } else { + areLimitsTemporarilyDisabled.switchMap { areLimitsTemporarilyDisabled -> + if (areLimitsTemporarilyDisabled) { + liveDataFromValue(BlockingReason.None) + } else { + checkCategoryBlockedTimeAreas( + temporarilyTrustedMinuteOfWeek = temporarilyTrustedMinuteOfWeek, + blockedMinutesInWeek = category.blockedMinutesInWeek.dataNotToModify + ).switchMap { blockedTimeAreasReason -> + if (blockedTimeAreasReason != BlockingReason.None) { + liveDataFromValue(blockedTimeAreasReason) + } else { + checkCategoryTimeLimitRules( + temporarilyTrustedDate = temporarilyTrustedDate, + category = category, + rules = appLogic.database.timeLimitRules().getTimeLimitRulesByCategory(category.id) + ) + } } } } diff --git a/app/src/main/java/io/timelimit/android/logic/extension/BatteryStatus.kt b/app/src/main/java/io/timelimit/android/logic/extension/BatteryStatus.kt new file mode 100644 index 0000000..a305fae --- /dev/null +++ b/app/src/main/java/io/timelimit/android/logic/extension/BatteryStatus.kt @@ -0,0 +1,29 @@ +/* + * 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.logic.extension + +import io.timelimit.android.data.model.Category +import io.timelimit.android.integration.platform.BatteryStatus + +fun BatteryStatus.isCategoryAllowed(category: Category?): Boolean { + return if (category == null) { + true + } else if (this.charging) { + this.level >= category.minBatteryLevelWhileCharging + } else { + this.level >= category.minBatteryLevelMobile + } +} \ No newline at end of file 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 c64adfc..d313572 100644 --- a/app/src/main/java/io/timelimit/android/sync/ApplyServerDataStatus.kt +++ b/app/src/main/java/io/timelimit/android/sync/ApplyServerDataStatus.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 Jonas Lochmann + * TimeLimit Copyright 2019 - 2020 Jonas Lochmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -309,7 +309,9 @@ object ApplyServerDataStatus { timeLimitRulesVersion = "", usedTimesVersion = "", parentCategoryId = newCategory.parentCategoryId, - timeWarnings = newCategory.timeWarnings + timeWarnings = newCategory.timeWarnings, + minBatteryLevelMobile = newCategory.minBatteryLevelMobile, + minBatteryLevelWhileCharging = newCategory.minBatteryLevelCharging )) } else { val updatedCategory = oldCategory.copy( @@ -321,7 +323,9 @@ object ApplyServerDataStatus { blockAllNotifications = newCategory.blockAllNotifications, baseVersion = newCategory.baseDataVersion, parentCategoryId = newCategory.parentCategoryId, - timeWarnings = newCategory.timeWarnings + timeWarnings = newCategory.timeWarnings, + minBatteryLevelMobile = newCategory.minBatteryLevelMobile, + minBatteryLevelWhileCharging = newCategory.minBatteryLevelCharging ) if (updatedCategory != oldCategory) { 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 528330b..36b5565 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 @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 Jonas Lochmann + * TimeLimit Copyright 2019 - 2020 Jonas Lochmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -677,7 +677,47 @@ data class SetParentCategory(val categoryId: String, val parentCategory: String) writer.endObject() } } +data class UpdateCategoryBatteryLimit(val categoryId: String, val chargingLimit: Int?, val mobileLimit: Int?): ParentAction() { + companion object { + private const val TYPE_VALUE = "UPDATE_CATEGORY_BATTERY_LIMIT" + private const val CATEGORY_ID = "categoryId" + private const val CHARGE_LIMIT = "chargeLimit" + private const val MOBILE_LIMIT = "mobileLimit" + } + init { + IdGenerator.assertIdValid(categoryId) + + if (chargingLimit != null) { + if (chargingLimit < 0 || chargingLimit > 100) { + throw IllegalArgumentException() + } + } + + if (mobileLimit != null) { + if (mobileLimit < 0 || mobileLimit > 100) { + throw IllegalArgumentException() + } + } + } + + override fun serialize(writer: JsonWriter) { + writer.beginObject() + + writer.name(TYPE).value(TYPE_VALUE) + writer.name(CATEGORY_ID).value(categoryId) + + if (chargingLimit != null) { + writer.name(CHARGE_LIMIT).value(chargingLimit) + } + + if (mobileLimit != null) { + writer.name(MOBILE_LIMIT).value(mobileLimit) + } + + writer.endObject() + } +} // DeviceDao data class UpdateDeviceStatusAction( diff --git a/app/src/main/java/io/timelimit/android/sync/actions/Parser.kt b/app/src/main/java/io/timelimit/android/sync/actions/Parser.kt index 3804f24..cf71d0a 100644 --- a/app/src/main/java/io/timelimit/android/sync/actions/Parser.kt +++ b/app/src/main/java/io/timelimit/android/sync/actions/Parser.kt @@ -65,6 +65,7 @@ object ActionParser { // UpdateCategoryBlockAllNotificationsAction // UpdateEnableActivityLevelBlocking // UpdateCategoryTimeWarningsAction + // UpdateCategoryBatteryLimit else -> throw IllegalStateException() } } diff --git a/app/src/main/java/io/timelimit/android/sync/actions/dispatch/ParentAction.kt b/app/src/main/java/io/timelimit/android/sync/actions/dispatch/ParentAction.kt index ee095f8..231dcd0 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 @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 Jonas Lochmann + * TimeLimit Copyright 2019 - 2020 Jonas Lochmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -80,7 +80,9 @@ object LocalDatabaseParentActionDispatcher { usedTimesVersion = "", parentCategoryId = "", blockAllNotifications = false, - timeWarnings = 0 + timeWarnings = 0, + minBatteryLevelWhileCharging = 0, + minBatteryLevelMobile = 0 )) } is DeleteCategoryAction -> { @@ -539,6 +541,17 @@ object LocalDatabaseParentActionDispatcher { ) ) } + is UpdateCategoryBatteryLimit -> { + val categoryEntry = database.category().getCategoryByIdSync(action.categoryId) + ?: throw IllegalArgumentException("can not update battery limit for a category which does not exist") + + database.category().updateCategorySync( + categoryEntry.copy( + minBatteryLevelWhileCharging = action.chargingLimit ?: categoryEntry.minBatteryLevelWhileCharging, + minBatteryLevelMobile = action.mobileLimit ?: categoryEntry.minBatteryLevelMobile + ) + ) + } }.let { } database.setTransactionSuccessful() 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 e3a7ca7..2a90f28 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 @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 Jonas Lochmann + * TimeLimit Copyright 2019 - 2020 Jonas Lochmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -339,7 +339,9 @@ data class ServerUpdatedCategoryBaseData( val baseDataVersion: String, val parentCategoryId: String, val blockAllNotifications: Boolean, - val timeWarnings: Int + val timeWarnings: Int, + val minBatteryLevelCharging: Int, + val minBatteryLevelMobile: Int ) { companion object { private const val CATEGORY_ID = "categoryId" @@ -352,6 +354,8 @@ data class ServerUpdatedCategoryBaseData( private const val PARENT_CATEGORY_ID = "parentCategoryId" private const val BLOCK_ALL_NOTIFICATIONS = "blockAllNotifications" private const val TIME_WARNINGS = "timeWarnings" + private const val MIN_BATTERY_LEVEL_MOBILE = "mblMobile" + private const val MIN_BATTERY_LEVEL_CHARGING = "mblCharging" fun parse(reader: JsonReader): ServerUpdatedCategoryBaseData { var categoryId: String? = null @@ -365,6 +369,8 @@ data class ServerUpdatedCategoryBaseData( // added later -> default values var blockAllNotifications = false var timeWarnings = 0 + var minBatteryLevelCharging = 0 + var minBatteryLevelMobile = 0 reader.beginObject() while (reader.hasNext()) { @@ -379,6 +385,8 @@ data class ServerUpdatedCategoryBaseData( PARENT_CATEGORY_ID -> parentCategoryId = reader.nextString() BLOCK_ALL_NOTIFICATIONS -> blockAllNotifications = reader.nextBoolean() TIME_WARNINGS -> timeWarnings = reader.nextInt() + MIN_BATTERY_LEVEL_CHARGING -> minBatteryLevelCharging = reader.nextInt() + MIN_BATTERY_LEVEL_MOBILE -> minBatteryLevelMobile = reader.nextInt() else -> reader.skipValue() } } @@ -394,7 +402,9 @@ data class ServerUpdatedCategoryBaseData( baseDataVersion = baseDataVersion!!, parentCategoryId = parentCategoryId!!, blockAllNotifications = blockAllNotifications, - timeWarnings = timeWarnings + timeWarnings = timeWarnings, + minBatteryLevelCharging = minBatteryLevelCharging, + minBatteryLevelMobile = minBatteryLevelMobile ) } diff --git a/app/src/main/java/io/timelimit/android/ui/diagnose/DiagnoseBatteryFragment.kt b/app/src/main/java/io/timelimit/android/ui/diagnose/DiagnoseBatteryFragment.kt new file mode 100644 index 0000000..ba02aae --- /dev/null +++ b/app/src/main/java/io/timelimit/android/ui/diagnose/DiagnoseBatteryFragment.kt @@ -0,0 +1,39 @@ +/* + * TimeLimit Copyright 2019 - 2020 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package io.timelimit.android.ui.diagnose + +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.lifecycle.Observer +import io.timelimit.android.databinding.DiagnoseBatteryFragmentBinding +import io.timelimit.android.logic.DefaultAppLogic + +class DiagnoseBatteryFragment : Fragment() { + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val binding = DiagnoseBatteryFragmentBinding.inflate(inflater, container, false) + val logic = DefaultAppLogic.with(context!!) + + logic.platformIntegration.getBatteryStatusLive().observe(this, Observer { + binding.charging = it.charging + binding.level = it.level + }) + + return binding.root + } +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/diagnose/DiagnoseMainFragment.kt b/app/src/main/java/io/timelimit/android/ui/diagnose/DiagnoseMainFragment.kt index 7a6159a..06e6533 100644 --- a/app/src/main/java/io/timelimit/android/ui/diagnose/DiagnoseMainFragment.kt +++ b/app/src/main/java/io/timelimit/android/ui/diagnose/DiagnoseMainFragment.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 Jonas Lochmann + * TimeLimit Copyright 2019 - 2020 Jonas Lochmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -54,6 +54,13 @@ class DiagnoseMainFragment : Fragment() { ) } + binding.diagnoseBatteryButton.setOnClickListener { + navigation.safeNavigate( + DiagnoseMainFragmentDirections.actionDiagnoseMainFragmentToDiagnoseBatteryFragment(), + R.id.diagnoseMainFragment + ) + } + binding.diagnoseFgaButton.setOnClickListener { navigation.safeNavigate( DiagnoseMainFragmentDirections.actionDiagnoseMainFragmentToDiagnoseForegroundAppFragment(), diff --git a/app/src/main/java/io/timelimit/android/ui/manage/category/settings/CategoryBatteryLimitView.kt b/app/src/main/java/io/timelimit/android/ui/manage/category/settings/CategoryBatteryLimitView.kt new file mode 100644 index 0000000..6c96458 --- /dev/null +++ b/app/src/main/java/io/timelimit/android/ui/manage/category/settings/CategoryBatteryLimitView.kt @@ -0,0 +1,82 @@ +/* + * TimeLimit Copyright 2019 - 2020 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package io.timelimit.android.ui.manage.category.settings + +import android.widget.SeekBar +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import com.google.android.material.snackbar.Snackbar +import io.timelimit.android.R +import io.timelimit.android.data.model.Category +import io.timelimit.android.databinding.CategoryBatteryLimitViewBinding +import io.timelimit.android.livedata.ignoreUnchanged +import io.timelimit.android.livedata.map +import io.timelimit.android.sync.actions.UpdateCategoryBatteryLimit +import io.timelimit.android.ui.main.ActivityViewModel + +object CategoryBatteryLimitView { + fun bind( + binding: CategoryBatteryLimitViewBinding, + lifecycleOwner: LifecycleOwner, + category: LiveData, + auth: ActivityViewModel, + categoryId: String + ) { + binding.seekbarCharging.setOnSeekBarChangeListener(object: SeekBar.OnSeekBarChangeListener { + override fun onStartTrackingTouch(p0: SeekBar?) = Unit + override fun onStopTrackingTouch(p0: SeekBar?) = Unit + + override fun onProgressChanged(p0: SeekBar?, p1: Int, p2: Boolean) { + binding.minLevelCharging = p1 * 10 + } + }) + + binding.seekbarMobile.setOnSeekBarChangeListener(object: SeekBar.OnSeekBarChangeListener { + override fun onStartTrackingTouch(p0: SeekBar?) = Unit + override fun onStopTrackingTouch(p0: SeekBar?) = Unit + + override fun onProgressChanged(p0: SeekBar?, p1: Int, p2: Boolean) { + binding.minLevelMobile = p1 * 10 + } + }) + + category.map { + it?.run { it.minBatteryLevelMobile / 10 to it.minBatteryLevelWhileCharging / 10 } + }.ignoreUnchanged().observe(lifecycleOwner, Observer { + if (it != null) { + val (mobile, charging) = it + + binding.seekbarMobile.progress = mobile + binding.seekbarCharging.progress = charging + } + }) + + binding.confirmBtn.setOnClickListener { + if ( + auth.tryDispatchParentAction( + UpdateCategoryBatteryLimit( + categoryId = categoryId, + mobileLimit = binding.seekbarMobile.progress * 10, + chargingLimit = binding.seekbarCharging.progress * 10 + ) + ) + ) { + Snackbar.make(binding.root, R.string.category_settings_battery_limit_confirm_toast, Snackbar.LENGTH_SHORT).show() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/manage/category/settings/CategorySettingsFragment.kt b/app/src/main/java/io/timelimit/android/ui/manage/category/settings/CategorySettingsFragment.kt index 4b2348a..7656fd1 100644 --- a/app/src/main/java/io/timelimit/android/ui/manage/category/settings/CategorySettingsFragment.kt +++ b/app/src/main/java/io/timelimit/android/ui/manage/category/settings/CategorySettingsFragment.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 Jonas Lochmann + * TimeLimit Copyright 2019 - 2020 Jonas Lochmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -61,6 +61,14 @@ class CategorySettingsFragment : Fragment() { auth = auth ) + CategoryBatteryLimitView.bind( + binding = binding.batteryLimit, + lifecycleOwner = this, + category = categoryEntry, + auth = auth, + categoryId = params.categoryId + ) + ParentCategoryView.bind( binding = binding.parentCategory, lifecycleOwner = this, diff --git a/app/src/main/res/layout/category_battery_limit_view.xml b/app/src/main/res/layout/category_battery_limit_view.xml new file mode 100644 index 0000000..51a176c --- /dev/null +++ b/app/src/main/res/layout/category_battery_limit_view.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +