mirror of
https://codeberg.org/timelimit/opentimelimit-android.git
synced 2025-10-04 10:19:21 +02:00
Add parent category support
This commit is contained in:
parent
b7bf364aa6
commit
917f134d77
29 changed files with 1009 additions and 82 deletions
443
app/schemas/io.timelimit.android.data.RoomDatabase/3.json
Normal file
443
app/schemas/io.timelimit.android.data.RoomDatabase/3.json
Normal file
|
@ -0,0 +1,443 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 3,
|
||||||
|
"identityHash": "80fe6fe576c0c935a61ae412e7d14437",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "user",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `password` TEXT NOT NULL, `type` TEXT NOT NULL, `timezone` TEXT NOT NULL, `disable_limits_until` INTEGER NOT NULL, `category_for_not_assigned_apps` 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": "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": "categoryForNotAssignedApps",
|
||||||
|
"columnName": "category_for_not_assigned_apps",
|
||||||
|
"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, `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, `had_manipulation` 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": "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": "hadManipulation",
|
||||||
|
"columnName": "had_manipulation",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "app",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `title` TEXT NOT NULL, `launchable` INTEGER NOT NULL, `recommendation` TEXT NOT NULL, PRIMARY KEY(`package_name`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"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": [
|
||||||
|
"package_name"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_app_package_name",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"package_name"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX `index_app_package_name` ON `${TABLE_NAME}` (`package_name`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "category_app",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`category_id` TEXT NOT NULL, `package_name` TEXT NOT NULL, PRIMARY KEY(`category_id`, `package_name`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "categoryId",
|
||||||
|
"columnName": "category_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "packageName",
|
||||||
|
"columnName": "package_name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"category_id",
|
||||||
|
"package_name"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_category_app_category_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"category_id"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX `index_category_app_category_id` ON `${TABLE_NAME}` (`category_id`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_category_app_package_name",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"package_name"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX `index_category_app_package_name` ON `${TABLE_NAME}` (`package_name`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "category",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `child_id` TEXT NOT NULL, `title` TEXT NOT NULL, `blocked_times` TEXT NOT NULL, `extra_time` INTEGER NOT NULL, `temporarily_blocked` INTEGER NOT NULL, `parent_category_id` TEXT 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": "parentCategoryId",
|
||||||
|
"columnName": "parent_category_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"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}` (`package_name` TEXT NOT NULL, PRIMARY KEY(`package_name`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "packageName",
|
||||||
|
"columnName": "package_name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"package_name"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"80fe6fe576c0c935a61ae412e7d14437\")"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,4 +9,10 @@ object DatabaseMigrations {
|
||||||
database.execSQL("ALTER TABLE `user` ADD COLUMN `category_for_not_assigned_apps` TEXT NOT NULL DEFAULT \"\"")
|
database.execSQL("ALTER TABLE `user` ADD COLUMN `category_for_not_assigned_apps` TEXT NOT NULL DEFAULT \"\"")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val MIGRATE_TO_V3 = object: Migration(2, 3) {
|
||||||
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
|
database.execSQL("ALTER TABLE `category` ADD COLUMN `parent_category_id` TEXT NOT NULL DEFAULT \"\"")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -31,7 +31,7 @@ import io.timelimit.android.data.model.*
|
||||||
TimeLimitRule::class,
|
TimeLimitRule::class,
|
||||||
ConfigurationItem::class,
|
ConfigurationItem::class,
|
||||||
TemporarilyAllowedApp::class
|
TemporarilyAllowedApp::class
|
||||||
], version = 2)
|
], version = 3)
|
||||||
abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database {
|
abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database {
|
||||||
companion object {
|
companion object {
|
||||||
private val lock = Object()
|
private val lock = Object()
|
||||||
|
@ -67,7 +67,8 @@ abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database
|
||||||
.setJournalMode(JournalMode.TRUNCATE)
|
.setJournalMode(JournalMode.TRUNCATE)
|
||||||
.fallbackToDestructiveMigration()
|
.fallbackToDestructiveMigration()
|
||||||
.addMigrations(
|
.addMigrations(
|
||||||
DatabaseMigrations.MIGRATE_TO_V2
|
DatabaseMigrations.MIGRATE_TO_V2,
|
||||||
|
DatabaseMigrations.MIGRATE_TO_V3
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,6 +65,9 @@ abstract class CategoryDao {
|
||||||
|
|
||||||
@Query("SELECT id, child_id, temporarily_blocked FROM category")
|
@Query("SELECT id, child_id, temporarily_blocked FROM category")
|
||||||
abstract fun getAllCategoriesShortInfo(): LiveData<List<CategoryShortInfo>>
|
abstract fun getAllCategoriesShortInfo(): LiveData<List<CategoryShortInfo>>
|
||||||
|
|
||||||
|
@Query("UPDATE category SET parent_category_id = :parentCategoryId WHERE id = :categoryId")
|
||||||
|
abstract fun updateParentCategory(categoryId: String, parentCategoryId: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class CategoryShortInfo(
|
data class CategoryShortInfo(
|
||||||
|
|
|
@ -42,7 +42,9 @@ data class Category(
|
||||||
@ColumnInfo(name = "extra_time")
|
@ColumnInfo(name = "extra_time")
|
||||||
val extraTimeInMillis: Long,
|
val extraTimeInMillis: Long,
|
||||||
@ColumnInfo(name = "temporarily_blocked")
|
@ColumnInfo(name = "temporarily_blocked")
|
||||||
val temporarilyBlocked: Boolean
|
val temporarilyBlocked: Boolean,
|
||||||
|
@ColumnInfo(name = "parent_category_id")
|
||||||
|
val parentCategoryId: String
|
||||||
): JsonSerializable {
|
): JsonSerializable {
|
||||||
companion object {
|
companion object {
|
||||||
const val MINUTES_PER_DAY = 60 * 24
|
const val MINUTES_PER_DAY = 60 * 24
|
||||||
|
@ -54,6 +56,7 @@ data class Category(
|
||||||
private const val BLOCKED_MINUTES_IN_WEEK = "blockedMinutesInWeek"
|
private const val BLOCKED_MINUTES_IN_WEEK = "blockedMinutesInWeek"
|
||||||
private const val EXTRA_TIME_IN_MILLIS = "extraTimeInMillis"
|
private const val EXTRA_TIME_IN_MILLIS = "extraTimeInMillis"
|
||||||
private const val TEMPORARILY_BLOCKED = "temporarilyBlocked"
|
private const val TEMPORARILY_BLOCKED = "temporarilyBlocked"
|
||||||
|
private const val PARENT_CATEGORY_ID = "parentCategoryId"
|
||||||
|
|
||||||
fun parse(reader: JsonReader): Category {
|
fun parse(reader: JsonReader): Category {
|
||||||
var id: String? = null
|
var id: String? = null
|
||||||
|
@ -62,6 +65,8 @@ data class Category(
|
||||||
var blockedMinutesInWeek: ImmutableBitmask? = null
|
var blockedMinutesInWeek: ImmutableBitmask? = null
|
||||||
var extraTimeInMillis: Long? = null
|
var extraTimeInMillis: Long? = null
|
||||||
var temporarilyBlocked: Boolean? = null
|
var temporarilyBlocked: Boolean? = null
|
||||||
|
// this field was added later so it has got a default value
|
||||||
|
var parentCategoryId = ""
|
||||||
|
|
||||||
reader.beginObject()
|
reader.beginObject()
|
||||||
|
|
||||||
|
@ -73,6 +78,7 @@ data class Category(
|
||||||
BLOCKED_MINUTES_IN_WEEK -> blockedMinutesInWeek = ImmutableBitmaskJson.parse(reader.nextString(), BLOCKED_MINUTES_IN_WEEK_LENGTH)
|
BLOCKED_MINUTES_IN_WEEK -> blockedMinutesInWeek = ImmutableBitmaskJson.parse(reader.nextString(), BLOCKED_MINUTES_IN_WEEK_LENGTH)
|
||||||
EXTRA_TIME_IN_MILLIS -> extraTimeInMillis = reader.nextLong()
|
EXTRA_TIME_IN_MILLIS -> extraTimeInMillis = reader.nextLong()
|
||||||
TEMPORARILY_BLOCKED -> temporarilyBlocked = reader.nextBoolean()
|
TEMPORARILY_BLOCKED -> temporarilyBlocked = reader.nextBoolean()
|
||||||
|
PARENT_CATEGORY_ID -> parentCategoryId = reader.nextString()
|
||||||
else -> reader.skipValue()
|
else -> reader.skipValue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -85,7 +91,8 @@ data class Category(
|
||||||
title = title!!,
|
title = title!!,
|
||||||
blockedMinutesInWeek = blockedMinutesInWeek!!,
|
blockedMinutesInWeek = blockedMinutesInWeek!!,
|
||||||
extraTimeInMillis = extraTimeInMillis!!,
|
extraTimeInMillis = extraTimeInMillis!!,
|
||||||
temporarilyBlocked = temporarilyBlocked!!
|
temporarilyBlocked = temporarilyBlocked!!,
|
||||||
|
parentCategoryId = parentCategoryId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -112,6 +119,7 @@ data class Category(
|
||||||
writer.name(BLOCKED_MINUTES_IN_WEEK).value(ImmutableBitmaskJson.serialize(blockedMinutesInWeek))
|
writer.name(BLOCKED_MINUTES_IN_WEEK).value(ImmutableBitmaskJson.serialize(blockedMinutesInWeek))
|
||||||
writer.name(EXTRA_TIME_IN_MILLIS).value(extraTimeInMillis)
|
writer.name(EXTRA_TIME_IN_MILLIS).value(extraTimeInMillis)
|
||||||
writer.name(TEMPORARILY_BLOCKED).value(temporarilyBlocked)
|
writer.name(TEMPORARILY_BLOCKED).value(temporarilyBlocked)
|
||||||
|
writer.name(PARENT_CATEGORY_ID).value(parentCategoryId)
|
||||||
|
|
||||||
writer.endObject()
|
writer.endObject()
|
||||||
}
|
}
|
||||||
|
|
|
@ -136,7 +136,8 @@ class AppSetupLogic(private val appLogic: AppLogic) {
|
||||||
title = defaultCategories.allowedAppsTitle,
|
title = defaultCategories.allowedAppsTitle,
|
||||||
blockedMinutesInWeek = ImmutableBitmask((BitSet())),
|
blockedMinutesInWeek = ImmutableBitmask((BitSet())),
|
||||||
extraTimeInMillis = 0,
|
extraTimeInMillis = 0,
|
||||||
temporarilyBlocked = false
|
temporarilyBlocked = false,
|
||||||
|
parentCategoryId = ""
|
||||||
))
|
))
|
||||||
|
|
||||||
appLogic.database.category().addCategory(Category(
|
appLogic.database.category().addCategory(Category(
|
||||||
|
@ -145,7 +146,8 @@ class AppSetupLogic(private val appLogic: AppLogic) {
|
||||||
title = defaultCategories.allowedGamesTitle,
|
title = defaultCategories.allowedGamesTitle,
|
||||||
blockedMinutesInWeek = defaultCategories.allowedGamesBlockedTimes,
|
blockedMinutesInWeek = defaultCategories.allowedGamesBlockedTimes,
|
||||||
extraTimeInMillis = 0,
|
extraTimeInMillis = 0,
|
||||||
temporarilyBlocked = false
|
temporarilyBlocked = false,
|
||||||
|
parentCategoryId = ""
|
||||||
))
|
))
|
||||||
|
|
||||||
// add default allowed apps
|
// add default allowed apps
|
||||||
|
|
|
@ -186,6 +186,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
val appCategory = appCategories.get(Pair(foregroundAppPackageName, categories.map { it.id })).waitForNullableValue()
|
val appCategory = appCategories.get(Pair(foregroundAppPackageName, categories.map { it.id })).waitForNullableValue()
|
||||||
val category = categories.find { it.id == appCategory?.categoryId }
|
val category = categories.find { it.id == appCategory?.categoryId }
|
||||||
?: categories.find { it.id == deviceUserEntry.categoryForNotAssignedApps }
|
?: categories.find { it.id == deviceUserEntry.categoryForNotAssignedApps }
|
||||||
|
val parentCategory = categories.find { it.id == category?.parentCategoryId }
|
||||||
|
|
||||||
if (category == null) {
|
if (category == null) {
|
||||||
usedTimeUpdateHelper?.commit(appLogic)
|
usedTimeUpdateHelper?.commit(appLogic)
|
||||||
|
@ -196,7 +197,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
))
|
))
|
||||||
appLogic.platformIntegration.setSuspendedApps(listOf(foregroundAppPackageName), true)
|
appLogic.platformIntegration.setSuspendedApps(listOf(foregroundAppPackageName), true)
|
||||||
appLogic.platformIntegration.showAppLockScreen(foregroundAppPackageName)
|
appLogic.platformIntegration.showAppLockScreen(foregroundAppPackageName)
|
||||||
} else if (category.temporarilyBlocked) {
|
} else if (category.temporarilyBlocked or (parentCategory?.temporarilyBlocked == true)) {
|
||||||
usedTimeUpdateHelper?.commit(appLogic)
|
usedTimeUpdateHelper?.commit(appLogic)
|
||||||
|
|
||||||
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||||
|
@ -218,7 +219,8 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
))
|
))
|
||||||
} else if (
|
} else if (
|
||||||
// check blocked time areas
|
// check blocked time areas
|
||||||
(category.blockedMinutesInWeek.read(minuteOfWeek))
|
(category.blockedMinutesInWeek.read(minuteOfWeek)) or
|
||||||
|
(parentCategory?.blockedMinutesInWeek?.read(minuteOfWeek) == true)
|
||||||
) {
|
) {
|
||||||
usedTimeUpdateHelper?.commit(appLogic)
|
usedTimeUpdateHelper?.commit(appLogic)
|
||||||
|
|
||||||
|
@ -230,8 +232,11 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
} else {
|
} else {
|
||||||
// check time limits
|
// check time limits
|
||||||
val rules = timeLimitRules.get(category.id).waitForNonNullValue()
|
val rules = timeLimitRules.get(category.id).waitForNonNullValue()
|
||||||
|
val parentRules = parentCategory?.let {
|
||||||
|
timeLimitRules.get(it.id).waitForNonNullValue()
|
||||||
|
} ?: emptyList()
|
||||||
|
|
||||||
if (rules.isEmpty()) {
|
if (rules.isEmpty() and parentRules.isEmpty()) {
|
||||||
// unlimited
|
// unlimited
|
||||||
usedTimeUpdateHelper?.commit(appLogic)
|
usedTimeUpdateHelper?.commit(appLogic)
|
||||||
|
|
||||||
|
@ -241,33 +246,61 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
val usedTimes = usedTimesOfCategoryAndWeekByFirstDayOfWeek.get(Pair(category.id, nowDate.dayOfEpoch - nowDate.dayOfWeek)).waitForNonNullValue()
|
val usedTimes = usedTimesOfCategoryAndWeekByFirstDayOfWeek.get(Pair(category.id, nowDate.dayOfEpoch - nowDate.dayOfWeek)).waitForNonNullValue()
|
||||||
|
val parentUsedTimes = parentCategory?.let {
|
||||||
|
usedTimesOfCategoryAndWeekByFirstDayOfWeek.get(Pair(it.id, nowDate.dayOfEpoch - nowDate.dayOfWeek)).waitForNonNullValue()
|
||||||
|
} ?: SparseArray()
|
||||||
|
|
||||||
val newUsedTimeItemBatchUpdateHelper = UsedTimeItemBatchUpdateHelper.eventuallyUpdateInstance(
|
val newUsedTimeItemBatchUpdateHelper = UsedTimeItemBatchUpdateHelper.eventuallyUpdateInstance(
|
||||||
date = nowDate,
|
date = nowDate,
|
||||||
categoryId = category.id,
|
childCategoryId = category.id,
|
||||||
|
parentCategoryId = parentCategory?.id,
|
||||||
oldInstance = usedTimeUpdateHelper,
|
oldInstance = usedTimeUpdateHelper,
|
||||||
usedTimeItemForDay = usedTimes.get(nowDate.dayOfWeek),
|
usedTimeItemForDayChild = usedTimes.get(nowDate.dayOfWeek),
|
||||||
|
usedTimeItemForDayParent = parentUsedTimes.get(nowDate.dayOfWeek),
|
||||||
logic = appLogic
|
logic = appLogic
|
||||||
)
|
)
|
||||||
usedTimeUpdateHelper = newUsedTimeItemBatchUpdateHelper
|
usedTimeUpdateHelper = newUsedTimeItemBatchUpdateHelper
|
||||||
|
|
||||||
val usedTimesSparseArray = SparseLongArray()
|
fun buildUsedTimesSparseArray(items: SparseArray<UsedTimeItem>, isParentCategory: Boolean): SparseLongArray {
|
||||||
|
val result = SparseLongArray()
|
||||||
|
|
||||||
for (i in 0..6) {
|
for (i in 0..6) {
|
||||||
val usedTimesItem = usedTimes[i]?.usedMillis
|
val usedTimesItem = items[i]?.usedMillis
|
||||||
|
|
||||||
if (newUsedTimeItemBatchUpdateHelper.date.dayOfWeek == i) {
|
if (newUsedTimeItemBatchUpdateHelper.date.dayOfWeek == i) {
|
||||||
usedTimesSparseArray.put(i, newUsedTimeItemBatchUpdateHelper.getTotalUsedTime())
|
result.put(
|
||||||
} else {
|
i,
|
||||||
usedTimesSparseArray.put(i, (if (usedTimesItem != null) usedTimesItem else 0))
|
if (isParentCategory)
|
||||||
|
newUsedTimeItemBatchUpdateHelper.getTotalUsedTimeParent()
|
||||||
|
else
|
||||||
|
newUsedTimeItemBatchUpdateHelper.getTotalUsedTimeChild()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
result.put(i, usedTimesItem ?: 0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
val remaining = RemainingTime.getRemainingTime(
|
val remainingChild = RemainingTime.getRemainingTime(
|
||||||
nowDate.dayOfWeek, usedTimesSparseArray, rules,
|
nowDate.dayOfWeek,
|
||||||
|
buildUsedTimesSparseArray(usedTimes, isParentCategory = false),
|
||||||
|
rules,
|
||||||
Math.max(0, category.extraTimeInMillis - newUsedTimeItemBatchUpdateHelper.getCachedExtraTimeToSubtract())
|
Math.max(0, category.extraTimeInMillis - newUsedTimeItemBatchUpdateHelper.getCachedExtraTimeToSubtract())
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val remainingParent = parentCategory?.let {
|
||||||
|
RemainingTime.getRemainingTime(
|
||||||
|
nowDate.dayOfWeek,
|
||||||
|
buildUsedTimesSparseArray(parentUsedTimes, isParentCategory = true),
|
||||||
|
parentRules,
|
||||||
|
Math.max(0, parentCategory.extraTimeInMillis - newUsedTimeItemBatchUpdateHelper.getCachedExtraTimeToSubtract())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val remaining = RemainingTime.min(remainingChild, remainingParent)
|
||||||
|
|
||||||
if (remaining == null) {
|
if (remaining == null) {
|
||||||
// unlimited
|
// unlimited
|
||||||
|
|
||||||
|
|
|
@ -129,22 +129,24 @@ class BlockingReasonUtil(private val appLogic: AppLogic) {
|
||||||
if (categoryEntry2 == null) {
|
if (categoryEntry2 == null) {
|
||||||
liveDataFromValue(BlockingReason.NotPartOfAnCategory)
|
liveDataFromValue(BlockingReason.NotPartOfAnCategory)
|
||||||
} else {
|
} else {
|
||||||
getBlockingReasonStep4Point5(categoryEntry2, child, timeZone)
|
getBlockingReasonStep4Point5(categoryEntry2, child, timeZone, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (categoryEntry.temporarilyBlocked) {
|
|
||||||
liveDataFromValue(BlockingReason.TemporarilyBlocked)
|
|
||||||
} else {
|
} else {
|
||||||
getBlockingReasonStep4Point5(categoryEntry, child, timeZone)
|
getBlockingReasonStep4Point5(categoryEntry, child, timeZone, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getBlockingReasonStep4Point5(category: Category, child: User, timeZone: TimeZone): LiveData<BlockingReason> {
|
private fun getBlockingReasonStep4Point5(category: Category, child: User, timeZone: TimeZone, isParentCategory: Boolean): LiveData<BlockingReason> {
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
Log.d(LOG_TAG, "step 4.5")
|
Log.d(LOG_TAG, "step 4.5")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (category.temporarilyBlocked) {
|
||||||
|
return liveDataFromValue(BlockingReason.TemporarilyBlocked)
|
||||||
|
}
|
||||||
|
|
||||||
val areLimitsDisabled: LiveData<Boolean>
|
val areLimitsDisabled: LiveData<Boolean>
|
||||||
|
|
||||||
if (child.disableLimitsUntil == 0L) {
|
if (child.disableLimitsUntil == 0L) {
|
||||||
|
@ -163,6 +165,18 @@ class BlockingReasonUtil(private val appLogic: AppLogic) {
|
||||||
} else {
|
} else {
|
||||||
getBlockingReasonStep5(category, timeZone)
|
getBlockingReasonStep5(category, timeZone)
|
||||||
}
|
}
|
||||||
|
}.switchMap { result ->
|
||||||
|
if (result == BlockingReason.None && (!isParentCategory) && category.parentCategoryId.isNotEmpty()) {
|
||||||
|
appLogic.database.category().getCategoryByChildIdAndId(child.id, category.parentCategoryId).switchMap { parentCategory ->
|
||||||
|
if (parentCategory == null) {
|
||||||
|
liveDataFromValue(BlockingReason.None)
|
||||||
|
} else {
|
||||||
|
getBlockingReasonStep4Point5(parentCategory, child, timeZone, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
liveDataFromValue(result)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,17 @@ data class RemainingTime(val includingExtraTime: Long, val default: Long) {
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
fun min(a: RemainingTime?, b: RemainingTime?): RemainingTime? = if (a == null) {
|
||||||
|
b
|
||||||
|
} else if (b == null) {
|
||||||
|
a
|
||||||
|
} else {
|
||||||
|
RemainingTime(
|
||||||
|
includingExtraTime = Math.min(a.includingExtraTime, b.includingExtraTime),
|
||||||
|
default = Math.min(a.default, b.default)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun getRulesRelatedToDay(dayOfWeek: Int, rules: List<TimeLimitRule>): List<TimeLimitRule> {
|
private fun getRulesRelatedToDay(dayOfWeek: Int, rules: List<TimeLimitRule>): List<TimeLimitRule> {
|
||||||
return rules.filter { (it.dayMask.toInt() and (1 shl dayOfWeek)) != 0 }
|
return rules.filter { (it.dayMask.toInt() and (1 shl dayOfWeek)) != 0 }
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,18 +22,35 @@ import io.timelimit.android.livedata.waitForNullableValue
|
||||||
import io.timelimit.android.sync.actions.AddUsedTimeAction
|
import io.timelimit.android.sync.actions.AddUsedTimeAction
|
||||||
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
||||||
|
|
||||||
class UsedTimeItemBatchUpdateHelper(val date: DateInTimezone, val categoryId: String, var cachedItem: UsedTimeItem?) {
|
class UsedTimeItemBatchUpdateHelper(
|
||||||
|
val date: DateInTimezone,
|
||||||
|
val childCategoryId: String,
|
||||||
|
val parentCategoryId: String?,
|
||||||
|
var cachedItemChild: UsedTimeItem?,
|
||||||
|
var cachedItemParent: UsedTimeItem?
|
||||||
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
suspend fun eventuallyUpdateInstance(
|
suspend fun eventuallyUpdateInstance(
|
||||||
date: DateInTimezone,
|
date: DateInTimezone,
|
||||||
categoryId: String,
|
childCategoryId: String,
|
||||||
|
parentCategoryId: String?,
|
||||||
oldInstance: UsedTimeItemBatchUpdateHelper?,
|
oldInstance: UsedTimeItemBatchUpdateHelper?,
|
||||||
usedTimeItemForDay: UsedTimeItem?,
|
usedTimeItemForDayChild: UsedTimeItem?,
|
||||||
|
usedTimeItemForDayParent: UsedTimeItem?,
|
||||||
logic: AppLogic
|
logic: AppLogic
|
||||||
): UsedTimeItemBatchUpdateHelper {
|
): UsedTimeItemBatchUpdateHelper {
|
||||||
if (oldInstance != null && oldInstance.date == date && oldInstance.categoryId == categoryId) {
|
if (
|
||||||
if (oldInstance.cachedItem != usedTimeItemForDay) {
|
oldInstance != null &&
|
||||||
oldInstance.cachedItem = usedTimeItemForDay
|
oldInstance.date == date &&
|
||||||
|
oldInstance.childCategoryId == childCategoryId &&
|
||||||
|
oldInstance.parentCategoryId == parentCategoryId
|
||||||
|
) {
|
||||||
|
if (oldInstance.cachedItemChild != usedTimeItemForDayChild) {
|
||||||
|
oldInstance.cachedItemChild = usedTimeItemForDayChild
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldInstance.cachedItemParent != usedTimeItemForDayParent) {
|
||||||
|
oldInstance.cachedItemParent = usedTimeItemForDayParent
|
||||||
}
|
}
|
||||||
|
|
||||||
return oldInstance
|
return oldInstance
|
||||||
|
@ -44,8 +61,10 @@ class UsedTimeItemBatchUpdateHelper(val date: DateInTimezone, val categoryId: St
|
||||||
|
|
||||||
return UsedTimeItemBatchUpdateHelper(
|
return UsedTimeItemBatchUpdateHelper(
|
||||||
date = date,
|
date = date,
|
||||||
categoryId = categoryId,
|
childCategoryId = childCategoryId,
|
||||||
cachedItem = usedTimeItemForDay
|
parentCategoryId = parentCategoryId,
|
||||||
|
cachedItemChild = usedTimeItemForDayChild,
|
||||||
|
cachedItemParent = usedTimeItemForDayParent
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,18 +85,18 @@ class UsedTimeItemBatchUpdateHelper(val date: DateInTimezone, val categoryId: St
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getTotalUsedTime(): Long {
|
fun getTotalUsedTimeChild(): Long = (cachedItemChild?.usedMillis ?: 0) + timeToAdd
|
||||||
val cachedItem = cachedItem
|
fun getTotalUsedTimeParent(): Long = (cachedItemParent?.usedMillis ?: 0) + timeToAdd
|
||||||
|
|
||||||
return (if (cachedItem == null) 0 else cachedItem.usedMillis) + timeToAdd
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getCachedExtraTimeToSubtract(): Int {
|
fun getCachedExtraTimeToSubtract(): Int {
|
||||||
return extraTimeToSubtract
|
return extraTimeToSubtract
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun queryCurrentStatusFromDatabase(database: Database) {
|
suspend fun queryCurrentStatusFromDatabase(database: Database) {
|
||||||
cachedItem = database.usedTimes().getUsedTimeItem(categoryId, date.dayOfEpoch).waitForNullableValue()
|
cachedItemChild = database.usedTimes().getUsedTimeItem(childCategoryId, date.dayOfEpoch).waitForNullableValue()
|
||||||
|
cachedItemParent = parentCategoryId?.let {
|
||||||
|
database.usedTimes().getUsedTimeItem(parentCategoryId, date.dayOfEpoch).waitForNullableValue()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun commit(logic: AppLogic) {
|
suspend fun commit(logic: AppLogic) {
|
||||||
|
@ -86,7 +105,7 @@ class UsedTimeItemBatchUpdateHelper(val date: DateInTimezone, val categoryId: St
|
||||||
} else {
|
} else {
|
||||||
ApplyActionUtil.applyAppLogicAction(
|
ApplyActionUtil.applyAppLogicAction(
|
||||||
AddUsedTimeAction(
|
AddUsedTimeAction(
|
||||||
categoryId = categoryId,
|
categoryId = childCategoryId,
|
||||||
timeToAdd = timeToAdd,
|
timeToAdd = timeToAdd,
|
||||||
dayOfEpoch = date.dayOfEpoch,
|
dayOfEpoch = date.dayOfEpoch,
|
||||||
extraTimeToSubtract = extraTimeToSubtract
|
extraTimeToSubtract = extraTimeToSubtract
|
||||||
|
|
|
@ -137,6 +137,17 @@ data class SetCategoryForUnassignedApps(val childId: String, val categoryId: Str
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
data class SetParentCategory(val categoryId: String, val parentCategory: String): ParentAction() {
|
||||||
|
// parent category id can be empty
|
||||||
|
|
||||||
|
init {
|
||||||
|
IdGenerator.assertIdValid(categoryId)
|
||||||
|
|
||||||
|
if (parentCategory.isNotEmpty()) {
|
||||||
|
IdGenerator.assertIdValid(parentCategory)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// DeviceDao
|
// DeviceDao
|
||||||
|
|
||||||
|
|
|
@ -33,34 +33,46 @@ object LocalDatabaseAppLogicActionDispatcher {
|
||||||
try {
|
try {
|
||||||
when(action) {
|
when(action) {
|
||||||
is AddUsedTimeAction -> {
|
is AddUsedTimeAction -> {
|
||||||
DatabaseValidation.assertCategoryExists(database, action.categoryId)
|
val categoryEntry = database.category().getCategoryByIdSync(action.categoryId)!!
|
||||||
|
val parentCategoryEntry = if (categoryEntry.parentCategoryId.isNotEmpty())
|
||||||
|
database.category().getCategoryByIdSync(categoryEntry.parentCategoryId)
|
||||||
|
else
|
||||||
|
null
|
||||||
|
|
||||||
// try to update
|
fun handleAddUsedTime(categoryId: String) {
|
||||||
val updatedRows = database.usedTimes().addUsedTime(
|
// try to update
|
||||||
categoryId = action.categoryId,
|
val updatedRows = database.usedTimes().addUsedTime(
|
||||||
timeToAdd = action.timeToAdd,
|
categoryId = categoryId,
|
||||||
dayOfEpoch = action.dayOfEpoch
|
timeToAdd = action.timeToAdd,
|
||||||
)
|
dayOfEpoch = action.dayOfEpoch
|
||||||
|
|
||||||
if (updatedRows == 0) {
|
|
||||||
// create new entry
|
|
||||||
|
|
||||||
database.usedTimes().insertUsedTime(UsedTimeItem(
|
|
||||||
categoryId = action.categoryId,
|
|
||||||
dayOfEpoch = action.dayOfEpoch,
|
|
||||||
usedMillis = action.timeToAdd.toLong()
|
|
||||||
))
|
|
||||||
} // required to make this compile
|
|
||||||
|
|
||||||
|
|
||||||
if (action.extraTimeToSubtract != 0) {
|
|
||||||
database.category().subtractCategoryExtraTime(
|
|
||||||
categoryId = action.categoryId,
|
|
||||||
removedExtraTime = action.extraTimeToSubtract
|
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
// required to make this compile
|
if (updatedRows == 0) {
|
||||||
|
// create new entry
|
||||||
|
|
||||||
|
database.usedTimes().insertUsedTime(UsedTimeItem(
|
||||||
|
categoryId = categoryId,
|
||||||
|
dayOfEpoch = action.dayOfEpoch,
|
||||||
|
usedMillis = action.timeToAdd.toLong()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (action.extraTimeToSubtract != 0) {
|
||||||
|
database.category().subtractCategoryExtraTime(
|
||||||
|
categoryId = categoryId,
|
||||||
|
removedExtraTime = action.extraTimeToSubtract
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleAddUsedTime(categoryEntry.id)
|
||||||
|
|
||||||
|
if (parentCategoryEntry?.childId == categoryEntry.childId) {
|
||||||
|
handleAddUsedTime(parentCategoryEntry.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
null
|
||||||
}
|
}
|
||||||
is AddInstalledAppsAction -> {
|
is AddInstalledAppsAction -> {
|
||||||
database.app().addAppsSync(
|
database.app().addAppsSync(
|
||||||
|
|
|
@ -73,7 +73,8 @@ object LocalDatabaseParentActionDispatcher {
|
||||||
// nothing blocked by default
|
// nothing blocked by default
|
||||||
blockedMinutesInWeek = ImmutableBitmask(BitSet()),
|
blockedMinutesInWeek = ImmutableBitmask(BitSet()),
|
||||||
extraTimeInMillis = 0,
|
extraTimeInMillis = 0,
|
||||||
temporarilyBlocked = false
|
temporarilyBlocked = false,
|
||||||
|
parentCategoryId = ""
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
is DeleteCategoryAction -> {
|
is DeleteCategoryAction -> {
|
||||||
|
@ -104,13 +105,24 @@ object LocalDatabaseParentActionDispatcher {
|
||||||
database.category().updateCategoryExtraTime(action.categoryId, action.newExtraTime)
|
database.category().updateCategoryExtraTime(action.categoryId, action.newExtraTime)
|
||||||
}
|
}
|
||||||
is IncrementCategoryExtraTimeAction -> {
|
is IncrementCategoryExtraTimeAction -> {
|
||||||
DatabaseValidation.assertCategoryExists(database, action.categoryId)
|
|
||||||
|
|
||||||
if (action.addedExtraTime < 0) {
|
if (action.addedExtraTime < 0) {
|
||||||
throw IllegalArgumentException("invalid added extra time")
|
throw IllegalArgumentException("invalid added extra time")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val category = database.category().getCategoryByIdSync(action.categoryId)
|
||||||
|
?: throw IllegalArgumentException("category ${action.categoryId} does not exist")
|
||||||
|
|
||||||
database.category().incrementCategoryExtraTime(action.categoryId, action.addedExtraTime)
|
database.category().incrementCategoryExtraTime(action.categoryId, action.addedExtraTime)
|
||||||
|
|
||||||
|
if (category.parentCategoryId.isNotEmpty()) {
|
||||||
|
val parentCategory = database.category().getCategoryByIdSync(category.parentCategoryId)
|
||||||
|
|
||||||
|
if (parentCategory?.childId == category.childId) {
|
||||||
|
database.category().incrementCategoryExtraTime(parentCategory.id, action.addedExtraTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
null
|
||||||
}
|
}
|
||||||
is UpdateCategoryTemporarilyBlockedAction -> {
|
is UpdateCategoryTemporarilyBlockedAction -> {
|
||||||
DatabaseValidation.assertCategoryExists(database, action.categoryId)
|
DatabaseValidation.assertCategoryExists(database, action.categoryId)
|
||||||
|
@ -281,6 +293,29 @@ object LocalDatabaseParentActionDispatcher {
|
||||||
childId = action.childId
|
childId = action.childId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
is SetParentCategory -> {
|
||||||
|
val category = database.category().getCategoryByIdSync(action.categoryId)!!
|
||||||
|
|
||||||
|
if (action.parentCategory.isNotEmpty()) {
|
||||||
|
val categories = database.category().getCategoriesByChildIdSync(category.childId)
|
||||||
|
|
||||||
|
val parentCategoryItem = categories.find { it.id == action.parentCategory }
|
||||||
|
?: throw IllegalArgumentException("selected parent category does not exist")
|
||||||
|
|
||||||
|
if (parentCategoryItem.parentCategoryId.isNotEmpty()) {
|
||||||
|
throw IllegalArgumentException("can not set a category as parent which itself has got a parent")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (categories.find { it.parentCategoryId == action.categoryId } != null) {
|
||||||
|
throw IllegalArgumentException("can not make category a child category if it is already a parent category")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
database.category().updateParentCategory(
|
||||||
|
categoryId = action.categoryId,
|
||||||
|
parentCategoryId = action.parentCategory
|
||||||
|
)
|
||||||
|
}
|
||||||
}.let { }
|
}.let { }
|
||||||
|
|
||||||
database.setTransactionSuccessful()
|
database.setTransactionSuccessful()
|
||||||
|
|
|
@ -58,6 +58,16 @@ class CategorySettingsFragment : Fragment() {
|
||||||
auth = auth
|
auth = auth
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ParentCategoryView.bind(
|
||||||
|
binding = binding.parentCategory,
|
||||||
|
lifecycleOwner = this,
|
||||||
|
categoryId = params.categoryId,
|
||||||
|
childId = params.childId,
|
||||||
|
database = appLogic.database,
|
||||||
|
fragmentManager = fragmentManager!!,
|
||||||
|
auth = auth
|
||||||
|
)
|
||||||
|
|
||||||
binding.btnDeleteCategory.setOnClickListener { deleteCategory() }
|
binding.btnDeleteCategory.setOnClickListener { deleteCategory() }
|
||||||
binding.editCategoryTitleGo.setOnClickListener { renameCategory() }
|
binding.editCategoryTitleGo.setOnClickListener { renameCategory() }
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation version 3 of the License.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.ui.manage.category.settings
|
||||||
|
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import io.timelimit.android.data.Database
|
||||||
|
import io.timelimit.android.databinding.ManageParentCategoryBinding
|
||||||
|
import io.timelimit.android.ui.main.ActivityViewModel
|
||||||
|
|
||||||
|
object ParentCategoryView {
|
||||||
|
fun bind(
|
||||||
|
binding: ManageParentCategoryBinding,
|
||||||
|
auth: ActivityViewModel,
|
||||||
|
lifecycleOwner: LifecycleOwner,
|
||||||
|
categoryId: String,
|
||||||
|
childId: String,
|
||||||
|
database: Database,
|
||||||
|
fragmentManager: FragmentManager
|
||||||
|
) {
|
||||||
|
database.category().getCategoriesByChildId(childId).observe(lifecycleOwner, Observer { categories ->
|
||||||
|
val ownCategory = categories.find { it.id == categoryId }
|
||||||
|
val parentCategory = categories.find { it.id == ownCategory?.parentCategoryId }
|
||||||
|
val hasSubCategories = categories.find { it.parentCategoryId == categoryId } != null
|
||||||
|
|
||||||
|
binding.parentCategoryTitle = parentCategory?.title
|
||||||
|
binding.isParentCategory = hasSubCategories
|
||||||
|
})
|
||||||
|
|
||||||
|
binding.selectParentButton.setOnClickListener {
|
||||||
|
if (auth.requestAuthenticationOrReturnTrue()) {
|
||||||
|
SelectParentCategoryDialogFragment.newInstance(
|
||||||
|
childId = childId,
|
||||||
|
categoryId = categoryId
|
||||||
|
).show(fragmentManager)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,151 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation version 3 of the License.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.ui.manage.category.settings
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.CheckedTextView
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
|
import io.timelimit.android.R
|
||||||
|
import io.timelimit.android.data.Database
|
||||||
|
import io.timelimit.android.data.model.Category
|
||||||
|
import io.timelimit.android.data.model.UserType
|
||||||
|
import io.timelimit.android.databinding.BottomSheetSelectionListBinding
|
||||||
|
import io.timelimit.android.extensions.showSafe
|
||||||
|
import io.timelimit.android.logic.AppLogic
|
||||||
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
|
import io.timelimit.android.sync.actions.SetParentCategory
|
||||||
|
import io.timelimit.android.ui.main.ActivityViewModel
|
||||||
|
import io.timelimit.android.ui.main.ActivityViewModelHolder
|
||||||
|
|
||||||
|
class SelectParentCategoryDialogFragment: BottomSheetDialogFragment() {
|
||||||
|
companion object {
|
||||||
|
private const val DIALOG_TAG = "SelectParentCategoryDialogFragment"
|
||||||
|
private const val CATEGORY_ID = "categoryId"
|
||||||
|
private const val CHILD_ID = "childId"
|
||||||
|
|
||||||
|
fun newInstance(childId: String, categoryId: String) = SelectParentCategoryDialogFragment().apply {
|
||||||
|
arguments = Bundle().apply {
|
||||||
|
putString(CHILD_ID, childId)
|
||||||
|
putString(CATEGORY_ID, categoryId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val childId: String by lazy { arguments!!.getString(CHILD_ID) }
|
||||||
|
val categoryId: String by lazy { arguments!!.getString(CATEGORY_ID) }
|
||||||
|
|
||||||
|
val logic: AppLogic by lazy { DefaultAppLogic.with(context!!) }
|
||||||
|
val database: Database by lazy { logic.database }
|
||||||
|
val auth: ActivityViewModel by lazy { (activity as ActivityViewModelHolder).getActivityViewModel() }
|
||||||
|
|
||||||
|
val childCategoryEntries: LiveData<List<Category>> by lazy {
|
||||||
|
database.category().getCategoriesByChildId(childId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
childCategoryEntries.observe(this, Observer { categories ->
|
||||||
|
val ownCategory = categories.find { it.id == categoryId }
|
||||||
|
val hasSubCategories = categories.find { it.parentCategoryId == categoryId } != null
|
||||||
|
|
||||||
|
if (ownCategory == null || hasSubCategories) {
|
||||||
|
dismissAllowingStateLoss()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
auth.authenticatedUser.observe(this, Observer {
|
||||||
|
if (it?.second?.type != UserType.Parent) {
|
||||||
|
dismissAllowingStateLoss()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
val binding = BottomSheetSelectionListBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
|
binding.title = getString(R.string.category_settings_parent_category_title)
|
||||||
|
|
||||||
|
val list = binding.list
|
||||||
|
|
||||||
|
childCategoryEntries.observe(this, Observer { categories ->
|
||||||
|
list.removeAllViews()
|
||||||
|
|
||||||
|
val ownCategory = categories.find { it.id == categoryId }
|
||||||
|
val ownParentCategory = categories.find { it.id == ownCategory?.parentCategoryId }
|
||||||
|
|
||||||
|
fun buildRow(): CheckedTextView = LayoutInflater.from(context!!).inflate(
|
||||||
|
android.R.layout.simple_list_item_single_choice,
|
||||||
|
list,
|
||||||
|
false
|
||||||
|
) as CheckedTextView
|
||||||
|
|
||||||
|
categories.forEach { category ->
|
||||||
|
if (category.id != categoryId) {
|
||||||
|
val row = buildRow()
|
||||||
|
|
||||||
|
row.text = category.title
|
||||||
|
row.isChecked = category.id == ownCategory?.parentCategoryId
|
||||||
|
row.isEnabled = categories.find { it.id == category.parentCategoryId } == null
|
||||||
|
row.setOnClickListener {
|
||||||
|
if (!row.isChecked) {
|
||||||
|
auth.tryDispatchParentAction(
|
||||||
|
SetParentCategory(
|
||||||
|
categoryId = categoryId,
|
||||||
|
parentCategory = category.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
list.addView(row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildRow().let { row ->
|
||||||
|
row.setText(R.string.category_settings_parent_category_none)
|
||||||
|
row.isChecked = ownParentCategory == null
|
||||||
|
|
||||||
|
row.setOnClickListener {
|
||||||
|
if (!row.isChecked) {
|
||||||
|
auth.tryDispatchParentAction(
|
||||||
|
SetParentCategory(
|
||||||
|
categoryId = categoryId,
|
||||||
|
parentCategory = ""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
list.addView(row)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
fun show(fragmentManager: FragmentManager) = showSafe(fragmentManager, DIALOG_TAG)
|
||||||
|
}
|
|
@ -28,7 +28,7 @@ import io.timelimit.android.R
|
||||||
import io.timelimit.android.data.Database
|
import io.timelimit.android.data.Database
|
||||||
import io.timelimit.android.data.model.Category
|
import io.timelimit.android.data.model.Category
|
||||||
import io.timelimit.android.data.model.UserType
|
import io.timelimit.android.data.model.UserType
|
||||||
import io.timelimit.android.databinding.AssignAppDialogBinding
|
import io.timelimit.android.databinding.BottomSheetSelectionListBinding
|
||||||
import io.timelimit.android.extensions.showSafe
|
import io.timelimit.android.extensions.showSafe
|
||||||
import io.timelimit.android.logic.AppLogic
|
import io.timelimit.android.logic.AppLogic
|
||||||
import io.timelimit.android.logic.DefaultAppLogic
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
|
@ -73,10 +73,10 @@ class AssignAllAppsCategoryDialogFragment: BottomSheetDialogFragment() {
|
||||||
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
val binding = AssignAppDialogBinding.inflate(inflater, container, false)
|
val binding = BottomSheetSelectionListBinding.inflate(inflater, container, false)
|
||||||
val list = binding.categoryList
|
val list = binding.list
|
||||||
|
|
||||||
binding.appTitle = resources.getQuantityString(R.plurals.generic_plural_app, appPackageNames.size, appPackageNames.size)
|
binding.title = resources.getQuantityString(R.plurals.generic_plural_app, appPackageNames.size, appPackageNames.size)
|
||||||
|
|
||||||
childCategoryEntries.observe(this, Observer { categories ->
|
childCategoryEntries.observe(this, Observer { categories ->
|
||||||
fun buildRow(): CheckedTextView = LayoutInflater.from(context!!).inflate(
|
fun buildRow(): CheckedTextView = LayoutInflater.from(context!!).inflate(
|
||||||
|
|
|
@ -30,7 +30,7 @@ import io.timelimit.android.data.model.App
|
||||||
import io.timelimit.android.data.model.Category
|
import io.timelimit.android.data.model.Category
|
||||||
import io.timelimit.android.data.model.CategoryApp
|
import io.timelimit.android.data.model.CategoryApp
|
||||||
import io.timelimit.android.data.model.UserType
|
import io.timelimit.android.data.model.UserType
|
||||||
import io.timelimit.android.databinding.AssignAppDialogBinding
|
import io.timelimit.android.databinding.BottomSheetSelectionListBinding
|
||||||
import io.timelimit.android.extensions.showSafe
|
import io.timelimit.android.extensions.showSafe
|
||||||
import io.timelimit.android.livedata.map
|
import io.timelimit.android.livedata.map
|
||||||
import io.timelimit.android.livedata.switchMap
|
import io.timelimit.android.livedata.switchMap
|
||||||
|
@ -96,9 +96,9 @@ class AssignAppCategoryDialogFragment: BottomSheetDialogFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
val binding = AssignAppDialogBinding.inflate(inflater, container, false)
|
val binding = BottomSheetSelectionListBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
val list = binding.categoryList
|
val list = binding.list
|
||||||
|
|
||||||
childCategoryEntries.switchMap { categories ->
|
childCategoryEntries.switchMap { categories ->
|
||||||
categoryAppEntry.map { appCategory ->
|
categoryAppEntry.map { appCategory ->
|
||||||
|
@ -157,7 +157,7 @@ class AssignAppCategoryDialogFragment: BottomSheetDialogFragment() {
|
||||||
})
|
})
|
||||||
|
|
||||||
matchingAppEntries.observe(this, Observer {
|
matchingAppEntries.observe(this, Observer {
|
||||||
binding.appTitle = it.firstOrNull()?.title
|
binding.title = it.firstOrNull()?.title
|
||||||
})
|
})
|
||||||
|
|
||||||
return binding.root
|
return binding.root
|
||||||
|
|
|
@ -125,6 +125,7 @@ class Adapter: RecyclerView.Adapter<ViewHolder>() {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
binding.usedForAppsWithoutCategory = item.usedForNotAssignedApps
|
binding.usedForAppsWithoutCategory = item.usedForNotAssignedApps
|
||||||
|
binding.parentCategoryTitle = item.parentCategoryTitle
|
||||||
|
|
||||||
binding.card.setOnClickListener { handlers?.onCategoryClicked(item.category) }
|
binding.card.setOnClickListener { handlers?.onCategoryClicked(item.category) }
|
||||||
|
|
||||||
|
|
|
@ -26,5 +26,6 @@ data class CategoryItem(
|
||||||
val isBlockedTimeNow: Boolean,
|
val isBlockedTimeNow: Boolean,
|
||||||
val remainingTimeToday: Long?,
|
val remainingTimeToday: Long?,
|
||||||
val usedTimeToday: Long,
|
val usedTimeToday: Long,
|
||||||
val usedForNotAssignedApps: Boolean
|
val usedForNotAssignedApps: Boolean,
|
||||||
|
val parentCategoryTitle: String?
|
||||||
): ManageChildCategoriesListItem()
|
): ManageChildCategoriesListItem()
|
||||||
|
|
|
@ -87,10 +87,10 @@ class ManageChildCategoriesModel(application: Application): AndroidViewModel(app
|
||||||
val firstDayOfWeek = childDate.dayOfEpoch - childDate.dayOfWeek
|
val firstDayOfWeek = childDate.dayOfEpoch - childDate.dayOfWeek
|
||||||
|
|
||||||
categories.map { category ->
|
categories.map { category ->
|
||||||
|
|
||||||
val rules = rulesByCategoryId[category.id] ?: emptyList()
|
val rules = rulesByCategoryId[category.id] ?: emptyList()
|
||||||
val usedTimeItemsForCategory = usedTimesByCategory[category.id]
|
val usedTimeItemsForCategory = usedTimesByCategory[category.id]
|
||||||
?: emptyList()
|
?: emptyList()
|
||||||
|
val parentCategory = categories.find { it.id == category.parentCategoryId }
|
||||||
|
|
||||||
CategoryItem(
|
CategoryItem(
|
||||||
category = category,
|
category = category,
|
||||||
|
@ -110,7 +110,8 @@ class ManageChildCategoriesModel(application: Application): AndroidViewModel(app
|
||||||
)?.includingExtraTime,
|
)?.includingExtraTime,
|
||||||
usedTimeToday = usedTimeItemsForCategory.find { item -> item.dayOfEpoch == childDate.dayOfEpoch }?.usedMillis
|
usedTimeToday = usedTimeItemsForCategory.find { item -> item.dayOfEpoch == childDate.dayOfEpoch }?.usedMillis
|
||||||
?: 0,
|
?: 0,
|
||||||
usedForNotAssignedApps = categoryForUnassignedApps == category.id
|
usedForNotAssignedApps = categoryForUnassignedApps == category.id,
|
||||||
|
parentCategoryTitle = parentCategory?.title
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
|
|
||||||
<data>
|
<data>
|
||||||
<variable
|
<variable
|
||||||
name="appTitle"
|
name="title"
|
||||||
type="String" />
|
type="String" />
|
||||||
</data>
|
</data>
|
||||||
|
|
||||||
|
@ -31,13 +31,13 @@
|
||||||
<TextView
|
<TextView
|
||||||
android:textAppearance="?android:textAppearanceLarge"
|
android:textAppearance="?android:textAppearanceLarge"
|
||||||
android:padding="8dp"
|
android:padding="8dp"
|
||||||
android:text="@{appTitle}"
|
android:text="@{title}"
|
||||||
tools:text="Systemeinstellungen"
|
tools:text="Systemeinstellungen"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/category_list"
|
android:id="@+id/list"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content">
|
|
@ -39,6 +39,10 @@
|
||||||
name="usedForAppsWithoutCategory"
|
name="usedForAppsWithoutCategory"
|
||||||
type="boolean" />
|
type="boolean" />
|
||||||
|
|
||||||
|
<variable
|
||||||
|
name="parentCategoryTitle"
|
||||||
|
type="String" />
|
||||||
|
|
||||||
<import type="android.text.TextUtils" />
|
<import type="android.text.TextUtils" />
|
||||||
<import type="android.view.View" />
|
<import type="android.view.View" />
|
||||||
</data>
|
</data>
|
||||||
|
@ -71,6 +75,14 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:textAppearance="?android:textAppearanceMedium"
|
||||||
|
android:visibility="@{TextUtils.isEmpty(parentCategoryTitle) ? View.GONE : View.VISIBLE}"
|
||||||
|
tools:text="@string/manage_child_category_is_child"
|
||||||
|
android:text="@{@string/manage_child_category_is_child(parentCategoryTitle)}"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:visibility="@{TextUtils.isEmpty(usedTimeToday) ? View.GONE : View.VISIBLE}"
|
android:visibility="@{TextUtils.isEmpty(usedTimeToday) ? View.GONE : View.VISIBLE}"
|
||||||
android:text="@{usedTimeToday}"
|
android:text="@{usedTimeToday}"
|
||||||
|
|
|
@ -68,6 +68,9 @@
|
||||||
<include android:id="@+id/category_for_unassigned_apps"
|
<include android:id="@+id/category_for_unassigned_apps"
|
||||||
layout="@layout/manage_category_for_unassigned_apps" />
|
layout="@layout/manage_category_for_unassigned_apps" />
|
||||||
|
|
||||||
|
<include android:id="@+id/parent_category"
|
||||||
|
layout="@layout/manage_parent_category" />
|
||||||
|
|
||||||
<androidx.cardview.widget.CardView
|
<androidx.cardview.widget.CardView
|
||||||
app:cardUseCompatPadding="true"
|
app:cardUseCompatPadding="true"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|
81
app/src/main/res/layout/manage_parent_category.xml
Normal file
81
app/src/main/res/layout/manage_parent_category.xml
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation version 3 of the License.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<data>
|
||||||
|
<variable
|
||||||
|
name="isParentCategory"
|
||||||
|
type="boolean" />
|
||||||
|
|
||||||
|
<variable
|
||||||
|
name="parentCategoryTitle"
|
||||||
|
type="String" />
|
||||||
|
|
||||||
|
<import type="android.view.View" />
|
||||||
|
<import type="android.text.TextUtils" />
|
||||||
|
</data>
|
||||||
|
|
||||||
|
<androidx.cardview.widget.CardView
|
||||||
|
app:cardUseCompatPadding="true"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
<LinearLayout
|
||||||
|
android:padding="8dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:text="@string/category_settings_parent_category_title"
|
||||||
|
android:textAppearance="?android:textAppearanceLarge"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:text="@string/category_settings_parent_category_intro"
|
||||||
|
android:textAppearance="?android:textAppearanceMedium"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:visibility="@{isParentCategory ? View.VISIBLE : View.GONE}"
|
||||||
|
android:text="@string/category_settings_parent_category_already_used_as_parent"
|
||||||
|
android:textAppearance="?android:textAppearanceMedium"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:visibility="@{TextUtils.isEmpty(parentCategoryTitle) ? View.GONE : View.VISIBLE}"
|
||||||
|
tools:text="@string/category_settings_parent_category_assigned_to"
|
||||||
|
android:text="@{@string/category_settings_parent_category_assigned_to(parentCategoryTitle)}"
|
||||||
|
android:textAppearance="?android:textAppearanceMedium"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/select_parent_button"
|
||||||
|
android:enabled="@{!isParentCategory}"
|
||||||
|
android:text="@string/category_settings_parent_category_button"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
|
</layout>
|
|
@ -27,4 +27,11 @@
|
||||||
<string name="category_settings_extra_time_change_toast">Die neue Extrazeit wurde gespeichert</string>
|
<string name="category_settings_extra_time_change_toast">Die neue Extrazeit wurde gespeichert</string>
|
||||||
|
|
||||||
<string name="category_settings_delete_dialog">Möchten Sie die Kategorie %s löschen?</string>
|
<string name="category_settings_delete_dialog">Möchten Sie die Kategorie %s löschen?</string>
|
||||||
|
|
||||||
|
<string name="category_settings_parent_category_title">Ober-Kategorie</string>
|
||||||
|
<string name="category_settings_parent_category_intro">Wenn die Oberkategorie einer Kategorie blockiert ist (z.B. weil die Zeit verbraucht wurde), dann wird auch die Unterkategorie blockiert. Wenn die Unterkategorie verwendet wird, dann wird die Nutzungsdauer auch zur Ober-Kategorie hinzugefügt.</string>
|
||||||
|
<string name="category_settings_parent_category_already_used_as_parent">Sie können keine Oberkategorie wählen, weil diese Kategorie bereits die Oberkategorie für eine andere Kategorie ist.</string>
|
||||||
|
<string name="category_settings_parent_category_assigned_to">Diese Kategorie ist eine Unterkategorie von %s.</string>
|
||||||
|
<string name="category_settings_parent_category_button">Oberkategorie wählen</string>
|
||||||
|
<string name="category_settings_parent_category_none">keine Oberkategorie</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -39,4 +39,5 @@
|
||||||
</string>
|
</string>
|
||||||
|
|
||||||
<string name="manage_child_category_for_unassigned_apps">wird für Apps ohne Kategorie verwendet</string>
|
<string name="manage_child_category_for_unassigned_apps">wird für Apps ohne Kategorie verwendet</string>
|
||||||
|
<string name="manage_child_category_is_child">Unterkategorie von %s</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -28,4 +28,11 @@
|
||||||
<string name="category_settings_extra_time_change_toast">The new extra time was saved</string>
|
<string name="category_settings_extra_time_change_toast">The new extra time was saved</string>
|
||||||
|
|
||||||
<string name="category_settings_delete_dialog">Do you want to delete the category %s?</string>
|
<string name="category_settings_delete_dialog">Do you want to delete the category %s?</string>
|
||||||
|
|
||||||
|
<string name="category_settings_parent_category_title">Parent category</string>
|
||||||
|
<string name="category_settings_parent_category_intro">When the parent category of a category is blocked (e.g. because the time is over), then the child category is blocked too. When using the child category, the used time is also added to the parent category.</string>
|
||||||
|
<string name="category_settings_parent_category_already_used_as_parent">You can not select a parent category because this category is the parent of an other one.</string>
|
||||||
|
<string name="category_settings_parent_category_assigned_to">This category is a child category of %s now.</string>
|
||||||
|
<string name="category_settings_parent_category_button">Select parent category</string>
|
||||||
|
<string name="category_settings_parent_category_none">no parent category</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -40,4 +40,5 @@
|
||||||
</string>
|
</string>
|
||||||
|
|
||||||
<string name="manage_child_category_for_unassigned_apps">used for Apps without category</string>
|
<string name="manage_child_category_for_unassigned_apps">used for Apps without category</string>
|
||||||
|
<string name="manage_child_category_is_child">Child category of %s</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue