Add counting background music playback

This commit is contained in:
Jonas Lochmann 2020-01-27 01:00:00 +01:00
parent c632609211
commit 192b871ae5
No known key found for this signature in database
GPG key ID: 8B8C9AEE10FA5B36
13 changed files with 694 additions and 301 deletions

View file

@ -32,7 +32,7 @@ Je nach Android-Version verwendet TimeLimit die Berechtigung zum Nutzungsdatenzu
Die Geräte-Administrator-Berechtigung wird genutzt, um ein Deinstallieren von TimeLimit zu erkennen.
TimeLimit verwendet den Benachrichtigungszugriff, um auch die Benachrichtigungen von gesperrten Apps zu sperren und um Medienplayer vollständig zu beenden. Es erfolgt keine Speicherung von Benachrichtigungen oder deren Inhalten.
TimeLimit verwendet den Benachrichtigungszugriff, um auch die Benachrichtigungen von gesperrten Apps zu sperren und zum Erfassen und Sperren von Hintergrundmusikwiedergaben. Es erfolgt keine Speicherung von Benachrichtigungen.
TimeLimit verwendet die Bedienhilfe-Berechtigung, um den Home-Button vor dem Aufruf des Sperrbildschirms zu drücken, um das Sperren in einigen Fällen zu verbessern. Außerdem ist das eine Möglichkeit, um es TimeLimit unter neueren Android-Versionen zu ermöglichen, den Sperrbildschirm zu öffnen.

View file

@ -33,7 +33,7 @@ Depending on the Android version, TimeLimit uses the permission for the usage st
The device admin permission is used to detect an uninstallation of TimeLimit.
TimeLimit uses the notification access to block notifications of blocked apps and to terminate media players completely. Notifications and their contents are not saved.
TimeLimit uses the notification access to block notifications of blocked apps and to count and block background playback. Notifications and their contents are not saved.
TimeLimit uses an accessibility service to press the home button before showing the lock screen. This fixes blocking in some cases. Moreover, this allows opening the lockscreen at newer Android versions.

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 Jonas Lochmann
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -69,6 +69,9 @@ abstract class UsedTimeDao {
@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 = :categoryId AND day_of_epoch = :dayOfEpoch")
abstract fun getUsedTimesByCategoryIdAndDayOfEpoch(categoryId: String, dayOfEpoch: Int): LiveData<UsedTimeItem?>
@Query("SELECT * FROM used_time WHERE category_id IN (:categoryIds) AND day_of_epoch >= :startingDayOfEpoch AND day_of_epoch <= :endDayOfEpoch")
abstract fun getUsedTimesByDayAndCategoryIds(categoryIds: List<String>, startingDayOfEpoch: Int, endDayOfEpoch: Int): LiveData<List<UsedTimeItem>>

View file

@ -49,6 +49,7 @@ abstract class PlatformIntegration(
abstract fun setShowBlockingOverlay(show: Boolean)
// this should throw an SecurityException if the permission is missing
abstract suspend fun getForegroundApp(result: ForegroundAppSpec, queryInterval: Long)
abstract fun getMusicPlaybackPackage(): String?
abstract fun setAppStatusMessage(message: AppStatusMessage?)
abstract fun isScreenOn(): Boolean
abstract fun setShowNotificationToRevokeTemporarilyAllowedApps(show: Boolean)

View file

@ -29,6 +29,7 @@ import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import android.media.session.MediaSessionManager
import android.media.session.PlaybackState
import android.os.Build
import android.os.PowerManager
import android.os.UserManager
@ -126,6 +127,23 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio
return foregroundAppHelper.getPermissionStatus()
}
override fun getMusicPlaybackPackage(): String? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (getNotificationAccessPermissionStatus() == NewPermissionStatus.Granted) {
val manager = context.getSystemService(Context.MEDIA_SESSION_SERVICE) as MediaSessionManager
val sessions = manager.getActiveSessions(ComponentName(context, NotificationListener::class.java))
return sessions.find {
it.playbackState?.state == PlaybackState.STATE_PLAYING ||
it.playbackState?.state == PlaybackState.STATE_FAST_FORWARDING ||
it.playbackState?.state == PlaybackState.STATE_REWINDING
}?.packageName
}
}
return null
}
override fun showOverlayMessage(text: String) {
Toast.makeText(context, text, Toast.LENGTH_SHORT).show()
}

View file

@ -121,6 +121,8 @@ class DummyIntegration(
result.activityName = null
}
override fun getMusicPlaybackPackage(): String? = null
override fun setAppStatusMessage(message: AppStatusMessage?) {
lastAppStatusMessage = message
}

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 Jonas Lochmann
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -18,7 +18,6 @@ package io.timelimit.android.logic
import android.util.Log
import android.util.SparseArray
import android.util.SparseLongArray
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import io.timelimit.android.BuildConfig
import io.timelimit.android.R
@ -34,9 +33,7 @@ import io.timelimit.android.integration.platform.AppStatusMessage
import io.timelimit.android.integration.platform.ForegroundAppSpec
import io.timelimit.android.integration.platform.ProtectionLevel
import io.timelimit.android.integration.platform.android.AccessibilityService
import io.timelimit.android.integration.platform.android.AndroidIntegrationApps
import io.timelimit.android.livedata.*
import io.timelimit.android.logic.extension.isCategoryAllowed
import io.timelimit.android.sync.actions.UpdateDeviceStatusAction
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
import io.timelimit.android.ui.IsAppInForeground
@ -49,7 +46,7 @@ import kotlinx.coroutines.sync.withLock
import java.util.*
class BackgroundTaskLogic(val appLogic: AppLogic) {
var pauseBackgroundLoop = false
var pauseForegroundAppBackgroundLoop = false
val lastLoopException = MutableLiveData<Exception?>().apply { value = null }
companion object {
@ -104,47 +101,16 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
}
}
private val deviceUserEntryLive = SingleItemLiveDataCache(appLogic.deviceUserEntry.ignoreUnchanged())
private val isThisDeviceTheCurrentDeviceLive = SingleItemLiveDataCache(appLogic.currentDeviceLogic.isThisDeviceTheCurrentDevice)
private val childCategories = object: MultiKeyLiveDataCache<List<Category>, String?>() {
// key = child id
override fun createValue(key: String?): LiveData<List<Category>> {
if (key == null) {
// this should rarely happen
return liveDataFromValue(Collections.emptyList())
} else {
return appLogic.database.category().getCategoriesByChildId(key).ignoreUnchanged()
}
}
}
private val appCategories = object: MultiKeyLiveDataCache<CategoryApp?, Pair<String, List<String>>>() {
// key = package name, category ids
override fun createValue(key: Pair<String, List<String>>): LiveData<CategoryApp?> {
return appLogic.database.categoryApp().getCategoryApp(key.second, key.first)
}
}
private val timeLimitRules = object: MultiKeyLiveDataCache<List<TimeLimitRule>, String>() {
override fun createValue(key: String): LiveData<List<TimeLimitRule>> {
return appLogic.database.timeLimitRules().getTimeLimitRulesByCategory(key)
}
}
private val usedTimesOfCategoryAndWeekByFirstDayOfWeek = object: MultiKeyLiveDataCache<SparseArray<UsedTimeItem>, Pair<String, Int>>() {
override fun createValue(key: Pair<String, Int>): LiveData<SparseArray<UsedTimeItem>> {
return appLogic.database.usedTimes().getUsedTimesOfWeek(key.first, key.second)
}
}
private val shouldDoAutomaticSignOut = SingleItemLiveDataCacheWithRequery { -> appLogic.defaultUserLogic.hasAutomaticSignOut()}
private val cache = BackgroundTaskLogicCache(appLogic)
private val deviceUserEntryLive = cache.deviceUserEntryLive
private val childCategories = cache.childCategories
private val timeLimitRules = cache.timeLimitRules
private val usedTimesOfCategoryAndWeekByFirstDayOfWeek = cache.usedTimesOfCategoryAndWeekByFirstDayOfWeek
private val shouldDoAutomaticSignOut = cache.shouldDoAutomaticSignOut
private val liveDataCaches = cache.liveDataCaches
private val liveDataCaches = LiveDataCaches(arrayOf(
deviceUserEntryLive,
childCategories,
appCategories,
timeLimitRules,
usedTimesOfCategoryAndWeekByFirstDayOfWeek,
shouldDoAutomaticSignOut
))
private var usedTimeUpdateHelper: UsedTimeItemBatchUpdateHelper? = null
private var usedTimeUpdateHelperForegroundApp: UsedTimeItemBatchUpdateHelper? = null
private var usedTimeUpdateHelperBackgroundPlayback: UsedTimeItemBatchUpdateHelper? = null
private var previousMainLogicExecutionTime = 0
private var previousMainLoopEndTime = 0L
private val dayChangeTracker = DayChangeTracker(
@ -155,11 +121,6 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
private val appTitleCache = QueryAppTitleCache(appLogic.platformIntegration)
private suspend fun openLockscreen(blockedAppPackageName: String, blockedAppActivityName: String?) {
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
title = appTitleCache.query(blockedAppPackageName),
text = appLogic.context.getString(R.string.background_logic_opening_lockscreen)
))
appLogic.platformIntegration.setShowBlockingOverlay(true)
if (appLogic.platformIntegration.isAccessibilityServiceEnabled()) {
@ -175,6 +136,13 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
}
private val foregroundAppSpec = ForegroundAppSpec.newInstance()
val foregroundAppHandling = BackgroundTaskRestrictionLogicResult()
val audioPlaybackHandling = BackgroundTaskRestrictionLogicResult()
private suspend fun commitUsedTimeUpdaters() {
usedTimeUpdateHelperForegroundApp?.commit(appLogic)
usedTimeUpdateHelperBackgroundPlayback?.commit(appLogic)
}
private suspend fun backgroundServiceLoop() {
val realTime = RealTime.newInstance()
@ -182,7 +150,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
while (true) {
// app must be enabled
if (!appLogic.enable.waitForNonNullValue()) {
usedTimeUpdateHelper?.commit(appLogic)
commitUsedTimeUpdaters()
liveDataCaches.removeAllItems()
appLogic.platformIntegration.setAppStatusMessage(null)
appLogic.platformIntegration.setShowBlockingOverlay(false)
@ -195,7 +163,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
val deviceUserEntry = deviceUserEntryLive.read().waitForNullableValue()
if (deviceUserEntry == null || deviceUserEntry.type != UserType.Child) {
usedTimeUpdateHelper?.commit(appLogic)
commitUsedTimeUpdaters()
val shouldDoAutomaticSignOut = shouldDoAutomaticSignOut.read()
if (shouldDoAutomaticSignOut.waitForNonNullValue()) {
@ -275,262 +243,406 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
appLogic.platformIntegration.getForegroundApp(foregroundAppSpec, appLogic.getForegroundAppQueryInterval())
val foregroundAppPackageName = foregroundAppSpec.packageName
val foregroundAppActivityName = foregroundAppSpec.activityName
val audioPlaybackPackageName = appLogic.platformIntegration.getMusicPlaybackPackage()
val activityLevelBlocking = appLogic.deviceEntry.value?.enableActivityLevelBlocking ?: false
fun showStatusMessageWithCurrentAppTitle(text: String, titlePrefix: String? = "") {
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
titlePrefix + appTitleCache.query(foregroundAppPackageName ?: "invalid"),
text,
if (activityLevelBlocking) foregroundAppActivityName?.removePrefix(foregroundAppPackageName ?: "invalid") else null
))
foregroundAppHandling.reset()
audioPlaybackHandling.reset()
BackgroundTaskRestrictionLogic.getHandling(
foregroundAppPackageName = foregroundAppPackageName,
foregroundAppActivityName = foregroundAppActivityName,
pauseForegroundAppBackgroundLoop = pauseForegroundAppBackgroundLoop,
temporarilyAllowedApps = temporarilyAllowedApps,
categories = categories,
activityLevelBlocking = activityLevelBlocking,
deviceUserEntry = deviceUserEntry,
batteryStatus = batteryStatus,
shouldTrustTimeTemporarily = realTime.shouldTrustTimeTemporarily,
nowTimestamp = nowTimestamp,
minuteOfWeek = minuteOfWeek,
cache = cache,
result = foregroundAppHandling
)
BackgroundTaskRestrictionLogic.getHandling(
foregroundAppPackageName = audioPlaybackPackageName,
foregroundAppActivityName = null,
pauseForegroundAppBackgroundLoop = false,
temporarilyAllowedApps = temporarilyAllowedApps,
categories = categories,
activityLevelBlocking = activityLevelBlocking,
deviceUserEntry = deviceUserEntry,
batteryStatus = batteryStatus,
shouldTrustTimeTemporarily = realTime.shouldTrustTimeTemporarily,
nowTimestamp = nowTimestamp,
minuteOfWeek = minuteOfWeek,
cache = cache,
result = audioPlaybackHandling
)
// check times
fun buildUsedTimesSparseArray(items: SparseArray<UsedTimeItem>, categoryId: String): SparseLongArray {
val a = usedTimeUpdateHelperForegroundApp
val b = usedTimeUpdateHelperBackgroundPlayback
val result = SparseLongArray()
for (i in 0..6) {
val usedTimesItem = items[i]?.usedMillis
if (a?.date?.dayOfWeek == i && a.childCategoryId == categoryId) {
result.put(i, a.getTotalUsedTimeChild())
} else if (a?.date?.dayOfWeek == i && a.parentCategoryId == categoryId) {
result.put(i, a.getTotalUsedTimeParent())
} else if (b?.date?.dayOfWeek == i && b.childCategoryId == categoryId) {
result.put(i, b.getTotalUsedTimeChild())
} else if (b?.date?.dayOfWeek == i && b.parentCategoryId == categoryId) {
result.put(i, b.getTotalUsedTimeParent())
} else {
result.put(i, usedTimesItem ?: 0)
}
}
return result
}
// the following is not executed if the permission is missing
fun getCachedExtraTimeToSubstract(categoryId: String): Int {
val a = usedTimeUpdateHelperForegroundApp
val b = usedTimeUpdateHelperBackgroundPlayback
if (pauseBackgroundLoop) {
usedTimeUpdateHelper?.commit(appLogic)
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
title = appLogic.context.getString(R.string.background_logic_paused_title),
text = appLogic.context.getString(R.string.background_logic_paused_text)
))
appLogic.platformIntegration.setShowBlockingOverlay(false)
} else if (
(foregroundAppPackageName == BuildConfig.APPLICATION_ID) ||
(foregroundAppPackageName != null && AndroidIntegrationApps.ignoredApps[foregroundAppPackageName].let {
when (it) {
null -> false
AndroidIntegrationApps.IgnoredAppHandling.Ignore -> true
AndroidIntegrationApps.IgnoredAppHandling.IgnoreOnStoreOtherwiseWhitelistAndDontDisable -> BuildConfig.storeCompilant
}
}) ||
(foregroundAppPackageName != null && foregroundAppActivityName != null &&
AndroidIntegrationApps.shouldIgnoreActivity(foregroundAppPackageName, foregroundAppActivityName))
) {
usedTimeUpdateHelper?.commit(appLogic)
showStatusMessageWithCurrentAppTitle(
text = appLogic.context.getString(R.string.background_logic_whitelisted)
if (a?.childCategoryId == categoryId || a?.parentCategoryId == categoryId) {
return a.getCachedExtraTimeToSubtract()
}
if (b?.childCategoryId == categoryId || b?.parentCategoryId == categoryId) {
return b.getCachedExtraTimeToSubtract()
}
return 0
}
suspend fun getRemainingTime(categoryId: String?, parentCategoryId: String?): RemainingTime? {
categoryId ?: return null
val category = categories.find { it.id == categoryId } ?: return null
val parentCategory = categories.find { it.id == parentCategoryId }
val rules = timeLimitRules.get(category.id).waitForNonNullValue()
val parentRules = parentCategory?.let {
timeLimitRules.get(it.id).waitForNonNullValue()
} ?: emptyList()
val usedTimes = usedTimesOfCategoryAndWeekByFirstDayOfWeek.get(Pair(category.id, nowDate.dayOfEpoch - nowDate.dayOfWeek)).waitForNonNullValue()
val parentUsedTimes = parentCategory?.let {
usedTimesOfCategoryAndWeekByFirstDayOfWeek.get(Pair(it.id, nowDate.dayOfEpoch - nowDate.dayOfWeek)).waitForNonNullValue()
} ?: SparseArray()
val remainingChild = RemainingTime.getRemainingTime(
nowDate.dayOfWeek,
buildUsedTimesSparseArray(usedTimes, categoryId),
rules,
Math.max(0, category.extraTimeInMillis - getCachedExtraTimeToSubstract(category.id))
)
appLogic.platformIntegration.setShowBlockingOverlay(false)
} else if (foregroundAppPackageName != null && temporarilyAllowedApps.contains(foregroundAppPackageName)) {
usedTimeUpdateHelper?.commit(appLogic)
showStatusMessageWithCurrentAppTitle(appLogic.context.getString(R.string.background_logic_temporarily_allowed))
appLogic.platformIntegration.setShowBlockingOverlay(false)
} else if (foregroundAppPackageName != null) {
val categoryIds = categories.map { it.id }
val appCategory = run {
val appLevelCategoryLive = appCategories.get(foregroundAppPackageName to categoryIds)
val remainingParent = parentCategory?.let {
RemainingTime.getRemainingTime(
nowDate.dayOfWeek,
buildUsedTimesSparseArray(parentUsedTimes, parentCategory.id),
parentRules,
Math.max(0, parentCategory.extraTimeInMillis - getCachedExtraTimeToSubstract(parentCategory.id))
)
}
if (activityLevelBlocking) {
val appActivityCategoryLive = appCategories.get("$foregroundAppPackageName:$foregroundAppActivityName" to categoryIds)
return RemainingTime.min(remainingChild, remainingParent)
}
appActivityCategoryLive.waitForNullableValue() ?: appLevelCategoryLive.waitForNullableValue()
val remainingTimeForegroundApp = getRemainingTime(foregroundAppHandling.categoryId, foregroundAppHandling.parentCategoryId)
val remainingTimeBackgroundApp = getRemainingTime(audioPlaybackHandling.categoryId, audioPlaybackHandling.parentCategoryId)
// eventually block
if (remainingTimeForegroundApp?.hasRemainingTime == false) {
foregroundAppHandling.status = BackgroundTaskLogicAppStatus.ShouldBlock
}
if (remainingTimeBackgroundApp?.hasRemainingTime == false) {
audioPlaybackHandling.status = BackgroundTaskLogicAppStatus.ShouldBlock
}
// update times
suspend fun getUsedTimeItem(categoryId: String?): UsedTimeItem? {
categoryId ?: return null
return cache.usedTimesOfCategoryAndDayOfEpoch.get(categoryId to nowDate.dayOfEpoch).waitForNullableValue()
}
val shouldCountForegroundApp = remainingTimeForegroundApp != null && isScreenOn && remainingTimeForegroundApp.hasRemainingTime
val shouldCountBackgroundApp = remainingTimeBackgroundApp != null && remainingTimeBackgroundApp.hasRemainingTime
val countTwoTypes = shouldCountForegroundApp && shouldCountBackgroundApp
val doCategoriesMatch = (
foregroundAppHandling.categoryId != null && (
foregroundAppHandling.categoryId == audioPlaybackHandling.categoryId ||
foregroundAppHandling.categoryId == audioPlaybackHandling.parentCategoryId
)
) || (
foregroundAppHandling.parentCategoryId != null && (
foregroundAppHandling.parentCategoryId == audioPlaybackHandling.categoryId ||
foregroundAppHandling.parentCategoryId == audioPlaybackHandling.parentCategoryId
)
)
val useSingleCounter = countTwoTypes && doCategoriesMatch
if (useSingleCounter) {
val isBackgroundCategoryHigher = audioPlaybackHandling.categoryId != null &&
audioPlaybackHandling.categoryId == foregroundAppHandling.parentCategoryId
if (usedTimeUpdateHelperForegroundApp !== usedTimeUpdateHelperBackgroundPlayback) {
if (isBackgroundCategoryHigher) {
usedTimeUpdateHelperForegroundApp?.commit(appLogic)
usedTimeUpdateHelperForegroundApp = null
} else {
appLevelCategoryLive.waitForNullableValue()
usedTimeUpdateHelperBackgroundPlayback?.commit(appLogic)
usedTimeUpdateHelperBackgroundPlayback = null
}
}
val category = categories.find { it.id == appCategory?.categoryId }
?: categories.find { it.id == deviceUserEntry.categoryForNotAssignedApps }
val parentCategory = categories.find { it.id == category?.parentCategoryId }
if (isBackgroundCategoryHigher) {
usedTimeUpdateHelperBackgroundPlayback = UsedTimeItemBatchUpdateHelper.eventuallyUpdateInstance(
date = nowDate,
childCategoryId = audioPlaybackHandling.categoryId!!,
parentCategoryId = audioPlaybackHandling.parentCategoryId,
oldInstance = usedTimeUpdateHelperBackgroundPlayback,
usedTimeItemForDayChild = getUsedTimeItem(audioPlaybackHandling.categoryId),
usedTimeItemForDayParent = getUsedTimeItem(audioPlaybackHandling.parentCategoryId),
logic = appLogic
)
if (category == null) {
usedTimeUpdateHelper?.commit(appLogic)
openLockscreen(foregroundAppPackageName, foregroundAppActivityName)
} else if ((!batteryStatus.isCategoryAllowed(category)) || (!batteryStatus.isCategoryAllowed(parentCategory))) {
usedTimeUpdateHelper?.commit(appLogic)
openLockscreen(foregroundAppPackageName, foregroundAppActivityName)
} else if (category.temporarilyBlocked or (parentCategory?.temporarilyBlocked == true)) {
usedTimeUpdateHelper?.commit(appLogic)
openLockscreen(foregroundAppPackageName, foregroundAppActivityName)
usedTimeUpdateHelperForegroundApp = usedTimeUpdateHelperBackgroundPlayback
} else {
// disable time limits temporarily feature
if (realTime.shouldTrustTimeTemporarily && nowTimestamp < deviceUserEntry.disableLimitsUntil) {
showStatusMessageWithCurrentAppTitle(appLogic.context.getString(R.string.background_logic_limits_disabled))
appLogic.platformIntegration.setShowBlockingOverlay(false)
} else if (
// check blocked time areas
// directly blocked
(category.blockedMinutesInWeek.read(minuteOfWeek)) or
(parentCategory?.blockedMinutesInWeek?.read(minuteOfWeek) == true) or
// or no safe time
(
(
(category.blockedMinutesInWeek.dataNotToModify.isEmpty == false) or
(parentCategory?.blockedMinutesInWeek?.dataNotToModify?.isEmpty == false)
) &&
(!realTime.shouldTrustTimeTemporarily)
)
) {
usedTimeUpdateHelper?.commit(appLogic)
usedTimeUpdateHelperForegroundApp = UsedTimeItemBatchUpdateHelper.eventuallyUpdateInstance(
date = nowDate,
childCategoryId = foregroundAppHandling.categoryId!!,
parentCategoryId = foregroundAppHandling.parentCategoryId,
oldInstance = usedTimeUpdateHelperForegroundApp,
usedTimeItemForDayChild = getUsedTimeItem(foregroundAppHandling.categoryId),
usedTimeItemForDayParent = getUsedTimeItem(foregroundAppHandling.parentCategoryId),
logic = appLogic
)
openLockscreen(foregroundAppPackageName, foregroundAppActivityName)
} else {
// check time limits
val rules = timeLimitRules.get(category.id).waitForNonNullValue()
val parentRules = parentCategory?.let {
timeLimitRules.get(it.id).waitForNonNullValue()
} ?: emptyList()
if (rules.isEmpty() and parentRules.isEmpty()) {
// unlimited
usedTimeUpdateHelper?.commit(appLogic)
showStatusMessageWithCurrentAppTitle(
text = appLogic.context.getString(R.string.background_logic_no_timelimit),
titlePrefix = category.title + " - "
)
appLogic.platformIntegration.setShowBlockingOverlay(false)
} else {
val isCurrentDevice = isThisDeviceTheCurrentDeviceLive.read().waitForNonNullValue()
if (!isCurrentDevice) {
usedTimeUpdateHelper?.commit(appLogic)
openLockscreen(foregroundAppPackageName, foregroundAppActivityName)
} else if (realTime.shouldTrustTimeTemporarily) {
val usedTimes = usedTimesOfCategoryAndWeekByFirstDayOfWeek.get(Pair(category.id, nowDate.dayOfEpoch - nowDate.dayOfWeek)).waitForNonNullValue()
val parentUsedTimes = parentCategory?.let {
usedTimesOfCategoryAndWeekByFirstDayOfWeek.get(Pair(it.id, nowDate.dayOfEpoch - nowDate.dayOfWeek)).waitForNonNullValue()
} ?: SparseArray()
val newUsedTimeItemBatchUpdateHelper = UsedTimeItemBatchUpdateHelper.eventuallyUpdateInstance(
date = nowDate,
childCategoryId = category.id,
parentCategoryId = parentCategory?.id,
oldInstance = usedTimeUpdateHelper,
usedTimeItemForDayChild = usedTimes.get(nowDate.dayOfWeek),
usedTimeItemForDayParent = parentUsedTimes.get(nowDate.dayOfWeek),
logic = appLogic
)
usedTimeUpdateHelper = newUsedTimeItemBatchUpdateHelper
fun buildUsedTimesSparseArray(items: SparseArray<UsedTimeItem>, isParentCategory: Boolean): SparseLongArray {
val result = SparseLongArray()
for (i in 0..6) {
val usedTimesItem = items[i]?.usedMillis
if (newUsedTimeItemBatchUpdateHelper.date.dayOfWeek == i) {
result.put(
i,
if (isParentCategory)
newUsedTimeItemBatchUpdateHelper.getTotalUsedTimeParent()
else
newUsedTimeItemBatchUpdateHelper.getTotalUsedTimeChild()
)
} else {
result.put(i, usedTimesItem ?: 0)
}
}
return result
}
val remainingChild = RemainingTime.getRemainingTime(
nowDate.dayOfWeek,
buildUsedTimesSparseArray(usedTimes, isParentCategory = false),
rules,
Math.max(0, category.extraTimeInMillis - newUsedTimeItemBatchUpdateHelper.getCachedExtraTimeToSubtract())
)
val remainingParent = parentCategory?.let {
RemainingTime.getRemainingTime(
nowDate.dayOfWeek,
buildUsedTimesSparseArray(parentUsedTimes, isParentCategory = true),
parentRules,
Math.max(0, parentCategory.extraTimeInMillis - newUsedTimeItemBatchUpdateHelper.getCachedExtraTimeToSubtract())
)
}
val remaining = RemainingTime.min(remainingChild, remainingParent)
if (remaining == null) {
// unlimited
usedTimeUpdateHelper?.commit(appLogic)
showStatusMessageWithCurrentAppTitle(
text = appLogic.context.getString(R.string.background_logic_no_timelimit),
titlePrefix = category.title + " - "
)
appLogic.platformIntegration.setShowBlockingOverlay(false)
} else {
// time limited
if (remaining.includingExtraTime > 0) {
var subtractExtraTime: Boolean
if (remaining.default == 0L) {
// using extra time
showStatusMessageWithCurrentAppTitle(
text = appLogic.context.getString(R.string.background_logic_using_extra_time, TimeTextUtil.remaining(remaining.includingExtraTime.toInt(), appLogic.context)),
titlePrefix = category.title + " - "
)
subtractExtraTime = true
} else {
// using normal contingent
showStatusMessageWithCurrentAppTitle(
text = TimeTextUtil.remaining(remaining.default.toInt(), appLogic.context),
titlePrefix = category.title + " - "
)
subtractExtraTime = false
}
appLogic.platformIntegration.setShowBlockingOverlay(false)
if (isScreenOn) {
// never save more than a second of used time
val timeToSubtract = Math.min(previousMainLogicExecutionTime, MAX_USED_TIME_PER_ROUND)
newUsedTimeItemBatchUpdateHelper.addUsedTime(
timeToSubtract,
subtractExtraTime,
appLogic
)
val oldRemainingTime = remaining.includingExtraTime
val newRemainingTime = oldRemainingTime - timeToSubtract
if (oldRemainingTime / (1000 * 60) != newRemainingTime / (1000 * 60)) {
// eventually show remaining time warning
val roundedNewTime = ((newRemainingTime / (1000 * 60)) + 1) * (1000 * 60)
val flagIndex = CategoryTimeWarnings.durationToBitIndex[roundedNewTime]
if (flagIndex != null && category.timeWarnings and (1 shl flagIndex) != 0) {
appLogic.platformIntegration.showTimeWarningNotification(
title = appLogic.context.getString(R.string.time_warning_not_title, category.title),
text = TimeTextUtil.remaining(roundedNewTime.toInt(), appLogic.context)
)
}
}
}
} else {
// there is not time anymore
newUsedTimeItemBatchUpdateHelper.commit(appLogic)
openLockscreen(foregroundAppPackageName, foregroundAppActivityName)
}
}
} else {
// if should not trust the time temporarily
usedTimeUpdateHelper?.commit(appLogic)
openLockscreen(foregroundAppPackageName, foregroundAppActivityName)
}
}
}
usedTimeUpdateHelperBackgroundPlayback = usedTimeUpdateHelperForegroundApp
}
} else {
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
appLogic.context.getString(R.string.background_logic_idle_title),
if (shouldCountForegroundApp) {
usedTimeUpdateHelperForegroundApp = UsedTimeItemBatchUpdateHelper.eventuallyUpdateInstance(
date = nowDate,
childCategoryId = foregroundAppHandling.categoryId!!,
parentCategoryId = foregroundAppHandling.parentCategoryId,
oldInstance = usedTimeUpdateHelperForegroundApp,
usedTimeItemForDayChild = getUsedTimeItem(foregroundAppHandling.categoryId),
usedTimeItemForDayParent = getUsedTimeItem(foregroundAppHandling.parentCategoryId),
logic = appLogic
)
} else {
usedTimeUpdateHelperForegroundApp?.commit(appLogic)
usedTimeUpdateHelperForegroundApp = null
}
if (shouldCountBackgroundApp) {
usedTimeUpdateHelperBackgroundPlayback = UsedTimeItemBatchUpdateHelper.eventuallyUpdateInstance(
date = nowDate,
childCategoryId = audioPlaybackHandling.categoryId!!,
parentCategoryId = audioPlaybackHandling.parentCategoryId,
oldInstance = usedTimeUpdateHelperBackgroundPlayback,
usedTimeItemForDayChild = getUsedTimeItem(audioPlaybackHandling.categoryId),
usedTimeItemForDayParent = getUsedTimeItem(audioPlaybackHandling.parentCategoryId),
logic = appLogic
)
} else {
usedTimeUpdateHelperBackgroundPlayback?.commit(appLogic)
usedTimeUpdateHelperBackgroundPlayback = null
}
}
// count times
// never save more than a second of used time
// FIXME: currently, this uses extra time if the parent or the child category needs it and it subtracts it from both
val timeToSubtract = Math.min(previousMainLogicExecutionTime, MAX_USED_TIME_PER_ROUND)
if (usedTimeUpdateHelperForegroundApp === usedTimeUpdateHelperBackgroundPlayback) {
usedTimeUpdateHelperForegroundApp?.addUsedTime(
time = timeToSubtract,
subtractExtraTime = remainingTimeForegroundApp!!.usingExtraTime or remainingTimeBackgroundApp!!.usingExtraTime,
appLogic = appLogic
)
} else {
usedTimeUpdateHelperForegroundApp?.addUsedTime(
time = timeToSubtract,
subtractExtraTime = remainingTimeForegroundApp!!.usingExtraTime,
appLogic = appLogic
)
usedTimeUpdateHelperBackgroundPlayback?.addUsedTime(
time = timeToSubtract,
subtractExtraTime = remainingTimeBackgroundApp!!.usingExtraTime,
appLogic = appLogic
)
}
// trigger time warnings
// FIXME: this uses the resulting time, not the time per category
fun eventuallyTriggerTimeWarning(remaining: RemainingTime, categoryId: String?) {
val category = categories.find { it.id == categoryId } ?: return
val oldRemainingTime = remaining.includingExtraTime
val newRemainingTime = oldRemainingTime - timeToSubtract
if (oldRemainingTime / (1000 * 60) != newRemainingTime / (1000 * 60)) {
// eventually show remaining time warning
val roundedNewTime = ((newRemainingTime / (1000 * 60)) + 1) * (1000 * 60)
val flagIndex = CategoryTimeWarnings.durationToBitIndex[roundedNewTime]
if (flagIndex != null && category.timeWarnings and (1 shl flagIndex) != 0) {
appLogic.platformIntegration.showTimeWarningNotification(
title = appLogic.context.getString(R.string.time_warning_not_title, category.title),
text = TimeTextUtil.remaining(roundedNewTime.toInt(), appLogic.context)
)
}
}
}
if (remainingTimeForegroundApp != null) {
eventuallyTriggerTimeWarning(remainingTimeForegroundApp, foregroundAppHandling.categoryId)
}
if (remainingTimeBackgroundApp != null) {
eventuallyTriggerTimeWarning(remainingTimeBackgroundApp, foregroundAppHandling.categoryId)
}
// show notification
fun buildStatusMessageWithCurrentAppTitle(
text: String,
titlePrefix: String = "",
titleSuffix: String = "",
appPackageName: String?,
appActivityToShow: String?
) = AppStatusMessage(
titlePrefix + appTitleCache.query(appPackageName ?: "invalid") + titleSuffix,
text,
if (appActivityToShow != null && appPackageName != null) appActivityToShow.removePrefix(appPackageName) else null
)
fun getCategoryTitle(categoryId: String?): String = categories.find { it.id == categoryId }?.title ?: categoryId.toString()
fun buildStatusMessage(
handling: BackgroundTaskRestrictionLogicResult,
remainingTime: RemainingTime?,
suffix: String,
appPackageName: String?,
appActivityToShow: String?
): AppStatusMessage = when (handling.status) {
BackgroundTaskLogicAppStatus.ShouldBlock -> buildStatusMessageWithCurrentAppTitle(
text = appLogic.context.getString(R.string.background_logic_opening_lockscreen),
titleSuffix = suffix,
appPackageName = appPackageName,
appActivityToShow = appActivityToShow
)
BackgroundTaskLogicAppStatus.BackgroundLogicPaused -> AppStatusMessage(
title = appLogic.context.getString(R.string.background_logic_paused_title) + suffix,
text = appLogic.context.getString(R.string.background_logic_paused_text)
)
BackgroundTaskLogicAppStatus.InternalWhitelist -> buildStatusMessageWithCurrentAppTitle(
text = appLogic.context.getString(R.string.background_logic_whitelisted),
titleSuffix = suffix,
appPackageName = appPackageName,
appActivityToShow = appActivityToShow
)
BackgroundTaskLogicAppStatus.TemporarilyAllowed -> buildStatusMessageWithCurrentAppTitle(
text = appLogic.context.getString(R.string.background_logic_temporarily_allowed),
titleSuffix = suffix,
appPackageName = appPackageName,
appActivityToShow = appActivityToShow
)
BackgroundTaskLogicAppStatus.LimitsDisabled -> buildStatusMessageWithCurrentAppTitle(
text = appLogic.context.getString(R.string.background_logic_limits_disabled),
titleSuffix = suffix,
appPackageName = appPackageName,
appActivityToShow = appActivityToShow
)
BackgroundTaskLogicAppStatus.AllowedNoTimelimit -> buildStatusMessageWithCurrentAppTitle(
text = appLogic.context.getString(R.string.background_logic_no_timelimit),
titlePrefix = getCategoryTitle(handling.categoryId) + " - ",
titleSuffix = suffix,
appPackageName = appPackageName,
appActivityToShow = appActivityToShow
)
BackgroundTaskLogicAppStatus.AllowedCountAndCheckTime -> (
if (remainingTime?.usingExtraTime == true) {
// using extra time
buildStatusMessageWithCurrentAppTitle(
text = appLogic.context.getString(R.string.background_logic_using_extra_time, TimeTextUtil.remaining(remainingTime.includingExtraTime.toInt(), appLogic.context)),
titlePrefix = getCategoryTitle(handling.categoryId) + " - ",
titleSuffix = suffix,
appPackageName = appPackageName,
appActivityToShow = appActivityToShow
)
} else {
// using normal contingent
buildStatusMessageWithCurrentAppTitle(
text = TimeTextUtil.remaining(remainingTime?.default?.toInt() ?: 0, appLogic.context),
titlePrefix = getCategoryTitle(handling.categoryId) + " - ",
titleSuffix = suffix,
appPackageName = appPackageName,
appActivityToShow = appActivityToShow
)
}
)
BackgroundTaskLogicAppStatus.Idle -> AppStatusMessage(
appLogic.context.getString(R.string.background_logic_idle_title) + suffix,
appLogic.context.getString(R.string.background_logic_idle_text)
))
)
}
val showBackgroundStatus = audioPlaybackHandling.status != BackgroundTaskLogicAppStatus.Idle &&
audioPlaybackHandling.status != BackgroundTaskLogicAppStatus.ShouldBlock &&
audioPlaybackPackageName != foregroundAppPackageName
if (showBackgroundStatus && nowTimestamp % 6000 >= 3000) {
// show notification for music
appLogic.platformIntegration.setAppStatusMessage(
buildStatusMessage(
handling = audioPlaybackHandling,
remainingTime = remainingTimeBackgroundApp,
suffix = " (2/2)",
appPackageName = audioPlaybackPackageName,
appActivityToShow = null
)
)
} else {
// show regular notification
appLogic.platformIntegration.setAppStatusMessage(
buildStatusMessage(
handling = foregroundAppHandling,
remainingTime = remainingTimeForegroundApp,
suffix = if (showBackgroundStatus) " (1/2)" else "",
appPackageName = foregroundAppPackageName,
appActivityToShow = if (activityLevelBlocking) foregroundAppActivityName else null
)
)
}
// handle blocking
if (foregroundAppHandling.status == BackgroundTaskLogicAppStatus.ShouldBlock) {
openLockscreen(foregroundAppPackageName!!, foregroundAppActivityName)
usedTimeUpdateHelperForegroundApp?.commit(appLogic)
} else {
appLogic.platformIntegration.setShowBlockingOverlay(false)
}
if (audioPlaybackHandling.status == BackgroundTaskLogicAppStatus.ShouldBlock && audioPlaybackPackageName != null) {
appLogic.platformIntegration.muteAudioIfPossible(audioPlaybackPackageName)
usedTimeUpdateHelperBackgroundPlayback?.commit(appLogic)
}
} catch (ex: SecurityException) {
// this is handled by an other main loop (with a delay)
lastLoopException.postValue(ex)

View file

@ -0,0 +1,74 @@
/*
* 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 android.util.SparseArray
import androidx.lifecycle.LiveData
import io.timelimit.android.data.model.Category
import io.timelimit.android.data.model.CategoryApp
import io.timelimit.android.data.model.TimeLimitRule
import io.timelimit.android.data.model.UsedTimeItem
import io.timelimit.android.livedata.*
import java.util.*
class BackgroundTaskLogicCache (private val appLogic: AppLogic) {
val deviceUserEntryLive = SingleItemLiveDataCache(appLogic.deviceUserEntry.ignoreUnchanged())
val isThisDeviceTheCurrentDeviceLive = SingleItemLiveDataCache(appLogic.currentDeviceLogic.isThisDeviceTheCurrentDevice)
val childCategories = object: MultiKeyLiveDataCache<List<Category>, String?>() {
// key = child id
override fun createValue(key: String?): LiveData<List<Category>> {
if (key == null) {
// this should rarely happen
return liveDataFromValue(Collections.emptyList())
} else {
return appLogic.database.category().getCategoriesByChildId(key).ignoreUnchanged()
}
}
}
val appCategories = object: MultiKeyLiveDataCache<CategoryApp?, Pair<String, List<String>>>() {
// key = package name, category ids
override fun createValue(key: Pair<String, List<String>>): LiveData<CategoryApp?> {
return appLogic.database.categoryApp().getCategoryApp(key.second, key.first)
}
}
val timeLimitRules = object: MultiKeyLiveDataCache<List<TimeLimitRule>, String>() {
override fun createValue(key: String): LiveData<List<TimeLimitRule>> {
return appLogic.database.timeLimitRules().getTimeLimitRulesByCategory(key)
}
}
val usedTimesOfCategoryAndWeekByFirstDayOfWeek = object: MultiKeyLiveDataCache<SparseArray<UsedTimeItem>, Pair<String, Int>>() {
override fun createValue(key: Pair<String, Int>): LiveData<SparseArray<UsedTimeItem>> {
return appLogic.database.usedTimes().getUsedTimesOfWeek(key.first, key.second)
}
}
val usedTimesOfCategoryAndDayOfEpoch = object: MultiKeyLiveDataCache<UsedTimeItem?, Pair<String, Int>>() {
override fun createValue(key: Pair<String, Int>): LiveData<UsedTimeItem?> {
return appLogic.database.usedTimes().getUsedTimesByCategoryIdAndDayOfEpoch(key.first, key.second)
}
}
val shouldDoAutomaticSignOut = SingleItemLiveDataCacheWithRequery { -> appLogic.defaultUserLogic.hasAutomaticSignOut()}
val liveDataCaches = LiveDataCaches(arrayOf(
deviceUserEntryLive,
isThisDeviceTheCurrentDeviceLive,
childCategories,
appCategories,
timeLimitRules,
usedTimesOfCategoryAndWeekByFirstDayOfWeek,
usedTimesOfCategoryAndDayOfEpoch,
shouldDoAutomaticSignOut
))
}

View file

@ -0,0 +1,181 @@
/*
* 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.BuildConfig
import io.timelimit.android.data.model.Category
import io.timelimit.android.data.model.User
import io.timelimit.android.integration.platform.BatteryStatus
import io.timelimit.android.integration.platform.android.AndroidIntegrationApps
import io.timelimit.android.livedata.waitForNonNullValue
import io.timelimit.android.livedata.waitForNullableValue
import io.timelimit.android.logic.extension.isCategoryAllowed
object BackgroundTaskRestrictionLogic {
suspend fun getHandling(
foregroundAppPackageName: String?,
foregroundAppActivityName: String?,
pauseForegroundAppBackgroundLoop: Boolean,
temporarilyAllowedApps: List<String>,
categories: List<Category>,
activityLevelBlocking: Boolean,
deviceUserEntry: User,
batteryStatus: BatteryStatus,
shouldTrustTimeTemporarily: Boolean,
nowTimestamp: Long,
minuteOfWeek: Int,
cache: BackgroundTaskLogicCache,
result: BackgroundTaskRestrictionLogicResult
) {
if (pauseForegroundAppBackgroundLoop) {
result.status = BackgroundTaskLogicAppStatus.BackgroundLogicPaused
return
} else if (
(foregroundAppPackageName == BuildConfig.APPLICATION_ID) ||
(foregroundAppPackageName != null && AndroidIntegrationApps.ignoredApps[foregroundAppPackageName].let {
when (it) {
null -> false
AndroidIntegrationApps.IgnoredAppHandling.Ignore -> true
AndroidIntegrationApps.IgnoredAppHandling.IgnoreOnStoreOtherwiseWhitelistAndDontDisable -> BuildConfig.storeCompilant
}
}) ||
(foregroundAppPackageName != null && foregroundAppActivityName != null &&
AndroidIntegrationApps.shouldIgnoreActivity(foregroundAppPackageName, foregroundAppActivityName))
) {
result.status = BackgroundTaskLogicAppStatus.InternalWhitelist
return
} else if (foregroundAppPackageName != null && temporarilyAllowedApps.contains(foregroundAppPackageName)) {
result.status = BackgroundTaskLogicAppStatus.TemporarilyAllowed
return
} else if (foregroundAppPackageName != null) {
val categoryIds = categories.map { it.id }
val appCategory = run {
val appLevelCategoryLive = cache.appCategories.get(foregroundAppPackageName to categoryIds)
if (activityLevelBlocking && foregroundAppActivityName != null) {
val appActivityCategoryLive = cache.appCategories.get("$foregroundAppPackageName:$foregroundAppActivityName" to categoryIds)
appActivityCategoryLive.waitForNullableValue() ?: appLevelCategoryLive.waitForNullableValue()
} else {
appLevelCategoryLive.waitForNullableValue()
}
}
val category = categories.find { it.id == appCategory?.categoryId }
?: categories.find { it.id == deviceUserEntry.categoryForNotAssignedApps }
val parentCategory = categories.find { it.id == category?.parentCategoryId }
result.categoryId = category?.id
result.parentCategoryId = parentCategory?.id
if (category == null) {
result.status = BackgroundTaskLogicAppStatus.ShouldBlock
return
} else if ((!batteryStatus.isCategoryAllowed(category)) || (!batteryStatus.isCategoryAllowed(parentCategory))) {
result.status = BackgroundTaskLogicAppStatus.ShouldBlock
return
} else if (category.temporarilyBlocked or (parentCategory?.temporarilyBlocked == true)) {
result.status = BackgroundTaskLogicAppStatus.ShouldBlock
return
} else {
// disable time limits temporarily feature
if (shouldTrustTimeTemporarily && nowTimestamp < deviceUserEntry.disableLimitsUntil) {
result.status = BackgroundTaskLogicAppStatus.LimitsDisabled
return
} else if (
// check blocked time areas
// directly blocked
(category.blockedMinutesInWeek.read(minuteOfWeek)) or
(parentCategory?.blockedMinutesInWeek?.read(minuteOfWeek) == true) or
// or no safe time
(
(
(category.blockedMinutesInWeek.dataNotToModify.isEmpty == false) or
(parentCategory?.blockedMinutesInWeek?.dataNotToModify?.isEmpty == false)
) &&
(!shouldTrustTimeTemporarily)
)
) {
result.status = BackgroundTaskLogicAppStatus.ShouldBlock
return
} else {
// check time limits
val rules = cache.timeLimitRules.get(category.id).waitForNonNullValue()
val parentRules = parentCategory?.let {
cache.timeLimitRules.get(it.id).waitForNonNullValue()
} ?: emptyList()
if (rules.isEmpty() and parentRules.isEmpty()) {
// unlimited
result.status = BackgroundTaskLogicAppStatus.AllowedNoTimelimit
return
} else {
val isCurrentDevice = cache.isThisDeviceTheCurrentDeviceLive.read().waitForNonNullValue()
if (!isCurrentDevice) {
result.status = BackgroundTaskLogicAppStatus.ShouldBlock
return
} else if (shouldTrustTimeTemporarily) {
result.status = BackgroundTaskLogicAppStatus.AllowedCountAndCheckTime
return
} else {
result.status = BackgroundTaskLogicAppStatus.ShouldBlock
return
}
}
}
}
} else {
result.status = BackgroundTaskLogicAppStatus.Idle
}
}
}
class BackgroundTaskRestrictionLogicResult {
var status: BackgroundTaskLogicAppStatus = BackgroundTaskLogicAppStatus.Idle
var categoryId: String? = null
var parentCategoryId: String? = null
fun reset() {
status = BackgroundTaskLogicAppStatus.Idle
categoryId = null
parentCategoryId = null
}
}
enum class BackgroundTaskLogicAppStatus {
ShouldBlock,
BackgroundLogicPaused,
InternalWhitelist,
TemporarilyAllowed,
LimitsDisabled,
AllowedNoTimelimit,
AllowedCountAndCheckTime,
Idle
}

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 Jonas Lochmann
* TimeLimit Copyright <C> 2019- 2020 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -19,6 +19,9 @@ import android.util.SparseLongArray
import io.timelimit.android.data.model.TimeLimitRule
data class RemainingTime(val includingExtraTime: Long, val default: Long) {
val hasRemainingTime = includingExtraTime > 0
val usingExtraTime = includingExtraTime > 0 && default == 0L
init {
if (includingExtraTime < 0 || default < 0) {
throw IllegalStateException("time is < 0")

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 Jonas Lochmann
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -176,7 +176,7 @@ class ContactsFragment : Fragment() {
val logic = DefaultAppLogic.with(context!!)
try {
logic.backgroundTaskLogic.pauseBackgroundLoop = true
logic.backgroundTaskLogic.pauseForegroundAppBackgroundLoop = true
startActivity(intent)
@ -190,7 +190,7 @@ class ContactsFragment : Fragment() {
delay(500)
logic.backgroundTaskLogic.pauseBackgroundLoop = false
logic.backgroundTaskLogic.pauseForegroundAppBackgroundLoop = false
Snackbar.make(view!!, R.string.contacts_snackbar_call_started, Snackbar.LENGTH_LONG).show()
}
@ -199,7 +199,7 @@ class ContactsFragment : Fragment() {
Log.w(LOG_TAG, "could not start call", ex)
}
logic.backgroundTaskLogic.pauseBackgroundLoop = false
logic.backgroundTaskLogic.pauseForegroundAppBackgroundLoop = false
Snackbar.make(view!!, R.string.contacts_snackbar_call_failed, Snackbar.LENGTH_SHORT).show()
}

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
TimeLimit Copyright <C> 2019 Jonas Lochmann
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License.
@ -56,8 +56,7 @@
<string name="manage_device_permission_notification_access_text">
Der Benachrichtigungszugriff wird von TimeLimit genutzt, um nicht nur Apps, sondern auch
deren Benachrichtigungen zu sperren.
Zusätzlich wird dadurch die Wiedergaben von Medienplayern beendet, wenn die entsprechenden
Medienplayer gesperrt werden.
Zusätzlich wird dadurch die Hintergrundwiedergabe von Medienplayern erfasst und eingeschränkt.
</string>
<string name="manage_device_permissions_overlay_title">Über anderen Apps anzeigen</string>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
TimeLimit Copyright <C> 2019 Jonas Lochmann
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License.
@ -55,8 +55,8 @@
<string name="manage_device_permission_notification_access_title">Notification access</string>
<string name="manage_device_permission_notification_access_text">
TimeLimit uses the notification access to block notifications of blocked apps.
Notifications and their contents are not saved. Additionally, this blocks
the playback of media players if the media player is blocked.
Notifications and their contents are not saved. Additionally, this is used to
detect and block background music playback.
</string>
<string name="manage_device_permissions_overlay_title">Draw over other Apps</string>