mirror of
https://codeberg.org/timelimit/timelimit-android.git
synced 2025-10-03 17:59:51 +02:00
Add custom time warnings
This commit is contained in:
parent
2d8156add2
commit
14c7804ba8
26 changed files with 2030 additions and 170 deletions
1264
app/schemas/io.timelimit.android.data.RoomDatabase/40.json
Normal file
1264
app/schemas/io.timelimit.android.data.RoomDatabase/40.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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
|
||||||
|
|
|
@ -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 )")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 -> {
|
||||||
|
|
|
@ -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,31 +529,41 @@ 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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
reader.endObject()
|
reader.endObject()
|
||||||
|
|
||||||
return ServerUpdatedCategoryBaseData(
|
return ServerUpdatedCategoryBaseData(
|
||||||
categoryId = categoryId!!,
|
categoryId = categoryId!!,
|
||||||
childId = childId!!,
|
childId = childId!!,
|
||||||
title = title!!,
|
title = title!!,
|
||||||
blockedMinutesInWeek = blockedMinutesInWeek!!,
|
blockedMinutesInWeek = blockedMinutesInWeek!!,
|
||||||
extraTimeInMillis = extraTimeInMillis!!,
|
extraTimeInMillis = extraTimeInMillis!!,
|
||||||
extraTimeDay = extraTimeDay,
|
extraTimeDay = extraTimeDay,
|
||||||
temporarilyBlocked = temporarilyBlocked!!,
|
temporarilyBlocked = temporarilyBlocked!!,
|
||||||
temporarilyBlockedEndTime = temporarilyBlockedEndTime,
|
temporarilyBlockedEndTime = temporarilyBlockedEndTime,
|
||||||
baseDataVersion = baseDataVersion!!,
|
baseDataVersion = baseDataVersion!!,
|
||||||
parentCategoryId = parentCategoryId!!,
|
parentCategoryId = parentCategoryId!!,
|
||||||
blockAllNotifications = blockAllNotifications,
|
blockAllNotifications = blockAllNotifications,
|
||||||
timeWarnings = timeWarnings,
|
timeWarnings = timeWarnings,
|
||||||
minBatteryLevelCharging = minBatteryLevelCharging,
|
minBatteryLevelCharging = minBatteryLevelCharging,
|
||||||
minBatteryLevelMobile = minBatteryLevelMobile,
|
minBatteryLevelMobile = minBatteryLevelMobile,
|
||||||
sort = sort,
|
sort = sort,
|
||||||
networks = networks,
|
networks = networks,
|
||||||
disableLimitsUntil = disableLimitsUntil,
|
disableLimitsUntil = disableLimitsUntil,
|
||||||
flags = flags,
|
flags = flags,
|
||||||
blockNotificationDelay = blockNotificationDelay
|
blockNotificationDelay = blockNotificationDelay,
|
||||||
|
additionalTimeWarnings = additionalTimeWarnings
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) }
|
||||||
|
@ -122,11 +151,13 @@ 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) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
@ -89,13 +89,23 @@ object DuplicateChildActions {
|
||||||
oldCategory.category.timeWarnings.let { timeWarnings ->
|
oldCategory.category.timeWarnings.let { timeWarnings ->
|
||||||
if (timeWarnings != 0) {
|
if (timeWarnings != 0) {
|
||||||
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,
|
||||||
|
|
60
app/src/main/res/layout/add_time_warning_dialog.xml
Normal file
60
app/src/main/res/layout/add_time_warning_dialog.xml
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue