Add parent category support

This commit is contained in:
Jonas L 2019-01-28 16:35:56 +01:00
parent b7bf364aa6
commit 917f134d77
29 changed files with 1009 additions and 82 deletions

View file

@ -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 \"\"")
}
}
}

View file

@ -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()
}

View file

@ -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(

View file

@ -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()
}

View file

@ -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

View file

@ -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

View file

@ -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)
}
}
}

View file

@ -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 }
}

View file

@ -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

View file

@ -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

View file

@ -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(

View file

@ -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()

View file

@ -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() }

View file

@ -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)
}
}
}
}

View file

@ -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)
}

View file

@ -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(

View file

@ -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

View file

@ -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) }

View file

@ -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()

View file

@ -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
)
}
}