Add custom time warnings

This commit is contained in:
Jonas Lochmann 2022-03-28 02:00:00 +02:00
parent 2d8156add2
commit 14c7804ba8
No known key found for this signature in database
GPG key ID: 8B8C9AEE10FA5B36
26 changed files with 2030 additions and 170 deletions

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -40,6 +40,7 @@ interface Database {
fun userLimitLoginCategoryDao(): UserLimitLoginCategoryDao fun userLimitLoginCategoryDao(): UserLimitLoginCategoryDao
fun categoryNetworkId(): CategoryNetworkIdDao fun categoryNetworkId(): CategoryNetworkIdDao
fun childTasks(): ChildTaskDao fun childTasks(): ChildTaskDao
fun timeWarning(): CategoryTimeWarningDao
fun <T> runInTransaction(block: () -> T): T fun <T> runInTransaction(block: () -> T): T
fun <T> runInUnobservedTransaction(block: () -> T): T fun <T> runInUnobservedTransaction(block: () -> T): T

View file

@ -280,4 +280,10 @@ object DatabaseMigrations {
// nothing to do, there was just a new config item type added // nothing to do, there was just a new config item type added
} }
} }
val MIGRATE_TO_V40 = object: Migration(39, 40) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `category_time_warning` (`category_id` TEXT NOT NULL, `minutes` INTEGER NOT NULL, PRIMARY KEY(`category_id`, `minutes`), FOREIGN KEY(`category_id`) REFERENCES `category`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )")
}
}
} }

View file

@ -50,8 +50,9 @@ import java.util.concurrent.TimeUnit
SessionDuration::class, SessionDuration::class,
UserLimitLoginCategory::class, UserLimitLoginCategory::class,
CategoryNetworkId::class, CategoryNetworkId::class,
ChildTask::class ChildTask::class,
], version = 39) CategoryTimeWarning::class
], version = 40)
abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database { abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database {
companion object { companion object {
private val lock = Object() private val lock = Object()
@ -124,7 +125,8 @@ abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database
DatabaseMigrations.MIGRATE_TO_V36, DatabaseMigrations.MIGRATE_TO_V36,
DatabaseMigrations.MIGRATE_TO_V37, DatabaseMigrations.MIGRATE_TO_V37,
DatabaseMigrations.MIGRATE_TO_V38, DatabaseMigrations.MIGRATE_TO_V38,
DatabaseMigrations.MIGRATE_TO_V39 DatabaseMigrations.MIGRATE_TO_V39,
DatabaseMigrations.MIGRATE_TO_V40
) )
.setQueryExecutor(Threads.database) .setQueryExecutor(Threads.database)
.addCallback(object: Callback() { .addCallback(object: Callback() {

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -45,6 +45,7 @@ object DatabaseBackupLowlevel {
private const val USER_LIMIT_LOGIN_CATEGORY = "userLimitLoginCategory" private const val USER_LIMIT_LOGIN_CATEGORY = "userLimitLoginCategory"
private const val CATEGORY_NETWORK_ID = "categoryNetworkId" private const val CATEGORY_NETWORK_ID = "categoryNetworkId"
private const val CHILD_TASK = "childTask" private const val CHILD_TASK = "childTask"
private const val CATEGORY_TIME_WARNINGS = "timeWarnings"
fun outputAsBackupJson(database: Database, outputStream: OutputStream) { fun outputAsBackupJson(database: Database, outputStream: OutputStream) {
val writer = JsonWriter(OutputStreamWriter(outputStream, Charsets.UTF_8)) val writer = JsonWriter(OutputStreamWriter(outputStream, Charsets.UTF_8))
@ -73,6 +74,14 @@ object DatabaseBackupLowlevel {
writer.endArray() writer.endArray()
} }
fun <T: JsonSerializable> handleCollection(name: String, items: List<T>) {
writer.name(name).beginArray()
items.forEach { it.serialize(writer) }
writer.endArray()
}
handleCollection(APP) {offset, pageSize -> database.app().getAppPageSync(offset, pageSize) } handleCollection(APP) {offset, pageSize -> database.app().getAppPageSync(offset, pageSize) }
handleCollection(CATEGORY) {offset: Int, pageSize: Int -> database.category().getCategoryPageSync(offset, pageSize) } handleCollection(CATEGORY) {offset: Int, pageSize: Int -> database.category().getCategoryPageSync(offset, pageSize) }
handleCollection(CATEGORY_APP) { offset, pageSize -> database.categoryApp().getCategoryAppPageSync(offset, pageSize) } handleCollection(CATEGORY_APP) { offset, pageSize -> database.categoryApp().getCategoryAppPageSync(offset, pageSize) }
@ -94,6 +103,7 @@ object DatabaseBackupLowlevel {
handleCollection(USER_LIMIT_LOGIN_CATEGORY) { offset, pageSize -> database.userLimitLoginCategoryDao().getAllowedContactPageSync(offset, pageSize) } handleCollection(USER_LIMIT_LOGIN_CATEGORY) { offset, pageSize -> database.userLimitLoginCategoryDao().getAllowedContactPageSync(offset, pageSize) }
handleCollection(CATEGORY_NETWORK_ID) { offset, pageSize -> database.categoryNetworkId().getPageSync(offset, pageSize) } handleCollection(CATEGORY_NETWORK_ID) { offset, pageSize -> database.categoryNetworkId().getPageSync(offset, pageSize) }
handleCollection(CHILD_TASK) { offset, pageSize -> database.childTasks().getPageSync(offset, pageSize) } handleCollection(CHILD_TASK) { offset, pageSize -> database.childTasks().getPageSync(offset, pageSize) }
handleCollection(CATEGORY_TIME_WARNINGS, database.timeWarning().getAllItemsSync())
writer.endObject().flush() writer.endObject().flush()
} }
@ -104,6 +114,7 @@ object DatabaseBackupLowlevel {
var userLoginLimitCategories = emptyList<UserLimitLoginCategory>() var userLoginLimitCategories = emptyList<UserLimitLoginCategory>()
var categoryNetworkId = emptyList<CategoryNetworkId>() var categoryNetworkId = emptyList<CategoryNetworkId>()
var childTasks = emptyList<ChildTask>() var childTasks = emptyList<ChildTask>()
var timeWarnings = emptyList<CategoryTimeWarning>()
database.runInTransaction { database.runInTransaction {
database.deleteAllData() database.deleteAllData()
@ -283,6 +294,19 @@ object DatabaseBackupLowlevel {
reader.endArray() reader.endArray()
} }
CATEGORY_TIME_WARNINGS -> {
reader.beginArray()
mutableListOf<CategoryTimeWarning>().let { list ->
while (reader.hasNext()) {
list.add(CategoryTimeWarning.parse(reader))
}
timeWarnings = list
}
reader.endArray()
}
else -> reader.skipValue() else -> reader.skipValue()
} }
} }
@ -291,6 +315,7 @@ object DatabaseBackupLowlevel {
if (userLoginLimitCategories.isNotEmpty()) { database.userLimitLoginCategoryDao().addItemsSync(userLoginLimitCategories) } if (userLoginLimitCategories.isNotEmpty()) { database.userLimitLoginCategoryDao().addItemsSync(userLoginLimitCategories) }
if (categoryNetworkId.isNotEmpty()) { database.categoryNetworkId().insertItemsSync(categoryNetworkId) } if (categoryNetworkId.isNotEmpty()) { database.categoryNetworkId().insertItemsSync(categoryNetworkId) }
if (childTasks.isNotEmpty()) { database.childTasks().insertItemsSync(childTasks) } if (childTasks.isNotEmpty()) { database.childTasks().insertItemsSync(childTasks) }
if (timeWarnings.isNotEmpty()) { database.timeWarning().insertItemsSync(timeWarnings) }
} }
} }
} }

View file

@ -0,0 +1,44 @@
/*
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.timelimit.android.data.dao
import androidx.lifecycle.LiveData
import androidx.room.*
import io.timelimit.android.data.model.CategoryTimeWarning
@Dao
interface CategoryTimeWarningDao {
@Query("SELECT * FROM category_time_warning")
fun getAllItemsSync(): List<CategoryTimeWarning>
@Query("SELECT * FROM category_time_warning WHERE category_id = :categoryId")
fun getItemsByCategoryIdSync(categoryId: String): List<CategoryTimeWarning>
@Query("SELECT * FROM category_time_warning WHERE category_id = :categoryId")
fun getItemsByCategoryIdLive(categoryId: String): LiveData<List<CategoryTimeWarning>>
@Insert
fun insertItemsSync(items: List<CategoryTimeWarning>)
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun insertItemIgnoreConflictSync(item: CategoryTimeWarning)
@Query("DELETE FROM category_time_warning WHERE category_id = :categoryId AND minutes = :minutes")
fun deleteItem(categoryId: String, minutes: Int)
@Query("DELETE FROM category_time_warning WHERE category_id = :categoryId")
fun deleteByCategoryIdSync(categoryId: String)
}

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -33,7 +33,8 @@ enum class Table {
User, User,
UserKey, UserKey,
UserLimitLoginCategory, UserLimitLoginCategory,
CategoryNetworkId CategoryNetworkId,
CategoryTimeWarning
} }
object TableNames { object TableNames {
@ -54,6 +55,7 @@ object TableNames {
const val USER_KEY = "user_key" const val USER_KEY = "user_key"
const val USER_LIMIT_LOGIN_CATEGORY = "user_limit_login_category" const val USER_LIMIT_LOGIN_CATEGORY = "user_limit_login_category"
const val CATEGORY_NETWORK_ID = "category_network_id" const val CATEGORY_NETWORK_ID = "category_network_id"
const val CATEGORY_TIME_WARNING = "category_time_warning"
} }
object TableUtil { object TableUtil {
@ -75,6 +77,7 @@ object TableUtil {
Table.UserKey -> TableNames.USER_KEY Table.UserKey -> TableNames.USER_KEY
Table.UserLimitLoginCategory -> TableNames.USER_LIMIT_LOGIN_CATEGORY Table.UserLimitLoginCategory -> TableNames.USER_LIMIT_LOGIN_CATEGORY
Table.CategoryNetworkId -> TableNames.CATEGORY_NETWORK_ID Table.CategoryNetworkId -> TableNames.CATEGORY_NETWORK_ID
Table.CategoryTimeWarning -> TableNames.CATEGORY_TIME_WARNING
} }
fun toEnum(value: String): Table = when (value) { fun toEnum(value: String): Table = when (value) {
@ -95,6 +98,7 @@ object TableUtil {
TableNames.USER_KEY -> Table.UserKey TableNames.USER_KEY -> Table.UserKey
TableNames.USER_LIMIT_LOGIN_CATEGORY -> Table.UserLimitLoginCategory TableNames.USER_LIMIT_LOGIN_CATEGORY -> Table.UserLimitLoginCategory
TableNames.CATEGORY_NETWORK_ID -> Table.CategoryNetworkId TableNames.CATEGORY_NETWORK_ID -> Table.CategoryNetworkId
TableNames.CATEGORY_TIME_WARNING -> Table.CategoryTimeWarning
else -> throw IllegalArgumentException() else -> throw IllegalArgumentException()
} }
} }

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -263,15 +263,13 @@ data class Category(
} }
object CategoryTimeWarnings { object CategoryTimeWarnings {
val durationToBitIndex = mapOf( val durationInMinutesToBitIndex = mapOf(
1000L * 60 to 0, // 1 minute 1 to 0,
1000L * 60 * 3 to 1, // 3 minutes 3 to 1,
1000L * 60 * 5 to 2, // 5 minutes 5 to 2,
1000L * 60 * 10 to 3, // 10 minutes 10 to 3,
1000L * 60 * 15 to 4 // 15 minutes 15 to 4
) )
val durations = durationToBitIndex.keys
} }
object CategoryFlags { object CategoryFlags {

View file

@ -0,0 +1,85 @@
/*
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.timelimit.android.data.model
import android.util.JsonReader
import android.util.JsonWriter
import androidx.room.*
import io.timelimit.android.data.IdGenerator
import io.timelimit.android.data.JsonSerializable
@Entity(
tableName = "category_time_warning",
primaryKeys = ["category_id", "minutes"],
foreignKeys = [
ForeignKey(
entity = Category::class,
parentColumns = ["id"],
childColumns = ["category_id"],
onUpdate = ForeignKey.CASCADE,
onDelete = ForeignKey.CASCADE
)
]
)
data class CategoryTimeWarning (
@ColumnInfo(name = "category_id")
val categoryId: String,
val minutes: Int
): JsonSerializable {
companion object {
private const val CATEGORY_ID = "categoryId"
private const val MINUTES = "minutes"
const val MIN = 1
const val MAX = 60 * 24 * 7 - 2
fun parse(reader: JsonReader): CategoryTimeWarning {
var categoryId: String? = null
var minutes: Int? = null
reader.beginObject()
while (reader.hasNext()) {
when (reader.nextName()) {
CATEGORY_ID -> categoryId = reader.nextString()
MINUTES -> minutes = reader.nextInt()
else -> reader.skipValue()
}
}
reader.endObject()
return CategoryTimeWarning(
categoryId = categoryId!!,
minutes = minutes!!
)
}
}
init {
IdGenerator.assertIdValid(categoryId)
if (minutes < MIN || minutes > MAX) {
throw IllegalArgumentException()
}
}
override fun serialize(writer: JsonWriter) {
writer.beginObject()
writer.name(CATEGORY_ID).value(categoryId)
writer.name(MINUTES).value(minutes)
writer.endObject()
}
}

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -25,7 +25,8 @@ data class CategoryRelatedData(
val usedTimes: List<UsedTimeItem>, val usedTimes: List<UsedTimeItem>,
val durations: List<SessionDuration>, val durations: List<SessionDuration>,
val networks: List<CategoryNetworkId>, val networks: List<CategoryNetworkId>,
val limitLoginCategories: List<UserLimitLoginCategory> val limitLoginCategories: List<UserLimitLoginCategory>,
val additionalTimeWarnings: List<CategoryTimeWarning>
) { ) {
companion object { companion object {
fun load(category: Category, database: Database): CategoryRelatedData = database.runInUnobservedTransaction { fun load(category: Category, database: Database): CategoryRelatedData = database.runInUnobservedTransaction {
@ -34,6 +35,7 @@ data class CategoryRelatedData(
val durations = database.sessionDuration().getSessionDurationItemsByCategoryIdSync(category.id) val durations = database.sessionDuration().getSessionDurationItemsByCategoryIdSync(category.id)
val networks = database.categoryNetworkId().getByCategoryIdSync(category.id) val networks = database.categoryNetworkId().getByCategoryIdSync(category.id)
val limitLoginCategories = database.userLimitLoginCategoryDao().getByCategoryIdSync(category.id) val limitLoginCategories = database.userLimitLoginCategoryDao().getByCategoryIdSync(category.id)
val additionalTimeWarnings = database.timeWarning().getItemsByCategoryIdSync(category.id)
CategoryRelatedData( CategoryRelatedData(
category = category, category = category,
@ -41,11 +43,24 @@ data class CategoryRelatedData(
usedTimes = usedTimes, usedTimes = usedTimes,
durations = durations, durations = durations,
networks = networks, networks = networks,
limitLoginCategories = limitLoginCategories limitLoginCategories = limitLoginCategories,
additionalTimeWarnings = additionalTimeWarnings
) )
} }
} }
val allTimeWarningMinutes: Set<Int> by lazy {
mutableSetOf<Int>().also { result ->
CategoryTimeWarnings.durationInMinutesToBitIndex.entries.forEach { (durationInMinutes, bitIndex) ->
if (category.timeWarnings and (1 shl bitIndex) != 0) {
result.add(durationInMinutes)
}
}
additionalTimeWarnings.forEach { result.add(it.minutes) }
}
}
fun update( fun update(
category: Category, category: Category,
updateRules: Boolean, updateRules: Boolean,
@ -53,6 +68,7 @@ data class CategoryRelatedData(
updateDurations: Boolean, updateDurations: Boolean,
updateNetworks: Boolean, updateNetworks: Boolean,
updateLimitLoginCategories: Boolean, updateLimitLoginCategories: Boolean,
updateTimeWarnings: Boolean,
database: Database database: Database
): CategoryRelatedData = database.runInUnobservedTransaction { ): CategoryRelatedData = database.runInUnobservedTransaction {
if (category.id != this.category.id) { if (category.id != this.category.id) {
@ -64,10 +80,12 @@ data class CategoryRelatedData(
val durations = if (updateDurations) database.sessionDuration().getSessionDurationItemsByCategoryIdSync(category.id) else durations val durations = if (updateDurations) database.sessionDuration().getSessionDurationItemsByCategoryIdSync(category.id) else durations
val networks = if (updateNetworks) database.categoryNetworkId().getByCategoryIdSync(category.id) else networks val networks = if (updateNetworks) database.categoryNetworkId().getByCategoryIdSync(category.id) else networks
val limitLoginCategories = if (updateLimitLoginCategories) database.userLimitLoginCategoryDao().getByCategoryIdSync(category.id) else limitLoginCategories val limitLoginCategories = if (updateLimitLoginCategories) database.userLimitLoginCategoryDao().getByCategoryIdSync(category.id) else limitLoginCategories
val additionalTimeWarnings = if (updateTimeWarnings) database.timeWarning().getItemsByCategoryIdSync(category.id) else additionalTimeWarnings
if ( if (
category == this.category && rules == this.rules && usedTimes == this.usedTimes && category == this.category && rules == this.rules && usedTimes == this.usedTimes &&
durations == this.durations && networks == this.networks && limitLoginCategories == this.limitLoginCategories durations == this.durations && networks == this.networks &&
limitLoginCategories == this.limitLoginCategories && additionalTimeWarnings == this.additionalTimeWarnings
) { ) {
this this
} else { } else {
@ -77,7 +95,8 @@ data class CategoryRelatedData(
usedTimes = usedTimes, usedTimes = usedTimes,
durations = durations, durations = durations,
networks = networks, networks = networks,
limitLoginCategories = limitLoginCategories limitLoginCategories = limitLoginCategories,
additionalTimeWarnings = additionalTimeWarnings
) )
} }
} }

View file

@ -39,7 +39,7 @@ data class UserRelatedData(
private val relatedTables = arrayOf( private val relatedTables = arrayOf(
Table.User, Table.Category, Table.TimeLimitRule, Table.User, Table.Category, Table.TimeLimitRule,
Table.UsedTimeItem, Table.SessionDuration, Table.CategoryApp, Table.UsedTimeItem, Table.SessionDuration, Table.CategoryApp,
Table.CategoryNetworkId Table.CategoryNetworkId, Table.UserLimitLoginCategory, Table.CategoryTimeWarning
) )
fun load(user: User, database: Database): UserRelatedData = database.runInUnobservedTransaction { fun load(user: User, database: Database): UserRelatedData = database.runInUnobservedTransaction {
@ -103,11 +103,12 @@ data class UserRelatedData(
private var categoryAppsInvalidated = false private var categoryAppsInvalidated = false
private var categoryNetworksInvalidated = false private var categoryNetworksInvalidated = false
private var limitLoginCategoriesInvalidated = false private var limitLoginCategoriesInvalidated = false
private var timeWarningsInvalidated = false
private val invalidated private val invalidated
get() = userInvalidated || categoriesInvalidated || rulesInvalidated || usedTimesInvalidated || get() = userInvalidated || categoriesInvalidated || rulesInvalidated || usedTimesInvalidated ||
sessionDurationsInvalidated || categoryAppsInvalidated || categoryNetworksInvalidated || sessionDurationsInvalidated || categoryAppsInvalidated || categoryNetworksInvalidated ||
limitLoginCategoriesInvalidated limitLoginCategoriesInvalidated || timeWarningsInvalidated
override fun onInvalidated(tables: Set<Table>) { override fun onInvalidated(tables: Set<Table>) {
tables.forEach { tables.forEach {
@ -120,6 +121,7 @@ data class UserRelatedData(
Table.CategoryApp -> categoryAppsInvalidated = true Table.CategoryApp -> categoryAppsInvalidated = true
Table.CategoryNetworkId -> categoryNetworksInvalidated = true Table.CategoryNetworkId -> categoryNetworksInvalidated = true
Table.UserLimitLoginCategory -> limitLoginCategoriesInvalidated = true Table.UserLimitLoginCategory -> limitLoginCategoriesInvalidated = true
Table.CategoryTimeWarning -> timeWarningsInvalidated = true
else -> {/* do nothing */} else -> {/* do nothing */}
} }
} }
@ -144,13 +146,17 @@ data class UserRelatedData(
updateRules = rulesInvalidated, updateRules = rulesInvalidated,
updateTimes = usedTimesInvalidated, updateTimes = usedTimesInvalidated,
updateNetworks = categoryNetworksInvalidated, updateNetworks = categoryNetworksInvalidated,
updateLimitLoginCategories = limitLoginCategoriesInvalidated updateLimitLoginCategories = limitLoginCategoriesInvalidated,
updateTimeWarnings = timeWarningsInvalidated
) ?: CategoryRelatedData.load( ) ?: CategoryRelatedData.load(
category = category, category = category,
database = database database = database
) )
} }
} else if (sessionDurationsInvalidated || rulesInvalidated || usedTimesInvalidated || categoryNetworksInvalidated || limitLoginCategoriesInvalidated) { } else if (
sessionDurationsInvalidated || rulesInvalidated || usedTimesInvalidated ||
categoryNetworksInvalidated || limitLoginCategoriesInvalidated || timeWarningsInvalidated
) {
categories.map { categories.map {
it.update( it.update(
category = it.category, category = it.category,
@ -159,7 +165,8 @@ data class UserRelatedData(
updateRules = rulesInvalidated, updateRules = rulesInvalidated,
updateTimes = usedTimesInvalidated, updateTimes = usedTimesInvalidated,
updateNetworks = categoryNetworksInvalidated, updateNetworks = categoryNetworksInvalidated,
updateLimitLoginCategories = limitLoginCategoriesInvalidated updateLimitLoginCategories = limitLoginCategoriesInvalidated,
updateTimeWarnings = timeWarningsInvalidated
) )
} }
} else { } else {

View file

@ -25,7 +25,6 @@ import io.timelimit.android.coroutines.executeAndWait
import io.timelimit.android.coroutines.runAsync import io.timelimit.android.coroutines.runAsync
import io.timelimit.android.coroutines.runAsyncExpectForever import io.timelimit.android.coroutines.runAsyncExpectForever
import io.timelimit.android.data.backup.DatabaseBackup import io.timelimit.android.data.backup.DatabaseBackup
import io.timelimit.android.data.model.CategoryTimeWarnings
import io.timelimit.android.data.model.ExperimentalFlags import io.timelimit.android.data.model.ExperimentalFlags
import io.timelimit.android.data.model.UserType import io.timelimit.android.data.model.UserType
import io.timelimit.android.data.model.derived.UserRelatedData import io.timelimit.android.data.model.derived.UserRelatedData
@ -46,7 +45,6 @@ import io.timelimit.android.logic.blockingreason.needsNetworkId
import io.timelimit.android.sync.actions.ForceSyncAction import io.timelimit.android.sync.actions.ForceSyncAction
import io.timelimit.android.sync.actions.UpdateDeviceStatusAction import io.timelimit.android.sync.actions.UpdateDeviceStatusAction
import io.timelimit.android.sync.actions.apply.ApplyActionUtil import io.timelimit.android.sync.actions.apply.ApplyActionUtil
import io.timelimit.android.ui.IsAppInForeground
import io.timelimit.android.ui.lock.LockActivity import io.timelimit.android.ui.lock.LockActivity
import io.timelimit.android.util.AndroidVersion import io.timelimit.android.util.AndroidVersion
import io.timelimit.android.util.TimeTextUtil import io.timelimit.android.util.TimeTextUtil
@ -393,33 +391,50 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
val oldSessionDuration = handling.remainingSessionDuration?.let { it - timeToSubtractForCategory } val oldSessionDuration = handling.remainingSessionDuration?.let { it - timeToSubtractForCategory }
// trigger time warnings // trigger time warnings
fun showTimeWarningNotification(title: Int, roundedNewTime: Long) { fun handleTimeWarnings(
appLogic.platformIntegration.showTimeWarningNotification( notificationTitleStringResource: Int,
title = appLogic.context.getString(title, category.title), roundedNewTimeInMilliseconds: Long
text = TimeTextUtil.remaining(roundedNewTime.toInt(), appLogic.context) ) {
val roundedNewTimeInMinutes = roundedNewTimeInMilliseconds / (1000 * 60)
if (
// CategoryTimeWarning.MAX is still small enough for an integer
roundedNewTimeInMilliseconds >= 0 &&
roundedNewTimeInMilliseconds < Int.MAX_VALUE &&
roundedNewTimeInMinutes >= 0 &&
roundedNewTimeInMinutes < Int.MAX_VALUE &&
handling.createdWithCategoryRelatedData.allTimeWarningMinutes.contains(
roundedNewTimeInMinutes.toInt()
) )
) {
appLogic.platformIntegration.showTimeWarningNotification(
title = appLogic.context.getString(
notificationTitleStringResource,
category.title
),
text = TimeTextUtil.remaining(
roundedNewTimeInMilliseconds.toInt(),
appLogic.context
)
)
}
} }
if (oldRemainingTime / (1000 * 60) != newRemainingTime / (1000 * 60)) { if (oldRemainingTime / (1000 * 60) != newRemainingTime / (1000 * 60)) {
// eventually show remaining time warning handleTimeWarnings(
val roundedNewTime = ((newRemainingTime / (1000 * 60)) + 1) * (1000 * 60) notificationTitleStringResource = R.string.time_warning_not_title,
val flagIndex = CategoryTimeWarnings.durationToBitIndex[roundedNewTime] roundedNewTimeInMilliseconds = ((newRemainingTime / (1000 * 60)) + 1) * 1000 * 60
)
if (flagIndex != null && category.timeWarnings and (1 shl flagIndex) != 0) {
showTimeWarningNotification(title = R.string.time_warning_not_title, roundedNewTime = roundedNewTime)
}
} }
if (oldSessionDuration != null) { if (oldSessionDuration != null) {
val newSessionDuration = oldSessionDuration - timeToSubtract val newSessionDuration = oldSessionDuration - timeToSubtract
// eventually show session duration warning
if (oldSessionDuration / (1000 * 60) != newSessionDuration / (1000 * 60)) {
val roundedNewTime = ((newSessionDuration / (1000 * 60)) + 1) * (1000 * 60)
val flagIndex = CategoryTimeWarnings.durationToBitIndex[roundedNewTime]
if (flagIndex != null && category.timeWarnings and (1 shl flagIndex) != 0) { if (oldSessionDuration / (1000 * 60) != newSessionDuration / (1000 * 60)) {
showTimeWarningNotification(title = R.string.time_warning_not_title_session, roundedNewTime = roundedNewTime) handleTimeWarnings(
} notificationTitleStringResource = R.string.time_warning_not_title_session,
roundedNewTimeInMilliseconds = ((newSessionDuration / (1000 * 60)) + 1) * (1000 * 60)
)
} }
} }
@ -434,12 +449,11 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
if (nextBlockedMinute != null) { if (nextBlockedMinute != null) {
val minutesUntilNextBlockedMinute = nextBlockedMinute - nowMinuteOfWeek val minutesUntilNextBlockedMinute = nextBlockedMinute - nowMinuteOfWeek
val msUntilNextBlocking = minutesUntilNextBlockedMinute.toLong() * 1000 * 60
val flagIndex = CategoryTimeWarnings.durationToBitIndex[msUntilNextBlocking]
if (flagIndex != null && category.timeWarnings and (1 shl flagIndex) != 0) { handleTimeWarnings(
showTimeWarningNotification(title = R.string.time_warning_not_title_blocked_time_area, roundedNewTime = msUntilNextBlocking) notificationTitleStringResource = R.string.time_warning_not_title_blocked_time_area,
} roundedNewTimeInMilliseconds = minutesUntilNextBlockedMinute.toLong() * 1000 * 60
)
} }
} }

