Initial commit

This commit is contained in:
Jonas L 2019-01-14 10:11:01 +01:00
commit afb86e9d0e
420 changed files with 26435 additions and 0 deletions

View file

@ -0,0 +1,463 @@
/*
* Open TimeLimit Copyright <C> 2019 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.Log
import android.util.SparseArray
import android.util.SparseLongArray
import androidx.lifecycle.LiveData
import io.timelimit.android.BuildConfig
import io.timelimit.android.R
import io.timelimit.android.async.Threads
import io.timelimit.android.coroutines.executeAndWait
import io.timelimit.android.coroutines.runAsync
import io.timelimit.android.coroutines.runAsyncExpectForever
import io.timelimit.android.data.backup.DatabaseBackup
import io.timelimit.android.data.model.*
import io.timelimit.android.date.DateInTimezone
import io.timelimit.android.date.getMinuteOfWeek
import io.timelimit.android.integration.platform.AppStatusMessage
import io.timelimit.android.integration.platform.ProtectionLevel
import io.timelimit.android.integration.platform.android.AndroidIntegrationApps
import io.timelimit.android.livedata.*
import io.timelimit.android.sync.actions.UpdateDeviceStatusAction
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
import io.timelimit.android.util.TimeTextUtil
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.util.*
class BackgroundTaskLogic(val appLogic: AppLogic) {
companion object {
private const val CHECK_PERMISSION_INTERVAL = 10 * 1000L // all 10 seconds
private const val BACKGROUND_SERVICE_INTERVAL = 100L // all 100 ms
private const val MAX_USED_TIME_PER_ROUND = 1000 // 1 second
private const val LOG_TAG = "BackgroundTaskLogic"
}
private val temporarilyAllowedApps = appLogic.database.temporarilyAllowedApp().getTemporarilyAllowedApps()
init {
runAsyncExpectForever { backgroundServiceLoop() }
runAsyncExpectForever { syncDeviceStatusLoop() }
runAsyncExpectForever { backupDatabaseLoop() }
runAsync {
// this is effective after an reboot
if (appLogic.deviceEntryIfEnabled.waitForNullableValue() != null) {
appLogic.platformIntegration.setEnableSystemLockdown(true)
}
}
appLogic.deviceEntryIfEnabled
.map { it?.id }
.ignoreUnchanged()
.observeForever {
_ ->
runAsync {
syncInstalledAppVersion()
}
}
temporarilyAllowedApps.map { it.isNotEmpty() }.ignoreUnchanged().observeForever {
appLogic.platformIntegration.setShowNotificationToRevokeTemporarilyAllowedApps(it!!)
}
}
private val deviceUserEntryLive = SingleItemLiveDataCache(appLogic.deviceUserEntry.ignoreUnchanged())
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 liveDataCaches = LiveDataCaches(arrayOf(
deviceUserEntryLive,
childCategories,
appCategories,
timeLimitRules,
usedTimesOfCategoryAndWeekByFirstDayOfWeek
))
private var usedTimeUpdateHelper: UsedTimeItemBatchUpdateHelper? = null
private var previousMainLogicExecutionTime = 0
private var previousMainLoopEndTime = 0L
private val appTitleCache = QueryAppTitleCache(appLogic.platformIntegration)
private suspend fun backgroundServiceLoop() {
while (true) {
// app must be enabled
if (!appLogic.enable.waitForNonNullValue()) {
usedTimeUpdateHelper?.commit(appLogic)
liveDataCaches.removeAllItems()
appLogic.platformIntegration.setAppStatusMessage(null)
appLogic.enable.waitUntilValueMatches { it == true }
continue
}
// device must be used by a child
val deviceUserEntry = deviceUserEntryLive.read().waitForNullableValue()
if (deviceUserEntry == null || deviceUserEntry.type != UserType.Child) {
usedTimeUpdateHelper?.commit(appLogic)
liveDataCaches.removeAllItems()
appLogic.platformIntegration.setAppStatusMessage(null)
deviceUserEntryLive.read().waitUntilValueMatches { it != null && it.type == UserType.Child }
continue
}
// loop logic
try {
// get the current time
val nowTimestamp = appLogic.timeApi.getCurrentTimeInMillis()
// get the categories
val categories = childCategories.get(deviceUserEntry.id).waitForNonNullValue()
val temporarilyAllowedApps = temporarilyAllowedApps.waitForNonNullValue()
// get the current status
val isScreenOn = appLogic.platformIntegration.isScreenOn()
if (!isScreenOn) {
if (temporarilyAllowedApps.isNotEmpty()) {
resetTemporarilyAllowedApps()
}
}
val foregroundAppPackageName = appLogic.platformIntegration.getForegroundAppPackageName()
// the following is not executed if the permission is missing
if (foregroundAppPackageName == BuildConfig.APPLICATION_ID) {
// this app itself runs now -> no need for an status message
usedTimeUpdateHelper?.commit(appLogic)
appLogic.platformIntegration.setAppStatusMessage(null)
} else if (foregroundAppPackageName != null && AndroidIntegrationApps.ignoredApps.contains(foregroundAppPackageName)) {
usedTimeUpdateHelper?.commit(appLogic)
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
appTitleCache.query(foregroundAppPackageName),
appLogic.context.getString(R.string.background_logic_whitelisted)
))
} else if (foregroundAppPackageName != null && temporarilyAllowedApps.contains(foregroundAppPackageName)) {
usedTimeUpdateHelper?.commit(appLogic)
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
appTitleCache.query(foregroundAppPackageName),
appLogic.context.getString(R.string.background_logic_temporarily_allowed)
))
} else if (foregroundAppPackageName != null) {
val appCategory = appCategories.get(Pair(foregroundAppPackageName, categories.map { it.id })).waitForNullableValue()
val category = categories.find { it.id == appCategory?.categoryId }
if (category == null) {
usedTimeUpdateHelper?.commit(appLogic)
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
title = appTitleCache.query(foregroundAppPackageName),
text = appLogic.context.getString(R.string.background_logic_opening_lockscreen)
))
appLogic.platformIntegration.setSuspendedApps(listOf(foregroundAppPackageName), true)
appLogic.platformIntegration.showAppLockScreen(foregroundAppPackageName)
} else if (category.temporarilyBlocked) {
usedTimeUpdateHelper?.commit(appLogic)
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
title = appTitleCache.query(foregroundAppPackageName),
text = appLogic.context.getString(R.string.background_logic_opening_lockscreen)
))
appLogic.platformIntegration.showAppLockScreen(foregroundAppPackageName)
} else {
val nowTimezone = TimeZone.getTimeZone(deviceUserEntry.timeZone)
val nowDate = DateInTimezone.newInstance(nowTimestamp, nowTimezone)
val minuteOfWeek = getMinuteOfWeek(nowTimestamp, nowTimezone)
// disable time limits temporarily feature
if (nowTimestamp < deviceUserEntry.disableLimitsUntil) {
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
title = appTitleCache.query(foregroundAppPackageName),
text = appLogic.context.getString(R.string.background_logic_limits_disabled)
))
} else if (
// check blocked time areas
(category.blockedMinutesInWeek.read(minuteOfWeek))
) {
usedTimeUpdateHelper?.commit(appLogic)
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
title = appTitleCache.query(foregroundAppPackageName),
text = appLogic.context.getString(R.string.background_logic_opening_lockscreen)
))
appLogic.platformIntegration.showAppLockScreen(foregroundAppPackageName)
} else {
// check time limits
val rules = timeLimitRules.get(category.id).waitForNonNullValue()
if (rules.isEmpty()) {
// unlimited
usedTimeUpdateHelper?.commit(appLogic)
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
category.title + " - " + appTitleCache.query(foregroundAppPackageName),
appLogic.context.getString(R.string.background_logic_no_timelimit)
))
} else {
val usedTimes = usedTimesOfCategoryAndWeekByFirstDayOfWeek.get(Pair(category.id, nowDate.dayOfEpoch - nowDate.dayOfWeek)).waitForNonNullValue()
val newUsedTimeItemBatchUpdateHelper = UsedTimeItemBatchUpdateHelper.eventuallyUpdateInstance(
date = nowDate,
categoryId = category.id,
oldInstance = usedTimeUpdateHelper,
usedTimeItemForDay = usedTimes.get(nowDate.dayOfWeek),
logic = appLogic
)
usedTimeUpdateHelper = newUsedTimeItemBatchUpdateHelper
val usedTimesSparseArray = SparseLongArray()
for (i in 0..6) {
val usedTimesItem = usedTimes[i]?.usedMillis
if (newUsedTimeItemBatchUpdateHelper.date.dayOfWeek == i) {
usedTimesSparseArray.put(i, newUsedTimeItemBatchUpdateHelper.getTotalUsedTime())
} else {
usedTimesSparseArray.put(i, (if (usedTimesItem != null) usedTimesItem else 0))
}
}
val remaining = RemainingTime.getRemainingTime(
nowDate.dayOfWeek, usedTimesSparseArray, rules,
Math.max(0, category.extraTimeInMillis - newUsedTimeItemBatchUpdateHelper.getCachedExtraTimeToSubtract())
)
if (remaining == null) {
// unlimited
usedTimeUpdateHelper?.commit(appLogic)
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
category.title + " - " + appTitleCache.query(foregroundAppPackageName),
appLogic.context.getString(R.string.background_logic_no_timelimit)
))
} else {
// time limited
if (remaining.includingExtraTime > 0) {
if (remaining.default == 0L) {
// using extra time
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
category.title + " - " + appTitleCache.query(foregroundAppPackageName),
appLogic.context.getString(R.string.background_logic_using_extra_time, TimeTextUtil.remaining(remaining.includingExtraTime.toInt(), appLogic.context))
))
if (isScreenOn) {
newUsedTimeItemBatchUpdateHelper.addUsedTime(
Math.min(previousMainLogicExecutionTime, MAX_USED_TIME_PER_ROUND), // never save more than a second of used time
true,
appLogic
)
}
} else {
// using normal contingent
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
category.title + " - " + appTitleCache.query(foregroundAppPackageName),
TimeTextUtil.remaining(remaining.default.toInt(), appLogic.context)
))
if (isScreenOn) {
newUsedTimeItemBatchUpdateHelper.addUsedTime(
Math.min(previousMainLogicExecutionTime, MAX_USED_TIME_PER_ROUND), // never save more than a second of used time
false,
appLogic
)
}
}
} else {
// there is not time anymore
newUsedTimeItemBatchUpdateHelper.commit(appLogic)
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
title = appTitleCache.query(foregroundAppPackageName),
text = appLogic.context.getString(R.string.background_logic_opening_lockscreen)
))
appLogic.platformIntegration.showAppLockScreen(foregroundAppPackageName)
}
}
}
}
}
} else {
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
appLogic.context.getString(R.string.background_logic_idle_title),
appLogic.context.getString(R.string.background_logic_idle_text)
))
}
} catch (ex: SecurityException) {
// this is handled by an other main loop (with a delay)
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
appLogic.context.getString(R.string.background_logic_error),
appLogic.context.getString(R.string.background_logic_error_permission)
))
} catch (ex: Exception) {
if (BuildConfig.DEBUG) {
Log.w(LOG_TAG, "exception during running main loop", ex)
}
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
appLogic.context.getString(R.string.background_logic_error),
appLogic.context.getString(R.string.background_logic_error_internal)
))
}
liveDataCaches.reportLoopDone()
// delay before running next time
val endTime = appLogic.timeApi.getCurrentUptimeInMillis()
previousMainLogicExecutionTime = (endTime - previousMainLoopEndTime).toInt()
previousMainLoopEndTime = endTime
val timeToWait = Math.max(10, BACKGROUND_SERVICE_INTERVAL - previousMainLogicExecutionTime)
appLogic.timeApi.sleep(timeToWait)
}
}
private suspend fun syncInstalledAppVersion() {
val currentAppVersion = BuildConfig.VERSION_CODE
val deviceEntry = appLogic.deviceEntry.waitForNullableValue()
if (deviceEntry != null) {
if (deviceEntry.currentAppVersion != currentAppVersion) {
ApplyActionUtil.applyAppLogicAction(
UpdateDeviceStatusAction(
newProtectionLevel = null,
newUsageStatsPermissionStatus = null,
newNotificationAccessPermission = null,
newAppVersion = currentAppVersion
),
appLogic
)
}
}
}
fun syncDeviceStatusAsync() {
runAsync {
syncDeviceStatus()
}
}
private suspend fun syncDeviceStatusLoop() {
while (true) {
appLogic.deviceEntryIfEnabled.waitUntilValueMatches { it != null }
syncDeviceStatus()
appLogic.timeApi.sleep(CHECK_PERMISSION_INTERVAL)
}
}
private val syncDeviceStatusLock = Mutex()
private suspend fun syncDeviceStatus() {
syncDeviceStatusLock.withLock {
val deviceEntry = appLogic.deviceEntry.waitForNullableValue()
if (deviceEntry != null) {
val protectionLevel = appLogic.platformIntegration.getCurrentProtectionLevel()
val usageStatsPermission = appLogic.platformIntegration.getForegroundAppPermissionStatus()
val notificationAccess = appLogic.platformIntegration.getNotificationAccessPermissionStatus()
val emptyChanges = UpdateDeviceStatusAction(
newProtectionLevel = null,
newUsageStatsPermissionStatus = null,
newNotificationAccessPermission = null,
newAppVersion = null
)
var changes = emptyChanges
if (protectionLevel != deviceEntry.currentProtectionLevel) {
changes = changes.copy(
newProtectionLevel = protectionLevel
)
if (protectionLevel == ProtectionLevel.DeviceOwner) {
appLogic.platformIntegration.setEnableSystemLockdown(true)
}
}
if (usageStatsPermission != deviceEntry.currentUsageStatsPermission) {
changes = changes.copy(
newUsageStatsPermissionStatus = usageStatsPermission
)
}
if (notificationAccess != deviceEntry.currentNotificationAccessPermission) {
changes = changes.copy(
newNotificationAccessPermission = notificationAccess
)
}
if (changes != emptyChanges) {
ApplyActionUtil.applyAppLogicAction(changes, appLogic)
}
}
}
}
suspend fun resetTemporarilyAllowedApps() {
Threads.database.executeAndWait(Runnable {
appLogic.database.temporarilyAllowedApp().removeAllTemporarilyAllowedAppsSync()
})
}
private suspend fun backupDatabaseLoop() {
appLogic.timeApi.sleep(1000 * 60 * 5 /* 5 minutes */)
while (true) {
DatabaseBackup.with(appLogic.context).tryCreateDatabaseBackupAsync()
appLogic.timeApi.sleep(1000 * 60 * 60 * 3 /* 3 hours */)
}
}
}