From 917f134d77acb7dd2d158a2f801b089c44c0cd9f Mon Sep 17 00:00:00 2001 From: Jonas L Date: Mon, 28 Jan 2019 16:35:56 +0100 Subject: [PATCH] Add parent category support --- .../3.json | 443 ++++++++++++++++++ .../android/data/DatabaseMigrations.kt | 6 + .../io/timelimit/android/data/RoomDatabase.kt | 5 +- .../timelimit/android/data/dao/CategoryDao.kt | 3 + .../timelimit/android/data/model/Category.kt | 12 +- .../timelimit/android/logic/AppSetupLogic.kt | 6 +- .../android/logic/BackgroundTaskLogic.kt | 61 ++- .../timelimit/android/logic/BlockingReason.kt | 24 +- .../timelimit/android/logic/RemainingTime.kt | 11 + .../logic/UsedTimeItemBatchUpdateHelper.kt | 49 +- .../timelimit/android/sync/actions/Actions.kt | 11 + .../sync/actions/dispatch/AppLogicAction.kt | 62 ++- .../sync/actions/dispatch/ParentAction.kt | 41 +- .../settings/CategorySettingsFragment.kt | 10 + .../category/settings/ParentCategoryView.kt | 53 +++ .../SelectParentCategoryDialogFragment.kt | 151 ++++++ .../AssignAllAppsCategoryDialogFragment.kt | 8 +- .../assign/AssignAppCategoryDialogFragment.kt | 8 +- .../ui/manage/child/category/Adapter.kt | 1 + .../android/ui/manage/child/category/Items.kt | 3 +- .../category/ManageChildCategoriesModel.kt | 5 +- ...og.xml => bottom_sheet_selection_list.xml} | 6 +- .../main/res/layout/category_rich_card.xml | 12 + .../res/layout/fragment_category_settings.xml | 3 + .../res/layout/manage_parent_category.xml | 81 ++++ .../values-de/strings-category-settings.xml | 7 + .../res/values-de/strings-manage-child.xml | 1 + .../res/values/strings-category-settings.xml | 7 + .../main/res/values/strings-manage-child.xml | 1 + 29 files changed, 1009 insertions(+), 82 deletions(-) create mode 100644 app/schemas/io.timelimit.android.data.RoomDatabase/3.json create mode 100644 app/src/main/java/io/timelimit/android/ui/manage/category/settings/ParentCategoryView.kt create mode 100644 app/src/main/java/io/timelimit/android/ui/manage/category/settings/SelectParentCategoryDialogFragment.kt rename app/src/main/res/layout/{assign_app_dialog.xml => bottom_sheet_selection_list.xml} (93%) create mode 100644 app/src/main/res/layout/manage_parent_category.xml diff --git a/app/schemas/io.timelimit.android.data.RoomDatabase/3.json b/app/schemas/io.timelimit.android.data.RoomDatabase/3.json new file mode 100644 index 0000000..b37e115 --- /dev/null +++ b/app/schemas/io.timelimit.android.data.RoomDatabase/3.json @@ -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\")" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/data/DatabaseMigrations.kt b/app/src/main/java/io/timelimit/android/data/DatabaseMigrations.kt index 0fe7110..51dd9e5 100644 --- a/app/src/main/java/io/timelimit/android/data/DatabaseMigrations.kt +++ b/app/src/main/java/io/timelimit/android/data/DatabaseMigrations.kt @@ -9,4 +9,10 @@ object DatabaseMigrations { 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 \"\"") + } + } } \ No newline at end of file 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 134095b..9e8508a 100644 --- a/app/src/main/java/io/timelimit/android/data/RoomDatabase.kt +++ b/app/src/main/java/io/timelimit/android/data/RoomDatabase.kt @@ -31,7 +31,7 @@ import io.timelimit.android.data.model.* TimeLimitRule::class, ConfigurationItem::class, TemporarilyAllowedApp::class -], version = 2) +], version = 3) abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database { companion object { private val lock = Object() @@ -67,7 +67,8 @@ abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database .setJournalMode(JournalMode.TRUNCATE) .fallbackToDestructiveMigration() .addMigrations( - DatabaseMigrations.MIGRATE_TO_V2 + DatabaseMigrations.MIGRATE_TO_V2, + DatabaseMigrations.MIGRATE_TO_V3 ) .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 8ac4fa8..86b804b 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 @@ -65,6 +65,9 @@ abstract class CategoryDao { @Query("SELECT id, child_id, temporarily_blocked FROM category") abstract fun getAllCategoriesShortInfo(): LiveData> + + @Query("UPDATE category SET parent_category_id = :parentCategoryId WHERE id = :categoryId") + abstract fun updateParentCategory(categoryId: String, parentCategoryId: String) } data class CategoryShortInfo( 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 95771dc..95a7083 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 @@ -42,7 +42,9 @@ data class Category( @ColumnInfo(name = "extra_time") val extraTimeInMillis: Long, @ColumnInfo(name = "temporarily_blocked") - val temporarilyBlocked: Boolean + val temporarilyBlocked: Boolean, + @ColumnInfo(name = "parent_category_id") + val parentCategoryId: String ): JsonSerializable { companion object { 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 EXTRA_TIME_IN_MILLIS = "extraTimeInMillis" private const val TEMPORARILY_BLOCKED = "temporarilyBlocked" + private const val PARENT_CATEGORY_ID = "parentCategoryId" fun parse(reader: JsonReader): Category { var id: String? = null @@ -62,6 +65,8 @@ data class Category( var blockedMinutesInWeek: ImmutableBitmask? = null var extraTimeInMillis: Long? = null var temporarilyBlocked: Boolean? = null + // this field was added later so it has got a default value + var parentCategoryId = "" reader.beginObject() @@ -73,6 +78,7 @@ data class Category( BLOCKED_MINUTES_IN_WEEK -> blockedMinutesInWeek = ImmutableBitmaskJson.parse(reader.nextString(), BLOCKED_MINUTES_IN_WEEK_LENGTH) EXTRA_TIME_IN_MILLIS -> extraTimeInMillis = reader.nextLong() TEMPORARILY_BLOCKED -> temporarilyBlocked = reader.nextBoolean() + PARENT_CATEGORY_ID -> parentCategoryId = reader.nextString() else -> reader.skipValue() } } @@ -85,7 +91,8 @@ data class Category( title = title!!, blockedMinutesInWeek = blockedMinutesInWeek!!, 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(EXTRA_TIME_IN_MILLIS).value(extraTimeInMillis) writer.name(TEMPORARILY_BLOCKED).value(temporarilyBlocked) + writer.name(PARENT_CATEGORY_ID).value(parentCategoryId) 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 7fa0746..13995b5 100644 --- a/app/src/main/java/io/timelimit/android/logic/AppSetupLogic.kt +++ b/app/src/main/java/io/timelimit/android/logic/AppSetupLogic.kt @@ -136,7 +136,8 @@ class AppSetupLogic(private val appLogic: AppLogic) { title = defaultCategories.allowedAppsTitle, blockedMinutesInWeek = ImmutableBitmask((BitSet())), extraTimeInMillis = 0, - temporarilyBlocked = false + temporarilyBlocked = false, + parentCategoryId = "" )) appLogic.database.category().addCategory(Category( @@ -145,7 +146,8 @@ class AppSetupLogic(private val appLogic: AppLogic) { title = defaultCategories.allowedGamesTitle, blockedMinutesInWeek = defaultCategories.allowedGamesBlockedTimes, extraTimeInMillis = 0, - temporarilyBlocked = false + temporarilyBlocked = false, + parentCategoryId = "" )) // add default allowed apps diff --git a/app/src/main/java/io/timelimit/android/logic/BackgroundTaskLogic.kt b/app/src/main/java/io/timelimit/android/logic/BackgroundTaskLogic.kt index d82247e..e9aea62 100644 --- a/app/src/main/java/io/timelimit/android/logic/BackgroundTaskLogic.kt +++ b/app/src/main/java/io/timelimit/android/logic/BackgroundTaskLogic.kt @@ -186,6 +186,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) { val appCategory = appCategories.get(Pair(foregroundAppPackageName, categories.map { it.id })).waitForNullableValue() val category = categories.find { it.id == appCategory?.categoryId } ?: categories.find { it.id == deviceUserEntry.categoryForNotAssignedApps } + val parentCategory = categories.find { it.id == category?.parentCategoryId } if (category == null) { usedTimeUpdateHelper?.commit(appLogic) @@ -196,7 +197,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) { )) appLogic.platformIntegration.setSuspendedApps(listOf(foregroundAppPackageName), true) appLogic.platformIntegration.showAppLockScreen(foregroundAppPackageName) - } else if (category.temporarilyBlocked) { + } else if (category.temporarilyBlocked or (parentCategory?.temporarilyBlocked == true)) { usedTimeUpdateHelper?.commit(appLogic) appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage( @@ -218,7 +219,8 @@ class BackgroundTaskLogic(val appLogic: AppLogic) { )) } else if ( // check blocked time areas - (category.blockedMinutesInWeek.read(minuteOfWeek)) + (category.blockedMinutesInWeek.read(minuteOfWeek)) or + (parentCategory?.blockedMinutesInWeek?.read(minuteOfWeek) == true) ) { usedTimeUpdateHelper?.commit(appLogic) @@ -230,8 +232,11 @@ class BackgroundTaskLogic(val appLogic: AppLogic) { } else { // check time limits 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 usedTimeUpdateHelper?.commit(appLogic) @@ -241,33 +246,61 @@ class BackgroundTaskLogic(val appLogic: AppLogic) { )) } else { 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( date = nowDate, - categoryId = category.id, + childCategoryId = category.id, + parentCategoryId = parentCategory?.id, oldInstance = usedTimeUpdateHelper, - usedTimeItemForDay = usedTimes.get(nowDate.dayOfWeek), + usedTimeItemForDayChild = usedTimes.get(nowDate.dayOfWeek), + usedTimeItemForDayParent = parentUsedTimes.get(nowDate.dayOfWeek), logic = appLogic ) usedTimeUpdateHelper = newUsedTimeItemBatchUpdateHelper - val usedTimesSparseArray = SparseLongArray() + fun buildUsedTimesSparseArray(items: SparseArray, isParentCategory: Boolean): SparseLongArray { + val result = SparseLongArray() - for (i in 0..6) { - val usedTimesItem = usedTimes[i]?.usedMillis + for (i in 0..6) { + val usedTimesItem = items[i]?.usedMillis - if (newUsedTimeItemBatchUpdateHelper.date.dayOfWeek == i) { - usedTimesSparseArray.put(i, newUsedTimeItemBatchUpdateHelper.getTotalUsedTime()) - } else { - usedTimesSparseArray.put(i, (if (usedTimesItem != null) usedTimesItem else 0)) + if (newUsedTimeItemBatchUpdateHelper.date.dayOfWeek == i) { + result.put( + i, + if (isParentCategory) + newUsedTimeItemBatchUpdateHelper.getTotalUsedTimeParent() + else + newUsedTimeItemBatchUpdateHelper.getTotalUsedTimeChild() + ) + } else { + result.put(i, usedTimesItem ?: 0) + } } + + return result } - val remaining = RemainingTime.getRemainingTime( - nowDate.dayOfWeek, usedTimesSparseArray, rules, + val remainingChild = RemainingTime.getRemainingTime( + nowDate.dayOfWeek, + buildUsedTimesSparseArray(usedTimes, isParentCategory = false), + rules, 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) { // unlimited diff --git a/app/src/main/java/io/timelimit/android/logic/BlockingReason.kt b/app/src/main/java/io/timelimit/android/logic/BlockingReason.kt index 2ff00cd..9e61ef8 100644 --- a/app/src/main/java/io/timelimit/android/logic/BlockingReason.kt +++ b/app/src/main/java/io/timelimit/android/logic/BlockingReason.kt @@ -129,22 +129,24 @@ class BlockingReasonUtil(private val appLogic: AppLogic) { if (categoryEntry2 == null) { liveDataFromValue(BlockingReason.NotPartOfAnCategory) } else { - getBlockingReasonStep4Point5(categoryEntry2, child, timeZone) + getBlockingReasonStep4Point5(categoryEntry2, child, timeZone, false) } } - } else if (categoryEntry.temporarilyBlocked) { - liveDataFromValue(BlockingReason.TemporarilyBlocked) } else { - getBlockingReasonStep4Point5(categoryEntry, child, timeZone) + getBlockingReasonStep4Point5(categoryEntry, child, timeZone, false) } } } - private fun getBlockingReasonStep4Point5(category: Category, child: User, timeZone: TimeZone): LiveData { + private fun getBlockingReasonStep4Point5(category: Category, child: User, timeZone: TimeZone, isParentCategory: Boolean): LiveData { if (BuildConfig.DEBUG) { Log.d(LOG_TAG, "step 4.5") } + if (category.temporarilyBlocked) { + return liveDataFromValue(BlockingReason.TemporarilyBlocked) + } + val areLimitsDisabled: LiveData if (child.disableLimitsUntil == 0L) { @@ -163,6 +165,18 @@ class BlockingReasonUtil(private val appLogic: AppLogic) { } else { 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) + } } } diff --git a/app/src/main/java/io/timelimit/android/logic/RemainingTime.kt b/app/src/main/java/io/timelimit/android/logic/RemainingTime.kt index e796d6a..35c1ef3 100644 --- a/app/src/main/java/io/timelimit/android/logic/RemainingTime.kt +++ b/app/src/main/java/io/timelimit/android/logic/RemainingTime.kt @@ -30,6 +30,17 @@ data class RemainingTime(val includingExtraTime: Long, val default: Long) { } 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): List { return rules.filter { (it.dayMask.toInt() and (1 shl dayOfWeek)) != 0 } } diff --git a/app/src/main/java/io/timelimit/android/logic/UsedTimeItemBatchUpdateHelper.kt b/app/src/main/java/io/timelimit/android/logic/UsedTimeItemBatchUpdateHelper.kt index dbe4834..93a32e8 100644 --- a/app/src/main/java/io/timelimit/android/logic/UsedTimeItemBatchUpdateHelper.kt +++ b/app/src/main/java/io/timelimit/android/logic/UsedTimeItemBatchUpdateHelper.kt @@ -22,18 +22,35 @@ import io.timelimit.android.livedata.waitForNullableValue import io.timelimit.android.sync.actions.AddUsedTimeAction 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 { suspend fun eventuallyUpdateInstance( date: DateInTimezone, - categoryId: String, + childCategoryId: String, + parentCategoryId: String?, oldInstance: UsedTimeItemBatchUpdateHelper?, - usedTimeItemForDay: UsedTimeItem?, + usedTimeItemForDayChild: UsedTimeItem?, + usedTimeItemForDayParent: UsedTimeItem?, logic: AppLogic ): UsedTimeItemBatchUpdateHelper { - if (oldInstance != null && oldInstance.date == date && oldInstance.categoryId == categoryId) { - if (oldInstance.cachedItem != usedTimeItemForDay) { - oldInstance.cachedItem = usedTimeItemForDay + if ( + oldInstance != null && + 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 @@ -44,8 +61,10 @@ class UsedTimeItemBatchUpdateHelper(val date: DateInTimezone, val categoryId: St return UsedTimeItemBatchUpdateHelper( date = date, - categoryId = categoryId, - cachedItem = usedTimeItemForDay + childCategoryId = childCategoryId, + parentCategoryId = parentCategoryId, + cachedItemChild = usedTimeItemForDayChild, + cachedItemParent = usedTimeItemForDayParent ) } } @@ -66,18 +85,18 @@ class UsedTimeItemBatchUpdateHelper(val date: DateInTimezone, val categoryId: St } } - fun getTotalUsedTime(): Long { - val cachedItem = cachedItem - - return (if (cachedItem == null) 0 else cachedItem.usedMillis) + timeToAdd - } + fun getTotalUsedTimeChild(): Long = (cachedItemChild?.usedMillis ?: 0) + timeToAdd + fun getTotalUsedTimeParent(): Long = (cachedItemParent?.usedMillis ?: 0) + timeToAdd fun getCachedExtraTimeToSubtract(): Int { return extraTimeToSubtract } 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) { @@ -86,7 +105,7 @@ class UsedTimeItemBatchUpdateHelper(val date: DateInTimezone, val categoryId: St } else { ApplyActionUtil.applyAppLogicAction( AddUsedTimeAction( - categoryId = categoryId, + categoryId = childCategoryId, timeToAdd = timeToAdd, dayOfEpoch = date.dayOfEpoch, extraTimeToSubtract = extraTimeToSubtract 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 a8ad50c..aec9562 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 @@ -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 diff --git a/app/src/main/java/io/timelimit/android/sync/actions/dispatch/AppLogicAction.kt b/app/src/main/java/io/timelimit/android/sync/actions/dispatch/AppLogicAction.kt index 44c9627..36d6bc7 100644 --- a/app/src/main/java/io/timelimit/android/sync/actions/dispatch/AppLogicAction.kt +++ b/app/src/main/java/io/timelimit/android/sync/actions/dispatch/AppLogicAction.kt @@ -33,34 +33,46 @@ object LocalDatabaseAppLogicActionDispatcher { try { when(action) { 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 - val updatedRows = database.usedTimes().addUsedTime( - categoryId = action.categoryId, - 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 + fun handleAddUsedTime(categoryId: String) { + // try to update + val updatedRows = database.usedTimes().addUsedTime( + categoryId = categoryId, + timeToAdd = action.timeToAdd, + dayOfEpoch = action.dayOfEpoch ) - } 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 -> { database.app().addAppsSync( 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 f6f1f3b..4be5bd8 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 @@ -73,7 +73,8 @@ object LocalDatabaseParentActionDispatcher { // nothing blocked by default blockedMinutesInWeek = ImmutableBitmask(BitSet()), extraTimeInMillis = 0, - temporarilyBlocked = false + temporarilyBlocked = false, + parentCategoryId = "" )) } is DeleteCategoryAction -> { @@ -104,13 +105,24 @@ object LocalDatabaseParentActionDispatcher { database.category().updateCategoryExtraTime(action.categoryId, action.newExtraTime) } is IncrementCategoryExtraTimeAction -> { - DatabaseValidation.assertCategoryExists(database, action.categoryId) - if (action.addedExtraTime < 0) { 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) + + 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 -> { DatabaseValidation.assertCategoryExists(database, action.categoryId) @@ -281,6 +293,29 @@ object LocalDatabaseParentActionDispatcher { 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 { } database.setTransactionSuccessful() diff --git a/app/src/main/java/io/timelimit/android/ui/manage/category/settings/CategorySettingsFragment.kt b/app/src/main/java/io/timelimit/android/ui/manage/category/settings/CategorySettingsFragment.kt index 58c5803..7e71265 100644 --- a/app/src/main/java/io/timelimit/android/ui/manage/category/settings/CategorySettingsFragment.kt +++ b/app/src/main/java/io/timelimit/android/ui/manage/category/settings/CategorySettingsFragment.kt @@ -58,6 +58,16 @@ class CategorySettingsFragment : Fragment() { 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.editCategoryTitleGo.setOnClickListener { renameCategory() } diff --git a/app/src/main/java/io/timelimit/android/ui/manage/category/settings/ParentCategoryView.kt b/app/src/main/java/io/timelimit/android/ui/manage/category/settings/ParentCategoryView.kt new file mode 100644 index 0000000..c8c15e6 --- /dev/null +++ b/app/src/main/java/io/timelimit/android/ui/manage/category/settings/ParentCategoryView.kt @@ -0,0 +1,53 @@ +/* + * Open TimeLimit Copyright 2019 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package io.timelimit.android.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) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/manage/category/settings/SelectParentCategoryDialogFragment.kt b/app/src/main/java/io/timelimit/android/ui/manage/category/settings/SelectParentCategoryDialogFragment.kt new file mode 100644 index 0000000..3fc5433 --- /dev/null +++ b/app/src/main/java/io/timelimit/android/ui/manage/category/settings/SelectParentCategoryDialogFragment.kt @@ -0,0 +1,151 @@ +/* + * Open TimeLimit Copyright 2019 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package io.timelimit.android.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> 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) +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/manage/child/apps/assign/AssignAllAppsCategoryDialogFragment.kt b/app/src/main/java/io/timelimit/android/ui/manage/child/apps/assign/AssignAllAppsCategoryDialogFragment.kt index add4ba1..2676f75 100644 --- a/app/src/main/java/io/timelimit/android/ui/manage/child/apps/assign/AssignAllAppsCategoryDialogFragment.kt +++ b/app/src/main/java/io/timelimit/android/ui/manage/child/apps/assign/AssignAllAppsCategoryDialogFragment.kt @@ -28,7 +28,7 @@ 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.AssignAppDialogBinding +import io.timelimit.android.databinding.BottomSheetSelectionListBinding import io.timelimit.android.extensions.showSafe import io.timelimit.android.logic.AppLogic import io.timelimit.android.logic.DefaultAppLogic @@ -73,10 +73,10 @@ class AssignAllAppsCategoryDialogFragment: BottomSheetDialogFragment() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - val binding = AssignAppDialogBinding.inflate(inflater, container, false) - val list = binding.categoryList + val binding = BottomSheetSelectionListBinding.inflate(inflater, container, false) + 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 -> fun buildRow(): CheckedTextView = LayoutInflater.from(context!!).inflate( diff --git a/app/src/main/java/io/timelimit/android/ui/manage/child/apps/assign/AssignAppCategoryDialogFragment.kt b/app/src/main/java/io/timelimit/android/ui/manage/child/apps/assign/AssignAppCategoryDialogFragment.kt index e2cc60a..200d744 100644 --- a/app/src/main/java/io/timelimit/android/ui/manage/child/apps/assign/AssignAppCategoryDialogFragment.kt +++ b/app/src/main/java/io/timelimit/android/ui/manage/child/apps/assign/AssignAppCategoryDialogFragment.kt @@ -30,7 +30,7 @@ import io.timelimit.android.data.model.App import io.timelimit.android.data.model.Category import io.timelimit.android.data.model.CategoryApp 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.livedata.map import io.timelimit.android.livedata.switchMap @@ -96,9 +96,9 @@ class AssignAppCategoryDialogFragment: BottomSheetDialogFragment() { } 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 -> categoryAppEntry.map { appCategory -> @@ -157,7 +157,7 @@ class AssignAppCategoryDialogFragment: BottomSheetDialogFragment() { }) matchingAppEntries.observe(this, Observer { - binding.appTitle = it.firstOrNull()?.title + binding.title = it.firstOrNull()?.title }) return binding.root diff --git a/app/src/main/java/io/timelimit/android/ui/manage/child/category/Adapter.kt b/app/src/main/java/io/timelimit/android/ui/manage/child/category/Adapter.kt index eacae4c..9ad828d 100644 --- a/app/src/main/java/io/timelimit/android/ui/manage/child/category/Adapter.kt +++ b/app/src/main/java/io/timelimit/android/ui/manage/child/category/Adapter.kt @@ -125,6 +125,7 @@ class Adapter: RecyclerView.Adapter() { null } binding.usedForAppsWithoutCategory = item.usedForNotAssignedApps + binding.parentCategoryTitle = item.parentCategoryTitle binding.card.setOnClickListener { handlers?.onCategoryClicked(item.category) } diff --git a/app/src/main/java/io/timelimit/android/ui/manage/child/category/Items.kt b/app/src/main/java/io/timelimit/android/ui/manage/child/category/Items.kt index cc73604..4718e3a 100644 --- a/app/src/main/java/io/timelimit/android/ui/manage/child/category/Items.kt +++ b/app/src/main/java/io/timelimit/android/ui/manage/child/category/Items.kt @@ -26,5 +26,6 @@ data class CategoryItem( val isBlockedTimeNow: Boolean, val remainingTimeToday: Long?, val usedTimeToday: Long, - val usedForNotAssignedApps: Boolean + val usedForNotAssignedApps: Boolean, + val parentCategoryTitle: String? ): ManageChildCategoriesListItem() 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 d4c18bc..7b5e555 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 @@ -87,10 +87,10 @@ class ManageChildCategoriesModel(application: Application): AndroidViewModel(app val firstDayOfWeek = childDate.dayOfEpoch - childDate.dayOfWeek categories.map { category -> - val rules = rulesByCategoryId[category.id] ?: emptyList() val usedTimeItemsForCategory = usedTimesByCategory[category.id] ?: emptyList() + val parentCategory = categories.find { it.id == category.parentCategoryId } CategoryItem( category = category, @@ -110,7 +110,8 @@ class ManageChildCategoriesModel(application: Application): AndroidViewModel(app )?.includingExtraTime, usedTimeToday = usedTimeItemsForCategory.find { item -> item.dayOfEpoch == childDate.dayOfEpoch }?.usedMillis ?: 0, - usedForNotAssignedApps = categoryForUnassignedApps == category.id + usedForNotAssignedApps = categoryForUnassignedApps == category.id, + parentCategoryTitle = parentCategory?.title ) } } diff --git a/app/src/main/res/layout/assign_app_dialog.xml b/app/src/main/res/layout/bottom_sheet_selection_list.xml similarity index 93% rename from app/src/main/res/layout/assign_app_dialog.xml rename to app/src/main/res/layout/bottom_sheet_selection_list.xml index 142a590..e6c6c60 100644 --- a/app/src/main/res/layout/assign_app_dialog.xml +++ b/app/src/main/res/layout/bottom_sheet_selection_list.xml @@ -19,7 +19,7 @@ @@ -31,13 +31,13 @@ diff --git a/app/src/main/res/layout/category_rich_card.xml b/app/src/main/res/layout/category_rich_card.xml index 029fa95..e20852c 100644 --- a/app/src/main/res/layout/category_rich_card.xml +++ b/app/src/main/res/layout/category_rich_card.xml @@ -39,6 +39,10 @@ name="usedForAppsWithoutCategory" type="boolean" /> + + @@ -71,6 +75,14 @@ android:layout_width="match_parent" android:layout_height="wrap_content" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + +