mirror of
https://codeberg.org/timelimit/timelimit-android.git
synced 2025-10-05 19:42:20 +02:00
Add counting background music playback
This commit is contained in:
parent
c632609211
commit
192b871ae5
13 changed files with 694 additions and 301 deletions
|
@ -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.
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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>>
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -121,6 +121,8 @@ class DummyIntegration(
|
|||
result.activityName = null
|
||||
}
|
||||
|
||||
override fun getMusicPlaybackPackage(): String? = null
|
||||
|
||||
override fun setAppStatusMessage(message: AppStatusMessage?) {
|
||||
lastAppStatusMessage = message
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
))
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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")
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue