Make the categories sortable

This commit is contained in:
Jonas Lochmann 2020-02-10 01:00:00 +01:00
parent 412966df26
commit 0d2dd7bed1
No known key found for this signature in database
GPG key ID: 8B8C9AEE10FA5B36
16 changed files with 1020 additions and 33 deletions

View file

@ -0,0 +1,828 @@
{
"formatVersion": 1,
"database": {
"version": 25,
"identityHash": "2a202b43acf918df8278ab09c67b5ddf",
"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, `temporarily_blocked_end_time` INTEGER NOT NULL, `base_version` TEXT NOT NULL, `apps_version` TEXT NOT NULL, `rules_version` TEXT NOT NULL, `usedtimes_version` TEXT NOT NULL, `parent_category_id` TEXT NOT NULL, `block_all_notifications` INTEGER NOT NULL, `time_warnings` INTEGER NOT NULL, `min_battery_charging` INTEGER NOT NULL, `min_battery_mobile` INTEGER NOT NULL, `sort` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "childId",
"columnName": "child_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "blockedMinutesInWeek",
"columnName": "blocked_times",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "extraTimeInMillis",
"columnName": "extra_time",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "temporarilyBlocked",
"columnName": "temporarily_blocked",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "temporarilyBlockedEndTime",
"columnName": "temporarily_blocked_end_time",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "baseVersion",
"columnName": "base_version",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "assignedAppsVersion",
"columnName": "apps_version",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "timeLimitRulesVersion",
"columnName": "rules_version",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "usedTimesVersion",
"columnName": "usedtimes_version",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "parentCategoryId",
"columnName": "parent_category_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "blockAllNotifications",
"columnName": "block_all_notifications",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "timeWarnings",
"columnName": "time_warnings",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "minBatteryLevelWhileCharging",
"columnName": "min_battery_charging",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "minBatteryLevelMobile",
"columnName": "min_battery_mobile",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "sort",
"columnName": "sort",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "used_time",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`day_of_epoch` INTEGER NOT NULL, `used_time` INTEGER NOT NULL, `category_id` TEXT NOT NULL, 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, '2a202b43acf918df8278ab09c67b5ddf')"
]
}
}

View file

@ -172,4 +172,10 @@ object DatabaseMigrations {
database.execSQL("ALTER TABLE `category` ADD COLUMN `temporarily_blocked_end_time` INTEGER NOT NULL DEFAULT 0")
}
}
val MIGRATE_TO_V25 = object: Migration(24, 25) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `category` ADD COLUMN `sort` INTEGER NOT NULL DEFAULT 0")
}
}
}

View file

@ -35,7 +35,7 @@ import io.timelimit.android.data.model.*
AppActivity::class,
Notification::class,
AllowedContact::class
], version = 24)
], version = 25)
abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database {
companion object {
private val lock = Object()
@ -93,7 +93,8 @@ abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database
DatabaseMigrations.MIGRATE_TO_V21,
DatabaseMigrations.MIGRATE_TO_V22,
DatabaseMigrations.MIGRATE_TO_V23,
DatabaseMigrations.MIGRATE_TO_V24
DatabaseMigrations.MIGRATE_TO_V24,
DatabaseMigrations.MIGRATE_TO_V25
)
.build()
}

View file

