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.appcompat:appcompat:1.1.0'
|
||||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||||
implementation 'androidx.cardview:cardview: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 "com.google.android.material:material:1.1.0"
|
||||||
|
|
||||||
implementation "android.arch.navigation:navigation-fragment-ktx:$nav_version"
|
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
|
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.json.JSONObject
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class Actions {
|
class Actions {
|
||||||
|
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
|
@Test
|
||||||
fun decrementCategoryExtraTimeShouldBeSerializedAndParsedCorrectly() {
|
fun testActionSerializationAndDeserializationWorks() {
|
||||||
val originalAction = DecrementCategoryExtraTimeAction(categoryId = "abcdef", extraTimeToSubtract = 1000 * 30)
|
appLogicActions.forEach { originalAction ->
|
||||||
|
val serializedAction = SerializationUtil.serializeAction(originalAction)
|
||||||
|
val parsedAction = ActionParser.parseAppLogicAction(JSONObject(serializedAction))
|
||||||
|
|
||||||
val serializedAction = SerializationUtil.serializeAction(originalAction)
|
assert(parsedAction == 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 notification(): NotificationDao
|
||||||
fun allowedContact(): AllowedContactDao
|
fun allowedContact(): AllowedContactDao
|
||||||
fun userKey(): UserKeyDao
|
fun userKey(): UserKeyDao
|
||||||
|
fun sessionDuration(): SessionDurationDao
|
||||||
|
|
||||||
fun beginTransaction()
|
fun beginTransaction()
|
||||||
fun setTransactionSuccessful()
|
fun setTransactionSuccessful()
|
||||||
|
|
|
@ -17,6 +17,8 @@ package io.timelimit.android.data
|
||||||
|
|
||||||
import androidx.room.migration.Migration
|
import androidx.room.migration.Migration
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
|
import io.timelimit.android.data.model.TimeLimitRule
|
||||||
|
import io.timelimit.android.extensions.MinuteOfDay
|
||||||
|
|
||||||
object DatabaseMigrations {
|
object DatabaseMigrations {
|
||||||
val MIGRATE_TO_V2 = object: Migration(1, 2) {
|
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`)")
|
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,
|
AppActivity::class,
|
||||||
Notification::class,
|
Notification::class,
|
||||||
AllowedContact::class,
|
AllowedContact::class,
|
||||||
UserKey::class
|
UserKey::class,
|
||||||
], version = 28)
|
SessionDuration::class
|
||||||
|
], version = 29)
|
||||||
abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database {
|
abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database {
|
||||||
companion object {
|
companion object {
|
||||||
private val lock = Object()
|
private val lock = Object()
|
||||||
|
@ -98,7 +99,8 @@ abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database
|
||||||
DatabaseMigrations.MIGRATE_TO_V25,
|
DatabaseMigrations.MIGRATE_TO_V25,
|
||||||
DatabaseMigrations.MIGRATE_TO_V26,
|
DatabaseMigrations.MIGRATE_TO_V26,
|
||||||
DatabaseMigrations.MIGRATE_TO_V27,
|
DatabaseMigrations.MIGRATE_TO_V27,
|
||||||
DatabaseMigrations.MIGRATE_TO_V28
|
DatabaseMigrations.MIGRATE_TO_V28,
|
||||||
|
DatabaseMigrations.MIGRATE_TO_V29
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,12 +36,13 @@ object DatabaseBackupLowlevel {
|
||||||
private const val DEVICE = "device"
|
private const val DEVICE = "device"
|
||||||
private const val PENDING_SYNC_ACTION = "pendingSyncAction"
|
private const val PENDING_SYNC_ACTION = "pendingSyncAction"
|
||||||
private const val TIME_LIMIT_RULE = "timelimitRule"
|
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 USER = "user"
|
||||||
private const val APP_ACTIVITY = "appActivity"
|
private const val APP_ACTIVITY = "appActivity"
|
||||||
private const val NOTIFICATION = "notification"
|
private const val NOTIFICATION = "notification"
|
||||||
private const val ALLOWED_CONTACT = "allowedContact"
|
private const val ALLOWED_CONTACT = "allowedContact"
|
||||||
private const val USER_KEY = "userKey"
|
private const val USER_KEY = "userKey"
|
||||||
|
private const val SESSION_DURATION = "sessionDuration"
|
||||||
|
|
||||||
fun outputAsBackupJson(database: Database, outputStream: OutputStream) {
|
fun outputAsBackupJson(database: Database, outputStream: OutputStream) {
|
||||||
val writer = JsonWriter(OutputStreamWriter(outputStream, Charsets.UTF_8))
|
val writer = JsonWriter(OutputStreamWriter(outputStream, Charsets.UTF_8))
|
||||||
|
@ -87,6 +88,7 @@ object DatabaseBackupLowlevel {
|
||||||
handleCollection(NOTIFICATION) { offset, pageSize -> database.notification().getNotificationPageSync(offset, pageSize) }
|
handleCollection(NOTIFICATION) { offset, pageSize -> database.notification().getNotificationPageSync(offset, pageSize) }
|
||||||
handleCollection(ALLOWED_CONTACT) { offset, pageSize -> database.allowedContact().getAllowedContactPageSync(offset, pageSize) }
|
handleCollection(ALLOWED_CONTACT) { offset, pageSize -> database.allowedContact().getAllowedContactPageSync(offset, pageSize) }
|
||||||
handleCollection(USER_KEY) { offset, pageSize -> database.userKey().getUserKeyPageSync(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()
|
writer.endObject().flush()
|
||||||
}
|
}
|
||||||
|
@ -226,6 +228,15 @@ object DatabaseBackupLowlevel {
|
||||||
|
|
||||||
reader.endArray()
|
reader.endArray()
|
||||||
}
|
}
|
||||||
|
SESSION_DURATION -> {
|
||||||
|
reader.beginArray()
|
||||||
|
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
database.sessionDuration().addSessionDurationIgnoreErrorsSync(SessionDuration.parse(reader))
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.endArray()
|
||||||
|
}
|
||||||
else -> reader.skipValue()
|
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
|
package io.timelimit.android.data.dao
|
||||||
|
|
||||||
import android.util.SparseArray
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.Transformations
|
|
||||||
import androidx.paging.DataSource
|
import androidx.paging.DataSource
|
||||||
import androidx.room.Dao
|
import androidx.room.Dao
|
||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import io.timelimit.android.data.model.UsedTimeItem
|
import io.timelimit.android.data.model.UsedTimeItem
|
||||||
|
import io.timelimit.android.data.model.UsedTimeListItem
|
||||||
import io.timelimit.android.livedata.ignoreUnchanged
|
import io.timelimit.android.livedata.ignoreUnchanged
|
||||||
|
|
||||||
@Dao
|
@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")
|
@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>>
|
protected abstract fun getUsedTimesOfWeekInternal(categoryId: String, startingDayOfEpoch: Int, endDayOfEpoch: Int): LiveData<List<UsedTimeItem>>
|
||||||
|
|
||||||
fun getUsedTimesOfWeek(categoryId: String, firstDayOfWeekAsEpochDay: Int): LiveData<SparseArray<UsedTimeItem>> {
|
fun getUsedTimesOfWeek(categoryId: String, firstDayOfWeekAsEpochDay: Int): LiveData<List<UsedTimeItem>> {
|
||||||
return Transformations.map(getUsedTimesOfWeekInternal(categoryId, firstDayOfWeekAsEpochDay, firstDayOfWeekAsEpochDay + 6).ignoreUnchanged()) {
|
return getUsedTimesOfWeekInternal(categoryId, firstDayOfWeekAsEpochDay, firstDayOfWeekAsEpochDay + 6).ignoreUnchanged()
|
||||||
val result = SparseArray<UsedTimeItem>()
|
|
||||||
|
|
||||||
it.forEach {
|
|
||||||
result.put(it.dayOfEpoch - firstDayOfWeekAsEpochDay, it)
|
|
||||||
}
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Insert
|
@Insert
|
||||||
|
@ -48,14 +39,11 @@ abstract class UsedTimeDao {
|
||||||
@Insert
|
@Insert
|
||||||
abstract fun insertUsedTimes(item: List<UsedTimeItem>)
|
abstract fun insertUsedTimes(item: List<UsedTimeItem>)
|
||||||
|
|
||||||
@Query("UPDATE used_time SET used_time = :newUsedTime WHERE category_id = :categoryId AND day_of_epoch = :dayOfEpoch")
|
@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 updateUsedTime(categoryId: String, dayOfEpoch: Int, newUsedTime: Long)
|
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")
|
@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 addUsedTime(categoryId: String, dayOfEpoch: Int, timeToAdd: Int): Int
|
abstract fun getUsedTimeItemSync(categoryId: String, dayOfEpoch: Int, start: Int, end: Int): UsedTimeItem?
|
||||||
|
|
||||||
@Query("SELECT * FROM used_time WHERE category_id = :categoryId AND day_of_epoch = :dayOfEpoch")
|
|
||||||
abstract fun getUsedTimeItem(categoryId: String, dayOfEpoch: Int): LiveData<UsedTimeItem?>
|
|
||||||
|
|
||||||
@Query("DELETE FROM used_time WHERE category_id = :categoryId")
|
@Query("DELETE FROM used_time WHERE category_id = :categoryId")
|
||||||
abstract fun deleteUsedTimeItems(categoryId: String)
|
abstract fun deleteUsedTimeItems(categoryId: String)
|
||||||
|
@ -66,12 +54,13 @@ abstract class UsedTimeDao {
|
||||||
@Query("SELECT * FROM used_time LIMIT :pageSize OFFSET :offset")
|
@Query("SELECT * FROM used_time LIMIT :pageSize OFFSET :offset")
|
||||||
abstract fun getUsedTimePageSync(offset: Int, pageSize: Int): List<UsedTimeItem>
|
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")
|
@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>>
|
abstract fun getUsedTimesByDayAndCategoryIds(categoryIds: List<String>, startingDayOfEpoch: Int, endDayOfEpoch: Int): LiveData<List<UsedTimeItem>>
|
||||||
|
|
||||||
@Query("SELECT * FROM used_time")
|
@Query("SELECT * FROM used_time")
|
||||||
abstract fun getAllUsedTimeItemsSync(): List<UsedTimeItem>
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -18,6 +18,7 @@ package io.timelimit.android.data.model
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.util.JsonReader
|
import android.util.JsonReader
|
||||||
import android.util.JsonWriter
|
import android.util.JsonWriter
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
|
@ -25,6 +26,9 @@ import androidx.room.TypeConverters
|
||||||
import io.timelimit.android.data.IdGenerator
|
import io.timelimit.android.data.IdGenerator
|
||||||
import io.timelimit.android.data.JsonSerializable
|
import io.timelimit.android.data.JsonSerializable
|
||||||
import io.timelimit.android.data.customtypes.ImmutableBitmaskAdapter
|
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
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
|
||||||
@Entity(tableName = "time_limit_rule")
|
@Entity(tableName = "time_limit_rule")
|
||||||
|
@ -41,7 +45,15 @@ data class TimeLimitRule(
|
||||||
@ColumnInfo(name = "day_mask")
|
@ColumnInfo(name = "day_mask")
|
||||||
val dayMask: Byte,
|
val dayMask: Byte,
|
||||||
@ColumnInfo(name = "max_time")
|
@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 {
|
): Parcelable, JsonSerializable {
|
||||||
companion object {
|
companion object {
|
||||||
private const val RULE_ID = "ruleId"
|
private const val RULE_ID = "ruleId"
|
||||||
|
@ -49,6 +61,13 @@ data class TimeLimitRule(
|
||||||
private const val MAX_TIME_IN_MILLIS = "time"
|
private const val MAX_TIME_IN_MILLIS = "time"
|
||||||
private const val DAY_MASK = "days"
|
private const val DAY_MASK = "days"
|
||||||
private const val APPLY_TO_EXTRA_TIME_USAGE = "extraTime"
|
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 {
|
fun parse(reader: JsonReader): TimeLimitRule {
|
||||||
var id: String? = null
|
var id: String? = null
|
||||||
|
@ -56,6 +75,10 @@ data class TimeLimitRule(
|
||||||
var applyToExtraTimeUsage: Boolean? = null
|
var applyToExtraTimeUsage: Boolean? = null
|
||||||
var dayMask: Byte? = null
|
var dayMask: Byte? = null
|
||||||
var maximumTimeInMillis: Int? = null
|
var maximumTimeInMillis: Int? = null
|
||||||
|
var startMinuteOfDay = MIN_START_MINUTE
|
||||||
|
var endMinuteOfDay = MAX_END_MINUTE
|
||||||
|
var sessionDurationMilliseconds = 0
|
||||||
|
var sessionPauseMilliseconds = 0
|
||||||
|
|
||||||
reader.beginObject()
|
reader.beginObject()
|
||||||
|
|
||||||
|
@ -66,6 +89,10 @@ data class TimeLimitRule(
|
||||||
MAX_TIME_IN_MILLIS -> maximumTimeInMillis = reader.nextInt()
|
MAX_TIME_IN_MILLIS -> maximumTimeInMillis = reader.nextInt()
|
||||||
DAY_MASK -> dayMask = reader.nextInt().toByte()
|
DAY_MASK -> dayMask = reader.nextInt().toByte()
|
||||||
APPLY_TO_EXTRA_TIME_USAGE -> applyToExtraTimeUsage = reader.nextBoolean()
|
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()
|
else -> reader.skipValue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,7 +104,11 @@ data class TimeLimitRule(
|
||||||
categoryId = categoryId!!,
|
categoryId = categoryId!!,
|
||||||
applyToExtraTimeUsage = applyToExtraTimeUsage!!,
|
applyToExtraTimeUsage = applyToExtraTimeUsage!!,
|
||||||
dayMask = dayMask!!,
|
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)) {
|
if (dayMask < 0 || dayMask > (1 or 2 or 4 or 8 or 16 or 32 or 64)) {
|
||||||
throw IllegalArgumentException()
|
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) {
|
override fun serialize(writer: JsonWriter) {
|
||||||
writer.beginObject()
|
writer.beginObject()
|
||||||
|
|
||||||
|
@ -103,7 +148,28 @@ data class TimeLimitRule(
|
||||||
writer.name(MAX_TIME_IN_MILLIS).value(maximumTimeInMillis)
|
writer.name(MAX_TIME_IN_MILLIS).value(maximumTimeInMillis)
|
||||||
writer.name(DAY_MASK).value(dayMask)
|
writer.name(DAY_MASK).value(dayMask)
|
||||||
writer.name(APPLY_TO_EXTRA_TIME_USAGE).value(applyToExtraTimeUsage)
|
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()
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -21,20 +21,27 @@ import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import io.timelimit.android.data.IdGenerator
|
import io.timelimit.android.data.IdGenerator
|
||||||
import io.timelimit.android.data.JsonSerializable
|
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(
|
data class UsedTimeItem(
|
||||||
@ColumnInfo(name = "day_of_epoch")
|
@ColumnInfo(name = "day_of_epoch")
|
||||||
val dayOfEpoch: Int,
|
val dayOfEpoch: Int,
|
||||||
@ColumnInfo(name = "used_time")
|
@ColumnInfo(name = "used_time")
|
||||||
val usedMillis: Long,
|
val usedMillis: Long,
|
||||||
@ColumnInfo(name = "category_id")
|
@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 {
|
): JsonSerializable {
|
||||||
companion object {
|
companion object {
|
||||||
private const val DAY_OF_EPOCH = "day"
|
private const val DAY_OF_EPOCH = "day"
|
||||||
private const val USED_TIME_MILLIS = "time"
|
private const val USED_TIME_MILLIS = "time"
|
||||||
private const val CATEGORY_ID = "category"
|
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 {
|
fun parse(reader: JsonReader): UsedTimeItem {
|
||||||
reader.beginObject()
|
reader.beginObject()
|
||||||
|
@ -42,12 +49,16 @@ data class UsedTimeItem(
|
||||||
var dayOfEpoch: Int? = null
|
var dayOfEpoch: Int? = null
|
||||||
var usedMillis: Long? = null
|
var usedMillis: Long? = null
|
||||||
var categoryId: String? = null
|
var categoryId: String? = null
|
||||||
|
var startTimeOfDay = MinuteOfDay.MIN
|
||||||
|
var endTimeOfDay = MinuteOfDay.MAX
|
||||||
|
|
||||||
while (reader.hasNext()) {
|
while (reader.hasNext()) {
|
||||||
when (reader.nextName()) {
|
when (reader.nextName()) {
|
||||||
DAY_OF_EPOCH -> dayOfEpoch = reader.nextInt()
|
DAY_OF_EPOCH -> dayOfEpoch = reader.nextInt()
|
||||||
USED_TIME_MILLIS -> usedMillis = reader.nextLong()
|
USED_TIME_MILLIS -> usedMillis = reader.nextLong()
|
||||||
CATEGORY_ID -> categoryId = reader.nextString()
|
CATEGORY_ID -> categoryId = reader.nextString()
|
||||||
|
START_TIME_OF_DAY -> startTimeOfDay = reader.nextInt()
|
||||||
|
END_TIME_OF_DAY -> endTimeOfDay = reader.nextInt()
|
||||||
else -> reader.skipValue()
|
else -> reader.skipValue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,7 +68,9 @@ data class UsedTimeItem(
|
||||||
return UsedTimeItem(
|
return UsedTimeItem(
|
||||||
dayOfEpoch = dayOfEpoch!!,
|
dayOfEpoch = dayOfEpoch!!,
|
||||||
usedMillis = usedMillis!!,
|
usedMillis = usedMillis!!,
|
||||||
categoryId = categoryId!!
|
categoryId = categoryId!!,
|
||||||
|
startTimeOfDay = startTimeOfDay,
|
||||||
|
endTimeOfDay = endTimeOfDay
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -72,6 +85,10 @@ data class UsedTimeItem(
|
||||||
if (usedMillis < 0) {
|
if (usedMillis < 0) {
|
||||||
throw IllegalArgumentException()
|
throw IllegalArgumentException()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (startTimeOfDay < MinuteOfDay.MIN || endTimeOfDay > MinuteOfDay.MAX || startTimeOfDay > endTimeOfDay) {
|
||||||
|
throw IllegalArgumentException()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun serialize(writer: JsonWriter) {
|
override fun serialize(writer: JsonWriter) {
|
||||||
|
@ -80,6 +97,8 @@ data class UsedTimeItem(
|
||||||
writer.name(DAY_OF_EPOCH).value(dayOfEpoch)
|
writer.name(DAY_OF_EPOCH).value(dayOfEpoch)
|
||||||
writer.name(USED_TIME_MILLIS).value(usedMillis)
|
writer.name(USED_TIME_MILLIS).value(usedMillis)
|
||||||
writer.name(CATEGORY_ID).value(categoryId)
|
writer.name(CATEGORY_ID).value(categoryId)
|
||||||
|
writer.name(START_TIME_OF_DAY).value(startTimeOfDay)
|
||||||
|
writer.name(END_TIME_OF_DAY).value(endTimeOfDay)
|
||||||
|
|
||||||
writer.endObject()
|
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.RequiresCurrentDevice -> getString(R.string.lock_reason_short_requires_current_device)
|
||||||
BlockingReason.NotificationsAreBlocked -> getString(R.string.lock_reason_short_notification_blocking)
|
BlockingReason.NotificationsAreBlocked -> getString(R.string.lock_reason_short_notification_blocking)
|
||||||
BlockingReason.BatteryLimit -> getString(R.string.lock_reason_short_battery_limit)
|
BlockingReason.BatteryLimit -> getString(R.string.lock_reason_short_battery_limit)
|
||||||
|
BlockingReason.SessionDurationLimit -> getString(R.string.lock_reason_short_session_duration)
|
||||||
BlockingReason.None -> throw IllegalStateException()
|
BlockingReason.None -> throw IllegalStateException()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -16,8 +16,6 @@
|
||||||
package io.timelimit.android.logic
|
package io.timelimit.android.logic
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.SparseArray
|
|
||||||
import android.util.SparseLongArray
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import io.timelimit.android.BuildConfig
|
import io.timelimit.android.BuildConfig
|
||||||
import io.timelimit.android.R
|
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.data.model.*
|
||||||
import io.timelimit.android.date.DateInTimezone
|
import io.timelimit.android.date.DateInTimezone
|
||||||
import io.timelimit.android.date.getMinuteOfWeek
|
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.AppStatusMessage
|
||||||
import io.timelimit.android.integration.platform.ForegroundAppSpec
|
import io.timelimit.android.integration.platform.ForegroundAppSpec
|
||||||
import io.timelimit.android.integration.platform.ProtectionLevel
|
import io.timelimit.android.integration.platform.ProtectionLevel
|
||||||
import io.timelimit.android.integration.platform.android.AccessibilityService
|
import io.timelimit.android.integration.platform.android.AccessibilityService
|
||||||
import io.timelimit.android.livedata.*
|
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.UpdateDeviceStatusAction
|
||||||
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
||||||
import io.timelimit.android.ui.IsAppInForeground
|
import io.timelimit.android.ui.IsAppInForeground
|
||||||
|
@ -235,7 +236,8 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
|
|
||||||
fun deleteOldUsedTimes() = UsedTimeDeleter.deleteOldUsedTimeItems(
|
fun deleteOldUsedTimes() = UsedTimeDeleter.deleteOldUsedTimeItems(
|
||||||
database = appLogic.database,
|
database = appLogic.database,
|
||||||
date = nowDate
|
date = nowDate,
|
||||||
|
timestamp = nowTimestamp
|
||||||
)
|
)
|
||||||
|
|
||||||
if (realTime.isNetworkTime) {
|
if (realTime.isNetworkTime) {
|
||||||
|
@ -315,17 +317,28 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
val usedTimeUpdateHelper = usedTimeUpdateHelper!!
|
val usedTimeUpdateHelper = usedTimeUpdateHelper!!
|
||||||
|
|
||||||
// check times
|
// check times
|
||||||
fun buildUsedTimesSparseArray(items: SparseArray<UsedTimeItem>, categoryId: String): SparseLongArray {
|
fun buildDummyUsedTimeItems(categoryId: String): List<UsedTimeItem> {
|
||||||
val result = SparseLongArray()
|
if (!usedTimeUpdateHelper.timeToAdd.containsKey(categoryId)) {
|
||||||
|
return emptyList()
|
||||||
for (i in 0..6) {
|
|
||||||
val usedTimesItem = items[i]?.usedMillis ?: 0
|
|
||||||
val timeToAddButNotCommited = usedTimeUpdateHelper.timeToAdd[categoryId] ?: 0
|
|
||||||
|
|
||||||
result.put(i, usedTimesItem + timeToAddButNotCommited)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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? {
|
suspend fun getRemainingTime(categoryId: String?): RemainingTime? {
|
||||||
|
@ -338,31 +351,66 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
return null
|
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(
|
return RemainingTime.getRemainingTime(
|
||||||
nowDate.dayOfWeek,
|
nowDate.dayOfWeek,
|
||||||
buildUsedTimesSparseArray(usedTimes, categoryId),
|
minuteOfWeek % MinuteOfDay.LENGTH,
|
||||||
|
usedTimes + buildDummyUsedTimeItems(categoryId),
|
||||||
rules,
|
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
|
// 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 remainingTimeForegroundAppChild = if (foregroundAppHandling.status == BackgroundTaskLogicAppStatus.AllowedCountAndCheckTime) getRemainingTime(foregroundAppHandling.categoryId) else null
|
||||||
val remainingTimeForegroundAppParent = if (foregroundAppHandling.status == BackgroundTaskLogicAppStatus.AllowedCountAndCheckTime) getRemainingTime(foregroundAppHandling.parentCategoryId) else null
|
val remainingTimeForegroundAppParent = if (foregroundAppHandling.status == BackgroundTaskLogicAppStatus.AllowedCountAndCheckTime) getRemainingTime(foregroundAppHandling.parentCategoryId) else null
|
||||||
val remainingTimeForegroundApp = RemainingTime.min(remainingTimeForegroundAppChild, remainingTimeForegroundAppParent)
|
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 remainingTimeBackgroundAppChild = if (audioPlaybackHandling.status == BackgroundTaskLogicAppStatus.AllowedCountAndCheckTime) getRemainingTime(audioPlaybackHandling.categoryId) else null
|
||||||
val remainingTimeBackgroundAppParent = if (audioPlaybackHandling.status == BackgroundTaskLogicAppStatus.AllowedCountAndCheckTime) getRemainingTime(audioPlaybackHandling.parentCategoryId) else null
|
val remainingTimeBackgroundAppParent = if (audioPlaybackHandling.status == BackgroundTaskLogicAppStatus.AllowedCountAndCheckTime) getRemainingTime(audioPlaybackHandling.parentCategoryId) else null
|
||||||
val remainingTimeBackgroundApp = RemainingTime.min(remainingTimeBackgroundAppChild, remainingTimeBackgroundAppParent)
|
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
|
// eventually block
|
||||||
if (remainingTimeForegroundApp?.hasRemainingTime == false) {
|
if (remainingTimeForegroundApp?.hasRemainingTime == false || sessionDurationLimitReachedForegroundApp) {
|
||||||
foregroundAppHandling.status = BackgroundTaskLogicAppStatus.ShouldBlock
|
foregroundAppHandling.status = BackgroundTaskLogicAppStatus.ShouldBlock
|
||||||
}
|
}
|
||||||
|
|
||||||
if (remainingTimeBackgroundApp?.hasRemainingTime == false) {
|
if (remainingTimeBackgroundApp?.hasRemainingTime == false || sessionDurationLimitReachedBackgroundApp) {
|
||||||
audioPlaybackHandling.status = BackgroundTaskLogicAppStatus.ShouldBlock
|
audioPlaybackHandling.status = BackgroundTaskLogicAppStatus.ShouldBlock
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -375,6 +423,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
|
|
||||||
val categoriesToCount = mutableSetOf<String>()
|
val categoriesToCount = mutableSetOf<String>()
|
||||||
val categoriesToCountExtraTime = mutableSetOf<String>()
|
val categoriesToCountExtraTime = mutableSetOf<String>()
|
||||||
|
val categoriesToCountSessionDurations = mutableSetOf<String>()
|
||||||
|
|
||||||
if (shouldCountForegroundApp) {
|
if (shouldCountForegroundApp) {
|
||||||
remainingTimeForegroundAppChild?.let { remainingTime ->
|
remainingTimeForegroundAppChild?.let { remainingTime ->
|
||||||
|
@ -384,6 +433,10 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
if (remainingTime.usingExtraTime) {
|
if (remainingTime.usingExtraTime) {
|
||||||
categoriesToCountExtraTime.add(categoryId)
|
categoriesToCountExtraTime.add(categoryId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!sessionDurationLimitReachedForegroundApp) {
|
||||||
|
categoriesToCountSessionDurations.add(categoryId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -394,6 +447,10 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
if (remainingTime.usingExtraTime) {
|
if (remainingTime.usingExtraTime) {
|
||||||
categoriesToCountExtraTime.add(it)
|
categoriesToCountExtraTime.add(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!sessionDurationLimitReachedForegroundApp) {
|
||||||
|
categoriesToCountSessionDurations.add(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -406,6 +463,10 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
if (remainingTime.usingExtraTime) {
|
if (remainingTime.usingExtraTime) {
|
||||||
categoriesToCountExtraTime.add(it)
|
categoriesToCountExtraTime.add(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!sessionDurationLimitReachedBackgroundApp) {
|
||||||
|
categoriesToCountSessionDurations.add(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -416,16 +477,61 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
if (remainingTime.usingExtraTime) {
|
if (remainingTime.usingExtraTime) {
|
||||||
categoriesToCountExtraTime.add(it)
|
categoriesToCountExtraTime.add(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!sessionDurationLimitReachedBackgroundApp) {
|
||||||
|
categoriesToCountSessionDurations.add(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (categoriesToCount.isNotEmpty()) {
|
if (categoriesToCount.isNotEmpty()) {
|
||||||
categoriesToCount.forEach { categoryId ->
|
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(
|
usedTimeUpdateHelper.add(
|
||||||
categoryId = categoryId,
|
categoryId = categoryId,
|
||||||
time = timeToSubtract,
|
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(
|
fun buildStatusMessage(
|
||||||
handling: BackgroundTaskRestrictionLogicResult,
|
handling: BackgroundTaskRestrictionLogicResult,
|
||||||
remainingTime: RemainingTime?,
|
remainingTime: RemainingTime?,
|
||||||
|
remainingSessionDuration: Long?,
|
||||||
suffix: String,
|
suffix: String,
|
||||||
appPackageName: String?,
|
appPackageName: String?,
|
||||||
appActivityToShow: String?
|
appActivityToShow: String?
|
||||||
|
@ -518,27 +625,18 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
appPackageName = appPackageName,
|
appPackageName = appPackageName,
|
||||||
appActivityToShow = appActivityToShow
|
appActivityToShow = appActivityToShow
|
||||||
)
|
)
|
||||||
BackgroundTaskLogicAppStatus.AllowedCountAndCheckTime -> (
|
BackgroundTaskLogicAppStatus.AllowedCountAndCheckTime -> buildStatusMessageWithCurrentAppTitle(
|
||||||
if (remainingTime?.usingExtraTime == true) {
|
text = if (remainingTime?.usingExtraTime == true)
|
||||||
// using extra time
|
appLogic.context.getString(R.string.background_logic_using_extra_time, TimeTextUtil.remaining(remainingTime.includingExtraTime.toInt(), appLogic.context))
|
||||||
buildStatusMessageWithCurrentAppTitle(
|
else if (remainingTime != null && remainingSessionDuration != null && remainingSessionDuration < remainingTime.default)
|
||||||
text = appLogic.context.getString(R.string.background_logic_using_extra_time, TimeTextUtil.remaining(remainingTime.includingExtraTime.toInt(), appLogic.context)),
|
TimeTextUtil.pauseIn(remainingSessionDuration.toInt(), appLogic.context)
|
||||||
titlePrefix = getCategoryTitle(handling.categoryId) + " - ",
|
else
|
||||||
titleSuffix = suffix,
|
TimeTextUtil.remaining(remainingTime?.default?.toInt() ?: 0, appLogic.context),
|
||||||
appPackageName = appPackageName,
|
titlePrefix = getCategoryTitle(handling.categoryId) + " - ",
|
||||||
appActivityToShow = appActivityToShow
|
titleSuffix = suffix,
|
||||||
)
|
appPackageName = appPackageName,
|
||||||
} else {
|
appActivityToShow = appActivityToShow
|
||||||
// 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(
|
BackgroundTaskLogicAppStatus.Idle -> AppStatusMessage(
|
||||||
appLogic.context.getString(R.string.background_logic_idle_title) + suffix,
|
appLogic.context.getString(R.string.background_logic_idle_title) + suffix,
|
||||||
appLogic.context.getString(R.string.background_logic_idle_text)
|
appLogic.context.getString(R.string.background_logic_idle_text)
|
||||||
|
@ -557,7 +655,8 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
remainingTime = remainingTimeBackgroundApp,
|
remainingTime = remainingTimeBackgroundApp,
|
||||||
suffix = " (2/2)",
|
suffix = " (2/2)",
|
||||||
appPackageName = audioPlaybackPackageName,
|
appPackageName = audioPlaybackPackageName,
|
||||||
appActivityToShow = null
|
appActivityToShow = null,
|
||||||
|
remainingSessionDuration = remainingSessionDurationBackgroundApp
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
@ -568,7 +667,8 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
remainingTime = remainingTimeForegroundApp,
|
remainingTime = remainingTimeForegroundApp,
|
||||||
suffix = if (showBackgroundStatus) " (1/2)" else "",
|
suffix = if (showBackgroundStatus) " (1/2)" else "",
|
||||||
appPackageName = foregroundAppPackageName,
|
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
|
package io.timelimit.android.logic
|
||||||
|
|
||||||
import android.util.SparseArray
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import io.timelimit.android.data.model.Category
|
import io.timelimit.android.data.model.*
|
||||||
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.livedata.*
|
import io.timelimit.android.livedata.*
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
@ -49,11 +45,16 @@ class BackgroundTaskLogicCache (private val appLogic: AppLogic) {
|
||||||
return appLogic.database.timeLimitRules().getTimeLimitRulesByCategory(key)
|
return appLogic.database.timeLimitRules().getTimeLimitRulesByCategory(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val usedTimesOfCategoryAndWeekByFirstDayOfWeek = object: MultiKeyLiveDataCache<SparseArray<UsedTimeItem>, Pair<String, Int>>() {
|
val usedTimesOfCategoryAndWeekByFirstDayOfWeek = object: MultiKeyLiveDataCache<List<UsedTimeItem>, Pair<String, Int>>() {
|
||||||
override fun createValue(key: Pair<String, Int>): LiveData<SparseArray<UsedTimeItem>> {
|
override fun createValue(key: Pair<String, Int>): LiveData<List<UsedTimeItem>> {
|
||||||
return appLogic.database.usedTimes().getUsedTimesOfWeek(key.first, key.second)
|
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 shouldDoAutomaticSignOut = SingleItemLiveDataCacheWithRequery { -> appLogic.defaultUserLogic.hasAutomaticSignOut()}
|
||||||
|
|
||||||
val liveDataCaches = LiveDataCaches(arrayOf(
|
val liveDataCaches = LiveDataCaches(arrayOf(
|
||||||
|
|
|
@ -16,15 +16,14 @@
|
||||||
package io.timelimit.android.logic
|
package io.timelimit.android.logic
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.SparseLongArray
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.Transformations
|
import androidx.lifecycle.Transformations
|
||||||
import io.timelimit.android.BuildConfig
|
import io.timelimit.android.BuildConfig
|
||||||
import io.timelimit.android.data.model.*
|
import io.timelimit.android.data.model.*
|
||||||
import io.timelimit.android.date.DateInTimezone
|
import io.timelimit.android.date.DateInTimezone
|
||||||
import io.timelimit.android.date.getMinuteOfWeek
|
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.platform.android.AndroidIntegrationApps
|
||||||
import io.timelimit.android.integration.time.TimeApi
|
|
||||||
import io.timelimit.android.livedata.*
|
import io.timelimit.android.livedata.*
|
||||||
import io.timelimit.android.logic.extension.isCategoryAllowed
|
import io.timelimit.android.logic.extension.isCategoryAllowed
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
@ -39,7 +38,8 @@ enum class BlockingReason {
|
||||||
MissingNetworkTime,
|
MissingNetworkTime,
|
||||||
RequiresCurrentDevice,
|
RequiresCurrentDevice,
|
||||||
NotificationsAreBlocked,
|
NotificationsAreBlocked,
|
||||||
BatteryLimit
|
BatteryLimit,
|
||||||
|
SessionDurationLimit
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class BlockingLevel {
|
enum class BlockingLevel {
|
||||||
|
@ -319,18 +319,18 @@ class BlockingReasonUtil(private val appLogic: AppLogic) {
|
||||||
trustedMinuteOfWeek ->
|
trustedMinuteOfWeek ->
|
||||||
|
|
||||||
if (category.blockedMinutesInWeek.dataNotToModify.isEmpty) {
|
if (category.blockedMinutesInWeek.dataNotToModify.isEmpty) {
|
||||||
getBlockingReasonStep6(category, timeZone)
|
getBlockingReasonStep6(category, timeZone, trustedMinuteOfWeek)
|
||||||
} else if (trustedMinuteOfWeek == null) {
|
} else if (trustedMinuteOfWeek == null) {
|
||||||
liveDataFromValue(BlockingReason.MissingNetworkTime)
|
liveDataFromValue(BlockingReason.MissingNetworkTime)
|
||||||
} else if (category.blockedMinutesInWeek.read(trustedMinuteOfWeek)) {
|
} else if (category.blockedMinutesInWeek.read(trustedMinuteOfWeek)) {
|
||||||
liveDataFromValue(BlockingReason.BlockedAtThisTime)
|
liveDataFromValue(BlockingReason.BlockedAtThisTime)
|
||||||
} else {
|
} 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) {
|
if (BuildConfig.DEBUG) {
|
||||||
Log.d(LOG_TAG, "step 6")
|
Log.d(LOG_TAG, "step 6")
|
||||||
}
|
}
|
||||||
|
@ -343,54 +343,67 @@ class BlockingReasonUtil(private val appLogic: AppLogic) {
|
||||||
|
|
||||||
if (rules.isEmpty()) {
|
if (rules.isEmpty()) {
|
||||||
liveDataFromValue(BlockingReason.None)
|
liveDataFromValue(BlockingReason.None)
|
||||||
} else if (nowTrustedDate == null) {
|
} else if (nowTrustedDate == null || trustedMinuteOfWeek == null) {
|
||||||
liveDataFromValue(BlockingReason.MissingNetworkTime)
|
liveDataFromValue(BlockingReason.MissingNetworkTime)
|
||||||
} else {
|
} 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) {
|
if (BuildConfig.DEBUG) {
|
||||||
Log.d(LOG_TAG, "step 6 - 2")
|
Log.d(LOG_TAG, "step 6 - 2")
|
||||||
}
|
}
|
||||||
|
|
||||||
return appLogic.currentDeviceLogic.isThisDeviceTheCurrentDevice.switchMap { isCurrentDevice ->
|
return appLogic.currentDeviceLogic.isThisDeviceTheCurrentDevice.switchMap { isCurrentDevice ->
|
||||||
if (isCurrentDevice) {
|
if (isCurrentDevice) {
|
||||||
getBlockingReasonStep7(category, nowTrustedDate, rules)
|
getBlockingReasonStep7(category, nowTrustedDate, trustedMinuteOfWeek, rules)
|
||||||
} else {
|
} else {
|
||||||
liveDataFromValue(BlockingReason.RequiresCurrentDevice)
|
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) {
|
if (BuildConfig.DEBUG) {
|
||||||
Log.d(LOG_TAG, "step 7")
|
Log.d(LOG_TAG, "step 7")
|
||||||
}
|
}
|
||||||
|
|
||||||
val extraTime = category.getExtraTime(dayOfEpoch = nowTrustedDate.dayOfEpoch)
|
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 {
|
return appLogic.database.usedTimes().getUsedTimesOfWeek(category.id, firstDayOfWeekAsEpochDay = firstDayOfWeekAsEpochDay).switchMap { usedTimes ->
|
||||||
usedTimes ->
|
val remaining = RemainingTime.getRemainingTime(nowTrustedDate.dayOfWeek, trustedMinuteOfWeek % MinuteOfDay.LENGTH, usedTimes, rules, extraTime, firstDayOfWeekAsEpochDay)
|
||||||
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)
|
|
||||||
|
|
||||||
if (remaining == null || remaining.includingExtraTime > 0) {
|
if (remaining == null || remaining.includingExtraTime > 0) {
|
||||||
BlockingReason.None
|
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 {
|
||||||
|
BlockingReason.SessionDurationLimit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (extraTime > 0) {
|
if (extraTime > 0) {
|
||||||
BlockingReason.TimeOverExtraTimeCanBeUsedLater
|
liveDataFromValue(BlockingReason.TimeOverExtraTimeCanBeUsedLater)
|
||||||
} else {
|
} else {
|
||||||
BlockingReason.TimeOver
|
liveDataFromValue(BlockingReason.TimeOver)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,12 +16,12 @@
|
||||||
package io.timelimit.android.logic
|
package io.timelimit.android.logic
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.SparseLongArray
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MediatorLiveData
|
import androidx.lifecycle.MediatorLiveData
|
||||||
import io.timelimit.android.BuildConfig
|
import io.timelimit.android.BuildConfig
|
||||||
import io.timelimit.android.data.model.*
|
import io.timelimit.android.data.model.*
|
||||||
import io.timelimit.android.date.DateInTimezone
|
import io.timelimit.android.date.DateInTimezone
|
||||||
|
import io.timelimit.android.extensions.MinuteOfDay
|
||||||
import io.timelimit.android.livedata.*
|
import io.timelimit.android.livedata.*
|
||||||
import io.timelimit.android.logic.extension.isCategoryAllowed
|
import io.timelimit.android.logic.extension.isCategoryAllowed
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
@ -146,7 +146,8 @@ class CategoriesBlockingReasonUtil(private val appLogic: AppLogic) {
|
||||||
checkCategoryTimeLimitRules(
|
checkCategoryTimeLimitRules(
|
||||||
temporarilyTrustedDate = temporarilyTrustedDate,
|
temporarilyTrustedDate = temporarilyTrustedDate,
|
||||||
category = category,
|
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(
|
private fun checkCategoryTimeLimitRules(
|
||||||
temporarilyTrustedDate: LiveData<DateInTimezone?>,
|
temporarilyTrustedDate: LiveData<DateInTimezone?>,
|
||||||
|
temporarilyTrustedMinuteOfWeek: LiveData<Int?>,
|
||||||
rules: LiveData<List<TimeLimitRule>>,
|
rules: LiveData<List<TimeLimitRule>>,
|
||||||
category: Category
|
category: Category
|
||||||
): LiveData<BlockingReason> = rules.switchMap { rules ->
|
): LiveData<BlockingReason> = rules.switchMap { rules ->
|
||||||
|
@ -218,43 +220,60 @@ class CategoriesBlockingReasonUtil(private val appLogic: AppLogic) {
|
||||||
liveDataFromValue(BlockingReason.None)
|
liveDataFromValue(BlockingReason.None)
|
||||||
} else {
|
} else {
|
||||||
temporarilyTrustedDate.switchMap { temporarilyTrustedDate ->
|
temporarilyTrustedDate.switchMap { temporarilyTrustedDate ->
|
||||||
if (temporarilyTrustedDate == null) {
|
temporarilyTrustedMinuteOfWeek.switchMap { temporarilyTrustedMinuteOfWeek ->
|
||||||
liveDataFromValue(BlockingReason.MissingNetworkTime)
|
if (temporarilyTrustedDate == null || temporarilyTrustedMinuteOfWeek == null) {
|
||||||
} else {
|
liveDataFromValue(BlockingReason.MissingNetworkTime)
|
||||||
getBlockingReasonStep7(
|
} else {
|
||||||
category = category,
|
getBlockingReasonStep7(
|
||||||
nowTrustedDate = temporarilyTrustedDate,
|
category = category,
|
||||||
rules = rules
|
nowTrustedDate = temporarilyTrustedDate,
|
||||||
)
|
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) {
|
if (BuildConfig.DEBUG) {
|
||||||
Log.d(LOG_TAG, "step 7")
|
Log.d(LOG_TAG, "step 7")
|
||||||
}
|
}
|
||||||
|
|
||||||
val extraTime = category.getExtraTime(dayOfEpoch = nowTrustedDate.dayOfEpoch)
|
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 ->
|
return appLogic.database.usedTimes().getUsedTimesOfWeek(category.id, firstDayOfWeekAsEpochDay).switchMap { usedTimes ->
|
||||||
val usedTimesSparseArray = SparseLongArray()
|
val remaining = RemainingTime.getRemainingTime(nowTrustedDate.dayOfWeek, trustedMinuteOfWeek % MinuteOfDay.LENGTH, usedTimes, rules, extraTime, firstDayOfWeekAsEpochDay)
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
if (remaining == null || remaining.includingExtraTime > 0) {
|
if (remaining == null || remaining.includingExtraTime > 0) {
|
||||||
BlockingReason.None
|
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 {
|
||||||
|
BlockingReason.SessionDurationLimit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (extraTime > 0) {
|
if (extraTime > 0) {
|
||||||
BlockingReason.TimeOverExtraTimeCanBeUsedLater
|
liveDataFromValue(BlockingReason.TimeOverExtraTimeCanBeUsedLater)
|
||||||
} else {
|
} else {
|
||||||
BlockingReason.TimeOver
|
liveDataFromValue(BlockingReason.TimeOver)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.ignoreUnchanged()
|
}.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
|
package io.timelimit.android.logic
|
||||||
|
|
||||||
import android.util.SparseLongArray
|
|
||||||
import io.timelimit.android.data.model.TimeLimitRule
|
import io.timelimit.android.data.model.TimeLimitRule
|
||||||
|
import io.timelimit.android.data.model.UsedTimeItem
|
||||||
|
|
||||||
data class RemainingTime(val includingExtraTime: Long, val default: Long) {
|
data class RemainingTime(val includingExtraTime: Long, val default: Long) {
|
||||||
val hasRemainingTime = includingExtraTime > 0
|
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> {
|
private fun getRulesRelatedToDay(dayOfWeek: Int, minuteOfDay: Int, rules: List<TimeLimitRule>): List<TimeLimitRule> {
|
||||||
return rules.filter { (it.dayMask.toInt() and (1 shl dayOfWeek)) != 0 }
|
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) {
|
if (extraTime < 0) {
|
||||||
throw IllegalStateException("extra time < 0")
|
throw IllegalStateException("extra time < 0")
|
||||||
}
|
}
|
||||||
|
|
||||||
val relatedRules = getRulesRelatedToDay(dayOfWeek, rules)
|
val relatedRules = getRulesRelatedToDay(dayOfWeek, minuteOfDay, rules)
|
||||||
val withoutExtraTime = getRemainingTime(usedTimes, relatedRules, false)
|
val withoutExtraTime = getRemainingTime(usedTimes, relatedRules, false, firstDayOfWeekAsEpochDay)
|
||||||
val withExtraTime = getRemainingTime(usedTimes, relatedRules, true)
|
val withExtraTime = getRemainingTime(usedTimes, relatedRules, true, firstDayOfWeekAsEpochDay)
|
||||||
|
|
||||||
if (withoutExtraTime == null && withExtraTime == null) {
|
if (withoutExtraTime == null && withExtraTime == null) {
|
||||||
// no rules
|
// 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? {
|
private fun getRemainingTime(usedTimes: List<UsedTimeItem>, relatedRules: List<TimeLimitRule>, assumeMaximalExtraTime: Boolean, firstDayOfWeekAsEpochDay: Int): Long? {
|
||||||
return relatedRules.filter { (!assumeMaximalExtraTime) || it.applyToExtraTimeUsage }.map {
|
return relatedRules.filter { (!assumeMaximalExtraTime) || it.applyToExtraTimeUsage }.map { rule ->
|
||||||
var usedTime = 0L
|
var usedTime = 0L
|
||||||
|
|
||||||
for (day in 0..6) {
|
usedTimes.forEach { usedTimeItem ->
|
||||||
if ((it.dayMask.toInt() and (1 shl day)) != 0) {
|
if (usedTimeItem.dayOfEpoch >= firstDayOfWeekAsEpochDay && usedTimeItem.dayOfEpoch <= firstDayOfWeekAsEpochDay + 6) {
|
||||||
usedTime += usedTimes[day]
|
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)
|
val remaining = Math.max(0, maxTime - usedTime)
|
||||||
|
|
||||||
remaining
|
remaining
|
||||||
|
|
|
@ -21,7 +21,7 @@ import io.timelimit.android.data.transaction
|
||||||
import io.timelimit.android.date.DateInTimezone
|
import io.timelimit.android.date.DateInTimezone
|
||||||
|
|
||||||
object UsedTimeDeleter {
|
object UsedTimeDeleter {
|
||||||
fun deleteOldUsedTimeItems(database: Database, date: DateInTimezone) {
|
fun deleteOldUsedTimeItems(database: Database, date: DateInTimezone, timestamp: Long) {
|
||||||
Threads.database.execute {
|
Threads.database.execute {
|
||||||
database.transaction().use {
|
database.transaction().use {
|
||||||
if (database.config().getDeviceAuthTokenSync().isNotEmpty()) {
|
if (database.config().getDeviceAuthTokenSync().isNotEmpty()) {
|
||||||
|
@ -38,6 +38,8 @@ object UsedTimeDeleter {
|
||||||
|
|
||||||
database.usedTimes().deleteOldUsedTimeItems(lastDayToKeep = date.dayOfEpoch - date.dayOfWeek)
|
database.usedTimes().deleteOldUsedTimeItems(lastDayToKeep = date.dayOfEpoch - date.dayOfWeek)
|
||||||
|
|
||||||
|
database.sessionDuration().deleteOldSessionDurationItemsSync(trustedTimestamp = timestamp)
|
||||||
|
|
||||||
it.setSuccess()
|
it.setSuccess()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,8 @@ import android.util.Log
|
||||||
import io.timelimit.android.BuildConfig
|
import io.timelimit.android.BuildConfig
|
||||||
import io.timelimit.android.date.DateInTimezone
|
import io.timelimit.android.date.DateInTimezone
|
||||||
import io.timelimit.android.sync.actions.AddUsedTimeActionItem
|
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.AddUsedTimeActionVersion2
|
||||||
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
||||||
import io.timelimit.android.sync.actions.dispatch.CategoryNotFoundException
|
import io.timelimit.android.sync.actions.dispatch.CategoryNotFoundException
|
||||||
|
@ -30,9 +32,16 @@ class UsedTimeUpdateHelper (val date: DateInTimezone) {
|
||||||
|
|
||||||
val timeToAdd = mutableMapOf<String, Int>()
|
val timeToAdd = mutableMapOf<String, Int>()
|
||||||
val extraTimeToSubtract = 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
|
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) {
|
if (time < 0) {
|
||||||
throw IllegalArgumentException()
|
throw IllegalArgumentException()
|
||||||
}
|
}
|
||||||
|
@ -43,10 +52,24 @@ class UsedTimeUpdateHelper (val date: DateInTimezone) {
|
||||||
|
|
||||||
timeToAdd[categoryId] = (timeToAdd[categoryId] ?: 0) + time
|
timeToAdd[categoryId] = (timeToAdd[categoryId] ?: 0) + time
|
||||||
|
|
||||||
|
if (sessionDurationLimits.isNotEmpty()) {
|
||||||
|
this.sessionDurationLimitSlots[categoryId] = sessionDurationLimits
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sessionDurationLimits.isNotEmpty() && trustedTimestamp != 0L) {
|
||||||
|
this.trustedTimestamp = trustedTimestamp
|
||||||
|
}
|
||||||
|
|
||||||
if (includingExtraTime) {
|
if (includingExtraTime) {
|
||||||
extraTimeToSubtract[categoryId] = (extraTimeToSubtract[categoryId] ?: 0) + time
|
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) {
|
if (timeToAdd[categoryId]!! >= 1000 * 10) {
|
||||||
shouldDoAutoCommit = true
|
shouldDoAutoCommit = true
|
||||||
}
|
}
|
||||||
|
@ -77,9 +100,12 @@ class UsedTimeUpdateHelper (val date: DateInTimezone) {
|
||||||
AddUsedTimeActionItem(
|
AddUsedTimeActionItem(
|
||||||
categoryId = categoryId,
|
categoryId = categoryId,
|
||||||
timeToAdd = timeToAdd[categoryId] ?: 0,
|
timeToAdd = timeToAdd[categoryId] ?: 0,
|
||||||
extraTimeToSubtract = extraTimeToSubtract[categoryId] ?: 0
|
extraTimeToSubtract = extraTimeToSubtract[categoryId] ?: 0,
|
||||||
|
additionalCountingSlots = additionalSlots[categoryId] ?: emptySet(),
|
||||||
|
sessionDurationLimits = sessionDurationLimitSlots[categoryId] ?: emptySet()
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
trustedTimestamp = trustedTimestamp
|
||||||
),
|
),
|
||||||
appLogic = appLogic,
|
appLogic = appLogic,
|
||||||
ignoreIfDeviceIsNotConfigured = true
|
ignoreIfDeviceIsNotConfigured = true
|
||||||
|
@ -96,6 +122,9 @@ class UsedTimeUpdateHelper (val date: DateInTimezone) {
|
||||||
|
|
||||||
timeToAdd.clear()
|
timeToAdd.clear()
|
||||||
extraTimeToSubtract.clear()
|
extraTimeToSubtract.clear()
|
||||||
|
sessionDurationLimitSlots.clear()
|
||||||
|
trustedTimestamp = 0
|
||||||
|
additionalSlots.clear()
|
||||||
shouldDoAutoCommit = false
|
shouldDoAutoCommit = false
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -360,7 +360,24 @@ object ApplyServerDataStatus {
|
||||||
UsedTimeItem(
|
UsedTimeItem(
|
||||||
dayOfEpoch = it.dayOfEpoch,
|
dayOfEpoch = it.dayOfEpoch,
|
||||||
usedMillis = it.usedMillis,
|
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.ImmutableBitmask
|
||||||
import io.timelimit.android.data.customtypes.ImmutableBitmaskJson
|
import io.timelimit.android.data.customtypes.ImmutableBitmaskJson
|
||||||
import io.timelimit.android.data.model.*
|
import io.timelimit.android.data.model.*
|
||||||
|
import io.timelimit.android.extensions.MinuteOfDay
|
||||||
import io.timelimit.android.integration.platform.*
|
import io.timelimit.android.integration.platform.*
|
||||||
import io.timelimit.android.sync.network.ParentPassword
|
import io.timelimit.android.sync.network.ParentPassword
|
||||||
import io.timelimit.android.sync.validation.ListValidation
|
import io.timelimit.android.sync.validation.ListValidation
|
||||||
|
import org.json.JSONArray
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.util.*
|
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 {
|
companion object {
|
||||||
const val TYPE_VALUE = "ADD_USED_TIME_V2"
|
const val TYPE_VALUE = "ADD_USED_TIME_V2"
|
||||||
private const val DAY_OF_EPOCH = "d"
|
private const val DAY_OF_EPOCH = "d"
|
||||||
private const val ITEMS = "i"
|
private const val ITEMS = "i"
|
||||||
|
private const val TRUSTED_TIMESTAMP = "t"
|
||||||
|
|
||||||
fun parse(action: JSONObject): AddUsedTimeActionVersion2 = AddUsedTimeActionVersion2(
|
fun parse(action: JSONObject): AddUsedTimeActionVersion2 = AddUsedTimeActionVersion2(
|
||||||
dayOfEpoch = action.getInt(DAY_OF_EPOCH),
|
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 {
|
init {
|
||||||
if (dayOfEpoch < 0) {
|
if (dayOfEpoch < 0 || trustedTimestamp < 0) {
|
||||||
throw IllegalArgumentException()
|
throw IllegalArgumentException()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,20 +186,42 @@ data class AddUsedTimeActionVersion2(val dayOfEpoch: Int, val items: List<AddUse
|
||||||
items.forEach { it.serialize(writer) }
|
items.forEach { it.serialize(writer) }
|
||||||
writer.endArray()
|
writer.endArray()
|
||||||
|
|
||||||
|
if (trustedTimestamp != 0L) {
|
||||||
|
writer.name(TRUSTED_TIMESTAMP).value(trustedTimestamp)
|
||||||
|
}
|
||||||
|
|
||||||
writer.endObject()
|
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 {
|
companion object {
|
||||||
private const val CATEGORY_ID = "categoryId"
|
private const val CATEGORY_ID = "categoryId"
|
||||||
private const val TIME_TO_ADD = "tta"
|
private const val TIME_TO_ADD = "tta"
|
||||||
private const val EXTRA_TIME_TO_SUBTRACT = "etts"
|
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(
|
fun parse(item: JSONObject): AddUsedTimeActionItem = AddUsedTimeActionItem(
|
||||||
categoryId = item.getString(CATEGORY_ID),
|
categoryId = item.getString(CATEGORY_ID),
|
||||||
timeToAdd = item.getInt(TIME_TO_ADD),
|
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(TIME_TO_ADD).value(timeToAdd)
|
||||||
writer.name(EXTRA_TIME_TO_SUBTRACT).value(extraTimeToSubtract)
|
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()
|
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 ClearTemporarilyAllowedAppsAction(val deviceId: String): AppLogicAction(), LocalOnlyAction
|
||||||
|
|
||||||
data class InstalledApp(val packageName: String, val title: String, val isLaunchable: Boolean, val recommendation: AppRecommendation) {
|
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 {
|
companion object {
|
||||||
const val TYPE_VALUE = "UPDATE_TIMELIMIT_RULE"
|
const val TYPE_VALUE = "UPDATE_TIMELIMIT_RULE"
|
||||||
private const val RULE_ID = "ruleId"
|
private const val RULE_ID = "ruleId"
|
||||||
private const val MAX_TIME_IN_MILLIS = "time"
|
private const val MAX_TIME_IN_MILLIS = "time"
|
||||||
private const val DAY_MASK = "days"
|
private const val DAY_MASK = "days"
|
||||||
private const val APPLY_TO_EXTRA_TIME_USAGE = "extraTime"
|
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 {
|
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)) {
|
if (dayMask < 0 || dayMask > (1 or 2 or 4 or 8 or 16 or 32 or 64)) {
|
||||||
throw IllegalArgumentException()
|
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) {
|
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(MAX_TIME_IN_MILLIS).value(maximumTimeInMillis)
|
||||||
writer.name(DAY_MASK).value(dayMask)
|
writer.name(DAY_MASK).value(dayMask)
|
||||||
writer.name(APPLY_TO_EXTRA_TIME_USAGE).value(applyToExtraTimeUsage)
|
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()
|
writer.endObject()
|
||||||
}
|
}
|
||||||
|
@ -1369,7 +1502,7 @@ data class AddUserAction(val name: String, val userType: UserType, val password:
|
||||||
|
|
||||||
fun parse(action: JSONObject): AddUserAction {
|
fun parse(action: JSONObject): AddUserAction {
|
||||||
var password: ParentPassword? = null
|
var password: ParentPassword? = null
|
||||||
val passwordObject = action.getJSONObject(PASSWORD)
|
val passwordObject = action.optJSONObject(PASSWORD)
|
||||||
|
|
||||||
if (passwordObject != null) {
|
if (passwordObject != null) {
|
||||||
password = ParentPassword.parse(passwordObject)
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -19,16 +19,7 @@ import android.util.JsonWriter
|
||||||
import java.io.StringWriter
|
import java.io.StringWriter
|
||||||
|
|
||||||
object SerializationUtil {
|
object SerializationUtil {
|
||||||
fun serializeAction(action: ParentAction): String {
|
fun serializeAction(action: Action): String {
|
||||||
val stringWriter = StringWriter()
|
|
||||||
val jsonWriter = JsonWriter(stringWriter)
|
|
||||||
|
|
||||||
action.serialize(jsonWriter)
|
|
||||||
|
|
||||||
return stringWriter.buffer.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun serializeAction(action: AppLogicAction): String {
|
|
||||||
val stringWriter = StringWriter()
|
val stringWriter = StringWriter()
|
||||||
val jsonWriter = JsonWriter(stringWriter)
|
val jsonWriter = JsonWriter(stringWriter)
|
||||||
|
|
||||||
|
|
|
@ -117,8 +117,22 @@ object ApplyActionUtil {
|
||||||
|
|
||||||
if (parsed is AddUsedTimeActionVersion2 && parsed.dayOfEpoch == action.dayOfEpoch) {
|
if (parsed is AddUsedTimeActionVersion2 && parsed.dayOfEpoch == action.dayOfEpoch) {
|
||||||
var updatedAction: AddUsedTimeActionVersion2 = parsed
|
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 ->
|
action.items.forEach { newItem ->
|
||||||
|
if (issues) return@forEach
|
||||||
|
|
||||||
val oldItem = updatedAction.items.find { it.categoryId == newItem.categoryId }
|
val oldItem = updatedAction.items.find { it.categoryId == newItem.categoryId }
|
||||||
|
|
||||||
if (oldItem == null) {
|
if (oldItem == null) {
|
||||||
|
@ -126,10 +140,28 @@ object ApplyActionUtil {
|
||||||
items = updatedAction.items + listOf(newItem)
|
items = updatedAction.items + listOf(newItem)
|
||||||
)
|
)
|
||||||
} else {
|
} 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(
|
val mergedItem = AddUsedTimeActionItem(
|
||||||
timeToAdd = oldItem.timeToAdd + newItem.timeToAdd,
|
timeToAdd = oldItem.timeToAdd + newItem.timeToAdd,
|
||||||
extraTimeToSubtract = oldItem.extraTimeToSubtract + newItem.extraTimeToSubtract,
|
extraTimeToSubtract = oldItem.extraTimeToSubtract + newItem.extraTimeToSubtract,
|
||||||
categoryId = newItem.categoryId
|
categoryId = newItem.categoryId,
|
||||||
|
additionalCountingSlots = oldItem.additionalCountingSlots,
|
||||||
|
sessionDurationLimits = oldItem.sessionDurationLimits
|
||||||
)
|
)
|
||||||
|
|
||||||
updatedAction = updatedAction.copy(
|
updatedAction = updatedAction.copy(
|
||||||
|
@ -138,20 +170,22 @@ object ApplyActionUtil {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// update the previous action
|
if (!issues) {
|
||||||
database.pendingSyncAction().updateEncodedActionSync(
|
// update the previous action
|
||||||
sequenceNumber = previousAction.sequenceNumber,
|
database.pendingSyncAction().updateEncodedActionSync(
|
||||||
action = StringWriter().apply {
|
sequenceNumber = previousAction.sequenceNumber,
|
||||||
JsonWriter(this).apply {
|
action = StringWriter().apply {
|
||||||
updatedAction.serialize(this)
|
JsonWriter(this).apply {
|
||||||
}
|
updatedAction.serialize(this)
|
||||||
}.toString()
|
}
|
||||||
)
|
}.toString()
|
||||||
|
)
|
||||||
|
|
||||||
database.setTransactionSuccessful()
|
database.setTransactionSuccessful()
|
||||||
syncUtil.requestVeryUnimportantSync()
|
syncUtil.requestVeryUnimportantSync()
|
||||||
|
|
||||||
return@executeAndWait
|
return@executeAndWait
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,10 +16,8 @@
|
||||||
package io.timelimit.android.sync.actions.dispatch
|
package io.timelimit.android.sync.actions.dispatch
|
||||||
|
|
||||||
import io.timelimit.android.data.Database
|
import io.timelimit.android.data.Database
|
||||||
import io.timelimit.android.data.model.App
|
import io.timelimit.android.data.model.*
|
||||||
import io.timelimit.android.data.model.AppActivity
|
import io.timelimit.android.extensions.MinuteOfDay
|
||||||
import io.timelimit.android.data.model.HadManipulationFlag
|
|
||||||
import io.timelimit.android.data.model.UsedTimeItem
|
|
||||||
import io.timelimit.android.integration.platform.NewPermissionStatusUtil
|
import io.timelimit.android.integration.platform.NewPermissionStatusUtil
|
||||||
import io.timelimit.android.integration.platform.ProtectionLevelUtil
|
import io.timelimit.android.integration.platform.ProtectionLevelUtil
|
||||||
import io.timelimit.android.integration.platform.RuntimePermissionStatusUtil
|
import io.timelimit.android.integration.platform.RuntimePermissionStatusUtil
|
||||||
|
@ -46,7 +44,9 @@ object LocalDatabaseAppLogicActionDispatcher {
|
||||||
val updatedRows = database.usedTimes().addUsedTime(
|
val updatedRows = database.usedTimes().addUsedTime(
|
||||||
categoryId = categoryId,
|
categoryId = categoryId,
|
||||||
timeToAdd = action.timeToAdd,
|
timeToAdd = action.timeToAdd,
|
||||||
dayOfEpoch = action.dayOfEpoch
|
dayOfEpoch = action.dayOfEpoch,
|
||||||
|
start = MinuteOfDay.MIN,
|
||||||
|
end = MinuteOfDay.MAX
|
||||||
)
|
)
|
||||||
|
|
||||||
if (updatedRows == 0) {
|
if (updatedRows == 0) {
|
||||||
|
@ -55,7 +55,9 @@ object LocalDatabaseAppLogicActionDispatcher {
|
||||||
database.usedTimes().insertUsedTime(UsedTimeItem(
|
database.usedTimes().insertUsedTime(UsedTimeItem(
|
||||||
categoryId = categoryId,
|
categoryId = categoryId,
|
||||||
dayOfEpoch = action.dayOfEpoch,
|
dayOfEpoch = action.dayOfEpoch,
|
||||||
usedMillis = action.timeToAdd.toLong()
|
usedMillis = action.timeToAdd.toLong(),
|
||||||
|
startTimeOfDay = MinuteOfDay.MIN,
|
||||||
|
endTimeOfDay = MinuteOfDay.MAX
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,22 +83,67 @@ object LocalDatabaseAppLogicActionDispatcher {
|
||||||
database.category().getCategoryByIdSync(item.categoryId)
|
database.category().getCategoryByIdSync(item.categoryId)
|
||||||
?: throw CategoryNotFoundException()
|
?: throw CategoryNotFoundException()
|
||||||
|
|
||||||
val updatedRows = database.usedTimes().addUsedTime(
|
fun handle(start: Int, end: Int) {
|
||||||
categoryId = item.categoryId,
|
val updatedRows = database.usedTimes().addUsedTime(
|
||||||
timeToAdd = item.timeToAdd,
|
|
||||||
dayOfEpoch = action.dayOfEpoch
|
|
||||||
)
|
|
||||||
|
|
||||||
if (updatedRows == 0) {
|
|
||||||
// create new entry
|
|
||||||
|
|
||||||
database.usedTimes().insertUsedTime(UsedTimeItem(
|
|
||||||
categoryId = item.categoryId,
|
categoryId = item.categoryId,
|
||||||
|
timeToAdd = item.timeToAdd,
|
||||||
dayOfEpoch = action.dayOfEpoch,
|
dayOfEpoch = action.dayOfEpoch,
|
||||||
usedMillis = item.timeToAdd.toLong()
|
start = start,
|
||||||
))
|
end = end
|
||||||
|
)
|
||||||
|
|
||||||
|
if (updatedRows == 0) {
|
||||||
|
// create new entry
|
||||||
|
|
||||||
|
database.usedTimes().insertUsedTime(UsedTimeItem(
|
||||||
|
categoryId = item.categoryId,
|
||||||
|
dayOfEpoch = action.dayOfEpoch,
|
||||||
|
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) {
|
if (item.extraTimeToSubtract != 0) {
|
||||||
database.category().subtractCategoryExtraTime(
|
database.category().subtractCategoryExtraTime(
|
||||||
|
|
|
@ -205,7 +205,11 @@ object LocalDatabaseParentActionDispatcher {
|
||||||
database.timeLimitRules().updateTimeLimitRule(oldRule.copy(
|
database.timeLimitRules().updateTimeLimitRule(oldRule.copy(
|
||||||
maximumTimeInMillis = action.maximumTimeInMillis,
|
maximumTimeInMillis = action.maximumTimeInMillis,
|
||||||
dayMask = action.dayMask,
|
dayMask = action.dayMask,
|
||||||
applyToExtraTimeUsage = action.applyToExtraTimeUsage
|
applyToExtraTimeUsage = action.applyToExtraTimeUsage,
|
||||||
|
startMinuteOfDay = action.start,
|
||||||
|
endMinuteOfDay = action.end,
|
||||||
|
sessionDurationMilliseconds = action.sessionDurationMilliseconds,
|
||||||
|
sessionPauseMilliseconds = action.sessionPauseMilliseconds
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
is SetDeviceUserAction -> {
|
is SetDeviceUserAction -> {
|
||||||
|
|
|
@ -33,6 +33,8 @@ data class ClientDataStatus(
|
||||||
private const val APPS = "apps"
|
private const val APPS = "apps"
|
||||||
private const val CATEGORIES = "categories"
|
private const val CATEGORIES = "categories"
|
||||||
private const val USERS = "users"
|
private const val USERS = "users"
|
||||||
|
private const val CLIENT_LEVEL = "clientLevel"
|
||||||
|
private const val CLIENT_LEVEL_VALUE = 2
|
||||||
|
|
||||||
val empty = ClientDataStatus(
|
val empty = ClientDataStatus(
|
||||||
deviceListVersion = "",
|
deviceListVersion = "",
|
||||||
|
@ -75,6 +77,7 @@ data class ClientDataStatus(
|
||||||
fun serialize(writer: JsonWriter) {
|
fun serialize(writer: JsonWriter) {
|
||||||
writer.beginObject()
|
writer.beginObject()
|
||||||
|
|
||||||
|
writer.name(CLIENT_LEVEL).value(CLIENT_LEVEL_VALUE)
|
||||||
writer.name(DEVICES).value(deviceListVersion)
|
writer.name(DEVICES).value(deviceListVersion)
|
||||||
writer.name(USERS).value(userListVersion)
|
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.ImmutableBitmask
|
||||||
import io.timelimit.android.data.customtypes.ImmutableBitmaskJson
|
import io.timelimit.android.data.customtypes.ImmutableBitmaskJson
|
||||||
import io.timelimit.android.data.model.*
|
import io.timelimit.android.data.model.*
|
||||||
|
import io.timelimit.android.extensions.MinuteOfDay
|
||||||
import io.timelimit.android.extensions.parseList
|
import io.timelimit.android.extensions.parseList
|
||||||
import io.timelimit.android.integration.platform.*
|
import io.timelimit.android.integration.platform.*
|
||||||
import io.timelimit.android.sync.actions.AppActivityItem
|
import io.timelimit.android.sync.actions.AppActivityItem
|
||||||
|
@ -476,16 +477,19 @@ data class ServerUpdatedCategoryAssignedApps(
|
||||||
data class ServerUpdatedCategoryUsedTimes(
|
data class ServerUpdatedCategoryUsedTimes(
|
||||||
val categoryId: String,
|
val categoryId: String,
|
||||||
val usedTimeItems: List<ServerUsedTimeItem>,
|
val usedTimeItems: List<ServerUsedTimeItem>,
|
||||||
|
val sessionDurations: List<ServerSessionDuration>,
|
||||||
val version: String
|
val version: String
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
private const val CATEGORY_ID = "categoryId"
|
private const val CATEGORY_ID = "categoryId"
|
||||||
private const val USED_TIMES_ITEMS = "times"
|
private const val USED_TIMES_ITEMS = "times"
|
||||||
|
private const val SESSION_DURATIONS = "sessionDurations"
|
||||||
private const val VERSION = "version"
|
private const val VERSION = "version"
|
||||||
|
|
||||||
fun parse(reader: JsonReader): ServerUpdatedCategoryUsedTimes {
|
fun parse(reader: JsonReader): ServerUpdatedCategoryUsedTimes {
|
||||||
var categoryId: String? = null
|
var categoryId: String? = null
|
||||||
var usedTimeItems: List<ServerUsedTimeItem>? = null
|
var usedTimeItems: List<ServerUsedTimeItem>? = null
|
||||||
|
var sessionDurations = emptyList<ServerSessionDuration>()
|
||||||
var version: String? = null
|
var version: String? = null
|
||||||
|
|
||||||
reader.beginObject()
|
reader.beginObject()
|
||||||
|
@ -493,6 +497,7 @@ data class ServerUpdatedCategoryUsedTimes(
|
||||||
when (reader.nextName()) {
|
when (reader.nextName()) {
|
||||||
CATEGORY_ID -> categoryId = reader.nextString()
|
CATEGORY_ID -> categoryId = reader.nextString()
|
||||||
USED_TIMES_ITEMS -> usedTimeItems = ServerUsedTimeItem.parseList(reader)
|
USED_TIMES_ITEMS -> usedTimeItems = ServerUsedTimeItem.parseList(reader)
|
||||||
|
SESSION_DURATIONS -> sessionDurations = ServerSessionDuration.parseList(reader)
|
||||||
VERSION -> version = reader.nextString()
|
VERSION -> version = reader.nextString()
|
||||||
else -> reader.skipValue()
|
else -> reader.skipValue()
|
||||||
}
|
}
|
||||||
|
@ -502,6 +507,7 @@ data class ServerUpdatedCategoryUsedTimes(
|
||||||
return ServerUpdatedCategoryUsedTimes(
|
return ServerUpdatedCategoryUsedTimes(
|
||||||
categoryId = categoryId!!,
|
categoryId = categoryId!!,
|
||||||
usedTimeItems = usedTimeItems!!,
|
usedTimeItems = usedTimeItems!!,
|
||||||
|
sessionDurations = sessionDurations,
|
||||||
version = version!!
|
version = version!!
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -512,21 +518,29 @@ data class ServerUpdatedCategoryUsedTimes(
|
||||||
|
|
||||||
data class ServerUsedTimeItem(
|
data class ServerUsedTimeItem(
|
||||||
val dayOfEpoch: Int,
|
val dayOfEpoch: Int,
|
||||||
val usedMillis: Long
|
val usedMillis: Long,
|
||||||
|
val startTimeOfDay: Int,
|
||||||
|
val endTimeOfDay: Int
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
private const val DAY_OF_EPOCH = "day"
|
private const val DAY_OF_EPOCH = "day"
|
||||||
private const val USED_MILLIS = "time"
|
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 {
|
fun parse(reader: JsonReader): ServerUsedTimeItem {
|
||||||
var dayOfEpoch: Int? = null
|
var dayOfEpoch: Int? = null
|
||||||
var usedMillis: Long? = null
|
var usedMillis: Long? = null
|
||||||
|
var startTimeOfDay: Int = MinuteOfDay.MIN
|
||||||
|
var endTimeOfDay: Int = MinuteOfDay.MAX
|
||||||
|
|
||||||
reader.beginObject()
|
reader.beginObject()
|
||||||
while (reader.hasNext()) {
|
while (reader.hasNext()) {
|
||||||
when (reader.nextName()) {
|
when (reader.nextName()) {
|
||||||
DAY_OF_EPOCH -> dayOfEpoch = reader.nextInt()
|
DAY_OF_EPOCH -> dayOfEpoch = reader.nextInt()
|
||||||
USED_MILLIS -> usedMillis = reader.nextLong()
|
USED_MILLIS -> usedMillis = reader.nextLong()
|
||||||
|
START_TIME_OF_DAY -> startTimeOfDay = reader.nextInt()
|
||||||
|
END_TIME_OF_DAY -> endTimeOfDay = reader.nextInt()
|
||||||
else -> reader.skipValue()
|
else -> reader.skipValue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -534,7 +548,9 @@ data class ServerUsedTimeItem(
|
||||||
|
|
||||||
return ServerUsedTimeItem(
|
return ServerUsedTimeItem(
|
||||||
dayOfEpoch = dayOfEpoch!!,
|
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(
|
data class ServerUpdatedTimeLimitRules(
|
||||||
val categoryId: String,
|
val categoryId: String,
|
||||||
val version: String,
|
val version: String,
|
||||||
|
@ -593,19 +671,31 @@ data class ServerTimeLimitRule(
|
||||||
val id: String,
|
val id: String,
|
||||||
val applyToExtraTimeUsage: Boolean,
|
val applyToExtraTimeUsage: Boolean,
|
||||||
val dayMask: Byte,
|
val dayMask: Byte,
|
||||||
val maximumTimeInMillis: Int
|
val maximumTimeInMillis: Int,
|
||||||
|
val startMinuteOfDay: Int,
|
||||||
|
val endMinuteOfDay: Int,
|
||||||
|
val sessionDurationMilliseconds: Int,
|
||||||
|
val sessionPauseMilliseconds: Int
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
private const val ID = "id"
|
private const val ID = "id"
|
||||||
private const val APPLY_TO_EXTRA_TIME_USAGE = "extraTime"
|
private const val APPLY_TO_EXTRA_TIME_USAGE = "extraTime"
|
||||||
private const val DAY_MASK = "dayMask"
|
private const val DAY_MASK = "dayMask"
|
||||||
private const val MAXIMUM_TIME_IN_MILLIS = "maxTime"
|
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 {
|
fun parse(reader: JsonReader): ServerTimeLimitRule {
|
||||||
var id: String? = null
|
var id: String? = null
|
||||||
var applyToExtraTimeUsage: Boolean? = null
|
var applyToExtraTimeUsage: Boolean? = null
|
||||||
var dayMask: Byte? = null
|
var dayMask: Byte? = null
|
||||||
var maximumTimeInMillis: Int? = 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()
|
reader.beginObject()
|
||||||
while (reader.hasNext()) {
|
while (reader.hasNext()) {
|
||||||
|
@ -614,6 +704,10 @@ data class ServerTimeLimitRule(
|
||||||
APPLY_TO_EXTRA_TIME_USAGE -> applyToExtraTimeUsage = reader.nextBoolean()
|
APPLY_TO_EXTRA_TIME_USAGE -> applyToExtraTimeUsage = reader.nextBoolean()
|
||||||
DAY_MASK -> dayMask = reader.nextInt().toByte()
|
DAY_MASK -> dayMask = reader.nextInt().toByte()
|
||||||
MAXIMUM_TIME_IN_MILLIS -> maximumTimeInMillis = reader.nextInt()
|
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()
|
else -> reader.skipValue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -623,7 +717,11 @@ data class ServerTimeLimitRule(
|
||||||
id = id!!,
|
id = id!!,
|
||||||
applyToExtraTimeUsage = applyToExtraTimeUsage!!,
|
applyToExtraTimeUsage = applyToExtraTimeUsage!!,
|
||||||
dayMask = dayMask!!,
|
dayMask = dayMask!!,
|
||||||
maximumTimeInMillis = maximumTimeInMillis!!
|
maximumTimeInMillis = maximumTimeInMillis!!,
|
||||||
|
startMinuteOfDay = startMinuteOfDay,
|
||||||
|
endMinuteOfDay = endMinuteOfDay,
|
||||||
|
sessionDurationMilliseconds = sessionDurationMilliseconds,
|
||||||
|
sessionPauseMilliseconds = sessionPauseMilliseconds
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -645,7 +743,11 @@ data class ServerTimeLimitRule(
|
||||||
applyToExtraTimeUsage = applyToExtraTimeUsage,
|
applyToExtraTimeUsage = applyToExtraTimeUsage,
|
||||||
dayMask = dayMask,
|
dayMask = dayMask,
|
||||||
maximumTimeInMillis = maximumTimeInMillis,
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -21,9 +21,11 @@ import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
import io.timelimit.android.data.model.TimeLimitRule
|
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.AddItemViewBinding
|
||||||
import io.timelimit.android.databinding.FragmentCategoryTimeLimitRuleItemBinding
|
import io.timelimit.android.databinding.FragmentCategoryTimeLimitRuleItemBinding
|
||||||
import io.timelimit.android.databinding.TimeLimitRuleIntroductionBinding
|
import io.timelimit.android.databinding.TimeLimitRuleIntroductionBinding
|
||||||
|
import io.timelimit.android.extensions.MinuteOfDay
|
||||||
import io.timelimit.android.util.JoinUtil
|
import io.timelimit.android.util.JoinUtil
|
||||||
import io.timelimit.android.util.TimeTextUtil
|
import io.timelimit.android.util.TimeTextUtil
|
||||||
import kotlin.properties.Delegates
|
import kotlin.properties.Delegates
|
||||||
|
@ -36,7 +38,8 @@ class Adapter: RecyclerView.Adapter<ViewHolder>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
var data: List<TimeLimitRuleItem> by Delegates.observable(emptyList()) { _, _, _ -> notifyDataSetChanged() }
|
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
|
var handlers: Handlers? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
@ -110,26 +113,42 @@ class Adapter: RecyclerView.Adapter<ViewHolder>() {
|
||||||
is TimeLimitRuleRuleItem -> {
|
is TimeLimitRuleRuleItem -> {
|
||||||
val rule = item.rule
|
val rule = item.rule
|
||||||
val binding = (holder as ItemViewHolder).view
|
val binding = (holder as ItemViewHolder).view
|
||||||
|
val context = binding.root.context
|
||||||
val dayNames = binding.root.resources.getStringArray(R.array.days_of_week_array)
|
val dayNames = binding.root.resources.getStringArray(R.array.days_of_week_array)
|
||||||
val usedTime = usedTimes?.mapIndexed { index, value ->
|
val usedTime = usedTimes.filter { usedTime ->
|
||||||
if (rule.dayMask.toInt() and (1 shl index) != 0) {
|
val dayOfWeek = usedTime.dayOfEpoch - epochDayOfStartOfWeek
|
||||||
value
|
usedTime.startTimeOfDay == rule.startMinuteOfDay && usedTime.endTimeOfDay == rule.endMinuteOfDay &&
|
||||||
} else {
|
(rule.dayMask.toInt() and (1 shl dayOfWeek) != 0)
|
||||||
0
|
}.map { it.usedMillis }.sum().toInt()
|
||||||
}
|
|
||||||
}?.sum()?.toInt() ?: 0
|
|
||||||
|
|
||||||
binding.maxTimeString = TimeTextUtil.time(rule.maximumTimeInMillis, binding.root.context)
|
binding.maxTimeString = TimeTextUtil.time(rule.maximumTimeInMillis, context)
|
||||||
binding.usageAsText = TimeTextUtil.used(usedTime, binding.root.context)
|
binding.usageAsText = TimeTextUtil.used(usedTime, context)
|
||||||
binding.usageProgressInPercent = if (rule.maximumTimeInMillis > 0)
|
binding.usageProgressInPercent = if (rule.maximumTimeInMillis > 0)
|
||||||
(usedTime * 100 / rule.maximumTimeInMillis)
|
(usedTime * 100 / rule.maximumTimeInMillis)
|
||||||
else
|
else
|
||||||
100
|
100
|
||||||
binding.daysString = JoinUtil.join(
|
binding.daysString = JoinUtil.join(
|
||||||
dayNames.filterIndexed { index, _ -> (rule.dayMask.toInt() and (1 shl index)) != 0 },
|
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.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.card.setOnClickListener { handlers?.onTimeLimitRuleClicked(rule) }
|
||||||
|
|
||||||
binding.executePendingBindings()
|
binding.executePendingBindings()
|
||||||
|
|
|
@ -74,14 +74,19 @@ class CategoryTimeLimitRulesFragment : Fragment(), EditTimeLimitRuleDialogFragme
|
||||||
|
|
||||||
val userDate = database.user().getUserByIdLive(params.childId).getDateLive(logic.realTimeLogic)
|
val userDate = database.user().getUserByIdLive(params.childId).getDateLive(logic.realTimeLogic)
|
||||||
|
|
||||||
val usedTimeItems = userDate.switchMap {
|
userDate.switchMap { date ->
|
||||||
date ->
|
val firstDayOfWeekAsEpochDay = date.dayOfEpoch - date.dayOfWeek
|
||||||
|
|
||||||
database.usedTimes().getUsedTimesOfWeek(
|
database.usedTimes().getUsedTimesOfWeek(
|
||||||
categoryId = params.categoryId,
|
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)
|
val hasHiddenIntro = database.config().wereHintsShown(HintsToShow.TIME_LIMIT_RULE_INTRODUCTION)
|
||||||
|
|
||||||
|
@ -97,16 +102,10 @@ class CategoryTimeLimitRulesFragment : Fragment(), EditTimeLimitRuleDialogFragme
|
||||||
listOf(TimeLimitRuleIntroductionItem) + baseList
|
listOf(TimeLimitRuleIntroductionItem) + baseList
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.observe(this, Observer {
|
}.observe(viewLifecycleOwner, Observer {
|
||||||
adapter.data = it
|
adapter.data = it
|
||||||
})
|
})
|
||||||
|
|
||||||
usedTimeItems.observe(this, Observer {
|
|
||||||
usedTimes ->
|
|
||||||
|
|
||||||
adapter.usedTimes = (0..6).map { usedTimes[it]?.usedMillis ?: 0 } }
|
|
||||||
)
|
|
||||||
|
|
||||||
adapter.handlers = object: Handlers {
|
adapter.handlers = object: Handlers {
|
||||||
override fun onTimeLimitRuleClicked(rule: TimeLimitRule) {
|
override fun onTimeLimitRuleClicked(rule: TimeLimitRule) {
|
||||||
if (auth.requestAuthenticationOrReturnTrue()) {
|
if (auth.requestAuthenticationOrReturnTrue()) {
|
||||||
|
@ -171,7 +170,11 @@ class CategoryTimeLimitRulesFragment : Fragment(), EditTimeLimitRuleDialogFragme
|
||||||
ruleId = oldRule.id,
|
ruleId = oldRule.id,
|
||||||
applyToExtraTimeUsage = oldRule.applyToExtraTimeUsage,
|
applyToExtraTimeUsage = oldRule.applyToExtraTimeUsage,
|
||||||
maximumTimeInMillis = oldRule.maximumTimeInMillis,
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -20,12 +20,13 @@ import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import com.google.android.material.R
|
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
|
import io.timelimit.android.R
|
||||||
import io.timelimit.android.async.Threads
|
import io.timelimit.android.async.Threads
|
||||||
import io.timelimit.android.coroutines.runAsync
|
import io.timelimit.android.coroutines.runAsync
|
||||||
import io.timelimit.android.data.IdGenerator
|
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.TimeLimitRule
|
||||||
import io.timelimit.android.data.model.UserType
|
import io.timelimit.android.data.model.UserType
|
||||||
import io.timelimit.android.databinding.FragmentEditTimeLimitRuleDialogBinding
|
import io.timelimit.android.databinding.FragmentEditTimeLimitRuleDialogBinding
|
||||||
|
import io.timelimit.android.extensions.MinuteOfDay
|
||||||
import io.timelimit.android.extensions.showSafe
|
import io.timelimit.android.extensions.showSafe
|
||||||
import io.timelimit.android.livedata.waitForNonNullValue
|
import io.timelimit.android.livedata.waitForNonNullValue
|
||||||
import io.timelimit.android.logic.DefaultAppLogic
|
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.mustread.MustReadFragment
|
||||||
import io.timelimit.android.ui.view.SelectDayViewHandlers
|
import io.timelimit.android.ui.view.SelectDayViewHandlers
|
||||||
import io.timelimit.android.ui.view.SelectTimeSpanViewListener
|
import io.timelimit.android.ui.view.SelectTimeSpanViewListener
|
||||||
|
import io.timelimit.android.util.TimeTextUtil
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
|
||||||
class EditTimeLimitRuleDialogFragment : BottomSheetDialogFragment() {
|
class EditTimeLimitRuleDialogFragment : BottomSheetDialogFragment(), DurationPickerDialogFragmentListener {
|
||||||
companion object {
|
companion object {
|
||||||
private const val PARAM_EXISTING_RULE = "a"
|
private const val PARAM_EXISTING_RULE = "a"
|
||||||
private const val PARAM_CATEGORY_ID = "b"
|
private const val PARAM_CATEGORY_ID = "b"
|
||||||
|
@ -76,6 +79,8 @@ class EditTimeLimitRuleDialogFragment : BottomSheetDialogFragment() {
|
||||||
|
|
||||||
var existingRule: TimeLimitRule? = null
|
var existingRule: TimeLimitRule? = null
|
||||||
var savedNewRule: TimeLimitRule? = null
|
var savedNewRule: TimeLimitRule? = null
|
||||||
|
lateinit var newRule: TimeLimitRule
|
||||||
|
lateinit var view: FragmentEditTimeLimitRuleDialogBinding
|
||||||
|
|
||||||
private val categoryId: String by lazy {
|
private val categoryId: String by lazy {
|
||||||
if (existingRule != null) {
|
if (existingRule != null) {
|
||||||
|
@ -109,13 +114,37 @@ class EditTimeLimitRuleDialogFragment : BottomSheetDialogFragment() {
|
||||||
?: arguments?.getParcelable<TimeLimitRule?>(PARAM_EXISTING_RULE)
|
?: arguments?.getParcelable<TimeLimitRule?>(PARAM_EXISTING_RULE)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun bindRule() {
|
||||||
|
savedNewRule = newRule
|
||||||
|
|
||||||
|
view.daySelection.selectedDays = BitSet.valueOf(
|
||||||
|
ByteBuffer.allocate(1).put(newRule.dayMask).apply {
|
||||||
|
position(0)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
view.applyToExtraTime = newRule.applyToExtraTimeUsage
|
||||||
|
view.timeSpan.timeInMillis = newRule.maximumTimeInMillis.toLong()
|
||||||
|
|
||||||
|
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? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
val view = FragmentEditTimeLimitRuleDialogBinding.inflate(layoutInflater, container, false)
|
|
||||||
val listener = targetFragment as EditTimeLimitRuleDialogFragmentListener
|
val listener = targetFragment as EditTimeLimitRuleDialogFragmentListener
|
||||||
var newRule: TimeLimitRule
|
|
||||||
val database = DefaultAppLogic.with(context!!).database
|
val database = DefaultAppLogic.with(context!!).database
|
||||||
|
|
||||||
auth.authenticatedUser.observe(this, Observer {
|
view = FragmentEditTimeLimitRuleDialogBinding.inflate(layoutInflater, container, false)
|
||||||
|
|
||||||
|
auth.authenticatedUser.observe(viewLifecycleOwner, Observer {
|
||||||
if (it == null || it.second.type != UserType.Parent) {
|
if (it == null || it.second.type != UserType.Parent) {
|
||||||
dismissAllowingStateLoss()
|
dismissAllowingStateLoss()
|
||||||
}
|
}
|
||||||
|
@ -129,7 +158,11 @@ class EditTimeLimitRuleDialogFragment : BottomSheetDialogFragment() {
|
||||||
categoryId = categoryId,
|
categoryId = categoryId,
|
||||||
applyToExtraTimeUsage = false,
|
applyToExtraTimeUsage = false,
|
||||||
dayMask = 0,
|
dayMask = 0,
|
||||||
maximumTimeInMillis = 1000 * 60 * 60 * 5 / 2 // 2,5 (5/2) hours
|
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 {
|
} else {
|
||||||
view.isNewRule = false
|
view.isNewRule = false
|
||||||
|
@ -145,22 +178,6 @@ class EditTimeLimitRuleDialogFragment : BottomSheetDialogFragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bindRule() {
|
|
||||||
savedNewRule = newRule
|
|
||||||
|
|
||||||
view.daySelection.selectedDays = BitSet.valueOf(
|
|
||||||
ByteBuffer.allocate(1).put(newRule.dayMask).apply {
|
|
||||||
position(0)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
view.applyToExtraTime = newRule.applyToExtraTimeUsage
|
|
||||||
view.timeSpan.timeInMillis = newRule.maximumTimeInMillis.toLong()
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
bindRule()
|
bindRule()
|
||||||
view.daySelection.handlers = object: SelectDayViewHandlers {
|
view.daySelection.handlers = object: SelectDayViewHandlers {
|
||||||
override fun updateDayChecked(day: Int) {
|
override fun updateDayChecked(day: Int) {
|
||||||
|
@ -181,6 +198,72 @@ class EditTimeLimitRuleDialogFragment : BottomSheetDialogFragment() {
|
||||||
bindRule()
|
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() {
|
override fun onSaveRule() {
|
||||||
view.timeSpan.clearNumberPickerFocus()
|
view.timeSpan.clearNumberPickerFocus()
|
||||||
|
|
||||||
|
@ -191,7 +274,11 @@ class EditTimeLimitRuleDialogFragment : BottomSheetDialogFragment() {
|
||||||
ruleId = newRule.id,
|
ruleId = newRule.id,
|
||||||
maximumTimeInMillis = newRule.maximumTimeInMillis,
|
maximumTimeInMillis = newRule.maximumTimeInMillis,
|
||||||
dayMask = newRule.dayMask,
|
dayMask = newRule.dayMask,
|
||||||
applyToExtraTimeUsage = newRule.applyToExtraTimeUsage
|
applyToExtraTimeUsage = newRule.applyToExtraTimeUsage,
|
||||||
|
start = newRule.startMinuteOfDay,
|
||||||
|
end = newRule.endMinuteOfDay,
|
||||||
|
sessionDurationMilliseconds = newRule.sessionDurationMilliseconds,
|
||||||
|
sessionPauseMilliseconds = newRule.sessionPauseMilliseconds
|
||||||
)
|
)
|
||||||
)) {
|
)) {
|
||||||
return
|
return
|
||||||
|
@ -245,13 +332,13 @@ class EditTimeLimitRuleDialogFragment : BottomSheetDialogFragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
database.config().getEnableAlternativeDurationSelectionAsync().observe(this, Observer {
|
database.config().getEnableAlternativeDurationSelectionAsync().observe(viewLifecycleOwner, Observer {
|
||||||
view.timeSpan.enablePickerMode(it)
|
view.timeSpan.enablePickerMode(it)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (existingRule != null) {
|
if (existingRule != null) {
|
||||||
database.timeLimitRules()
|
database.timeLimitRules()
|
||||||
.getTimeLimitRuleByIdLive(existingRule!!.id).observe(this, Observer {
|
.getTimeLimitRuleByIdLive(existingRule!!.id).observe(viewLifecycleOwner, Observer {
|
||||||
if (it == null) {
|
if (it == null) {
|
||||||
// rule was deleted
|
// rule was deleted
|
||||||
dismissAllowingStateLoss()
|
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 {
|
interface Handlers {
|
||||||
fun updateApplyToExtraTime(apply: Boolean)
|
fun updateApplyToExtraTime(apply: Boolean)
|
||||||
|
fun updateApplyToWholeDay(apply: Boolean)
|
||||||
|
fun updateStartTime()
|
||||||
|
fun updateEndTime()
|
||||||
|
fun updateSessionDurationLimit(enable: Boolean)
|
||||||
|
fun updateSessionLength()
|
||||||
|
fun updateSessionBreak()
|
||||||
fun onSaveRule()
|
fun onSaveRule()
|
||||||
fun onDeleteRule()
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -16,24 +16,28 @@
|
||||||
package io.timelimit.android.ui.manage.category.usagehistory
|
package io.timelimit.android.ui.manage.category.usagehistory
|
||||||
|
|
||||||
import android.text.format.DateFormat
|
import android.text.format.DateFormat
|
||||||
|
import android.text.format.DateUtils
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.paging.PagedListAdapter
|
import androidx.paging.PagedListAdapter
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
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.databinding.FragmentUsageHistoryItemBinding
|
||||||
|
import io.timelimit.android.extensions.MinuteOfDay
|
||||||
import io.timelimit.android.util.TimeTextUtil
|
import io.timelimit.android.util.TimeTextUtil
|
||||||
import org.threeten.bp.LocalDate
|
import org.threeten.bp.LocalDate
|
||||||
import org.threeten.bp.ZoneOffset
|
import org.threeten.bp.ZoneOffset
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class UsageHistoryAdapter: PagedListAdapter<UsedTimeItem, UsageHistoryViewHolder>(diffCallback) {
|
class UsageHistoryAdapter: PagedListAdapter<UsedTimeListItem, UsageHistoryViewHolder>(diffCallback) {
|
||||||
companion object {
|
companion object {
|
||||||
private val diffCallback = object: DiffUtil.ItemCallback<UsedTimeItem>() {
|
private val diffCallback = object: DiffUtil.ItemCallback<UsedTimeListItem>() {
|
||||||
override fun areContentsTheSame(oldItem: UsedTimeItem, newItem: UsedTimeItem) = oldItem == newItem
|
override fun areContentsTheSame(oldItem: UsedTimeListItem, newItem: UsedTimeListItem) = oldItem == newItem
|
||||||
override fun areItemsTheSame(oldItem: UsedTimeItem, newItem: UsedTimeItem) =
|
override fun areItemsTheSame(oldItem: UsedTimeListItem, newItem: UsedTimeListItem) =
|
||||||
(oldItem.dayOfEpoch == newItem.dayOfEpoch) && (oldItem.categoryId == newItem.categoryId)
|
(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 binding = holder.binding
|
||||||
val context = binding.root.context
|
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.date = ""
|
||||||
binding.usedTime = ""
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -42,11 +42,11 @@ class UsageHistoryFragment : Fragment() {
|
||||||
val adapter = UsageHistoryAdapter()
|
val adapter = UsageHistoryAdapter()
|
||||||
|
|
||||||
LivePagedListBuilder(
|
LivePagedListBuilder(
|
||||||
database.usedTimes().getUsedTimesByCategoryId(params.categoryId),
|
database.usedTimes().getUsedTimeListItemsByCategoryId(params.categoryId),
|
||||||
10
|
10
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
.observe(this, Observer {
|
.observe(viewLifecycleOwner, Observer {
|
||||||
binding.isEmpty = it.isEmpty()
|
binding.isEmpty = it.isEmpty()
|
||||||
adapter.submitList(it)
|
adapter.submitList(it)
|
||||||
})
|
})
|
||||||
|
|
|
@ -16,7 +16,6 @@
|
||||||
package io.timelimit.android.ui.manage.child.category
|
package io.timelimit.android.ui.manage.child.category
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.util.SparseLongArray
|
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import io.timelimit.android.data.extensions.mapToTimezone
|
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.data.model.HintsToShow
|
||||||
import io.timelimit.android.date.DateInTimezone
|
import io.timelimit.android.date.DateInTimezone
|
||||||
import io.timelimit.android.date.getMinuteOfWeek
|
import io.timelimit.android.date.getMinuteOfWeek
|
||||||
|
import io.timelimit.android.extensions.MinuteOfDay
|
||||||
import io.timelimit.android.livedata.ignoreUnchanged
|
import io.timelimit.android.livedata.ignoreUnchanged
|
||||||
import io.timelimit.android.livedata.liveDataFromFunction
|
import io.timelimit.android.livedata.liveDataFromFunction
|
||||||
import io.timelimit.android.livedata.map
|
import io.timelimit.android.livedata.map
|
||||||
|
@ -100,19 +100,16 @@ class ManageChildCategoriesModel(application: Application): AndroidViewModel(app
|
||||||
isBlockedTimeNow = category.blockedMinutesInWeek.read(childMinuteOfWeek),
|
isBlockedTimeNow = category.blockedMinutesInWeek.read(childMinuteOfWeek),
|
||||||
remainingTimeToday = RemainingTime.getRemainingTime(
|
remainingTimeToday = RemainingTime.getRemainingTime(
|
||||||
dayOfWeek = childDate.dayOfWeek,
|
dayOfWeek = childDate.dayOfWeek,
|
||||||
usedTimes = SparseLongArray().apply {
|
usedTimes = usedTimeItemsForCategory,
|
||||||
usedTimeItemsForCategory.forEach { usedTimeItem ->
|
|
||||||
|
|
||||||
val dayOfWeek = usedTimeItem.dayOfEpoch - firstDayOfWeek
|
|
||||||
|
|
||||||
put(dayOfWeek, usedTimeItem.usedMillis)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
rules = rules,
|
rules = rules,
|
||||||
extraTime = category.getExtraTime(dayOfEpoch = childDate.dayOfEpoch)
|
extraTime = category.getExtraTime(dayOfEpoch = childDate.dayOfEpoch),
|
||||||
|
minuteOfDay = childMinuteOfWeek % MinuteOfDay.LENGTH,
|
||||||
|
firstDayOfWeekAsEpochDay = firstDayOfWeek
|
||||||
)?.includingExtraTime,
|
)?.includingExtraTime,
|
||||||
usedTimeToday = usedTimeItemsForCategory.find { item -> item.dayOfEpoch == childDate.dayOfEpoch }?.usedMillis
|
usedTimeToday = usedTimeItemsForCategory.find { item ->
|
||||||
?: 0,
|
item.dayOfEpoch == childDate.dayOfEpoch && item.startTimeOfDay == MinuteOfDay.MIN &&
|
||||||
|
item.endTimeOfDay == MinuteOfDay.MAX
|
||||||
|
}?.usedMillis ?: 0,
|
||||||
usedForNotAssignedApps = categoryForUnassignedApps == category.id,
|
usedForNotAssignedApps = categoryForUnassignedApps == category.id,
|
||||||
parentCategoryTitle = parentCategory?.title
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -64,7 +64,11 @@ class DefaultCategories private constructor(private val context: Context) {
|
||||||
categoryId = categoryId,
|
categoryId = categoryId,
|
||||||
applyToExtraTimeUsage = false,
|
applyToExtraTimeUsage = false,
|
||||||
dayMask = (1 shl day).toByte(),
|
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,
|
categoryId = categoryId,
|
||||||
applyToExtraTimeUsage = false,
|
applyToExtraTimeUsage = false,
|
||||||
dayMask = (1 shl day).toByte(),
|
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,
|
categoryId = categoryId,
|
||||||
applyToExtraTimeUsage = false,
|
applyToExtraTimeUsage = false,
|
||||||
dayMask = 1 + 2 + 4 + 8 + 16 + 32 + 64,
|
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 io.timelimit.android.util.TimeTextUtil
|
||||||
import kotlin.properties.Delegates
|
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)
|
private val binding = ViewSelectTimeSpanBinding.inflate(LayoutInflater.from(context), this, false)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
|
@ -18,7 +18,11 @@ package io.timelimit.android.ui.widget
|
||||||
import android.util.SparseLongArray
|
import android.util.SparseLongArray
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import io.timelimit.android.data.extensions.mapToTimezone
|
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.DateInTimezone
|
||||||
|
import io.timelimit.android.date.getMinuteOfWeek
|
||||||
|
import io.timelimit.android.extensions.MinuteOfDay
|
||||||
import io.timelimit.android.livedata.ignoreUnchanged
|
import io.timelimit.android.livedata.ignoreUnchanged
|
||||||
import io.timelimit.android.livedata.liveDataFromFunction
|
import io.timelimit.android.livedata.liveDataFromFunction
|
||||||
import io.timelimit.android.livedata.map
|
import io.timelimit.android.livedata.map
|
||||||
|
@ -34,6 +38,11 @@ object TimesWidgetItems {
|
||||||
val userDate = userTimezone.switchMap { timeZone ->
|
val userDate = userTimezone.switchMap { timeZone ->
|
||||||
liveDataFromFunction { DateInTimezone.newInstance(logic.realTimeLogic.getCurrentTimeInMillis(), timeZone) }
|
liveDataFromFunction { DateInTimezone.newInstance(logic.realTimeLogic.getCurrentTimeInMillis(), timeZone) }
|
||||||
}.ignoreUnchanged()
|
}.ignoreUnchanged()
|
||||||
|
val userMinuteOfWeek = userTimezone.switchMap { timeZone ->
|
||||||
|
liveDataFromFunction {
|
||||||
|
getMinuteOfWeek(logic.realTimeLogic.getCurrentTimeInMillis(), timeZone)
|
||||||
|
}
|
||||||
|
}.ignoreUnchanged()
|
||||||
val categories = userId.switchMap { logic.database.category().getCategoriesByChildId(it) }
|
val categories = userId.switchMap { logic.database.category().getCategoriesByChildId(it) }
|
||||||
val usedTimeItemsForWeek = userDate.switchMap { date ->
|
val usedTimeItemsForWeek = userDate.switchMap { date ->
|
||||||
categories.switchMap { categories ->
|
categories.switchMap { categories ->
|
||||||
|
@ -49,36 +58,37 @@ object TimesWidgetItems {
|
||||||
categories.map { category -> category.id }
|
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 ->
|
val categoryItems = categories.switchMap { categories ->
|
||||||
timeLimitRules.switchMap { timeLimitRules ->
|
timeLimitRules.switchMap { timeLimitRules ->
|
||||||
userDate.switchMap { childDate ->
|
timeLimitSlot.switchMap { timeLimitSlot ->
|
||||||
usedTimeItemsForWeek.map { usedTimeItemsForWeek ->
|
userDate.switchMap { childDate ->
|
||||||
val rulesByCategoryId = timeLimitRules.groupBy { rule -> rule.categoryId }
|
usedTimeItemsForWeek.map { usedTimeItemsForWeek ->
|
||||||
val usedTimesByCategory = usedTimeItemsForWeek.groupBy { item -> item.categoryId }
|
val rulesByCategoryId = timeLimitRules.groupBy { rule -> rule.categoryId }
|
||||||
val firstDayOfWeek = childDate.dayOfEpoch - childDate.dayOfWeek
|
val usedTimesByCategory = usedTimeItemsForWeek.groupBy { item -> item.categoryId }
|
||||||
|
val firstDayOfWeek = childDate.dayOfEpoch - childDate.dayOfWeek
|
||||||
|
|
||||||
categories.map { category ->
|
categories.map { category ->
|
||||||
val rules = rulesByCategoryId[category.id] ?: emptyList()
|
val rules = rulesByCategoryId[category.id] ?: emptyList()
|
||||||
val usedTimeItemsForCategory = usedTimesByCategory[category.id]
|
val usedTimeItemsForCategory = usedTimesByCategory[category.id]
|
||||||
?: emptyList()
|
?: emptyList()
|
||||||
val parentCategory = categories.find { it.id == category.parentCategoryId }
|
|
||||||
|
|
||||||
TimesWidgetItem(
|
TimesWidgetItem(
|
||||||
title = category.title,
|
title = category.title,
|
||||||
remainingTimeToday = RemainingTime.getRemainingTime(
|
remainingTimeToday = RemainingTime.getRemainingTime(
|
||||||
dayOfWeek = childDate.dayOfWeek,
|
dayOfWeek = childDate.dayOfWeek,
|
||||||
usedTimes = SparseLongArray().apply {
|
usedTimes = usedTimeItemsForCategory,
|
||||||
usedTimeItemsForCategory.forEach { usedTimeItem ->
|
rules = rules,
|
||||||
|
extraTime = category.getExtraTime(dayOfEpoch = childDate.dayOfEpoch),
|
||||||
val dayOfWeek = usedTimeItem.dayOfEpoch - firstDayOfWeek
|
minuteOfDay = timeLimitSlot,
|
||||||
|
firstDayOfWeekAsEpochDay = firstDayOfWeek
|
||||||
put(dayOfWeek, usedTimeItem.usedMillis)
|
)?.includingExtraTime
|
||||||
}
|
)
|
||||||
},
|
}
|
||||||
rules = rules,
|
|
||||||
extraTime = category.getExtraTime(dayOfEpoch = childDate.dayOfEpoch)
|
|
||||||
)?.includingExtraTime
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -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 {
|
fun used(time: Int, context: Context): String {
|
||||||
return if (time <= 0) {
|
return if (time <= 0) {
|
||||||
context.resources.getString(R.string.util_time_unused)
|
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"?>
|
<?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
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
the Free Software Foundation version 3 of the License.
|
the Free Software Foundation version 3 of the License.
|
||||||
|
@ -34,6 +34,14 @@
|
||||||
name="daysString"
|
name="daysString"
|
||||||
type="String" />
|
type="String" />
|
||||||
|
|
||||||
|
<variable
|
||||||
|
name="timeAreaString"
|
||||||
|
type="String" />
|
||||||
|
|
||||||
|
<variable
|
||||||
|
name="sessionLimitString"
|
||||||
|
type="String" />
|
||||||
|
|
||||||
<variable
|
<variable
|
||||||
name="appliesToExtraTime"
|
name="appliesToExtraTime"
|
||||||
type="Boolean" />
|
type="Boolean" />
|
||||||
|
@ -72,6 +80,22 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content" />
|
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
|
<TextView
|
||||||
android:visibility="@{safeUnbox(appliesToExtraTime) ? View.VISIBLE : View.GONE}"
|
android:visibility="@{safeUnbox(appliesToExtraTime) ? View.VISIBLE : View.GONE}"
|
||||||
android:textAppearance="?android:textAppearanceSmall"
|
android:textAppearance="?android:textAppearanceSmall"
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
-->
|
-->
|
||||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
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">
|
tools:context="io.timelimit.android.ui.manage.category.timelimit_rules.edit.EditTimeLimitRuleDialogFragment">
|
||||||
|
|
||||||
<data>
|
<data>
|
||||||
|
@ -25,6 +26,10 @@
|
||||||
name="applyToExtraTime"
|
name="applyToExtraTime"
|
||||||
type="Boolean" />
|
type="Boolean" />
|
||||||
|
|
||||||
|
<variable
|
||||||
|
name="applyToWholeDay"
|
||||||
|
type="Boolean" />
|
||||||
|
|
||||||
<variable
|
<variable
|
||||||
name="handlers"
|
name="handlers"
|
||||||
type="io.timelimit.android.ui.manage.category.timelimit_rules.edit.Handlers" />
|
type="io.timelimit.android.ui.manage.category.timelimit_rules.edit.Handlers" />
|
||||||
|
@ -33,74 +38,223 @@
|
||||||
name="affectsMultipleDays"
|
name="affectsMultipleDays"
|
||||||
type="boolean" />
|
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" />
|
<import type="android.view.View" />
|
||||||
</data>
|
</data>
|
||||||
|
|
||||||
<LinearLayout
|
<androidx.core.widget.NestedScrollView
|
||||||
android:padding="8dp"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content"
|
||||||
|
android:id="@+id/scroll">
|
||||||
<TextView
|
|
||||||
android:textAppearance="?android:textAppearanceLarge"
|
|
||||||
tools:text="@string/category_time_limit_rule_dialog_new"
|
|
||||||
android:text="@{safeUnbox(isNewRule) ? @string/category_time_limit_rule_dialog_new : @string/category_time_limit_rule_dialog_edit}"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<include layout="@layout/view_select_days" android:id="@+id/day_selection" />
|
|
||||||
|
|
||||||
<io.timelimit.android.ui.view.SelectTimeSpanView
|
|
||||||
android:id="@+id/time_span"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
|
||||||
android:onClick="@{() -> handlers.updateApplyToExtraTime(!safeUnbox(applyToExtraTime))}"
|
|
||||||
android:checked="@{safeUnbox(applyToExtraTime)}"
|
|
||||||
android:text="@string/category_time_limit_rules_apply_to_extra_time"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:visibility="@{affectsMultipleDays ? View.VISIBLE : View.GONE}"
|
|
||||||
android:textAppearance="?android:textAppearanceSmall"
|
|
||||||
android:text="@string/category_time_limit_rules_warning_multiple_days"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:orientation="horizontal"
|
android:padding="8dp"
|
||||||
|
android:orientation="vertical"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
<View
|
<TextView
|
||||||
android:layout_weight="1"
|
android:textAppearance="?android:textAppearanceLarge"
|
||||||
android:layout_width="0dp"
|
tools:text="@string/category_time_limit_rule_dialog_new"
|
||||||
android:layout_height="0dp" />
|
android:text="@{safeUnbox(isNewRule) ? @string/category_time_limit_rule_dialog_new : @string/category_time_limit_rule_dialog_edit}"
|
||||||
|
android:layout_width="match_parent"
|
||||||
<Button
|
|
||||||
android:layout_marginEnd="4dp"
|
|
||||||
style="?borderlessButtonStyle"
|
|
||||||
android:onClick="@{() -> handlers.onDeleteRule()}"
|
|
||||||
android:visibility="@{safeUnbox(isNewRule) ? View.GONE : View.VISIBLE}"
|
|
||||||
android:textColor="@color/text_red"
|
|
||||||
android:text="@string/generic_delete"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
<Button
|
<include layout="@layout/view_select_days" android:id="@+id/day_selection" />
|
||||||
android:layout_marginEnd="4dp"
|
|
||||||
android:layout_marginStart="4dp"
|
<io.timelimit.android.ui.view.SelectTimeSpanView
|
||||||
android:onClick="@{() -> handlers.onSaveRule()}"
|
android:id="@+id/time_span"
|
||||||
tools:text="@string/generic_create"
|
android:layout_width="match_parent"
|
||||||
android:text="@{safeUnbox(isNewRule) ? @string/generic_create : @string/generic_save}"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||||
|
android:onClick="@{() -> handlers.updateApplyToExtraTime(!safeUnbox(applyToExtraTime))}"
|
||||||
|
android:checked="@{safeUnbox(applyToExtraTime)}"
|
||||||
|
android:text="@string/category_time_limit_rules_apply_to_extra_time"
|
||||||
|
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: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"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
style="?borderlessButtonStyle"
|
||||||
|
android:onClick="@{() -> handlers.onDeleteRule()}"
|
||||||
|
android:visibility="@{safeUnbox(isNewRule) ? View.GONE : View.VISIBLE}"
|
||||||
|
android:textColor="@color/text_red"
|
||||||
|
android:text="@string/generic_delete"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
android:onClick="@{() -> handlers.onSaveRule()}"
|
||||||
|
tools:text="@string/generic_create"
|
||||||
|
android:text="@{safeUnbox(isNewRule) ? @string/generic_create : @string/generic_save}"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
</androidx.core.widget.NestedScrollView>
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</layout>
|
</layout>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
the Free Software Foundation version 3 of the License.
|
the Free Software Foundation version 3 of the License.
|
||||||
|
@ -20,9 +20,16 @@
|
||||||
name="date"
|
name="date"
|
||||||
type="String" />
|
type="String" />
|
||||||
|
|
||||||
|
<variable
|
||||||
|
name="timeArea"
|
||||||
|
type="String" />
|
||||||
|
|
||||||
<variable
|
<variable
|
||||||
name="usedTime"
|
name="usedTime"
|
||||||
type="String" />
|
type="String" />
|
||||||
|
|
||||||
|
<import type="android.text.TextUtils" />
|
||||||
|
<import type="android.view.View" />
|
||||||
</data>
|
</data>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
@ -38,6 +45,14 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content" />
|
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
|
<TextView
|
||||||
android:textAppearance="?android:textAppearanceMedium"
|
android:textAppearance="?android:textAppearanceMedium"
|
||||||
android:text="@{usedTime}"
|
android:text="@{usedTime}"
|
||||||
|
|
|
@ -275,6 +275,20 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content" />
|
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
|
<ProgressBar
|
||||||
android:visibility="@{reason == null ? View.VISIBLE : View.GONE}"
|
android:visibility="@{reason == null ? View.VISIBLE : View.GONE}"
|
||||||
android:padding="8dp"
|
android:padding="8dp"
|
||||||
|
@ -513,7 +527,7 @@
|
||||||
</androidx.cardview.widget.CardView>
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
||||||
<io.timelimit.android.ui.view.ManageDisableTimelimitsView
|
<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:id="@+id/manage_disable_time_limits"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content" />
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content">
|
||||||
<HorizontalScrollView
|
<HorizontalScrollView
|
||||||
android:layout_centerHorizontal="true"
|
android:layout_centerHorizontal="true"
|
||||||
android:id="@+id/scroll"
|
android:id="@+id/select_days_scroll"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content">
|
||||||
<com.google.android.material.button.MaterialButtonToggleGroup
|
<com.google.android.material.button.MaterialButtonToggleGroup
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
the Free Software Foundation version 3 of the License.
|
the Free Software Foundation version 3 of the License.
|
||||||
|
@ -23,9 +23,19 @@
|
||||||
Regeln, bei denen nur ein Tag gewählt wurde, begrenzen nur diesen Tag.
|
Regeln, bei denen nur ein Tag gewählt wurde, begrenzen nur diesen Tag.
|
||||||
</string>
|
</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_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_new">Regel erstellen</string>
|
||||||
<string name="category_time_limit_rule_dialog_edit">Regel bearbeiten</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_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_created">Regel wurde erstellt</string>
|
||||||
<string name="category_time_limit_rules_snackbar_updated">Regel wurde geändert</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.
|
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.
|
Wenn Sei die Begrenzung je Tag wollen, dann erstellen Sie eine Regel je Tag.
|
||||||
</string>
|
</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>
|
</resources>
|
||||||
|
|
|
@ -80,6 +80,10 @@
|
||||||
und dieses Akkulimit wurde erreicht.
|
und dieses Akkulimit wurde erreicht.
|
||||||
Zum Wiederfreigeben muss der Akku aufgeladen werden.
|
Zum Wiederfreigeben muss der Akku aufgeladen werden.
|
||||||
</string>
|
</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_no_category">keine Kategorie</string>
|
||||||
<string name="lock_reason_short_temporarily_blocked">vorübergehend gesperrt</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_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_notification_blocking">alle Benachrichtigungen werden blockiert</string>
|
||||||
<string name="lock_reason_short_battery_limit">Akkulimit unterschritten</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">
|
<string name="lock_overlay_warning">
|
||||||
Öffnen des Sperrbildschirms fehlgeschlagen.
|
Öffnen des Sperrbildschirms fehlgeschlagen.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
the Free Software Foundation version 3 of the License.
|
the Free Software Foundation version 3 of the License.
|
||||||
|
@ -20,4 +20,7 @@
|
||||||
Die Nutzungszeiten werden nur erfasst,
|
Die Nutzungszeiten werden nur erfasst,
|
||||||
wenn Zeitbegrenzungsregeln existieren und die zeitbegrenzten Apps genutzt werden
|
wenn Zeitbegrenzungsregeln existieren und die zeitbegrenzten Apps genutzt werden
|
||||||
</string>
|
</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>
|
</resources>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
the Free Software Foundation version 3 of the License.
|
the Free Software Foundation version 3 of the License.
|
||||||
|
@ -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_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_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_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_unused">Unbenutzt</string>
|
||||||
<string name="util_time_used"><xliff:g example="4 minutes" id="used time">%1$s</xliff:g> benutzt</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"?>
|
<?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
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
the Free Software Foundation version 3 of the License.
|
the Free Software Foundation version 3 of the License.
|
||||||
|
@ -23,9 +23,19 @@
|
||||||
Rules at which only one day is checked only limit the selected day.
|
Rules at which only one day is checked only limit the selected day.
|
||||||
</string>
|
</string>
|
||||||
<string name="category_time_limit_rules_applied_to_extra_time">is applied to extra time, too</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_new">Create rule</string>
|
||||||
<string name="category_time_limit_rule_dialog_edit">Edit 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_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_created">Rule was created</string>
|
||||||
<string name="category_time_limit_rules_snackbar_updated">Rule was modified</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.
|
will limit the total usage duration during one week at the selected days.
|
||||||
If you want it per day, create one rule per day.
|
If you want it per day, create one rule per day.
|
||||||
</string>
|
</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>
|
</resources>
|
||||||
|
|
|
@ -84,6 +84,11 @@
|
||||||
battery limit was reached.
|
battery limit was reached.
|
||||||
To unlock it, charge the battery.
|
To unlock it, charge the battery.
|
||||||
</string>
|
</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_no_category">no category</string>
|
||||||
<string name="lock_reason_short_temporarily_blocked">temporarily blocked</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_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_notification_blocking">all notifications are blocked</string>
|
||||||
<string name="lock_reason_short_battery_limit">battery limit reached</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">
|
<string name="lock_overlay_warning">
|
||||||
Failed to open the lock screen.
|
Failed to open the lock screen.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
the Free Software Foundation version 3 of the License.
|
the Free Software Foundation version 3 of the License.
|
||||||
|
@ -21,4 +21,7 @@
|
||||||
when there are time limits and
|
when there are time limits and
|
||||||
the limited Apps are used
|
the limited Apps are used
|
||||||
</string>
|
</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>
|
</resources>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
the Free Software Foundation version 3 of the License.
|
the Free Software Foundation version 3 of the License.
|
||||||
|
@ -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_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_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_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_unused">Unused</string>
|
||||||
<string name="util_time_used">Used <xliff:g example="4 minutes" id="used time">%1$s</xliff:g></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