mirror of
https://codeberg.org/timelimit/timelimit-android.git
synced 2025-10-03 09:49:25 +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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -40,6 +40,7 @@ interface Database {
|
|||
fun userLimitLoginCategoryDao(): UserLimitLoginCategoryDao
|
||||
fun categoryNetworkId(): CategoryNetworkIdDao
|
||||
fun childTasks(): ChildTaskDao
|
||||
fun timeWarning(): CategoryTimeWarningDao
|
||||
|
||||
fun <T> runInTransaction(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
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
UserLimitLoginCategory::class,
|
||||
CategoryNetworkId::class,
|
||||
ChildTask::class
|
||||
], version = 39)
|
||||
ChildTask::class,
|
||||
CategoryTimeWarning::class
|
||||
], version = 40)
|
||||
abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database {
|
||||
companion 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_V37,
|
||||
DatabaseMigrations.MIGRATE_TO_V38,
|
||||
DatabaseMigrations.MIGRATE_TO_V39
|
||||
DatabaseMigrations.MIGRATE_TO_V39,
|
||||
DatabaseMigrations.MIGRATE_TO_V40
|
||||
)
|
||||
.setQueryExecutor(Threads.database)
|
||||
.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
|
||||
* 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 CATEGORY_NETWORK_ID = "categoryNetworkId"
|
||||
private const val CHILD_TASK = "childTask"
|
||||
private const val CATEGORY_TIME_WARNINGS = "timeWarnings"
|
||||
|
||||
fun outputAsBackupJson(database: Database, outputStream: OutputStream) {
|
||||
val writer = JsonWriter(OutputStreamWriter(outputStream, Charsets.UTF_8))
|
||||
|
@ -73,6 +74,14 @@ object DatabaseBackupLowlevel {
|
|||
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(CATEGORY) {offset: Int, pageSize: Int -> database.category().getCategoryPageSync(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(CATEGORY_NETWORK_ID) { offset, pageSize -> database.categoryNetworkId().getPageSync(offset, pageSize) }
|
||||
handleCollection(CHILD_TASK) { offset, pageSize -> database.childTasks().getPageSync(offset, pageSize) }
|
||||
handleCollection(CATEGORY_TIME_WARNINGS, database.timeWarning().getAllItemsSync())
|
||||
|
||||
writer.endObject().flush()
|
||||
}
|
||||
|
@ -104,6 +114,7 @@ object DatabaseBackupLowlevel {
|
|||
var userLoginLimitCategories = emptyList<UserLimitLoginCategory>()
|
||||
var categoryNetworkId = emptyList<CategoryNetworkId>()
|
||||
var childTasks = emptyList<ChildTask>()
|
||||
var timeWarnings = emptyList<CategoryTimeWarning>()
|
||||
|
||||
database.runInTransaction {
|
||||
database.deleteAllData()
|
||||
|
@ -283,6 +294,19 @@ object DatabaseBackupLowlevel {
|
|||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
@ -291,6 +315,7 @@ object DatabaseBackupLowlevel {
|
|||
if (userLoginLimitCategories.isNotEmpty()) { database.userLimitLoginCategoryDao().addItemsSync(userLoginLimitCategories) }
|
||||
if (categoryNetworkId.isNotEmpty()) { database.categoryNetworkId().insertItemsSync(categoryNetworkId) }
|
||||
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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -33,7 +33,8 @@ enum class Table {
|
|||
User,
|
||||
UserKey,
|
||||
UserLimitLoginCategory,
|
||||
CategoryNetworkId
|
||||
CategoryNetworkId,
|
||||
CategoryTimeWarning
|
||||
}
|
||||
|
||||
object TableNames {
|
||||
|
@ -54,6 +55,7 @@ object TableNames {
|
|||
const val USER_KEY = "user_key"
|
||||
const val USER_LIMIT_LOGIN_CATEGORY = "user_limit_login_category"
|
||||
const val CATEGORY_NETWORK_ID = "category_network_id"
|
||||
const val CATEGORY_TIME_WARNING = "category_time_warning"
|
||||
}
|
||||
|
||||
object TableUtil {
|
||||
|
@ -75,6 +77,7 @@ object TableUtil {
|
|||
Table.UserKey -> TableNames.USER_KEY
|
||||
Table.UserLimitLoginCategory -> TableNames.USER_LIMIT_LOGIN_CATEGORY
|
||||
Table.CategoryNetworkId -> TableNames.CATEGORY_NETWORK_ID
|
||||
Table.CategoryTimeWarning -> TableNames.CATEGORY_TIME_WARNING
|
||||
}
|
||||
|
||||
fun toEnum(value: String): Table = when (value) {
|
||||
|
@ -95,6 +98,7 @@ object TableUtil {
|
|||
TableNames.USER_KEY -> Table.UserKey
|
||||
TableNames.USER_LIMIT_LOGIN_CATEGORY -> Table.UserLimitLoginCategory
|
||||
TableNames.CATEGORY_NETWORK_ID -> Table.CategoryNetworkId
|
||||
TableNames.CATEGORY_TIME_WARNING -> Table.CategoryTimeWarning
|
||||
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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -263,15 +263,13 @@ data class Category(
|
|||
}
|
||||
|
||||
object CategoryTimeWarnings {
|
||||
val durationToBitIndex = mapOf(
|
||||
1000L * 60 to 0, // 1 minute
|
||||
1000L * 60 * 3 to 1, // 3 minutes
|
||||
1000L * 60 * 5 to 2, // 5 minutes
|
||||
1000L * 60 * 10 to 3, // 10 minutes
|
||||
1000L * 60 * 15 to 4 // 15 minutes
|
||||
val durationInMinutesToBitIndex = mapOf(
|
||||
1 to 0,
|
||||
3 to 1,
|
||||
5 to 2,
|
||||
10 to 3,
|
||||
15 to 4
|
||||
)
|
||||
|
||||
val durations = durationToBitIndex.keys
|
||||
}
|
||||
|
||||
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
|
||||
* 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 durations: List<SessionDuration>,
|
||||
val networks: List<CategoryNetworkId>,
|
||||
val limitLoginCategories: List<UserLimitLoginCategory>
|
||||
val limitLoginCategories: List<UserLimitLoginCategory>,
|
||||
val additionalTimeWarnings: List<CategoryTimeWarning>
|
||||
) {
|
||||
companion object {
|
||||
fun load(category: Category, database: Database): CategoryRelatedData = database.runInUnobservedTransaction {
|
||||
|
@ -34,6 +35,7 @@ data class CategoryRelatedData(
|
|||
val durations = database.sessionDuration().getSessionDurationItemsByCategoryIdSync(category.id)
|
||||
val networks = database.categoryNetworkId().getByCategoryIdSync(category.id)
|
||||
val limitLoginCategories = database.userLimitLoginCategoryDao().getByCategoryIdSync(category.id)
|
||||
val additionalTimeWarnings = database.timeWarning().getItemsByCategoryIdSync(category.id)
|
||||
|
||||
CategoryRelatedData(
|
||||
category = category,
|
||||
|
@ -41,11 +43,24 @@ data class CategoryRelatedData(
|
|||
usedTimes = usedTimes,
|
||||
durations = durations,
|
||||
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(
|
||||
category: Category,
|
||||
updateRules: Boolean,
|
||||
|
@ -53,6 +68,7 @@ data class CategoryRelatedData(
|
|||
updateDurations: Boolean,
|
||||
updateNetworks: Boolean,
|
||||
updateLimitLoginCategories: Boolean,
|
||||
updateTimeWarnings: Boolean,
|
||||
database: Database
|
||||
): CategoryRelatedData = database.runInUnobservedTransaction {
|
||||
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 networks = if (updateNetworks) database.categoryNetworkId().getByCategoryIdSync(category.id) else networks
|
||||
val limitLoginCategories = if (updateLimitLoginCategories) database.userLimitLoginCategoryDao().getByCategoryIdSync(category.id) else limitLoginCategories
|
||||
val additionalTimeWarnings = if (updateTimeWarnings) database.timeWarning().getItemsByCategoryIdSync(category.id) else additionalTimeWarnings
|
||||
|
||||
if (
|
||||
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
|
||||
} else {
|
||||
|
@ -77,7 +95,8 @@ data class CategoryRelatedData(
|
|||
usedTimes = usedTimes,
|
||||
durations = durations,
|
||||
networks = networks,
|
||||
limitLoginCategories = limitLoginCategories
|
||||
limitLoginCategories = limitLoginCategories,
|
||||
additionalTimeWarnings = additionalTimeWarnings
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ data class UserRelatedData(
|
|||
private val relatedTables = arrayOf(
|
||||
Table.User, Table.Category, Table.TimeLimitRule,
|
||||
Table.UsedTimeItem, Table.SessionDuration, Table.CategoryApp,
|
||||
Table.CategoryNetworkId
|
||||
Table.CategoryNetworkId, Table.UserLimitLoginCategory, Table.CategoryTimeWarning
|
||||
)
|
||||
|
||||
fun load(user: User, database: Database): UserRelatedData = database.runInUnobservedTransaction {
|
||||
|
@ -103,11 +103,12 @@ data class UserRelatedData(
|
|||
private var categoryAppsInvalidated = false
|
||||
private var categoryNetworksInvalidated = false
|
||||
private var limitLoginCategoriesInvalidated = false
|
||||
private var timeWarningsInvalidated = false
|
||||
|
||||
private val invalidated
|
||||
get() = userInvalidated || categoriesInvalidated || rulesInvalidated || usedTimesInvalidated ||
|
||||
sessionDurationsInvalidated || categoryAppsInvalidated || categoryNetworksInvalidated ||
|
||||
limitLoginCategoriesInvalidated
|
||||
limitLoginCategoriesInvalidated || timeWarningsInvalidated
|
||||
|
||||
override fun onInvalidated(tables: Set<Table>) {
|
||||
tables.forEach {
|
||||
|
@ -120,6 +121,7 @@ data class UserRelatedData(
|
|||
Table.CategoryApp -> categoryAppsInvalidated = true
|
||||
Table.CategoryNetworkId -> categoryNetworksInvalidated = true
|
||||
Table.UserLimitLoginCategory -> limitLoginCategoriesInvalidated = true
|
||||
Table.CategoryTimeWarning -> timeWarningsInvalidated = true
|
||||
else -> {/* do nothing */}
|
||||
}
|
||||
}
|
||||
|
@ -144,13 +146,17 @@ data class UserRelatedData(
|
|||
updateRules = rulesInvalidated,
|
||||
updateTimes = usedTimesInvalidated,
|
||||
updateNetworks = categoryNetworksInvalidated,
|
||||
updateLimitLoginCategories = limitLoginCategoriesInvalidated
|
||||
updateLimitLoginCategories = limitLoginCategoriesInvalidated,
|
||||
updateTimeWarnings = timeWarningsInvalidated
|
||||
) ?: CategoryRelatedData.load(
|
||||
category = category,
|
||||
database = database
|
||||
)
|
||||
}
|
||||
} else if (sessionDurationsInvalidated || rulesInvalidated || usedTimesInvalidated || categoryNetworksInvalidated || limitLoginCategoriesInvalidated) {
|
||||
} else if (
|
||||
sessionDurationsInvalidated || rulesInvalidated || usedTimesInvalidated ||
|
||||
categoryNetworksInvalidated || limitLoginCategoriesInvalidated || timeWarningsInvalidated
|
||||
) {
|
||||
categories.map {
|
||||
it.update(
|
||||
category = it.category,
|
||||
|
@ -159,7 +165,8 @@ data class UserRelatedData(
|
|||
updateRules = rulesInvalidated,
|
||||
updateTimes = usedTimesInvalidated,
|
||||
updateNetworks = categoryNetworksInvalidated,
|
||||
updateLimitLoginCategories = limitLoginCategoriesInvalidated
|
||||
updateLimitLoginCategories = limitLoginCategoriesInvalidated,
|
||||
updateTimeWarnings = timeWarningsInvalidated
|
||||
)
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -25,7 +25,6 @@ import io.timelimit.android.coroutines.executeAndWait
|
|||
import io.timelimit.android.coroutines.runAsync
|
||||
import io.timelimit.android.coroutines.runAsyncExpectForever
|
||||
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.UserType
|
||||
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.UpdateDeviceStatusAction
|
||||
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.util.AndroidVersion
|
||||
import io.timelimit.android.util.TimeTextUtil
|
||||
|
@ -393,33 +391,50 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
|||
val oldSessionDuration = handling.remainingSessionDuration?.let { it - timeToSubtractForCategory }
|
||||
|
||||
// trigger time warnings
|
||||
fun showTimeWarningNotification(title: Int, roundedNewTime: Long) {
|
||||
appLogic.platformIntegration.showTimeWarningNotification(
|
||||
title = appLogic.context.getString(title, category.title),
|
||||
text = TimeTextUtil.remaining(roundedNewTime.toInt(), appLogic.context)
|
||||
)
|
||||
fun handleTimeWarnings(
|
||||
notificationTitleStringResource: Int,
|
||||
roundedNewTimeInMilliseconds: Long
|
||||
) {
|
||||
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)) {
|
||||
// eventually show remaining time warning
|
||||
val roundedNewTime = ((newRemainingTime / (1000 * 60)) + 1) * (1000 * 60)
|
||||
val flagIndex = CategoryTimeWarnings.durationToBitIndex[roundedNewTime]
|
||||
|
||||
if (flagIndex != null && category.timeWarnings and (1 shl flagIndex) != 0) {
|
||||
showTimeWarningNotification(title = R.string.time_warning_not_title, roundedNewTime = roundedNewTime)
|
||||
}
|
||||
handleTimeWarnings(
|
||||
notificationTitleStringResource = R.string.time_warning_not_title,
|
||||
roundedNewTimeInMilliseconds = ((newRemainingTime / (1000 * 60)) + 1) * 1000 * 60
|
||||
)
|
||||
}
|
||||
|
||||
if (oldSessionDuration != null) {
|
||||
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) {
|
||||
showTimeWarningNotification(title = R.string.time_warning_not_title_session, roundedNewTime = roundedNewTime)
|
||||
}
|
||||
if (oldSessionDuration / (1000 * 60) != newSessionDuration / (1000 * 60)) {
|
||||
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) {
|
||||
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) {
|
||||
showTimeWarningNotification(title = R.string.time_warning_not_title_blocked_time_area, roundedNewTime = msUntilNextBlocking)
|
||||
}
|
||||
handleTimeWarnings(
|
||||
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()
|
||||
}
|
||||
}
|
||||
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 {
|
||||
const val TYPE_VALUE = "UPDATE_CATEGORY_TIME_WARNINGS"
|
||||
private const val CATEGORY_ID = "categoryId"
|
||||
private const val ENABLE = "enable"
|
||||
private const val FLAGS = "flags"
|
||||
private const val MINUTES = "minutes"
|
||||
}
|
||||
|
||||
init {
|
||||
IdGenerator.assertIdValid(categoryId)
|
||||
|
||||
if (minutes != null) {
|
||||
if (minutes < CategoryTimeWarning.MIN || minutes > CategoryTimeWarning.MAX) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun serialize(writer: JsonWriter) {
|
||||
|
@ -746,6 +758,10 @@ data class UpdateCategoryTimeWarningsAction(val categoryId: String, val enable:
|
|||
writer.name(ENABLE).value(enable)
|
||||
writer.name(FLAGS).value(flags)
|
||||
|
||||
if (minutes != null) {
|
||||
writer.name(MINUTES).value(minutes)
|
||||
}
|
||||
|
||||
writer.endObject()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -686,6 +686,19 @@ object LocalDatabaseParentActionDispatcher {
|
|||
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
|
||||
}
|
||||
is UpdateCategoryBatteryLimit -> {
|
||||
|
|
|
@ -459,7 +459,8 @@ data class ServerUpdatedCategoryBaseData(
|
|||
val networks: List<ServerCategoryNetworkId>,
|
||||
val disableLimitsUntil: Long,
|
||||
val flags: Long,
|
||||
val blockNotificationDelay: Long
|
||||
val blockNotificationDelay: Long,
|
||||
val additionalTimeWarnings: Set<Int>
|
||||
) {
|
||||
companion object {
|
||||
private const val CATEGORY_ID = "categoryId"
|
||||
|
@ -481,6 +482,7 @@ data class ServerUpdatedCategoryBaseData(
|
|||
private const val DISABLE_LIMITS_UNTIL = "dlu"
|
||||
private const val FLAGS = "flags"
|
||||
private const val BLOCK_NOTIFICATION_DELAY = "blockNotificationDelay"
|
||||
private const val ADDITIONAL_TIME_WARNINGS = "atw"
|
||||
|
||||
fun parse(reader: JsonReader): ServerUpdatedCategoryBaseData {
|
||||
var categoryId: String? = null
|
||||
|
@ -503,6 +505,7 @@ data class ServerUpdatedCategoryBaseData(
|
|||
var disableLimitsUntil = 0L
|
||||
var flags = 0L
|
||||
var blockNotificationDelay = 0L
|
||||
var additionalTimeWarnings = emptySet<Int>()
|
||||
|
||||
reader.beginObject()
|
||||
while (reader.hasNext()) {
|
||||
|
@ -526,31 +529,41 @@ data class ServerUpdatedCategoryBaseData(
|
|||
DISABLE_LIMITS_UNTIL -> disableLimitsUntil = reader.nextLong()
|
||||
FLAGS -> flags = 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()
|
||||
}
|
||||
}
|
||||
reader.endObject()
|
||||
|
||||
return ServerUpdatedCategoryBaseData(
|
||||
categoryId = categoryId!!,
|
||||
childId = childId!!,
|
||||
title = title!!,
|
||||
blockedMinutesInWeek = blockedMinutesInWeek!!,
|
||||
extraTimeInMillis = extraTimeInMillis!!,
|
||||
extraTimeDay = extraTimeDay,
|
||||
temporarilyBlocked = temporarilyBlocked!!,
|
||||
temporarilyBlockedEndTime = temporarilyBlockedEndTime,
|
||||
baseDataVersion = baseDataVersion!!,
|
||||
parentCategoryId = parentCategoryId!!,
|
||||
blockAllNotifications = blockAllNotifications,
|
||||
timeWarnings = timeWarnings,
|
||||
minBatteryLevelCharging = minBatteryLevelCharging,
|
||||
minBatteryLevelMobile = minBatteryLevelMobile,
|
||||
sort = sort,
|
||||
networks = networks,
|
||||
disableLimitsUntil = disableLimitsUntil,
|
||||
flags = flags,
|
||||
blockNotificationDelay = blockNotificationDelay
|
||||
categoryId = categoryId!!,
|
||||
childId = childId!!,
|
||||
title = title!!,
|
||||
blockedMinutesInWeek = blockedMinutesInWeek!!,
|
||||
extraTimeInMillis = extraTimeInMillis!!,
|
||||
extraTimeDay = extraTimeDay,
|
||||
temporarilyBlocked = temporarilyBlocked!!,
|
||||
temporarilyBlockedEndTime = temporarilyBlockedEndTime,
|
||||
baseDataVersion = baseDataVersion!!,
|
||||
parentCategoryId = parentCategoryId!!,
|
||||
blockAllNotifications = blockAllNotifications,
|
||||
timeWarnings = timeWarnings,
|
||||
minBatteryLevelCharging = minBatteryLevelCharging,
|
||||
minBatteryLevelMobile = minBatteryLevelMobile,
|
||||
sort = sort,
|
||||
networks = networks,
|
||||
disableLimitsUntil = disableLimitsUntil,
|
||||
flags = flags,
|
||||
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
|
||||
* 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 androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
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.manage.category.settings.addusedtime.AddUsedTimeDialogFragment
|
||||
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.util.bind
|
||||
|
||||
|
@ -46,6 +49,7 @@ class CategorySettingsFragment : Fragment() {
|
|||
private const val PERMISSION_REQUEST_CODE = 1
|
||||
private const val CHILD_ID = "childId"
|
||||
private const val CATEGORY_ID = "categoryId"
|
||||
private const val TIME_WARNING_STATUS = "timeWarningStatus"
|
||||
|
||||
fun newInstance(childId: String, categoryId: String) = CategorySettingsFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
|
@ -61,10 +65,35 @@ class CategorySettingsFragment : Fragment() {
|
|||
private val categoryId: String get() = requireArguments().getString(CATEGORY_ID)!!
|
||||
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? {
|
||||
val binding = FragmentCategorySettingsBinding.inflate(inflater, container, false)
|
||||
|
||||
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 ->
|
||||
liveDataFromFunction (1000 * 10L) { DateInTimezone.newInstance(appLogic.timeApi.getCurrentTimeInMillis(), timezone) }
|
||||
|
@ -122,11 +151,13 @@ class CategorySettingsFragment : Fragment() {
|
|||
)
|
||||
|
||||
CategoryTimeWarningView.bind(
|
||||
view = binding.timeWarnings,
|
||||
auth = auth,
|
||||
categoryLive = categoryEntry,
|
||||
lifecycleOwner = this,
|
||||
fragmentManager = parentFragmentManager
|
||||
view = binding.timeWarnings,
|
||||
auth = auth,
|
||||
statusLive = timeWarningStatus,
|
||||
lifecycleOwner = this,
|
||||
fragmentManager = parentFragmentManager,
|
||||
categoryId = categoryId,
|
||||
serverApiLevelInfo = appLogic.serverApiLevelLogic.infoLive
|
||||
)
|
||||
|
||||
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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -89,13 +89,23 @@ object DuplicateChildActions {
|
|||
oldCategory.category.timeWarnings.let { timeWarnings ->
|
||||
if (timeWarnings != 0) {
|
||||
result.add(UpdateCategoryTimeWarningsAction(
|
||||
categoryId = newCategoryId,
|
||||
enable = true,
|
||||
flags = timeWarnings
|
||||
categoryId = newCategoryId,
|
||||
enable = true,
|
||||
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) {
|
||||
result.add(UpdateCategoryBatteryLimit(
|
||||
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"?>
|
||||
<!--
|
||||
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
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation version 3 of the License.
|
||||
|
@ -45,6 +45,14 @@
|
|||
<!-- checkboxes will be added hear -->
|
||||
</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>
|
||||
</androidx.cardview.widget.CardView>
|
||||
</layout>
|
|
@ -1123,6 +1123,7 @@
|
|||
|
||||
<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="time_warning_custom_add">Zeitwarnung hinzufügen</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>
|
||||
|
|
|
@ -1168,6 +1168,7 @@
|
|||
|
||||
<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="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_text">Notification if the premium version expires shortly</string>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue