mirror of
https://codeberg.org/timelimit/opentimelimit-android.git
synced 2025-10-05 02:39:34 +02:00
Add parent category support
This commit is contained in:
parent
b7bf364aa6
commit
917f134d77
29 changed files with 1009 additions and 82 deletions
|
@ -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 \"\"")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -65,6 +65,9 @@ abstract class CategoryDao {
|
|||
|
||||
@Query("SELECT id, child_id, temporarily_blocked FROM category")
|
||||
abstract fun getAllCategoriesShortInfo(): LiveData<List<CategoryShortInfo>>
|
||||
|
||||
@Query("UPDATE category SET parent_category_id = :parentCategoryId WHERE id = :categoryId")
|
||||
abstract fun updateParentCategory(categoryId: String, parentCategoryId: String)
|
||||
}
|
||||
|
||||
data class CategoryShortInfo(
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<UsedTimeItem>, 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
|
||||
|
||||
|
|
|
@ -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<BlockingReason> {
|
||||
private fun getBlockingReasonStep4Point5(category: Category, child: User, timeZone: TimeZone, isParentCategory: Boolean): LiveData<BlockingReason> {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "step 4.5")
|
||||
}
|
||||
|
||||
if (category.temporarilyBlocked) {
|
||||
return liveDataFromValue(BlockingReason.TemporarilyBlocked)
|
||||
}
|
||||
|
||||
val areLimitsDisabled: LiveData<Boolean>
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<TimeLimitRule>): List<TimeLimitRule> {
|
||||
return rules.filter { (it.dayMask.toInt() and (1 shl dayOfWeek)) != 0 }
|
||||
}
|
||||
|
|
|
@ -22,18 +22,35 @@ import io.timelimit.android.livedata.waitForNullableValue
|
|||
import io.timelimit.android.sync.actions.AddUsedTimeAction
|
||||
import io.timelimit.android.sync.actions.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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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() }
|
||||
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.ui.manage.category.settings
|
||||
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.Observer
|
||||
import io.timelimit.android.data.Database
|
||||
import io.timelimit.android.databinding.ManageParentCategoryBinding
|
||||
import io.timelimit.android.ui.main.ActivityViewModel
|
||||
|
||||
object ParentCategoryView {
|
||||
fun bind(
|
||||
binding: ManageParentCategoryBinding,
|
||||
auth: ActivityViewModel,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
categoryId: String,
|
||||
childId: String,
|
||||
database: Database,
|
||||
fragmentManager: FragmentManager
|
||||
) {
|
||||
database.category().getCategoriesByChildId(childId).observe(lifecycleOwner, Observer { categories ->
|
||||
val ownCategory = categories.find { it.id == categoryId }
|
||||
val parentCategory = categories.find { it.id == ownCategory?.parentCategoryId }
|
||||
val hasSubCategories = categories.find { it.parentCategoryId == categoryId } != null
|
||||
|
||||
binding.parentCategoryTitle = parentCategory?.title
|
||||
binding.isParentCategory = hasSubCategories
|
||||
})
|
||||
|
||||
binding.selectParentButton.setOnClickListener {
|
||||
if (auth.requestAuthenticationOrReturnTrue()) {
|
||||
SelectParentCategoryDialogFragment.newInstance(
|
||||
childId = childId,
|
||||
categoryId = categoryId
|
||||
).show(fragmentManager)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.ui.manage.category.settings
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.CheckedTextView
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.data.Database
|
||||
import io.timelimit.android.data.model.Category
|
||||
import io.timelimit.android.data.model.UserType
|
||||
import io.timelimit.android.databinding.BottomSheetSelectionListBinding
|
||||
import io.timelimit.android.extensions.showSafe
|
||||
import io.timelimit.android.logic.AppLogic
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
import io.timelimit.android.sync.actions.SetParentCategory
|
||||
import io.timelimit.android.ui.main.ActivityViewModel
|
||||
import io.timelimit.android.ui.main.ActivityViewModelHolder
|
||||
|
||||
class SelectParentCategoryDialogFragment: BottomSheetDialogFragment() {
|
||||
companion object {
|
||||
private const val DIALOG_TAG = "SelectParentCategoryDialogFragment"
|
||||
private const val CATEGORY_ID = "categoryId"
|
||||
private const val CHILD_ID = "childId"
|
||||
|
||||
fun newInstance(childId: String, categoryId: String) = SelectParentCategoryDialogFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putString(CHILD_ID, childId)
|
||||
putString(CATEGORY_ID, categoryId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val childId: String by lazy { arguments!!.getString(CHILD_ID) }
|
||||
val categoryId: String by lazy { arguments!!.getString(CATEGORY_ID) }
|
||||
|
||||
val logic: AppLogic by lazy { DefaultAppLogic.with(context!!) }
|
||||
val database: Database by lazy { logic.database }
|
||||
val auth: ActivityViewModel by lazy { (activity as ActivityViewModelHolder).getActivityViewModel() }
|
||||
|
||||
val childCategoryEntries: LiveData<List<Category>> by lazy {
|
||||
database.category().getCategoriesByChildId(childId)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
childCategoryEntries.observe(this, Observer { categories ->
|
||||
val ownCategory = categories.find { it.id == categoryId }
|
||||
val hasSubCategories = categories.find { it.parentCategoryId == categoryId } != null
|
||||
|
||||
if (ownCategory == null || hasSubCategories) {
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
})
|
||||
|
||||
auth.authenticatedUser.observe(this, Observer {
|
||||
if (it?.second?.type != UserType.Parent) {
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val binding = BottomSheetSelectionListBinding.inflate(inflater, container, false)
|
||||
|
||||
binding.title = getString(R.string.category_settings_parent_category_title)
|
||||
|
||||
val list = binding.list
|
||||
|
||||
childCategoryEntries.observe(this, Observer { categories ->
|
||||
list.removeAllViews()
|
||||
|
||||
val ownCategory = categories.find { it.id == categoryId }
|
||||
val ownParentCategory = categories.find { it.id == ownCategory?.parentCategoryId }
|
||||
|
||||
fun buildRow(): CheckedTextView = LayoutInflater.from(context!!).inflate(
|
||||
android.R.layout.simple_list_item_single_choice,
|
||||
list,
|
||||
false
|
||||
) as CheckedTextView
|
||||
|
||||
categories.forEach { category ->
|
||||
if (category.id != categoryId) {
|
||||
val row = buildRow()
|
||||
|
||||
row.text = category.title
|
||||
row.isChecked = category.id == ownCategory?.parentCategoryId
|
||||
row.isEnabled = categories.find { it.id == category.parentCategoryId } == null
|
||||
row.setOnClickListener {
|
||||
if (!row.isChecked) {
|
||||
auth.tryDispatchParentAction(
|
||||
SetParentCategory(
|
||||
categoryId = categoryId,
|
||||
parentCategory = category.id
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
dismiss()
|
||||
}
|
||||
|
||||
list.addView(row)
|
||||
}
|
||||
}
|
||||
|
||||
buildRow().let { row ->
|
||||
row.setText(R.string.category_settings_parent_category_none)
|
||||
row.isChecked = ownParentCategory == null
|
||||
|
||||
row.setOnClickListener {
|
||||
if (!row.isChecked) {
|
||||
auth.tryDispatchParentAction(
|
||||
SetParentCategory(
|
||||
categoryId = categoryId,
|
||||
parentCategory = ""
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
dismiss()
|
||||
}
|
||||
|
||||
list.addView(row)
|
||||
}
|
||||
})
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
fun show(fragmentManager: FragmentManager) = showSafe(fragmentManager, DIALOG_TAG)
|
||||
}
|
|
@ -28,7 +28,7 @@ import io.timelimit.android.R
|
|||
import io.timelimit.android.data.Database
|
||||
import io.timelimit.android.data.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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -125,6 +125,7 @@ class Adapter: RecyclerView.Adapter<ViewHolder>() {
|
|||
null
|
||||
}
|
||||
binding.usedForAppsWithoutCategory = item.usedForNotAssignedApps
|
||||
binding.parentCategoryTitle = item.parentCategoryTitle
|
||||
|
||||
binding.card.setOnClickListener { handlers?.onCategoryClicked(item.category) }
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue