Add option for daily rules

This commit is contained in:
Jonas Lochmann 2020-11-23 01:00:00 +01:00
parent 4ddd79bc94
commit 8fb9f496aa
No known key found for this signature in database
GPG key ID: 8B8C9AEE10FA5B36
19 changed files with 1375 additions and 149 deletions

File diff suppressed because it is too large Load diff

View file

@ -250,4 +250,10 @@ object DatabaseMigrations {
database.execSQL("ALTER TABLE `category` ADD COLUMN `tasks_version` TEXT NOT NULL DEFAULT ''") database.execSQL("ALTER TABLE `category` ADD COLUMN `tasks_version` TEXT NOT NULL DEFAULT ''")
} }
} }
val MIGRATE_TO_V35 = object: Migration(34, 35) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `time_limit_rule` ADD COLUMN `per_day` INTEGER NOT NULL DEFAULT 0")
}
}
} }

View file

@ -50,7 +50,7 @@ import java.util.concurrent.TimeUnit
UserLimitLoginCategory::class, UserLimitLoginCategory::class,
CategoryNetworkId::class, CategoryNetworkId::class,
ChildTask::class ChildTask::class
], version = 34) ], version = 35)
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()
@ -118,7 +118,8 @@ abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database
DatabaseMigrations.MIGRATE_TO_V31, DatabaseMigrations.MIGRATE_TO_V31,
DatabaseMigrations.MIGRATE_TO_V32, DatabaseMigrations.MIGRATE_TO_V32,
DatabaseMigrations.MIGRATE_TO_V33, DatabaseMigrations.MIGRATE_TO_V33,
DatabaseMigrations.MIGRATE_TO_V34 DatabaseMigrations.MIGRATE_TO_V34,
DatabaseMigrations.MIGRATE_TO_V35
) )
.setQueryExecutor(Threads.database) .setQueryExecutor(Threads.database)
.build() .build()

View file

@ -211,7 +211,7 @@ object HintsToShow {
const val CATEGORIES_INTRODUCTION = 4L const val CATEGORIES_INTRODUCTION = 4L
const val TIME_LIMIT_RULE_INTRODUCTION = 8L const val TIME_LIMIT_RULE_INTRODUCTION = 8L
const val CONTACTS_INTRO = 16L const val CONTACTS_INTRO = 16L
const val TIMELIMIT_RULE_MUSTREAD = 32L private const val OBSOLETE_TIMELIMIT_RULE_MUSTREAD = 32L
const val BLOCKED_TIME_AREAS_OBSOLETE = 64L const val BLOCKED_TIME_AREAS_OBSOLETE = 64L
const val TASKS_INTRODUCTION = 128L const val TASKS_INTRODUCTION = 128L
} }

View file

@ -53,7 +53,9 @@ data class TimeLimitRule(
@ColumnInfo(name = "session_duration_milliseconds") @ColumnInfo(name = "session_duration_milliseconds")
val sessionDurationMilliseconds: Int, val sessionDurationMilliseconds: Int,
@ColumnInfo(name = "session_pause_milliseconds") @ColumnInfo(name = "session_pause_milliseconds")
val sessionPauseMilliseconds: Int val sessionPauseMilliseconds: Int,
@ColumnInfo(name = "per_day")
val perDay: Boolean
): Parcelable, JsonSerializable { ): Parcelable, JsonSerializable {
companion object { companion object {
private const val RULE_ID = "ruleId" private const val RULE_ID = "ruleId"
@ -65,6 +67,7 @@ data class TimeLimitRule(
private const val END_MINUTE_OF_DAY = "end" private const val END_MINUTE_OF_DAY = "end"
private const val SESSION_DURATION_MILLISECONDS = "dur" private const val SESSION_DURATION_MILLISECONDS = "dur"
private const val SESSION_PAUSE_MILLISECONDS = "pause" private const val SESSION_PAUSE_MILLISECONDS = "pause"
private const val PER_DAY = "perDay"
const val MIN_START_MINUTE = MinuteOfDay.MIN const val MIN_START_MINUTE = MinuteOfDay.MIN
const val MAX_END_MINUTE = MinuteOfDay.MAX const val MAX_END_MINUTE = MinuteOfDay.MAX
@ -79,6 +82,7 @@ data class TimeLimitRule(
var endMinuteOfDay = MAX_END_MINUTE var endMinuteOfDay = MAX_END_MINUTE
var sessionDurationMilliseconds = 0 var sessionDurationMilliseconds = 0
var sessionPauseMilliseconds = 0 var sessionPauseMilliseconds = 0
var perDay = false
reader.beginObject() reader.beginObject()
@ -93,6 +97,7 @@ data class TimeLimitRule(
END_MINUTE_OF_DAY -> endMinuteOfDay = reader.nextInt() END_MINUTE_OF_DAY -> endMinuteOfDay = reader.nextInt()
SESSION_DURATION_MILLISECONDS -> sessionDurationMilliseconds = reader.nextInt() SESSION_DURATION_MILLISECONDS -> sessionDurationMilliseconds = reader.nextInt()
SESSION_PAUSE_MILLISECONDS -> sessionPauseMilliseconds = reader.nextInt() SESSION_PAUSE_MILLISECONDS -> sessionPauseMilliseconds = reader.nextInt()
PER_DAY -> perDay = reader.nextBoolean()
else -> reader.skipValue() else -> reader.skipValue()
} }
} }
@ -108,7 +113,8 @@ data class TimeLimitRule(
startMinuteOfDay = startMinuteOfDay, startMinuteOfDay = startMinuteOfDay,
endMinuteOfDay = endMinuteOfDay, endMinuteOfDay = endMinuteOfDay,
sessionDurationMilliseconds = sessionDurationMilliseconds, sessionDurationMilliseconds = sessionDurationMilliseconds,
sessionPauseMilliseconds = sessionPauseMilliseconds sessionPauseMilliseconds = sessionPauseMilliseconds,
perDay = perDay
) )
} }
} }
@ -159,6 +165,8 @@ data class TimeLimitRule(
writer.name(SESSION_PAUSE_MILLISECONDS).value(sessionPauseMilliseconds) writer.name(SESSION_PAUSE_MILLISECONDS).value(sessionPauseMilliseconds)
} }
if (perDay) writer.name(PER_DAY).value(true)
writer.endObject() writer.endObject()
} }
} }

View file

@ -20,6 +20,8 @@ import org.threeten.bp.LocalDate
import java.util.* import java.util.*
data class DateInTimezone(val dayOfWeek: Int, val dayOfEpoch: Int, val localDate: LocalDate) { data class DateInTimezone(val dayOfWeek: Int, val dayOfEpoch: Int, val localDate: LocalDate) {
val firstDayOfWeekAsEpochDay get() = dayOfEpoch - dayOfWeek
companion object { companion object {
fun convertDayOfWeek(dayOfWeek: Int) = when(dayOfWeek) { fun convertDayOfWeek(dayOfWeek: Int) = when(dayOfWeek) {
Calendar.MONDAY -> 0 Calendar.MONDAY -> 0

View file

@ -57,8 +57,8 @@ data class RemainingTime(val includingExtraTime: Long, val default: Long) {
} }
val relatedRules = getRulesRelatedToDay(dayOfWeek, minuteOfDay, rules) val relatedRules = getRulesRelatedToDay(dayOfWeek, minuteOfDay, rules)
val withoutExtraTime = getRemainingTime(usedTimes, relatedRules, false, firstDayOfWeekAsEpochDay) val withoutExtraTime = getRemainingTime(usedTimes, relatedRules, false, firstDayOfWeekAsEpochDay, dayOfWeek)
val withExtraTime = getRemainingTime(usedTimes, relatedRules, true, firstDayOfWeekAsEpochDay) val withExtraTime = getRemainingTime(usedTimes, relatedRules, true, firstDayOfWeekAsEpochDay, dayOfWeek)
if (withoutExtraTime == null && withExtraTime == null) { if (withoutExtraTime == null && withExtraTime == null) {
// no rules // no rules
@ -86,15 +86,21 @@ data class RemainingTime(val includingExtraTime: Long, val default: Long) {
} }
} }
private fun getRemainingTime(usedTimes: List<UsedTimeItem>, relatedRules: List<TimeLimitRule>, assumeMaximalExtraTime: Boolean, firstDayOfWeekAsEpochDay: Int): Long? { private fun getRemainingTime(usedTimes: List<UsedTimeItem>, relatedRules: List<TimeLimitRule>, assumeMaximalExtraTime: Boolean, firstDayOfWeekAsEpochDay: Int, dayOfWeek: Int): Long? {
return relatedRules.filter { (!assumeMaximalExtraTime) || it.applyToExtraTimeUsage }.map { rule -> return relatedRules.filter { (!assumeMaximalExtraTime) || it.applyToExtraTimeUsage }.map { rule ->
var usedTime = 0L var usedTime = 0L
usedTimes.forEach { usedTimeItem -> usedTimes.forEach { usedTimeItem ->
if (usedTimeItem.dayOfEpoch >= firstDayOfWeekAsEpochDay && usedTimeItem.dayOfEpoch <= firstDayOfWeekAsEpochDay + 6) { val doesWeekMatch = usedTimeItem.dayOfEpoch >= firstDayOfWeekAsEpochDay && usedTimeItem.dayOfEpoch <= firstDayOfWeekAsEpochDay + 6
val usedTimeItemDayOfWeek = usedTimeItem.dayOfEpoch - firstDayOfWeekAsEpochDay
if ((rule.dayMask.toInt() and (1 shl usedTimeItemDayOfWeek)) != 0) { if (doesWeekMatch) {
val usedTimeItemDayOfWeek = usedTimeItem.dayOfEpoch - firstDayOfWeekAsEpochDay
val doesDayMaskMatch = (rule.dayMask.toInt() and (1 shl usedTimeItemDayOfWeek)) != 0
val doesCurrentDayMatch = dayOfWeek == usedTimeItemDayOfWeek
val usedTimeItemMatching = if (rule.perDay) doesCurrentDayMatch else doesDayMaskMatch
if (usedTimeItemMatching) {
if (rule.startMinuteOfDay == usedTimeItem.startTimeOfDay && rule.endMinuteOfDay == usedTimeItem.endTimeOfDay) { if (rule.startMinuteOfDay == usedTimeItem.startTimeOfDay && rule.endMinuteOfDay == usedTimeItem.endTimeOfDay) {
usedTime += usedTimeItem.usedMillis usedTime += usedTimeItem.usedMillis
} }
@ -106,7 +112,7 @@ data class RemainingTime(val includingExtraTime: Long, val default: Long) {
val remaining = Math.max(0, maxTime - usedTime) val remaining = Math.max(0, maxTime - usedTime)
remaining remaining
}.min() }.minOrNull()
} }
} }
} }

View file

@ -1442,7 +1442,8 @@ data class CreateTimeLimitRuleAction(val rule: TimeLimitRule): ParentAction() {
data class UpdateTimeLimitRuleAction( data class UpdateTimeLimitRuleAction(
val ruleId: String, val dayMask: Byte, val maximumTimeInMillis: Int, val applyToExtraTimeUsage: Boolean, val ruleId: String, val dayMask: Byte, val maximumTimeInMillis: Int, val applyToExtraTimeUsage: Boolean,
val start: Int, val end: Int, val sessionDurationMilliseconds: Int, val sessionPauseMilliseconds: Int val start: Int, val end: Int, val sessionDurationMilliseconds: Int, val sessionPauseMilliseconds: Int,
val perDay: Boolean
): ParentAction() { ): ParentAction() {
companion object { companion object {
const val TYPE_VALUE = "UPDATE_TIMELIMIT_RULE" const val TYPE_VALUE = "UPDATE_TIMELIMIT_RULE"
@ -1454,6 +1455,7 @@ data class UpdateTimeLimitRuleAction(
private const val END = "end" private const val END = "end"
private const val SESSION_DURATION_MILLISECONDS = "dur" private const val SESSION_DURATION_MILLISECONDS = "dur"
private const val SESSION_PAUSE_MILLISECONDS = "pause" private const val SESSION_PAUSE_MILLISECONDS = "pause"
private const val PER_DAY = "perDay"
} }
init { init {
@ -1492,6 +1494,8 @@ data class UpdateTimeLimitRuleAction(
writer.name(SESSION_PAUSE_MILLISECONDS).value(sessionPauseMilliseconds) writer.name(SESSION_PAUSE_MILLISECONDS).value(sessionPauseMilliseconds)
} }
if (perDay) writer.name(PER_DAY).value(true)
writer.endObject() writer.endObject()
} }
} }

View file

@ -315,7 +315,8 @@ object LocalDatabaseParentActionDispatcher {
startMinuteOfDay = action.start, startMinuteOfDay = action.start,
endMinuteOfDay = action.end, endMinuteOfDay = action.end,
sessionDurationMilliseconds = action.sessionDurationMilliseconds, sessionDurationMilliseconds = action.sessionDurationMilliseconds,
sessionPauseMilliseconds = action.sessionPauseMilliseconds sessionPauseMilliseconds = action.sessionPauseMilliseconds,
perDay = action.perDay
)) ))
} }
is SetDeviceUserAction -> { is SetDeviceUserAction -> {

View file

@ -821,7 +821,8 @@ data class ServerTimeLimitRule(
val startMinuteOfDay: Int, val startMinuteOfDay: Int,
val endMinuteOfDay: Int, val endMinuteOfDay: Int,
val sessionDurationMilliseconds: Int, val sessionDurationMilliseconds: Int,
val sessionPauseMilliseconds: Int val sessionPauseMilliseconds: Int,
val perDay: Boolean
) { ) {
companion object { companion object {
private const val ID = "id" private const val ID = "id"
@ -832,6 +833,7 @@ data class ServerTimeLimitRule(
private const val END_MINUTE_OF_DAY = "end" private const val END_MINUTE_OF_DAY = "end"
private const val SESSION_DURATION_MILLISECONDS = "session" private const val SESSION_DURATION_MILLISECONDS = "session"
private const val SESSION_PAUSE_MILLISECONDS = "pause" private const val SESSION_PAUSE_MILLISECONDS = "pause"
private const val PER_DAY = "perDay"
fun parse(reader: JsonReader): ServerTimeLimitRule { fun parse(reader: JsonReader): ServerTimeLimitRule {
var id: String? = null var id: String? = null
@ -842,6 +844,7 @@ data class ServerTimeLimitRule(
var endMinuteOfDay = TimeLimitRule.MAX_END_MINUTE var endMinuteOfDay = TimeLimitRule.MAX_END_MINUTE
var sessionDurationMilliseconds: Int = 0 var sessionDurationMilliseconds: Int = 0
var sessionPauseMilliseconds: Int = 0 var sessionPauseMilliseconds: Int = 0
var perDay = false
reader.beginObject() reader.beginObject()
while (reader.hasNext()) { while (reader.hasNext()) {
@ -854,6 +857,7 @@ data class ServerTimeLimitRule(
END_MINUTE_OF_DAY -> endMinuteOfDay = reader.nextInt() END_MINUTE_OF_DAY -> endMinuteOfDay = reader.nextInt()
SESSION_DURATION_MILLISECONDS -> sessionDurationMilliseconds = reader.nextInt() SESSION_DURATION_MILLISECONDS -> sessionDurationMilliseconds = reader.nextInt()
SESSION_PAUSE_MILLISECONDS -> sessionPauseMilliseconds = reader.nextInt() SESSION_PAUSE_MILLISECONDS -> sessionPauseMilliseconds = reader.nextInt()
PER_DAY -> perDay = reader.nextBoolean()
else -> reader.skipValue() else -> reader.skipValue()
} }
} }
@ -867,7 +871,8 @@ data class ServerTimeLimitRule(
startMinuteOfDay = startMinuteOfDay, startMinuteOfDay = startMinuteOfDay,
endMinuteOfDay = endMinuteOfDay, endMinuteOfDay = endMinuteOfDay,
sessionDurationMilliseconds = sessionDurationMilliseconds, sessionDurationMilliseconds = sessionDurationMilliseconds,
sessionPauseMilliseconds = sessionPauseMilliseconds sessionPauseMilliseconds = sessionPauseMilliseconds,
perDay = perDay
) )
} }
@ -893,7 +898,8 @@ data class ServerTimeLimitRule(
startMinuteOfDay = startMinuteOfDay, startMinuteOfDay = startMinuteOfDay,
endMinuteOfDay = endMinuteOfDay, endMinuteOfDay = endMinuteOfDay,
sessionDurationMilliseconds = sessionDurationMilliseconds, sessionDurationMilliseconds = sessionDurationMilliseconds,
sessionPauseMilliseconds = sessionPauseMilliseconds sessionPauseMilliseconds = sessionPauseMilliseconds,
perDay = perDay
) )
} }

View file

@ -23,6 +23,7 @@ import androidx.recyclerview.widget.RecyclerView
import io.timelimit.android.R import io.timelimit.android.R
import io.timelimit.android.data.model.UsedTimeItem import io.timelimit.android.data.model.UsedTimeItem
import io.timelimit.android.databinding.* import io.timelimit.android.databinding.*
import io.timelimit.android.date.DateInTimezone
import io.timelimit.android.extensions.MinuteOfDay import io.timelimit.android.extensions.MinuteOfDay
import io.timelimit.android.logic.DefaultAppLogic import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.logic.DummyApps import io.timelimit.android.logic.DummyApps
@ -46,7 +47,7 @@ class AppAndRuleAdapter: RecyclerView.Adapter<AppAndRuleAdapter.Holder>() {
var items: List<AppAndRuleItem> by Delegates.observable(emptyList()) { _, _, _ -> notifyDataSetChanged() } var items: List<AppAndRuleItem> by Delegates.observable(emptyList()) { _, _, _ -> notifyDataSetChanged() }
var usedTimes: List<UsedTimeItem> by Delegates.observable(emptyList()) { _, _, _ -> notifyDataSetChanged() } var usedTimes: List<UsedTimeItem> by Delegates.observable(emptyList()) { _, _, _ -> notifyDataSetChanged() }
var epochDayOfStartOfWeek: Int by Delegates.observable(0) { _, _, _ -> notifyDataSetChanged() } var date: DateInTimezone? by Delegates.observable(null as DateInTimezone?) { _, _, _ -> notifyDataSetChanged() }
var handlers: Handlers? = null var handlers: Handlers? = null
init { init {
@ -153,13 +154,29 @@ class AppAndRuleAdapter: RecyclerView.Adapter<AppAndRuleAdapter.Holder>() {
val binding = holder.itemView.tag as FragmentCategoryTimeLimitRuleItemBinding val binding = holder.itemView.tag as FragmentCategoryTimeLimitRuleItemBinding
val context = binding.root.context 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.filter { usedTime -> val usedTime = date?.let { date ->
val dayOfWeek = usedTime.dayOfEpoch - epochDayOfStartOfWeek usedTimes.filter { usedTime ->
usedTime.startTimeOfDay == rule.startMinuteOfDay && usedTime.endTimeOfDay == rule.endMinuteOfDay && val dayOfWeek = usedTime.dayOfEpoch - date.firstDayOfWeekAsEpochDay
(rule.dayMask.toInt() and (1 shl dayOfWeek) != 0) val matchingSlot = usedTime.startTimeOfDay == rule.startMinuteOfDay && usedTime.endTimeOfDay == rule.endMinuteOfDay
}.map { it.usedMillis }.sum().toInt() val matchingMask = (rule.dayMask.toInt() and (1 shl dayOfWeek) != 0)
val matchingDay = dayOfWeek == date.dayOfWeek
binding.maxTimeString = TimeTextUtil.time(rule.maximumTimeInMillis, context) matchingSlot && (if (rule.perDay) matchingDay else matchingMask)
}.map { it.usedMillis }.sum().toInt()
} ?: 0
binding.maxTimeString = rule.maximumTimeInMillis.let { time ->
val timeString = TimeTextUtil.time(time, context)
val weeklyDailyString = context.getString(
if (rule.perDay) R.string.category_time_limit_rules_per_day
else R.string.category_time_limit_rules_per_week
)
val zeroTime = time == 0
val onlySingleDay = rule.dayMask.countOneBits() <= 1
val hideDailyWeekly = zeroTime || onlySingleDay
if (hideDailyWeekly) timeString else "$weeklyDailyString $timeString"
}
binding.usageAsText = if (rule.maximumTimeInMillis > 0) TimeTextUtil.used(usedTime, context) else null binding.usageAsText = if (rule.maximumTimeInMillis > 0) TimeTextUtil.used(usedTime, context) else null
binding.usageProgressInPercent = if (rule.maximumTimeInMillis > 0) binding.usageProgressInPercent = if (rule.maximumTimeInMillis > 0)
(usedTime * 100 / rule.maximumTimeInMillis) (usedTime * 100 / rule.maximumTimeInMillis)

View file

@ -44,16 +44,14 @@ class AppsAndRulesModel(application: Application): AndroidViewModel(application)
private val userDayOfWeek = userDate.map { it.dayOfWeek } private val userDayOfWeek = userDate.map { it.dayOfWeek }
val firstDayOfWeekAndUsedTimes = userDate.switchMap { date -> val dateAndUsedTimes = userDate.switchMap { date ->
val firstDayOfWeekAsEpochDay = date.dayOfEpoch - date.dayOfWeek
categoryIdLive.switchMap { categoryId -> categoryIdLive.switchMap { categoryId ->
database.usedTimes().getUsedTimesOfWeek( database.usedTimes().getUsedTimesOfWeek(
categoryId = categoryId, categoryId = categoryId,
firstDayOfWeekAsEpochDay = firstDayOfWeekAsEpochDay firstDayOfWeekAsEpochDay = date.firstDayOfWeekAsEpochDay
) )
}.map { usedTimes -> }.map { usedTimes ->
firstDayOfWeekAsEpochDay to usedTimes date to usedTimes
} }
} }

View file

@ -63,8 +63,8 @@ abstract class CategoryAppsAndRulesFragment: Fragment(), Handlers, EditTimeLimit
recycler.layoutManager = LinearLayoutManager(requireContext()) recycler.layoutManager = LinearLayoutManager(requireContext())
recycler.adapter = adapter recycler.adapter = adapter
model.firstDayOfWeekAndUsedTimes.observe(viewLifecycleOwner) { (firstDayOfWeek, usedTimes) -> model.dateAndUsedTimes.observe(viewLifecycleOwner) { (date, usedTimes) ->
adapter.epochDayOfStartOfWeek = firstDayOfWeek adapter.date = date
adapter.usedTimes = usedTimes adapter.usedTimes = usedTimes
} }
@ -124,7 +124,8 @@ abstract class CategoryAppsAndRulesFragment: Fragment(), Handlers, EditTimeLimit
start = oldRule.startMinuteOfDay, start = oldRule.startMinuteOfDay,
end = oldRule.endMinuteOfDay, end = oldRule.endMinuteOfDay,
sessionDurationMilliseconds = oldRule.sessionDurationMilliseconds, sessionDurationMilliseconds = oldRule.sessionDurationMilliseconds,
sessionPauseMilliseconds = oldRule.sessionPauseMilliseconds sessionPauseMilliseconds = oldRule.sessionPauseMilliseconds,
perDay = oldRule.perDay
) )
) )
} }

View file

@ -28,22 +28,18 @@ 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.R
import io.timelimit.android.async.Threads import io.timelimit.android.async.Threads
import io.timelimit.android.coroutines.runAsync
import io.timelimit.android.data.IdGenerator import io.timelimit.android.data.IdGenerator
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.MinuteOfDay
import io.timelimit.android.extensions.showSafe import io.timelimit.android.extensions.showSafe
import io.timelimit.android.livedata.waitForNonNullValue
import io.timelimit.android.logic.DefaultAppLogic import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.sync.actions.CreateTimeLimitRuleAction import io.timelimit.android.sync.actions.CreateTimeLimitRuleAction
import io.timelimit.android.sync.actions.DeleteTimeLimitRuleAction import io.timelimit.android.sync.actions.DeleteTimeLimitRuleAction
import io.timelimit.android.sync.actions.UpdateTimeLimitRuleAction import io.timelimit.android.sync.actions.UpdateTimeLimitRuleAction
import io.timelimit.android.ui.main.ActivityViewModel import io.timelimit.android.ui.main.ActivityViewModel
import io.timelimit.android.ui.main.getActivityViewModel import io.timelimit.android.ui.main.getActivityViewModel
import io.timelimit.android.ui.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 io.timelimit.android.util.TimeTextUtil
@ -86,30 +82,14 @@ class EditTimeLimitRuleDialogFragment : BottomSheetDialogFragment(), DurationPic
if (existingRule != null) { if (existingRule != null) {
existingRule!!.categoryId existingRule!!.categoryId
} else { } else {
arguments!!.getString(PARAM_CATEGORY_ID)!! requireArguments().getString(PARAM_CATEGORY_ID)!!
} }
} }
private val auth: ActivityViewModel by lazy { getActivityViewModel(activity!!) } private val auth: ActivityViewModel by lazy { getActivityViewModel(requireActivity()) }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
val database = DefaultAppLogic.with(context!!).database
runAsync {
val wasShown = database.config().wereHintsShown(HintsToShow.TIMELIMIT_RULE_MUSTREAD).waitForNonNullValue()
if (!wasShown) {
MustReadFragment.newInstance(io.timelimit.android.R.string.must_read_timelimit_rules).show(fragmentManager!!)
Threads.database.execute {
database.config().setHintsShownSync(HintsToShow.TIMELIMIT_RULE_MUSTREAD)
}
}
}
}
existingRule = savedInstanceState?.getParcelable(PARAM_EXISTING_RULE) existingRule = savedInstanceState?.getParcelable(PARAM_EXISTING_RULE)
?: arguments?.getParcelable<TimeLimitRule?>(PARAM_EXISTING_RULE) ?: arguments?.getParcelable<TimeLimitRule?>(PARAM_EXISTING_RULE)
} }
@ -124,23 +104,28 @@ class EditTimeLimitRuleDialogFragment : BottomSheetDialogFragment(), DurationPic
) )
view.applyToExtraTime = newRule.applyToExtraTimeUsage view.applyToExtraTime = newRule.applyToExtraTimeUsage
val affectedDays = Math.max(0, (0..6).map { (newRule.dayMask.toInt() shr it) and 1 }.sum()) val affectedDays = (0..6).map { (newRule.dayMask.toInt() shr it) and 1 }.sum()
view.timeSpan.maxDays = Math.max(0, affectedDays - 1) // max prevents crash val maxDayDuration = if (newRule.perDay) 1 else affectedDays
view.timeSpan.maxDays = 0.coerceAtLeast(maxDayDuration - 1)
view.timeSpan.timeInMillis = newRule.maximumTimeInMillis.toLong() view.timeSpan.timeInMillis = newRule.maximumTimeInMillis.toLong()
view.affectsMultipleDays = affectedDays >= 2 view.showWeeklyDailyOption = affectedDays >= 2 && newRule.maximumTimeInMillis > 0
view.applyToWholeDay = newRule.appliesToWholeDay view.applyToWholeDay = newRule.appliesToWholeDay
view.startTime = MinuteOfDay.format(newRule.startMinuteOfDay) view.startTime = MinuteOfDay.format(newRule.startMinuteOfDay)
view.endTime = MinuteOfDay.format(newRule.endMinuteOfDay) view.endTime = MinuteOfDay.format(newRule.endMinuteOfDay)
view.enableSessionDurationLimit = newRule.sessionDurationLimitEnabled view.enableSessionDurationLimit = newRule.sessionDurationLimitEnabled
view.sessionBreakText = TimeTextUtil.minutes(newRule.sessionPauseMilliseconds / (1000 * 60), context!!) view.sessionBreakText = TimeTextUtil.minutes(newRule.sessionPauseMilliseconds / (1000 * 60), requireContext())
view.sessionLengthText = TimeTextUtil.minutes(newRule.sessionDurationMilliseconds / (1000 * 60), context!!) view.sessionLengthText = TimeTextUtil.minutes(newRule.sessionDurationMilliseconds / (1000 * 60), requireContext())
view.typePerDay.isChecked = newRule.perDay
view.typePerWeek.isChecked = !newRule.perDay
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val listener = targetFragment as EditTimeLimitRuleDialogFragmentListener val listener = targetFragment as EditTimeLimitRuleDialogFragmentListener
val database = DefaultAppLogic.with(context!!).database val database = DefaultAppLogic.with(requireContext()).database
view = FragmentEditTimeLimitRuleDialogBinding.inflate(layoutInflater, container, false) view = FragmentEditTimeLimitRuleDialogBinding.inflate(layoutInflater, container, false)
@ -162,7 +147,8 @@ class EditTimeLimitRuleDialogFragment : BottomSheetDialogFragment(), DurationPic
startMinuteOfDay = TimeLimitRule.MIN_START_MINUTE, startMinuteOfDay = TimeLimitRule.MIN_START_MINUTE,
endMinuteOfDay = TimeLimitRule.MAX_END_MINUTE, endMinuteOfDay = TimeLimitRule.MAX_END_MINUTE,
sessionPauseMilliseconds = 0, sessionPauseMilliseconds = 0,
sessionDurationMilliseconds = 0 sessionDurationMilliseconds = 0,
perDay = true
) )
} else { } else {
view.isNewRule = false view.isNewRule = false
@ -278,7 +264,8 @@ class EditTimeLimitRuleDialogFragment : BottomSheetDialogFragment(), DurationPic
start = newRule.startMinuteOfDay, start = newRule.startMinuteOfDay,
end = newRule.endMinuteOfDay, end = newRule.endMinuteOfDay,
sessionDurationMilliseconds = newRule.sessionDurationMilliseconds, sessionDurationMilliseconds = newRule.sessionDurationMilliseconds,
sessionPauseMilliseconds = newRule.sessionPauseMilliseconds sessionPauseMilliseconds = newRule.sessionPauseMilliseconds,
perDay = newRule.perDay
) )
)) { )) {
return return
@ -333,6 +320,9 @@ class EditTimeLimitRuleDialogFragment : BottomSheetDialogFragment(), DurationPic
} }
} }
view.typePerDay.setOnClickListener { newRule = newRule.copy(perDay = true); bindRule() }
view.typePerWeek.setOnClickListener { newRule = newRule.copy(perDay = false); bindRule() }
database.config().getEnableAlternativeDurationSelectionAsync().observe(viewLifecycleOwner, Observer { database.config().getEnableAlternativeDurationSelectionAsync().observe(viewLifecycleOwner, Observer {
view.timeSpan.enablePickerMode(it) view.timeSpan.enablePickerMode(it)
}) })

View file

@ -42,44 +42,38 @@ class DefaultCategories private constructor(private val context: Context) {
val rules = mutableListOf<TimeLimitRule>() val rules = mutableListOf<TimeLimitRule>()
// maximum time for each workday // maximum time for each workday
for (day in 0..4) { rules.add(
rules.add( TimeLimitRule(
TimeLimitRule( id = IdGenerator.generateId(),
id = IdGenerator.generateId(), categoryId = categoryId,
categoryId = categoryId, applyToExtraTimeUsage = false,
applyToExtraTimeUsage = false, dayMask = (1 or 2 or 4 or 8 or 16).toByte(),
dayMask = (1 shl day).toByte(), maximumTimeInMillis = 1000 * 60 * 30, // 30 minutes
maximumTimeInMillis = 1000 * 60 * 30, // 30 minutes startMinuteOfDay = TimeLimitRule.MIN_START_MINUTE,
startMinuteOfDay = TimeLimitRule.MIN_START_MINUTE, endMinuteOfDay = TimeLimitRule.MAX_END_MINUTE,
endMinuteOfDay = TimeLimitRule.MAX_END_MINUTE, sessionPauseMilliseconds = 0,
sessionPauseMilliseconds = 0, sessionDurationMilliseconds = 0,
sessionDurationMilliseconds = 0 perDay = true
) )
) )
}
// maximum time for each weekend day // maximum time for each weekend day
for (day in 5..6) { rules.add(
rules.add( TimeLimitRule(
TimeLimitRule( id = IdGenerator.generateId(),
id = IdGenerator.generateId(), categoryId = categoryId,
categoryId = categoryId, applyToExtraTimeUsage = false,
applyToExtraTimeUsage = false, dayMask = (32 or 64).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,
startMinuteOfDay = TimeLimitRule.MIN_START_MINUTE, endMinuteOfDay = TimeLimitRule.MAX_END_MINUTE,
endMinuteOfDay = TimeLimitRule.MAX_END_MINUTE, sessionPauseMilliseconds = 0,
sessionPauseMilliseconds = 0, sessionDurationMilliseconds = 0,
sessionDurationMilliseconds = 0 perDay = true
) )
) )
}
// maximum time per total week // maximum time per total week
val dayMask = BitSet()
dayMask.set(0, 7)
rules.add( rules.add(
TimeLimitRule( TimeLimitRule(
id = IdGenerator.generateId(), id = IdGenerator.generateId(),
@ -90,7 +84,8 @@ class DefaultCategories private constructor(private val context: Context) {
startMinuteOfDay = TimeLimitRule.MIN_START_MINUTE, startMinuteOfDay = TimeLimitRule.MIN_START_MINUTE,
endMinuteOfDay = TimeLimitRule.MAX_END_MINUTE, endMinuteOfDay = TimeLimitRule.MAX_END_MINUTE,
sessionPauseMilliseconds = 0, sessionPauseMilliseconds = 0,
sessionDurationMilliseconds = 0 sessionDurationMilliseconds = 0,
perDay = false
) )
) )
@ -105,7 +100,8 @@ class DefaultCategories private constructor(private val context: Context) {
startMinuteOfDay = 0, startMinuteOfDay = 0,
endMinuteOfDay = 6 * 60 - 1, endMinuteOfDay = 6 * 60 - 1,
sessionPauseMilliseconds = 0, sessionPauseMilliseconds = 0,
sessionDurationMilliseconds = 0 sessionDurationMilliseconds = 0,
perDay = false
) )
) )
@ -119,7 +115,8 @@ class DefaultCategories private constructor(private val context: Context) {
startMinuteOfDay = 18 * 60, startMinuteOfDay = 18 * 60,
endMinuteOfDay = MinuteOfDay.MAX, endMinuteOfDay = MinuteOfDay.MAX,
sessionPauseMilliseconds = 0, sessionPauseMilliseconds = 0,
sessionDurationMilliseconds = 0 sessionDurationMilliseconds = 0,
perDay = false
) )
) )
@ -134,7 +131,8 @@ class DefaultCategories private constructor(private val context: Context) {
startMinuteOfDay = 0, startMinuteOfDay = 0,
endMinuteOfDay = 9 * 60 - 1, endMinuteOfDay = 9 * 60 - 1,
sessionPauseMilliseconds = 0, sessionPauseMilliseconds = 0,
sessionDurationMilliseconds = 0 sessionDurationMilliseconds = 0,
perDay = false
) )
) )
@ -148,7 +146,8 @@ class DefaultCategories private constructor(private val context: Context) {
startMinuteOfDay = 20 * 60, startMinuteOfDay = 20 * 60,
endMinuteOfDay = MinuteOfDay.MAX, endMinuteOfDay = MinuteOfDay.MAX,
sessionPauseMilliseconds = 0, sessionPauseMilliseconds = 0,
sessionDurationMilliseconds = 0 sessionDurationMilliseconds = 0,
perDay = false
) )
) )

View file

@ -38,6 +38,10 @@
name="affectsMultipleDays" name="affectsMultipleDays"
type="boolean" /> type="boolean" />
<variable
name="showWeeklyDailyOption"
type="boolean" />
<variable <variable
name="startTime" name="startTime"
type="String" /> type="String" />
@ -80,6 +84,27 @@
<include layout="@layout/view_select_days" android:id="@+id/day_selection" /> <include layout="@layout/view_select_days" android:id="@+id/day_selection" />
<com.google.android.material.button.MaterialButtonToggleGroup
android:visibility="@{showWeeklyDailyOption ? View.VISIBLE : View.GONE}"
android:id="@+id/per_day_group"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.button.MaterialButton
android:id="@+id/type_per_day"
style="?materialButtonOutlinedStyle"
android:text="@string/category_time_limit_rules_per_day"
android:layout_weight="1"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<com.google.android.material.button.MaterialButton
android:id="@+id/type_per_week"
style="?materialButtonOutlinedStyle"
android:text="@string/category_time_limit_rules_per_week"
android:layout_weight="1"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</com.google.android.material.button.MaterialButtonToggleGroup>
<io.timelimit.android.ui.view.SelectTimeSpanView <io.timelimit.android.ui.view.SelectTimeSpanView
android:id="@+id/time_span" android:id="@+id/time_span"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -107,8 +132,9 @@
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<TextView <TextView
android:textAppearance="?android:textAppearanceMedium" android:textAppearance="?android:textAppearanceSmall"
android:padding="8dp" android:paddingStart="0dp"
android:paddingEnd="8dp"
android:text="@string/category_time_limit_rules_apply_to_part_day_1" android:text="@string/category_time_limit_rules_apply_to_part_day_1"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" /> android:layout_height="wrap_content" />
@ -122,7 +148,7 @@
android:layout_height="wrap_content" /> android:layout_height="wrap_content" />
<TextView <TextView
android:textAppearance="?android:textAppearanceMedium" android:textAppearance="?android:textAppearanceSmall"
android:padding="8dp" android:padding="8dp"
android:text="@string/category_time_limit_rules_apply_to_part_day_2" android:text="@string/category_time_limit_rules_apply_to_part_day_2"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -161,10 +187,10 @@
app:layout_gravity="fill_vertical|fill_horizontal" app:layout_gravity="fill_vertical|fill_horizontal"
android:gravity="center_vertical" android:gravity="center_vertical"
android:paddingEnd="8dp" android:paddingEnd="8dp"
android:paddingStart="8dp" android:paddingStart="0dp"
app:layout_row="1" app:layout_row="1"
app:layout_column="1" app:layout_column="1"
android:textAppearance="?android:textAppearanceMedium" android:textAppearance="?android:textAppearanceSmall"
android:text="@string/category_time_limit_rules_session_limit_duration" android:text="@string/category_time_limit_rules_session_limit_duration"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" /> android:layout_height="wrap_content" />
@ -187,8 +213,8 @@
app:layout_gravity="fill_vertical|fill_horizontal" app:layout_gravity="fill_vertical|fill_horizontal"
android:gravity="center_vertical" android:gravity="center_vertical"
android:paddingEnd="8dp" android:paddingEnd="8dp"
android:paddingStart="8dp" android:paddingStart="0dp"
android:textAppearance="?android:textAppearanceMedium" android:textAppearance="?android:textAppearanceSmall"
app:layout_row="2" app:layout_row="2"
app:layout_column="1" app:layout_column="1"
android:text="@string/category_time_limit_rules_session_limit_pause" android:text="@string/category_time_limit_rules_session_limit_pause"
@ -217,13 +243,6 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" /> 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 <LinearLayout
android:orientation="horizontal" android:orientation="horizontal"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -257,4 +276,4 @@
</LinearLayout> </LinearLayout>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>
</layout> </layout>

View file

@ -327,11 +327,9 @@
<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>
<string name="category_time_limit_rules_snackbar_deleted">Regel wurde gelöscht</string> <string name="category_time_limit_rules_snackbar_deleted">Regel wurde gelöscht</string>
<string name="category_time_limit_rules_per_day">täglich</string>
<string name="category_time_limit_rules_per_week">wöchentlich</string>
<string name="category_time_limit_rules_warning_multiple_days">Diese Regel
wird die Gesamtnutzungsdauer in einer Woche an den gewählten Tagen einschränken.
Wenn Sei die Begrenzung je Tag wollen, dann erstellen Sie eine Regel je Tag.
</string>
<string name="category_time_limit_rules_warning_day_part">Diese Regel begrenzt nur <string name="category_time_limit_rules_warning_day_part">Diese Regel begrenzt nur
die Nutzung im angegebenen Intervall. Ohne Regeln für andere Intervalle die Nutzung im angegebenen Intervall. Ohne Regeln für andere Intervalle
oder Sperrzeiten wird es keine Begrenzung außerhalb dieses Intervalls oder Sperrzeiten wird es keine Begrenzung außerhalb dieses Intervalls
@ -1034,14 +1032,6 @@
\n2. Wenn \"es nicht funktioniert\" liegt es oft an Energiesparfunktionen die deaktiviert werden müssen. \n2. Wenn \"es nicht funktioniert\" liegt es oft an Energiesparfunktionen die deaktiviert werden müssen.
Diese sind oft automatisch aktiviert, ohne das es Ihnen als Nutzer bewusst ist. Diese sind oft automatisch aktiviert, ohne das es Ihnen als Nutzer bewusst ist.
</string> </string>
<string name="must_read_timelimit_rules">
Da es manchmal falsch bedient wird:
Eine Regel begrenzt die Gesamtnutzungsdauer der Kategorie in einer Woche an den gewählten Tagen.
Das bedeutet, dass wenn eine Regel für Montag bis Freitag mit 3 Stunden eingestellt wird,
die Kategorie in jeder Woche von Montag bis Freitag insgesamt nur 3 Stunden benutzt werden kann.
Wenn je eine Regel für die Tage von Montag bis Freitag mit jeweils 3 Stunden gewählt wird,
kann die Kategorie an jedem dieser Tage für 3 Stunden genutzt werden.
</string>
<string name="must_read_add_already_assigned_apps"> <string name="must_read_add_already_assigned_apps">
Apps mit Kategorie werden beim Hinzufügen aus der vorherigen Kategorie entfernt - für mehrere Kategorien gleichzeitig können Sie Ober- und Unter-Kategorien verwenden Apps mit Kategorie werden beim Hinzufügen aus der vorherigen Kategorie entfernt - für mehrere Kategorien gleichzeitig können Sie Ober- und Unter-Kategorien verwenden
</string> </string>

View file

@ -962,14 +962,6 @@ Esta %s só é permitida em algumas redes, mas nenhuma conexão com tal rede foi
\n2nd: Se \"não funciona\" então isto é muito provavelmente causado por recursos de economia da bateria. \n2nd: Se \"não funciona\" então isto é muito provavelmente causado por recursos de economia da bateria.
Estos recursos são freqüentemente ativadas mesmo que você não o tenha feito. Estos recursos são freqüentemente ativadas mesmo que você não o tenha feito.
</string> </string>
<string name="must_read_timelimit_rules">
Isso acontece às vezes porque é configurado de forma errada :
Uma regra limita a duração total de uso de uma categoria nos dias selecionados da semana.
Se houver uma regra de segunda-feira a sexta-feira com 3 horas,
então há um limite total de 3 horas por semana, de segunda a sexta.
Se houver uma regra por dia com 3 horas,
então há um limite de 3 horas por dia.
</string>
<string name="must_read_add_already_assigned_apps"> <string name="must_read_add_already_assigned_apps">
Os aplicativos com categoria são excluídos da categoria anterior quando são adicionados a uma nova categoria - Os aplicativos com categoria são excluídos da categoria anterior quando são adicionados a uma nova categoria -
usar categorias de Pais e filhos para \"adicionar\" um aplicativo a múltiplas categorias usar categorias de Pais e filhos para \"adicionar\" um aplicativo a múltiplas categorias

View file

@ -367,15 +367,13 @@
<string name="category_time_limit_rules_enable_session_limit">Limit session duration</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_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_session_limit_pause">Break duration after session</string>
<string name="category_time_limit_rules_per_day">daily</string>
<string name="category_time_limit_rules_per_week">weekly</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>
<string name="category_time_limit_rules_snackbar_deleted">Rule was deleted</string> <string name="category_time_limit_rules_snackbar_deleted">Rule was deleted</string>
<string name="category_time_limit_rules_warning_multiple_days">This rule
will limit the total usage duration during one week at the selected days.
If you want it per day, create one rule per day.
</string>
<string name="category_time_limit_rules_warning_day_part">This rule <string name="category_time_limit_rules_warning_day_part">This rule
will limit the usage duration only in the specified interval. will limit the usage duration only in the specified interval.
Without rules for other times or blocked time areas, there will Without rules for other times or blocked time areas, there will
@ -1077,14 +1075,6 @@
\n2nd: If \"it does not work\" then this is most likely caused by power saving features. \n2nd: If \"it does not work\" then this is most likely caused by power saving features.
This features are often enabled even if you did not enable it actively. This features are often enabled even if you did not enable it actively.
</string> </string>
<string name="must_read_timelimit_rules">
Because it happens sometimes that it is configured wrongly:
A rule limits the total usage duration of a category at the selected days of the week.
If there is a rule from monday to friday with 3 hours,
then there is a limit of total 3 hours per week from monday to friday.
If there is one rule per day with 3 hours,
then there is a limit of 3 hours per day.
</string>
<string name="must_read_add_already_assigned_apps"> <string name="must_read_add_already_assigned_apps">
Apps with category are removed from the previous category when they are added to a new category - Apps with category are removed from the previous category when they are added to a new category -
use parent and child categories to \"add\" an App to multiple categories use parent and child categories to \"add\" an App to multiple categories