mirror of
https://codeberg.org/timelimit/timelimit-android.git
synced 2025-10-03 09:49:25 +02:00
Add new limit options
This commit is contained in:
parent
2945932c2b
commit
5a21314495
58 changed files with 3099 additions and 387 deletions
|
@ -158,6 +158,7 @@ dependencies {
|
|||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||
implementation "com.google.android.material:material:1.1.0"
|
||||
|
||||
implementation "android.arch.navigation:navigation-fragment-ktx:$nav_version"
|
||||
|
|
1006
app/schemas/io.timelimit.android.data.RoomDatabase/29.json
Normal file
1006
app/schemas/io.timelimit.android.data.RoomDatabase/29.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,16 +1,132 @@
|
|||
/*
|
||||
* 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.sync.actions
|
||||
|
||||
import io.timelimit.android.data.model.AppRecommendation
|
||||
import io.timelimit.android.integration.platform.NewPermissionStatus
|
||||
import io.timelimit.android.integration.platform.ProtectionLevel
|
||||
import io.timelimit.android.integration.platform.RuntimePermissionStatus
|
||||
import io.timelimit.android.sync.network.ParentPassword
|
||||
import org.json.JSONObject
|
||||
import org.junit.Test
|
||||
|
||||
class Actions {
|
||||
@Test
|
||||
fun decrementCategoryExtraTimeShouldBeSerializedAndParsedCorrectly() {
|
||||
val originalAction = DecrementCategoryExtraTimeAction(categoryId = "abcdef", extraTimeToSubtract = 1000 * 30)
|
||||
private val appLogicActions: List<AppLogicAction> = listOf(
|
||||
AddUsedTimeActionVersion2(
|
||||
trustedTimestamp = 13,
|
||||
dayOfEpoch = 674,
|
||||
items = listOf(
|
||||
AddUsedTimeActionItem(
|
||||
categoryId = "abcdef",
|
||||
sessionDurationLimits = setOf(
|
||||
AddUsedTimeActionItemSessionDurationLimitSlot(
|
||||
startMinuteOfDay = 10,
|
||||
endMinuteOfDay = 23,
|
||||
sessionPauseDuration = 1000,
|
||||
maxSessionDuration = 2000
|
||||
),
|
||||
AddUsedTimeActionItemSessionDurationLimitSlot(
|
||||
startMinuteOfDay = 14,
|
||||
endMinuteOfDay = 23,
|
||||
sessionPauseDuration = 1000,
|
||||
maxSessionDuration = 2000
|
||||
)
|
||||
),
|
||||
additionalCountingSlots = setOf(
|
||||
AddUsedTimeActionItemAdditionalCountingSlot(21, 31),
|
||||
AddUsedTimeActionItemAdditionalCountingSlot(30, 55)
|
||||
),
|
||||
extraTimeToSubtract = 100,
|
||||
timeToAdd = 1255
|
||||
)
|
||||
)
|
||||
),
|
||||
AddInstalledAppsAction(
|
||||
apps = listOf(
|
||||
InstalledApp(
|
||||
packageName = "com.demo.app",
|
||||
isLaunchable = true,
|
||||
title = "Demo",
|
||||
recommendation = AppRecommendation.Blacklist
|
||||
)
|
||||
)
|
||||
),
|
||||
RemoveInstalledAppsAction(
|
||||
packageNames = listOf("com.something.test")
|
||||
),
|
||||
UpdateAppActivitiesAction(
|
||||
removedActivities = listOf("com.demo" to "com.demo.MainActivity", "com.demo" to "com.demo.DemoActivity"),
|
||||
updatedOrAddedActivities = listOf(
|
||||
AppActivityItem(
|
||||
packageName = "com.demo.two",
|
||||
title = "Test",
|
||||
className = "com.demo.TwoActivity"
|
||||
)
|
||||
)
|
||||
),
|
||||
SignOutAtDeviceAction,
|
||||
UpdateDeviceStatusAction(
|
||||
newProtectionLevel = ProtectionLevel.PasswordDeviceAdmin,
|
||||
didReboot = true,
|
||||
isQOrLaterNow = true,
|
||||
newAccessibilityServiceEnabled = true,
|
||||
newAppVersion = 10,
|
||||
newNotificationAccessPermission = NewPermissionStatus.Granted,
|
||||
newOverlayPermission = RuntimePermissionStatus.NotRequired,
|
||||
newUsageStatsPermissionStatus = RuntimePermissionStatus.NotGranted
|
||||
),
|
||||
TriedDisablingDeviceAdminAction
|
||||
)
|
||||
|
||||
private val parentActions: List<ParentAction> = listOf(
|
||||
AddCategoryAppsAction(
|
||||
categoryId = "abedge",
|
||||
packageNames = listOf("com.demo.one", "com.demo.two")
|
||||
)
|
||||
// this list does not contain all actions
|
||||
)
|
||||
|
||||
private val childActions: List<ChildAction> = listOf(
|
||||
ChildSignInAction,
|
||||
ChildChangePasswordAction(
|
||||
password = ParentPassword.createSync("test")
|
||||
)
|
||||
)
|
||||
|
||||
@Test
|
||||
fun testActionSerializationAndDeserializationWorks() {
|
||||
appLogicActions.forEach { originalAction ->
|
||||
val serializedAction = SerializationUtil.serializeAction(originalAction)
|
||||
val parsedAction = ActionParser.parseAppLogicAction(JSONObject(serializedAction))
|
||||
|
||||
assert(parsedAction == originalAction)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCanSerializeParentActions() {
|
||||
parentActions.forEach {
|
||||
SerializationUtil.serializeAction(it)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCanSerializeChildActions() {
|
||||
childActions.forEach {
|
||||
SerializationUtil.serializeAction(it)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -33,6 +33,7 @@ interface Database {
|
|||
fun notification(): NotificationDao
|
||||
fun allowedContact(): AllowedContactDao
|
||||
fun userKey(): UserKeyDao
|
||||
fun sessionDuration(): SessionDurationDao
|
||||
|
||||
fun beginTransaction()
|
||||
fun setTransactionSuccessful()
|
||||
|
|
|
@ -17,6 +17,8 @@ package io.timelimit.android.data
|
|||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import io.timelimit.android.data.model.TimeLimitRule
|
||||
import io.timelimit.android.extensions.MinuteOfDay
|
||||
|
||||
object DatabaseMigrations {
|
||||
val MIGRATE_TO_V2 = object: Migration(1, 2) {
|
||||
|
@ -199,4 +201,21 @@ object DatabaseMigrations {
|
|||
database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_user_key_key` ON `user_key` (`key`)")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V29 = object: Migration(28, 29) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE `time_limit_rule` ADD COLUMN `start_minute_of_day` INTEGER NOT NULL DEFAULT ${TimeLimitRule.MIN_START_MINUTE}")
|
||||
database.execSQL("ALTER TABLE `time_limit_rule` ADD COLUMN `end_minute_of_day` INTEGER NOT NULL DEFAULT ${TimeLimitRule.MAX_END_MINUTE}")
|
||||
database.execSQL("ALTER TABLE `time_limit_rule` ADD COLUMN `session_duration_milliseconds` INTEGER NOT NULL DEFAULT 0")
|
||||
database.execSQL("ALTER TABLE `time_limit_rule` ADD COLUMN `session_pause_milliseconds` INTEGER NOT NULL DEFAULT 0")
|
||||
|
||||
database.execSQL("ALTER TABLE `used_time` RENAME TO `used_time_old`")
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `used_time` (`day_of_epoch` INTEGER NOT NULL, `used_time` INTEGER NOT NULL, `category_id` TEXT NOT NULL, `start_time_of_day` INTEGER NOT NULL, `end_time_of_day` INTEGER NOT NULL, PRIMARY KEY(`category_id`, `day_of_epoch`, `start_time_of_day`, `end_time_of_day`))")
|
||||
database.execSQL("INSERT INTO `used_time` SELECT `day_of_epoch`, `used_time`, `category_id`, ${MinuteOfDay.MIN} AS `start_time_of_day`, ${MinuteOfDay.MAX} AS `end_time_of_day` FROM `used_time_old`")
|
||||
database.execSQL("DROP TABLE `used_time_old`")
|
||||
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `session_duration` (`category_id` TEXT NOT NULL, `max_session_duration` INTEGER NOT NULL, `session_pause_duration` INTEGER NOT NULL, `start_minute_of_day` INTEGER NOT NULL, `end_minute_of_day` INTEGER NOT NULL, `last_usage` INTEGER NOT NULL, `last_session_duration` INTEGER NOT NULL, PRIMARY KEY(`category_id`, `max_session_duration`, `session_pause_duration`, `start_minute_of_day`, `end_minute_of_day`), FOREIGN KEY(`category_id`) REFERENCES `category`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )")
|
||||
database.execSQL("CREATE INDEX IF NOT EXISTS `session_duration_index_category_id` ON `session_duration` (`category_id`)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,8 +35,9 @@ import io.timelimit.android.data.model.*
|
|||
AppActivity::class,
|
||||
Notification::class,
|
||||
AllowedContact::class,
|
||||
UserKey::class
|
||||
], version = 28)
|
||||
UserKey::class,
|
||||
SessionDuration::class
|
||||
], version = 29)
|
||||
abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database {
|
||||
companion object {
|
||||
private val lock = Object()
|
||||
|
@ -98,7 +99,8 @@ abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database
|
|||
DatabaseMigrations.MIGRATE_TO_V25,
|
||||
DatabaseMigrations.MIGRATE_TO_V26,
|
||||
DatabaseMigrations.MIGRATE_TO_V27,
|
||||
DatabaseMigrations.MIGRATE_TO_V28
|
||||
DatabaseMigrations.MIGRATE_TO_V28,
|
||||
DatabaseMigrations.MIGRATE_TO_V29
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
|
|
@ -36,12 +36,13 @@ object DatabaseBackupLowlevel {
|
|||
private const val DEVICE = "device"
|
||||
private const val PENDING_SYNC_ACTION = "pendingSyncAction"
|
||||
private const val TIME_LIMIT_RULE = "timelimitRule"
|
||||
private const val USED_TIME_ITEM = "usedTime"
|
||||
private const val USED_TIME_ITEM = "usedTimeV2"
|
||||
private const val USER = "user"
|
||||
private const val APP_ACTIVITY = "appActivity"
|
||||
private const val NOTIFICATION = "notification"
|
||||
private const val ALLOWED_CONTACT = "allowedContact"
|
||||
private const val USER_KEY = "userKey"
|
||||
private const val SESSION_DURATION = "sessionDuration"
|
||||
|
||||
fun outputAsBackupJson(database: Database, outputStream: OutputStream) {
|
||||
val writer = JsonWriter(OutputStreamWriter(outputStream, Charsets.UTF_8))
|
||||
|
@ -87,6 +88,7 @@ object DatabaseBackupLowlevel {
|
|||
handleCollection(NOTIFICATION) { offset, pageSize -> database.notification().getNotificationPageSync(offset, pageSize) }
|
||||
handleCollection(ALLOWED_CONTACT) { offset, pageSize -> database.allowedContact().getAllowedContactPageSync(offset, pageSize) }
|
||||
handleCollection(USER_KEY) { offset, pageSize -> database.userKey().getUserKeyPageSync(offset, pageSize) }
|
||||
handleCollection(SESSION_DURATION) { offset, pageSize -> database.sessionDuration().getSessionDurationPageSync(offset, pageSize) }
|
||||
|
||||
writer.endObject().flush()
|
||||
}
|
||||
|
@ -226,6 +228,15 @@ object DatabaseBackupLowlevel {
|
|||
|
||||
reader.endArray()
|
||||
}
|
||||
SESSION_DURATION -> {
|
||||
reader.beginArray()
|
||||
|
||||
while (reader.hasNext()) {
|
||||
database.sessionDuration().addSessionDurationIgnoreErrorsSync(SessionDuration.parse(reader))
|
||||
}
|
||||
|
||||
reader.endArray()
|
||||
}
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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.data.dao
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.*
|
||||
import io.timelimit.android.data.model.SessionDuration
|
||||
|
||||
@Dao
|
||||
interface SessionDurationDao {
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
fun addSessionDurationIgnoreErrorsSync(item: SessionDuration)
|
||||
|
||||
@Query("SELECT * FROM session_duration LIMIT :pageSize OFFSET :offset")
|
||||
fun getSessionDurationPageSync(offset: Int, pageSize: Int): List<SessionDuration>
|
||||
|
||||
@Query("SELECT * FROM session_duration WHERE category_id = :categoryId AND max_session_duration = :maxSessionDuration AND session_pause_duration = :sessionPauseDuration AND start_minute_of_day = :startMinuteOfDay AND end_minute_of_day = :endMinuteOfDay")
|
||||
fun getSessionDurationItemSync(
|
||||
categoryId: String, maxSessionDuration: Int, sessionPauseDuration: Int, startMinuteOfDay: Int, endMinuteOfDay: Int
|
||||
): SessionDuration?
|
||||
|
||||
@Query("SELECT * FROM session_duration WHERE category_id = :categoryId")
|
||||
fun getSessionDurationItemsByCategoryId(categoryId: String): LiveData<List<SessionDuration>>
|
||||
|
||||
@Insert
|
||||
fun insertSessionDurationItemSync(item: SessionDuration)
|
||||
|
||||
@Insert
|
||||
fun insertSessionDurationItemsSync(item: List<SessionDuration>)
|
||||
|
||||
@Update
|
||||
fun updateSessionDurationItemSync(item: SessionDuration)
|
||||
|
||||
@Query("DELETE FROM session_duration WHERE last_usage + MIN(session_pause_duration + 1000 * 60 * 60, 1000 * 60 * 60 * 24) < :trustedTimestamp")
|
||||
fun deleteOldSessionDurationItemsSync(trustedTimestamp: Long)
|
||||
|
||||
@Query("DELETE FROM session_duration WHERE category_id = :categoryId")
|
||||
fun deleteByCategoryId(categoryId: String)
|
||||
}
|
|
@ -15,14 +15,13 @@
|
|||
*/
|
||||
package io.timelimit.android.data.dao
|
||||
|
||||
import android.util.SparseArray
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.paging.DataSource
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import io.timelimit.android.data.model.UsedTimeItem
|
||||
import io.timelimit.android.data.model.UsedTimeListItem
|
||||
import io.timelimit.android.livedata.ignoreUnchanged
|
||||
|
||||
@Dao
|
||||
|
@ -30,16 +29,8 @@ abstract class UsedTimeDao {
|
|||
@Query("SELECT * FROM used_time WHERE category_id = :categoryId AND day_of_epoch >= :startingDayOfEpoch AND day_of_epoch <= :endDayOfEpoch")
|
||||
protected abstract fun getUsedTimesOfWeekInternal(categoryId: String, startingDayOfEpoch: Int, endDayOfEpoch: Int): LiveData<List<UsedTimeItem>>
|
||||
|
||||
fun getUsedTimesOfWeek(categoryId: String, firstDayOfWeekAsEpochDay: Int): LiveData<SparseArray<UsedTimeItem>> {
|
||||
return Transformations.map(getUsedTimesOfWeekInternal(categoryId, firstDayOfWeekAsEpochDay, firstDayOfWeekAsEpochDay + 6).ignoreUnchanged()) {
|
||||
val result = SparseArray<UsedTimeItem>()
|
||||
|
||||
it.forEach {
|
||||
result.put(it.dayOfEpoch - firstDayOfWeekAsEpochDay, it)
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
fun getUsedTimesOfWeek(categoryId: String, firstDayOfWeekAsEpochDay: Int): LiveData<List<UsedTimeItem>> {
|
||||
return getUsedTimesOfWeekInternal(categoryId, firstDayOfWeekAsEpochDay, firstDayOfWeekAsEpochDay + 6).ignoreUnchanged()
|
||||
}
|
||||
|
||||
@Insert
|
||||
|
@ -48,14 +39,11 @@ abstract class UsedTimeDao {
|
|||
@Insert
|
||||
abstract fun insertUsedTimes(item: List<UsedTimeItem>)
|
||||
|
||||
@Query("UPDATE used_time SET used_time = :newUsedTime WHERE category_id = :categoryId AND day_of_epoch = :dayOfEpoch")
|
||||
abstract fun updateUsedTime(categoryId: String, dayOfEpoch: Int, newUsedTime: Long)
|
||||
@Query("UPDATE used_time SET used_time = used_time + :timeToAdd WHERE category_id = :categoryId AND day_of_epoch = :dayOfEpoch AND start_time_of_day = :start AND end_time_of_day = :end")
|
||||
abstract fun addUsedTime(categoryId: String, dayOfEpoch: Int, timeToAdd: Int, start: Int, end: Int): Int
|
||||
|
||||
@Query("UPDATE used_time SET used_time = used_time + :timeToAdd WHERE category_id = :categoryId AND day_of_epoch = :dayOfEpoch")
|
||||
abstract fun addUsedTime(categoryId: String, dayOfEpoch: Int, timeToAdd: Int): Int
|
||||
|
||||
@Query("SELECT * FROM used_time WHERE category_id = :categoryId AND day_of_epoch = :dayOfEpoch")
|
||||
abstract fun getUsedTimeItem(categoryId: String, dayOfEpoch: Int): LiveData<UsedTimeItem?>
|
||||
@Query("SELECT * FROM used_time WHERE category_id = :categoryId AND day_of_epoch = :dayOfEpoch AND start_time_of_day = :start AND end_time_of_day = :end")
|
||||
abstract fun getUsedTimeItemSync(categoryId: String, dayOfEpoch: Int, start: Int, end: Int): UsedTimeItem?
|
||||
|
||||
@Query("DELETE FROM used_time WHERE category_id = :categoryId")
|
||||
abstract fun deleteUsedTimeItems(categoryId: String)
|
||||
|
@ -66,12 +54,13 @@ abstract class UsedTimeDao {
|
|||
@Query("SELECT * FROM used_time LIMIT :pageSize OFFSET :offset")
|
||||
abstract fun getUsedTimePageSync(offset: Int, pageSize: Int): List<UsedTimeItem>
|
||||
|
||||
@Query("SELECT * FROM used_time WHERE category_id = :categoryId ORDER BY day_of_epoch DESC")
|
||||
abstract fun getUsedTimesByCategoryId(categoryId: String): DataSource.Factory<Int, UsedTimeItem>
|
||||
|
||||
@Query("SELECT * FROM used_time WHERE category_id IN (:categoryIds) AND day_of_epoch >= :startingDayOfEpoch AND day_of_epoch <= :endDayOfEpoch")
|
||||
abstract fun getUsedTimesByDayAndCategoryIds(categoryIds: List<String>, startingDayOfEpoch: Int, endDayOfEpoch: Int): LiveData<List<UsedTimeItem>>
|
||||
|
||||
@Query("SELECT * FROM used_time")
|
||||
abstract fun getAllUsedTimeItemsSync(): List<UsedTimeItem>
|
||||
|
||||
// breaking it into multiple lines causes issues during compilation ...
|
||||
@Query("SELECT 2 AS type, start_time_of_day AS startMinuteOfDay, end_time_of_day AS endMinuteOfDay, used_time AS duration, day_of_epoch AS day, NULL AS lastUsage, NULL AS maxSessionDuration, NULL AS pauseDuration FROM used_time WHERE category_id = :categoryId UNION ALL SELECT 1 AS type, start_minute_of_day AS startMinuteOfDay, end_minute_of_day AS endMinuteOfDay, last_session_duration AS duration, NULL AS day, last_usage AS lastUsage, max_session_duration AS maxSessionDuration, session_pause_duration AS pauseDuration FROM session_duration WHERE category_id = :categoryId ORDER BY type, day DESC, lastUsage DESC, startMinuteOfDay, endMinuteOfDay")
|
||||
abstract fun getUsedTimeListItemsByCategoryId(categoryId: String): DataSource.Factory<Int, UsedTimeListItem>
|
||||
}
|
||||
|
|
|
@ -0,0 +1,145 @@
|
|||
/*
|
||||
* 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.data.model
|
||||
|
||||
import android.util.JsonReader
|
||||
import android.util.JsonWriter
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.Index
|
||||
import io.timelimit.android.data.IdGenerator
|
||||
import io.timelimit.android.data.JsonSerializable
|
||||
import io.timelimit.android.extensions.MinuteOfDay
|
||||
|
||||
@Entity(
|
||||
tableName = "session_duration",
|
||||
primaryKeys = [
|
||||
"category_id",
|
||||
"max_session_duration",
|
||||
"session_pause_duration",
|
||||
"start_minute_of_day",
|
||||
"end_minute_of_day"
|
||||
],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = Category::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["category_id"],
|
||||
onDelete = ForeignKey.CASCADE,
|
||||
onUpdate = ForeignKey.CASCADE
|
||||
)
|
||||
],
|
||||
indices = [
|
||||
Index(
|
||||
name = "session_duration_index_category_id",
|
||||
value = ["category_id"],
|
||||
unique = false
|
||||
)
|
||||
]
|
||||
)
|
||||
data class SessionDuration(
|
||||
@ColumnInfo(name = "category_id")
|
||||
val categoryId: String,
|
||||
@ColumnInfo(name = "max_session_duration")
|
||||
val maxSessionDuration: Int,
|
||||
@ColumnInfo(name = "session_pause_duration")
|
||||
val sessionPauseDuration: Int,
|
||||
@ColumnInfo(name = "start_minute_of_day")
|
||||
val startMinuteOfDay: Int,
|
||||
@ColumnInfo(name = "end_minute_of_day")
|
||||
val endMinuteOfDay: Int,
|
||||
@ColumnInfo(name = "last_usage")
|
||||
val lastUsage: Long,
|
||||
@ColumnInfo(name = "last_session_duration")
|
||||
val lastSessionDuration: Long
|
||||
): JsonSerializable {
|
||||
companion object {
|
||||
private const val CATEGORY_ID = "c"
|
||||
private const val MAX_SESSION_DURATION = "md"
|
||||
private const val SESSION_PAUSE_DURATION = "spd"
|
||||
private const val START_MINUTE_OF_DAY = "sm"
|
||||
private const val END_MINUTE_OF_DAY = "em"
|
||||
private const val LAST_USAGE = "l"
|
||||
private const val LAST_SESSION_DURATION = "d"
|
||||
|
||||
fun parse(reader: JsonReader): SessionDuration {
|
||||
var categoryId: String? = null
|
||||
var maxSessionDuration: Int? = null
|
||||
var sessionPauseDuration: Int? = null
|
||||
var startMinuteOfDay: Int? = null
|
||||
var endMinuteOfDay: Int? = null
|
||||
var lastUsage: Long? = null
|
||||
var lastSessionDuration: Long? = null
|
||||
|
||||
reader.beginObject()
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
CATEGORY_ID -> categoryId = reader.nextString()
|
||||
MAX_SESSION_DURATION -> maxSessionDuration = reader.nextInt()
|
||||
SESSION_PAUSE_DURATION -> sessionPauseDuration = reader.nextInt()
|
||||
START_MINUTE_OF_DAY -> startMinuteOfDay = reader.nextInt()
|
||||
END_MINUTE_OF_DAY -> endMinuteOfDay = reader.nextInt()
|
||||
LAST_USAGE -> lastUsage = reader.nextLong()
|
||||
LAST_SESSION_DURATION -> lastSessionDuration = reader.nextLong()
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
reader.endObject()
|
||||
|
||||
return SessionDuration(
|
||||
categoryId = categoryId!!,
|
||||
maxSessionDuration = maxSessionDuration!!,
|
||||
sessionPauseDuration = sessionPauseDuration!!,
|
||||
startMinuteOfDay = startMinuteOfDay!!,
|
||||
endMinuteOfDay = endMinuteOfDay!!,
|
||||
lastUsage = lastUsage!!,
|
||||
lastSessionDuration = lastSessionDuration!!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
IdGenerator.assertIdValid(categoryId)
|
||||
|
||||
if (maxSessionDuration <= 0 || sessionPauseDuration <= 0) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
if (!(MinuteOfDay.isValid(startMinuteOfDay) && MinuteOfDay.isValid(endMinuteOfDay))) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
if (lastUsage < 0 || lastSessionDuration < 0) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
|
||||
override fun serialize(writer: JsonWriter) {
|
||||
writer.beginObject()
|
||||
|
||||
writer.name(CATEGORY_ID).value(categoryId)
|
||||
writer.name(MAX_SESSION_DURATION).value(maxSessionDuration)
|
||||
writer.name(SESSION_PAUSE_DURATION).value(sessionPauseDuration)
|
||||
writer.name(START_MINUTE_OF_DAY).value(startMinuteOfDay)
|
||||
writer.name(END_MINUTE_OF_DAY).value(endMinuteOfDay)
|
||||
writer.name(LAST_USAGE).value(lastUsage)
|
||||
writer.name(LAST_SESSION_DURATION).value(lastSessionDuration)
|
||||
|
||||
writer.endObject()
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
* 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
|
||||
|
@ -18,6 +18,7 @@ package io.timelimit.android.data.model
|
|||
import android.os.Parcelable
|
||||
import android.util.JsonReader
|
||||
import android.util.JsonWriter
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
@ -25,6 +26,9 @@ import androidx.room.TypeConverters
|
|||
import io.timelimit.android.data.IdGenerator
|
||||
import io.timelimit.android.data.JsonSerializable
|
||||
import io.timelimit.android.data.customtypes.ImmutableBitmaskAdapter
|
||||
import io.timelimit.android.extensions.MinuteOfDay
|
||||
import io.timelimit.android.livedata.ignoreUnchanged
|
||||
import io.timelimit.android.livedata.map
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Entity(tableName = "time_limit_rule")
|
||||
|
@ -41,7 +45,15 @@ data class TimeLimitRule(
|
|||
@ColumnInfo(name = "day_mask")
|
||||
val dayMask: Byte,
|
||||
@ColumnInfo(name = "max_time")
|
||||
val maximumTimeInMillis: Int
|
||||
val maximumTimeInMillis: Int,
|
||||
@ColumnInfo(name = "start_minute_of_day")
|
||||
val startMinuteOfDay: Int,
|
||||
@ColumnInfo(name = "end_minute_of_day")
|
||||
val endMinuteOfDay: Int,
|
||||
@ColumnInfo(name = "session_duration_milliseconds")
|
||||
val sessionDurationMilliseconds: Int,
|
||||
@ColumnInfo(name = "session_pause_milliseconds")
|
||||
val sessionPauseMilliseconds: Int
|
||||
): Parcelable, JsonSerializable {
|
||||
companion object {
|
||||
private const val RULE_ID = "ruleId"
|
||||
|
@ -49,6 +61,13 @@ data class TimeLimitRule(
|
|||
private const val MAX_TIME_IN_MILLIS = "time"
|
||||
private const val DAY_MASK = "days"
|
||||
private const val APPLY_TO_EXTRA_TIME_USAGE = "extraTime"
|
||||
private const val START_MINUTE_OF_DAY = "start"
|
||||
private const val END_MINUTE_OF_DAY = "end"
|
||||
private const val SESSION_DURATION_MILLISECONDS = "dur"
|
||||
private const val SESSION_PAUSE_MILLISECONDS = "pause"
|
||||
|
||||
const val MIN_START_MINUTE = MinuteOfDay.MIN
|
||||
const val MAX_END_MINUTE = MinuteOfDay.MAX
|
||||
|
||||
fun parse(reader: JsonReader): TimeLimitRule {
|
||||
var id: String? = null
|
||||
|
@ -56,6 +75,10 @@ data class TimeLimitRule(
|
|||
var applyToExtraTimeUsage: Boolean? = null
|
||||
var dayMask: Byte? = null
|
||||
var maximumTimeInMillis: Int? = null
|
||||
var startMinuteOfDay = MIN_START_MINUTE
|
||||
var endMinuteOfDay = MAX_END_MINUTE
|
||||
var sessionDurationMilliseconds = 0
|
||||
var sessionPauseMilliseconds = 0
|
||||
|
||||
reader.beginObject()
|
||||
|
||||
|
@ -66,6 +89,10 @@ data class TimeLimitRule(
|
|||
MAX_TIME_IN_MILLIS -> maximumTimeInMillis = reader.nextInt()
|
||||
DAY_MASK -> dayMask = reader.nextInt().toByte()
|
||||
APPLY_TO_EXTRA_TIME_USAGE -> applyToExtraTimeUsage = reader.nextBoolean()
|
||||
START_MINUTE_OF_DAY -> startMinuteOfDay = reader.nextInt()
|
||||
END_MINUTE_OF_DAY -> endMinuteOfDay = reader.nextInt()
|
||||
SESSION_DURATION_MILLISECONDS -> sessionDurationMilliseconds = reader.nextInt()
|
||||
SESSION_PAUSE_MILLISECONDS -> sessionPauseMilliseconds = reader.nextInt()
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
|
@ -77,7 +104,11 @@ data class TimeLimitRule(
|
|||
categoryId = categoryId!!,
|
||||
applyToExtraTimeUsage = applyToExtraTimeUsage!!,
|
||||
dayMask = dayMask!!,
|
||||
maximumTimeInMillis = maximumTimeInMillis!!
|
||||
maximumTimeInMillis = maximumTimeInMillis!!,
|
||||
startMinuteOfDay = startMinuteOfDay,
|
||||
endMinuteOfDay = endMinuteOfDay,
|
||||
sessionDurationMilliseconds = sessionDurationMilliseconds,
|
||||
sessionPauseMilliseconds = sessionPauseMilliseconds
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -93,8 +124,22 @@ data class TimeLimitRule(
|
|||
if (dayMask < 0 || dayMask > (1 or 2 or 4 or 8 or 16 or 32 or 64)) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
if (startMinuteOfDay < MIN_START_MINUTE || endMinuteOfDay > MAX_END_MINUTE || startMinuteOfDay > endMinuteOfDay) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
if (sessionDurationMilliseconds < 0 || sessionPauseMilliseconds < 0) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
|
||||
val appliesToWholeDay: Boolean
|
||||
get() = startMinuteOfDay == MIN_START_MINUTE && endMinuteOfDay == MAX_END_MINUTE
|
||||
|
||||
val sessionDurationLimitEnabled: Boolean
|
||||
get() = sessionPauseMilliseconds > 0 && sessionDurationMilliseconds > 0
|
||||
|
||||
override fun serialize(writer: JsonWriter) {
|
||||
writer.beginObject()
|
||||
|
||||
|
@ -103,7 +148,28 @@ data class TimeLimitRule(
|
|||
writer.name(MAX_TIME_IN_MILLIS).value(maximumTimeInMillis)
|
||||
writer.name(DAY_MASK).value(dayMask)
|
||||
writer.name(APPLY_TO_EXTRA_TIME_USAGE).value(applyToExtraTimeUsage)
|
||||
writer.name(START_MINUTE_OF_DAY).value(startMinuteOfDay)
|
||||
writer.name(END_MINUTE_OF_DAY).value(endMinuteOfDay)
|
||||
|
||||
if (sessionDurationMilliseconds != 0 || sessionPauseMilliseconds != 0) {
|
||||
writer.name(SESSION_DURATION_MILLISECONDS).value(sessionDurationMilliseconds)
|
||||
writer.name(SESSION_PAUSE_MILLISECONDS).value(sessionPauseMilliseconds)
|
||||
}
|
||||
|
||||
writer.endObject()
|
||||
}
|
||||
}
|
||||
|
||||
fun List<TimeLimitRule>.getSlotSwitchMinutes(): Set<Int> {
|
||||
val result = mutableSetOf<Int>()
|
||||
|
||||
result.add(MinuteOfDay.MIN)
|
||||
|
||||
forEach { rule -> result.add(rule.startMinuteOfDay); result.add(rule.endMinuteOfDay) }
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
fun getCurrentTimeSlotStartMinute(slots: Set<Int>, minuteOfDay: LiveData<Int>): LiveData<Int> = minuteOfDay.map { minuteOfDay ->
|
||||
slots.find { it >= minuteOfDay } ?: 0
|
||||
}.ignoreUnchanged()
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
* 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
|
||||
|
@ -21,20 +21,27 @@ import androidx.room.ColumnInfo
|
|||
import androidx.room.Entity
|
||||
import io.timelimit.android.data.IdGenerator
|
||||
import io.timelimit.android.data.JsonSerializable
|
||||
import io.timelimit.android.extensions.MinuteOfDay
|
||||
|
||||
@Entity(primaryKeys = ["category_id", "day_of_epoch"], tableName = "used_time")
|
||||
@Entity(primaryKeys = ["category_id", "day_of_epoch", "start_time_of_day", "end_time_of_day"], tableName = "used_time")
|
||||
data class UsedTimeItem(
|
||||
@ColumnInfo(name = "day_of_epoch")
|
||||
val dayOfEpoch: Int,
|
||||
@ColumnInfo(name = "used_time")
|
||||
val usedMillis: Long,
|
||||
@ColumnInfo(name = "category_id")
|
||||
val categoryId: String
|
||||
val categoryId: String,
|
||||
@ColumnInfo(name = "start_time_of_day")
|
||||
val startTimeOfDay: Int,
|
||||
@ColumnInfo(name = "end_time_of_day")
|
||||
val endTimeOfDay: Int
|
||||
): JsonSerializable {
|
||||
companion object {
|
||||
private const val DAY_OF_EPOCH = "day"
|
||||
private const val USED_TIME_MILLIS = "time"
|
||||
private const val CATEGORY_ID = "category"
|
||||
private const val START_TIME_OF_DAY = "start"
|
||||
private const val END_TIME_OF_DAY = "end"
|
||||
|
||||
fun parse(reader: JsonReader): UsedTimeItem {
|
||||
reader.beginObject()
|
||||
|
@ -42,12 +49,16 @@ data class UsedTimeItem(
|
|||
var dayOfEpoch: Int? = null
|
||||
var usedMillis: Long? = null
|
||||
var categoryId: String? = null
|
||||
var startTimeOfDay = MinuteOfDay.MIN
|
||||
var endTimeOfDay = MinuteOfDay.MAX
|
||||
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
DAY_OF_EPOCH -> dayOfEpoch = reader.nextInt()
|
||||
USED_TIME_MILLIS -> usedMillis = reader.nextLong()
|
||||
CATEGORY_ID -> categoryId = reader.nextString()
|
||||
START_TIME_OF_DAY -> startTimeOfDay = reader.nextInt()
|
||||
END_TIME_OF_DAY -> endTimeOfDay = reader.nextInt()
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
|
@ -57,7 +68,9 @@ data class UsedTimeItem(
|
|||
return UsedTimeItem(
|
||||
dayOfEpoch = dayOfEpoch!!,
|
||||
usedMillis = usedMillis!!,
|
||||
categoryId = categoryId!!
|
||||
categoryId = categoryId!!,
|
||||
startTimeOfDay = startTimeOfDay,
|
||||
endTimeOfDay = endTimeOfDay
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -72,6 +85,10 @@ data class UsedTimeItem(
|
|||
if (usedMillis < 0) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
if (startTimeOfDay < MinuteOfDay.MIN || endTimeOfDay > MinuteOfDay.MAX || startTimeOfDay > endTimeOfDay) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
|
||||
override fun serialize(writer: JsonWriter) {
|
||||
|
@ -80,6 +97,8 @@ data class UsedTimeItem(
|
|||
writer.name(DAY_OF_EPOCH).value(dayOfEpoch)
|
||||
writer.name(USED_TIME_MILLIS).value(usedMillis)
|
||||
writer.name(CATEGORY_ID).value(categoryId)
|
||||
writer.name(START_TIME_OF_DAY).value(startTimeOfDay)
|
||||
writer.name(END_TIME_OF_DAY).value(endTimeOfDay)
|
||||
|
||||
writer.endObject()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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.data.model
|
||||
|
||||
data class UsedTimeListItem(
|
||||
val startMinuteOfDay: Int,
|
||||
val endMinuteOfDay: Int,
|
||||
val duration: Long,
|
||||
// used time item
|
||||
val day: Long?,
|
||||
// session duration
|
||||
val lastUsage: Long?,
|
||||
val maxSessionDuration: Long?,
|
||||
val pauseDuration: Long?
|
||||
)
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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.extensions
|
||||
|
||||
object MinuteOfDay {
|
||||
const val MIN = 0
|
||||
const val MAX = 24 * 60 - 1
|
||||
const val LENGTH = 24 * 60
|
||||
|
||||
fun isValid(value: Int) = value >= MIN && value <= MAX
|
||||
|
||||
fun format(minuteOfDay: Int): String {
|
||||
if (minuteOfDay < MIN || minuteOfDay > MAX) {
|
||||
return "???"
|
||||
} else {
|
||||
val hour = minuteOfDay / 60
|
||||
val minute = minuteOfDay % 60
|
||||
|
||||
val hourString = hour.toString()
|
||||
val minuteString = minute.toString().padStart(2, '0')
|
||||
|
||||
return "$hourString:$minuteString"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -113,6 +113,7 @@ class NotificationListener: NotificationListenerService() {
|
|||
BlockingReason.RequiresCurrentDevice -> getString(R.string.lock_reason_short_requires_current_device)
|
||||
BlockingReason.NotificationsAreBlocked -> getString(R.string.lock_reason_short_notification_blocking)
|
||||
BlockingReason.BatteryLimit -> getString(R.string.lock_reason_short_battery_limit)
|
||||
BlockingReason.SessionDurationLimit -> getString(R.string.lock_reason_short_session_duration)
|
||||
BlockingReason.None -> throw IllegalStateException()
|
||||
}
|
||||
)
|
||||
|
|
|
@ -16,8 +16,6 @@
|
|||
package io.timelimit.android.logic
|
||||
|
||||
import android.util.Log
|
||||
import android.util.SparseArray
|
||||
import android.util.SparseLongArray
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.R
|
||||
|
@ -29,11 +27,14 @@ import io.timelimit.android.data.backup.DatabaseBackup
|
|||
import io.timelimit.android.data.model.*
|
||||
import io.timelimit.android.date.DateInTimezone
|
||||
import io.timelimit.android.date.getMinuteOfWeek
|
||||
import io.timelimit.android.extensions.MinuteOfDay
|
||||
import io.timelimit.android.integration.platform.AppStatusMessage
|
||||
import io.timelimit.android.integration.platform.ForegroundAppSpec
|
||||
import io.timelimit.android.integration.platform.ProtectionLevel
|
||||
import io.timelimit.android.integration.platform.android.AccessibilityService
|
||||
import io.timelimit.android.livedata.*
|
||||
import io.timelimit.android.sync.actions.AddUsedTimeActionItemAdditionalCountingSlot
|
||||
import io.timelimit.android.sync.actions.AddUsedTimeActionItemSessionDurationLimitSlot
|
||||
import io.timelimit.android.sync.actions.UpdateDeviceStatusAction
|
||||
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
||||
import io.timelimit.android.ui.IsAppInForeground
|
||||
|
@ -235,7 +236,8 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
|||
|
||||
fun deleteOldUsedTimes() = UsedTimeDeleter.deleteOldUsedTimeItems(
|
||||
database = appLogic.database,
|
||||
date = nowDate
|
||||
date = nowDate,
|
||||
timestamp = nowTimestamp
|
||||
)
|
||||
|
||||
if (realTime.isNetworkTime) {
|
||||
|
@ -315,17 +317,28 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
|||
val usedTimeUpdateHelper = usedTimeUpdateHelper!!
|
||||
|
||||
// check times
|
||||
fun buildUsedTimesSparseArray(items: SparseArray<UsedTimeItem>, categoryId: String): SparseLongArray {
|
||||
val result = SparseLongArray()
|
||||
|
||||
for (i in 0..6) {
|
||||
val usedTimesItem = items[i]?.usedMillis ?: 0
|
||||
val timeToAddButNotCommited = usedTimeUpdateHelper.timeToAdd[categoryId] ?: 0
|
||||
|
||||
result.put(i, usedTimesItem + timeToAddButNotCommited)
|
||||
fun buildDummyUsedTimeItems(categoryId: String): List<UsedTimeItem> {
|
||||
if (!usedTimeUpdateHelper.timeToAdd.containsKey(categoryId)) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
return result
|
||||
return (usedTimeUpdateHelper.additionalSlots[categoryId] ?: emptySet()).map {
|
||||
UsedTimeItem(
|
||||
categoryId = categoryId,
|
||||
startTimeOfDay = it.start,
|
||||
endTimeOfDay = it.end,
|
||||
dayOfEpoch = usedTimeUpdateHelper.date.dayOfEpoch,
|
||||
usedMillis = (usedTimeUpdateHelper.timeToAdd[categoryId] ?: 0).toLong()
|
||||
)
|
||||
} + listOf(
|
||||
UsedTimeItem(
|
||||
categoryId = categoryId,
|
||||
startTimeOfDay = MinuteOfDay.MIN,
|
||||
endTimeOfDay = MinuteOfDay.MAX,
|
||||
dayOfEpoch = usedTimeUpdateHelper.date.dayOfEpoch,
|
||||
usedMillis = (usedTimeUpdateHelper.timeToAdd[categoryId] ?: 0).toLong()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun getRemainingTime(categoryId: String?): RemainingTime? {
|
||||
|
@ -338,31 +351,66 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
|||
return null
|
||||
}
|
||||
|
||||
val usedTimes = usedTimesOfCategoryAndWeekByFirstDayOfWeek.get(Pair(category.id, nowDate.dayOfEpoch - nowDate.dayOfWeek)).waitForNonNullValue()
|
||||
val firstDayOfWeekAsEpochDay = nowDate.dayOfEpoch - nowDate.dayOfWeek
|
||||
val usedTimes = usedTimesOfCategoryAndWeekByFirstDayOfWeek.get(Pair(category.id, firstDayOfWeekAsEpochDay)).waitForNonNullValue()
|
||||
|
||||
return RemainingTime.getRemainingTime(
|
||||
nowDate.dayOfWeek,
|
||||
buildUsedTimesSparseArray(usedTimes, categoryId),
|
||||
minuteOfWeek % MinuteOfDay.LENGTH,
|
||||
usedTimes + buildDummyUsedTimeItems(categoryId),
|
||||
rules,
|
||||
Math.max(0, category.getExtraTime(dayOfEpoch = nowDate.dayOfEpoch) - (usedTimeUpdateHelper.extraTimeToSubtract.get(categoryId) ?: 0))
|
||||
Math.max(0, category.getExtraTime(dayOfEpoch = nowDate.dayOfEpoch) - (usedTimeUpdateHelper.extraTimeToSubtract.get(categoryId) ?: 0)),
|
||||
firstDayOfWeekAsEpochDay
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun getRemainingSessionDuration(categoryId: String?): Long? {
|
||||
categoryId ?: return null
|
||||
|
||||
val category = categories.find { it.id == categoryId } ?: return null
|
||||
val rules = timeLimitRules.get(category.id).waitForNonNullValue()
|
||||
val durations = cache.usedSessionDurationsByCategoryId.get(categoryId).waitForNonNullValue()
|
||||
val timeToAdd = usedTimeUpdateHelper.timeToAdd[categoryId] ?: 0
|
||||
|
||||
val result = RemainingSessionDuration.getRemainingSessionDuration(
|
||||
rules = rules,
|
||||
durationsOfCategory = durations,
|
||||
timestamp = nowTimestamp,
|
||||
dayOfWeek = nowDate.dayOfWeek,
|
||||
minuteOfDay = minuteOfWeek % MinuteOfDay.LENGTH
|
||||
)
|
||||
|
||||
if (result == null) {
|
||||
return null
|
||||
} else {
|
||||
return (result - timeToAdd).coerceAtLeast(0)
|
||||
}
|
||||
}
|
||||
|
||||
// note: remainingTime != null implicates that there are limits and they are currently not ignored
|
||||
val remainingTimeForegroundAppChild = if (foregroundAppHandling.status == BackgroundTaskLogicAppStatus.AllowedCountAndCheckTime) getRemainingTime(foregroundAppHandling.categoryId) else null
|
||||
val remainingTimeForegroundAppParent = if (foregroundAppHandling.status == BackgroundTaskLogicAppStatus.AllowedCountAndCheckTime) getRemainingTime(foregroundAppHandling.parentCategoryId) else null
|
||||
val remainingTimeForegroundApp = RemainingTime.min(remainingTimeForegroundAppChild, remainingTimeForegroundAppParent)
|
||||
val remainingSessionDurationForegroundAppChild = if (foregroundAppHandling.status == BackgroundTaskLogicAppStatus.AllowedCountAndCheckTime) getRemainingSessionDuration(foregroundAppHandling.categoryId) else null
|
||||
val remainingSessionDurationForegroundAppParent = if (foregroundAppHandling.status == BackgroundTaskLogicAppStatus.AllowedCountAndCheckTime) getRemainingSessionDuration(foregroundAppHandling.parentCategoryId) else null
|
||||
val remainingSessionDurationForegroundApp = RemainingSessionDuration.min(remainingSessionDurationForegroundAppChild, remainingSessionDurationForegroundAppParent)
|
||||
|
||||
val remainingTimeBackgroundAppChild = if (audioPlaybackHandling.status == BackgroundTaskLogicAppStatus.AllowedCountAndCheckTime) getRemainingTime(audioPlaybackHandling.categoryId) else null
|
||||
val remainingTimeBackgroundAppParent = if (audioPlaybackHandling.status == BackgroundTaskLogicAppStatus.AllowedCountAndCheckTime) getRemainingTime(audioPlaybackHandling.parentCategoryId) else null
|
||||
val remainingTimeBackgroundApp = RemainingTime.min(remainingTimeBackgroundAppChild, remainingTimeBackgroundAppParent)
|
||||
val remainingSessionDurationBackgroundAppChild = if (audioPlaybackHandling.status == BackgroundTaskLogicAppStatus.AllowedCountAndCheckTime) getRemainingSessionDuration(audioPlaybackHandling.categoryId) else null
|
||||
val remainingSessionDurationBackgroundAppParent = if (audioPlaybackHandling.status == BackgroundTaskLogicAppStatus.AllowedCountAndCheckTime) getRemainingSessionDuration(audioPlaybackHandling.parentCategoryId) else null
|
||||
val remainingSessionDurationBackgroundApp = RemainingSessionDuration.min(remainingSessionDurationBackgroundAppChild, remainingSessionDurationBackgroundAppParent)
|
||||
|
||||
val sessionDurationLimitReachedForegroundApp = (remainingSessionDurationForegroundApp != null && remainingSessionDurationForegroundApp == 0L)
|
||||
val sessionDurationLimitReachedBackgroundApp = (remainingSessionDurationBackgroundApp != null && remainingSessionDurationBackgroundApp == 0L)
|
||||
|
||||
// eventually block
|
||||
if (remainingTimeForegroundApp?.hasRemainingTime == false) {
|
||||
if (remainingTimeForegroundApp?.hasRemainingTime == false || sessionDurationLimitReachedForegroundApp) {
|
||||
foregroundAppHandling.status = BackgroundTaskLogicAppStatus.ShouldBlock
|
||||
}
|
||||
|
||||
if (remainingTimeBackgroundApp?.hasRemainingTime == false) {
|
||||
if (remainingTimeBackgroundApp?.hasRemainingTime == false || sessionDurationLimitReachedBackgroundApp) {
|
||||
audioPlaybackHandling.status = BackgroundTaskLogicAppStatus.ShouldBlock
|
||||
}
|
||||
|
||||
|
@ -375,6 +423,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
|||
|
||||
val categoriesToCount = mutableSetOf<String>()
|
||||
val categoriesToCountExtraTime = mutableSetOf<String>()
|
||||
val categoriesToCountSessionDurations = mutableSetOf<String>()
|
||||
|
||||
if (shouldCountForegroundApp) {
|
||||
remainingTimeForegroundAppChild?.let { remainingTime ->
|
||||
|
@ -384,6 +433,10 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
|||
if (remainingTime.usingExtraTime) {
|
||||
categoriesToCountExtraTime.add(categoryId)
|
||||
}
|
||||
|
||||
if (!sessionDurationLimitReachedForegroundApp) {
|
||||
categoriesToCountSessionDurations.add(categoryId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -394,6 +447,10 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
|||
if (remainingTime.usingExtraTime) {
|
||||
categoriesToCountExtraTime.add(it)
|
||||
}
|
||||
|
||||
if (!sessionDurationLimitReachedForegroundApp) {
|
||||
categoriesToCountSessionDurations.add(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -406,6 +463,10 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
|||
if (remainingTime.usingExtraTime) {
|
||||
categoriesToCountExtraTime.add(it)
|
||||
}
|
||||
|
||||
if (!sessionDurationLimitReachedBackgroundApp) {
|
||||
categoriesToCountSessionDurations.add(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -416,16 +477,61 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
|||
if (remainingTime.usingExtraTime) {
|
||||
categoriesToCountExtraTime.add(it)
|
||||
}
|
||||
|
||||
if (!sessionDurationLimitReachedBackgroundApp) {
|
||||
categoriesToCountSessionDurations.add(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (categoriesToCount.isNotEmpty()) {
|
||||
categoriesToCount.forEach { categoryId ->
|
||||
// only handle rules which are related to today
|
||||
val rules = timeLimitRules.get(categoryId).waitForNonNullValue().filter {
|
||||
(it.dayMask.toInt() and (1 shl nowDate.dayOfWeek)) != 0
|
||||
}
|
||||
|
||||
usedTimeUpdateHelper.add(
|
||||
categoryId = categoryId,
|
||||
time = timeToSubtract,
|
||||
includingExtraTime = categoriesToCountExtraTime.contains(categoryId)
|
||||
includingExtraTime = categoriesToCountExtraTime.contains(categoryId),
|
||||
slots = run {
|
||||
val slots = mutableSetOf<AddUsedTimeActionItemAdditionalCountingSlot>()
|
||||
|
||||
rules.forEach { rule ->
|
||||
if (!rule.appliesToWholeDay) {
|
||||
slots.add(
|
||||
AddUsedTimeActionItemAdditionalCountingSlot(
|
||||
rule.startMinuteOfDay, rule.endMinuteOfDay
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
slots
|
||||
},
|
||||
trustedTimestamp = if (realTime.shouldTrustTimePermanently) realTime.timeInMillis else 0,
|
||||
sessionDurationLimits = run {
|
||||
val slots = mutableSetOf<AddUsedTimeActionItemSessionDurationLimitSlot>()
|
||||
|
||||
if (categoriesToCountSessionDurations.contains(categoryId)) {
|
||||
rules.forEach { rule ->
|
||||
if (rule.sessionDurationLimitEnabled) {
|
||||
slots.add(
|
||||
AddUsedTimeActionItemSessionDurationLimitSlot(
|
||||
startMinuteOfDay = rule.startMinuteOfDay,
|
||||
endMinuteOfDay = rule.endMinuteOfDay,
|
||||
sessionPauseDuration = rule.sessionPauseMilliseconds,
|
||||
maxSessionDuration = rule.sessionDurationMilliseconds
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
slots
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -479,6 +585,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
|||
fun buildStatusMessage(
|
||||
handling: BackgroundTaskRestrictionLogicResult,
|
||||
remainingTime: RemainingTime?,
|
||||
remainingSessionDuration: Long?,
|
||||
suffix: String,
|
||||
appPackageName: String?,
|
||||
appActivityToShow: String?
|
||||
|
@ -518,27 +625,18 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
|||
appPackageName = appPackageName,
|
||||
appActivityToShow = appActivityToShow
|
||||
)
|
||||
BackgroundTaskLogicAppStatus.AllowedCountAndCheckTime -> (
|
||||
if (remainingTime?.usingExtraTime == true) {
|
||||
// using extra time
|
||||
buildStatusMessageWithCurrentAppTitle(
|
||||
text = appLogic.context.getString(R.string.background_logic_using_extra_time, TimeTextUtil.remaining(remainingTime.includingExtraTime.toInt(), appLogic.context)),
|
||||
BackgroundTaskLogicAppStatus.AllowedCountAndCheckTime -> buildStatusMessageWithCurrentAppTitle(
|
||||
text = if (remainingTime?.usingExtraTime == true)
|
||||
appLogic.context.getString(R.string.background_logic_using_extra_time, TimeTextUtil.remaining(remainingTime.includingExtraTime.toInt(), appLogic.context))
|
||||
else if (remainingTime != null && remainingSessionDuration != null && remainingSessionDuration < remainingTime.default)
|
||||
TimeTextUtil.pauseIn(remainingSessionDuration.toInt(), appLogic.context)
|
||||
else
|
||||
TimeTextUtil.remaining(remainingTime?.default?.toInt() ?: 0, appLogic.context),
|
||||
titlePrefix = getCategoryTitle(handling.categoryId) + " - ",
|
||||
titleSuffix = suffix,
|
||||
appPackageName = appPackageName,
|
||||
appActivityToShow = appActivityToShow
|
||||
)
|
||||
} else {
|
||||
// using normal contingent
|
||||
buildStatusMessageWithCurrentAppTitle(
|
||||
text = TimeTextUtil.remaining(remainingTime?.default?.toInt() ?: 0, appLogic.context),
|
||||
titlePrefix = getCategoryTitle(handling.categoryId) + " - ",
|
||||
titleSuffix = suffix,
|
||||
appPackageName = appPackageName,
|
||||
appActivityToShow = appActivityToShow
|
||||
)
|
||||
}
|
||||
)
|
||||
BackgroundTaskLogicAppStatus.Idle -> AppStatusMessage(
|
||||
appLogic.context.getString(R.string.background_logic_idle_title) + suffix,
|
||||
appLogic.context.getString(R.string.background_logic_idle_text)
|
||||
|
@ -557,7 +655,8 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
|||
remainingTime = remainingTimeBackgroundApp,
|
||||
suffix = " (2/2)",
|
||||
appPackageName = audioPlaybackPackageName,
|
||||
appActivityToShow = null
|
||||
appActivityToShow = null,
|
||||
remainingSessionDuration = remainingSessionDurationBackgroundApp
|
||||
)
|
||||
)
|
||||
} else {
|
||||
|
@ -568,7 +667,8 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
|||
remainingTime = remainingTimeForegroundApp,
|
||||
suffix = if (showBackgroundStatus) " (1/2)" else "",
|
||||
appPackageName = foregroundAppPackageName,
|
||||
appActivityToShow = if (activityLevelBlocking) foregroundAppActivityName else null
|
||||
appActivityToShow = if (activityLevelBlocking) foregroundAppActivityName else null,
|
||||
remainingSessionDuration = remainingSessionDurationForegroundApp
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -15,12 +15,8 @@
|
|||
*/
|
||||
package io.timelimit.android.logic
|
||||
|
||||
import android.util.SparseArray
|
||||
import androidx.lifecycle.LiveData
|
||||
import io.timelimit.android.data.model.Category
|
||||
import io.timelimit.android.data.model.CategoryApp
|
||||
import io.timelimit.android.data.model.TimeLimitRule
|
||||
import io.timelimit.android.data.model.UsedTimeItem
|
||||
import io.timelimit.android.data.model.*
|
||||
import io.timelimit.android.livedata.*
|
||||
import java.util.*
|
||||
|
||||
|
@ -49,11 +45,16 @@ class BackgroundTaskLogicCache (private val appLogic: AppLogic) {
|
|||
return appLogic.database.timeLimitRules().getTimeLimitRulesByCategory(key)
|
||||
}
|
||||
}
|
||||
val usedTimesOfCategoryAndWeekByFirstDayOfWeek = object: MultiKeyLiveDataCache<SparseArray<UsedTimeItem>, Pair<String, Int>>() {
|
||||
override fun createValue(key: Pair<String, Int>): LiveData<SparseArray<UsedTimeItem>> {
|
||||
val usedTimesOfCategoryAndWeekByFirstDayOfWeek = object: MultiKeyLiveDataCache<List<UsedTimeItem>, Pair<String, Int>>() {
|
||||
override fun createValue(key: Pair<String, Int>): LiveData<List<UsedTimeItem>> {
|
||||
return appLogic.database.usedTimes().getUsedTimesOfWeek(key.first, key.second)
|
||||
}
|
||||
}
|
||||
val usedSessionDurationsByCategoryId = object: MultiKeyLiveDataCache<List<SessionDuration>, String>() {
|
||||
override fun createValue(key: String): LiveData<List<SessionDuration>> {
|
||||
return appLogic.database.sessionDuration().getSessionDurationItemsByCategoryId(key)
|
||||
}
|
||||
}
|
||||
val shouldDoAutomaticSignOut = SingleItemLiveDataCacheWithRequery { -> appLogic.defaultUserLogic.hasAutomaticSignOut()}
|
||||
|
||||
val liveDataCaches = LiveDataCaches(arrayOf(
|
||||
|
|
|
@ -16,15 +16,14 @@
|
|||
package io.timelimit.android.logic
|
||||
|
||||
import android.util.Log
|
||||
import android.util.SparseLongArray
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.data.model.*
|
||||
import io.timelimit.android.date.DateInTimezone
|
||||
import io.timelimit.android.date.getMinuteOfWeek
|
||||
import io.timelimit.android.extensions.MinuteOfDay
|
||||
import io.timelimit.android.integration.platform.android.AndroidIntegrationApps
|
||||
import io.timelimit.android.integration.time.TimeApi
|
||||
import io.timelimit.android.livedata.*
|
||||
import io.timelimit.android.logic.extension.isCategoryAllowed
|
||||
import java.util.*
|
||||
|
@ -39,7 +38,8 @@ enum class BlockingReason {
|
|||
MissingNetworkTime,
|
||||
RequiresCurrentDevice,
|
||||
NotificationsAreBlocked,
|
||||
BatteryLimit
|
||||
BatteryLimit,
|
||||
SessionDurationLimit
|
||||
}
|
||||
|
||||
enum class BlockingLevel {
|
||||
|
@ -319,18 +319,18 @@ class BlockingReasonUtil(private val appLogic: AppLogic) {
|
|||
trustedMinuteOfWeek ->
|
||||
|
||||
if (category.blockedMinutesInWeek.dataNotToModify.isEmpty) {
|
||||
getBlockingReasonStep6(category, timeZone)
|
||||
getBlockingReasonStep6(category, timeZone, trustedMinuteOfWeek)
|
||||
} else if (trustedMinuteOfWeek == null) {
|
||||
liveDataFromValue(BlockingReason.MissingNetworkTime)
|
||||
} else if (category.blockedMinutesInWeek.read(trustedMinuteOfWeek)) {
|
||||
liveDataFromValue(BlockingReason.BlockedAtThisTime)
|
||||
} else {
|
||||
getBlockingReasonStep6(category, timeZone)
|
||||
getBlockingReasonStep6(category, timeZone, trustedMinuteOfWeek)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBlockingReasonStep6(category: Category, timeZone: TimeZone): LiveData<BlockingReason> {
|
||||
private fun getBlockingReasonStep6(category: Category, timeZone: TimeZone, trustedMinuteOfWeek: Int?): LiveData<BlockingReason> {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "step 6")
|
||||
}
|
||||
|
@ -343,54 +343,67 @@ class BlockingReasonUtil(private val appLogic: AppLogic) {
|
|||
|
||||
if (rules.isEmpty()) {
|
||||
liveDataFromValue(BlockingReason.None)
|
||||
} else if (nowTrustedDate == null) {
|
||||
} else if (nowTrustedDate == null || trustedMinuteOfWeek == null) {
|
||||
liveDataFromValue(BlockingReason.MissingNetworkTime)
|
||||
} else {
|
||||
getBlockingReasonStep6(category, nowTrustedDate, rules)
|
||||
getBlockingReasonStep6(category, nowTrustedDate, trustedMinuteOfWeek, rules)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBlockingReasonStep6(category: Category, nowTrustedDate: DateInTimezone, rules: List<TimeLimitRule>): LiveData<BlockingReason> {
|
||||
private fun getBlockingReasonStep6(category: Category, nowTrustedDate: DateInTimezone, trustedMinuteOfWeek: Int, rules: List<TimeLimitRule>): LiveData<BlockingReason> {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "step 6 - 2")
|
||||
}
|
||||
|
||||
return appLogic.currentDeviceLogic.isThisDeviceTheCurrentDevice.switchMap { isCurrentDevice ->
|
||||
if (isCurrentDevice) {
|
||||
getBlockingReasonStep7(category, nowTrustedDate, rules)
|
||||
getBlockingReasonStep7(category, nowTrustedDate, trustedMinuteOfWeek, rules)
|
||||
} else {
|
||||
liveDataFromValue(BlockingReason.RequiresCurrentDevice)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBlockingReasonStep7(category: Category, nowTrustedDate: DateInTimezone, rules: List<TimeLimitRule>): LiveData<BlockingReason> {
|
||||
private fun getBlockingReasonStep7(category: Category, nowTrustedDate: DateInTimezone, trustedMinuteOfWeek: Int, rules: List<TimeLimitRule>): LiveData<BlockingReason> {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "step 7")
|
||||
}
|
||||
|
||||
val extraTime = category.getExtraTime(dayOfEpoch = nowTrustedDate.dayOfEpoch)
|
||||
val firstDayOfWeekAsEpochDay = nowTrustedDate.dayOfEpoch - nowTrustedDate.dayOfWeek
|
||||
|
||||
return appLogic.database.usedTimes().getUsedTimesOfWeek(category.id, nowTrustedDate.dayOfEpoch - nowTrustedDate.dayOfWeek).map {
|
||||
usedTimes ->
|
||||
val usedTimesSparseArray = SparseLongArray()
|
||||
|
||||
for (i in 0..6) {
|
||||
val usedTimesItem = usedTimes[i]?.usedMillis
|
||||
usedTimesSparseArray.put(i, (if (usedTimesItem != null) usedTimesItem else 0))
|
||||
}
|
||||
|
||||
val remaining = RemainingTime.getRemainingTime(nowTrustedDate.dayOfWeek, usedTimesSparseArray, rules, extraTime)
|
||||
return appLogic.database.usedTimes().getUsedTimesOfWeek(category.id, firstDayOfWeekAsEpochDay = firstDayOfWeekAsEpochDay).switchMap { usedTimes ->
|
||||
val remaining = RemainingTime.getRemainingTime(nowTrustedDate.dayOfWeek, trustedMinuteOfWeek % MinuteOfDay.LENGTH, usedTimes, rules, extraTime, firstDayOfWeekAsEpochDay)
|
||||
|
||||
if (remaining == null || remaining.includingExtraTime > 0) {
|
||||
appLogic.database.sessionDuration().getSessionDurationItemsByCategoryId(category.id).switchMap { durations ->
|
||||
getTemporarilyTrustedTimeInMillis().map { timeInMillis ->
|
||||
if (timeInMillis == null) {
|
||||
BlockingReason.MissingNetworkTime
|
||||
} else {
|
||||
val remainingDuration = RemainingSessionDuration.getRemainingSessionDuration(
|
||||
rules = rules,
|
||||
dayOfWeek = nowTrustedDate.dayOfWeek,
|
||||
durationsOfCategory = durations,
|
||||
minuteOfDay = trustedMinuteOfWeek % MinuteOfDay.LENGTH,
|
||||
timestamp = timeInMillis
|
||||
)
|
||||
|
||||
if (remainingDuration == null || remainingDuration > 0) {
|
||||
BlockingReason.None
|
||||
} else {
|
||||
if (extraTime > 0) {
|
||||
BlockingReason.TimeOverExtraTimeCanBeUsedLater
|
||||
BlockingReason.SessionDurationLimit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
BlockingReason.TimeOver
|
||||
if (extraTime > 0) {
|
||||
liveDataFromValue(BlockingReason.TimeOverExtraTimeCanBeUsedLater)
|
||||
} else {
|
||||
liveDataFromValue(BlockingReason.TimeOver)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,12 +16,12 @@
|
|||
package io.timelimit.android.logic
|
||||
|
||||
import android.util.Log
|
||||
import android.util.SparseLongArray
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.data.model.*
|
||||
import io.timelimit.android.date.DateInTimezone
|
||||
import io.timelimit.android.extensions.MinuteOfDay
|
||||
import io.timelimit.android.livedata.*
|
||||
import io.timelimit.android.logic.extension.isCategoryAllowed
|
||||
import java.util.*
|
||||
|
@ -146,7 +146,8 @@ class CategoriesBlockingReasonUtil(private val appLogic: AppLogic) {
|
|||
checkCategoryTimeLimitRules(
|
||||
temporarilyTrustedDate = temporarilyTrustedDate,
|
||||
category = category,
|
||||
rules = appLogic.database.timeLimitRules().getTimeLimitRulesByCategory(category.id)
|
||||
rules = appLogic.database.timeLimitRules().getTimeLimitRulesByCategory(category.id),
|
||||
temporarilyTrustedMinuteOfWeek = temporarilyTrustedMinuteOfWeek
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -211,6 +212,7 @@ class CategoriesBlockingReasonUtil(private val appLogic: AppLogic) {
|
|||
|
||||
private fun checkCategoryTimeLimitRules(
|
||||
temporarilyTrustedDate: LiveData<DateInTimezone?>,
|
||||
temporarilyTrustedMinuteOfWeek: LiveData<Int?>,
|
||||
rules: LiveData<List<TimeLimitRule>>,
|
||||
category: Category
|
||||
): LiveData<BlockingReason> = rules.switchMap { rules ->
|
||||
|
@ -218,43 +220,60 @@ class CategoriesBlockingReasonUtil(private val appLogic: AppLogic) {
|
|||
liveDataFromValue(BlockingReason.None)
|
||||
} else {
|
||||
temporarilyTrustedDate.switchMap { temporarilyTrustedDate ->
|
||||
if (temporarilyTrustedDate == null) {
|
||||
temporarilyTrustedMinuteOfWeek.switchMap { temporarilyTrustedMinuteOfWeek ->
|
||||
if (temporarilyTrustedDate == null || temporarilyTrustedMinuteOfWeek == null) {
|
||||
liveDataFromValue(BlockingReason.MissingNetworkTime)
|
||||
} else {
|
||||
getBlockingReasonStep7(
|
||||
category = category,
|
||||
nowTrustedDate = temporarilyTrustedDate,
|
||||
rules = rules
|
||||
rules = rules,
|
||||
trustedMinuteOfWeek = temporarilyTrustedMinuteOfWeek
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBlockingReasonStep7(category: Category, nowTrustedDate: DateInTimezone, rules: List<TimeLimitRule>): LiveData<BlockingReason> {
|
||||
private fun getBlockingReasonStep7(category: Category, nowTrustedDate: DateInTimezone, trustedMinuteOfWeek: Int, rules: List<TimeLimitRule>): LiveData<BlockingReason> {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "step 7")
|
||||
}
|
||||
|
||||
val extraTime = category.getExtraTime(dayOfEpoch = nowTrustedDate.dayOfEpoch)
|
||||
val firstDayOfWeekAsEpochDay = nowTrustedDate.dayOfEpoch - nowTrustedDate.dayOfWeek
|
||||
|
||||
return appLogic.database.usedTimes().getUsedTimesOfWeek(category.id, nowTrustedDate.dayOfEpoch - nowTrustedDate.dayOfWeek).map { usedTimes ->
|
||||
val usedTimesSparseArray = SparseLongArray()
|
||||
|
||||
for (i in 0..6) {
|
||||
val usedTimesItem = usedTimes[i]?.usedMillis
|
||||
usedTimesSparseArray.put(i, (if (usedTimesItem != null) usedTimesItem else 0))
|
||||
}
|
||||
|
||||
val remaining = RemainingTime.getRemainingTime(nowTrustedDate.dayOfWeek, usedTimesSparseArray, rules, extraTime)
|
||||
return appLogic.database.usedTimes().getUsedTimesOfWeek(category.id, firstDayOfWeekAsEpochDay).switchMap { usedTimes ->
|
||||
val remaining = RemainingTime.getRemainingTime(nowTrustedDate.dayOfWeek, trustedMinuteOfWeek % MinuteOfDay.LENGTH, usedTimes, rules, extraTime, firstDayOfWeekAsEpochDay)
|
||||
|
||||
if (remaining == null || remaining.includingExtraTime > 0) {
|
||||
appLogic.database.sessionDuration().getSessionDurationItemsByCategoryId(category.id).switchMap { durations ->
|
||||
blockingReason.getTemporarilyTrustedTimeInMillis().map { timeInMillis ->
|
||||
if (timeInMillis == null) {
|
||||
BlockingReason.MissingNetworkTime
|
||||
} else {
|
||||
val remainingDuration = RemainingSessionDuration.getRemainingSessionDuration(
|
||||
rules = rules,
|
||||
dayOfWeek = nowTrustedDate.dayOfWeek,
|
||||
durationsOfCategory = durations,
|
||||
minuteOfDay = trustedMinuteOfWeek % MinuteOfDay.LENGTH,
|
||||
timestamp = timeInMillis
|
||||
)
|
||||
|
||||
if (remainingDuration == null || remainingDuration > 0) {
|
||||
BlockingReason.None
|
||||
} else {
|
||||
if (extraTime > 0) {
|
||||
BlockingReason.TimeOverExtraTimeCanBeUsedLater
|
||||
BlockingReason.SessionDurationLimit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
BlockingReason.TimeOver
|
||||
if (extraTime > 0) {
|
||||
liveDataFromValue(BlockingReason.TimeOverExtraTimeCanBeUsedLater)
|
||||
} else {
|
||||
liveDataFromValue(BlockingReason.TimeOver)
|
||||
}
|
||||
}
|
||||
}.ignoreUnchanged()
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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.logic
|
||||
|
||||
import io.timelimit.android.data.model.SessionDuration
|
||||
import io.timelimit.android.data.model.TimeLimitRule
|
||||
|
||||
object RemainingSessionDuration {
|
||||
fun min(a: Long?, b: Long?): Long? {
|
||||
if (a == null && b == null) {
|
||||
return null
|
||||
} else if (a == null) {
|
||||
return b
|
||||
} else if (b == null) {
|
||||
return a
|
||||
} else {
|
||||
return a.coerceAtMost(b)
|
||||
}
|
||||
}
|
||||
|
||||
fun getRemainingSessionDuration(
|
||||
rules: List<TimeLimitRule>, durationsOfCategory: List<SessionDuration>,
|
||||
dayOfWeek: Int, minuteOfDay: Int, timestamp: Long
|
||||
): Long? {
|
||||
var result: Long? = null
|
||||
|
||||
rules.forEach { rule ->
|
||||
if (
|
||||
rule.sessionDurationLimitEnabled &&
|
||||
rule.dayMask.toInt() and (1 shl dayOfWeek) != 0 &&
|
||||
rule.startMinuteOfDay <= minuteOfDay && rule.endMinuteOfDay >= minuteOfDay
|
||||
) {
|
||||
val remaining = durationsOfCategory.find {
|
||||
it.startMinuteOfDay == rule.startMinuteOfDay &&
|
||||
it.endMinuteOfDay == rule.endMinuteOfDay &&
|
||||
it.maxSessionDuration == rule.sessionDurationMilliseconds &&
|
||||
it.sessionPauseDuration == rule.sessionPauseMilliseconds &&
|
||||
it.lastUsage + it.sessionPauseDuration > timestamp
|
||||
}?.let { durationItem ->
|
||||
(durationItem.maxSessionDuration - durationItem.lastSessionDuration).coerceAtLeast(0)
|
||||
} ?: rule.sessionDurationMilliseconds.toLong()
|
||||
|
||||
result = min(result, remaining)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
|
@ -15,8 +15,8 @@
|
|||
*/
|
||||
package io.timelimit.android.logic
|
||||
|
||||
import android.util.SparseLongArray
|
||||
import io.timelimit.android.data.model.TimeLimitRule
|
||||
import io.timelimit.android.data.model.UsedTimeItem
|
||||
|
||||
data class RemainingTime(val includingExtraTime: Long, val default: Long) {
|
||||
val hasRemainingTime = includingExtraTime > 0
|
||||
|
@ -44,18 +44,21 @@ data class RemainingTime(val includingExtraTime: Long, val default: Long) {
|
|||
)
|
||||
}
|
||||
|
||||
private fun getRulesRelatedToDay(dayOfWeek: Int, rules: List<TimeLimitRule>): List<TimeLimitRule> {
|
||||
return rules.filter { (it.dayMask.toInt() and (1 shl dayOfWeek)) != 0 }
|
||||
private fun getRulesRelatedToDay(dayOfWeek: Int, minuteOfDay: Int, rules: List<TimeLimitRule>): List<TimeLimitRule> {
|
||||
return rules.filter {
|
||||
((it.dayMask.toInt() and (1 shl dayOfWeek)) != 0) &&
|
||||
minuteOfDay >= it.startMinuteOfDay && minuteOfDay <= it.endMinuteOfDay
|
||||
}
|
||||
}
|
||||
|
||||
fun getRemainingTime(dayOfWeek: Int, usedTimes: SparseLongArray, rules: List<TimeLimitRule>, extraTime: Long): RemainingTime? {
|
||||
fun getRemainingTime(dayOfWeek: Int, minuteOfDay: Int, usedTimes: List<UsedTimeItem>, rules: List<TimeLimitRule>, extraTime: Long, firstDayOfWeekAsEpochDay: Int): RemainingTime? {
|
||||
if (extraTime < 0) {
|
||||
throw IllegalStateException("extra time < 0")
|
||||
}
|
||||
|
||||
val relatedRules = getRulesRelatedToDay(dayOfWeek, rules)
|
||||
val withoutExtraTime = getRemainingTime(usedTimes, relatedRules, false)
|
||||
val withExtraTime = getRemainingTime(usedTimes, relatedRules, true)
|
||||
val relatedRules = getRulesRelatedToDay(dayOfWeek, minuteOfDay, rules)
|
||||
val withoutExtraTime = getRemainingTime(usedTimes, relatedRules, false, firstDayOfWeekAsEpochDay)
|
||||
val withExtraTime = getRemainingTime(usedTimes, relatedRules, true, firstDayOfWeekAsEpochDay)
|
||||
|
||||
if (withoutExtraTime == null && withExtraTime == null) {
|
||||
// no rules
|
||||
|
@ -83,17 +86,23 @@ data class RemainingTime(val includingExtraTime: Long, val default: Long) {
|
|||
}
|
||||
}
|
||||
|
||||
private fun getRemainingTime(usedTimes: SparseLongArray, relatedRules: List<TimeLimitRule>, assumeMaximalExtraTime: Boolean): Long? {
|
||||
return relatedRules.filter { (!assumeMaximalExtraTime) || it.applyToExtraTimeUsage }.map {
|
||||
private fun getRemainingTime(usedTimes: List<UsedTimeItem>, relatedRules: List<TimeLimitRule>, assumeMaximalExtraTime: Boolean, firstDayOfWeekAsEpochDay: Int): Long? {
|
||||
return relatedRules.filter { (!assumeMaximalExtraTime) || it.applyToExtraTimeUsage }.map { rule ->
|
||||
var usedTime = 0L
|
||||
|
||||
for (day in 0..6) {
|
||||
if ((it.dayMask.toInt() and (1 shl day)) != 0) {
|
||||
usedTime += usedTimes[day]
|
||||
usedTimes.forEach { usedTimeItem ->
|
||||
if (usedTimeItem.dayOfEpoch >= firstDayOfWeekAsEpochDay && usedTimeItem.dayOfEpoch <= firstDayOfWeekAsEpochDay + 6) {
|
||||
val usedTimeItemDayOfWeek = usedTimeItem.dayOfEpoch - firstDayOfWeekAsEpochDay
|
||||
|
||||
if ((rule.dayMask.toInt() and (1 shl usedTimeItemDayOfWeek)) != 0) {
|
||||
if (rule.startMinuteOfDay == usedTimeItem.startTimeOfDay && rule.endMinuteOfDay == usedTimeItem.endTimeOfDay) {
|
||||
usedTime += usedTimeItem.usedMillis
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val maxTime = it.maximumTimeInMillis
|
||||
val maxTime = rule.maximumTimeInMillis
|
||||
val remaining = Math.max(0, maxTime - usedTime)
|
||||
|
||||
remaining
|
||||
|
|
|
@ -21,7 +21,7 @@ import io.timelimit.android.data.transaction
|
|||
import io.timelimit.android.date.DateInTimezone
|
||||
|
||||
object UsedTimeDeleter {
|
||||
fun deleteOldUsedTimeItems(database: Database, date: DateInTimezone) {
|
||||
fun deleteOldUsedTimeItems(database: Database, date: DateInTimezone, timestamp: Long) {
|
||||
Threads.database.execute {
|
||||
database.transaction().use {
|
||||
if (database.config().getDeviceAuthTokenSync().isNotEmpty()) {
|
||||
|
@ -38,6 +38,8 @@ object UsedTimeDeleter {
|
|||
|
||||
database.usedTimes().deleteOldUsedTimeItems(lastDayToKeep = date.dayOfEpoch - date.dayOfWeek)
|
||||
|
||||
database.sessionDuration().deleteOldSessionDurationItemsSync(trustedTimestamp = timestamp)
|
||||
|
||||
it.setSuccess()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,8 @@ import android.util.Log
|
|||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.date.DateInTimezone
|
||||
import io.timelimit.android.sync.actions.AddUsedTimeActionItem
|
||||
import io.timelimit.android.sync.actions.AddUsedTimeActionItemAdditionalCountingSlot
|
||||
import io.timelimit.android.sync.actions.AddUsedTimeActionItemSessionDurationLimitSlot
|
||||
import io.timelimit.android.sync.actions.AddUsedTimeActionVersion2
|
||||
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
||||
import io.timelimit.android.sync.actions.dispatch.CategoryNotFoundException
|
||||
|
@ -30,9 +32,16 @@ class UsedTimeUpdateHelper (val date: DateInTimezone) {
|
|||
|
||||
val timeToAdd = mutableMapOf<String, Int>()
|
||||
val extraTimeToSubtract = mutableMapOf<String, Int>()
|
||||
val sessionDurationLimitSlots = mutableMapOf<String, Set<AddUsedTimeActionItemSessionDurationLimitSlot>>()
|
||||
var trustedTimestamp: Long = 0
|
||||
val additionalSlots = mutableMapOf<String, Set<AddUsedTimeActionItemAdditionalCountingSlot>>()
|
||||
var shouldDoAutoCommit = false
|
||||
|
||||
fun add(categoryId: String, time: Int, includingExtraTime: Boolean) {
|
||||
fun add(
|
||||
categoryId: String, time: Int, slots: Set<AddUsedTimeActionItemAdditionalCountingSlot>,
|
||||
includingExtraTime: Boolean, sessionDurationLimits: Set<AddUsedTimeActionItemSessionDurationLimitSlot>,
|
||||
trustedTimestamp: Long
|
||||
) {
|
||||
if (time < 0) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
|
@ -43,10 +52,24 @@ class UsedTimeUpdateHelper (val date: DateInTimezone) {
|
|||
|
||||
timeToAdd[categoryId] = (timeToAdd[categoryId] ?: 0) + time
|
||||
|
||||
if (sessionDurationLimits.isNotEmpty()) {
|
||||
this.sessionDurationLimitSlots[categoryId] = sessionDurationLimits
|
||||
}
|
||||
|
||||
if (sessionDurationLimits.isNotEmpty() && trustedTimestamp != 0L) {
|
||||
this.trustedTimestamp = trustedTimestamp
|
||||
}
|
||||
|
||||
if (includingExtraTime) {
|
||||
extraTimeToSubtract[categoryId] = (extraTimeToSubtract[categoryId] ?: 0) + time
|
||||
}
|
||||
|
||||
if (additionalSlots[categoryId] != null && slots != additionalSlots[categoryId]) {
|
||||
shouldDoAutoCommit = true
|
||||
} else if (slots.isNotEmpty()) {
|
||||
additionalSlots[categoryId] = slots
|
||||
}
|
||||
|
||||
if (timeToAdd[categoryId]!! >= 1000 * 10) {
|
||||
shouldDoAutoCommit = true
|
||||
}
|
||||
|
@ -77,9 +100,12 @@ class UsedTimeUpdateHelper (val date: DateInTimezone) {
|
|||
AddUsedTimeActionItem(
|
||||
categoryId = categoryId,
|
||||
timeToAdd = timeToAdd[categoryId] ?: 0,
|
||||
extraTimeToSubtract = extraTimeToSubtract[categoryId] ?: 0
|
||||
extraTimeToSubtract = extraTimeToSubtract[categoryId] ?: 0,
|
||||
additionalCountingSlots = additionalSlots[categoryId] ?: emptySet(),
|
||||
sessionDurationLimits = sessionDurationLimitSlots[categoryId] ?: emptySet()
|
||||
)
|
||||
}
|
||||
},
|
||||
trustedTimestamp = trustedTimestamp
|
||||
),
|
||||
appLogic = appLogic,
|
||||
ignoreIfDeviceIsNotConfigured = true
|
||||
|
@ -96,6 +122,9 @@ class UsedTimeUpdateHelper (val date: DateInTimezone) {
|
|||
|
||||
timeToAdd.clear()
|
||||
extraTimeToSubtract.clear()
|
||||
sessionDurationLimitSlots.clear()
|
||||
trustedTimestamp = 0
|
||||
additionalSlots.clear()
|
||||
shouldDoAutoCommit = false
|
||||
}
|
||||
}
|
|
@ -360,7 +360,24 @@ object ApplyServerDataStatus {
|
|||
UsedTimeItem(
|
||||
dayOfEpoch = it.dayOfEpoch,
|
||||
usedMillis = it.usedMillis,
|
||||
categoryId = categoryId
|
||||
categoryId = categoryId,
|
||||
startTimeOfDay = it.startTimeOfDay,
|
||||
endTimeOfDay = it.endTimeOfDay
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
database.sessionDuration().deleteByCategoryId(categoryId)
|
||||
database.sessionDuration().insertSessionDurationItemsSync(
|
||||
newUsedTime.sessionDurations.map {
|
||||
SessionDuration(
|
||||
categoryId = categoryId,
|
||||
maxSessionDuration = it.maxSessionDuration,
|
||||
sessionPauseDuration = it.sessionPauseDuration,
|
||||
startMinuteOfDay = it.startMinuteOfDay,
|
||||
endMinuteOfDay = it.endMinuteOfDay,
|
||||
lastUsage = it.lastUsage,
|
||||
lastSessionDuration = it.lastSessionDuration
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
|
@ -22,9 +22,11 @@ import io.timelimit.android.data.IdGenerator
|
|||
import io.timelimit.android.data.customtypes.ImmutableBitmask
|
||||
import io.timelimit.android.data.customtypes.ImmutableBitmaskJson
|
||||
import io.timelimit.android.data.model.*
|
||||
import io.timelimit.android.extensions.MinuteOfDay
|
||||
import io.timelimit.android.integration.platform.*
|
||||
import io.timelimit.android.sync.network.ParentPassword
|
||||
import io.timelimit.android.sync.validation.ListValidation
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.util.*
|
||||
|
||||
|
@ -142,20 +144,26 @@ data class AddUsedTimeAction(val categoryId: String, val dayOfEpoch: Int, val ti
|
|||
}
|
||||
}
|
||||
|
||||
data class AddUsedTimeActionVersion2(val dayOfEpoch: Int, val items: List<AddUsedTimeActionItem>): AppLogicAction() {
|
||||
data class AddUsedTimeActionVersion2(
|
||||
val dayOfEpoch: Int,
|
||||
val items: List<AddUsedTimeActionItem>,
|
||||
val trustedTimestamp: Long
|
||||
): AppLogicAction() {
|
||||
companion object {
|
||||
const val TYPE_VALUE = "ADD_USED_TIME_V2"
|
||||
private const val DAY_OF_EPOCH = "d"
|
||||
private const val ITEMS = "i"
|
||||
private const val TRUSTED_TIMESTAMP = "t"
|
||||
|
||||
fun parse(action: JSONObject): AddUsedTimeActionVersion2 = AddUsedTimeActionVersion2(
|
||||
dayOfEpoch = action.getInt(DAY_OF_EPOCH),
|
||||
items = ParseUtils.readObjectArray(action.getJSONArray(ITEMS)).map { AddUsedTimeActionItem.parse(it) }
|
||||
items = ParseUtils.readObjectArray(action.getJSONArray(ITEMS)).map { AddUsedTimeActionItem.parse(it) },
|
||||
trustedTimestamp = if (action.has(TRUSTED_TIMESTAMP)) action.getLong(TRUSTED_TIMESTAMP) else 0L
|
||||
)
|
||||
}
|
||||
|
||||
init {
|
||||
if (dayOfEpoch < 0) {
|
||||
if (dayOfEpoch < 0 || trustedTimestamp < 0) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
|
@ -178,20 +186,42 @@ data class AddUsedTimeActionVersion2(val dayOfEpoch: Int, val items: List<AddUse
|
|||
items.forEach { it.serialize(writer) }
|
||||
writer.endArray()
|
||||
|
||||
if (trustedTimestamp != 0L) {
|
||||
writer.name(TRUSTED_TIMESTAMP).value(trustedTimestamp)
|
||||
}
|
||||
|
||||
writer.endObject()
|
||||
}
|
||||
}
|
||||
|
||||
data class AddUsedTimeActionItem(val categoryId: String, val timeToAdd: Int, val extraTimeToSubtract: Int) {
|
||||
data class AddUsedTimeActionItem(
|
||||
val categoryId: String, val timeToAdd: Int, val extraTimeToSubtract: Int,
|
||||
val additionalCountingSlots: Set<AddUsedTimeActionItemAdditionalCountingSlot>,
|
||||
val sessionDurationLimits: Set<AddUsedTimeActionItemSessionDurationLimitSlot>
|
||||
) {
|
||||
companion object {
|
||||
private const val CATEGORY_ID = "categoryId"
|
||||
private const val TIME_TO_ADD = "tta"
|
||||
private const val EXTRA_TIME_TO_SUBTRACT = "etts"
|
||||
private const val ADDITIONAL_COUNTING_SLOTS = "as"
|
||||
private const val SESSION_DURATION_LIMITS = "sdl"
|
||||
|
||||
fun parse(item: JSONObject): AddUsedTimeActionItem = AddUsedTimeActionItem(
|
||||
categoryId = item.getString(CATEGORY_ID),
|
||||
timeToAdd = item.getInt(TIME_TO_ADD),
|
||||
extraTimeToSubtract = item.getInt(EXTRA_TIME_TO_SUBTRACT)
|
||||
extraTimeToSubtract = item.getInt(EXTRA_TIME_TO_SUBTRACT),
|
||||
additionalCountingSlots = if (item.has(ADDITIONAL_COUNTING_SLOTS))
|
||||
item.getJSONArray(ADDITIONAL_COUNTING_SLOTS).let { array ->
|
||||
(0 until array.length()).map { AddUsedTimeActionItemAdditionalCountingSlot.parse(array.getJSONArray(it)) }
|
||||
}.toSet()
|
||||
else
|
||||
emptySet(),
|
||||
sessionDurationLimits = if (item.has(SESSION_DURATION_LIMITS))
|
||||
item.getJSONArray(SESSION_DURATION_LIMITS).let { array ->
|
||||
(0 until array.length()).map { AddUsedTimeActionItemSessionDurationLimitSlot.parse(array.getJSONArray(it)) }
|
||||
}.toSet()
|
||||
else
|
||||
emptySet()
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -214,10 +244,91 @@ data class AddUsedTimeActionItem(val categoryId: String, val timeToAdd: Int, val
|
|||
writer.name(TIME_TO_ADD).value(timeToAdd)
|
||||
writer.name(EXTRA_TIME_TO_SUBTRACT).value(extraTimeToSubtract)
|
||||
|
||||
if (additionalCountingSlots.isNotEmpty()) {
|
||||
writer.name(ADDITIONAL_COUNTING_SLOTS).beginArray()
|
||||
additionalCountingSlots.forEach { it.serialize(writer) }
|
||||
writer.endArray()
|
||||
}
|
||||
|
||||
if (sessionDurationLimits.isNotEmpty()) {
|
||||
writer.name(SESSION_DURATION_LIMITS).beginArray()
|
||||
sessionDurationLimits.forEach { it.serialize(writer) }
|
||||
writer.endArray()
|
||||
}
|
||||
|
||||
writer.endObject()
|
||||
}
|
||||
}
|
||||
|
||||
data class AddUsedTimeActionItemAdditionalCountingSlot(val start: Int, val end: Int) {
|
||||
companion object {
|
||||
fun parse(array: JSONArray): AddUsedTimeActionItemAdditionalCountingSlot {
|
||||
val length = array.length()
|
||||
|
||||
if (length != 2) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
return AddUsedTimeActionItemAdditionalCountingSlot(
|
||||
start = array.getInt(0),
|
||||
end = array.getInt(1)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
if (start < MinuteOfDay.MIN || end > MinuteOfDay.MAX || start > end) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
if (start == MinuteOfDay.MIN && end == MinuteOfDay.MAX) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
|
||||
fun serialize(writer: JsonWriter) {
|
||||
writer.beginArray()
|
||||
.value(start).value(end)
|
||||
.endArray()
|
||||
}
|
||||
}
|
||||
|
||||
data class AddUsedTimeActionItemSessionDurationLimitSlot(
|
||||
val startMinuteOfDay: Int, val endMinuteOfDay: Int,
|
||||
val maxSessionDuration: Int, val sessionPauseDuration: Int
|
||||
) {
|
||||
companion object {
|
||||
fun parse(array: JSONArray): AddUsedTimeActionItemSessionDurationLimitSlot {
|
||||
if (array.length() != 4) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
return AddUsedTimeActionItemSessionDurationLimitSlot(
|
||||
array.getInt(0), array.getInt(1), array.getInt(2), array.getInt(3)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
if (startMinuteOfDay < MinuteOfDay.MIN || endMinuteOfDay > MinuteOfDay.MAX || startMinuteOfDay > endMinuteOfDay) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
if (maxSessionDuration <= 0 || sessionPauseDuration <= 0) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
|
||||
fun serialize(writer: JsonWriter) {
|
||||
writer.beginArray()
|
||||
.value(startMinuteOfDay)
|
||||
.value(endMinuteOfDay)
|
||||
.value(maxSessionDuration)
|
||||
.value(sessionPauseDuration)
|
||||
.endArray()
|
||||
}
|
||||
}
|
||||
|
||||
// data class ClearTemporarilyAllowedAppsAction(val deviceId: String): AppLogicAction(), LocalOnlyAction
|
||||
|
||||
data class InstalledApp(val packageName: String, val title: String, val isLaunchable: Boolean, val recommendation: AppRecommendation) {
|
||||
|
@ -1299,13 +1410,20 @@ data class CreateTimeLimitRuleAction(val rule: TimeLimitRule): ParentAction() {
|
|||
}
|
||||
}
|
||||
|
||||
data class UpdateTimeLimitRuleAction(val ruleId: String, val dayMask: Byte, val maximumTimeInMillis: Int, val applyToExtraTimeUsage: Boolean): ParentAction() {
|
||||
data class UpdateTimeLimitRuleAction(
|
||||
val ruleId: String, val dayMask: Byte, val maximumTimeInMillis: Int, val applyToExtraTimeUsage: Boolean,
|
||||
val start: Int, val end: Int, val sessionDurationMilliseconds: Int, val sessionPauseMilliseconds: Int
|
||||
): ParentAction() {
|
||||
companion object {
|
||||
const val TYPE_VALUE = "UPDATE_TIMELIMIT_RULE"
|
||||
private const val RULE_ID = "ruleId"
|
||||
private const val MAX_TIME_IN_MILLIS = "time"
|
||||
private const val DAY_MASK = "days"
|
||||
private const val APPLY_TO_EXTRA_TIME_USAGE = "extraTime"
|
||||
private const val START = "start"
|
||||
private const val END = "end"
|
||||
private const val SESSION_DURATION_MILLISECONDS = "dur"
|
||||
private const val SESSION_PAUSE_MILLISECONDS = "pause"
|
||||
}
|
||||
|
||||
init {
|
||||
|
@ -1318,6 +1436,14 @@ data class UpdateTimeLimitRuleAction(val ruleId: String, val dayMask: Byte, val
|
|||
if (dayMask < 0 || dayMask > (1 or 2 or 4 or 8 or 16 or 32 or 64)) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
if (start < MinuteOfDay.MIN || end > MinuteOfDay.MAX || start > end) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
if (sessionDurationMilliseconds < 0 || sessionPauseMilliseconds < 0) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
|
||||
override fun serialize(writer: JsonWriter) {
|
||||
|
@ -1328,6 +1454,13 @@ data class UpdateTimeLimitRuleAction(val ruleId: String, val dayMask: Byte, val
|
|||
writer.name(MAX_TIME_IN_MILLIS).value(maximumTimeInMillis)
|
||||
writer.name(DAY_MASK).value(dayMask)
|
||||
writer.name(APPLY_TO_EXTRA_TIME_USAGE).value(applyToExtraTimeUsage)
|
||||
writer.name(START).value(start)
|
||||
writer.name(END).value(end)
|
||||
|
||||
if (sessionPauseMilliseconds > 0 || sessionDurationMilliseconds > 0) {
|
||||
writer.name(SESSION_DURATION_MILLISECONDS).value(sessionDurationMilliseconds)
|
||||
writer.name(SESSION_PAUSE_MILLISECONDS).value(sessionPauseMilliseconds)
|
||||
}
|
||||
|
||||
writer.endObject()
|
||||
}
|
||||
|
@ -1369,7 +1502,7 @@ data class AddUserAction(val name: String, val userType: UserType, val password:
|
|||
|
||||
fun parse(action: JSONObject): AddUserAction {
|
||||
var password: ParentPassword? = null
|
||||
val passwordObject = action.getJSONObject(PASSWORD)
|
||||
val passwordObject = action.optJSONObject(PASSWORD)
|
||||
|
||||
if (passwordObject != null) {
|
||||
password = ParentPassword.parse(passwordObject)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
* 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
|
||||
|
@ -19,16 +19,7 @@ import android.util.JsonWriter
|
|||
import java.io.StringWriter
|
||||
|
||||
object SerializationUtil {
|
||||
fun serializeAction(action: ParentAction): String {
|
||||
val stringWriter = StringWriter()
|
||||
val jsonWriter = JsonWriter(stringWriter)
|
||||
|
||||
action.serialize(jsonWriter)
|
||||
|
||||
return stringWriter.buffer.toString()
|
||||
}
|
||||
|
||||
fun serializeAction(action: AppLogicAction): String {
|
||||
fun serializeAction(action: Action): String {
|
||||
val stringWriter = StringWriter()
|
||||
val jsonWriter = JsonWriter(stringWriter)
|
||||
|
||||
|
|
|
@ -117,8 +117,22 @@ object ApplyActionUtil {
|
|||
|
||||
if (parsed is AddUsedTimeActionVersion2 && parsed.dayOfEpoch == action.dayOfEpoch) {
|
||||
var updatedAction: AddUsedTimeActionVersion2 = parsed
|
||||
var issues = false
|
||||
|
||||
if (parsed.trustedTimestamp != 0L && action.trustedTimestamp != 0L) {
|
||||
issues = action.items.map { it.categoryId } != parsed.items.map { it.categoryId } ||
|
||||
parsed.trustedTimestamp >= action.trustedTimestamp
|
||||
|
||||
updatedAction = updatedAction.copy(trustedTimestamp = action.trustedTimestamp)
|
||||
|
||||
// keep timestamp of the old action
|
||||
} else if (parsed.trustedTimestamp != 0L || action.trustedTimestamp != 0L) {
|
||||
issues = true
|
||||
}
|
||||
|
||||
action.items.forEach { newItem ->
|
||||
if (issues) return@forEach
|
||||
|
||||
val oldItem = updatedAction.items.find { it.categoryId == newItem.categoryId }
|
||||
|
||||
if (oldItem == null) {
|
||||
|
@ -126,10 +140,28 @@ object ApplyActionUtil {
|
|||
items = updatedAction.items + listOf(newItem)
|
||||
)
|
||||
} else {
|
||||
if (
|
||||
oldItem.additionalCountingSlots != newItem.additionalCountingSlots ||
|
||||
oldItem.sessionDurationLimits != newItem.sessionDurationLimits
|
||||
) {
|
||||
issues = true
|
||||
}
|
||||
|
||||
if (parsed.trustedTimestamp != 0L && action.trustedTimestamp != 0L) {
|
||||
val timeBeforeCurrentItem = action.trustedTimestamp - newItem.timeToAdd
|
||||
val diff = Math.abs(timeBeforeCurrentItem - parsed.trustedTimestamp)
|
||||
|
||||
if (diff > 2 * 1000) {
|
||||
issues = true
|
||||
}
|
||||
}
|
||||
|
||||
val mergedItem = AddUsedTimeActionItem(
|
||||
timeToAdd = oldItem.timeToAdd + newItem.timeToAdd,
|
||||
extraTimeToSubtract = oldItem.extraTimeToSubtract + newItem.extraTimeToSubtract,
|
||||
categoryId = newItem.categoryId
|
||||
categoryId = newItem.categoryId,
|
||||
additionalCountingSlots = oldItem.additionalCountingSlots,
|
||||
sessionDurationLimits = oldItem.sessionDurationLimits
|
||||
)
|
||||
|
||||
updatedAction = updatedAction.copy(
|
||||
|
@ -138,6 +170,7 @@ object ApplyActionUtil {
|
|||
}
|
||||
}
|
||||
|
||||
if (!issues) {
|
||||
// update the previous action
|
||||
database.pendingSyncAction().updateEncodedActionSync(
|
||||
sequenceNumber = previousAction.sequenceNumber,
|
||||
|
@ -155,6 +188,7 @@ object ApplyActionUtil {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val serializedAction = StringWriter().apply {
|
||||
JsonWriter(this).apply {
|
||||
|
|
|
@ -16,10 +16,8 @@
|
|||
package io.timelimit.android.sync.actions.dispatch
|
||||
|
||||
import io.timelimit.android.data.Database
|
||||
import io.timelimit.android.data.model.App
|
||||
import io.timelimit.android.data.model.AppActivity
|
||||
import io.timelimit.android.data.model.HadManipulationFlag
|
||||
import io.timelimit.android.data.model.UsedTimeItem
|
||||
import io.timelimit.android.data.model.*
|
||||
import io.timelimit.android.extensions.MinuteOfDay
|
||||
import io.timelimit.android.integration.platform.NewPermissionStatusUtil
|
||||
import io.timelimit.android.integration.platform.ProtectionLevelUtil
|
||||
import io.timelimit.android.integration.platform.RuntimePermissionStatusUtil
|
||||
|
@ -46,7 +44,9 @@ object LocalDatabaseAppLogicActionDispatcher {
|
|||
val updatedRows = database.usedTimes().addUsedTime(
|
||||
categoryId = categoryId,
|
||||
timeToAdd = action.timeToAdd,
|
||||
dayOfEpoch = action.dayOfEpoch
|
||||
dayOfEpoch = action.dayOfEpoch,
|
||||
start = MinuteOfDay.MIN,
|
||||
end = MinuteOfDay.MAX
|
||||
)
|
||||
|
||||
if (updatedRows == 0) {
|
||||
|
@ -55,7 +55,9 @@ object LocalDatabaseAppLogicActionDispatcher {
|
|||
database.usedTimes().insertUsedTime(UsedTimeItem(
|
||||
categoryId = categoryId,
|
||||
dayOfEpoch = action.dayOfEpoch,
|
||||
usedMillis = action.timeToAdd.toLong()
|
||||
usedMillis = action.timeToAdd.toLong(),
|
||||
startTimeOfDay = MinuteOfDay.MIN,
|
||||
endTimeOfDay = MinuteOfDay.MAX
|
||||
))
|
||||
}
|
||||
|
||||
|
@ -81,10 +83,13 @@ object LocalDatabaseAppLogicActionDispatcher {
|
|||
database.category().getCategoryByIdSync(item.categoryId)
|
||||
?: throw CategoryNotFoundException()
|
||||
|
||||
fun handle(start: Int, end: Int) {
|
||||
val updatedRows = database.usedTimes().addUsedTime(
|
||||
categoryId = item.categoryId,
|
||||
timeToAdd = item.timeToAdd,
|
||||
dayOfEpoch = action.dayOfEpoch
|
||||
dayOfEpoch = action.dayOfEpoch,
|
||||
start = start,
|
||||
end = end
|
||||
)
|
||||
|
||||
if (updatedRows == 0) {
|
||||
|
@ -93,10 +98,52 @@ object LocalDatabaseAppLogicActionDispatcher {
|
|||
database.usedTimes().insertUsedTime(UsedTimeItem(
|
||||
categoryId = item.categoryId,
|
||||
dayOfEpoch = action.dayOfEpoch,
|
||||
usedMillis = item.timeToAdd.toLong()
|
||||
usedMillis = item.timeToAdd.toLong(),
|
||||
startTimeOfDay = start,
|
||||
endTimeOfDay = end
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
handle(MinuteOfDay.MIN, MinuteOfDay.MAX)
|
||||
item.additionalCountingSlots.forEach { handle(it.start, it.end) }
|
||||
|
||||
kotlin.run {
|
||||
val hasTrustedTimestamp = action.trustedTimestamp != 0L
|
||||
|
||||
item.sessionDurationLimits.forEach { limit ->
|
||||
val oldItem = database.sessionDuration().getSessionDurationItemSync(
|
||||
categoryId = item.categoryId,
|
||||
maxSessionDuration = limit.maxSessionDuration,
|
||||
sessionPauseDuration = limit.sessionPauseDuration,
|
||||
startMinuteOfDay = limit.startMinuteOfDay,
|
||||
endMinuteOfDay = limit.endMinuteOfDay
|
||||
)
|
||||
|
||||
val newItem = oldItem?.copy(
|
||||
lastUsage = if (hasTrustedTimestamp) action.trustedTimestamp else oldItem.lastUsage,
|
||||
lastSessionDuration = if (hasTrustedTimestamp && action.trustedTimestamp - item.timeToAdd > oldItem.lastUsage + oldItem.sessionPauseDuration)
|
||||
item.timeToAdd.toLong()
|
||||
else
|
||||
oldItem.lastSessionDuration + item.timeToAdd.toLong()
|
||||
) ?: SessionDuration(
|
||||
categoryId = item.categoryId,
|
||||
maxSessionDuration = limit.maxSessionDuration,
|
||||
sessionPauseDuration = limit.sessionPauseDuration,
|
||||
startMinuteOfDay = limit.startMinuteOfDay,
|
||||
endMinuteOfDay = limit.endMinuteOfDay,
|
||||
lastSessionDuration = item.timeToAdd.toLong(),
|
||||
// this will cause a small loss of session durations
|
||||
lastUsage = if (hasTrustedTimestamp) action.trustedTimestamp else 0
|
||||
)
|
||||
|
||||
if (oldItem == null) {
|
||||
database.sessionDuration().insertSessionDurationItemSync(newItem)
|
||||
} else {
|
||||
database.sessionDuration().updateSessionDurationItemSync(newItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (item.extraTimeToSubtract != 0) {
|
||||
database.category().subtractCategoryExtraTime(
|
||||
|
|
|
@ -205,7 +205,11 @@ object LocalDatabaseParentActionDispatcher {
|
|||
database.timeLimitRules().updateTimeLimitRule(oldRule.copy(
|
||||
maximumTimeInMillis = action.maximumTimeInMillis,
|
||||
dayMask = action.dayMask,
|
||||
applyToExtraTimeUsage = action.applyToExtraTimeUsage
|
||||
applyToExtraTimeUsage = action.applyToExtraTimeUsage,
|
||||
startMinuteOfDay = action.start,
|
||||
endMinuteOfDay = action.end,
|
||||
sessionDurationMilliseconds = action.sessionDurationMilliseconds,
|
||||
sessionPauseMilliseconds = action.sessionPauseMilliseconds
|
||||
))
|
||||
}
|
||||
is SetDeviceUserAction -> {
|
||||
|
|
|
@ -33,6 +33,8 @@ data class ClientDataStatus(
|
|||
private const val APPS = "apps"
|
||||
private const val CATEGORIES = "categories"
|
||||
private const val USERS = "users"
|
||||
private const val CLIENT_LEVEL = "clientLevel"
|
||||
private const val CLIENT_LEVEL_VALUE = 2
|
||||
|
||||
val empty = ClientDataStatus(
|
||||
deviceListVersion = "",
|
||||
|
@ -75,6 +77,7 @@ data class ClientDataStatus(
|
|||
fun serialize(writer: JsonWriter) {
|
||||
writer.beginObject()
|
||||
|
||||
writer.name(CLIENT_LEVEL).value(CLIENT_LEVEL_VALUE)
|
||||
writer.name(DEVICES).value(deviceListVersion)
|
||||
writer.name(USERS).value(userListVersion)
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ import android.util.JsonReader
|
|||
import io.timelimit.android.data.customtypes.ImmutableBitmask
|
||||
import io.timelimit.android.data.customtypes.ImmutableBitmaskJson
|
||||
import io.timelimit.android.data.model.*
|
||||
import io.timelimit.android.extensions.MinuteOfDay
|
||||
import io.timelimit.android.extensions.parseList
|
||||
import io.timelimit.android.integration.platform.*
|
||||
import io.timelimit.android.sync.actions.AppActivityItem
|
||||
|
@ -476,16 +477,19 @@ data class ServerUpdatedCategoryAssignedApps(
|
|||
data class ServerUpdatedCategoryUsedTimes(
|
||||
val categoryId: String,
|
||||
val usedTimeItems: List<ServerUsedTimeItem>,
|
||||
val sessionDurations: List<ServerSessionDuration>,
|
||||
val version: String
|
||||
) {
|
||||
companion object {
|
||||
private const val CATEGORY_ID = "categoryId"
|
||||
private const val USED_TIMES_ITEMS = "times"
|
||||
private const val SESSION_DURATIONS = "sessionDurations"
|
||||
private const val VERSION = "version"
|
||||
|
||||
fun parse(reader: JsonReader): ServerUpdatedCategoryUsedTimes {
|
||||
var categoryId: String? = null
|
||||
var usedTimeItems: List<ServerUsedTimeItem>? = null
|
||||
var sessionDurations = emptyList<ServerSessionDuration>()
|
||||
var version: String? = null
|
||||
|
||||
reader.beginObject()
|
||||
|
@ -493,6 +497,7 @@ data class ServerUpdatedCategoryUsedTimes(
|
|||
when (reader.nextName()) {
|
||||
CATEGORY_ID -> categoryId = reader.nextString()
|
||||
USED_TIMES_ITEMS -> usedTimeItems = ServerUsedTimeItem.parseList(reader)
|
||||
SESSION_DURATIONS -> sessionDurations = ServerSessionDuration.parseList(reader)
|
||||
VERSION -> version = reader.nextString()
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
|
@ -502,6 +507,7 @@ data class ServerUpdatedCategoryUsedTimes(
|
|||
return ServerUpdatedCategoryUsedTimes(
|
||||
categoryId = categoryId!!,
|
||||
usedTimeItems = usedTimeItems!!,
|
||||
sessionDurations = sessionDurations,
|
||||
version = version!!
|
||||
)
|
||||
}
|
||||
|
@ -512,21 +518,29 @@ data class ServerUpdatedCategoryUsedTimes(
|
|||
|
||||
data class ServerUsedTimeItem(
|
||||
val dayOfEpoch: Int,
|
||||
val usedMillis: Long
|
||||
val usedMillis: Long,
|
||||
val startTimeOfDay: Int,
|
||||
val endTimeOfDay: Int
|
||||
) {
|
||||
companion object {
|
||||
private const val DAY_OF_EPOCH = "day"
|
||||
private const val USED_MILLIS = "time"
|
||||
private const val START_TIME_OF_DAY = "start"
|
||||
private const val END_TIME_OF_DAY = "end"
|
||||
|
||||
fun parse(reader: JsonReader): ServerUsedTimeItem {
|
||||
var dayOfEpoch: Int? = null
|
||||
var usedMillis: Long? = null
|
||||
var startTimeOfDay: Int = MinuteOfDay.MIN
|
||||
var endTimeOfDay: Int = MinuteOfDay.MAX
|
||||
|
||||
reader.beginObject()
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
DAY_OF_EPOCH -> dayOfEpoch = reader.nextInt()
|
||||
USED_MILLIS -> usedMillis = reader.nextLong()
|
||||
START_TIME_OF_DAY -> startTimeOfDay = reader.nextInt()
|
||||
END_TIME_OF_DAY -> endTimeOfDay = reader.nextInt()
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
|
@ -534,7 +548,9 @@ data class ServerUsedTimeItem(
|
|||
|
||||
return ServerUsedTimeItem(
|
||||
dayOfEpoch = dayOfEpoch!!,
|
||||
usedMillis = usedMillis!!
|
||||
usedMillis = usedMillis!!,
|
||||
startTimeOfDay = startTimeOfDay,
|
||||
endTimeOfDay = endTimeOfDay
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -552,6 +568,68 @@ data class ServerUsedTimeItem(
|
|||
}
|
||||
}
|
||||
|
||||
data class ServerSessionDuration(
|
||||
val maxSessionDuration: Int,
|
||||
val sessionPauseDuration: Int,
|
||||
val startMinuteOfDay: Int,
|
||||
val endMinuteOfDay: Int,
|
||||
val lastUsage: Long,
|
||||
val lastSessionDuration: Long
|
||||
) {
|
||||
companion object {
|
||||
private const val MAX_SESSION_DURATION = "md"
|
||||
private const val SESSION_PAUSE_DURATION = "spd"
|
||||
private const val START_MINUTE_OF_DAY = "sm"
|
||||
private const val END_MINUTE_OF_DAY = "em"
|
||||
private const val LAST_USAGE = "l"
|
||||
private const val LAST_SESSION_DURATION = "d"
|
||||
|
||||
fun parse(reader: JsonReader): ServerSessionDuration {
|
||||
var maxSessionDuration: Int? = null
|
||||
var sessionPauseDuration: Int? = null
|
||||
var startMinuteOfDay: Int? = null
|
||||
var endMinuteOfDay: Int? = null
|
||||
var lastUsage: Long? = null
|
||||
var lastSessionDuration: Long? = null
|
||||
|
||||
reader.beginObject()
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
MAX_SESSION_DURATION -> maxSessionDuration = reader.nextInt()
|
||||
SESSION_PAUSE_DURATION -> sessionPauseDuration = reader.nextInt()
|
||||
START_MINUTE_OF_DAY -> startMinuteOfDay = reader.nextInt()
|
||||
END_MINUTE_OF_DAY -> endMinuteOfDay = reader.nextInt()
|
||||
LAST_USAGE -> lastUsage = reader.nextLong()
|
||||
LAST_SESSION_DURATION -> lastSessionDuration = reader.nextLong()
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
reader.endObject()
|
||||
|
||||
return ServerSessionDuration(
|
||||
maxSessionDuration = maxSessionDuration!!,
|
||||
sessionPauseDuration = sessionPauseDuration!!,
|
||||
startMinuteOfDay = startMinuteOfDay!!,
|
||||
endMinuteOfDay = endMinuteOfDay!!,
|
||||
lastUsage = lastUsage!!,
|
||||
lastSessionDuration = lastSessionDuration!!
|
||||
)
|
||||
}
|
||||
|
||||
fun parseList(reader: JsonReader): List<ServerSessionDuration> {
|
||||
val result = ArrayList<ServerSessionDuration>()
|
||||
|
||||
reader.beginArray()
|
||||
while (reader.hasNext()) {
|
||||
result.add(parse(reader))
|
||||
}
|
||||
reader.endArray()
|
||||
|
||||
return Collections.unmodifiableList(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class ServerUpdatedTimeLimitRules(
|
||||
val categoryId: String,
|
||||
val version: String,
|
||||
|
@ -593,19 +671,31 @@ data class ServerTimeLimitRule(
|
|||
val id: String,
|
||||
val applyToExtraTimeUsage: Boolean,
|
||||
val dayMask: Byte,
|
||||
val maximumTimeInMillis: Int
|
||||
val maximumTimeInMillis: Int,
|
||||
val startMinuteOfDay: Int,
|
||||
val endMinuteOfDay: Int,
|
||||
val sessionDurationMilliseconds: Int,
|
||||
val sessionPauseMilliseconds: Int
|
||||
) {
|
||||
companion object {
|
||||
private const val ID = "id"
|
||||
private const val APPLY_TO_EXTRA_TIME_USAGE = "extraTime"
|
||||
private const val DAY_MASK = "dayMask"
|
||||
private const val MAXIMUM_TIME_IN_MILLIS = "maxTime"
|
||||
private const val START_MINUTE_OF_DAY = "start"
|
||||
private const val END_MINUTE_OF_DAY = "end"
|
||||
private const val SESSION_DURATION_MILLISECONDS = "session"
|
||||
private const val SESSION_PAUSE_MILLISECONDS = "pause"
|
||||
|
||||
fun parse(reader: JsonReader): ServerTimeLimitRule {
|
||||
var id: String? = null
|
||||
var applyToExtraTimeUsage: Boolean? = null
|
||||
var dayMask: Byte? = null
|
||||
var maximumTimeInMillis: Int? = null
|
||||
var startMinuteOfDay = TimeLimitRule.MIN_START_MINUTE
|
||||
var endMinuteOfDay = TimeLimitRule.MAX_END_MINUTE
|
||||
var sessionDurationMilliseconds: Int = 0
|
||||
var sessionPauseMilliseconds: Int = 0
|
||||
|
||||
reader.beginObject()
|
||||
while (reader.hasNext()) {
|
||||
|
@ -614,6 +704,10 @@ data class ServerTimeLimitRule(
|
|||
APPLY_TO_EXTRA_TIME_USAGE -> applyToExtraTimeUsage = reader.nextBoolean()
|
||||
DAY_MASK -> dayMask = reader.nextInt().toByte()
|
||||
MAXIMUM_TIME_IN_MILLIS -> maximumTimeInMillis = reader.nextInt()
|
||||
START_MINUTE_OF_DAY -> startMinuteOfDay = reader.nextInt()
|
||||
END_MINUTE_OF_DAY -> endMinuteOfDay = reader.nextInt()
|
||||
SESSION_DURATION_MILLISECONDS -> sessionDurationMilliseconds = reader.nextInt()
|
||||
SESSION_PAUSE_MILLISECONDS -> sessionPauseMilliseconds = reader.nextInt()
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
|
@ -623,7 +717,11 @@ data class ServerTimeLimitRule(
|
|||
id = id!!,
|
||||
applyToExtraTimeUsage = applyToExtraTimeUsage!!,
|
||||
dayMask = dayMask!!,
|
||||
maximumTimeInMillis = maximumTimeInMillis!!
|
||||
maximumTimeInMillis = maximumTimeInMillis!!,
|
||||
startMinuteOfDay = startMinuteOfDay,
|
||||
endMinuteOfDay = endMinuteOfDay,
|
||||
sessionDurationMilliseconds = sessionDurationMilliseconds,
|
||||
sessionPauseMilliseconds = sessionPauseMilliseconds
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -645,7 +743,11 @@ data class ServerTimeLimitRule(
|
|||
applyToExtraTimeUsage = applyToExtraTimeUsage,
|
||||
dayMask = dayMask,
|
||||
maximumTimeInMillis = maximumTimeInMillis,
|
||||
categoryId = categoryId
|
||||
categoryId = categoryId,
|
||||
startMinuteOfDay = startMinuteOfDay,
|
||||
endMinuteOfDay = endMinuteOfDay,
|
||||
sessionDurationMilliseconds = sessionDurationMilliseconds,
|
||||
sessionPauseMilliseconds = sessionPauseMilliseconds
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
* 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
|
||||
|
@ -21,9 +21,11 @@ import android.view.ViewGroup
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.data.model.TimeLimitRule
|
||||
import io.timelimit.android.data.model.UsedTimeItem
|
||||
import io.timelimit.android.databinding.AddItemViewBinding
|
||||
import io.timelimit.android.databinding.FragmentCategoryTimeLimitRuleItemBinding
|
||||
import io.timelimit.android.databinding.TimeLimitRuleIntroductionBinding
|
||||
import io.timelimit.android.extensions.MinuteOfDay
|
||||
import io.timelimit.android.util.JoinUtil
|
||||
import io.timelimit.android.util.TimeTextUtil
|
||||
import kotlin.properties.Delegates
|
||||
|
@ -36,7 +38,8 @@ class Adapter: RecyclerView.Adapter<ViewHolder>() {
|
|||
}
|
||||
|
||||
var data: List<TimeLimitRuleItem> by Delegates.observable(emptyList()) { _, _, _ -> notifyDataSetChanged() }
|
||||
var usedTimes: List<Long>? by Delegates.observable(null as List<Long>?) { _, _, _ -> notifyDataSetChanged() }
|
||||
var usedTimes: List<UsedTimeItem> by Delegates.observable(emptyList()) { _, _, _ -> notifyDataSetChanged() }
|
||||
var epochDayOfStartOfWeek: Int by Delegates.observable(0) { _, _, _ -> notifyDataSetChanged() }
|
||||
var handlers: Handlers? = null
|
||||
|
||||
init {
|
||||
|
@ -110,26 +113,42 @@ class Adapter: RecyclerView.Adapter<ViewHolder>() {
|
|||
is TimeLimitRuleRuleItem -> {
|
||||
val rule = item.rule
|
||||
val binding = (holder as ItemViewHolder).view
|
||||
val context = binding.root.context
|
||||
val dayNames = binding.root.resources.getStringArray(R.array.days_of_week_array)
|
||||
val usedTime = usedTimes?.mapIndexed { index, value ->
|
||||
if (rule.dayMask.toInt() and (1 shl index) != 0) {
|
||||
value
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}?.sum()?.toInt() ?: 0
|
||||
val usedTime = usedTimes.filter { usedTime ->
|
||||
val dayOfWeek = usedTime.dayOfEpoch - epochDayOfStartOfWeek
|
||||
usedTime.startTimeOfDay == rule.startMinuteOfDay && usedTime.endTimeOfDay == rule.endMinuteOfDay &&
|
||||
(rule.dayMask.toInt() and (1 shl dayOfWeek) != 0)
|
||||
}.map { it.usedMillis }.sum().toInt()
|
||||
|
||||
binding.maxTimeString = TimeTextUtil.time(rule.maximumTimeInMillis, binding.root.context)
|
||||
binding.usageAsText = TimeTextUtil.used(usedTime, binding.root.context)
|
||||
binding.maxTimeString = TimeTextUtil.time(rule.maximumTimeInMillis, context)
|
||||
binding.usageAsText = TimeTextUtil.used(usedTime, context)
|
||||
binding.usageProgressInPercent = if (rule.maximumTimeInMillis > 0)
|
||||
(usedTime * 100 / rule.maximumTimeInMillis)
|
||||
else
|
||||
100
|
||||
binding.daysString = JoinUtil.join(
|
||||
dayNames.filterIndexed { index, _ -> (rule.dayMask.toInt() and (1 shl index)) != 0 },
|
||||
binding.root.context
|
||||
context
|
||||
)
|
||||
binding.timeAreaString = if (rule.appliesToWholeDay)
|
||||
null
|
||||
else
|
||||
context.getString(
|
||||
R.string.category_time_limit_rules_time_area,
|
||||
MinuteOfDay.format(rule.startMinuteOfDay),
|
||||
MinuteOfDay.format(rule.endMinuteOfDay)
|
||||
)
|
||||
binding.appliesToExtraTime = rule.applyToExtraTimeUsage
|
||||
binding.sessionLimitString = if (rule.sessionDurationLimitEnabled)
|
||||
context.getString(
|
||||
R.string.category_time_limit_rules_session_limit,
|
||||
TimeTextUtil.time(rule.sessionPauseMilliseconds, context),
|
||||
TimeTextUtil.time(rule.sessionDurationMilliseconds, context)
|
||||
)
|
||||
else
|
||||
null
|
||||
|
||||
binding.card.setOnClickListener { handlers?.onTimeLimitRuleClicked(rule) }
|
||||
|
||||
binding.executePendingBindings()
|
||||
|
|
|
@ -74,14 +74,19 @@ class CategoryTimeLimitRulesFragment : Fragment(), EditTimeLimitRuleDialogFragme
|
|||
|
||||
val userDate = database.user().getUserByIdLive(params.childId).getDateLive(logic.realTimeLogic)
|
||||
|
||||
val usedTimeItems = userDate.switchMap {
|
||||
date ->
|
||||
userDate.switchMap { date ->
|
||||
val firstDayOfWeekAsEpochDay = date.dayOfEpoch - date.dayOfWeek
|
||||
|
||||
database.usedTimes().getUsedTimesOfWeek(
|
||||
categoryId = params.categoryId,
|
||||
firstDayOfWeekAsEpochDay = date.dayOfEpoch - date.dayOfWeek
|
||||
)
|
||||
firstDayOfWeekAsEpochDay = firstDayOfWeekAsEpochDay
|
||||
).map { res ->
|
||||
firstDayOfWeekAsEpochDay to res
|
||||
}
|
||||
}.observe(viewLifecycleOwner, Observer {
|
||||
adapter.epochDayOfStartOfWeek = it.first
|
||||
adapter.usedTimes = it.second
|
||||
})
|
||||
|
||||
val hasHiddenIntro = database.config().wereHintsShown(HintsToShow.TIME_LIMIT_RULE_INTRODUCTION)
|
||||
|
||||
|
@ -97,16 +102,10 @@ class CategoryTimeLimitRulesFragment : Fragment(), EditTimeLimitRuleDialogFragme
|
|||
listOf(TimeLimitRuleIntroductionItem) + baseList
|
||||
}
|
||||
}
|
||||
}.observe(this, Observer {
|
||||
}.observe(viewLifecycleOwner, Observer {
|
||||
adapter.data = it
|
||||
})
|
||||
|
||||
usedTimeItems.observe(this, Observer {
|
||||
usedTimes ->
|
||||
|
||||
adapter.usedTimes = (0..6).map { usedTimes[it]?.usedMillis ?: 0 } }
|
||||
)
|
||||
|
||||
adapter.handlers = object: Handlers {
|
||||
override fun onTimeLimitRuleClicked(rule: TimeLimitRule) {
|
||||
if (auth.requestAuthenticationOrReturnTrue()) {
|
||||
|
@ -171,7 +170,11 @@ class CategoryTimeLimitRulesFragment : Fragment(), EditTimeLimitRuleDialogFragme
|
|||
ruleId = oldRule.id,
|
||||
applyToExtraTimeUsage = oldRule.applyToExtraTimeUsage,
|
||||
maximumTimeInMillis = oldRule.maximumTimeInMillis,
|
||||
dayMask = oldRule.dayMask
|
||||
dayMask = oldRule.dayMask,
|
||||
start = oldRule.startMinuteOfDay,
|
||||
end = oldRule.endMinuteOfDay,
|
||||
sessionDurationMilliseconds = oldRule.sessionDurationMilliseconds,
|
||||
sessionPauseMilliseconds = oldRule.sessionPauseMilliseconds
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* 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.timelimit_rules.edit
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.Observer
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.async.Threads
|
||||
import io.timelimit.android.databinding.DurationPickerDialogFragmentBinding
|
||||
import io.timelimit.android.extensions.showSafe
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
import io.timelimit.android.ui.view.SelectTimeSpanViewListener
|
||||
|
||||
class DurationPickerDialogFragment: DialogFragment() {
|
||||
companion object {
|
||||
private const val DIALOG_TAG = "DurationPickerDialogFragment"
|
||||
private const val TITLE_RES = "titleRes"
|
||||
private const val INDEX = "index"
|
||||
private const val START_TIME_IN_MILLIS = "startTimeInMillis"
|
||||
|
||||
fun newInstance(titleRes: Int, index: Int, target: Fragment, startTimeInMillis: Int) = DurationPickerDialogFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putInt(TITLE_RES, titleRes)
|
||||
putInt(INDEX, index)
|
||||
putInt(START_TIME_IN_MILLIS, startTimeInMillis)
|
||||
}
|
||||
|
||||
setTargetFragment(target, 0)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val binding = DurationPickerDialogFragmentBinding.inflate(LayoutInflater.from(context!!))
|
||||
val view = binding.duration
|
||||
val target = targetFragment as DurationPickerDialogFragmentListener
|
||||
val index = arguments!!.getInt(INDEX)
|
||||
val titleRes = arguments!!.getInt(TITLE_RES)
|
||||
val startTimeInMillis = arguments!!.getInt(START_TIME_IN_MILLIS)
|
||||
val config = DefaultAppLogic.with(context!!).database.config()
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
view.timeInMillis = startTimeInMillis.toLong()
|
||||
}
|
||||
|
||||
config.getEnableAlternativeDurationSelectionAsync().observe(this, Observer {
|
||||
view.enablePickerMode(it)
|
||||
})
|
||||
|
||||
view.listener = object: SelectTimeSpanViewListener {
|
||||
override fun onTimeSpanChanged(newTimeInMillis: Long) = Unit
|
||||
|
||||
override fun setEnablePickerMode(enable: Boolean) {
|
||||
Threads.database.execute {
|
||||
config.setEnableAlternativeDurationSelectionSync(enable)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return AlertDialog.Builder(context!!, theme)
|
||||
.setTitle(titleRes)
|
||||
.setView(binding.root)
|
||||
.setPositiveButton(R.string.generic_ok) { _, _ ->
|
||||
target.onDurationSelected(view.timeInMillis.toInt(), index)
|
||||
}
|
||||
.setNegativeButton(R.string.generic_cancel, null)
|
||||
.create()
|
||||
}
|
||||
|
||||
fun show(fragmentManager: FragmentManager) = showSafe(fragmentManager, DIALOG_TAG)
|
||||
}
|
||||
|
||||
interface DurationPickerDialogFragmentListener {
|
||||
fun onDurationSelected(durationInMillis: Int, index: Int)
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
* 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
|
||||
|
@ -20,12 +20,13 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.Observer
|
||||
import com.google.android.material.R
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.async.Threads
|
||||
import io.timelimit.android.coroutines.runAsync
|
||||
import io.timelimit.android.data.IdGenerator
|
||||
|
@ -33,6 +34,7 @@ import io.timelimit.android.data.model.HintsToShow
|
|||
import io.timelimit.android.data.model.TimeLimitRule
|
||||
import io.timelimit.android.data.model.UserType
|
||||
import io.timelimit.android.databinding.FragmentEditTimeLimitRuleDialogBinding
|
||||
import io.timelimit.android.extensions.MinuteOfDay
|
||||
import io.timelimit.android.extensions.showSafe
|
||||
import io.timelimit.android.livedata.waitForNonNullValue
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
|
@ -44,11 +46,12 @@ import io.timelimit.android.ui.main.getActivityViewModel
|
|||
import io.timelimit.android.ui.mustread.MustReadFragment
|
||||
import io.timelimit.android.ui.view.SelectDayViewHandlers
|
||||
import io.timelimit.android.ui.view.SelectTimeSpanViewListener
|
||||
import io.timelimit.android.util.TimeTextUtil
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.*
|
||||
|
||||
|
||||
class EditTimeLimitRuleDialogFragment : BottomSheetDialogFragment() {
|
||||
class EditTimeLimitRuleDialogFragment : BottomSheetDialogFragment(), DurationPickerDialogFragmentListener {
|
||||
companion object {
|
||||
private const val PARAM_EXISTING_RULE = "a"
|
||||
private const val PARAM_CATEGORY_ID = "b"
|
||||
|
@ -76,6 +79,8 @@ class EditTimeLimitRuleDialogFragment : BottomSheetDialogFragment() {
|
|||
|
||||
var existingRule: TimeLimitRule? = null
|
||||
var savedNewRule: TimeLimitRule? = null
|
||||
lateinit var newRule: TimeLimitRule
|
||||
lateinit var view: FragmentEditTimeLimitRuleDialogBinding
|
||||
|
||||
private val categoryId: String by lazy {
|
||||
if (existingRule != null) {
|
||||
|
@ -109,42 +114,6 @@ class EditTimeLimitRuleDialogFragment : BottomSheetDialogFragment() {
|
|||
?: arguments?.getParcelable<TimeLimitRule?>(PARAM_EXISTING_RULE)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val view = FragmentEditTimeLimitRuleDialogBinding.inflate(layoutInflater, container, false)
|
||||
val listener = targetFragment as EditTimeLimitRuleDialogFragmentListener
|
||||
var newRule: TimeLimitRule
|
||||
val database = DefaultAppLogic.with(context!!).database
|
||||
|
||||
auth.authenticatedUser.observe(this, Observer {
|
||||
if (it == null || it.second.type != UserType.Parent) {
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
})
|
||||
|
||||
if (existingRule == null) {
|
||||
view.isNewRule = true
|
||||
|
||||
newRule = TimeLimitRule(
|
||||
id = IdGenerator.generateId(),
|
||||
categoryId = categoryId,
|
||||
applyToExtraTimeUsage = false,
|
||||
dayMask = 0,
|
||||
maximumTimeInMillis = 1000 * 60 * 60 * 5 / 2 // 2,5 (5/2) hours
|
||||
)
|
||||
} else {
|
||||
view.isNewRule = false
|
||||
|
||||
newRule = existingRule!!
|
||||
}
|
||||
|
||||
run {
|
||||
val restoredRule = savedInstanceState?.getParcelable<TimeLimitRule>(STATE_RULE)
|
||||
|
||||
if (restoredRule != null) {
|
||||
newRule = restoredRule
|
||||
}
|
||||
}
|
||||
|
||||
fun bindRule() {
|
||||
savedNewRule = newRule
|
||||
|
||||
|
@ -159,6 +128,54 @@ class EditTimeLimitRuleDialogFragment : BottomSheetDialogFragment() {
|
|||
val affectedDays = Math.max(0, (0..6).map { (newRule.dayMask.toInt() shr it) and 1 }.sum())
|
||||
view.timeSpan.maxDays = Math.max(0, affectedDays - 1) // max prevents crash
|
||||
view.affectsMultipleDays = affectedDays >= 2
|
||||
|
||||
view.applyToWholeDay = newRule.appliesToWholeDay
|
||||
view.startTime = MinuteOfDay.format(newRule.startMinuteOfDay)
|
||||
view.endTime = MinuteOfDay.format(newRule.endMinuteOfDay)
|
||||
|
||||
view.enableSessionDurationLimit = newRule.sessionDurationLimitEnabled
|
||||
view.sessionBreakText = TimeTextUtil.minutes(newRule.sessionPauseMilliseconds / (1000 * 60), context!!)
|
||||
view.sessionLengthText = TimeTextUtil.minutes(newRule.sessionDurationMilliseconds / (1000 * 60), context!!)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val listener = targetFragment as EditTimeLimitRuleDialogFragmentListener
|
||||
val database = DefaultAppLogic.with(context!!).database
|
||||
|
||||
view = FragmentEditTimeLimitRuleDialogBinding.inflate(layoutInflater, container, false)
|
||||
|
||||
auth.authenticatedUser.observe(viewLifecycleOwner, Observer {
|
||||
if (it == null || it.second.type != UserType.Parent) {
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
})
|
||||
|
||||
if (existingRule == null) {
|
||||
view.isNewRule = true
|
||||
|
||||
newRule = TimeLimitRule(
|
||||
id = IdGenerator.generateId(),
|
||||
categoryId = categoryId,
|
||||
applyToExtraTimeUsage = false,
|
||||
dayMask = 0,
|
||||
maximumTimeInMillis = 1000 * 60 * 60 * 5 / 2, // 2,5 (5/2) hours
|
||||
startMinuteOfDay = TimeLimitRule.MIN_START_MINUTE,
|
||||
endMinuteOfDay = TimeLimitRule.MAX_END_MINUTE,
|
||||
sessionPauseMilliseconds = 0,
|
||||
sessionDurationMilliseconds = 0
|
||||
)
|
||||
} else {
|
||||
view.isNewRule = false
|
||||
|
||||
newRule = existingRule!!
|
||||
}
|
||||
|
||||
run {
|
||||
val restoredRule = savedInstanceState?.getParcelable<TimeLimitRule>(STATE_RULE)
|
||||
|
||||
if (restoredRule != null) {
|
||||
newRule = restoredRule
|
||||
}
|
||||
}
|
||||
|
||||
bindRule()
|
||||
|
@ -181,6 +198,72 @@ class EditTimeLimitRuleDialogFragment : BottomSheetDialogFragment() {
|
|||
bindRule()
|
||||
}
|
||||
|
||||
override fun updateApplyToWholeDay(apply: Boolean) {
|
||||
if (apply) {
|
||||
newRule = newRule.copy(
|
||||
startMinuteOfDay = TimeLimitRule.MIN_START_MINUTE,
|
||||
endMinuteOfDay = TimeLimitRule.MAX_END_MINUTE
|
||||
)
|
||||
} else {
|
||||
newRule = newRule.copy(
|
||||
startMinuteOfDay = 10 * 60,
|
||||
endMinuteOfDay = 16 * 60
|
||||
)
|
||||
}
|
||||
|
||||
bindRule()
|
||||
}
|
||||
|
||||
override fun updateStartTime() {
|
||||
TimePickerDialogFragment.newInstance(
|
||||
editTimeLimitRuleDialogFragment = this@EditTimeLimitRuleDialogFragment,
|
||||
index = 0,
|
||||
startMinuteOfDay = newRule.startMinuteOfDay
|
||||
).show(parentFragmentManager)
|
||||
}
|
||||
|
||||
override fun updateEndTime() {
|
||||
TimePickerDialogFragment.newInstance(
|
||||
editTimeLimitRuleDialogFragment = this@EditTimeLimitRuleDialogFragment,
|
||||
index = 1,
|
||||
startMinuteOfDay = newRule.endMinuteOfDay
|
||||
).show(parentFragmentManager)
|
||||
}
|
||||
|
||||
override fun updateSessionDurationLimit(enable: Boolean) {
|
||||
if (enable) {
|
||||
newRule = newRule.copy(
|
||||
sessionDurationMilliseconds = 1000 * 60 * 30,
|
||||
sessionPauseMilliseconds = 1000 * 60 * 10
|
||||
)
|
||||
} else {
|
||||
newRule = newRule.copy(
|
||||
sessionDurationMilliseconds = 0,
|
||||
sessionPauseMilliseconds = 0
|
||||
)
|
||||
}
|
||||
|
||||
bindRule()
|
||||
}
|
||||
|
||||
override fun updateSessionLength() {
|
||||
DurationPickerDialogFragment.newInstance(
|
||||
titleRes = R.string.category_time_limit_rules_session_limit_duration,
|
||||
index = 0,
|
||||
target = this@EditTimeLimitRuleDialogFragment,
|
||||
startTimeInMillis = newRule.sessionDurationMilliseconds
|
||||
).show(parentFragmentManager)
|
||||
}
|
||||
|
||||
override fun updateSessionBreak() {
|
||||
DurationPickerDialogFragment.newInstance(
|
||||
titleRes = R.string.category_time_limit_rules_session_limit_pause,
|
||||
index = 1,
|
||||
target = this@EditTimeLimitRuleDialogFragment,
|
||||
startTimeInMillis = newRule.sessionPauseMilliseconds
|
||||
).show(parentFragmentManager)
|
||||
}
|
||||
|
||||
override fun onSaveRule() {
|
||||
view.timeSpan.clearNumberPickerFocus()
|
||||
|
||||
|
@ -191,7 +274,11 @@ class EditTimeLimitRuleDialogFragment : BottomSheetDialogFragment() {
|
|||
ruleId = newRule.id,
|
||||
maximumTimeInMillis = newRule.maximumTimeInMillis,
|
||||
dayMask = newRule.dayMask,
|
||||
applyToExtraTimeUsage = newRule.applyToExtraTimeUsage
|
||||
applyToExtraTimeUsage = newRule.applyToExtraTimeUsage,
|
||||
start = newRule.startMinuteOfDay,
|
||||
end = newRule.endMinuteOfDay,
|
||||
sessionDurationMilliseconds = newRule.sessionDurationMilliseconds,
|
||||
sessionPauseMilliseconds = newRule.sessionPauseMilliseconds
|
||||
)
|
||||
)) {
|
||||
return
|
||||
|
@ -245,13 +332,13 @@ class EditTimeLimitRuleDialogFragment : BottomSheetDialogFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
database.config().getEnableAlternativeDurationSelectionAsync().observe(this, Observer {
|
||||
database.config().getEnableAlternativeDurationSelectionAsync().observe(viewLifecycleOwner, Observer {
|
||||
view.timeSpan.enablePickerMode(it)
|
||||
})
|
||||
|
||||
if (existingRule != null) {
|
||||
database.timeLimitRules()
|
||||
.getTimeLimitRuleByIdLive(existingRule!!.id).observe(this, Observer {
|
||||
.getTimeLimitRuleByIdLive(existingRule!!.id).observe(viewLifecycleOwner, Observer {
|
||||
if (it == null) {
|
||||
// rule was deleted
|
||||
dismissAllowingStateLoss()
|
||||
|
@ -301,10 +388,64 @@ class EditTimeLimitRuleDialogFragment : BottomSheetDialogFragment() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleTimePickerResult(index: Int, minuteOfDay: Int) {
|
||||
if (!MinuteOfDay.isValid(minuteOfDay)) {
|
||||
Toast.makeText(context, R.string.error_general, Toast.LENGTH_SHORT).show()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (index == 0) {
|
||||
// start minute
|
||||
|
||||
if (minuteOfDay > newRule.endMinuteOfDay) {
|
||||
Toast.makeText(context, R.string.category_time_limit_rules_invalid_range, Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
newRule = newRule.copy(startMinuteOfDay = minuteOfDay)
|
||||
bindRule()
|
||||
}
|
||||
} else if (index == 1) {
|
||||
// end minute
|
||||
|
||||
if (minuteOfDay < newRule.startMinuteOfDay) {
|
||||
Toast.makeText(context, R.string.category_time_limit_rules_invalid_range, Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
newRule = newRule.copy(endMinuteOfDay = minuteOfDay)
|
||||
bindRule()
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(context, R.string.error_general, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDurationSelected(durationInMillis: Int, index: Int) {
|
||||
if (index == 0) {
|
||||
newRule = newRule.copy(
|
||||
sessionDurationMilliseconds = durationInMillis
|
||||
)
|
||||
|
||||
bindRule()
|
||||
} else if (index == 1) {
|
||||
newRule = newRule.copy(
|
||||
sessionPauseMilliseconds = durationInMillis
|
||||
)
|
||||
|
||||
bindRule()
|
||||
} else {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface Handlers {
|
||||
fun updateApplyToExtraTime(apply: Boolean)
|
||||
fun updateApplyToWholeDay(apply: Boolean)
|
||||
fun updateStartTime()
|
||||
fun updateEndTime()
|
||||
fun updateSessionDurationLimit(enable: Boolean)
|
||||
fun updateSessionLength()
|
||||
fun updateSessionBreak()
|
||||
fun onSaveRule()
|
||||
fun onDeleteRule()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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.timelimit_rules.edit
|
||||
|
||||
import android.app.Dialog
|
||||
import android.app.TimePickerDialog
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import io.timelimit.android.extensions.showSafe
|
||||
|
||||
class TimePickerDialogFragment: DialogFragment() {
|
||||
companion object {
|
||||
private const val INDEX = "index"
|
||||
private const val START_MINUTE_OF_DAY = "startMinuteOfDay"
|
||||
private const val DIALOG_TAG = "TimePickerDialogFragment"
|
||||
|
||||
fun newInstance(
|
||||
editTimeLimitRuleDialogFragment: EditTimeLimitRuleDialogFragment,
|
||||
index: Int,
|
||||
startMinuteOfDay: Int
|
||||
) = TimePickerDialogFragment().apply {
|
||||
setTargetFragment(editTimeLimitRuleDialogFragment, 0)
|
||||
arguments = Bundle().apply {
|
||||
putInt(INDEX, index)
|
||||
putInt(START_MINUTE_OF_DAY, startMinuteOfDay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val fragment = targetFragment as EditTimeLimitRuleDialogFragment
|
||||
val index = arguments!!.getInt(INDEX)
|
||||
val startMinuteOfDay = arguments!!.getInt(START_MINUTE_OF_DAY)
|
||||
|
||||
return TimePickerDialog(context, theme, { _, hour, minute ->
|
||||
fragment.handleTimePickerResult(index, hour * 60 + minute)
|
||||
}, startMinuteOfDay / 60, startMinuteOfDay % 60, true)
|
||||
}
|
||||
|
||||
fun show(fragmentManager: FragmentManager) = showSafe(fragmentManager, DIALOG_TAG)
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
* 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
|
||||
|
@ -16,24 +16,28 @@
|
|||
package io.timelimit.android.ui.manage.category.usagehistory
|
||||
|
||||
import android.text.format.DateFormat
|
||||
import android.text.format.DateUtils
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.paging.PagedListAdapter
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.timelimit.android.data.model.UsedTimeItem
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.data.model.UsedTimeListItem
|
||||
import io.timelimit.android.databinding.FragmentUsageHistoryItemBinding
|
||||
import io.timelimit.android.extensions.MinuteOfDay
|
||||
import io.timelimit.android.util.TimeTextUtil
|
||||
import org.threeten.bp.LocalDate
|
||||
import org.threeten.bp.ZoneOffset
|
||||
import java.util.*
|
||||
|
||||
class UsageHistoryAdapter: PagedListAdapter<UsedTimeItem, UsageHistoryViewHolder>(diffCallback) {
|
||||
class UsageHistoryAdapter: PagedListAdapter<UsedTimeListItem, UsageHistoryViewHolder>(diffCallback) {
|
||||
companion object {
|
||||
private val diffCallback = object: DiffUtil.ItemCallback<UsedTimeItem>() {
|
||||
override fun areContentsTheSame(oldItem: UsedTimeItem, newItem: UsedTimeItem) = oldItem == newItem
|
||||
override fun areItemsTheSame(oldItem: UsedTimeItem, newItem: UsedTimeItem) =
|
||||
(oldItem.dayOfEpoch == newItem.dayOfEpoch) && (oldItem.categoryId == newItem.categoryId)
|
||||
private val diffCallback = object: DiffUtil.ItemCallback<UsedTimeListItem>() {
|
||||
override fun areContentsTheSame(oldItem: UsedTimeListItem, newItem: UsedTimeListItem) = oldItem == newItem
|
||||
override fun areItemsTheSame(oldItem: UsedTimeListItem, newItem: UsedTimeListItem) =
|
||||
(oldItem.day == newItem.day) && (oldItem.startMinuteOfDay == newItem.startMinuteOfDay) &&
|
||||
(oldItem.endMinuteOfDay == newItem.endMinuteOfDay) && (oldItem.maxSessionDuration == newItem.maxSessionDuration)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -50,17 +54,35 @@ class UsageHistoryAdapter: PagedListAdapter<UsedTimeItem, UsageHistoryViewHolder
|
|||
val binding = holder.binding
|
||||
val context = binding.root.context
|
||||
|
||||
if (item == null) {
|
||||
val timeAreaString = if (item == null || item.startMinuteOfDay == MinuteOfDay.MIN && item.endMinuteOfDay == MinuteOfDay.MAX)
|
||||
null
|
||||
else
|
||||
context.getString(R.string.usage_history_time_area, MinuteOfDay.format(item.startMinuteOfDay), MinuteOfDay.format(item.endMinuteOfDay))
|
||||
|
||||
if (item?.day != null) {
|
||||
val dateObject = LocalDate.ofEpochDay(item.day)
|
||||
val dateString = DateFormat.getDateFormat(context).apply {
|
||||
timeZone = TimeZone.getTimeZone("UTC")
|
||||
}.format(Date(dateObject.atStartOfDay().toEpochSecond(ZoneOffset.UTC) * 1000L))
|
||||
|
||||
binding.date = dateString
|
||||
binding.timeArea = timeAreaString
|
||||
binding.usedTime = TimeTextUtil.used(item.duration.toInt(), context)
|
||||
} else if (item?.lastUsage != null && item.maxSessionDuration != null && item.pauseDuration != null) {
|
||||
binding.date = context.getString(
|
||||
R.string.usage_history_item_session_duration_limit,
|
||||
TimeTextUtil.time(item.maxSessionDuration.toInt(), context),
|
||||
TimeTextUtil.time(item.pauseDuration.toInt(), context)
|
||||
)
|
||||
binding.timeArea = timeAreaString
|
||||
binding.usedTime = TimeTextUtil.used(item.duration.toInt(), context) + "\n" +
|
||||
context.getString(
|
||||
R.string.usage_history_item_last_usage,
|
||||
DateUtils.formatDateTime(context, item.lastUsage, DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_SHOW_DATE)
|
||||
)
|
||||
} else {
|
||||
binding.date = ""
|
||||
binding.usedTime = ""
|
||||
} else {
|
||||
val date = LocalDate.ofEpochDay(item.dayOfEpoch.toLong())
|
||||
|
||||
binding.date = DateFormat.getDateFormat(context).apply {
|
||||
timeZone = TimeZone.getTimeZone("UTC")
|
||||
}.format(Date(date.atStartOfDay().toEpochSecond(ZoneOffset.UTC) * 1000L))
|
||||
|
||||
binding.usedTime = TimeTextUtil.used(item.usedMillis.toInt(), context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
* 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
|
||||
|
@ -42,11 +42,11 @@ class UsageHistoryFragment : Fragment() {
|
|||
val adapter = UsageHistoryAdapter()
|
||||
|
||||
LivePagedListBuilder(
|
||||
database.usedTimes().getUsedTimesByCategoryId(params.categoryId),
|
||||
database.usedTimes().getUsedTimeListItemsByCategoryId(params.categoryId),
|
||||
10
|
||||
)
|
||||
.build()
|
||||
.observe(this, Observer {
|
||||
.observe(viewLifecycleOwner, Observer {
|
||||
binding.isEmpty = it.isEmpty()
|
||||
adapter.submitList(it)
|
||||
})
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
package io.timelimit.android.ui.manage.child.category
|
||||
|
||||
import android.app.Application
|
||||
import android.util.SparseLongArray
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import io.timelimit.android.data.extensions.mapToTimezone
|
||||
|
@ -24,6 +23,7 @@ import io.timelimit.android.data.extensions.sorted
|
|||
import io.timelimit.android.data.model.HintsToShow
|
||||
import io.timelimit.android.date.DateInTimezone
|
||||
import io.timelimit.android.date.getMinuteOfWeek
|
||||
import io.timelimit.android.extensions.MinuteOfDay
|
||||
import io.timelimit.android.livedata.ignoreUnchanged
|
||||
import io.timelimit.android.livedata.liveDataFromFunction
|
||||
import io.timelimit.android.livedata.map
|
||||
|
@ -100,19 +100,16 @@ class ManageChildCategoriesModel(application: Application): AndroidViewModel(app
|
|||
isBlockedTimeNow = category.blockedMinutesInWeek.read(childMinuteOfWeek),
|
||||
remainingTimeToday = RemainingTime.getRemainingTime(
|
||||
dayOfWeek = childDate.dayOfWeek,
|
||||
usedTimes = SparseLongArray().apply {
|
||||
usedTimeItemsForCategory.forEach { usedTimeItem ->
|
||||
|
||||
val dayOfWeek = usedTimeItem.dayOfEpoch - firstDayOfWeek
|
||||
|
||||
put(dayOfWeek, usedTimeItem.usedMillis)
|
||||
}
|
||||
},
|
||||
usedTimes = usedTimeItemsForCategory,
|
||||
rules = rules,
|
||||
extraTime = category.getExtraTime(dayOfEpoch = childDate.dayOfEpoch)
|
||||
extraTime = category.getExtraTime(dayOfEpoch = childDate.dayOfEpoch),
|
||||
minuteOfDay = childMinuteOfWeek % MinuteOfDay.LENGTH,
|
||||
firstDayOfWeekAsEpochDay = firstDayOfWeek
|
||||
)?.includingExtraTime,
|
||||
usedTimeToday = usedTimeItemsForCategory.find { item -> item.dayOfEpoch == childDate.dayOfEpoch }?.usedMillis
|
||||
?: 0,
|
||||
usedTimeToday = usedTimeItemsForCategory.find { item ->
|
||||
item.dayOfEpoch == childDate.dayOfEpoch && item.startTimeOfDay == MinuteOfDay.MIN &&
|
||||
item.endTimeOfDay == MinuteOfDay.MAX
|
||||
}?.usedMillis ?: 0,
|
||||
usedForNotAssignedApps = categoryForUnassignedApps == category.id,
|
||||
parentCategoryTitle = parentCategory?.title
|
||||
)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
* 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
|
||||
|
@ -64,7 +64,11 @@ class DefaultCategories private constructor(private val context: Context) {
|
|||
categoryId = categoryId,
|
||||
applyToExtraTimeUsage = false,
|
||||
dayMask = (1 shl day).toByte(),
|
||||
maximumTimeInMillis = 1000 * 60 * 30 // 30 minutes
|
||||
maximumTimeInMillis = 1000 * 60 * 30, // 30 minutes
|
||||
startMinuteOfDay = TimeLimitRule.MIN_START_MINUTE,
|
||||
endMinuteOfDay = TimeLimitRule.MAX_END_MINUTE,
|
||||
sessionPauseMilliseconds = 0,
|
||||
sessionDurationMilliseconds = 0
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -77,7 +81,11 @@ class DefaultCategories private constructor(private val context: Context) {
|
|||
categoryId = categoryId,
|
||||
applyToExtraTimeUsage = false,
|
||||
dayMask = (1 shl day).toByte(),
|
||||
maximumTimeInMillis = 1000 * 60 * 60 * 3 // 3 hours
|
||||
maximumTimeInMillis = 1000 * 60 * 60 * 3, // 3 hours
|
||||
startMinuteOfDay = TimeLimitRule.MIN_START_MINUTE,
|
||||
endMinuteOfDay = TimeLimitRule.MAX_END_MINUTE,
|
||||
sessionPauseMilliseconds = 0,
|
||||
sessionDurationMilliseconds = 0
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -93,7 +101,11 @@ class DefaultCategories private constructor(private val context: Context) {
|
|||
categoryId = categoryId,
|
||||
applyToExtraTimeUsage = false,
|
||||
dayMask = 1 + 2 + 4 + 8 + 16 + 32 + 64,
|
||||
maximumTimeInMillis = 1000 * 60 * 60 * 6 // 6 hours
|
||||
maximumTimeInMillis = 1000 * 60 * 60 * 6, // 6 hours
|
||||
startMinuteOfDay = TimeLimitRule.MIN_START_MINUTE,
|
||||
endMinuteOfDay = TimeLimitRule.MAX_END_MINUTE,
|
||||
sessionPauseMilliseconds = 0,
|
||||
sessionDurationMilliseconds = 0
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ import io.timelimit.android.databinding.ViewSelectTimeSpanBinding
|
|||
import io.timelimit.android.util.TimeTextUtil
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
class SelectTimeSpanView(context: Context, attributeSet: AttributeSet): FrameLayout(context, attributeSet) {
|
||||
class SelectTimeSpanView(context: Context, attributeSet: AttributeSet? = null): FrameLayout(context, attributeSet) {
|
||||
private val binding = ViewSelectTimeSpanBinding.inflate(LayoutInflater.from(context), this, false)
|
||||
|
||||
init {
|
||||
|
|
|
@ -18,7 +18,11 @@ package io.timelimit.android.ui.widget
|
|||
import android.util.SparseLongArray
|
||||
import androidx.lifecycle.LiveData
|
||||
import io.timelimit.android.data.extensions.mapToTimezone
|
||||
import io.timelimit.android.data.model.getCurrentTimeSlotStartMinute
|
||||
import io.timelimit.android.data.model.getSlotSwitchMinutes
|
||||
import io.timelimit.android.date.DateInTimezone
|
||||
import io.timelimit.android.date.getMinuteOfWeek
|
||||
import io.timelimit.android.extensions.MinuteOfDay
|
||||
import io.timelimit.android.livedata.ignoreUnchanged
|
||||
import io.timelimit.android.livedata.liveDataFromFunction
|
||||
import io.timelimit.android.livedata.map
|
||||
|
@ -34,6 +38,11 @@ object TimesWidgetItems {
|
|||
val userDate = userTimezone.switchMap { timeZone ->
|
||||
liveDataFromFunction { DateInTimezone.newInstance(logic.realTimeLogic.getCurrentTimeInMillis(), timeZone) }
|
||||
}.ignoreUnchanged()
|
||||
val userMinuteOfWeek = userTimezone.switchMap { timeZone ->
|
||||
liveDataFromFunction {
|
||||
getMinuteOfWeek(logic.realTimeLogic.getCurrentTimeInMillis(), timeZone)
|
||||
}
|
||||
}.ignoreUnchanged()
|
||||
val categories = userId.switchMap { logic.database.category().getCategoriesByChildId(it) }
|
||||
val usedTimeItemsForWeek = userDate.switchMap { date ->
|
||||
categories.switchMap { categories ->
|
||||
|
@ -49,8 +58,14 @@ object TimesWidgetItems {
|
|||
categories.map { category -> category.id }
|
||||
)
|
||||
}
|
||||
val timeLimitSlot = timeLimitRules.map { it.getSlotSwitchMinutes() }.switchMap {
|
||||
userMinuteOfWeek.switchMap { minuteOfWeek ->
|
||||
getCurrentTimeSlotStartMinute(it, userMinuteOfWeek.map { it % MinuteOfDay.LENGTH })
|
||||
}
|
||||
}
|
||||
val categoryItems = categories.switchMap { categories ->
|
||||
timeLimitRules.switchMap { timeLimitRules ->
|
||||
timeLimitSlot.switchMap { timeLimitSlot ->
|
||||
userDate.switchMap { childDate ->
|
||||
usedTimeItemsForWeek.map { usedTimeItemsForWeek ->
|
||||
val rulesByCategoryId = timeLimitRules.groupBy { rule -> rule.categoryId }
|
||||
|
@ -61,28 +76,23 @@ object TimesWidgetItems {
|
|||
val rules = rulesByCategoryId[category.id] ?: emptyList()
|
||||
val usedTimeItemsForCategory = usedTimesByCategory[category.id]
|
||||
?: emptyList()
|
||||
val parentCategory = categories.find { it.id == category.parentCategoryId }
|
||||
|
||||
TimesWidgetItem(
|
||||
title = category.title,
|
||||
remainingTimeToday = RemainingTime.getRemainingTime(
|
||||
dayOfWeek = childDate.dayOfWeek,
|
||||
usedTimes = SparseLongArray().apply {
|
||||
usedTimeItemsForCategory.forEach { usedTimeItem ->
|
||||
|
||||
val dayOfWeek = usedTimeItem.dayOfEpoch - firstDayOfWeek
|
||||
|
||||
put(dayOfWeek, usedTimeItem.usedMillis)
|
||||
}
|
||||
},
|
||||
usedTimes = usedTimeItemsForCategory,
|
||||
rules = rules,
|
||||
extraTime = category.getExtraTime(dayOfEpoch = childDate.dayOfEpoch)
|
||||
extraTime = category.getExtraTime(dayOfEpoch = childDate.dayOfEpoch),
|
||||
minuteOfDay = timeLimitSlot,
|
||||
firstDayOfWeekAsEpochDay = firstDayOfWeek
|
||||
)?.includingExtraTime
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.ignoreUnchanged()
|
||||
|
||||
return categoryItems
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
* 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
|
||||
|
@ -67,6 +67,14 @@ object TimeTextUtil {
|
|||
}
|
||||
}
|
||||
|
||||
fun pauseIn(time: Int, context: Context): String {
|
||||
return if (time <= 1000 * 60) {
|
||||
context.getString(R.string.util_time_pause_shortly)
|
||||
} else {
|
||||
context.getString(R.string.util_time_pause_in, time(time, context))
|
||||
}
|
||||
}
|
||||
|
||||
fun used(time: Int, context: Context): String {
|
||||
return if (time <= 0) {
|
||||
context.resources.getString(R.string.util_time_unused)
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M9,16h2L11,8L9,8v8zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM13,16h2L15,8h-2v8z"/>
|
||||
</vector>
|
22
app/src/main/res/layout/duration_picker_dialog_fragment.xml
Normal file
22
app/src/main/res/layout/duration_picker_dialog_fragment.xml
Normal file
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
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/>.
|
||||
-->
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<io.timelimit.android.ui.view.SelectTimeSpanView
|
||||
android:padding="16dp"
|
||||
android:id="@+id/duration"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
</layout>
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
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.
|
||||
|
@ -34,6 +34,14 @@
|
|||
name="daysString"
|
||||
type="String" />
|
||||
|
||||
<variable
|
||||
name="timeAreaString"
|
||||
type="String" />
|
||||
|
||||
<variable
|
||||
name="sessionLimitString"
|
||||
type="String" />
|
||||
|
||||
<variable
|
||||
name="appliesToExtraTime"
|
||||
type="Boolean" />
|
||||
|
@ -72,6 +80,22 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
android:text="@{timeAreaString}"
|
||||
tools:text="von 12:00 bis 18:00"
|
||||
android:visibility="@{TextUtils.isEmpty(timeAreaString) ? View.GONE : View.VISIBLE}"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
android:text="@{sessionLimitString}"
|
||||
tools:text="10 Minuten Pause nach 5 Minuten Nutzung"
|
||||
android:visibility="@{TextUtils.isEmpty(sessionLimitString) ? View.GONE : View.VISIBLE}"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:visibility="@{safeUnbox(appliesToExtraTime) ? View.VISIBLE : View.GONE}"
|
||||
android:textAppearance="?android:textAppearanceSmall"
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
-->
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
tools:context="io.timelimit.android.ui.manage.category.timelimit_rules.edit.EditTimeLimitRuleDialogFragment">
|
||||
|
||||
<data>
|
||||
|
@ -25,6 +26,10 @@
|
|||
name="applyToExtraTime"
|
||||
type="Boolean" />
|
||||
|
||||
<variable
|
||||
name="applyToWholeDay"
|
||||
type="Boolean" />
|
||||
|
||||
<variable
|
||||
name="handlers"
|
||||
type="io.timelimit.android.ui.manage.category.timelimit_rules.edit.Handlers" />
|
||||
|
@ -33,9 +38,33 @@
|
|||
name="affectsMultipleDays"
|
||||
type="boolean" />
|
||||
|
||||
<variable
|
||||
name="startTime"
|
||||
type="String" />
|
||||
|
||||
<variable
|
||||
name="endTime"
|
||||
type="String" />
|
||||
|
||||
<variable
|
||||
name="enableSessionDurationLimit"
|
||||
type="boolean" />
|
||||
|
||||
<variable
|
||||
name="sessionLengthText"
|
||||
type="String" />
|
||||
|
||||
<variable
|
||||
name="sessionBreakText"
|
||||
type="String" />
|
||||
|
||||
<import type="android.view.View" />
|
||||
</data>
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/scroll">
|
||||
<LinearLayout
|
||||
android:padding="8dp"
|
||||
android:orientation="vertical"
|
||||
|
@ -63,8 +92,133 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:onClick="@{() -> handlers.updateApplyToWholeDay(!safeUnbox(applyToWholeDay))}"
|
||||
android:checked="@{safeUnbox(applyToWholeDay)}"
|
||||
android:text="@string/category_time_limit_rules_apply_to_whole_day"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<com.google.android.flexbox.FlexboxLayout
|
||||
app:flexWrap="wrap"
|
||||
app:alignItems="center"
|
||||
android:visibility="@{safeUnbox(applyToWholeDay) ? View.GONE : View.VISIBLE}"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:visibility="@{affectsMultipleDays ? View.VISIBLE : View.GONE}"
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
android:padding="8dp"
|
||||
android:text="@string/category_time_limit_rules_apply_to_part_day_1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<Button
|
||||
style="?materialButtonOutlinedStyle"
|
||||
android:onClick="@{() -> handlers.updateStartTime()}"
|
||||
tools:text="10:00"
|
||||
android:text="@{startTime}"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
android:padding="8dp"
|
||||
android:text="@string/category_time_limit_rules_apply_to_part_day_2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<Button
|
||||
style="?materialButtonOutlinedStyle"
|
||||
android:onClick="@{() -> handlers.updateEndTime()}"
|
||||
tools:text="16:00"
|
||||
android:text="@{endTime}"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
android:padding="8dp"
|
||||
android:text="@string/category_time_limit_rules_apply_to_part_day_3"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
</com.google.android.flexbox.FlexboxLayout>
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:onClick="@{() -> handlers.updateSessionDurationLimit(!safeUnbox(enableSessionDurationLimit))}"
|
||||
android:checked="@{enableSessionDurationLimit}"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/category_time_limit_rules_enable_session_limit" />
|
||||
|
||||
<androidx.gridlayout.widget.GridLayout
|
||||
android:visibility="@{enableSessionDurationLimit ? View.VISIBLE : View.GONE}"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
app:layout_columnWeight="1"
|
||||
app:layout_gravity="fill_vertical"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingStart="8dp"
|
||||
app:layout_row="1"
|
||||
app:layout_column="1"
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
android:text="@string/category_time_limit_rules_session_limit_duration"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<Button
|
||||
android:layout_marginEnd="4dp"
|
||||
app:layout_columnWeight="0"
|
||||
android:onClick="@{() -> handlers.updateSessionLength()}"
|
||||
app:layout_gravity="center_vertical|fill_horizontal"
|
||||
style="?materialButtonOutlinedStyle"
|
||||
app:layout_row="1"
|
||||
app:layout_column="3"
|
||||
tools:text="199999 Minuten"
|
||||
android:text="@{sessionLengthText}"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
app:layout_columnWeight="1"
|
||||
app:layout_gravity="fill_vertical"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingStart="8dp"
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
app:layout_row="2"
|
||||
app:layout_column="1"
|
||||
android:text="@string/category_time_limit_rules_session_limit_pause"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<Button
|
||||
android:layout_marginEnd="4dp"
|
||||
app:layout_columnWeight="0"
|
||||
android:onClick="@{() -> handlers.updateSessionBreak()}"
|
||||
app:layout_gravity="center_vertical|fill_horizontal"
|
||||
style="?materialButtonOutlinedStyle"
|
||||
app:layout_row="2"
|
||||
app:layout_column="3"
|
||||
tools:text="20 Minuten"
|
||||
android:text="@{sessionBreakText}"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</androidx.gridlayout.widget.GridLayout>
|
||||
|
||||
<TextView
|
||||
android:visibility="@{safeUnbox(applyToWholeDay) ? View.GONE : View.VISIBLE}"
|
||||
android:textAppearance="?android:textAppearanceSmall"
|
||||
android:text="@string/category_time_limit_rules_warning_day_part"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:visibility="@{safeUnbox(affectsMultipleDays) ? View.VISIBLE : View.GONE}"
|
||||
android:textAppearance="?android:textAppearanceSmall"
|
||||
android:text="@string/category_time_limit_rules_warning_multiple_days"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -102,5 +256,5 @@
|
|||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
</layout>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
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.
|
||||
|
@ -20,9 +20,16 @@
|
|||
name="date"
|
||||
type="String" />
|
||||
|
||||
<variable
|
||||
name="timeArea"
|
||||
type="String" />
|
||||
|
||||
<variable
|
||||
name="usedTime"
|
||||
type="String" />
|
||||
|
||||
<import type="android.text.TextUtils" />
|
||||
<import type="android.view.View" />
|
||||
</data>
|
||||
|
||||
<LinearLayout
|
||||
|
@ -38,6 +45,14 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:visibility="@{TextUtils.isEmpty(timeArea) ? View.GONE : View.VISIBLE}"
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
android:text="@{timeArea}"
|
||||
tools:text="von 10:00 bis 16:00"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
android:text="@{usedTime}"
|
||||
|
|
|
@ -275,6 +275,20 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="0dp"
|
||||
tools:ignore="UnusedAttribute"
|
||||
android:drawablePadding="16dp"
|
||||
android:drawableTint="?colorOnSurface"
|
||||
android:drawableStart="@drawable/ic_pause_circle_outline_black_24dp"
|
||||
android:visibility="@{reason == BlockingReason.SessionDurationLimit ? View.VISIBLE : View.GONE}"
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
android:text="@{@string/lock_reason_session_duration(blockedKindLabel)}"
|
||||
tools:text="@string/lock_reason_session_duration"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<ProgressBar
|
||||
android:visibility="@{reason == null ? View.VISIBLE : View.GONE}"
|
||||
android:padding="8dp"
|
||||
|
@ -513,7 +527,7 @@
|
|||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<io.timelimit.android.ui.view.ManageDisableTimelimitsView
|
||||
android:visibility="@{reason == BlockingReason.BlockedAtThisTime || reason == BlockingReason.TimeOver || reason == BlockingReason.TimeOverExtraTimeCanBeUsedLater ? View.VISIBLE : View.GONE}"
|
||||
android:visibility="@{reason == BlockingReason.BlockedAtThisTime || reason == BlockingReason.TimeOver || reason == BlockingReason.TimeOverExtraTimeCanBeUsedLater || reason == BlockingReason.SessionDurationLimit ? View.VISIBLE : View.GONE}"
|
||||
android:id="@+id/manage_disable_time_limits"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
android:layout_height="wrap_content">
|
||||
<HorizontalScrollView
|
||||
android:layout_centerHorizontal="true"
|
||||
android:id="@+id/scroll"
|
||||
android:id="@+id/select_days_scroll"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
<com.google.android.material.button.MaterialButtonToggleGroup
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
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.
|
||||
|
@ -23,9 +23,19 @@
|
|||
Regeln, bei denen nur ein Tag gewählt wurde, begrenzen nur diesen Tag.
|
||||
</string>
|
||||
<string name="category_time_limit_rules_applied_to_extra_time">gilt auch für ggf. vorhandene Extra-Zeit</string>
|
||||
<string name="category_time_limit_rules_time_area">nur von %s bis %s gültig</string>
|
||||
<string name="category_time_limit_rules_session_limit">%s Pause nach %s Nutzung</string>
|
||||
<string name="category_time_limit_rule_dialog_new">Regel erstellen</string>
|
||||
<string name="category_time_limit_rule_dialog_edit">Regel bearbeiten</string>
|
||||
<string name="category_time_limit_rules_apply_to_extra_time">Auch auf die Extra-Zeit anwenden</string>
|
||||
<string name="category_time_limit_rules_apply_to_whole_day">Für den ganzen Tag anwenden</string>
|
||||
<string name="category_time_limit_rules_apply_to_part_day_1">Von</string>
|
||||
<string name="category_time_limit_rules_apply_to_part_day_2">bis</string>
|
||||
<string name="category_time_limit_rules_apply_to_part_day_3">anwenden</string>
|
||||
<string name="category_time_limit_rules_invalid_range">Die Anfangszeit muss vor der Endzeit liegen</string>
|
||||
<string name="category_time_limit_rules_enable_session_limit">Sitzungsdauer beschränken</string>
|
||||
<string name="category_time_limit_rules_session_limit_duration">maximale Nutzungsdauer</string>
|
||||
<string name="category_time_limit_rules_session_limit_pause">Pausendauer</string>
|
||||
|
||||
<string name="category_time_limit_rules_snackbar_created">Regel wurde erstellt</string>
|
||||
<string name="category_time_limit_rules_snackbar_updated">Regel wurde geändert</string>
|
||||
|
@ -35,4 +45,9 @@
|
|||
wird die Gesamtnutzungsdauer in einer Woche an den gewählten Tagen einschränken.
|
||||
Wenn Sei die Begrenzung je Tag wollen, dann erstellen Sie eine Regel je Tag.
|
||||
</string>
|
||||
<string name="category_time_limit_rules_warning_day_part">Diese Regel begrenzt nur
|
||||
die Nutzung im angegebenen Intervall. Ohne Regeln für andere Intervalle
|
||||
oder Sperrzeiten wird es keine Begrenzung außerhalb dieses Intervalls
|
||||
geben.
|
||||
</string>
|
||||
</resources>
|
||||
|
|
|
@ -80,6 +80,10 @@
|
|||
und dieses Akkulimit wurde erreicht.
|
||||
Zum Wiederfreigeben muss der Akku aufgeladen werden.
|
||||
</string>
|
||||
<string name="lock_reason_session_duration">
|
||||
Für diese %s gibt es eine Sitzungsdauerbegrenzung.
|
||||
Nach dem Ablauf der Pausenzeit wird die Sperre aufgehoben.
|
||||
</string>
|
||||
|
||||
<string name="lock_reason_short_no_category">keine Kategorie</string>
|
||||
<string name="lock_reason_short_temporarily_blocked">vorübergehend gesperrt</string>
|
||||
|
@ -89,6 +93,7 @@
|
|||
<string name="lock_reason_short_requires_current_device">nicht als aktuelles Gerät gewählt</string>
|
||||
<string name="lock_reason_short_notification_blocking">alle Benachrichtigungen werden blockiert</string>
|
||||
<string name="lock_reason_short_battery_limit">Akkulimit unterschritten</string>
|
||||
<string name="lock_reason_short_session_duration">Sitzungsdauergrenze erreicht</string>
|
||||
|
||||
<string name="lock_overlay_warning">
|
||||
Öffnen des Sperrbildschirms fehlgeschlagen.
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
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.
|
||||
|
@ -20,4 +20,7 @@
|
|||
Die Nutzungszeiten werden nur erfasst,
|
||||
wenn Zeitbegrenzungsregeln existieren und die zeitbegrenzten Apps genutzt werden
|
||||
</string>
|
||||
<string name="usage_history_time_area">von %s bis %s</string>
|
||||
<string name="usage_history_item_session_duration_limit">Sitzungsdauerbegrenzung von %s mit %s Pause</string>
|
||||
<string name="usage_history_item_last_usage">Letzte Verwendung: %s</string>
|
||||
</resources>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
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.
|
||||
|
@ -39,6 +39,8 @@
|
|||
<string name="util_limit_hours_and_minutes"><xliff:g id="hours">%1$s</xliff:g> und <xliff:g>%2$s</xliff:g></string>
|
||||
<string name="util_time_remaining">Noch <xliff:g example="3 minutes" id="time">%1$s</xliff:g></string>
|
||||
<string name="util_time_done">Zeit verbraucht</string>
|
||||
<string name="util_time_pause_in">Pause in %s</string>
|
||||
<string name="util_time_pause_shortly">gleich eine Pause</string>
|
||||
|
||||
<string name="util_time_unused">Unbenutzt</string>
|
||||
<string name="util_time_used"><xliff:g example="4 minutes" id="used time">%1$s</xliff:g> benutzt</string>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
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.
|
||||
|
@ -23,9 +23,19 @@
|
|||
Rules at which only one day is checked only limit the selected day.
|
||||
</string>
|
||||
<string name="category_time_limit_rules_applied_to_extra_time">is applied to extra time, too</string>
|
||||
<string name="category_time_limit_rules_time_area">only applied from %s to %s</string>
|
||||
<string name="category_time_limit_rules_session_limit">%s break after %s usage</string>
|
||||
<string name="category_time_limit_rule_dialog_new">Create rule</string>
|
||||
<string name="category_time_limit_rule_dialog_edit">Edit rule</string>
|
||||
<string name="category_time_limit_rules_apply_to_extra_time">Apply to the extra time</string>
|
||||
<string name="category_time_limit_rules_apply_to_whole_day">Apply to whole day</string>
|
||||
<string name="category_time_limit_rules_apply_to_part_day_1">Apply from</string>
|
||||
<string name="category_time_limit_rules_apply_to_part_day_2">to</string>
|
||||
<string name="category_time_limit_rules_apply_to_part_day_3"></string>
|
||||
<string name="category_time_limit_rules_invalid_range">The start time must be before the end time</string>
|
||||
<string name="category_time_limit_rules_enable_session_limit">Limit session duration</string>
|
||||
<string name="category_time_limit_rules_session_limit_duration">Maximum session duration</string>
|
||||
<string name="category_time_limit_rules_session_limit_pause">Break duration after session</string>
|
||||
|
||||
<string name="category_time_limit_rules_snackbar_created">Rule was created</string>
|
||||
<string name="category_time_limit_rules_snackbar_updated">Rule was modified</string>
|
||||
|
@ -35,4 +45,9 @@
|
|||
will limit the total usage duration during one week at the selected days.
|
||||
If you want it per day, create one rule per day.
|
||||
</string>
|
||||
<string name="category_time_limit_rules_warning_day_part">This rule
|
||||
will limit the usage duration only in the specified interval.
|
||||
Without rules for other times or blocked time areas, there will
|
||||
be no limit outside of this interval.
|
||||
</string>
|
||||
</resources>
|
||||
|
|
|
@ -84,6 +84,11 @@
|
|||
battery limit was reached.
|
||||
To unlock it, charge the battery.
|
||||
</string>
|
||||
<string name="lock_reason_session_duration">
|
||||
This %s is part of a category which has got a session duration limit
|
||||
and this limit was reached.
|
||||
It will be unlocked after the break duration.
|
||||
</string>
|
||||
|
||||
<string name="lock_reason_short_no_category">no category</string>
|
||||
<string name="lock_reason_short_temporarily_blocked">temporarily blocked</string>
|
||||
|
@ -93,6 +98,7 @@
|
|||
<string name="lock_reason_short_requires_current_device">device must be the current device</string>
|
||||
<string name="lock_reason_short_notification_blocking">all notifications are blocked</string>
|
||||
<string name="lock_reason_short_battery_limit">battery limit reached</string>
|
||||
<string name="lock_reason_short_session_duration">session duration limit reached</string>
|
||||
|
||||
<string name="lock_overlay_warning">
|
||||
Failed to open the lock screen.
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
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.
|
||||
|
@ -21,4 +21,7 @@
|
|||
when there are time limits and
|
||||
the limited Apps are used
|
||||
</string>
|
||||
<string name="usage_history_time_area">from %s until %s</string>
|
||||
<string name="usage_history_item_session_duration_limit">Session duration limit of %s with %s break</string>
|
||||
<string name="usage_history_item_last_usage">Last usage: %s</string>
|
||||
</resources>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
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.
|
||||
|
@ -39,6 +39,8 @@
|
|||
<string name="util_limit_hours_and_minutes"><xliff:g id="hours">%1$s</xliff:g> and <xliff:g>%2$s</xliff:g></string>
|
||||
<string name="util_time_remaining"><xliff:g example="3 minutes" id="time">%1$s</xliff:g> remaining</string>
|
||||
<string name="util_time_done">Time over</string>
|
||||
<string name="util_time_pause_in">Break in %s</string>
|
||||
<string name="util_time_pause_shortly">Break in a few moments</string>
|
||||
|
||||
<string name="util_time_unused">Unused</string>
|
||||
<string name="util_time_used">Used <xliff:g example="4 minutes" id="used time">%1$s</xliff:g></string>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue