From 0d2dd7bed11dff915b5309e9891a5490ebe30d9c Mon Sep 17 00:00:00 2001 From: Jonas Lochmann Date: Mon, 10 Feb 2020 01:00:00 +0100 Subject: [PATCH] Make the categories sortable --- .../25.json | 828 ++++++++++++++++++ .../io/timelimit/android/data/Migrations.kt | 6 + .../io/timelimit/android/data/RoomDatabase.kt | 5 +- .../timelimit/android/data/dao/CategoryDao.kt | 8 + .../android/data/extensions/Category.kt | 35 + .../timelimit/android/data/model/Category.kt | 11 +- .../timelimit/android/logic/AppSetupLogic.kt | 6 +- .../android/sync/ApplyServerDataStatus.kt | 6 +- .../timelimit/android/sync/actions/Actions.kt | 30 + .../timelimit/android/sync/actions/Parser.kt | 1 + .../sync/actions/dispatch/ParentAction.kt | 15 +- .../android/sync/network/ServerDataStatus.kt | 9 +- .../timelimit/android/ui/lock/LockFragment.kt | 3 +- .../ManageBlockTemporarilyItem.kt | 3 +- .../category/ManageChildCategoriesFragment.kt | 67 +- .../category/ManageChildCategoriesModel.kt | 20 +- 16 files changed, 1020 insertions(+), 33 deletions(-) create mode 100644 app/schemas/io.timelimit.android.data.RoomDatabase/25.json create mode 100644 app/src/main/java/io/timelimit/android/data/extensions/Category.kt diff --git a/app/schemas/io.timelimit.android.data.RoomDatabase/25.json b/app/schemas/io.timelimit.android.data.RoomDatabase/25.json new file mode 100644 index 0000000..70309e9 --- /dev/null +++ b/app/schemas/io.timelimit.android.data.RoomDatabase/25.json @@ -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')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/data/Migrations.kt b/app/src/main/java/io/timelimit/android/data/Migrations.kt index 050945b..c9faaeb 100644 --- a/app/src/main/java/io/timelimit/android/data/Migrations.kt +++ b/app/src/main/java/io/timelimit/android/data/Migrations.kt @@ -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") + } + } } diff --git a/app/src/main/java/io/timelimit/android/data/RoomDatabase.kt b/app/src/main/java/io/timelimit/android/data/RoomDatabase.kt index 32a6a00..0e3a6a3 100644 --- a/app/src/main/java/io/timelimit/android/data/RoomDatabase.kt +++ b/app/src/main/java/io/timelimit/android/data/RoomDatabase.kt @@ -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() } diff --git a/app/src/main/java/io/timelimit/android/data/dao/CategoryDao.kt b/app/src/main/java/io/timelimit/android/data/dao/CategoryDao.kt index d832759..981b539 100644 --- a/app/src/main/java/io/timelimit/android/data/dao/CategoryDao.kt +++ b/app/src/main/java/io/timelimit/android/data/dao/CategoryDao.kt @@ -97,6 +97,14 @@ abstract class CategoryDao { @Query("SELECT * FROM category") abstract fun getAllCategoriesSync(): List + + // 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( diff --git a/app/src/main/java/io/timelimit/android/data/extensions/Category.kt b/app/src/main/java/io/timelimit/android/data/extensions/Category.kt new file mode 100644 index 0000000..ac70f39 --- /dev/null +++ b/app/src/main/java/io/timelimit/android/data/extensions/Category.kt @@ -0,0 +1,35 @@ +/* + * TimeLimit Copyright 2019 - 2020 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package io.timelimit.android.data.extensions + +import io.timelimit.android.data.model.Category + +fun List.sorted(): List { + val categoryIds = this.map { it.id }.toSet() + + val sortedCategories = mutableListOf() + 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() +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/data/model/Category.kt b/app/src/main/java/io/timelimit/android/data/model/Category.kt index e96ceb8..8020097 100644 --- a/app/src/main/java/io/timelimit/android/data/model/Category.kt +++ b/app/src/main/java/io/timelimit/android/data/model/Category.kt @@ -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() } diff --git a/app/src/main/java/io/timelimit/android/logic/AppSetupLogic.kt b/app/src/main/java/io/timelimit/android/logic/AppSetupLogic.kt index d9816ee..dfc615f 100644 --- a/app/src/main/java/io/timelimit/android/logic/AppSetupLogic.kt +++ b/app/src/main/java/io/timelimit/android/logic/AppSetupLogic.kt @@ -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 diff --git a/app/src/main/java/io/timelimit/android/sync/ApplyServerDataStatus.kt b/app/src/main/java/io/timelimit/android/sync/ApplyServerDataStatus.kt index 2d37e6f..a031389 100644 --- a/app/src/main/java/io/timelimit/android/sync/ApplyServerDataStatus.kt +++ b/app/src/main/java/io/timelimit/android/sync/ApplyServerDataStatus.kt @@ -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) { diff --git a/app/src/main/java/io/timelimit/android/sync/actions/Actions.kt b/app/src/main/java/io/timelimit/android/sync/actions/Actions.kt index ed065b5..c818ed6 100644 --- a/app/src/main/java/io/timelimit/android/sync/actions/Actions.kt +++ b/app/src/main/java/io/timelimit/android/sync/actions/Actions.kt @@ -804,6 +804,36 @@ data class UpdateCategoryBatteryLimit(val categoryId: String, val chargingLimit: writer.endObject() } } +data class UpdateCategorySortingAction(val categoryIds: List): 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( diff --git a/app/src/main/java/io/timelimit/android/sync/actions/Parser.kt b/app/src/main/java/io/timelimit/android/sync/actions/Parser.kt index 61abb75..64bc5a6 100644 --- a/app/src/main/java/io/timelimit/android/sync/actions/Parser.kt +++ b/app/src/main/java/io/timelimit/android/sync/actions/Parser.kt @@ -67,6 +67,7 @@ object ActionParser { // UpdateEnableActivityLevelBlocking // UpdateCategoryTimeWarningsAction // UpdateCategoryBatteryLimit + // UpdateCategorySorting else -> throw IllegalStateException() } } diff --git a/app/src/main/java/io/timelimit/android/sync/actions/dispatch/ParentAction.kt b/app/src/main/java/io/timelimit/android/sync/actions/dispatch/ParentAction.kt index ac1e1c4..c5611fd 100644 --- a/app/src/main/java/io/timelimit/android/sync/actions/dispatch/ParentAction.kt +++ b/app/src/main/java/io/timelimit/android/sync/actions/dispatch/ParentAction.kt @@ -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() diff --git a/app/src/main/java/io/timelimit/android/sync/network/ServerDataStatus.kt b/app/src/main/java/io/timelimit/android/sync/network/ServerDataStatus.kt index 5ca0a9f..9ca820d 100644 --- a/app/src/main/java/io/timelimit/android/sync/network/ServerDataStatus.kt +++ b/app/src/main/java/io/timelimit/android/sync/network/ServerDataStatus.kt @@ -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 ) } diff --git a/app/src/main/java/io/timelimit/android/ui/lock/LockFragment.kt b/app/src/main/java/io/timelimit/android/ui/lock/LockFragment.kt index f6975ee..e98eb9d 100644 --- a/app/src/main/java/io/timelimit/android/ui/lock/LockFragment.kt +++ b/app/src/main/java/io/timelimit/android/ui/lock/LockFragment.kt @@ -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) diff --git a/app/src/main/java/io/timelimit/android/ui/manage/child/advanced/manageblocktemporarily/ManageBlockTemporarilyItem.kt b/app/src/main/java/io/timelimit/android/ui/manage/child/advanced/manageblocktemporarily/ManageBlockTemporarilyItem.kt index 7c4f145..ee836a9 100644 --- a/app/src/main/java/io/timelimit/android/ui/manage/child/advanced/manageblocktemporarily/ManageBlockTemporarilyItem.kt +++ b/app/src/main/java/io/timelimit/android/ui/manage/child/advanced/manageblocktemporarily/ManageBlockTemporarilyItem.kt @@ -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, diff --git a/app/src/main/java/io/timelimit/android/ui/manage/child/category/ManageChildCategoriesFragment.kt b/app/src/main/java/io/timelimit/android/ui/manage/child/category/ManageChildCategoriesFragment.kt index c3b4b3f..b6f5082 100644 --- a/app/src/main/java/io/timelimit/android/ui/manage/child/category/ManageChildCategoriesFragment.kt +++ b/app/src/main/java/io/timelimit/android/ui/manage/child/category/ManageChildCategoriesFragment.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 Jonas Lochmann + * TimeLimit Copyright 2019 - 2020 Jonas Lochmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -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() + + 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() + + 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 diff --git a/app/src/main/java/io/timelimit/android/ui/manage/child/category/ManageChildCategoriesModel.kt b/app/src/main/java/io/timelimit/android/ui/manage/child/category/ManageChildCategoriesModel.kt index de1f9f2..ae75b67 100644 --- a/app/src/main/java/io/timelimit/android/ui/manage/child/category/ManageChildCategoriesModel.kt +++ b/app/src/main/java/io/timelimit/android/ui/manage/child/category/ManageChildCategoriesModel.kt @@ -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() - 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 ->