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. 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. 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. 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. 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 * 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
@ -69,6 +69,9 @@ abstract class UsedTimeDao {
@Query("SELECT * FROM used_time WHERE category_id = :categoryId ORDER BY day_of_epoch DESC") @Query("SELECT * FROM used_time WHERE category_id = :categoryId ORDER BY day_of_epoch DESC")
abstract fun getUsedTimesByCategoryId(categoryId: String): DataSource.Factory<Int, UsedTimeItem> 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") @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>>

View file

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

View file

@ -29,6 +29,7 @@ import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.media.session.MediaSessionManager import android.media.session.MediaSessionManager
import android.media.session.PlaybackState
import android.os.Build import android.os.Build
import android.os.PowerManager import android.os.PowerManager
import android.os.UserManager import android.os.UserManager
@ -126,6 +127,23 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio
return foregroundAppHelper.getPermissionStatus() 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) { override fun showOverlayMessage(text: String) {
Toast.makeText(context, text, Toast.LENGTH_SHORT).show() Toast.makeText(context, text, Toast.LENGTH_SHORT).show()
} }

View file

@ -121,6 +121,8 @@ class DummyIntegration(
result.activityName = null result.activityName = null
} }
override fun getMusicPlaybackPackage(): String? = null
override fun setAppStatusMessage(message: AppStatusMessage?) { override fun setAppStatusMessage(message: AppStatusMessage?) {
lastAppStatusMessage = message 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 * 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,7 +18,6 @@ package io.timelimit.android.logic
import android.util.Log import android.util.Log
import android.util.SparseArray import android.util.SparseArray
import android.util.SparseLongArray import android.util.SparseLongArray
import androidx.lifecycle.LiveData
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
@ -34,9 +33,7 @@ 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.integration.platform.android.AndroidIntegrationApps
import io.timelimit.android.livedata.* 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.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
@ -49,7 +46,7 @@ import kotlinx.coroutines.sync.withLock
import java.util.* import java.util.*
class BackgroundTaskLogic(val appLogic: AppLogic) { class BackgroundTaskLogic(val appLogic: AppLogic) {
var pauseBackgroundLoop = false var pauseForegroundAppBackgroundLoop = false
val lastLoopException = MutableLiveData<Exception?>().apply { value = null } val lastLoopException = MutableLiveData<Exception?>().apply { value = null }
companion object { companion object {
@ -104,47 +101,16 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
} }
} }
private val deviceUserEntryLive = SingleItemLiveDataCache(appLogic.deviceUserEntry.ignoreUnchanged()) private val cache = BackgroundTaskLogicCache(appLogic)
private val isThisDeviceTheCurrentDeviceLive = SingleItemLiveDataCache(appLogic.currentDeviceLogic.isThisDeviceTheCurrentDevice) private val deviceUserEntryLive = cache.deviceUserEntryLive
private val childCategories = object: MultiKeyLiveDataCache<List<Category>, String?>() { private val childCategories = cache.childCategories
// key = child id private val timeLimitRules = cache.timeLimitRules
override fun createValue(key: String?): LiveData<List<Category>> { private val usedTimesOfCategoryAndWeekByFirstDayOfWeek = cache.usedTimesOfCategoryAndWeekByFirstDayOfWeek
if (key == null) { private val shouldDoAutomaticSignOut = cache.shouldDoAutomaticSignOut
// this should rarely happen private val liveDataCaches = cache.liveDataCaches
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 liveDataCaches = LiveDataCaches(arrayOf( private var usedTimeUpdateHelperForegroundApp: UsedTimeItemBatchUpdateHelper? = null
deviceUserEntryLive, private var usedTimeUpdateHelperBackgroundPlayback: UsedTimeItemBatchUpdateHelper? = null
childCategories,
appCategories,
timeLimitRules,
usedTimesOfCategoryAndWeekByFirstDayOfWeek,
shouldDoAutomaticSignOut
))
private var usedTimeUpdateHelper: UsedTimeItemBatchUpdateHelper? = null
private var previousMainLogicExecutionTime = 0 private var previousMainLogicExecutionTime = 0
private var previousMainLoopEndTime = 0L private var previousMainLoopEndTime = 0L
private val dayChangeTracker = DayChangeTracker( private val dayChangeTracker = DayChangeTracker(
@ -155,11 +121,6 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
private val appTitleCache = QueryAppTitleCache(appLogic.platformIntegration) private val appTitleCache = QueryAppTitleCache(appLogic.platformIntegration)
private suspend fun openLockscreen(blockedAppPackageName: String, blockedAppActivityName: String?) { 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) appLogic.platformIntegration.setShowBlockingOverlay(true)
if (appLogic.platformIntegration.isAccessibilityServiceEnabled()) { if (appLogic.platformIntegration.isAccessibilityServiceEnabled()) {
@ -175,6 +136,13 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
} }
private val foregroundAppSpec = ForegroundAppSpec.newInstance() 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() { private suspend fun backgroundServiceLoop() {
val realTime = RealTime.newInstance() val realTime = RealTime.newInstance()
@ -182,7 +150,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
while (true) { while (true) {
// app must be enabled // app must be enabled
if (!appLogic.enable.waitForNonNullValue()) { if (!appLogic.enable.waitForNonNullValue()) {
usedTimeUpdateHelper?.commit(appLogic) commitUsedTimeUpdaters()
liveDataCaches.removeAllItems() liveDataCaches.removeAllItems()
appLogic.platformIntegration.setAppStatusMessage(null) appLogic.platformIntegration.setAppStatusMessage(null)
appLogic.platformIntegration.setShowBlockingOverlay(false) appLogic.platformIntegration.setShowBlockingOverlay(false)
@ -195,7 +163,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
val deviceUserEntry = deviceUserEntryLive.read().waitForNullableValue() val deviceUserEntry = deviceUserEntryLive.read().waitForNullableValue()
if (deviceUserEntry == null || deviceUserEntry.type != UserType.Child) { if (deviceUserEntry == null || deviceUserEntry.type != UserType.Child) {
usedTimeUpdateHelper?.commit(appLogic) commitUsedTimeUpdaters()
val shouldDoAutomaticSignOut = shouldDoAutomaticSignOut.read() val shouldDoAutomaticSignOut = shouldDoAutomaticSignOut.read()
if (shouldDoAutomaticSignOut.waitForNonNullValue()) { if (shouldDoAutomaticSignOut.waitForNonNullValue()) {
@ -275,262 +243,406 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
appLogic.platformIntegration.getForegroundApp(foregroundAppSpec, appLogic.getForegroundAppQueryInterval()) appLogic.platformIntegration.getForegroundApp(foregroundAppSpec, appLogic.getForegroundAppQueryInterval())
val foregroundAppPackageName = foregroundAppSpec.packageName val foregroundAppPackageName = foregroundAppSpec.packageName
val foregroundAppActivityName = foregroundAppSpec.activityName val foregroundAppActivityName = foregroundAppSpec.activityName
val audioPlaybackPackageName = appLogic.platformIntegration.getMusicPlaybackPackage()
val activityLevelBlocking = appLogic.deviceEntry.value?.enableActivityLevelBlocking ?: false val activityLevelBlocking = appLogic.deviceEntry.value?.enableActivityLevelBlocking ?: false
fun showStatusMessageWithCurrentAppTitle(text: String, titlePrefix: String? = "") { foregroundAppHandling.reset()
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage( audioPlaybackHandling.reset()
titlePrefix + appTitleCache.query(foregroundAppPackageName ?: "invalid"),
text, BackgroundTaskRestrictionLogic.getHandling(
if (activityLevelBlocking) foregroundAppActivityName?.removePrefix(foregroundAppPackageName ?: "invalid") else null 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) { if (a?.childCategoryId == categoryId || a?.parentCategoryId == categoryId) {
usedTimeUpdateHelper?.commit(appLogic) return a.getCachedExtraTimeToSubtract()
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage( }
title = appLogic.context.getString(R.string.background_logic_paused_title),
text = appLogic.context.getString(R.string.background_logic_paused_text) if (b?.childCategoryId == categoryId || b?.parentCategoryId == categoryId) {
)) return b.getCachedExtraTimeToSubtract()
appLogic.platformIntegration.setShowBlockingOverlay(false) }
} else if (
(foregroundAppPackageName == BuildConfig.APPLICATION_ID) || return 0
(foregroundAppPackageName != null && AndroidIntegrationApps.ignoredApps[foregroundAppPackageName].let { }
when (it) {
null -> false suspend fun getRemainingTime(categoryId: String?, parentCategoryId: String?): RemainingTime? {
AndroidIntegrationApps.IgnoredAppHandling.Ignore -> true categoryId ?: return null
AndroidIntegrationApps.IgnoredAppHandling.IgnoreOnStoreOtherwiseWhitelistAndDontDisable -> BuildConfig.storeCompilant
} val category = categories.find { it.id == categoryId } ?: return null
}) || val parentCategory = categories.find { it.id == parentCategoryId }
(foregroundAppPackageName != null && foregroundAppActivityName != null &&
AndroidIntegrationApps.shouldIgnoreActivity(foregroundAppPackageName, foregroundAppActivityName)) val rules = timeLimitRules.get(category.id).waitForNonNullValue()
) { val parentRules = parentCategory?.let {
usedTimeUpdateHelper?.commit(appLogic) timeLimitRules.get(it.id).waitForNonNullValue()
showStatusMessageWithCurrentAppTitle( } ?: emptyList()
text = appLogic.context.getString(R.string.background_logic_whitelisted)
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 remainingParent = parentCategory?.let {
val appLevelCategoryLive = appCategories.get(foregroundAppPackageName to categoryIds) RemainingTime.getRemainingTime(
nowDate.dayOfWeek,
buildUsedTimesSparseArray(parentUsedTimes, parentCategory.id),
parentRules,
Math.max(0, parentCategory.extraTimeInMillis - getCachedExtraTimeToSubstract(parentCategory.id))
)
}
if (activityLevelBlocking) { return RemainingTime.min(remainingChild, remainingParent)
val appActivityCategoryLive = appCategories.get("$foregroundAppPackageName:$foregroundAppActivityName" to categoryIds) }
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 { } else {
appLevelCategoryLive.waitForNullableValue() usedTimeUpdateHelperBackgroundPlayback?.commit(appLogic)
usedTimeUpdateHelperBackgroundPlayback = null
} }
} }
val category = categories.find { it.id == appCategory?.categoryId } if (isBackgroundCategoryHigher) {
?: categories.find { it.id == deviceUserEntry.categoryForNotAssignedApps } usedTimeUpdateHelperBackgroundPlayback = UsedTimeItemBatchUpdateHelper.eventuallyUpdateInstance(
val parentCategory = categories.find { it.id == category?.parentCategoryId } date = nowDate,
childCategoryId = audioPlaybackHandling.categoryId!!,
parentCategoryId = audioPlaybackHandling.parentCategoryId,
oldInstance = usedTimeUpdateHelperBackgroundPlayback,
usedTimeItemForDayChild = getUsedTimeItem(audioPlaybackHandling.categoryId),
usedTimeItemForDayParent = getUsedTimeItem(audioPlaybackHandling.parentCategoryId),
logic = appLogic
)
if (category == null) { usedTimeUpdateHelperForegroundApp = usedTimeUpdateHelperBackgroundPlayback
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)
} else { } else {
// disable time limits temporarily feature usedTimeUpdateHelperForegroundApp = UsedTimeItemBatchUpdateHelper.eventuallyUpdateInstance(
if (realTime.shouldTrustTimeTemporarily && nowTimestamp < deviceUserEntry.disableLimitsUntil) { date = nowDate,
showStatusMessageWithCurrentAppTitle(appLogic.context.getString(R.string.background_logic_limits_disabled)) childCategoryId = foregroundAppHandling.categoryId!!,
appLogic.platformIntegration.setShowBlockingOverlay(false) parentCategoryId = foregroundAppHandling.parentCategoryId,
} else if ( oldInstance = usedTimeUpdateHelperForegroundApp,
// check blocked time areas usedTimeItemForDayChild = getUsedTimeItem(foregroundAppHandling.categoryId),
// directly blocked usedTimeItemForDayParent = getUsedTimeItem(foregroundAppHandling.parentCategoryId),
(category.blockedMinutesInWeek.read(minuteOfWeek)) or logic = appLogic
(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)
openLockscreen(foregroundAppPackageName, foregroundAppActivityName) usedTimeUpdateHelperBackgroundPlayback = usedTimeUpdateHelperForegroundApp
} 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)
}
}
}
} }
} else { } else {
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage( if (shouldCountForegroundApp) {
appLogic.context.getString(R.string.background_logic_idle_title), 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) 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) appLogic.platformIntegration.setShowBlockingOverlay(false)
} }
if (audioPlaybackHandling.status == BackgroundTaskLogicAppStatus.ShouldBlock && audioPlaybackPackageName != null) {
appLogic.platformIntegration.muteAudioIfPossible(audioPlaybackPackageName)
usedTimeUpdateHelperBackgroundPlayback?.commit(appLogic)
}
} catch (ex: SecurityException) { } catch (ex: SecurityException) {
// this is handled by an other main loop (with a delay) // this is handled by an other main loop (with a delay)
lastLoopException.postValue(ex) 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 * 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,6 +19,9 @@ import android.util.SparseLongArray
import io.timelimit.android.data.model.TimeLimitRule import io.timelimit.android.data.model.TimeLimitRule
data class RemainingTime(val includingExtraTime: Long, val default: Long) { data class RemainingTime(val includingExtraTime: Long, val default: Long) {
val hasRemainingTime = includingExtraTime > 0
val usingExtraTime = includingExtraTime > 0 && default == 0L
init { init {
if (includingExtraTime < 0 || default < 0) { if (includingExtraTime < 0 || default < 0) {
throw IllegalStateException("time is < 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 * 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
@ -176,7 +176,7 @@ class ContactsFragment : Fragment() {
val logic = DefaultAppLogic.with(context!!) val logic = DefaultAppLogic.with(context!!)
try { try {
logic.backgroundTaskLogic.pauseBackgroundLoop = true logic.backgroundTaskLogic.pauseForegroundAppBackgroundLoop = true
startActivity(intent) startActivity(intent)
@ -190,7 +190,7 @@ class ContactsFragment : Fragment() {
delay(500) delay(500)
logic.backgroundTaskLogic.pauseBackgroundLoop = false logic.backgroundTaskLogic.pauseForegroundAppBackgroundLoop = false
Snackbar.make(view!!, R.string.contacts_snackbar_call_started, Snackbar.LENGTH_LONG).show() 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) 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() 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"?> <?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.
@ -56,8 +56,7 @@
<string name="manage_device_permission_notification_access_text"> <string name="manage_device_permission_notification_access_text">
Der Benachrichtigungszugriff wird von TimeLimit genutzt, um nicht nur Apps, sondern auch Der Benachrichtigungszugriff wird von TimeLimit genutzt, um nicht nur Apps, sondern auch
deren Benachrichtigungen zu sperren. deren Benachrichtigungen zu sperren.
Zusätzlich wird dadurch die Wiedergaben von Medienplayern beendet, wenn die entsprechenden Zusätzlich wird dadurch die Hintergrundwiedergabe von Medienplayern erfasst und eingeschränkt.
Medienplayer gesperrt werden.
</string> </string>
<string name="manage_device_permissions_overlay_title">Über anderen Apps anzeigen</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"?> <?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.
@ -55,8 +55,8 @@
<string name="manage_device_permission_notification_access_title">Notification access</string> <string name="manage_device_permission_notification_access_title">Notification access</string>
<string name="manage_device_permission_notification_access_text"> <string name="manage_device_permission_notification_access_text">
TimeLimit uses the notification access to block notifications of blocked apps. TimeLimit uses the notification access to block notifications of blocked apps.
Notifications and their contents are not saved. Additionally, this blocks Notifications and their contents are not saved. Additionally, this is used to
the playback of media players if the media player is blocked. detect and block background music playback.
</string> </string>
<string name="manage_device_permissions_overlay_title">Draw over other Apps</string> <string name="manage_device_permissions_overlay_title">Draw over other Apps</string>