@ -97,6 +97,14 @@ abstract class CategoryDao {
@Query("SELECT * FROM category")
abstract fun getAllCategoriesSync(): List<Category>
// if there is no category, then the result is null
// Room converts null to 0 si it works
@Query("SELECT MAX(sort) + 1 FROM category WHERE child_id = :childId")
abstract fun getNextCategorySortKeyByChildId(childId: String): Int
@Query("UPDATE category SET sort = :sort WHERE id = :categoryId")
abstract fun updateCategorySorting(categoryId: String, sort: Int)
}
data class CategoryWithVersionNumbers(

View file

@ -0,0 +1,35 @@
/*
* TimeLimit Copyright <C> 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 <https://www.gnu.org/licenses/>.
*/
package io.timelimit.android.data.extensions
import io.timelimit.android.data.model.Category
fun List<Category>.sorted(): List<Category> {
val categoryIds = this.map { it.id }.toSet()
val sortedCategories = mutableListOf<Category>()
val childCategories = this.filter { categoryIds.contains(it.parentCategoryId) }.groupBy { it.parentCategoryId }
this.filterNot { categoryIds.contains(it.parentCategoryId) }.sortedBy { it.sort }.forEach { category ->
sortedCategories.add(category)
childCategories[category.id]?.sortedBy { it.sort }?.let { items ->
sortedCategories.addAll(items)
}
}
return sortedCategories.toList()
}

View file

@ -63,7 +63,9 @@ data class Category(
@ColumnInfo(name = "min_battery_charging")
val minBatteryLevelWhileCharging: Int,
@ColumnInfo(name = "min_battery_mobile")
val minBatteryLevelMobile: Int
val minBatteryLevelMobile: Int,
@ColumnInfo(name = "sort")
val sort: Int
): JsonSerializable {
companion object {
const val MINUTES_PER_DAY = 60 * 24
@ -85,6 +87,7 @@ data class Category(
private const val TIME_WARNINGS = "tw"
private const val MIN_BATTERY_CHARGING = "minBatteryCharging"
private const val MIN_BATTERY_MOBILE = "minBatteryMobile"
private const val SORT = "sort"
fun parse(reader: JsonReader): Category {
var id: String? = null
@ -104,6 +107,7 @@ data class Category(
var timeWarnings = 0
var minBatteryCharging = 0
var minBatteryMobile = 0
var sort = 0
reader.beginObject()
@ -125,6 +129,7 @@ data class Category(
TIME_WARNINGS -> timeWarnings = reader.nextInt()
MIN_BATTERY_CHARGING -> minBatteryCharging = reader.nextInt()
MIN_BATTERY_MOBILE -> minBatteryMobile = reader.nextInt()
SORT -> sort = reader.nextInt()
else -> reader.skipValue()
}
}
@ -147,7 +152,8 @@ data class Category(
blockAllNotifications = blockAllNotifications,
timeWarnings = timeWarnings,
minBatteryLevelWhileCharging = minBatteryCharging,
minBatteryLevelMobile = minBatteryMobile
minBatteryLevelMobile = minBatteryMobile,
sort = sort
)
}
}
@ -192,6 +198,7 @@ data class Category(
writer.name(TIME_WARNINGS).value(timeWarnings)
writer.name(MIN_BATTERY_CHARGING).value(minBatteryLevelWhileCharging)
writer.name(MIN_BATTERY_MOBILE).value(minBatteryLevelMobile)
writer.name(SORT).value(sort)
writer.endObject()
}

View file

@ -182,7 +182,8 @@ class AppSetupLogic(private val appLogic: AppLogic) {
blockAllNotifications = false,
timeWarnings = 0,
minBatteryLevelWhileCharging = 0,
minBatteryLevelMobile = 0
minBatteryLevelMobile = 0,
sort = 0
))
appLogic.database.category().addCategory(Category(
@ -201,7 +202,8 @@ class AppSetupLogic(private val appLogic: AppLogic) {
blockAllNotifications = false,
timeWarnings = 0,
minBatteryLevelWhileCharging = 0,
minBatteryLevelMobile = 0
minBatteryLevelMobile = 0,
sort = 1
))
// add default allowed apps

View file

@ -312,7 +312,8 @@ object ApplyServerDataStatus {
parentCategoryId = newCategory.parentCategoryId,
timeWarnings = newCategory.timeWarnings,
minBatteryLevelMobile = newCategory.minBatteryLevelMobile,
minBatteryLevelWhileCharging = newCategory.minBatteryLevelCharging
minBatteryLevelWhileCharging = newCategory.minBatteryLevelCharging,
sort = newCategory.sort
))
} else {
val updatedCategory = oldCategory.copy(
@ -327,7 +328,8 @@ object ApplyServerDataStatus {
parentCategoryId = newCategory.parentCategoryId,
timeWarnings = newCategory.timeWarnings,
minBatteryLevelMobile = newCategory.minBatteryLevelMobile,
minBatteryLevelWhileCharging = newCategory.minBatteryLevelCharging
minBatteryLevelWhileCharging = newCategory.minBatteryLevelCharging,
sort = newCategory.sort
)
if (updatedCategory != oldCategory) {

View file

@ -804,6 +804,36 @@ data class UpdateCategoryBatteryLimit(val categoryId: String, val chargingLimit:
writer.endObject()
}
}
data class UpdateCategorySortingAction(val categoryIds: List<String>): ParentAction() {
companion object {
private const val TYPE_VALUE = "UPDATE_CATEGORY_SORTING"
private const val CATEGORY_IDS = "categoryIds"
}
init {
if (categoryIds.isEmpty()) {
throw IllegalArgumentException()
}
if (categoryIds.distinct().size != categoryIds.size) {
throw IllegalArgumentException()
}
categoryIds.forEach { IdGenerator.assertIdValid(it) }
}
override fun serialize(writer: JsonWriter) {
writer.beginObject()
writer.name(TYPE).value(TYPE_VALUE)
writer.name(CATEGORY_IDS).beginArray()
categoryIds.forEach { writer.value(it) }
writer.endArray()
writer.endObject()
}
}
// DeviceDao
data class UpdateDeviceStatusAction(

View file

@ -67,6 +67,7 @@ object ActionParser {
// UpdateEnableActivityLevelBlocking
// UpdateCategoryTimeWarningsAction
// UpdateCategoryBatteryLimit
// UpdateCategorySorting
else -> throw IllegalStateException()
}
}

View file

@ -66,6 +66,8 @@ object LocalDatabaseParentActionDispatcher {
DatabaseValidation.assertChildExists(database, action.childId)
// create the category
val sort = database.category().getNextCategorySortKeyByChildId(action.childId)
database.category().addCategory(Category(
id = action.categoryId,
childId = action.childId,
@ -83,7 +85,8 @@ object LocalDatabaseParentActionDispatcher {
blockAllNotifications = false,
timeWarnings = 0,
minBatteryLevelWhileCharging = 0,
minBatteryLevelMobile = 0
minBatteryLevelMobile = 0,
sort = sort
))
}
is DeleteCategoryAction -> {
@ -557,6 +560,16 @@ object LocalDatabaseParentActionDispatcher {
)
)
}
is UpdateCategorySortingAction -> {
// no validation here:
// - only parents can do it
// - using it over categories which don't belong together destroys the sorting for both,
// but does not cause any trouble
action.categoryIds.forEachIndexed { index, categoryId ->
database.category().updateCategorySorting(categoryId, index)
}
}
}.let { }
database.setTransactionSuccessful()

View file

@ -342,7 +342,8 @@ data class ServerUpdatedCategoryBaseData(
val blockAllNotifications: Boolean,
val timeWarnings: Int,
val minBatteryLevelCharging: Int,
val minBatteryLevelMobile: Int
val minBatteryLevelMobile: Int,
val sort: Int
) {
companion object {
private const val CATEGORY_ID = "categoryId"
@ -358,6 +359,7 @@ data class ServerUpdatedCategoryBaseData(
private const val TIME_WARNINGS = "timeWarnings"
private const val MIN_BATTERY_LEVEL_MOBILE = "mblMobile"
private const val MIN_BATTERY_LEVEL_CHARGING = "mblCharging"
private const val SORT = "sort"
fun parse(reader: JsonReader): ServerUpdatedCategoryBaseData {
var categoryId: String? = null
@ -374,6 +376,7 @@ data class ServerUpdatedCategoryBaseData(
var timeWarnings = 0
var minBatteryLevelCharging = 0
var minBatteryLevelMobile = 0
var sort = 0
reader.beginObject()
while (reader.hasNext()) {
@ -391,6 +394,7 @@ data class ServerUpdatedCategoryBaseData(
TIME_WARNINGS -> timeWarnings = reader.nextInt()
MIN_BATTERY_LEVEL_CHARGING -> minBatteryLevelCharging = reader.nextInt()
MIN_BATTERY_LEVEL_MOBILE -> minBatteryLevelMobile = reader.nextInt()
SORT -> sort = reader.nextInt()
else -> reader.skipValue()
}
}
@ -409,7 +413,8 @@ data class ServerUpdatedCategoryBaseData(
blockAllNotifications = blockAllNotifications,
timeWarnings = timeWarnings,
minBatteryLevelCharging = minBatteryLevelCharging,
minBatteryLevelMobile = minBatteryLevelMobile
minBatteryLevelMobile = minBatteryLevelMobile,
sort = sort
)
}

View file

@ -32,6 +32,7 @@ import io.timelimit.android.R
import io.timelimit.android.async.Threads
import io.timelimit.android.coroutines.executeAndWait
import io.timelimit.android.coroutines.runAsync
import io.timelimit.android.data.extensions.sorted
import io.timelimit.android.data.model.*
import io.timelimit.android.databinding.LockFragmentBinding
import io.timelimit.android.livedata.*
@ -186,7 +187,7 @@ class LockFragment : Fragment() {
} else {
val (user, categoryEntries) = status
categoryEntries.forEach {
categoryEntries.sorted().forEach {
category ->
val button = Button(context)

View file

@ -16,6 +16,7 @@
package io.timelimit.android.ui.manage.child.advanced.manageblocktemporarily
import androidx.lifecycle.LiveData
import io.timelimit.android.data.extensions.sorted
import io.timelimit.android.data.model.Category
import io.timelimit.android.livedata.*
import io.timelimit.android.logic.RealTimeLogic
@ -35,7 +36,7 @@ object ManageBlockTemporarilyItems {
val time = liveDataFromFunction { realTimeLogic.getCurrentTimeInMillis() }
return categories.map { categories ->
categories.map { category ->
categories.sorted().map { category ->
ManageBlockTemporarilyItem(
categoryId = category.id,
categoryTitle = category.title,

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 Jonas Lochmann
* TimeLimit Copyright <C> 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
@ -33,6 +33,7 @@ import io.timelimit.android.data.model.HintsToShow
import io.timelimit.android.extensions.safeNavigate
import io.timelimit.android.logic.AppLogic
import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.sync.actions.UpdateCategorySortingAction
import io.timelimit.android.ui.main.ActivityViewModel
import io.timelimit.android.ui.main.getActivityViewModel
import io.timelimit.android.ui.manage.child.ManageChildFragmentArgs
@ -97,12 +98,74 @@ class ManageChildCategoriesFragment : Fragment() {
if (item == CategoriesIntroductionHeader) {
return makeFlag(ItemTouchHelper.ACTION_STATE_SWIPE, ItemTouchHelper.END or ItemTouchHelper.START) or
makeFlag(ItemTouchHelper.ACTION_STATE_IDLE, ItemTouchHelper.END or ItemTouchHelper.START)
} else if (item is CategoryItem) {
return makeFlag(ItemTouchHelper.ACTION_STATE_DRAG, ItemTouchHelper.UP or ItemTouchHelper.DOWN) or
makeFlag(ItemTouchHelper.ACTION_STATE_IDLE, ItemTouchHelper.UP or ItemTouchHelper.DOWN)
} else {
return 0
}
}
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder) = throw IllegalStateException()
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
val fromIndex = viewHolder.adapterPosition
val toIndex = target.adapterPosition
val categories = adapter.categories!!
if (fromIndex == RecyclerView.NO_POSITION || toIndex == RecyclerView.NO_POSITION) {
return false
}
val fromItem = categories[fromIndex]
val toItem = categories[toIndex]
if (!(fromItem is CategoryItem)) {
throw IllegalStateException()
}
if (!(toItem is CategoryItem)) {
return false
}
if (fromItem.parentCategoryTitle == null) {
if (toItem.parentCategoryTitle != null) {
return false
}
val parentCategories = mutableListOf<CategoryItem>()
categories.forEach { if (it is CategoryItem && it.parentCategoryTitle == null) { parentCategories.add(it) } }
val targetIndex = parentCategories.indexOf(toItem)
val sourceIndex = parentCategories.indexOf(fromItem)
parentCategories.add(targetIndex, parentCategories.removeAt(sourceIndex))
return auth.tryDispatchParentAction(
UpdateCategorySortingAction(
categoryIds = parentCategories.map { it.category.id }
)
)
} else {
if (toItem.category.parentCategoryId != fromItem.category.parentCategoryId) {
return false
}
val childCategories = mutableListOf<CategoryItem>()
categories.forEach { if (it is CategoryItem && it.category.parentCategoryId == fromItem.category.parentCategoryId) { childCategories.add(it) } }
val targetIndex = childCategories.indexOf(toItem)
val sourceIndex = childCategories.indexOf(fromItem)
childCategories.add(targetIndex, childCategories.removeAt(sourceIndex))
return auth.tryDispatchParentAction(
UpdateCategorySortingAction(
categoryIds = childCategories.map { it.category.id }
)
)
}
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val database = logic.database

View file

@ -20,7 +20,7 @@ import android.util.SparseLongArray
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import io.timelimit.android.data.extensions.mapToTimezone
import io.timelimit.android.data.model.Category
import io.timelimit.android.data.extensions.sorted
import io.timelimit.android.data.model.HintsToShow
import io.timelimit.android.date.DateInTimezone
import io.timelimit.android.date.getMinuteOfWeek
@ -30,7 +30,6 @@ import io.timelimit.android.livedata.map
import io.timelimit.android.livedata.switchMap
import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.logic.RemainingTime
import java.util.*
class ManageChildCategoriesModel(application: Application): AndroidViewModel(application) {
private val logic = DefaultAppLogic.with(application)
@ -78,22 +77,7 @@ class ManageChildCategoriesModel(application: Application): AndroidViewModel(app
)
}
private val sortedCategories = categories.map { categories ->
val categoryById = categories.associateBy { it.id }
val sortedCategories = mutableListOf<Category>()
val childCategories = categories.filter { categoryById.containsKey(it.parentCategoryId) }.groupBy { it.parentCategoryId }
categories.filterNot { categoryById.containsKey(it.parentCategoryId) }.sortedBy { it.title.toLowerCase(Locale.getDefault()) }.forEach { category ->
sortedCategories.add(category)
childCategories[category.id]?.sortedBy { it.title.toLowerCase(Locale.getDefault()) }?.let { items ->
sortedCategories.addAll(items)
}
}
sortedCategories.toList()
}
private val sortedCategories = categories.map { it.sorted() }
private val categoryItems = categoryForUnassignedAppsLive.switchMap { categoryForUnassignedApps ->
sortedCategories.switchMap { categories ->