View file

@ -387,6 +387,20 @@ object ApplyServerDataStatus {
} }
) )
} }
// apply time warnings
database.timeWarning().deleteByCategoryIdSync(newCategory.categoryId)
if (newCategory.additionalTimeWarnings.isNotEmpty()) {
database.timeWarning().insertItemsSync(
newCategory.additionalTimeWarnings.map { minutes ->
CategoryTimeWarning(
categoryId = newCategory.categoryId,
minutes = minutes
)
}
)
}
} }
} }
} }

View file

@ -726,16 +726,28 @@ data class UpdateCategoryTemporarilyBlockedAction(val categoryId: String, val bl
writer.endObject() writer.endObject()
} }
} }
data class UpdateCategoryTimeWarningsAction(val categoryId: String, val enable: Boolean, val flags: Int): ParentAction() { data class UpdateCategoryTimeWarningsAction(
val categoryId: String,
val enable: Boolean,
val flags: Int,
val minutes: Int?
): ParentAction() {
companion object { companion object {
const val TYPE_VALUE = "UPDATE_CATEGORY_TIME_WARNINGS" const val TYPE_VALUE = "UPDATE_CATEGORY_TIME_WARNINGS"
private const val CATEGORY_ID = "categoryId" private const val CATEGORY_ID = "categoryId"
private const val ENABLE = "enable" private const val ENABLE = "enable"
private const val FLAGS = "flags" private const val FLAGS = "flags"
private const val MINUTES = "minutes"
} }
init { init {
IdGenerator.assertIdValid(categoryId) IdGenerator.assertIdValid(categoryId)
if (minutes != null) {
if (minutes < CategoryTimeWarning.MIN || minutes > CategoryTimeWarning.MAX) {
throw IllegalArgumentException()
}
}
} }
override fun serialize(writer: JsonWriter) { override fun serialize(writer: JsonWriter) {
@ -746,6 +758,10 @@ data class UpdateCategoryTimeWarningsAction(val categoryId: String, val enable:
writer.name(ENABLE).value(enable) writer.name(ENABLE).value(enable)
writer.name(FLAGS).value(flags) writer.name(FLAGS).value(flags)
if (minutes != null) {
writer.name(MINUTES).value(minutes)
}
writer.endObject() writer.endObject()
} }
} }

View file

@ -686,6 +686,19 @@ object LocalDatabaseParentActionDispatcher {
database.category().updateCategorySync(modified) database.category().updateCategorySync(modified)
} }
if (action.minutes != null) {
if (action.enable) database.timeWarning().insertItemIgnoreConflictSync(
CategoryTimeWarning(
categoryId = action.categoryId,
minutes = action.minutes
)
)
else database.timeWarning().deleteItem(
categoryId = action.categoryId,
minutes = action.minutes
)
}
null null
} }
is UpdateCategoryBatteryLimit -> { is UpdateCategoryBatteryLimit -> {

View file

@ -459,7 +459,8 @@ data class ServerUpdatedCategoryBaseData(
val networks: List<ServerCategoryNetworkId>, val networks: List<ServerCategoryNetworkId>,
val disableLimitsUntil: Long, val disableLimitsUntil: Long,
val flags: Long, val flags: Long,
val blockNotificationDelay: Long val blockNotificationDelay: Long,
val additionalTimeWarnings: Set<Int>
) { ) {
companion object { companion object {
private const val CATEGORY_ID = "categoryId" private const val CATEGORY_ID = "categoryId"
@ -481,6 +482,7 @@ data class ServerUpdatedCategoryBaseData(
private const val DISABLE_LIMITS_UNTIL = "dlu" private const val DISABLE_LIMITS_UNTIL = "dlu"
private const val FLAGS = "flags" private const val FLAGS = "flags"
private const val BLOCK_NOTIFICATION_DELAY = "blockNotificationDelay" private const val BLOCK_NOTIFICATION_DELAY = "blockNotificationDelay"
private const val ADDITIONAL_TIME_WARNINGS = "atw"
fun parse(reader: JsonReader): ServerUpdatedCategoryBaseData { fun parse(reader: JsonReader): ServerUpdatedCategoryBaseData {
var categoryId: String? = null var categoryId: String? = null
@ -503,6 +505,7 @@ data class ServerUpdatedCategoryBaseData(
var disableLimitsUntil = 0L var disableLimitsUntil = 0L
var flags = 0L var flags = 0L
var blockNotificationDelay = 0L var blockNotificationDelay = 0L
var additionalTimeWarnings = emptySet<Int>()
reader.beginObject() reader.beginObject()
while (reader.hasNext()) { while (reader.hasNext()) {
@ -526,6 +529,15 @@ data class ServerUpdatedCategoryBaseData(
DISABLE_LIMITS_UNTIL -> disableLimitsUntil = reader.nextLong() DISABLE_LIMITS_UNTIL -> disableLimitsUntil = reader.nextLong()
FLAGS -> flags = reader.nextLong() FLAGS -> flags = reader.nextLong()
BLOCK_NOTIFICATION_DELAY -> blockNotificationDelay = reader.nextLong() BLOCK_NOTIFICATION_DELAY -> blockNotificationDelay = reader.nextLong()
ADDITIONAL_TIME_WARNINGS -> additionalTimeWarnings = mutableSetOf<Int>().also { result ->
reader.beginArray()
while (reader.hasNext()) {
result.add(reader.nextInt())
}
reader.endArray()
}
else -> reader.skipValue() else -> reader.skipValue()
} }
} }
@ -550,7 +562,8 @@ data class ServerUpdatedCategoryBaseData(
networks = networks, networks = networks,
disableLimitsUntil = disableLimitsUntil, disableLimitsUntil = disableLimitsUntil,
flags = flags, flags = flags,
blockNotificationDelay = blockNotificationDelay blockNotificationDelay = blockNotificationDelay,
additionalTimeWarnings = additionalTimeWarnings
) )
} }

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -23,6 +23,7 @@ import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import io.timelimit.android.R import io.timelimit.android.R
@ -38,6 +39,8 @@ import io.timelimit.android.ui.main.ActivityViewModel
import io.timelimit.android.ui.main.getActivityViewModel import io.timelimit.android.ui.main.getActivityViewModel
import io.timelimit.android.ui.manage.category.settings.addusedtime.AddUsedTimeDialogFragment import io.timelimit.android.ui.manage.category.settings.addusedtime.AddUsedTimeDialogFragment
import io.timelimit.android.ui.manage.category.settings.networks.ManageCategoryNetworksView import io.timelimit.android.ui.manage.category.settings.networks.ManageCategoryNetworksView
import io.timelimit.android.ui.manage.category.settings.timewarning.CategoryTimeWarningStatus
import io.timelimit.android.ui.manage.category.settings.timewarning.CategoryTimeWarningView
import io.timelimit.android.ui.payment.RequiresPurchaseDialogFragment import io.timelimit.android.ui.payment.RequiresPurchaseDialogFragment
import io.timelimit.android.ui.util.bind import io.timelimit.android.ui.util.bind
@ -46,6 +49,7 @@ class CategorySettingsFragment : Fragment() {
private const val PERMISSION_REQUEST_CODE = 1 private const val PERMISSION_REQUEST_CODE = 1
private const val CHILD_ID = "childId" private const val CHILD_ID = "childId"
private const val CATEGORY_ID = "categoryId" private const val CATEGORY_ID = "categoryId"
private const val TIME_WARNING_STATUS = "timeWarningStatus"
fun newInstance(childId: String, categoryId: String) = CategorySettingsFragment().apply { fun newInstance(childId: String, categoryId: String) = CategorySettingsFragment().apply {
arguments = Bundle().apply { arguments = Bundle().apply {
@ -61,10 +65,35 @@ class CategorySettingsFragment : Fragment() {
private val categoryId: String get() = requireArguments().getString(CATEGORY_ID)!! private val categoryId: String get() = requireArguments().getString(CATEGORY_ID)!!
private val notificationFilterModel: ManageNotificationFilterModel by viewModels() private val notificationFilterModel: ManageNotificationFilterModel by viewModels()
private val timeWarningStatus = MutableLiveData<CategoryTimeWarningStatus>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
timeWarningStatus.value = savedInstanceState?.getParcelable(TIME_WARNING_STATUS) ?: CategoryTimeWarningStatus.default
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val binding = FragmentCategorySettingsBinding.inflate(inflater, container, false) val binding = FragmentCategorySettingsBinding.inflate(inflater, container, false)
val categoryEntry = appLogic.database.category().getCategoryByChildIdAndId(childId, categoryId) val categoryEntry = appLogic.database.category().getCategoryByChildIdAndId(childId, categoryId)
val timeWarnings = appLogic.database.timeWarning().getItemsByCategoryIdLive(categoryId)
categoryEntry.observe(viewLifecycleOwner) {
if (it != null) {
timeWarningStatus.value?.let { old ->
timeWarningStatus.value = old.update(it)
}
}
}
timeWarnings.observe(viewLifecycleOwner) {
if (it != null) {
timeWarningStatus.value?.let { old ->
timeWarningStatus.value = old.update(it)
}
}
}
val childDate = appLogic.database.user().getChildUserByIdLive(childId).mapToTimezone().switchMap { timezone -> val childDate = appLogic.database.user().getChildUserByIdLive(childId).mapToTimezone().switchMap { timezone ->
liveDataFromFunction (1000 * 10L) { DateInTimezone.newInstance(appLogic.timeApi.getCurrentTimeInMillis(), timezone) } liveDataFromFunction (1000 * 10L) { DateInTimezone.newInstance(appLogic.timeApi.getCurrentTimeInMillis(), timezone) }
@ -124,9 +153,11 @@ class CategorySettingsFragment : Fragment() {
CategoryTimeWarningView.bind( CategoryTimeWarningView.bind(
view = binding.timeWarnings, view = binding.timeWarnings,
auth = auth, auth = auth,
categoryLive = categoryEntry, statusLive = timeWarningStatus,
lifecycleOwner = this, lifecycleOwner = this,
fragmentManager = parentFragmentManager fragmentManager = parentFragmentManager,
categoryId = categoryId,
serverApiLevelInfo = appLogic.serverApiLevelLogic.infoLive
) )
ManageCategoryNetworksView.bind( ManageCategoryNetworksView.bind(
@ -254,4 +285,10 @@ class CategorySettingsFragment : Fragment() {
} }
} }
} }
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
timeWarningStatus.value?.let { outState.putParcelable(TIME_WARNING_STATUS, it) }
}
} }

View file

@ -1,86 +0,0 @@
/*
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.timelimit.android.ui.manage.category.settings
import android.widget.CheckBox
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import io.timelimit.android.R
import io.timelimit.android.data.model.Category
import io.timelimit.android.data.model.CategoryTimeWarnings
import io.timelimit.android.databinding.CategoryTimeWarningsViewBinding
import io.timelimit.android.sync.actions.UpdateCategoryTimeWarningsAction
import io.timelimit.android.ui.help.HelpDialogFragment
import io.timelimit.android.ui.main.ActivityViewModel
import io.timelimit.android.util.TimeTextUtil
object CategoryTimeWarningView {
fun bind(
view: CategoryTimeWarningsViewBinding,
lifecycleOwner: LifecycleOwner,
categoryLive: LiveData<Category?>,
auth: ActivityViewModel,
fragmentManager: FragmentManager
) {
view.titleView.setOnClickListener {
HelpDialogFragment.newInstance(
title = R.string.time_warning_title,
text = R.string.time_warning_desc
).show(fragmentManager)
}
view.linearLayout.removeAllViews()
val durationToCheckbox = mutableMapOf<Long, CheckBox>()
CategoryTimeWarnings.durations.sorted().forEach { duration ->
CheckBox(view.root.context).let { checkbox ->
checkbox.text = TimeTextUtil.time(duration.toInt(), view.root.context)
view.linearLayout.addView(checkbox)
durationToCheckbox[duration] = checkbox
}
}
categoryLive.observe(lifecycleOwner, Observer { category ->
durationToCheckbox.entries.forEach { (duration, checkbox) ->
checkbox.setOnCheckedChangeListener { _, _ -> }
val flag = (1 shl CategoryTimeWarnings.durationToBitIndex[duration]!!)
val enable = (category?.timeWarnings ?: 0) and flag != 0
checkbox.isChecked = enable
checkbox.setOnCheckedChangeListener { _, isChecked ->
if (isChecked != enable && category != null) {
if (auth.tryDispatchParentAction(
UpdateCategoryTimeWarningsAction(
categoryId = category.id,
enable = isChecked,
flags = flag
)
)) {
// it worked
} else {
checkbox.isChecked = enable
}
}
}
}
})
}
}

View file

@ -0,0 +1,81 @@
/*
* TimeLimit Copyright <C> 2019 - 2022 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.timewarning
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.FragmentManager
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import io.timelimit.android.data.model.CategoryTimeWarning
import io.timelimit.android.data.model.CategoryTimeWarnings
import io.timelimit.android.databinding.AddTimeWarningDialogBinding
import io.timelimit.android.extensions.showSafe
import io.timelimit.android.sync.actions.UpdateCategoryTimeWarningsAction
import io.timelimit.android.ui.main.ActivityViewModelHolder
class AddTimeWarningDialogFragment: BottomSheetDialogFragment() {
companion object {
private const val DIALOG_TAG = "AddTimeWarningDialogFragment"
private const val CATEGORY_ID = "categoryId"
fun newInstance(categoryId: String) = AddTimeWarningDialogFragment().apply {
arguments = Bundle().apply {
putString(CATEGORY_ID, categoryId)
}
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val binding = AddTimeWarningDialogBinding.inflate(inflater, container, false)
val categoryId = requireArguments().getString(CATEGORY_ID)!!
val auth = (requireActivity() as ActivityViewModelHolder).getActivityViewModel()
auth.authenticatedUser.observe(viewLifecycleOwner) { if (it == null) dismissAllowingStateLoss() }
binding.numberPicker.minValue = CategoryTimeWarning.MIN
binding.numberPicker.maxValue = CategoryTimeWarning.MAX
binding.confirmButton.setOnClickListener {
val minutes = binding.numberPicker.value
val flagIndex = CategoryTimeWarnings.durationInMinutesToBitIndex[minutes]
val action = if (flagIndex != null) {
UpdateCategoryTimeWarningsAction(
categoryId = categoryId,
enable = true,
flags = 1 shl flagIndex,
minutes = null
)
} else {
UpdateCategoryTimeWarningsAction(
categoryId = categoryId,
enable = true,
flags = 0,
minutes = minutes
)
}
if (auth.tryDispatchParentAction(action)) {
dismissAllowingStateLoss()
}
}
return binding.root
}
fun show(fragmentManager: FragmentManager) = showSafe(fragmentManager, DIALOG_TAG)
}

View file

@ -0,0 +1,118 @@
/*
* TimeLimit Copyright <C> 2019 - 2022 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.timewarning
import android.os.Parcelable
import io.timelimit.android.data.model.Category
import io.timelimit.android.data.model.CategoryTimeWarning
import io.timelimit.android.data.model.CategoryTimeWarnings
import io.timelimit.android.sync.actions.UpdateCategoryTimeWarningsAction
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
import java.util.*
@Parcelize
data class CategoryTimeWarningStatus(
private val categoryFlags: Int?,
private val timeWarnings: Set<Int>?,
private val additionalTimeWarningSlots: Set<Int>
): Parcelable {
companion object {
val default = CategoryTimeWarningStatus(
categoryFlags = null,
timeWarnings = null,
additionalTimeWarningSlots = emptySet()
)
}
fun update(category: Category): CategoryTimeWarningStatus {
if (this.categoryFlags == category.timeWarnings) return this
return this.copy(categoryFlags = category.timeWarnings)
}
fun update(warnings: List<CategoryTimeWarning>): CategoryTimeWarningStatus {
val timeWarnings = warnings.map { it.minutes }.toSet()
if (this.timeWarnings == timeWarnings) return this
return this.copy(
timeWarnings = timeWarnings,
additionalTimeWarningSlots = additionalTimeWarningSlots + timeWarnings
)
}
fun buildAction(categoryId: String, minutes: Int, enable: Boolean): UpdateCategoryTimeWarningsAction {
val flagIndex = CategoryTimeWarnings.durationInMinutesToBitIndex[minutes]
return if (enable) {
if (flagIndex != null) UpdateCategoryTimeWarningsAction(
categoryId = categoryId,
enable = true,
flags = 1 shl flagIndex,
minutes = null
) else UpdateCategoryTimeWarningsAction(
categoryId = categoryId,
enable = true,
flags = 0,
minutes = minutes
)
} else {
UpdateCategoryTimeWarningsAction(
categoryId = categoryId,
enable = false,
flags = if (flagIndex != null) 1 shl flagIndex else 0,
minutes = if (timeWarnings != null && timeWarnings.contains(minutes)) minutes else null
)
}
}
@IgnoredOnParcel
val display = TreeMap<Int, CategoryTimeWarningOptionStatus>().also { result ->
val complete = categoryFlags != null && timeWarnings != null
additionalTimeWarningSlots.forEach { minute ->
result[minute] = if (complete) CategoryTimeWarningOptionStatus.Unchecked
else CategoryTimeWarningOptionStatus.Undefined
}
timeWarnings?.forEach { minute -> result[minute] = CategoryTimeWarningOptionStatus.Checked }
CategoryTimeWarnings.durationInMinutesToBitIndex.forEach { (minute, bitIndex) ->
result[minute] = if (complete && categoryFlags != null) {
if (categoryFlags and (1 shl bitIndex) != 0) CategoryTimeWarningOptionStatus.Checked
else CategoryTimeWarningOptionStatus.Unchecked
} else CategoryTimeWarningOptionStatus.Undefined
}
}.let { output ->
mutableListOf<CategoryTimeWarningOption>().also { result ->
output.entries.forEach { (minute, status) ->
result.add(CategoryTimeWarningOption(minute, status))
}
}
}.toList()
data class CategoryTimeWarningOption(
val minutes: Int,
val status: CategoryTimeWarningOptionStatus
)
enum class CategoryTimeWarningOptionStatus {
Checked,
Unchecked,
Undefined
}
}

View file

@ -0,0 +1,105 @@
/*
* TimeLimit Copyright <C> 2019 - 2022 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.timewarning
import android.view.View
import android.widget.CheckBox
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import io.timelimit.android.R
import io.timelimit.android.databinding.CategoryTimeWarningsViewBinding
import io.timelimit.android.logic.ServerApiLevelInfo
import io.timelimit.android.ui.help.HelpDialogFragment
import io.timelimit.android.ui.main.ActivityViewModel
import io.timelimit.android.util.TimeTextUtil
object CategoryTimeWarningView {
fun bind(
view: CategoryTimeWarningsViewBinding,
lifecycleOwner: LifecycleOwner,
statusLive: LiveData<CategoryTimeWarningStatus>,
auth: ActivityViewModel,
fragmentManager: FragmentManager,
categoryId: String,
serverApiLevelInfo: LiveData<ServerApiLevelInfo>
) {
view.titleView.setOnClickListener {
HelpDialogFragment.newInstance(
title = R.string.time_warning_title,
text = R.string.time_warning_desc
).show(fragmentManager)
}
view.addTimeWarningButton.setOnClickListener {
if (auth.requestAuthenticationOrReturnTrue()) {
AddTimeWarningDialogFragment.newInstance(categoryId).show(fragmentManager)
}
}
serverApiLevelInfo.observe(lifecycleOwner) { info ->
view.addTimeWarningButton.visibility = if (info.hasLevelOrIsOffline(3)) View.VISIBLE else View.GONE
}
view.linearLayout.removeAllViews()
val views = mutableListOf<CheckBox>()
statusLive.observe(lifecycleOwner) { status ->
if (views.size != status.display.size) {
views.clear()
view.linearLayout.removeAllViews()
for (index in 1..status.display.size) {
CheckBox(view.root.context).also { checkbox ->
views.add(checkbox)
view.linearLayout.addView(checkbox)
}
}
}
status.display.forEachIndexed { index, item ->
val checkbox = views[index]
val enabled = item.status != CategoryTimeWarningStatus.CategoryTimeWarningOptionStatus.Undefined
val checked = item.status == CategoryTimeWarningStatus.CategoryTimeWarningOptionStatus.Checked
checkbox.text = TimeTextUtil.time(item.minutes * 1000 * 60, view.root.context)
checkbox.setOnCheckedChangeListener(null)
checkbox.isEnabled = enabled
checkbox.isChecked = checked
if (item.status != CategoryTimeWarningStatus.CategoryTimeWarningOptionStatus.Undefined) {
checkbox.setOnCheckedChangeListener { _, isChecked ->
if (isChecked != checked) {
if (auth.tryDispatchParentAction(status.buildAction(
categoryId = categoryId,
minutes = item.minutes,
enable = isChecked
))) {
// it worked
} else {
checkbox.isChecked = checked
}
}
}
}
}
}
}
}

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -91,11 +91,21 @@ object DuplicateChildActions {
result.add(UpdateCategoryTimeWarningsAction( result.add(UpdateCategoryTimeWarningsAction(
categoryId = newCategoryId, categoryId = newCategoryId,
enable = true, enable = true,
flags = timeWarnings flags = timeWarnings,
minutes = null
)) ))
} }
} }
oldCategory.additionalTimeWarnings.forEach { timeWarning ->
result.add(UpdateCategoryTimeWarningsAction(
categoryId = newCategoryId,
enable = true,
flags = 0,
minutes = timeWarning.minutes
))
}
if (oldCategory.category.minBatteryLevelWhileCharging != 0 || oldCategory.category.minBatteryLevelMobile != 0) { if (oldCategory.category.minBatteryLevelWhileCharging != 0 || oldCategory.category.minBatteryLevelMobile != 0) {
result.add(UpdateCategoryBatteryLimit( result.add(UpdateCategoryBatteryLimit(
categoryId = newCategoryId, categoryId = newCategoryId,

View file

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
TimeLimit Copyright <C> 2019 - 2022 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/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout
android:orientation="vertical"
android:padding="8dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:textAppearance="?android:textAppearanceLarge"
android:text="@string/time_warning_custom_add"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<LinearLayout
android:orientation="horizontal"
android:layout_gravity="center_horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<NumberPicker
android:id="@+id/number_picker"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:paddingStart="8dp"
android:paddingEnd="0dp"
android:textAppearance="?android:textAppearanceMedium"
android:layout_gravity="center_vertical"
android:text="@string/select_time_span_view_minutes"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<Button
android:id="@+id/confirm_button"
android:text="@string/generic_ok"
android:layout_gravity="end"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</layout>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- <!--
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License. the Free Software Foundation version 3 of the License.
@ -45,6 +45,14 @@
<!-- checkboxes will be added hear --> <!-- checkboxes will be added hear -->
</LinearLayout> </LinearLayout>
<Button
style="?materialButtonOutlinedStyle"
android:layout_gravity="end"
android:text="@string/time_warning_custom_add"
android:id="@+id/add_time_warning_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout> </LinearLayout>
</androidx.cardview.widget.CardView> </androidx.cardview.widget.CardView>
</layout> </layout>

View file

@ -1123,6 +1123,7 @@
<string name="notification_channel_time_warning_title">Zeitwarnungen</string> <string name="notification_channel_time_warning_title">Zeitwarnungen</string>
<string name="notification_channel_time_warning_text">Benachrichtigungen für Kinder, wenn nur noch wenig Zeit übrig ist</string> <string name="notification_channel_time_warning_text">Benachrichtigungen für Kinder, wenn nur noch wenig Zeit übrig ist</string>
<string name="time_warning_custom_add">Zeitwarnung hinzufügen</string>
<string name="notification_channel_premium_expires_title">Vollversionablauf</string> <string name="notification_channel_premium_expires_title">Vollversionablauf</string>
<string name="notification_channel_premium_expires_text">Benachrichtigungen, wenn die Vollversion bald abläuft</string> <string name="notification_channel_premium_expires_text">Benachrichtigungen, wenn die Vollversion bald abläuft</string>

View file

@ -1168,6 +1168,7 @@
<string name="notification_channel_time_warning_title">Time warning</string> <string name="notification_channel_time_warning_title">Time warning</string>
<string name="notification_channel_time_warning_text">Notification for childs if there is not much time remaining</string> <string name="notification_channel_time_warning_text">Notification for childs if there is not much time remaining</string>
<string name="time_warning_custom_add">Add time warning</string>
<string name="notification_channel_premium_expires_title">Premium version expires</string> <string name="notification_channel_premium_expires_title">Premium version expires</string>
<string name="notification_channel_premium_expires_text">Notification if the premium version expires shortly</string> <string name="notification_channel_premium_expires_text">Notification if the premium version expires shortly</string>