Refactor blocking logic

This commit is contained in:
Jonas Lochmann 2020-06-29 02:00:00 +02:00
parent 5f6a1edd1a
commit 099c781f18
No known key found for this signature in database
GPG key ID: 8B8C9AEE10FA5B36
45 changed files with 2786 additions and 1950 deletions

View file

@ -16,7 +16,9 @@
package io.timelimit.android.data package io.timelimit.android.data
import io.timelimit.android.data.dao.* import io.timelimit.android.data.dao.*
import java.io.Closeable import io.timelimit.android.data.invalidation.Observer
import io.timelimit.android.data.invalidation.Table
import java.lang.ref.WeakReference
interface Database { interface Database {
fun app(): AppDao fun app(): AppDao
@ -34,8 +36,13 @@ interface Database {
fun allowedContact(): AllowedContactDao fun allowedContact(): AllowedContactDao
fun userKey(): UserKeyDao fun userKey(): UserKeyDao
fun sessionDuration(): SessionDurationDao fun sessionDuration(): SessionDurationDao
fun derivedDataDao(): DerivedDataDao
fun <T> runInTransaction(block: () -> T): T fun <T> runInTransaction(block: () -> T): T
fun <T> runInUnobservedTransaction(block: () -> T): T
fun registerWeakObserver(tables: Array<Table>, observer: WeakReference<Observer>)
fun registerTransactionCommitListener(listener: () -> Unit)
fun unregisterTransactionCommitListener(listener: () -> Unit)
fun deleteAllData() fun deleteAllData()
fun close() fun close()

View file

@ -15,11 +15,19 @@
*/ */
package io.timelimit.android.data package io.timelimit.android.data
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import androidx.room.Database import androidx.room.Database
import androidx.room.InvalidationTracker
import androidx.room.Room import androidx.room.Room
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import io.timelimit.android.data.dao.DerivedDataDao
import io.timelimit.android.data.invalidation.Observer
import io.timelimit.android.data.invalidation.Table
import io.timelimit.android.data.invalidation.TableUtil
import io.timelimit.android.data.model.* import io.timelimit.android.data.model.*
import java.lang.ref.WeakReference
import java.util.concurrent.CountDownLatch
@Database(entities = [ @Database(entities = [
User::class, User::class,
@ -107,11 +115,84 @@ abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database
} }
} }
private val derivedDataDao: DerivedDataDao by lazy { DerivedDataDao(this) }
override fun derivedDataDao(): DerivedDataDao = derivedDataDao
private val transactionCommitListeners = mutableSetOf<() -> Unit>()
override fun registerTransactionCommitListener(listener: () -> Unit): Unit = synchronized(transactionCommitListeners) {
transactionCommitListeners.add(listener)
}
override fun unregisterTransactionCommitListener(listener: () -> Unit): Unit = synchronized(transactionCommitListeners) {
transactionCommitListeners.remove(listener)
}
// the room compiler needs this // the room compiler needs this
override fun <T> runInTransaction(block: () -> T): T { override fun <T> runInTransaction(block: () -> T): T {
return super.runInTransaction(block) return super.runInTransaction(block)
} }
override fun <T> runInUnobservedTransaction(block: () -> T): T {
openHelper.readableDatabase.beginTransaction()
try {
val result = block()
openHelper.readableDatabase.setTransactionSuccessful()
return result
} finally {
openHelper.readableDatabase.endTransaction()
}
}
@SuppressLint("RestrictedApi")
override fun endTransaction() {
openHelper.writableDatabase.endTransaction()
if (!inTransaction()) {
// block the query thread of room until this is done
val latch = CountDownLatch(1)
try {
queryExecutor.execute { latch.await() }
// without requesting a async refresh, no sync refresh will happen
invalidationTracker.refreshVersionsAsync()
invalidationTracker.refreshVersionsSync()
openHelper.readableDatabase.beginTransaction()
try {
synchronized(transactionCommitListeners) { transactionCommitListeners.toList() }.forEach { it() }
} finally {
openHelper.readableDatabase.endTransaction()
}
} finally {
latch.countDown()
}
}
}
override fun registerWeakObserver(tables: Array<Table>, observer: WeakReference<Observer>) {
val tableNames = arrayOfNulls<String>(tables.size)
tables.forEachIndexed { index, table ->
tableNames[index] = TableUtil.toName(table)
}
invalidationTracker.addObserver(object: InvalidationTracker.Observer(tableNames) {
override fun onInvalidated(tables: MutableSet<String>) {
val item = observer.get()
if (item != null) {
item.onInvalidated(tables.map { TableUtil.toEnum(it) }.toSet())
} else {
invalidationTracker.removeObserver(this)
}
}
})
}
override fun deleteAllData() { override fun deleteAllData() {
clearAllTables() clearAllTables()
} }

View file

@ -0,0 +1,132 @@
/*
* 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.data.cache.multi
import android.os.SystemClock
import io.timelimit.android.async.Threads
import java.util.concurrent.atomic.AtomicInteger
fun <K, V> DataCacheUserInterface<K, V>.delayClosingItems(delay: Long): DataCacheUserInterface<K, V> {
if (delay <= 0) return this
fun now() = SystemClock.uptimeMillis()
val handler = Threads.mainThreadHandler
val parent = this
val lock = Object()
// 0 never occurs in user counters, key is only in wipe times or user counters
val userCounters = mutableMapOf<K, AtomicInteger>()
val wipeTimes = mutableMapOf<K, Long>()
var isClosed = false
var minWipeTime = Long.MAX_VALUE
lateinit var handleWipingRunnable: Runnable
fun scheduleWipingRunnable() = synchronized(lock) {
handler.removeCallbacks(handleWipingRunnable)
if (minWipeTime != Long.MAX_VALUE) {
val nextRunDelay = minWipeTime - now()
handler.postDelayed(handleWipingRunnable, nextRunDelay.coerceAtLeast(10))
}
}
handleWipingRunnable = Runnable {
synchronized(lock) {
if (isClosed) {
return@synchronized
}
val now = now()
var nextWipeTime = Long.MAX_VALUE
val iterator = wipeTimes.entries.iterator()
for ((key, time) in iterator) {
if (time >= now) {
parent.close(key, null)
iterator.remove()
} else {
nextWipeTime = nextWipeTime.coerceAtMost(time)
}
}
minWipeTime = nextWipeTime
scheduleWipingRunnable()
}
}
return object: DataCacheUserInterface<K, V> {
override fun openSync(key: K, listener: DataCacheListener<K, V>?): V {
val isFirstOpen = synchronized(lock) {
if (wipeTimes.containsKey(key)) {
wipeTimes.remove(key)
userCounters[key] = AtomicInteger(1)
false
} else {
val counter = userCounters[key]
?: AtomicInteger(0).also { userCounters[key] = it }
counter.getAndIncrement() == 0
}
}
// do one more open at the first open
if (isFirstOpen) {
parent.openSync(key, null)
}
return parent.openSync(key, listener)
}
override fun close(key: K, listener: DataCacheListener<K, V>?) {
synchronized(lock) {
val counter = userCounters[key]!!
val isLastClose = counter.decrementAndGet() == 0
if (isLastClose) {
val now = now()
val closeTime = now + delay
userCounters.remove(key)
wipeTimes[key] = closeTime
if (closeTime < minWipeTime) {
minWipeTime = closeTime
scheduleWipingRunnable()
}
}
isLastClose
}
parent.close(key, listener)
}
override fun close() {
synchronized(lock) {
isClosed = true
parent.close()
}
}
}
}

View file

@ -0,0 +1,154 @@
/*
* 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.data.cache.multi
internal class ListenerHolder<K, V> (val listener: DataCacheListener<K, V>) {
var closed = false
}
internal class DataCacheElement<K, IV, EV> (var value: IV) {
var users = 1
val listeners = mutableListOf<ListenerHolder<K, EV>>()
}
// thread safe, but most likely slower than possible
fun <K, IV, EV> DataCacheHelperInterface<K, IV, EV>.createCache(): DataCache<K, EV> {
val helper = this
val elements = mutableMapOf<K, DataCacheElement<K, IV, EV>>()
val updateLock = Object()
val closeLock = Object()
var isClosed = false
fun assertNotClosed() {
if (isClosed) {
throw IllegalStateException()
}
}
fun updateSync(key: K, item: DataCacheElement<K, IV, EV>) {
synchronized(updateLock) {
assertNotClosed()
val oldValue = item.value
val newValue = helper.updateItemSync(key, oldValue)
if (newValue !== oldValue) {
item.value = newValue
val listeners = synchronized(closeLock) { item.listeners.toList() }
listeners.forEach {
if (!it.closed) {
it.listener.onElementUpdated(key, helper.prepareForUser(oldValue), helper.prepareForUser(newValue))
}
}
}
}
}
fun updateSync() {
synchronized(updateLock) {
assertNotClosed()
elements.forEach { updateSync(it.key, it.value) }
}
}
fun openSync(key: K, listener: DataCacheListener<K, EV>?): EV {
synchronized(updateLock) {
assertNotClosed()
val oldItemToReturn = synchronized(closeLock) {
elements[key]?.also { oldItem -> oldItem.users++ }
}
if (oldItemToReturn != null) {
updateSync(key, oldItemToReturn)
synchronized(closeLock) {
if (listener != null) {
if (oldItemToReturn.listeners.find { it.listener === listener } == null) {
oldItemToReturn.listeners.add(ListenerHolder(listener))
}
}
}
return helper.prepareForUser(oldItemToReturn.value)
}
val value = helper.openItemSync(key)
synchronized(closeLock) {
elements[key] = DataCacheElement<K, IV, EV>(value).also {
if (listener != null) {
it.listeners.add(ListenerHolder(listener))
}
}
}
return helper.prepareForUser(value)
}
}
fun close(key: K, listener: DataCacheListener<K, EV>?) {
synchronized(closeLock) {
assertNotClosed()
val item = elements[key] ?: throw IllegalStateException()
item.listeners.removeAll { if (it.listener === listener) { it.closed = true; true } else false }
item.users--
if (item.users < 0) {
throw IllegalStateException()
}
if (item.users == 0) {
if (item.listeners.isNotEmpty()) {
throw IllegalStateException()
}
helper.disposeItemFast(key, item.value)
elements.remove(key)
}
}
}
fun close() {
synchronized(updateLock) {
synchronized(closeLock) {
assertNotClosed()
elements.entries.forEach { it.value.listeners.clear(); helper.disposeItemFast(it.key, it.value.value) }
elements.clear()
isClosed = true
helper.close()
}
}
}
val ownerInterface = object: DataCacheOwnerInterface { override fun updateSync() = helper.wrapOpenOrUpdate { updateSync() } }
val userInterface = object: DataCacheUserInterface<K, EV> {
override fun openSync(key: K, listener: DataCacheListener<K, EV>?): EV = helper.wrapOpenOrUpdate { openSync(key, listener) }
override fun close(key: K, listener: DataCacheListener<K, EV>?) = close(key, listener)
override fun close() = close()
}
return DataCache(ownerInterface, userInterface)
}

View file

@ -0,0 +1,43 @@
/*
* 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.data.cache.multi
interface DataCacheUserInterface<K, V>: AutoCloseable {
fun openSync(key: K, listener: DataCacheListener<K, V>?): V
fun close(key: K, listener: DataCacheListener<K, V>?)
}
interface DataCacheOwnerInterface {
fun updateSync()
}
data class DataCache<K, V>(
val ownerInterface: DataCacheOwnerInterface,
val userInterface: DataCacheUserInterface<K, V>
)
interface DataCacheListener<K, V> {
fun onElementUpdated(key: K, oldValue: V, newValue: V): Unit
}
interface DataCacheHelperInterface<K, IV, EV> {
fun openItemSync(key: K): IV
fun updateItemSync(key: K, item: IV): IV
fun disposeItemFast(key: K, item: IV)
fun close()
fun prepareForUser(item: IV): EV
fun <R> wrapOpenOrUpdate(block: () -> R): R
}

View file

@ -0,0 +1,51 @@
/*
* 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.data.cache.multi
import androidx.lifecycle.LiveData
import io.timelimit.android.async.Threads
import java.util.concurrent.Executor
fun <K, V> DataCacheUserInterface<K, V>.openLive(key: K, executor: Executor): LiveData<V> {
val cache = this
return object: LiveData<V>() {
val listener = object: DataCacheListener<K, V> {
override fun onElementUpdated(key: K, oldValue: V, newValue: V) {
postValue(newValue)
}
}
override fun onActive() {
super.onActive()
executor.execute {
val initialValue = cache.openSync(key, listener)
postValue(initialValue)
}
}
override fun onInactive() {
super.onInactive()
cache.close(key, listener)
}
}
}
fun <K, V> DataCacheUserInterface<K, V>.openLiveAtDatabaseThread(key: K) = openLive(key, Threads.database)

View file

@ -0,0 +1,64 @@
/*
* 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.data.cache.single
import io.timelimit.android.async.Threads
fun <V> SingleItemDataCacheUserInterface<V>.delayClosingItem(delay: Long): SingleItemDataCacheUserInterface<V> {
if (delay <= 0) return this
val handler = Threads.mainThreadHandler
val parent = this
val lock = Object()
var userCounter = 0
val doWipeRunnable = Runnable {
synchronized(lock) {
if (userCounter == 0) {
parent.close(null)
}
}
}
return object: SingleItemDataCacheUserInterface<V> {
override fun openSync(listener: SingleItemDataCacheListener<V>?): V {
val isFirstOpen = synchronized(lock) {
if (userCounter++ == 0) {
handler.removeCallbacks(doWipeRunnable)
true
} else {
false
}
}
if (isFirstOpen) { openSync(null) }
return parent.openSync(listener)
}
override fun close(listener: SingleItemDataCacheListener<V>?) = synchronized(lock) {
if (userCounter <= 0) { throw IllegalStateException() }
parent.close(listener)
if (--userCounter == 0) {
handler.postDelayed(doWipeRunnable, delay)
}
}
}
}

View file

@ -0,0 +1,121 @@
/*
* 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.data.cache.single
internal class ListenerHolder<V> (val listener: SingleItemDataCacheListener<V>) {
var closed = false
}
internal class DataCacheElement<IV, EV> (var value: IV) {
var users = 1
val listeners = mutableListOf<ListenerHolder<EV>>()
}
// thread safe, but most likely slower than possible
fun <IV, EV> SingleItemDataCacheHelperInterface<IV, EV>.createCache(): SingleItemDataCache<EV> {
val helper = this
val updateLock = Object()
val closeLock = Object()
var element: DataCacheElement<IV, EV>? = null
fun updateSync() {
synchronized(updateLock) {
val item = element ?: return@synchronized
val oldValue = item.value
val newValue = helper.updateItemSync(item.value)
if (newValue !== oldValue) {
item.value = newValue
val listeners = synchronized(closeLock) { item.listeners.toList() }
listeners.forEach {
if (!it.closed) {
it.listener.onElementUpdated(helper.prepareForUser(oldValue), helper.prepareForUser(newValue))
}
}
}
}
}
fun openSync(listener: SingleItemDataCacheListener<EV>?): EV {
synchronized(updateLock) {
val oldItemToReturn = synchronized(closeLock) {
element?.also { oldItem -> oldItem.users++ }
}
if (oldItemToReturn != null) {
updateSync()
synchronized(closeLock) {
if (listener != null) {
if (oldItemToReturn.listeners.find { it.listener === listener } == null) {
oldItemToReturn.listeners.add(ListenerHolder(listener))
}
}
}
return helper.prepareForUser(oldItemToReturn.value)
} else {
val value = helper.openItemSync()
synchronized(closeLock) {
element = DataCacheElement<IV, EV>(value).also {
if (listener != null) {
it.listeners.add(ListenerHolder(listener))
}
}
}
return helper.prepareForUser(value)
}
}
}
fun close(listener: SingleItemDataCacheListener<EV>?) {
synchronized(closeLock) {
val item = element ?: throw IllegalStateException()
item.listeners.removeAll { if (it.listener === listener) { it.closed = true; true } else false }
item.users--
if (item.users < 0) {
throw IllegalStateException()
}
if (item.users == 0) {
if (item.listeners.isNotEmpty()) {
throw IllegalStateException()
}
helper.disposeItemFast(item.value)
element = null
}
}
}
val ownerInterface = object: SingleItemDataCacheOwnerInterface { override fun updateSync() = helper.wrapOpenOrUpdate { updateSync() } }
val userInterface = object: SingleItemDataCacheUserInterface<EV> {
override fun openSync(listener: SingleItemDataCacheListener<EV>?): EV = helper.wrapOpenOrUpdate { openSync(listener) }
override fun close(listener: SingleItemDataCacheListener<EV>?) = close(listener)
}
return SingleItemDataCache(ownerInterface, userInterface)
}

View file

@ -0,0 +1,43 @@
/*
* 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.data.cache.single
interface SingleItemDataCacheUserInterface<V> {
fun openSync(listener: SingleItemDataCacheListener<V>?): V
fun close(listener: SingleItemDataCacheListener<V>?)
}
interface SingleItemDataCacheOwnerInterface {
fun updateSync()
}
data class SingleItemDataCache<V>(
val ownerInterface: SingleItemDataCacheOwnerInterface,
val userInterface: SingleItemDataCacheUserInterface<V>
)
interface SingleItemDataCacheListener<V> {
fun onElementUpdated(oldValue: V, newValue: V): Unit
}
interface SingleItemDataCacheHelperInterface<IV, EV> {
fun openItemSync(): IV
fun updateItemSync(item: IV): IV
fun disposeItemFast(item: IV)
fun prepareForUser(item: IV): EV
fun <R> wrapOpenOrUpdate(block: () -> R): R
}

View file

@ -0,0 +1,51 @@
/*
* 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.data.cache.single
import androidx.lifecycle.LiveData
import io.timelimit.android.async.Threads
import java.util.concurrent.Executor
fun <V> SingleItemDataCacheUserInterface<V>.openLive(executor: Executor): LiveData<V> {
val cache = this
return object: LiveData<V>() {
val listener = object: SingleItemDataCacheListener<V> {
override fun onElementUpdated(oldValue: V, newValue: V) {
postValue(newValue)
}
}
override fun onActive() {
super.onActive()
executor.execute {
val initialValue = cache.openSync(listener)
postValue(initialValue)
}
}
override fun onInactive() {
super.onInactive()
cache.close(listener)
}
}
}
fun <V> SingleItemDataCacheUserInterface<V>.openLiveAtDatabaseThread() = openLive(Threads.database)

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
@ -32,6 +32,9 @@ abstract class CategoryAppDao {
@Query("SELECT * FROM category_app WHERE category_id IN (:categoryIds)") @Query("SELECT * FROM category_app WHERE category_id IN (:categoryIds)")
abstract fun getCategoryApps(categoryIds: List<String>): LiveData<List<CategoryApp>> abstract fun getCategoryApps(categoryIds: List<String>): LiveData<List<CategoryApp>>
@Query("SELECT * FROM category_app WHERE category_id IN (SELECT category_id FROM category WHERE child_id = :userId)")
abstract fun getCategoryAppsByUserIdSync(userId: String): List<CategoryApp>
@Insert @Insert
abstract fun addCategoryAppsSync(items: Collection<CategoryApp>) abstract fun addCategoryAppsSync(items: Collection<CategoryApp>)

View file

@ -250,7 +250,7 @@ abstract class ConfigDao {
val experimentalFlags: LiveData<Long> by lazy { getExperimentalFlagsLive() } val experimentalFlags: LiveData<Long> by lazy { getExperimentalFlagsLive() }
private fun getExperimentalFlagsSync(): Long { fun getExperimentalFlagsSync(): Long {
val v = getValueOfKeySync(ConfigurationItemType.ExperimentalFlags) val v = getValueOfKeySync(ConfigurationItemType.ExperimentalFlags)
if (v == null) { if (v == null) {

View file

@ -0,0 +1,141 @@
/*
* 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.data.dao
import androidx.lifecycle.LiveData
import io.timelimit.android.data.Database
import io.timelimit.android.data.cache.multi.DataCacheHelperInterface
import io.timelimit.android.data.cache.multi.createCache
import io.timelimit.android.data.cache.multi.delayClosingItems
import io.timelimit.android.data.cache.single.*
import io.timelimit.android.data.model.derived.DeviceAndUserRelatedData
import io.timelimit.android.data.model.derived.DeviceRelatedData
import io.timelimit.android.data.model.derived.UserRelatedData
class DerivedDataDao (private val database: Database) {
private val userRelatedDataCache = object : DataCacheHelperInterface<String, UserRelatedData?, UserRelatedData?> {
override fun openItemSync(key: String): UserRelatedData? {
val user = database.user().getUserByIdSync(key) ?: return null
return UserRelatedData.load(user, database)
}
override fun updateItemSync(key: String, item: UserRelatedData?): UserRelatedData? {
return if (item != null) {
item.update(database)
} else {
openItemSync(key)
}
}
override fun <R> wrapOpenOrUpdate(block: () -> R): R = database.runInUnobservedTransaction { block() }
override fun disposeItemFast(key: String, item: UserRelatedData?) = Unit
override fun prepareForUser(item: UserRelatedData?): UserRelatedData? = item
override fun close() = Unit
}.createCache()
private val deviceRelatedDataCache = object: SingleItemDataCacheHelperInterface<DeviceRelatedData?, DeviceRelatedData?> {
override fun openItemSync(): DeviceRelatedData? = DeviceRelatedData.load(database)
override fun updateItemSync(item: DeviceRelatedData?): DeviceRelatedData? = if (item != null) {
item.update(database)
} else {
openItemSync()
}
override fun <R> wrapOpenOrUpdate(block: () -> R): R = database.runInUnobservedTransaction { block() }
override fun prepareForUser(item: DeviceRelatedData?): DeviceRelatedData? = item
override fun disposeItemFast(item: DeviceRelatedData?): Unit = Unit
}.createCache()
private val usableUserRelatedData = userRelatedDataCache.userInterface.delayClosingItems(15 * 1000 /* 15 seconds */)
private val usableDeviceRelatedData = deviceRelatedDataCache.userInterface.delayClosingItem(60 * 1000 /* 1 minute */)
private val deviceAndUserRelatedDataCache = object: SingleItemDataCacheHelperInterface<DeviceAndUserRelatedData?, DeviceAndUserRelatedData?> {
override fun openItemSync(): DeviceAndUserRelatedData? {
val deviceRelatedData = usableDeviceRelatedData.openSync(null) ?: return null
val userRelatedData = if (deviceRelatedData.deviceEntry.currentUserId.isNotEmpty())
usableUserRelatedData.openSync(deviceRelatedData.deviceEntry.currentUserId, null)
else
null
return DeviceAndUserRelatedData(
deviceRelatedData = deviceRelatedData,
userRelatedData = userRelatedData
)
}
override fun updateItemSync(item: DeviceAndUserRelatedData?): DeviceAndUserRelatedData? {
val deviceRelatedData = usableDeviceRelatedData.openSync(null) ?: run {
// close old listener instances
disposeItemFast(item)
return null
}
val userRelatedData = if (deviceRelatedData.deviceEntry.currentUserId.isNotEmpty())
usableUserRelatedData.openSync(deviceRelatedData.deviceEntry.currentUserId, null)
else
null
// close old listener instances
disposeItemFast(item)
return if (deviceRelatedData == item?.deviceRelatedData && userRelatedData == item.userRelatedData) {
item
} else {
DeviceAndUserRelatedData(
deviceRelatedData = deviceRelatedData,
userRelatedData = userRelatedData
)
}
}
override fun <R> wrapOpenOrUpdate(block: () -> R): R = database.runInUnobservedTransaction { block() }
override fun prepareForUser(item: DeviceAndUserRelatedData?): DeviceAndUserRelatedData? = item
override fun disposeItemFast(item: DeviceAndUserRelatedData?) {
if (item != null) {
usableDeviceRelatedData.close(null)
item.userRelatedData?.user?.let { usableUserRelatedData.close(it.id, null) }
}
}
}.createCache()
private val usableDeviceAndUserRelatedDataCache = deviceAndUserRelatedDataCache.userInterface.delayClosingItem(5000)
private val deviceAndUserRelatedDataLive = usableDeviceAndUserRelatedDataCache.openLiveAtDatabaseThread()
init {
database.registerTransactionCommitListener {
userRelatedDataCache.ownerInterface.updateSync()
deviceRelatedDataCache.ownerInterface.updateSync()
deviceAndUserRelatedDataCache.ownerInterface.updateSync()
}
}
fun getUserAndDeviceRelatedDataSync(): DeviceAndUserRelatedData? {
val result = usableDeviceAndUserRelatedDataCache.openSync(null)
usableDeviceAndUserRelatedDataCache.close(null)
return result
}
fun getUserAndDeviceRelatedDataLive(): LiveData<DeviceAndUserRelatedData?> = deviceAndUserRelatedDataLive
}

View file

@ -16,7 +16,6 @@
package io.timelimit.android.data.dao package io.timelimit.android.data.dao
import androidx.lifecycle.LiveData
import androidx.room.* import androidx.room.*
import io.timelimit.android.data.model.SessionDuration import io.timelimit.android.data.model.SessionDuration
@ -34,7 +33,7 @@ interface SessionDurationDao {
): SessionDuration? ): SessionDuration?
@Query("SELECT * FROM session_duration WHERE category_id = :categoryId") @Query("SELECT * FROM session_duration WHERE category_id = :categoryId")
fun getSessionDurationItemsByCategoryId(categoryId: String): LiveData<List<SessionDuration>> fun getSessionDurationItemsByCategoryIdSync(categoryId: String): List<SessionDuration>
@Insert @Insert
fun insertSessionDurationItemSync(item: SessionDuration) fun insertSessionDurationItemSync(item: SessionDuration)

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
@ -15,8 +15,6 @@
*/ */
package io.timelimit.android.data.dao package io.timelimit.android.data.dao
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.Query import androidx.room.Query
@ -24,12 +22,8 @@ import io.timelimit.android.data.model.TemporarilyAllowedApp
@Dao @Dao
abstract class TemporarilyAllowedAppDao { abstract class TemporarilyAllowedAppDao {
@Query("SELECT * FROM temporarily_allowed_app WHERE device_id = :deviceId") @Query("SELECT package_name FROM temporarily_allowed_app")
abstract fun getTemporarilyAllowedAppsInternal(deviceId: String): LiveData<List<TemporarilyAllowedApp>> abstract fun getTemporarilyAllowedAppsSync(): List<String>
fun getTemporarilyAllowedApps(deviceId: String): LiveData<List<String>> {
return Transformations.map(getTemporarilyAllowedAppsInternal(deviceId)) { it.map { it.packageName } }
}
@Insert @Insert
abstract fun addTemporarilyAllowedAppSync(app: TemporarilyAllowedApp) abstract fun addTemporarilyAllowedAppSync(app: TemporarilyAllowedApp)

View file

@ -57,6 +57,9 @@ abstract class UsedTimeDao {
@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>>
@Query("SELECT * FROM used_time WHERE category_id = :categoryId")
abstract fun getUsedTimeItemsByCategoryId(categoryId: String): List<UsedTimeItem>
@Query("SELECT * FROM used_time") @Query("SELECT * FROM used_time")
abstract fun getAllUsedTimeItemsSync(): List<UsedTimeItem> abstract fun getAllUsedTimeItemsSync(): List<UsedTimeItem>

View file

@ -16,7 +16,9 @@
package io.timelimit.android.data.extensions package io.timelimit.android.data.extensions
import io.timelimit.android.data.model.Category import io.timelimit.android.data.model.Category
import io.timelimit.android.data.model.derived.CategoryRelatedData
// TODO: remove this
fun List<Category>.sorted(): List<Category> { fun List<Category>.sorted(): List<Category> {
val categoryIds = this.map { it.id }.toSet() val categoryIds = this.map { it.id }.toSet()
@ -31,5 +33,22 @@ fun List<Category>.sorted(): List<Category> {
} }
} }
return sortedCategories.toList()
}
fun List<CategoryRelatedData>.sortedCategories(): List<CategoryRelatedData> {
val categoryIds = this.map { it.category.id }.toSet()
val sortedCategories = mutableListOf<CategoryRelatedData>()
val childCategories = this.filter { categoryIds.contains(it.category.parentCategoryId) }.groupBy { it.category.parentCategoryId }
this.filterNot { categoryIds.contains(it.category.parentCategoryId) }.sortedBy { it.category.sort }.forEach { category ->
sortedCategories.add(category)
childCategories[category.category.id]?.sortedBy { it.category.sort }?.let { items ->
sortedCategories.addAll(items)
}
}
return sortedCategories.toList() return sortedCategories.toList()
} }

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
@ -13,17 +13,9 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package io.timelimit.android.logic.extension
import io.timelimit.android.data.model.Category package io.timelimit.android.data.invalidation
import io.timelimit.android.integration.platform.BatteryStatus
fun BatteryStatus.isCategoryAllowed(category: Category?): Boolean { interface Observer {
return if (category == null) { fun onInvalidated(tables: Set<Table>)
true
} else if (this.charging) {
this.level >= category.minBatteryLevelWhileCharging
} else {
this.level >= category.minBatteryLevelMobile
}
} }

View file

@ -0,0 +1,92 @@
/*
* 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.data.invalidation
enum class Table {
AllowedContact,
App,
AppActivity,
Category,
CategoryApp,
ConfigurationItem,
Device,
Notification,
PendingSyncAction,
SessionDuration,
TemporarilyAllowedApp,
TimeLimitRule,
UsedTimeItem,
User,
UserKey
}
object TableNames {
const val ALLOWED_CONTACT = "allowed_contact"
const val APP = "app"
const val APP_ACTIVITY = "app_activity"
const val CATEGORY = "category"
const val CATEGORY_APP = "category_app"
const val CONFIGURATION_ITEM = "config"
const val DEVICE = "device"
const val NOTIFICATION = "notification"
const val PENDING_SYNC_ACTION = "pending_sync_action"
const val SESSION_DURATION = "session_duration"
const val TEMPORARILY_ALLOWED_APP = "temporarily_allowed_app"
const val TIME_LIMIT_RULE = "time_limit_rule"
const val USED_TIME_ITEM = "used_time"
const val USER = "user"
const val USER_KEY = "user_key"
}
object TableUtil {
fun toName(value: Table): String = when (value) {
Table.AllowedContact -> TableNames.ALLOWED_CONTACT
Table.App -> TableNames.APP
Table.AppActivity -> TableNames.APP_ACTIVITY
Table.Category -> TableNames.CATEGORY
Table.CategoryApp -> TableNames.CATEGORY_APP
Table.ConfigurationItem -> TableNames.CONFIGURATION_ITEM
Table.Device -> TableNames.DEVICE
Table.Notification -> TableNames.NOTIFICATION
Table.PendingSyncAction -> TableNames.PENDING_SYNC_ACTION
Table.SessionDuration -> TableNames.SESSION_DURATION
Table.TemporarilyAllowedApp -> TableNames.TEMPORARILY_ALLOWED_APP
Table.TimeLimitRule -> TableNames.TIME_LIMIT_RULE
Table.UsedTimeItem -> TableNames.USED_TIME_ITEM
Table.User -> TableNames.USER
Table.UserKey -> TableNames.USER_KEY
}
fun toEnum(value: String): Table = when (value) {
TableNames.ALLOWED_CONTACT -> Table.AllowedContact
TableNames.APP -> Table.App
TableNames.APP_ACTIVITY -> Table.AppActivity
TableNames.CATEGORY -> Table.Category
TableNames.CATEGORY_APP -> Table.CategoryApp
TableNames.CONFIGURATION_ITEM -> Table.ConfigurationItem
TableNames.DEVICE -> Table.Device
TableNames.NOTIFICATION -> Table.Notification
TableNames.PENDING_SYNC_ACTION -> Table.PendingSyncAction
TableNames.SESSION_DURATION -> Table.SessionDuration
TableNames.TEMPORARILY_ALLOWED_APP -> Table.TemporarilyAllowedApp
TableNames.TIME_LIMIT_RULE -> Table.TimeLimitRule
TableNames.USED_TIME_ITEM -> Table.UsedTimeItem
TableNames.USER -> Table.User
TableNames.USER_KEY -> Table.UserKey
else -> throw IllegalArgumentException()
}
}

View file

@ -0,0 +1,72 @@
/*
* 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.data.model.derived
import io.timelimit.android.data.Database
import io.timelimit.android.data.model.Category
import io.timelimit.android.data.model.SessionDuration
import io.timelimit.android.data.model.TimeLimitRule
import io.timelimit.android.data.model.UsedTimeItem
data class CategoryRelatedData(
val category: Category,
val rules: List<TimeLimitRule>,
val usedTimes: List<UsedTimeItem>,
val durations: List<SessionDuration>
) {
companion object {
fun load(category: Category, database: Database): CategoryRelatedData = database.runInUnobservedTransaction {
val rules = database.timeLimitRules().getTimeLimitRulesByCategorySync(category.id)
val usedTimes = database.usedTimes().getUsedTimeItemsByCategoryId(category.id)
val durations = database.sessionDuration().getSessionDurationItemsByCategoryIdSync(category.id)
CategoryRelatedData(
category = category,
rules = rules,
usedTimes = usedTimes,
durations = durations
)
}
}
fun update(
category: Category,
updateRules: Boolean,
updateTimes: Boolean,
updateDurations: Boolean,
database: Database
): CategoryRelatedData = database.runInUnobservedTransaction {
if (category.id != this.category.id) {
throw IllegalStateException()
}
val rules = if (updateRules) database.timeLimitRules().getTimeLimitRulesByCategorySync(category.id) else rules
val usedTimes = if (updateTimes) database.usedTimes().getUsedTimeItemsByCategoryId(category.id) else usedTimes
val durations = if (updateDurations) database.sessionDuration().getSessionDurationItemsByCategoryIdSync(category.id) else durations
if (category == this.category && rules == this.rules && usedTimes == this.usedTimes && durations == this.durations) {
this
} else {
CategoryRelatedData(
category = category,
rules = rules,
usedTimes = usedTimes,
durations = durations
)
}
}
}

View file

@ -0,0 +1,22 @@
/*
* 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.data.model.derived
data class DeviceAndUserRelatedData(
val deviceRelatedData: DeviceRelatedData,
val userRelatedData: UserRelatedData?
)

View file

@ -0,0 +1,71 @@
/*
* 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.data.model.derived
import io.timelimit.android.data.Database
import io.timelimit.android.data.invalidation.Observer
import io.timelimit.android.data.invalidation.Table
import io.timelimit.android.data.model.Device
import java.lang.ref.WeakReference
data class DeviceRelatedData (
val deviceEntry: Device,
val isConnectedAndHasPremium: Boolean,
val isLocalMode: Boolean,
val hasValidDefaultUser: Boolean,
val temporarilyAllowedApps: Set<String>,
val experimentalFlags: Long
): Observer {
companion object {
private val relatedTables = arrayOf(Table.ConfigurationItem, Table.Device, Table.User, Table.TemporarilyAllowedApp)
fun load(database: Database): DeviceRelatedData? = database.runInUnobservedTransaction {
val deviceId = database.config().getOwnDeviceIdSync() ?: return@runInUnobservedTransaction null
val deviceEntry = database.device().getDeviceByIdSync(deviceId) ?: return@runInUnobservedTransaction null
val hasPremium = database.config().getFullVersionUntilSync() != 0L
val isLocalMode = database.config().getDeviceAuthTokenSync().isEmpty()
val hasValidDefaultUser = database.user().getUserByIdSync(deviceEntry.defaultUser) != null
val temporarilyAllowedApps = database.temporarilyAllowedApp().getTemporarilyAllowedAppsSync().toSet()
val experimentalFlags = database.config().getExperimentalFlagsSync()
DeviceRelatedData(
deviceEntry = deviceEntry,
isConnectedAndHasPremium = hasPremium && !isLocalMode,
isLocalMode = isLocalMode,
hasValidDefaultUser = hasValidDefaultUser,
temporarilyAllowedApps = temporarilyAllowedApps,
experimentalFlags = experimentalFlags
).also {
database.registerWeakObserver(relatedTables, WeakReference(it))
}
}
}
private var invalidated = false
override fun onInvalidated(tables: Set<Table>) { invalidated = true }
fun update(database: Database): DeviceRelatedData? {
if (!invalidated) {
return this
}
return load(database)
}
fun isExperimentalFlagSetSync(flags: Long) = (experimentalFlags and flags) == flags
}

View file

@ -0,0 +1,147 @@
/*
* 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.data.model.derived
import androidx.collection.LruCache
import io.timelimit.android.BuildConfig
import io.timelimit.android.data.Database
import io.timelimit.android.data.IdGenerator
import io.timelimit.android.data.extensions.getTimezone
import io.timelimit.android.data.invalidation.Observer
import io.timelimit.android.data.invalidation.Table
import io.timelimit.android.data.model.CategoryApp
import io.timelimit.android.data.model.User
import java.lang.ref.WeakReference
import java.util.*
data class UserRelatedData(
val user: User,
val categories: List<CategoryRelatedData>,
val categoryApps: List<CategoryApp>
): Observer {
companion object {
private val notFoundCategoryApp = CategoryApp(categoryId = IdGenerator.generateId(), packageName = BuildConfig.APPLICATION_ID)
private val relatedTables = arrayOf(
Table.User, Table.Category, Table.TimeLimitRule,
Table.UsedTimeItem, Table.SessionDuration, Table.CategoryApp
)
fun load(user: User, database: Database): UserRelatedData = database.runInUnobservedTransaction {
val categoryEntries = database.category().getCategoriesByChildIdSync(childId = user.id)
val categories = categoryEntries.map { CategoryRelatedData.load(category = it, database = database) }
val categoryApps = database.categoryApp().getCategoryAppsByUserIdSync(userId = user.id)
UserRelatedData(
user = user,
categories = categories,
categoryApps = categoryApps
).also { database.registerWeakObserver(relatedTables, WeakReference(it)) }
}
}
val categoryById: Map<String, CategoryRelatedData> by lazy { categories.associateBy { it.category.id } }
val timeZone: TimeZone by lazy { user.getTimezone() }
// O(n), but saves memory and index building time
// additionally a cache
// notFoundCategoryApp is a workaround because the lru cache does not support null
private val categoryAppLruCache = object: LruCache<String, CategoryApp>(8) {
override fun create(key: String): CategoryApp {
return categoryApps.find { it.packageName == key } ?: notFoundCategoryApp
}
}
fun findCategoryApp(packageName: String): CategoryApp? {
val item = categoryAppLruCache[packageName]
// important: strict equality/ same object instance
if (item === notFoundCategoryApp) {
return null
} else {
return item
}
}
private var userInvalidated = false
private var categoriesInvalidated = false
private var rulesInvalidated = false
private var usedTimesInvalidated = false
private var sessionDurationsInvalidated = false
private var categoryAppsInvalidated = false
private val invalidated
get() = userInvalidated || categoriesInvalidated || rulesInvalidated || usedTimesInvalidated || sessionDurationsInvalidated || categoryAppsInvalidated
override fun onInvalidated(tables: Set<Table>) {
tables.forEach {
when (it) {
Table.User -> userInvalidated = true
Table.Category -> categoriesInvalidated = true
Table.TimeLimitRule -> rulesInvalidated = true
Table.UsedTimeItem -> usedTimesInvalidated = true
Table.SessionDuration -> sessionDurationsInvalidated = true
Table.CategoryApp -> categoryAppsInvalidated = true
else -> {/* do nothing */}
}
}
}
fun update(database: Database) = database.runInUnobservedTransaction {
if (!invalidated) {
return@runInUnobservedTransaction this
}
val user = if (userInvalidated) database.user().getUserByIdSync(user.id) ?: return@runInUnobservedTransaction null else user
val categories = if (categoriesInvalidated) {
val oldCategoriesById = this.categories.associateBy { it.category.id }
database.category().getCategoriesByChildIdSync(childId = user.id).map { category ->
val oldItem = oldCategoriesById[category.id]
oldItem?.update(
category = category,
database = database,
updateDurations = sessionDurationsInvalidated,
updateRules = rulesInvalidated,
updateTimes = usedTimesInvalidated
) ?: CategoryRelatedData.load(
category = category,
database = database
)
}
} else if (sessionDurationsInvalidated || rulesInvalidated || usedTimesInvalidated) {
categories.map {
it.update(
category = it.category,
database = database,
updateDurations = sessionDurationsInvalidated,
updateRules = rulesInvalidated,
updateTimes = usedTimesInvalidated
)
}
} else {
categories
}
val categoryApps = if (categoryAppsInvalidated) database.categoryApp().getCategoryAppsByUserIdSync(userId = user.id) else categoryApps
UserRelatedData(
user = user,
categories = categories,
categoryApps = categoryApps
).also { database.registerWeakObserver(relatedTables, WeakReference(it)) }
}
}

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
@ -15,11 +15,12 @@
*/ */
package io.timelimit.android.date package io.timelimit.android.date
import org.threeten.bp.DayOfWeek
import org.threeten.bp.LocalDate import org.threeten.bp.LocalDate
import org.threeten.bp.temporal.ChronoUnit import org.threeten.bp.temporal.ChronoUnit
import java.util.* import java.util.*
data class DateInTimezone(val dayOfWeek: Int, val dayOfEpoch: Int) { data class DateInTimezone(val dayOfWeek: Int, val dayOfEpoch: Int, val localDate: LocalDate) {
companion object { companion object {
fun convertDayOfWeek(dayOfWeek: Int) = when(dayOfWeek) { fun convertDayOfWeek(dayOfWeek: Int) = when(dayOfWeek) {
Calendar.MONDAY -> 0 Calendar.MONDAY -> 0
@ -32,7 +33,18 @@ data class DateInTimezone(val dayOfWeek: Int, val dayOfEpoch: Int) {
else -> throw IllegalStateException() else -> throw IllegalStateException()
} }
fun newInstance(timeInMillis: Long, timeZone: TimeZone): DateInTimezone { fun convertDayOfWeek(dayOfWeek: DayOfWeek) = when(dayOfWeek) {
DayOfWeek.MONDAY -> 0
DayOfWeek.TUESDAY -> 1
DayOfWeek.WEDNESDAY -> 2
DayOfWeek.THURSDAY -> 3
DayOfWeek.FRIDAY -> 4
DayOfWeek.SATURDAY -> 5
DayOfWeek.SUNDAY -> 6
else -> throw IllegalStateException()
}
fun getLocalDate(timeInMillis: Long, timeZone: TimeZone): LocalDate {
val calendar = CalendarCache.getCalendar() val calendar = CalendarCache.getCalendar()
calendar.firstDayOfWeek = Calendar.MONDAY calendar.firstDayOfWeek = Calendar.MONDAY
@ -40,17 +52,19 @@ data class DateInTimezone(val dayOfWeek: Int, val dayOfEpoch: Int) {
calendar.timeZone = timeZone calendar.timeZone = timeZone
calendar.timeInMillis = timeInMillis calendar.timeInMillis = timeInMillis
val dayOfWeek = convertDayOfWeek(calendar.get(Calendar.DAY_OF_WEEK)) return LocalDate.of(
val localDate = LocalDate.of(
calendar.get(Calendar.YEAR), calendar.get(Calendar.YEAR),
calendar.get(Calendar.MONTH) + 1, calendar.get(Calendar.MONTH) + 1,
calendar.get(Calendar.DAY_OF_MONTH) calendar.get(Calendar.DAY_OF_MONTH)
) )
val dayOfEpoch = ChronoUnit.DAYS.between(LocalDate.ofEpochDay(0), localDate).toInt()
return DateInTimezone(dayOfWeek, dayOfEpoch)
} }
fun newInstance(localDate: LocalDate): DateInTimezone = DateInTimezone(
dayOfEpoch = localDate.toEpochDay().toInt(),
dayOfWeek = convertDayOfWeek(localDate.dayOfWeek),
localDate = localDate
)
fun newInstance(timeInMillis: Long, timeZone: TimeZone) = newInstance(getLocalDate(timeInMillis, timeZone))
} }
} }

View file

@ -73,6 +73,7 @@ abstract class PlatformIntegration(
abstract fun setForceNetworkTime(enable: Boolean) abstract fun setForceNetworkTime(enable: Boolean)
var installedAppsChangeListener: Runnable? = null var installedAppsChangeListener: Runnable? = null
var systemClockChangeListener: Runnable? = null
} }
data class ForegroundAppSpec(var packageName: String?, var activityName: String?) { data class ForegroundAppSpec(var packageName: String?, var activityName: String?) {

View file

@ -87,6 +87,12 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio
installedAppsChangeListener?.run() installedAppsChangeListener?.run()
} }
}) })
context.registerReceiver(object: BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
systemClockChangeListener?.run()
}
}, IntentFilter(Intent.ACTION_TIME_CHANGED))
} }
override fun getLocalApps(deviceId: String): Collection<App> { override fun getLocalApps(deviceId: String): Collection<App> {

View file

@ -27,9 +27,13 @@ import android.util.Log
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import io.timelimit.android.BuildConfig import io.timelimit.android.BuildConfig
import io.timelimit.android.R 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.runAsync
import io.timelimit.android.livedata.waitForNonNullValue import io.timelimit.android.data.model.UserType
import io.timelimit.android.logic.* import io.timelimit.android.logic.*
import io.timelimit.android.logic.blockingreason.AppBaseHandling
import io.timelimit.android.logic.blockingreason.CategoryItselfHandling
@TargetApi(Build.VERSION_CODES.LOLLIPOP) @TargetApi(Build.VERSION_CODES.LOLLIPOP)
class NotificationListener: NotificationListenerService() { class NotificationListener: NotificationListenerService() {
@ -39,7 +43,6 @@ class NotificationListener: NotificationListenerService() {
} }
private val appLogic: AppLogic by lazy { DefaultAppLogic.with(this) } private val appLogic: AppLogic by lazy { DefaultAppLogic.with(this) }
private val blockingReasonUtil: BlockingReasonUtil by lazy { BlockingReasonUtil(appLogic) }
private val notificationManager: NotificationManager by lazy { getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager } private val notificationManager: NotificationManager by lazy { getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager }
private val queryAppTitleCache: QueryAppTitleCache by lazy { QueryAppTitleCache(appLogic.platformIntegration) } private val queryAppTitleCache: QueryAppTitleCache by lazy { QueryAppTitleCache(appLogic.platformIntegration) }
private val lastOngoingNotificationHidden = mutableSetOf<String>() private val lastOngoingNotificationHidden = mutableSetOf<String>()
@ -140,31 +143,53 @@ class NotificationListener: NotificationListenerService() {
return BlockingReason.None return BlockingReason.None
} }
val blockingReason = blockingReasonUtil.getBlockingReason( val deviceAndUserRelatedData = Threads.database.executeAndWait {
packageName = sbn.packageName, appLogic.database.derivedDataDao().getUserAndDeviceRelatedDataSync()
activityName = null
).waitForNonNullValue()
if (blockingReason.areNotificationsBlocked) {
if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "blocking notification of ${sbn.packageName} because notifications are blocked")
}
return BlockingReason.NotificationsAreBlocked
} }
return when (blockingReason) { return if (deviceAndUserRelatedData?.userRelatedData?.user?.type != UserType.Child) {
is NoBlockingReason -> BlockingReason.None BlockingReason.None
is BlockedReasonDetails -> { } else {
if (isSystemApp(sbn.packageName) && blockingReason.reason == BlockingReason.NotPartOfAnCategory) { val appHandling = AppBaseHandling.calculate(
return BlockingReason.None foregroundAppPackageName = sbn.packageName,
foregroundAppActivityName = null,
pauseCounting = false,
pauseForegroundAppBackgroundLoop = false,
userRelatedData = deviceAndUserRelatedData.userRelatedData,
deviceRelatedData = deviceAndUserRelatedData.deviceRelatedData
)
if (appHandling is AppBaseHandling.BlockDueToNoCategory && !isSystemApp(sbn.packageName)) {
BlockingReason.NotPartOfAnCategory
} else if (appHandling is AppBaseHandling.UseCategories) {
val time = RealTime.newInstance()
val battery = appLogic.platformIntegration.getBatteryStatus()
val allowNotificationFilter = deviceAndUserRelatedData.deviceRelatedData.isConnectedAndHasPremium || deviceAndUserRelatedData.deviceRelatedData.isLocalMode
appLogic.realTimeLogic.getRealTime(time)
val categoryHandlings = appHandling.categoryIds.map { categoryId ->
CategoryItselfHandling.calculate(
categoryRelatedData = deviceAndUserRelatedData.userRelatedData.categoryById[categoryId]!!,
user = deviceAndUserRelatedData.userRelatedData,
assumeCurrentDevice = CurrentDeviceLogic.handleDeviceAsCurrentDevice(
device = deviceAndUserRelatedData.deviceRelatedData,
user = deviceAndUserRelatedData.userRelatedData
),
batteryStatus = battery,
shouldTrustTimeTemporarily = time.shouldTrustTimeTemporarily,
timeInMillis = time.timeInMillis
)
} }
if (BuildConfig.DEBUG) { if (allowNotificationFilter && categoryHandlings.find { it.blockAllNotifications } != null) {
Log.d(LOG_TAG, "blocking notification of ${sbn.packageName} because ${blockingReason.reason}") BlockingReason.NotificationsAreBlocked
} else {
categoryHandlings.find { it.shouldBlockActivities }?.activityBlockingReason
?: BlockingReason.None
} }
} else {
return blockingReason.reason BlockingReason.None
} }
} }
} }

View file

@ -42,6 +42,8 @@ class DummyTimeApi(var timeStepSizeInMillis: Long): TimeApi() {
scheduledActions.add(ScheduledAction(currentUptime + delayInMillis, runnable)) scheduledActions.add(ScheduledAction(currentUptime + delayInMillis, runnable))
} }
override fun runDelayedByUptime(runnable: Runnable, delayInMillis: Long) = runDelayed(runnable, delayInMillis)
override fun cancelScheduledAction(runnable: Runnable) { override fun cancelScheduledAction(runnable: Runnable) {
scheduledActions.removeAll { it.action === runnable } scheduledActions.removeAll { it.action === runnable }
} }

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
@ -21,7 +21,47 @@ import android.os.SystemClock
import java.util.* import java.util.*
object RealTimeApi: TimeApi() { object RealTimeApi: TimeApi() {
internal class QueueItem(val runnable: Runnable, val targetUptime: Long): Comparable<QueueItem> {
override fun compareTo(other: QueueItem): Int = this.targetUptime.compareTo(other.targetUptime)
}
private val handler = Handler(Looper.getMainLooper()) private val handler = Handler(Looper.getMainLooper())
private val queue = PriorityQueue<QueueItem>()
// why this? because the handler does not use elapsedRealtime
// this is a workaround
private val queueProcessor = Runnable {
synchronized(queue) {
try {
val now = getCurrentUptimeInMillis()
while (true) {
val head = queue.peek()
if (head == null || head.targetUptime > now) break
queue.remove()
head.runnable.run()
}
} finally {
scheduleQueue()
}
}
}
private fun scheduleQueue() {
synchronized(queue) {
handler.removeCallbacks(queueProcessor)
queue.peek()?.let { head ->
val delay = head.targetUptime - getCurrentTimeInMillis()
// at most 5 seconds so that sleeps don't cause trouble
handler.postDelayed(queueProcessor, delay.coerceAtLeast(0).coerceAtMost(5 * 1000))
}
}
}
override fun getCurrentTimeInMillis(): Long { override fun getCurrentTimeInMillis(): Long {
return System.currentTimeMillis() return System.currentTimeMillis()
@ -35,8 +75,21 @@ object RealTimeApi: TimeApi() {
handler.postDelayed(runnable, delayInMillis) handler.postDelayed(runnable, delayInMillis)
} }
override fun runDelayedByUptime(runnable: Runnable, delayInMillis: Long) {
synchronized(queue) {
queue.add(QueueItem(runnable = runnable, targetUptime = getCurrentUptimeInMillis() + delayInMillis))
scheduleQueue()
}
}
override fun cancelScheduledAction(runnable: Runnable) { override fun cancelScheduledAction(runnable: Runnable) {
handler.removeCallbacks(runnable) handler.removeCallbacks(runnable)
synchronized(queue) {
val itemsToRemove = queue.filter { it.runnable === runnable }
itemsToRemove.forEach { queue.remove(it) }
}
} }
override fun getSystemTimeZone() = TimeZone.getDefault() override fun getSystemTimeZone() = TimeZone.getDefault()

View file

@ -26,6 +26,7 @@ abstract class TimeApi {
abstract fun getCurrentUptimeInMillis(): Long abstract fun getCurrentUptimeInMillis(): Long
// function to run something delayed at the UI Thread // function to run something delayed at the UI Thread
abstract fun runDelayed(runnable: Runnable, delayInMillis: Long) abstract fun runDelayed(runnable: Runnable, delayInMillis: Long)
abstract fun runDelayedByUptime(runnable: Runnable, delayInMillis: Long)
abstract fun cancelScheduledAction(runnable: Runnable) abstract fun cancelScheduledAction(runnable: Runnable)
suspend fun sleep(timeInMillis: Long) = suspendCoroutine<Void?> { suspend fun sleep(timeInMillis: Long) = suspendCoroutine<Void?> {
runDelayed(Runnable { runDelayed(Runnable {

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
@ -15,24 +15,28 @@
*/ */
package io.timelimit.android.logic package io.timelimit.android.logic
import io.timelimit.android.async.Threads
import io.timelimit.android.coroutines.executeAndWait
import io.timelimit.android.data.model.UserType import io.timelimit.android.data.model.UserType
import io.timelimit.android.integration.platform.ForegroundAppSpec import io.timelimit.android.integration.platform.ForegroundAppSpec
import io.timelimit.android.livedata.waitForNonNullValue import io.timelimit.android.livedata.waitForNonNullValue
import io.timelimit.android.livedata.waitForNullableValue import io.timelimit.android.livedata.waitForNullableValue
import io.timelimit.android.logic.blockingreason.AppBaseHandling
object AppAffectedByPrimaryDeviceUtil { object AppAffectedByPrimaryDeviceUtil {
suspend fun isCurrentAppAffectedByPrimaryDevice( suspend fun isCurrentAppAffectedByPrimaryDevice(
logic: AppLogic logic: AppLogic
): Boolean { ): Boolean {
val user = logic.deviceUserEntry.waitForNullableValue() val deviceAndUserRelatedData = Threads.database.executeAndWait {
?: throw NullPointerException("no user is signed in") logic.database.derivedDataDao().getUserAndDeviceRelatedDataSync()
if (user.type != UserType.Child) {
throw IllegalStateException("no child is signed in")
} }
if (user.relaxPrimaryDevice) { if (deviceAndUserRelatedData?.userRelatedData?.user?.type != UserType.Child) {
if (logic.fullVersion.shouldProvideFullVersionFunctions.waitForNonNullValue() == true) { return false
}
if (deviceAndUserRelatedData.userRelatedData.user.relaxPrimaryDevice) {
if (deviceAndUserRelatedData.deviceRelatedData.isConnectedAndHasPremium) {
return false return false
} }
} }
@ -49,50 +53,26 @@ object AppAffectedByPrimaryDeviceUtil {
return false return false
} }
val categories = logic.database.category().getCategoriesByChildId(user.id).waitForNonNullValue() val handling = AppBaseHandling.calculate(
val categoryId = run { foregroundAppPackageName = currentApp.packageName,
val categoryIdAtAppLevel = logic.database.categoryApp().getCategoryApp( foregroundAppActivityName = currentApp.activityName,
categoryIds = categories.map { it.id }, deviceRelatedData = deviceAndUserRelatedData.deviceRelatedData,
packageName = currentApp.packageName!! userRelatedData = deviceAndUserRelatedData.userRelatedData,
).waitForNullableValue()?.categoryId pauseCounting = false,
pauseForegroundAppBackgroundLoop = false
)
if (logic.deviceEntry.waitForNullableValue()?.enableActivityLevelBlocking == true) { if (!(handling is AppBaseHandling.UseCategories)) {
val categoryIdAtActivityLevel = logic.database.categoryApp().getCategoryApp(
categoryIds = categories.map { it.id },
packageName = "${currentApp.packageName}:${currentApp.activityName}"
).waitForNullableValue()?.categoryId
categoryIdAtActivityLevel ?: categoryIdAtAppLevel
} else {
categoryIdAtAppLevel
}
} ?: user.categoryForNotAssignedApps
val category = categories.find { it.id == categoryId }
val parentCategory = categories.find { it.id == category?.parentCategoryId }
if (category == null) {
return false return false
} }
// check blocked time areas return handling.categoryIds.find { categoryId ->
if ( val category = deviceAndUserRelatedData.userRelatedData.categoryById[categoryId]!!
(category.blockedMinutesInWeek.dataNotToModify.isEmpty == false) ||
(parentCategory?.blockedMinutesInWeek?.dataNotToModify?.isEmpty == false)
) {
return true
}
// check time limit rules val hasBlockedTimeAreas = !category.category.blockedMinutesInWeek.dataNotToModify.isEmpty
val rules = logic.database.timeLimitRules().getTimeLimitRulesByCategories( val hasRules = category.rules.isNotEmpty()
categoryIds = listOf(categoryId) +
(if (parentCategory != null) listOf(parentCategory.id) else emptyList())
).waitForNonNullValue()
if (rules.isNotEmpty()) { hasBlockedTimeAreas || hasRules
return true } != null
}
return false
} }
} }

View file

@ -26,16 +26,15 @@ import io.timelimit.android.coroutines.runAsync
import io.timelimit.android.coroutines.runAsyncExpectForever import io.timelimit.android.coroutines.runAsyncExpectForever
import io.timelimit.android.data.backup.DatabaseBackup import io.timelimit.android.data.backup.DatabaseBackup
import io.timelimit.android.data.model.* import io.timelimit.android.data.model.*
import io.timelimit.android.data.model.derived.UserRelatedData
import io.timelimit.android.date.DateInTimezone import io.timelimit.android.date.DateInTimezone
import io.timelimit.android.date.getMinuteOfWeek
import io.timelimit.android.extensions.MinuteOfDay
import io.timelimit.android.integration.platform.AppStatusMessage 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.livedata.* import io.timelimit.android.livedata.*
import io.timelimit.android.sync.actions.AddUsedTimeActionItemAdditionalCountingSlot import io.timelimit.android.logic.blockingreason.AppBaseHandling
import io.timelimit.android.sync.actions.AddUsedTimeActionItemSessionDurationLimitSlot import io.timelimit.android.logic.blockingreason.CategoryHandlingCache
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
@ -64,14 +63,6 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
private const val MAX_USED_TIME_PER_ROUND_LONG = 2000 // 1 second private const val MAX_USED_TIME_PER_ROUND_LONG = 2000 // 1 second
} }
private val temporarilyAllowedApps = appLogic.deviceId.switchMap {
if (it != null) {
appLogic.database.temporarilyAllowedApp().getTemporarilyAllowedApps(it)
} else {
liveDataFromValue(Collections.emptyList<String>())
}
}
init { init {
runAsyncExpectForever { backgroundServiceLoop() } runAsyncExpectForever { backgroundServiceLoop() }
runAsyncExpectForever { syncDeviceStatusLoop() } runAsyncExpectForever { syncDeviceStatusLoop() }
@ -96,10 +87,6 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
} }
} }
temporarilyAllowedApps.map { it.isNotEmpty() }.ignoreUnchanged().observeForever {
appLogic.platformIntegration.setShowNotificationToRevokeTemporarilyAllowedApps(it!!)
}
appLogic.database.config().getEnableBackgroundSyncAsync().ignoreUnchanged().observeForever { appLogic.database.config().getEnableBackgroundSyncAsync().ignoreUnchanged().observeForever {
if (it) { if (it) {
PeriodicSyncInBackgroundWorker.enable() PeriodicSyncInBackgroundWorker.enable()
@ -121,15 +108,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
} }
} }
private val cache = BackgroundTaskLogicCache(appLogic) private val usedTimeUpdateHelper = UsedTimeUpdateHelper(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 var usedTimeUpdateHelper: UsedTimeUpdateHelper? = 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(
@ -138,6 +117,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
) )
private val appTitleCache = QueryAppTitleCache(appLogic.platformIntegration) private val appTitleCache = QueryAppTitleCache(appLogic.platformIntegration)
private val categoryHandlingCache = CategoryHandlingCache()
private val isChromeOs = appLogic.context.packageManager.hasSystemFeature(PackageManager.FEATURE_PC) private val isChromeOs = appLogic.context.packageManager.hasSystemFeature(PackageManager.FEATURE_PC)
@ -167,12 +147,19 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
appLogic.platformIntegration.showAppLockScreen(blockedAppPackageName, blockedAppActivityName) appLogic.platformIntegration.showAppLockScreen(blockedAppPackageName, blockedAppActivityName)
} }
private var showNotificationToRevokeTemporarilyAllowedApps: Boolean? = null
private fun setShowNotificationToRevokeTemporarilyAllowedApps(show: Boolean) {
if (showNotificationToRevokeTemporarilyAllowedApps != show) {
showNotificationToRevokeTemporarilyAllowedApps = show
appLogic.platformIntegration.setShowNotificationToRevokeTemporarilyAllowedApps(show)
}
}
private val foregroundAppSpec = ForegroundAppSpec.newInstance() private val foregroundAppSpec = ForegroundAppSpec.newInstance()
val foregroundAppHandling = BackgroundTaskRestrictionLogicResult()
val audioPlaybackHandling = BackgroundTaskRestrictionLogicResult()
private suspend fun commitUsedTimeUpdaters() { private suspend fun commitUsedTimeUpdaters() {
usedTimeUpdateHelper?.forceCommit(appLogic) usedTimeUpdateHelper.flush()
} }
private suspend fun backgroundServiceLoop() { private suspend fun backgroundServiceLoop() {
@ -192,22 +179,30 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
// app must be enabled // app must be enabled
if (!appLogic.enable.waitForNonNullValue()) { if (!appLogic.enable.waitForNonNullValue()) {
commitUsedTimeUpdaters() commitUsedTimeUpdaters()
liveDataCaches.removeAllItems()
appLogic.platformIntegration.setAppStatusMessage(null) appLogic.platformIntegration.setAppStatusMessage(null)
appLogic.platformIntegration.setShowBlockingOverlay(false) appLogic.platformIntegration.setShowBlockingOverlay(false)
setShowNotificationToRevokeTemporarilyAllowedApps(false)
appLogic.enable.waitUntilValueMatches { it == true } appLogic.enable.waitUntilValueMatches { it == true }
continue continue
} }
val deviceAndUSerRelatedData = Threads.database.executeAndWait {
appLogic.database.derivedDataDao().getUserAndDeviceRelatedDataSync()
}
val deviceRelatedData = deviceAndUSerRelatedData?.deviceRelatedData
val userRelatedData = deviceAndUSerRelatedData?.userRelatedData
setShowNotificationToRevokeTemporarilyAllowedApps(deviceRelatedData?.temporarilyAllowedApps?.isNotEmpty() ?: false)
// device must be used by a child // device must be used by a child
val deviceUserEntry = deviceUserEntryLive.read().waitForNullableValue() if (deviceRelatedData == null || userRelatedData == null || userRelatedData.user.type != UserType.Child) {
if (deviceUserEntry == null || deviceUserEntry.type != UserType.Child) {
commitUsedTimeUpdaters() commitUsedTimeUpdaters()
val shouldDoAutomaticSignOut = shouldDoAutomaticSignOut.read()
if (shouldDoAutomaticSignOut.waitForNonNullValue()) { val shouldDoAutomaticSignOut = deviceRelatedData != null && DefaultUserLogic.hasAutomaticSignOut(deviceRelatedData)
if (shouldDoAutomaticSignOut) {
appLogic.defaultUserLogic.reportScreenOn(appLogic.platformIntegration.isScreenOn()) appLogic.defaultUserLogic.reportScreenOn(appLogic.platformIntegration.isScreenOn())
appLogic.platformIntegration.setAppStatusMessage( appLogic.platformIntegration.setAppStatusMessage(
@ -219,16 +214,12 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
) )
appLogic.platformIntegration.setShowBlockingOverlay(false) appLogic.platformIntegration.setShowBlockingOverlay(false)
liveDataCaches.reportLoopDone()
appLogic.timeApi.sleep(backgroundServiceInterval) appLogic.timeApi.sleep(backgroundServiceInterval)
} else { } else {
liveDataCaches.removeAllItems()
appLogic.platformIntegration.setAppStatusMessage(null) appLogic.platformIntegration.setAppStatusMessage(null)
appLogic.platformIntegration.setShowBlockingOverlay(false) appLogic.platformIntegration.setShowBlockingOverlay(false)
val isChildSignedIn = deviceUserEntryLive.read().map { it != null && it.type == UserType.Child } appLogic.timeApi.sleep(backgroundServiceInterval)
isChildSignedIn.or(shouldDoAutomaticSignOut).waitUntilValueMatches { it == true }
} }
continue continue
@ -240,18 +231,18 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
appLogic.realTimeLogic.getRealTime(realTime) appLogic.realTimeLogic.getRealTime(realTime)
val nowTimestamp = realTime.timeInMillis val nowTimestamp = realTime.timeInMillis
val nowTimezone = TimeZone.getTimeZone(deviceUserEntry.timeZone) val nowTimezone = TimeZone.getTimeZone(userRelatedData.user.timeZone)
val nowDate = DateInTimezone.newInstance(nowTimestamp, nowTimezone) val nowDate = DateInTimezone.getLocalDate(nowTimestamp, nowTimezone)
val minuteOfWeek = getMinuteOfWeek(nowTimestamp, nowTimezone) val dayOfEpoch = nowDate.toEpochDay().toInt()
// eventually remove old used time data // eventually remove old used time data
if (realTime.shouldTrustTimePermanently) { if (realTime.shouldTrustTimePermanently) {
val dayChange = dayChangeTracker.reportDayChange(nowDate.dayOfEpoch) val dayChange = dayChangeTracker.reportDayChange(dayOfEpoch)
fun deleteOldUsedTimes() = UsedTimeDeleter.deleteOldUsedTimeItems( fun deleteOldUsedTimes() = UsedTimeDeleter.deleteOldUsedTimeItems(
database = appLogic.database, database = appLogic.database,
date = nowDate, date = DateInTimezone.newInstance(nowDate),
timestamp = nowTimestamp timestamp = nowTimestamp
) )
@ -266,10 +257,6 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
} }
} }
// get the categories
val categories = childCategories.get(deviceUserEntry.id).waitForNonNullValue()
val temporarilyAllowedApps = temporarilyAllowedApps.waitForNonNullValue()
// get the current status // get the current status
val isScreenOn = appLogic.platformIntegration.isScreenOn() val isScreenOn = appLogic.platformIntegration.isScreenOn()
val batteryStatus = appLogic.platformIntegration.getBatteryStatus() val batteryStatus = appLogic.platformIntegration.getBatteryStatus()
@ -277,293 +264,104 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
appLogic.defaultUserLogic.reportScreenOn(isScreenOn) appLogic.defaultUserLogic.reportScreenOn(isScreenOn)
if (!isScreenOn) { if (!isScreenOn) {
if (temporarilyAllowedApps.isNotEmpty()) { if (deviceRelatedData.temporarilyAllowedApps.isNotEmpty()) {
resetTemporarilyAllowedApps() resetTemporarilyAllowedApps()
} }
} }
fun reportStatusToCategoryHandlingCache(userRelatedData: UserRelatedData) {
categoryHandlingCache.reportStatus(
user = userRelatedData,
timeInMillis = nowTimestamp,
shouldTrustTimeTemporarily = realTime.shouldTrustTimeTemporarily,
assumeCurrentDevice = CurrentDeviceLogic.handleDeviceAsCurrentDevice(deviceRelatedData, userRelatedData),
batteryStatus = batteryStatus
)
}; reportStatusToCategoryHandlingCache(userRelatedData)
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 audioPlaybackPackageName = appLogic.platformIntegration.getMusicPlaybackPackage()
val activityLevelBlocking = appLogic.deviceEntry.value?.enableActivityLevelBlocking ?: false val activityLevelBlocking = appLogic.deviceEntry.value?.enableActivityLevelBlocking ?: false
foregroundAppHandling.reset() val foregroundAppBaseHandling = AppBaseHandling.calculate(
audioPlaybackHandling.reset()
BackgroundTaskRestrictionLogic.getHandling(
foregroundAppPackageName = foregroundAppPackageName, foregroundAppPackageName = foregroundAppPackageName,
foregroundAppActivityName = foregroundAppActivityName, foregroundAppActivityName = foregroundAppActivityName,
pauseForegroundAppBackgroundLoop = pauseForegroundAppBackgroundLoop, pauseForegroundAppBackgroundLoop = pauseForegroundAppBackgroundLoop,
temporarilyAllowedApps = temporarilyAllowedApps, userRelatedData = userRelatedData,
categories = categories, deviceRelatedData = deviceRelatedData,
activityLevelBlocking = activityLevelBlocking, pauseCounting = !isScreenOn
deviceUserEntry = deviceUserEntry,
batteryStatus = batteryStatus,
shouldTrustTimeTemporarily = realTime.shouldTrustTimeTemporarily,
nowTimestamp = nowTimestamp,
minuteOfWeek = minuteOfWeek,
cache = cache,
result = foregroundAppHandling
) )
BackgroundTaskRestrictionLogic.getHandling( val backgroundAppBaseHandling = AppBaseHandling.calculate(
foregroundAppPackageName = audioPlaybackPackageName, foregroundAppPackageName = audioPlaybackPackageName,
foregroundAppActivityName = null, foregroundAppActivityName = null,
pauseForegroundAppBackgroundLoop = false, pauseForegroundAppBackgroundLoop = false,
temporarilyAllowedApps = temporarilyAllowedApps, userRelatedData = userRelatedData,
categories = categories, deviceRelatedData = deviceRelatedData,
activityLevelBlocking = activityLevelBlocking, pauseCounting = false
deviceUserEntry = deviceUserEntry,
batteryStatus = batteryStatus,
shouldTrustTimeTemporarily = realTime.shouldTrustTimeTemporarily,
nowTimestamp = nowTimestamp,
minuteOfWeek = minuteOfWeek,
cache = cache,
result = audioPlaybackHandling
) )
// update used time helper if date does not match // check if should be blocked
if (usedTimeUpdateHelper?.date != nowDate) { val blockForegroundApp = foregroundAppBaseHandling is AppBaseHandling.BlockDueToNoCategory ||
usedTimeUpdateHelper?.forceCommit(appLogic) (foregroundAppBaseHandling is AppBaseHandling.UseCategories && foregroundAppBaseHandling.categoryIds.find {
usedTimeUpdateHelper = UsedTimeUpdateHelper(nowDate) categoryHandlingCache.get(it).shouldBlockActivities
} } != null)
val usedTimeUpdateHelper = usedTimeUpdateHelper!! val blockAudioPlayback = backgroundAppBaseHandling is AppBaseHandling.BlockDueToNoCategory ||
(backgroundAppBaseHandling is AppBaseHandling.UseCategories && backgroundAppBaseHandling.categoryIds.find {
val handling = categoryHandlingCache.get(it)
val hasPremium = deviceRelatedData.isLocalMode || deviceRelatedData.isConnectedAndHasPremium
val blockAllNotifications = handling.blockAllNotifications && hasPremium
// check times handling.shouldBlockActivities || blockAllNotifications
fun buildDummyUsedTimeItems(categoryId: String): List<UsedTimeItem> { } != null)
if (!usedTimeUpdateHelper.timeToAdd.containsKey(categoryId)) {
return emptyList()
}
return (usedTimeUpdateHelper.additionalSlots[categoryId] ?: emptySet()).map {
UsedTimeItem(
categoryId = categoryId,
startTimeOfDay = it.start,
endTimeOfDay = it.end,
dayOfEpoch = usedTimeUpdateHelper.date.dayOfEpoch,
usedMillis = (usedTimeUpdateHelper.timeToAdd[categoryId] ?: 0).toLong()
)
} + listOf(
UsedTimeItem(
categoryId = categoryId,
startTimeOfDay = MinuteOfDay.MIN,
endTimeOfDay = MinuteOfDay.MAX,
dayOfEpoch = usedTimeUpdateHelper.date.dayOfEpoch,
usedMillis = (usedTimeUpdateHelper.timeToAdd[categoryId] ?: 0).toLong()
)
)
}
suspend fun getRemainingTime(categoryId: String?): RemainingTime? {
categoryId ?: return null
val category = categories.find { it.id == categoryId } ?: return null
val rules = timeLimitRules.get(category.id).waitForNonNullValue()
if (rules.isEmpty()) {
return null
}
val firstDayOfWeekAsEpochDay = nowDate.dayOfEpoch - nowDate.dayOfWeek
val usedTimes = usedTimesOfCategoryAndWeekByFirstDayOfWeek.get(Pair(category.id, firstDayOfWeekAsEpochDay)).waitForNonNullValue()
return RemainingTime.getRemainingTime(
nowDate.dayOfWeek,
minuteOfWeek % MinuteOfDay.LENGTH,
usedTimes + buildDummyUsedTimeItems(categoryId),
rules,
Math.max(0, category.getExtraTime(dayOfEpoch = nowDate.dayOfEpoch) - (usedTimeUpdateHelper.extraTimeToSubtract.get(categoryId) ?: 0)),
firstDayOfWeekAsEpochDay
)
}
suspend fun getRemainingSessionDuration(categoryId: String?): Long? {
categoryId ?: return null
val category = categories.find { it.id == categoryId } ?: return null
val rules = timeLimitRules.get(category.id).waitForNonNullValue()
val durations = cache.usedSessionDurationsByCategoryId.get(categoryId).waitForNonNullValue()
val timeToAdd = usedTimeUpdateHelper.timeToAdd[categoryId] ?: 0
val result = RemainingSessionDuration.getRemainingSessionDuration(
rules = rules,
durationsOfCategory = durations,
timestamp = nowTimestamp,
dayOfWeek = nowDate.dayOfWeek,
minuteOfDay = minuteOfWeek % MinuteOfDay.LENGTH
)
if (result == null) {
return null
} else {
return (result - timeToAdd).coerceAtLeast(0)
}
}
// note: remainingTime != null implicates that there are limits and they are currently not ignored
val remainingTimeForegroundAppChild = if (foregroundAppHandling.status == BackgroundTaskLogicAppStatus.AllowedCountAndCheckTime) getRemainingTime(foregroundAppHandling.categoryId) else null
val remainingTimeForegroundAppParent = if (foregroundAppHandling.status == BackgroundTaskLogicAppStatus.AllowedCountAndCheckTime) getRemainingTime(foregroundAppHandling.parentCategoryId) else null
val remainingTimeForegroundApp = RemainingTime.min(remainingTimeForegroundAppChild, remainingTimeForegroundAppParent)
val remainingSessionDurationForegroundAppChild = if (foregroundAppHandling.status == BackgroundTaskLogicAppStatus.AllowedCountAndCheckTime) getRemainingSessionDuration(foregroundAppHandling.categoryId) else null
val remainingSessionDurationForegroundAppParent = if (foregroundAppHandling.status == BackgroundTaskLogicAppStatus.AllowedCountAndCheckTime) getRemainingSessionDuration(foregroundAppHandling.parentCategoryId) else null
val remainingSessionDurationForegroundApp = RemainingSessionDuration.min(remainingSessionDurationForegroundAppChild, remainingSessionDurationForegroundAppParent)
val remainingTimeBackgroundAppChild = if (audioPlaybackHandling.status == BackgroundTaskLogicAppStatus.AllowedCountAndCheckTime) getRemainingTime(audioPlaybackHandling.categoryId) else null
val remainingTimeBackgroundAppParent = if (audioPlaybackHandling.status == BackgroundTaskLogicAppStatus.AllowedCountAndCheckTime) getRemainingTime(audioPlaybackHandling.parentCategoryId) else null
val remainingTimeBackgroundApp = RemainingTime.min(remainingTimeBackgroundAppChild, remainingTimeBackgroundAppParent)
val remainingSessionDurationBackgroundAppChild = if (audioPlaybackHandling.status == BackgroundTaskLogicAppStatus.AllowedCountAndCheckTime) getRemainingSessionDuration(audioPlaybackHandling.categoryId) else null
val remainingSessionDurationBackgroundAppParent = if (audioPlaybackHandling.status == BackgroundTaskLogicAppStatus.AllowedCountAndCheckTime) getRemainingSessionDuration(audioPlaybackHandling.parentCategoryId) else null
val remainingSessionDurationBackgroundApp = RemainingSessionDuration.min(remainingSessionDurationBackgroundAppChild, remainingSessionDurationBackgroundAppParent)
val sessionDurationLimitReachedForegroundApp = (remainingSessionDurationForegroundApp != null && remainingSessionDurationForegroundApp == 0L)
val sessionDurationLimitReachedBackgroundApp = (remainingSessionDurationBackgroundApp != null && remainingSessionDurationBackgroundApp == 0L)
// eventually block
if (remainingTimeForegroundApp?.hasRemainingTime == false || sessionDurationLimitReachedForegroundApp) {
foregroundAppHandling.status = BackgroundTaskLogicAppStatus.ShouldBlock
}
if (remainingTimeBackgroundApp?.hasRemainingTime == false || sessionDurationLimitReachedBackgroundApp) {
audioPlaybackHandling.status = BackgroundTaskLogicAppStatus.ShouldBlock
}
// update times // update times
val timeToSubtract = Math.min(previousMainLogicExecutionTime, maxUsedTimeToAdd) val timeToSubtract = Math.min(previousMainLogicExecutionTime, maxUsedTimeToAdd)
// see note above declaration of remainingTimeForegroundAppChild val categoryHandlingsToCount = AppBaseHandling.getCategoriesForCounting(foregroundAppBaseHandling, backgroundAppBaseHandling)
val shouldCountForegroundApp = remainingTimeForegroundApp != null && isScreenOn && remainingTimeForegroundApp.hasRemainingTime .map { categoryHandlingCache.get(it) }
val shouldCountBackgroundApp = remainingTimeBackgroundApp != null && remainingTimeBackgroundApp.hasRemainingTime .filter { it.shouldCountTime }
val categoriesToCount = mutableSetOf<String>() if (
val categoriesToCountExtraTime = mutableSetOf<String>() usedTimeUpdateHelper.report(
val categoriesToCountSessionDurations = mutableSetOf<String>() duration = timeToSubtract,
dayOfEpoch = dayOfEpoch,
if (shouldCountForegroundApp) {
remainingTimeForegroundAppChild?.let { remainingTime ->
foregroundAppHandling.categoryId?.let { categoryId ->
categoriesToCount.add(categoryId)
if (remainingTime.usingExtraTime) {
categoriesToCountExtraTime.add(categoryId)
}
if (!sessionDurationLimitReachedForegroundApp) {
categoriesToCountSessionDurations.add(categoryId)
}
}
}
remainingTimeForegroundAppParent?.let { remainingTime ->
foregroundAppHandling.parentCategoryId?.let {
categoriesToCount.add(it)
if (remainingTime.usingExtraTime) {
categoriesToCountExtraTime.add(it)
}
if (!sessionDurationLimitReachedForegroundApp) {
categoriesToCountSessionDurations.add(it)
}
}
}
}
if (shouldCountBackgroundApp) {
remainingTimeBackgroundAppChild?.let { remainingTime ->
audioPlaybackHandling.categoryId?.let {
categoriesToCount.add(it)
if (remainingTime.usingExtraTime) {
categoriesToCountExtraTime.add(it)
}
if (!sessionDurationLimitReachedBackgroundApp) {
categoriesToCountSessionDurations.add(it)
}
}
}
remainingTimeBackgroundAppParent?.let { remainingTime ->
audioPlaybackHandling.parentCategoryId?.let {
categoriesToCount.add(it)
if (remainingTime.usingExtraTime) {
categoriesToCountExtraTime.add(it)
}
if (!sessionDurationLimitReachedBackgroundApp) {
categoriesToCountSessionDurations.add(it)
}
}
}
}
if (categoriesToCount.isNotEmpty()) {
categoriesToCount.forEach { categoryId ->
// only handle rules which are related at today and the current time
val rules = RemainingTime.getRulesRelatedToDay(
dayOfWeek = nowDate.dayOfWeek,
minuteOfDay = minuteOfWeek % MinuteOfDay.LENGTH,
rules = timeLimitRules.get(categoryId).waitForNonNullValue()
)
usedTimeUpdateHelper.add(
categoryId = categoryId,
time = timeToSubtract,
includingExtraTime = categoriesToCountExtraTime.contains(categoryId),
slots = run {
val slots = mutableSetOf<AddUsedTimeActionItemAdditionalCountingSlot>()
rules.forEach { rule ->
if (!rule.appliesToWholeDay) {
slots.add(
AddUsedTimeActionItemAdditionalCountingSlot(
rule.startMinuteOfDay, rule.endMinuteOfDay
)
)
}
}
slots
},
trustedTimestamp = if (realTime.shouldTrustTimePermanently) realTime.timeInMillis else 0, trustedTimestamp = if (realTime.shouldTrustTimePermanently) realTime.timeInMillis else 0,
sessionDurationLimits = run { handlings = categoryHandlingsToCount
val slots = mutableSetOf<AddUsedTimeActionItemSessionDurationLimitSlot>()
if (categoriesToCountSessionDurations.contains(categoryId)) {
rules.forEach { rule ->
if (rule.sessionDurationLimitEnabled) {
slots.add(
AddUsedTimeActionItemSessionDurationLimitSlot(
startMinuteOfDay = rule.startMinuteOfDay,
endMinuteOfDay = rule.endMinuteOfDay,
sessionPauseDuration = rule.sessionPauseMilliseconds,
maxSessionDuration = rule.sessionDurationMilliseconds
)
)
}
}
}
slots
}
) )
) {
val newDeviceAndUserRelatedData = Threads.database.executeAndWait {
appLogic.database.derivedDataDao().getUserAndDeviceRelatedDataSync()
} }
if (
newDeviceAndUserRelatedData?.userRelatedData?.user?.id != deviceAndUSerRelatedData.userRelatedData.user.id ||
newDeviceAndUserRelatedData.userRelatedData.categoryById.keys != deviceAndUSerRelatedData.userRelatedData.categoryById.keys
) {
// start the loop directly again
continue
}
reportStatusToCategoryHandlingCache(userRelatedData = newDeviceAndUserRelatedData.userRelatedData)
} }
usedTimeUpdateHelper.reportCurrentCategories(categoriesToCount) val categoriesToCount = categoryHandlingsToCount.map { it.createdWithCategoryRelatedData.category.id }
if (usedTimeUpdateHelper.shouldDoAutoCommit) { fun timeToSubtractForCategory(categoryId: String): Int {
usedTimeUpdateHelper.forceCommit(appLogic) return if (usedTimeUpdateHelper.getCountedCategoryIds().contains(categoryId)) usedTimeUpdateHelper.getCountedTime() else 0
} }
// trigger time warnings // trigger time warnings
fun eventuallyTriggerTimeWarning(remaining: RemainingTime, categoryId: String?) { categoriesToCount.forEach { categoryId ->
val category = categories.find { it.id == categoryId } ?: return val category = userRelatedData.categoryById[categoryId]!!.category
val oldRemainingTime = remaining.includingExtraTime val handling = categoryHandlingCache.get(categoryId)
val newRemainingTime = oldRemainingTime - timeToSubtract val nowRemaining = handling.remainingTime ?: return@forEach // category is not limited anymore
val newRemainingTime = nowRemaining.includingExtraTime - timeToSubtractForCategory(categoryId)
val oldRemainingTime = newRemainingTime + timeToSubtract
if (oldRemainingTime / (1000 * 60) != newRemainingTime / (1000 * 60)) { if (oldRemainingTime / (1000 * 60) != newRemainingTime / (1000 * 60)) {
// eventually show remaining time warning // eventually show remaining time warning
@ -579,11 +377,6 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
} }
} }
remainingTimeForegroundAppChild?.let { eventuallyTriggerTimeWarning(it, foregroundAppHandling.categoryId) }
remainingTimeForegroundAppParent?.let { eventuallyTriggerTimeWarning(it, foregroundAppHandling.parentCategoryId) }
remainingTimeBackgroundAppChild?.let { eventuallyTriggerTimeWarning(it, audioPlaybackHandling.categoryId) }
remainingTimeBackgroundAppParent?.let { eventuallyTriggerTimeWarning(it, audioPlaybackHandling.parentCategoryId) }
// show notification // show notification
fun buildStatusMessageWithCurrentAppTitle( fun buildStatusMessageWithCurrentAppTitle(
text: String, text: String,
@ -597,107 +390,156 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
if (appActivityToShow != null && appPackageName != null) appActivityToShow.removePrefix(appPackageName) else null if (appActivityToShow != null && appPackageName != null) appActivityToShow.removePrefix(appPackageName) else null
) )
fun getCategoryTitle(categoryId: String?): String = categories.find { it.id == categoryId }?.title ?: categoryId.toString() fun getCategoryTitle(categoryId: String?): String = categoryId.let { userRelatedData.categoryById[it]?.category?.title } ?: categoryId.toString()
fun buildStatusMessage( fun buildNotificationForAppWithCategoryUsage(
handling: BackgroundTaskRestrictionLogicResult, suffix: String,
remainingTime: RemainingTime?, appPackageName: String?,
remainingSessionDuration: Long?, appActivityToShow: String?,
categoryId: String
): AppStatusMessage {
val handling = categoryHandlingCache.get(categoryId)
return if (handling.areLimitsTemporarilyDisabled) {
buildStatusMessageWithCurrentAppTitle(
text = appLogic.context.getString(R.string.background_logic_limits_disabled),
titleSuffix = suffix,
appPackageName = appPackageName,
appActivityToShow = appActivityToShow
)
} else if (handling.remainingTime == null) {
buildStatusMessageWithCurrentAppTitle(
text = appLogic.context.getString(R.string.background_logic_no_timelimit),
titlePrefix = getCategoryTitle(categoryId) + " - ",
titleSuffix = suffix,
appPackageName = appPackageName,
appActivityToShow = appActivityToShow
)
} else {
val remainingTimeFromCache = handling.remainingTime
val timeSubtractedFromThisCategory = timeToSubtractForCategory(categoryId)
val realRemainingTimeDefault = (remainingTimeFromCache.default - timeSubtractedFromThisCategory).coerceAtLeast(0)
val realRemainingTimeWithExtraTime = (remainingTimeFromCache.includingExtraTime - timeSubtractedFromThisCategory).coerceAtLeast(0)
val realRemainingTimeUsingExtraTime = realRemainingTimeDefault == 0L && realRemainingTimeWithExtraTime > 0
val remainingSessionDuration = handling.remainingSessionDuration?.let { (it - timeToSubtractForCategory(categoryId)).coerceAtLeast(0) }
buildStatusMessageWithCurrentAppTitle(
text = if (realRemainingTimeUsingExtraTime)
appLogic.context.getString(R.string.background_logic_using_extra_time, TimeTextUtil.remaining(realRemainingTimeWithExtraTime.toInt(), appLogic.context))
else if (remainingSessionDuration != null && remainingSessionDuration < realRemainingTimeDefault)
TimeTextUtil.pauseIn(remainingSessionDuration.toInt(), appLogic.context)
else
TimeTextUtil.remaining(realRemainingTimeDefault.toInt() ?: 0, appLogic.context),
titlePrefix = getCategoryTitle(categoryId) + " - ",
titleSuffix = suffix,
appPackageName = appPackageName,
appActivityToShow = appActivityToShow
)
}
}
fun buildNotificationForAppWithoutCategoryUsage(
handling: AppBaseHandling,
suffix: String, suffix: String,
appPackageName: String?, appPackageName: String?,
appActivityToShow: String? appActivityToShow: String?
): AppStatusMessage = when (handling.status) { ): AppStatusMessage = when (handling) {
BackgroundTaskLogicAppStatus.ShouldBlock -> buildStatusMessageWithCurrentAppTitle( is AppBaseHandling.UseCategories -> throw IllegalArgumentException()
text = appLogic.context.getString(R.string.background_logic_opening_lockscreen), AppBaseHandling.BlockDueToNoCategory -> throw IllegalArgumentException()
titleSuffix = suffix, AppBaseHandling.PauseLogic -> AppStatusMessage(
appPackageName = appPackageName,
appActivityToShow = appActivityToShow
)
BackgroundTaskLogicAppStatus.BackgroundLogicPaused -> AppStatusMessage(
title = appLogic.context.getString(R.string.background_logic_paused_title) + suffix, title = appLogic.context.getString(R.string.background_logic_paused_title) + suffix,
text = appLogic.context.getString(R.string.background_logic_paused_text) text = appLogic.context.getString(R.string.background_logic_paused_text)
) )
BackgroundTaskLogicAppStatus.InternalWhitelist -> buildStatusMessageWithCurrentAppTitle( AppBaseHandling.Whitelist -> buildStatusMessageWithCurrentAppTitle(
text = appLogic.context.getString(R.string.background_logic_whitelisted), text = appLogic.context.getString(R.string.background_logic_whitelisted),
titleSuffix = suffix, titleSuffix = suffix,
appPackageName = appPackageName, appPackageName = appPackageName,
appActivityToShow = appActivityToShow appActivityToShow = appActivityToShow
) )
BackgroundTaskLogicAppStatus.TemporarilyAllowed -> buildStatusMessageWithCurrentAppTitle( AppBaseHandling.TemporarilyAllowed -> buildStatusMessageWithCurrentAppTitle(
text = appLogic.context.getString(R.string.background_logic_temporarily_allowed), text = appLogic.context.getString(R.string.background_logic_temporarily_allowed),
titleSuffix = suffix, titleSuffix = suffix,
appPackageName = appPackageName, appPackageName = appPackageName,
appActivityToShow = appActivityToShow appActivityToShow = appActivityToShow
) )
BackgroundTaskLogicAppStatus.LimitsDisabled -> buildStatusMessageWithCurrentAppTitle( AppBaseHandling.Idle -> AppStatusMessage(
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 -> buildStatusMessageWithCurrentAppTitle(
text = if (remainingTime?.usingExtraTime == true)
appLogic.context.getString(R.string.background_logic_using_extra_time, TimeTextUtil.remaining(remainingTime.includingExtraTime.toInt(), appLogic.context))
else if (remainingTime != null && remainingSessionDuration != null && remainingSessionDuration < remainingTime.default)
TimeTextUtil.pauseIn(remainingSessionDuration.toInt(), appLogic.context)
else
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_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 && val showBackgroundStatus = !(backgroundAppBaseHandling is AppBaseHandling.Idle) &&
audioPlaybackHandling.status != BackgroundTaskLogicAppStatus.ShouldBlock && !blockAudioPlayback &&
audioPlaybackPackageName != foregroundAppPackageName audioPlaybackPackageName != foregroundAppPackageName
if (showBackgroundStatus && nowTimestamp % 6000 >= 3000) { val statusMessage = if (blockForegroundApp) {
// show notification for music buildStatusMessageWithCurrentAppTitle(
appLogic.platformIntegration.setAppStatusMessage( text = appLogic.context.getString(R.string.background_logic_opening_lockscreen),
buildStatusMessage( appPackageName = foregroundAppPackageName,
handling = audioPlaybackHandling, appActivityToShow = if (activityLevelBlocking) foregroundAppActivityName else null
remainingTime = remainingTimeBackgroundApp,
suffix = " (2/2)",
appPackageName = audioPlaybackPackageName,
appActivityToShow = null,
remainingSessionDuration = remainingSessionDurationBackgroundApp
)
) )
} else { } else {
// show regular notification val pagesForTheForegroundApp = if (foregroundAppBaseHandling is AppBaseHandling.UseCategories) foregroundAppBaseHandling.categoryIds.size else 1
appLogic.platformIntegration.setAppStatusMessage( val pagesForTheBackgroundApp = if (!showBackgroundStatus) 0 else if (backgroundAppBaseHandling is AppBaseHandling.UseCategories) backgroundAppBaseHandling.categoryIds.size else 1
buildStatusMessage( val totalPages = pagesForTheForegroundApp + pagesForTheBackgroundApp
handling = foregroundAppHandling, val currentPage = (nowTimestamp / 3000 % totalPages).toInt()
remainingTime = remainingTimeForegroundApp,
suffix = if (showBackgroundStatus) " (1/2)" else "", val suffix = if (totalPages == 1) "" else " (${currentPage + 1} / $totalPages)"
if (currentPage < pagesForTheForegroundApp) {
val pageWithin = currentPage
if (foregroundAppBaseHandling is AppBaseHandling.UseCategories) {
val categoryId = foregroundAppBaseHandling.categoryIds.toList()[pageWithin]
buildNotificationForAppWithCategoryUsage(
appPackageName = foregroundAppPackageName, appPackageName = foregroundAppPackageName,
appActivityToShow = if (activityLevelBlocking) foregroundAppActivityName else null, appActivityToShow = if (activityLevelBlocking) foregroundAppActivityName else null,
remainingSessionDuration = remainingSessionDurationForegroundApp suffix = suffix,
categoryId = categoryId
) )
) } else {
buildNotificationForAppWithoutCategoryUsage(
appPackageName = foregroundAppPackageName,
appActivityToShow = if (activityLevelBlocking) foregroundAppActivityName else null,
suffix = suffix,
handling = foregroundAppBaseHandling
)
}
} else {
val pageWithin = currentPage - pagesForTheForegroundApp
if (backgroundAppBaseHandling is AppBaseHandling.UseCategories) {
val categoryId = backgroundAppBaseHandling.categoryIds.toList()[pageWithin]
buildNotificationForAppWithCategoryUsage(
appPackageName = audioPlaybackPackageName,
appActivityToShow = null,
suffix = suffix,
categoryId = categoryId
)
} else {
buildNotificationForAppWithoutCategoryUsage(
appPackageName = audioPlaybackPackageName,
appActivityToShow = null,
suffix = suffix,
handling = backgroundAppBaseHandling
)
}
}
} }
appLogic.platformIntegration.setAppStatusMessage(statusMessage)
// handle blocking // handle blocking
if (foregroundAppHandling.status == BackgroundTaskLogicAppStatus.ShouldBlock) { if (blockForegroundApp) {
openLockscreen(foregroundAppPackageName!!, foregroundAppActivityName) openLockscreen(foregroundAppPackageName!!, foregroundAppActivityName)
} else { } else {
appLogic.platformIntegration.setShowBlockingOverlay(false) appLogic.platformIntegration.setShowBlockingOverlay(false)
} }
if (audioPlaybackHandling.status == BackgroundTaskLogicAppStatus.ShouldBlock && audioPlaybackPackageName != null) { if (blockAudioPlayback && audioPlaybackPackageName != null) {
appLogic.platformIntegration.muteAudioIfPossible(audioPlaybackPackageName) appLogic.platformIntegration.muteAudioIfPossible(audioPlaybackPackageName)
} }
} catch (ex: SecurityException) { } catch (ex: SecurityException) {
@ -723,8 +565,6 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
appLogic.platformIntegration.setShowBlockingOverlay(false) appLogic.platformIntegration.setShowBlockingOverlay(false)
} }
liveDataCaches.reportLoopDone()
// delay before running next time // delay before running next time
val endTime = appLogic.timeApi.getCurrentUptimeInMillis() val endTime = appLogic.timeApi.getCurrentUptimeInMillis()
previousMainLogicExecutionTime = (endTime - previousMainLoopEndTime).toInt() previousMainLogicExecutionTime = (endTime - previousMainLoopEndTime).toInt()

View file

@ -1,70 +0,0 @@
/*
* 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 androidx.lifecycle.LiveData
import io.timelimit.android.data.model.*
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<List<UsedTimeItem>, Pair<String, Int>>() {
override fun createValue(key: Pair<String, Int>): LiveData<List<UsedTimeItem>> {
return appLogic.database.usedTimes().getUsedTimesOfWeek(key.first, key.second)
}
}
val usedSessionDurationsByCategoryId = object: MultiKeyLiveDataCache<List<SessionDuration>, String>() {
override fun createValue(key: String): LiveData<List<SessionDuration>> {
return appLogic.database.sessionDuration().getSessionDurationItemsByCategoryId(key)
}
}
val shouldDoAutomaticSignOut = SingleItemLiveDataCacheWithRequery { -> appLogic.defaultUserLogic.hasAutomaticSignOut()}
val liveDataCaches = LiveDataCaches(arrayOf(
deviceUserEntryLive,
isThisDeviceTheCurrentDeviceLive,
childCategories,
appCategories,
timeLimitRules,
usedTimesOfCategoryAndWeekByFirstDayOfWeek,
usedSessionDurationsByCategoryId,
shouldDoAutomaticSignOut
))
}

View file

@ -1,186 +0,0 @@
/*
* 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 && (
(!shouldTrustTimeTemporarily) || (category.temporarilyBlockedEndTime == 0L) || (category.temporarilyBlockedEndTime > nowTimestamp))) or
(parentCategory?.temporarilyBlocked == true && (
(!shouldTrustTimeTemporarily) || (parentCategory.temporarilyBlockedEndTime == 0L) || (parentCategory.temporarilyBlockedEndTime > nowTimestamp)))
) {
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

@ -15,17 +15,10 @@
*/ */
package io.timelimit.android.logic package io.timelimit.android.logic
import android.util.Log
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import io.timelimit.android.BuildConfig
import io.timelimit.android.data.model.*
import io.timelimit.android.date.DateInTimezone import io.timelimit.android.date.DateInTimezone
import io.timelimit.android.date.getMinuteOfWeek import io.timelimit.android.date.getMinuteOfWeek
import io.timelimit.android.extensions.MinuteOfDay
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 java.util.* import java.util.*
enum class BlockingReason { enum class BlockingReason {
@ -47,382 +40,8 @@ enum class BlockingLevel {
Activity Activity
} }
sealed class BlockingReasonDetail {
abstract val areNotificationsBlocked: Boolean
}
data class NoBlockingReason(
override val areNotificationsBlocked: Boolean
): BlockingReasonDetail() {
companion object {
private val instanceWithoutNotificationsBlocked = NoBlockingReason(areNotificationsBlocked = false)
private val instanceWithNotificationsBlocked = NoBlockingReason(areNotificationsBlocked = true)
fun getInstance(areNotificationsBlocked: Boolean) = if (areNotificationsBlocked)
instanceWithNotificationsBlocked
else
instanceWithoutNotificationsBlocked
}
}
data class BlockedReasonDetails(
val reason: BlockingReason,
val level: BlockingLevel,
val categoryId: String?,
override val areNotificationsBlocked: Boolean
): BlockingReasonDetail()
class BlockingReasonUtil(private val appLogic: AppLogic) { class BlockingReasonUtil(private val appLogic: AppLogic) {
companion object {
private const val LOG_TAG = "BlockingReason"
}
private val enableActivityLevelFiltering = appLogic.deviceEntry.map { it?.enableActivityLevelBlocking ?: false }
private val batteryLevel = appLogic.platformIntegration.getBatteryStatusLive()
fun getBlockingReason(packageName: String, activityName: String?): LiveData<BlockingReasonDetail> {
// check precondition that the app is running
return appLogic.enable.switchMap {
enabled ->
if (enabled == null || enabled == false) {
liveDataFromValue(NoBlockingReason.getInstance(areNotificationsBlocked = false) as BlockingReasonDetail)
} else {
appLogic.deviceUserEntry.switchMap {
user ->
if (user == null || user.type != UserType.Child) {
liveDataFromValue(NoBlockingReason.getInstance(areNotificationsBlocked = false) as BlockingReasonDetail)
} else {
getBlockingReasonStep2(packageName, activityName, user, TimeZone.getTimeZone(user.timeZone))
}
}
}
}
}
private fun getBlockingReasonStep2(packageName: String, activityName: String?, child: User, timeZone: TimeZone): LiveData<BlockingReasonDetail> {
if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "step 2")
}
// check internal whitelist
if (packageName == BuildConfig.APPLICATION_ID) {
return liveDataFromValue(NoBlockingReason.getInstance(areNotificationsBlocked = false))
} else if (AndroidIntegrationApps.ignoredApps[packageName].let {
when (it) {
null -> false
AndroidIntegrationApps.IgnoredAppHandling.Ignore -> true
AndroidIntegrationApps.IgnoredAppHandling.IgnoreOnStoreOtherwiseWhitelistAndDontDisable -> BuildConfig.storeCompilant
}
}) {
return liveDataFromValue(NoBlockingReason.getInstance(areNotificationsBlocked = false))
} else {
return getBlockingReasonStep3(packageName, activityName, child, timeZone)
}
}
private fun getBlockingReasonStep3(packageName: String, activityName: String?, child: User, timeZone: TimeZone): LiveData<BlockingReasonDetail> {
if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "step 3")
}
// check temporarily allowed Apps
return appLogic.deviceId.switchMap {
if (it != null) {
appLogic.database.temporarilyAllowedApp().getTemporarilyAllowedApps(deviceId = it)
} else {
liveDataFromValue(Collections.emptyList())
}
}.switchMap {
temporarilyAllowedApps ->
if (temporarilyAllowedApps.contains(packageName)) {
liveDataFromValue(NoBlockingReason.getInstance(areNotificationsBlocked = false) as BlockingReasonDetail)
} else {
getBlockingReasonStep4(packageName, activityName, child, timeZone)
}
}
}
private fun getBlockingReasonStep4(packageName: String, activityName: String?, child: User, timeZone: TimeZone): LiveData<BlockingReasonDetail> {
if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "step 4")
}
return appLogic.database.category().getCategoriesByChildId(child.id).switchMap {
childCategories ->
val categoryAppLevel = appLogic.database.categoryApp().getCategoryApp(childCategories.map { it.id }, packageName)
val categoryAppActivityLevel = enableActivityLevelFiltering.switchMap {
if (it)
appLogic.database.categoryApp().getCategoryApp(childCategories.map { it.id }, "$packageName:$activityName")
else
liveDataFromValue(null as CategoryApp?)
}
val categoryApp = categoryAppLevel.switchMap { appLevel ->
categoryAppActivityLevel.map { activityLevel ->
activityLevel?.let { it to BlockingLevel.Activity } ?: appLevel?.let { it to BlockingLevel.App }
}
}
Transformations.map(categoryApp) {
categoryApp ->
if (categoryApp == null) {
null
} else {
childCategories.find { it.id == categoryApp.first.categoryId }?.let { it to categoryApp.second }
}
}
}.switchMap {
categoryEntry ->
if (categoryEntry == null) {
val defaultCategory = if (child.categoryForNotAssignedApps.isEmpty())
liveDataFromValue(null as Category?)
else
appLogic.database.category().getCategoryByChildIdAndId(child.id, child.categoryForNotAssignedApps)
defaultCategory.switchMap { categoryEntry2 ->
if (categoryEntry2 == null) {
liveDataFromValue(
BlockedReasonDetails(
areNotificationsBlocked = false,
level = BlockingLevel.App,
reason = BlockingReason.NotPartOfAnCategory,
categoryId = null
) as BlockingReasonDetail
)
} else {
getBlockingReasonStep4Point5(categoryEntry2, child, timeZone, false, BlockingLevel.App)
}
}
} else {
getBlockingReasonStep4Point5(categoryEntry.first, child, timeZone, false, categoryEntry.second)
}
}
}
private fun getBlockingReasonStep4Point5(category: Category, child: User, timeZone: TimeZone, isParentCategory: Boolean, blockingLevel: BlockingLevel): LiveData<BlockingReasonDetail> {
if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "step 4.5")
}
val shouldBlockNotifications = if (category.blockAllNotifications)
appLogic.fullVersion.shouldProvideFullVersionFunctions
else
liveDataFromValue(false)
val nextLevel = getBlockingReasonStep4Point6(category, child, timeZone, isParentCategory, blockingLevel)
return shouldBlockNotifications.switchMap { blockNotifications ->
nextLevel.map { blockingReason ->
if (blockingReason == BlockingReason.None) {
NoBlockingReason.getInstance(areNotificationsBlocked = blockNotifications)
} else {
BlockedReasonDetails(
areNotificationsBlocked = blockNotifications,
level = blockingLevel,
reason = blockingReason,
categoryId = category.id
)
}
}
}
}
private fun getBlockingReasonStep4Point6(category: Category, child: User, timeZone: TimeZone, isParentCategory: Boolean, blockingLevel: BlockingLevel): LiveData<BlockingReason> {
val next = getBlockingReasonStep4Point7(category, child, timeZone, isParentCategory, blockingLevel)
return if (category.minBatteryLevelWhileCharging == 0 && category.minBatteryLevelMobile == 0) {
next
} else {
val batteryLevelOk = batteryLevel.map { it.isCategoryAllowed(category) }.ignoreUnchanged()
batteryLevelOk.switchMap { ok ->
if (ok) {
next
} else {
liveDataFromValue(BlockingReason.BatteryLimit)
}
}
}
}
private fun getBlockingReasonStep4Point7(category: Category, child: User, timeZone: TimeZone, isParentCategory: Boolean, blockingLevel: BlockingLevel): LiveData<BlockingReason> {
if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "step 4.7")
}
if (category.temporarilyBlocked) {
if (category.temporarilyBlockedEndTime == 0L) {
return liveDataFromValue(BlockingReason.TemporarilyBlocked)
} else {
return getTemporarilyTrustedTimeInMillis().switchMap { time ->
if (time == null) {
liveDataFromValue(BlockingReason.MissingNetworkTime)
} else if (time < category.temporarilyBlockedEndTime) {
liveDataFromValue(BlockingReason.TemporarilyBlocked)
} else {
getBlockingReasonStep4Point8(category, child, timeZone, isParentCategory, blockingLevel)
}
}
}
} else {
return getBlockingReasonStep4Point8(category, child, timeZone, isParentCategory, blockingLevel)
}
}
private fun getBlockingReasonStep4Point8(category: Category, child: User, timeZone: TimeZone, isParentCategory: Boolean, blockingLevel: BlockingLevel): LiveData<BlockingReason> {
val areLimitsDisabled: LiveData<Boolean>
if (child.disableLimitsUntil == 0L) {
areLimitsDisabled = liveDataFromValue(false)
} else {
areLimitsDisabled = getTemporarilyTrustedTimeInMillis().map {
trustedTimeInMillis ->
trustedTimeInMillis != null && child.disableLimitsUntil > trustedTimeInMillis
}
}
return areLimitsDisabled.switchMap {
limitsDisabled ->
if (limitsDisabled) {
liveDataFromValue(BlockingReason.None)
} else {
getBlockingReasonStep5(category, timeZone)
}
}.switchMap { result ->
if (result == BlockingReason.None && (!isParentCategory) && category.parentCategoryId.isNotEmpty()) {
appLogic.database.category().getCategoryByChildIdAndId(child.id, category.parentCategoryId).switchMap { parentCategory ->
if (parentCategory == null) {
liveDataFromValue(BlockingReason.None)
} else {
getBlockingReasonStep4Point6(parentCategory, child, timeZone, true, blockingLevel)
}
}
} else {
liveDataFromValue(result)
}
}
}
private fun getBlockingReasonStep5(category: Category, timeZone: TimeZone): LiveData<BlockingReason> {
if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "step 5")
}
return Transformations.switchMap(getTrustedMinuteOfWeekLive(timeZone)) {
trustedMinuteOfWeek ->
if (category.blockedMinutesInWeek.dataNotToModify.isEmpty) {
getBlockingReasonStep6(category, timeZone, trustedMinuteOfWeek)
} else if (trustedMinuteOfWeek == null) {
liveDataFromValue(BlockingReason.MissingNetworkTime)
} else if (category.blockedMinutesInWeek.read(trustedMinuteOfWeek)) {
liveDataFromValue(BlockingReason.BlockedAtThisTime)
} else {
getBlockingReasonStep6(category, timeZone, trustedMinuteOfWeek)
}
}
}
private fun getBlockingReasonStep6(category: Category, timeZone: TimeZone, trustedMinuteOfWeek: Int?): LiveData<BlockingReason> {
if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "step 6")
}
return getTrustedDateLive(timeZone).switchMap {
nowTrustedDate ->
appLogic.database.timeLimitRules().getTimeLimitRulesByCategory(category.id).switchMap {
rules ->
if (rules.isEmpty()) {
liveDataFromValue(BlockingReason.None)
} else if (nowTrustedDate == null || trustedMinuteOfWeek == null) {
liveDataFromValue(BlockingReason.MissingNetworkTime)
} else {
getBlockingReasonStep6(category, nowTrustedDate, trustedMinuteOfWeek, rules)
}
}
}
}
private fun getBlockingReasonStep6(category: Category, nowTrustedDate: DateInTimezone, trustedMinuteOfWeek: Int, rules: List<TimeLimitRule>): LiveData<BlockingReason> {
if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "step 6 - 2")
}
return appLogic.currentDeviceLogic.isThisDeviceTheCurrentDevice.switchMap { isCurrentDevice ->
if (isCurrentDevice) {
getBlockingReasonStep7(category, nowTrustedDate, trustedMinuteOfWeek, rules)
} else {
liveDataFromValue(BlockingReason.RequiresCurrentDevice)
}
}
}
private fun getBlockingReasonStep7(category: Category, nowTrustedDate: DateInTimezone, trustedMinuteOfWeek: Int, rules: List<TimeLimitRule>): LiveData<BlockingReason> {
if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "step 7")
}
val extraTime = category.getExtraTime(dayOfEpoch = nowTrustedDate.dayOfEpoch)
val firstDayOfWeekAsEpochDay = nowTrustedDate.dayOfEpoch - nowTrustedDate.dayOfWeek
return appLogic.database.usedTimes().getUsedTimesOfWeek(category.id, firstDayOfWeekAsEpochDay = firstDayOfWeekAsEpochDay).switchMap { usedTimes ->
val remaining = RemainingTime.getRemainingTime(nowTrustedDate.dayOfWeek, trustedMinuteOfWeek % MinuteOfDay.LENGTH, usedTimes, rules, extraTime, firstDayOfWeekAsEpochDay)
if (remaining == null || remaining.includingExtraTime > 0) {
appLogic.database.sessionDuration().getSessionDurationItemsByCategoryId(category.id).switchMap { durations ->
getTemporarilyTrustedTimeInMillis().map { timeInMillis ->
if (timeInMillis == null) {
BlockingReason.MissingNetworkTime
} else {
val remainingDuration = RemainingSessionDuration.getRemainingSessionDuration(
rules = rules,
dayOfWeek = nowTrustedDate.dayOfWeek,
durationsOfCategory = durations,
minuteOfDay = trustedMinuteOfWeek % MinuteOfDay.LENGTH,
timestamp = timeInMillis
)
if (remainingDuration == null || remainingDuration > 0) {
BlockingReason.None
} else {
BlockingReason.SessionDurationLimit
}
}
}
}
} else {
if (extraTime > 0) {
liveDataFromValue(BlockingReason.TimeOverExtraTimeCanBeUsedLater)
} else {
liveDataFromValue(BlockingReason.TimeOver)
}
}
}
}
fun getTemporarilyTrustedTimeInMillis(): LiveData<Long?> {
val realTime = RealTime.newInstance()
return liveDataFromFunction {
appLogic.realTimeLogic.getRealTime(realTime)
if (realTime.shouldTrustTimeTemporarily) {
realTime.timeInMillis
} else {
null
}
}
}
fun getTrustedMinuteOfWeekLive(timeZone: TimeZone): LiveData<Int?> { fun getTrustedMinuteOfWeekLive(timeZone: TimeZone): LiveData<Int?> {
val realTime = RealTime.newInstance() val realTime = RealTime.newInstance()
@ -468,50 +87,4 @@ class BlockingReasonUtil(private val appLogic: AppLogic) {
} }
}.ignoreUnchanged() }.ignoreUnchanged()
} }
fun getTrustedDateLive(timeZone: TimeZone): LiveData<DateInTimezone?> {
val realTime = RealTime.newInstance()
return object: LiveData<DateInTimezone?>() {
fun update() {
appLogic.realTimeLogic.getRealTime(realTime)
if (realTime.shouldTrustTimeTemporarily) {
value = DateInTimezone.newInstance(realTime.timeInMillis, timeZone)
} else {
value = null
}
}
init {
update()
}
val scheduledUpdateRunnable = Runnable {
update()
scheduleUpdate()
}
fun scheduleUpdate() {
appLogic.timeApi.runDelayed(scheduledUpdateRunnable, 1000L /* every second */)
}
fun cancelScheduledUpdate() {
appLogic.timeApi.cancelScheduledAction(scheduledUpdateRunnable)
}
override fun onActive() {
super.onActive()
update()
scheduleUpdate()
}
override fun onInactive() {
super.onInactive()
cancelScheduledUpdate()
}
}.ignoreUnchanged()
}
} }

View file

@ -1,281 +0,0 @@
/*
* 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.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import io.timelimit.android.BuildConfig
import io.timelimit.android.data.model.*
import io.timelimit.android.date.DateInTimezone
import io.timelimit.android.extensions.MinuteOfDay
import io.timelimit.android.livedata.*
import io.timelimit.android.logic.extension.isCategoryAllowed
import java.util.*
class CategoriesBlockingReasonUtil(private val appLogic: AppLogic) {
companion object {
private const val LOG_TAG = "CategoryBlockingReason"
}
private val blockingReason = BlockingReasonUtil(appLogic)
private val temporarilyTrustedTimeInMillis = blockingReason.getTemporarilyTrustedTimeInMillis()
private val batteryLevel = appLogic.platformIntegration.getBatteryStatusLive()
// NOTE: this ignores the current device rule
fun getCategoryBlockingReasons(
childDisableLimitsUntil: LiveData<Long>,
timeZone: LiveData<TimeZone>,
categories: List<Category>
): LiveData<Map<String, BlockingReason>> {
val result = MediatorLiveData<Map<String, BlockingReason>>()
val status = mutableMapOf<String, BlockingReason>()
val reasons = getCategoryBlockingReasonsInternal(childDisableLimitsUntil, timeZone, categories)
var missing = reasons.size
reasons.entries.forEach { (k, v) ->
var ready = false
result.addSource(v) { newStatus ->
status[k] = newStatus
if (!ready) {
ready = true
missing--
}
if (missing == 0) {
result.value = status.toMap()
}
}
}
return result
}
// NOTE: this ignores the current device rule
private fun getCategoryBlockingReasonsInternal(
childDisableLimitsUntil: LiveData<Long>,
timeZone: LiveData<TimeZone>,
categories: List<Category>
): Map<String, LiveData<BlockingReason>> {
val result = mutableMapOf<String, LiveData<BlockingReason>>()
val categoryById = categories.associateBy { it.id }
val areLimitsTemporarilyDisabled = areLimitsDisabled(
temporarilyTrustedTimeInMillis = temporarilyTrustedTimeInMillis,
childDisableLimitsUntil = childDisableLimitsUntil
)
val temporarilyTrustedMinuteOfWeek = timeZone.switchMap { timeZone ->
blockingReason.getTrustedMinuteOfWeekLive(timeZone)
}
val temporarilyTrustedDate = timeZone.switchMap { timeZone ->
blockingReason.getTrustedDateLive(timeZone)
}
fun handleCategory(categoryId: String, depth: Int) {
if (depth > 2) {
return
}
categoryById[categoryId]?.let { category ->
result[categoryId] = result[categoryId] ?: kotlin.run {
handleCategory(category.parentCategoryId, depth + 1)
val parentCategoryBlockingReason = result[category.parentCategoryId]
val selfReason = getCategoryBlockingReason(
category = liveDataFromValue(category),
temporarilyTrustedMinuteOfWeek = temporarilyTrustedMinuteOfWeek,
temporarilyTrustedDate = temporarilyTrustedDate,
areLimitsTemporarilyDisabled = areLimitsTemporarilyDisabled
)
selfReason.switchMap { self ->
if (self == BlockingReason.None && parentCategoryBlockingReason != null) {
parentCategoryBlockingReason
} else {
liveDataFromValue(self)
}
}
}
}
}
categoryById.keys.forEach { handleCategory(it, 0) }
return result
}
// NOTE: this ignores parent categories (otherwise would check parent category if category has no blocking reason)
// NOTE: this ignores the current device rule
private fun getCategoryBlockingReason(
category: LiveData<Category>,
temporarilyTrustedMinuteOfWeek: LiveData<Int?>,
temporarilyTrustedDate: LiveData<DateInTimezone?>,
areLimitsTemporarilyDisabled: LiveData<Boolean>
): LiveData<BlockingReason> {
return category.switchMap { category ->
val batteryOk = batteryLevel.map { it.isCategoryAllowed(category) }.ignoreUnchanged()
val elseCase = areLimitsTemporarilyDisabled.switchMap { areLimitsTemporarilyDisabled ->
if (areLimitsTemporarilyDisabled) {
liveDataFromValue(BlockingReason.None)
} else {
checkCategoryBlockedTimeAreas(
temporarilyTrustedMinuteOfWeek = temporarilyTrustedMinuteOfWeek,
blockedMinutesInWeek = category.blockedMinutesInWeek.dataNotToModify
).switchMap { blockedTimeAreasReason ->
if (blockedTimeAreasReason != BlockingReason.None) {
liveDataFromValue(blockedTimeAreasReason)
} else {
checkCategoryTimeLimitRules(
temporarilyTrustedDate = temporarilyTrustedDate,
category = category,
rules = appLogic.database.timeLimitRules().getTimeLimitRulesByCategory(category.id),
temporarilyTrustedMinuteOfWeek = temporarilyTrustedMinuteOfWeek
)
}
}
}
}
batteryOk.switchMap { ok ->
if (!ok) {
liveDataFromValue(BlockingReason.BatteryLimit)
} else if (category.temporarilyBlocked) {
if (category.temporarilyBlockedEndTime == 0L) {
liveDataFromValue(BlockingReason.TemporarilyBlocked)
} else {
temporarilyTrustedTimeInMillis.switchMap { timeInMillis ->
if (timeInMillis == null) {
liveDataFromValue(BlockingReason.MissingNetworkTime)
} else if (timeInMillis < category.temporarilyBlockedEndTime) {
liveDataFromValue(BlockingReason.TemporarilyBlocked)
} else {
elseCase
}
}
}
} else {
elseCase
}
}
}
}
private fun areLimitsDisabled(
temporarilyTrustedTimeInMillis: LiveData<Long?>,
childDisableLimitsUntil: LiveData<Long>
): LiveData<Boolean> = childDisableLimitsUntil.switchMap { childDisableLimitsUntil ->
if (childDisableLimitsUntil == 0L) {
liveDataFromValue(false)
} else {
temporarilyTrustedTimeInMillis.map {
trustedTimeInMillis ->
trustedTimeInMillis != null && childDisableLimitsUntil > trustedTimeInMillis
}.ignoreUnchanged()
}
}
private fun checkCategoryBlockedTimeAreas(blockedMinutesInWeek: BitSet, temporarilyTrustedMinuteOfWeek: LiveData<Int?>): LiveData<BlockingReason> {
if (blockedMinutesInWeek.isEmpty) {
return liveDataFromValue(BlockingReason.None)
} else {
return temporarilyTrustedMinuteOfWeek.map { temporarilyTrustedMinuteOfWeek ->
if (temporarilyTrustedMinuteOfWeek == null) {
BlockingReason.MissingNetworkTime
} else if (blockedMinutesInWeek[temporarilyTrustedMinuteOfWeek]) {
BlockingReason.BlockedAtThisTime
} else {
BlockingReason.None
}
}.ignoreUnchanged()
}
}
private fun checkCategoryTimeLimitRules(
temporarilyTrustedDate: LiveData<DateInTimezone?>,
temporarilyTrustedMinuteOfWeek: LiveData<Int?>,
rules: LiveData<List<TimeLimitRule>>,
category: Category
): LiveData<BlockingReason> = rules.switchMap { rules ->
if (rules.isEmpty()) {
liveDataFromValue(BlockingReason.None)
} else {
temporarilyTrustedDate.switchMap { temporarilyTrustedDate ->
temporarilyTrustedMinuteOfWeek.switchMap { temporarilyTrustedMinuteOfWeek ->
if (temporarilyTrustedDate == null || temporarilyTrustedMinuteOfWeek == null) {
liveDataFromValue(BlockingReason.MissingNetworkTime)
} else {
getBlockingReasonStep7(
category = category,
nowTrustedDate = temporarilyTrustedDate,
rules = rules,
trustedMinuteOfWeek = temporarilyTrustedMinuteOfWeek
)
}
}
}
}
}
private fun getBlockingReasonStep7(category: Category, nowTrustedDate: DateInTimezone, trustedMinuteOfWeek: Int, rules: List<TimeLimitRule>): LiveData<BlockingReason> {
if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "step 7")
}
val extraTime = category.getExtraTime(dayOfEpoch = nowTrustedDate.dayOfEpoch)
val firstDayOfWeekAsEpochDay = nowTrustedDate.dayOfEpoch - nowTrustedDate.dayOfWeek
return appLogic.database.usedTimes().getUsedTimesOfWeek(category.id, firstDayOfWeekAsEpochDay).switchMap { usedTimes ->
val remaining = RemainingTime.getRemainingTime(nowTrustedDate.dayOfWeek, trustedMinuteOfWeek % MinuteOfDay.LENGTH, usedTimes, rules, extraTime, firstDayOfWeekAsEpochDay)
if (remaining == null || remaining.includingExtraTime > 0) {
appLogic.database.sessionDuration().getSessionDurationItemsByCategoryId(category.id).switchMap { durations ->
blockingReason.getTemporarilyTrustedTimeInMillis().map { timeInMillis ->
if (timeInMillis == null) {
BlockingReason.MissingNetworkTime
} else {
val remainingDuration = RemainingSessionDuration.getRemainingSessionDuration(
rules = rules,
dayOfWeek = nowTrustedDate.dayOfWeek,
durationsOfCategory = durations,
minuteOfDay = trustedMinuteOfWeek % MinuteOfDay.LENGTH,
timestamp = timeInMillis
)
if (remainingDuration == null || remainingDuration > 0) {
BlockingReason.None
} else {
BlockingReason.SessionDurationLimit
}
}
}
}
} else {
if (extraTime > 0) {
liveDataFromValue(BlockingReason.TimeOverExtraTimeCanBeUsedLater)
} else {
liveDataFromValue(BlockingReason.TimeOver)
}
}
}.ignoreUnchanged()
}
}

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
@ -16,14 +16,26 @@
package io.timelimit.android.logic package io.timelimit.android.logic
import io.timelimit.android.data.model.Device import io.timelimit.android.data.model.Device
import io.timelimit.android.data.model.derived.DeviceRelatedData
import io.timelimit.android.data.model.derived.UserRelatedData
import io.timelimit.android.livedata.* import io.timelimit.android.livedata.*
class CurrentDeviceLogic(private val appLogic: AppLogic) { class CurrentDeviceLogic(private val appLogic: AppLogic) {
private val disabledPrimaryDeviceCheck = appLogic.deviceUserEntry.switchMap { userEntry -> companion object {
if (userEntry?.relaxPrimaryDevice == true) { fun handleDeviceAsCurrentDevice(device: DeviceRelatedData, user: UserRelatedData): Boolean {
appLogic.fullVersion.shouldProvideFullVersionFunctions if (device.isLocalMode) {
} else { return true
liveDataFromValue(false) }
if (user.user.currentDevice == device.deviceEntry.id) {
return true
}
if (user.user.relaxPrimaryDevice && device.isConnectedAndHasPremium) {
return true
}
return false
} }
} }
@ -41,19 +53,6 @@ class CurrentDeviceLogic(private val appLogic: AppLogic) {
} }
} }
private val isThisDeviceMarkedAsCurrentDevice = appLogic.deviceEntry
.map { it?.id }
.switchMap { ownDeviceId ->
appLogic.deviceUserEntry.map { userEntry ->
userEntry?.currentDevice == ownDeviceId
}
}
val isThisDeviceTheCurrentDevice = appLogic.fullVersion.isLocalMode
.or(isThisDeviceMarkedAsCurrentDevice)
.or(disabledPrimaryDeviceCheck)
.ignoreUnchanged()
val otherAssignedDevice = appLogic.deviceUserEntry.switchMap { userEntry -> val otherAssignedDevice = appLogic.deviceUserEntry.switchMap { userEntry ->
if (userEntry?.currentDevice == null) { if (userEntry?.currentDevice == null) {
liveDataFromValue(null as Device?) liveDataFromValue(null as Device?)

View file

@ -21,6 +21,7 @@ import io.timelimit.android.async.Threads
import io.timelimit.android.coroutines.executeAndWait import io.timelimit.android.coroutines.executeAndWait
import io.timelimit.android.coroutines.runAsync import io.timelimit.android.coroutines.runAsync
import io.timelimit.android.data.model.User import io.timelimit.android.data.model.User
import io.timelimit.android.data.model.derived.DeviceRelatedData
import io.timelimit.android.livedata.* import io.timelimit.android.livedata.*
import io.timelimit.android.sync.actions.SignOutAtDeviceAction import io.timelimit.android.sync.actions.SignOutAtDeviceAction
import io.timelimit.android.sync.actions.apply.ApplyActionUtil import io.timelimit.android.sync.actions.apply.ApplyActionUtil
@ -30,6 +31,8 @@ import kotlinx.coroutines.sync.withLock
class DefaultUserLogic(private val appLogic: AppLogic) { class DefaultUserLogic(private val appLogic: AppLogic) {
companion object { companion object {
private const val LOG_TAG = "DefaultUserLogic" private const val LOG_TAG = "DefaultUserLogic"
fun hasAutomaticSignOut(device: DeviceRelatedData): Boolean = device.hasValidDefaultUser && device.deviceEntry.defaultUserTimeout > 0
} }
private fun defaultUserEntry() = appLogic.deviceEntry.map { device -> private fun defaultUserEntry() = appLogic.deviceEntry.map { device ->
@ -40,10 +43,7 @@ class DefaultUserLogic(private val appLogic: AppLogic) {
else else
liveDataFromValue(null as User?) liveDataFromValue(null as User?)
} }
private fun hasDefaultUser() = defaultUserEntry().map { it != null }.ignoreUnchanged()
private fun defaultUserTimeout() = appLogic.deviceEntry.map { it?.defaultUserTimeout ?: 0 }.ignoreUnchanged() private fun defaultUserTimeout() = appLogic.deviceEntry.map { it?.defaultUserTimeout ?: 0 }.ignoreUnchanged()
private fun hasDefaultUserTimeout() = defaultUserTimeout().map { it != 0 }.ignoreUnchanged()
fun hasAutomaticSignOut() = hasDefaultUser().and(hasDefaultUserTimeout())
private val logoutLock = Mutex() private val logoutLock = Mutex()

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
@ -17,6 +17,7 @@ package io.timelimit.android.logic
import android.util.Log import android.util.Log
import io.timelimit.android.BuildConfig import io.timelimit.android.BuildConfig
import io.timelimit.android.async.Threads
import io.timelimit.android.coroutines.runAsync import io.timelimit.android.coroutines.runAsync
import io.timelimit.android.data.model.NetworkTime import io.timelimit.android.data.model.NetworkTime
import io.timelimit.android.livedata.ignoreUnchanged import io.timelimit.android.livedata.ignoreUnchanged
@ -27,6 +28,7 @@ import java.io.IOException
class RealTimeLogic(private val appLogic: AppLogic) { class RealTimeLogic(private val appLogic: AppLogic) {
companion object { companion object {
private const val LOG_TAG = "RealTimeLogic" private const val LOG_TAG = "RealTimeLogic"
private const val MISSING_NETWORK_TIME_GRACE_PERIOD = 5 * 1000L
} }
private val deviceEntry = appLogic.deviceEntryIfEnabled private val deviceEntry = appLogic.deviceEntryIfEnabled
@ -48,6 +50,10 @@ class RealTimeLogic(private val appLogic: AppLogic) {
requireRemoteTimeUptime = appLogic.timeApi.getCurrentUptimeInMillis() requireRemoteTimeUptime = appLogic.timeApi.getCurrentUptimeInMillis()
tryQueryTime() tryQueryTime()
Threads.mainThreadHandler.postDelayed({
callTimeModificationListeners()
}, MISSING_NETWORK_TIME_GRACE_PERIOD)
} else { } else {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "shouldQueryTime = false") Log.d(LOG_TAG, "shouldQueryTime = false")
@ -56,6 +62,10 @@ class RealTimeLogic(private val appLogic: AppLogic) {
appLogic.timeApi.cancelScheduledAction(tryQueryTime) appLogic.timeApi.cancelScheduledAction(tryQueryTime)
} }
} }
appLogic.platformIntegration.systemClockChangeListener = Runnable {
callTimeModificationListeners()
}
} }
private var lastSuccessfullyTimeRequestUptime: Long? = null private var lastSuccessfullyTimeRequestUptime: Long? = null
@ -65,6 +75,19 @@ class RealTimeLogic(private val appLogic: AppLogic) {
private val queryTimeLock = Mutex() private val queryTimeLock = Mutex()
private val tryQueryTime = Runnable { tryQueryTime() } private val tryQueryTime = Runnable { tryQueryTime() }
private val timeModificationListeners = mutableSetOf<() -> Unit>()
fun registerTimeModificationListener(listener: () -> Unit) = synchronized(timeModificationListeners) {
timeModificationListeners.add(listener)
}
fun unregisterTimeModificationListener(listener: () -> Unit) = synchronized(timeModificationListeners) {
timeModificationListeners.remove(listener)
}
fun callTimeModificationListeners() = synchronized(timeModificationListeners) {
timeModificationListeners.forEach { it() }
}
fun tryQueryTime() { fun tryQueryTime() {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
@ -103,6 +126,8 @@ class RealTimeLogic(private val appLogic: AppLogic) {
// schedule refresh in 2 hours // schedule refresh in 2 hours
appLogic.timeApi.runDelayed(tryQueryTime, 1000 * 60 * 60 * 2) appLogic.timeApi.runDelayed(tryQueryTime, 1000 * 60 * 60 * 2)
callTimeModificationListeners()
} catch (ex: Exception) { } catch (ex: Exception) {
if (uptimeRealTimeOffset == null) { if (uptimeRealTimeOffset == null) {
// schedule next attempt in 10 seconds // schedule next attempt in 10 seconds
@ -127,6 +152,8 @@ class RealTimeLogic(private val appLogic: AppLogic) {
val systemTime = appLogic.timeApi.getCurrentTimeInMillis() val systemTime = appLogic.timeApi.getCurrentTimeInMillis()
confirmedUptimeSystemTimeOffset = systemTime - uptime confirmedUptimeSystemTimeOffset = systemTime - uptime
callTimeModificationListeners()
} }
fun getRealTime(time: RealTime) { fun getRealTime(time: RealTime) {
@ -174,7 +201,7 @@ class RealTimeLogic(private val appLogic: AppLogic) {
} else { } else {
time.timeInMillis = systemTime time.timeInMillis = systemTime
// 5 seconds grace period // 5 seconds grace period
time.shouldTrustTimeTemporarily = requireRemoteTimeUptime + 5000 > systemUptime time.shouldTrustTimeTemporarily = requireRemoteTimeUptime + MISSING_NETWORK_TIME_GRACE_PERIOD > systemUptime
time.shouldTrustTimePermanently = false time.shouldTrustTimePermanently = false
time.isNetworkTime = false time.isNetworkTime = false
} }

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
@ -15,104 +15,154 @@
*/ */
package io.timelimit.android.logic package io.timelimit.android.logic
import androidx.lifecycle.LiveData import io.timelimit.android.async.Threads
import io.timelimit.android.data.model.Category import io.timelimit.android.data.invalidation.Observer
import io.timelimit.android.data.invalidation.Table
import io.timelimit.android.data.model.CategoryApp import io.timelimit.android.data.model.CategoryApp
import io.timelimit.android.data.model.ExperimentalFlags import io.timelimit.android.data.model.ExperimentalFlags
import io.timelimit.android.data.model.UserType import io.timelimit.android.data.model.UserType
import io.timelimit.android.data.model.derived.UserRelatedData
import io.timelimit.android.integration.platform.ProtectionLevel
import io.timelimit.android.integration.platform.android.AndroidIntegrationApps import io.timelimit.android.integration.platform.android.AndroidIntegrationApps
import io.timelimit.android.livedata.ignoreUnchanged import io.timelimit.android.logic.blockingreason.CategoryHandlingCache
import io.timelimit.android.livedata.liveDataFromValue import java.lang.ref.WeakReference
import io.timelimit.android.livedata.map import java.util.concurrent.CountDownLatch
import io.timelimit.android.livedata.switchMap import java.util.concurrent.Executors
import java.util.* import java.util.concurrent.atomic.AtomicBoolean
class SuspendAppsLogic(private val appLogic: AppLogic) { class SuspendAppsLogic(private val appLogic: AppLogic): Observer {
private val blockingAtActivityLevel = appLogic.deviceEntry.map { it?.enableActivityLevelBlocking ?: false } private var lastDefaultCategory: String? = null
private val blockingReasonUtil = CategoriesBlockingReasonUtil(appLogic) private var lastAllowedCategoryList = emptySet<String>()
private var lastCategoryApps = emptyList<CategoryApp>()
private val installedAppsModified = AtomicBoolean(false)
private val categoryHandlingCache = CategoryHandlingCache()
private val realTime = RealTime.newInstance()
private var batteryStatus = appLogic.platformIntegration.getBatteryStatus()
private val pendingSync = AtomicBoolean(true)
private val executor = Executors.newSingleThreadExecutor()
private val knownInstalledApps = appLogic.deviceId.switchMap { deviceId -> private val backgroundRunnable = Runnable {
if (deviceId.isNullOrEmpty()) { while (pendingSync.getAndSet(false)) {
liveDataFromValue(emptyList()) updateBlockingSync()
} else {
appLogic.database.app().getAppsByDeviceIdAsync(deviceId).map { apps ->
apps.map { it.packageName }
}
}
}.ignoreUnchanged()
private val categoryData = appLogic.deviceUserEntry.switchMap { deviceUser -> Thread.sleep(500)
if (deviceUser?.type == UserType.Child) {
appLogic.database.category().getCategoriesByChildId(deviceUser.id).switchMap { categories ->
appLogic.database.categoryApp().getCategoryApps(categories.map { it.id }).map { categoryApps ->
RealCategoryData(
categoryForUnassignedApps = deviceUser.categoryForNotAssignedApps,
categories = categories,
categoryApps = categoryApps
) as CategoryData
}
}
} else {
liveDataFromValue(NoChildUser as CategoryData)
}
}.ignoreUnchanged()
private val categoryBlockingReasons: LiveData<Map<String, BlockingReason>?> = appLogic.deviceUserEntry.switchMap { deviceUser ->
if (deviceUser?.type == UserType.Child) {
appLogic.database.category().getCategoriesByChildId(deviceUser.id).switchMap { categories ->
blockingReasonUtil.getCategoryBlockingReasons(
childDisableLimitsUntil = liveDataFromValue(deviceUser.disableLimitsUntil),
timeZone = liveDataFromValue(TimeZone.getTimeZone(deviceUser.timeZone)),
categories = categories
) as LiveData<Map<String, BlockingReason>?>
}
} else {
liveDataFromValue(null as Map<String, BlockingReason>?)
}
}.ignoreUnchanged()
private val appsToBlock = categoryBlockingReasons.switchMap { blockingReasons ->
if (blockingReasons == null) {
liveDataFromValue(emptyList<String>())
} else {
categoryData.switchMap { categories ->
when (categories) {
is NoChildUser -> liveDataFromValue(emptyList<String>())
is RealCategoryData -> {
knownInstalledApps.switchMap { installedApps ->
blockingAtActivityLevel.map { blockingAtActivityLevel ->
val prepared = getAppsWithCategories(installedApps, categories, blockingAtActivityLevel)
val result = mutableListOf<String>()
installedApps.forEach { packageName ->
val appCategories = prepared[packageName] ?: emptySet()
if (appCategories.find { categoryId -> (blockingReasons[categoryId] ?: BlockingReason.None) == BlockingReason.None } == null) {
if (!AndroidIntegrationApps.appsToNotSuspend.contains(packageName)) {
result.add(packageName)
}
}
}
result
}
}
}
} as LiveData<List<String>>
}
} }
} }
private val realAppsToBlock = appLogic.database.config().isExperimentalFlagsSetAsync(ExperimentalFlags.SYSTEM_LEVEL_BLOCKING).switchMap { systemLevelBlocking -> private val triggerRunnable = Runnable {
if (systemLevelBlocking) { triggerUpdate()
appsToBlock }
} else {
liveDataFromValue(emptyList())
}
}.ignoreUnchanged()
private fun getAppsWithCategories(packageNames: List<String>, data: RealCategoryData, blockingAtActivityLevel: Boolean): Map<String, Set<String>> { private fun triggerUpdate() {
val categoryForUnassignedApps = if (data.categories.find { it.id == data.categoryForUnassignedApps } != null) data.categoryForUnassignedApps else null pendingSync.set(true); executor.submit(backgroundRunnable)
}
private fun scheduleUpdate(delay: Long) {
appLogic.timeApi.cancelScheduledAction(triggerRunnable)
appLogic.timeApi.runDelayedByUptime(triggerRunnable, delay)
}
init {
appLogic.database.registerWeakObserver(arrayOf(Table.App), WeakReference(this))
appLogic.platformIntegration.getBatteryStatusLive().observeForever { batteryStatus = it; triggerUpdate() }
appLogic.realTimeLogic.registerTimeModificationListener { triggerUpdate() }
appLogic.database.derivedDataDao().getUserAndDeviceRelatedDataLive().observeForever { triggerUpdate() }
}
override fun onInvalidated(tables: Set<Table>) {
installedAppsModified.set(true); triggerUpdate()
}
private fun updateBlockingSync() {
val userAndDeviceRelatedData = appLogic.database.derivedDataDao().getUserAndDeviceRelatedDataSync()
val isRestrictedUser = userAndDeviceRelatedData?.userRelatedData?.user?.type == UserType.Child
val enableBlockingAtSystemLevel = userAndDeviceRelatedData?.deviceRelatedData?.isExperimentalFlagSetSync(ExperimentalFlags.SYSTEM_LEVEL_BLOCKING) ?: false
val hasPermission = appLogic.platformIntegration.getCurrentProtectionLevel() == ProtectionLevel.DeviceOwner
val enableBlocking = isRestrictedUser && enableBlockingAtSystemLevel && hasPermission
if (!enableBlocking) {
appLogic.platformIntegration.stopSuspendingForAllApps()
lastAllowedCategoryList = emptySet()
lastCategoryApps = emptyList()
return
}
val userRelatedData = userAndDeviceRelatedData!!.userRelatedData!!
val latch = CountDownLatch(1)
Threads.mainThreadHandler.post { appLogic.realTimeLogic.getRealTime(realTime); latch.countDown() }
latch.await()
categoryHandlingCache.reportStatus(
user = userRelatedData,
shouldTrustTimeTemporarily = realTime.shouldTrustTimeTemporarily,
timeInMillis = realTime.timeInMillis,
batteryStatus = batteryStatus,
assumeCurrentDevice = CurrentDeviceLogic.handleDeviceAsCurrentDevice(
device = userAndDeviceRelatedData.deviceRelatedData,
user = userRelatedData
)
)
val defaultCategory = userRelatedData.user.categoryForNotAssignedApps
val blockingAtActivityLevel = userAndDeviceRelatedData.deviceRelatedData.deviceEntry.enableActivityLevelBlocking
val categoryApps = userRelatedData.categoryApps
val categoryHandlings = userRelatedData.categoryById.keys.map { categoryHandlingCache.get(it) }
val categoryIdsToAllow = categoryHandlings.filterNot { it.shouldBlockAtSystemLevel }.map { it.createdWithCategoryRelatedData.category.id }.toMutableSet()
var didModify: Boolean
do {
didModify = false
val iterator = categoryIdsToAllow.iterator()
for (categoryId in iterator) {
val parentCategory = userRelatedData.categoryById[userRelatedData.categoryById[categoryId]?.category?.parentCategoryId]
if (parentCategory != null && !categoryIdsToAllow.contains(parentCategory.category.id)) {
iterator.remove(); didModify = true
}
}
} while (didModify)
categoryHandlings.minBy { it.dependsOnMaxTime }?.let {
scheduleUpdate((it.dependsOnMaxTime - realTime.timeInMillis))
}
if (
categoryIdsToAllow != lastAllowedCategoryList || categoryApps != lastCategoryApps ||
installedAppsModified.getAndSet(false) || defaultCategory != lastDefaultCategory
) {
lastAllowedCategoryList = categoryIdsToAllow
lastCategoryApps = categoryApps
lastDefaultCategory = defaultCategory
val installedApps = appLogic.platformIntegration.getLocalAppPackageNames()
val prepared = getAppsWithCategories(installedApps, userRelatedData, blockingAtActivityLevel)
val appsToBlock = mutableListOf<String>()
installedApps.forEach { packageName ->
val appCategories = prepared[packageName] ?: emptySet()
if (appCategories.find { categoryId -> categoryIdsToAllow.contains(categoryId) } == null) {
if (!AndroidIntegrationApps.appsToNotSuspend.contains(packageName)) {
appsToBlock.add(packageName)
}
}
}
applySuspendedApps(appsToBlock)
}
}
private fun getAppsWithCategories(packageNames: List<String>, data: UserRelatedData, blockingAtActivityLevel: Boolean): Map<String, Set<String>> {
val categoryForUnassignedApps = data.categoryById[data.user.categoryForNotAssignedApps]
if (blockingAtActivityLevel) { if (blockingAtActivityLevel) {
val categoriesByPackageName = data.categoryApps.groupBy { it.packageNameWithoutActivityName } val categoriesByPackageName = data.categoryApps.groupBy { it.packageNameWithoutActivityName }
@ -126,7 +176,7 @@ class SuspendAppsLogic(private val appLogic: AppLogic) {
if (!isMainAppIncluded) { if (!isMainAppIncluded) {
if (categoryForUnassignedApps != null) { if (categoryForUnassignedApps != null) {
categories.add(categoryForUnassignedApps) categories.add(categoryForUnassignedApps.category.id)
} }
} }
@ -140,7 +190,7 @@ class SuspendAppsLogic(private val appLogic: AppLogic) {
val result = mutableMapOf<String, Set<String>>() val result = mutableMapOf<String, Set<String>>()
packageNames.forEach { packageName -> packageNames.forEach { packageName ->
val category = categoryByPackageName[packageName]?.categoryId ?: categoryForUnassignedApps val category = categoryByPackageName[packageName]?.categoryId ?: categoryForUnassignedApps?.category?.id
result[packageName] = if (category != null) setOf(category) else emptySet() result[packageName] = if (category != null) setOf(category) else emptySet()
} }
@ -160,16 +210,4 @@ class SuspendAppsLogic(private val appLogic: AppLogic) {
appLogic.platformIntegration.setSuspendedApps(packageNames, true) appLogic.platformIntegration.setSuspendedApps(packageNames, true)
} }
} }
}
init {
realAppsToBlock.observeForever { appsToBlock -> applySuspendedApps(appsToBlock) }
}
}
internal sealed class CategoryData
internal object NoChildUser: CategoryData()
internal class RealCategoryData(
val categoryForUnassignedApps: String,
val categories: List<Category>,
val categoryApps: List<CategoryApp>
): CategoryData()

View file

@ -13,118 +13,134 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package io.timelimit.android.logic package io.timelimit.android.logic
import android.util.Log import android.util.Log
import io.timelimit.android.BuildConfig import io.timelimit.android.BuildConfig
import io.timelimit.android.date.DateInTimezone import io.timelimit.android.logic.blockingreason.CategoryItselfHandling
import io.timelimit.android.sync.actions.AddUsedTimeActionItem import io.timelimit.android.sync.actions.AddUsedTimeActionItem
import io.timelimit.android.sync.actions.AddUsedTimeActionItemAdditionalCountingSlot
import io.timelimit.android.sync.actions.AddUsedTimeActionItemSessionDurationLimitSlot
import io.timelimit.android.sync.actions.AddUsedTimeActionVersion2 import io.timelimit.android.sync.actions.AddUsedTimeActionVersion2
import io.timelimit.android.sync.actions.apply.ApplyActionUtil import io.timelimit.android.sync.actions.apply.ApplyActionUtil
import io.timelimit.android.sync.actions.dispatch.CategoryNotFoundException import io.timelimit.android.sync.actions.dispatch.CategoryNotFoundException
class UsedTimeUpdateHelper (val date: DateInTimezone) { class UsedTimeUpdateHelper (private val appLogic: AppLogic) {
companion object { companion object {
private const val LOG_TAG = "UsedTimeUpdateHelper" private const val LOG_TAG = "NewUsedTimeUpdateHelper"
} }
val timeToAdd = mutableMapOf<String, Int>() private var countedTime = 0
val extraTimeToSubtract = mutableMapOf<String, Int>() private var lastCategoryHandlings = emptyList<CategoryItselfHandling>()
val sessionDurationLimitSlots = mutableMapOf<String, Set<AddUsedTimeActionItemSessionDurationLimitSlot>>() private var categoryIds = emptySet<String>()
var trustedTimestamp: Long = 0 private var trustedTimestamp = 0L
val additionalSlots = mutableMapOf<String, Set<AddUsedTimeActionItemAdditionalCountingSlot>>() private var dayOfEpoch = 0
var shouldDoAutoCommit = false private var maxTimeToAdd = Long.MAX_VALUE
fun add( fun getCountedTime() = countedTime
categoryId: String, time: Int, slots: Set<AddUsedTimeActionItemAdditionalCountingSlot>, fun getCountedCategoryIds() = categoryIds
includingExtraTime: Boolean, sessionDurationLimits: Set<AddUsedTimeActionItemSessionDurationLimitSlot>,
trustedTimestamp: Long // returns true if it made a commit
) { suspend fun report(duration: Int, handlings: List<CategoryItselfHandling>, trustedTimestamp: Long, dayOfEpoch: Int): Boolean {
if (time < 0) { if (handlings.find { !it.shouldCountTime } != null || duration < 0) {
throw IllegalArgumentException() throw IllegalArgumentException()
} }
if (time == 0) { if (duration == 0) {
return return false
} }
timeToAdd[categoryId] = (timeToAdd[categoryId] ?: 0) + time // the time is counted for the previous categories
// otherwise, the time is so much that the previous session of a session duration limit
// will be extend which causes an unintended blocking
countedTime += duration
if (sessionDurationLimits.isNotEmpty()) { val makeCommitByDifferntHandling = if (handlings != lastCategoryHandlings) {
this.sessionDurationLimitSlots[categoryId] = sessionDurationLimits val newIds = handlings.map { it.createdWithCategoryRelatedData.category.id }.toSet()
} val oldIds = categoryIds
if (sessionDurationLimits.isNotEmpty() && trustedTimestamp != 0L) { maxTimeToAdd = handlings.minBy { it.maxTimeToAdd }?.maxTimeToAdd ?: Long.MAX_VALUE
this.trustedTimestamp = trustedTimestamp categoryIds = newIds
}
if (includingExtraTime) { if (lastCategoryHandlings.size != handlings.size) {
extraTimeToSubtract[categoryId] = (extraTimeToSubtract[categoryId] ?: 0) + time true
} } else {
if ((oldIds - newIds).isNotEmpty() || (newIds - oldIds).isNotEmpty()) {
true
} else {
val oldHandlingById = lastCategoryHandlings.associateBy { it.createdWithCategoryRelatedData.category.id }
if (additionalSlots[categoryId] != null && slots != additionalSlots[categoryId]) { handlings.find { newHandling ->
shouldDoAutoCommit = true val oldHandling = oldHandlingById[newHandling.createdWithCategoryRelatedData.category.id]!!
} else if (slots.isNotEmpty()) {
additionalSlots[categoryId] = slots
}
if (timeToAdd[categoryId]!! >= 1000 * 10) { oldHandling.shouldCountExtraTime != newHandling.shouldCountExtraTime ||
shouldDoAutoCommit = true oldHandling.additionalTimeCountingSlots != newHandling.additionalTimeCountingSlots ||
} oldHandling.sessionDurationSlotsToCount != newHandling.sessionDurationSlotsToCount
} } != null
}
fun reportCurrentCategories(categories: Set<String>) {
if (!categories.containsAll(timeToAdd.keys)) {
shouldDoAutoCommit = true
}
if (!categories.containsAll(extraTimeToSubtract.keys)) {
shouldDoAutoCommit = true
}
}
suspend fun forceCommit(appLogic: AppLogic) {
if (timeToAdd.isEmpty() && extraTimeToSubtract.isEmpty()) {
return
}
val categoryIds = timeToAdd.keys + extraTimeToSubtract.keys
try {
ApplyActionUtil.applyAppLogicAction(
action = AddUsedTimeActionVersion2(
dayOfEpoch = date.dayOfEpoch,
items = categoryIds.map { categoryId ->
AddUsedTimeActionItem(
categoryId = categoryId,
timeToAdd = timeToAdd[categoryId] ?: 0,
extraTimeToSubtract = extraTimeToSubtract[categoryId] ?: 0,
additionalCountingSlots = additionalSlots[categoryId] ?: emptySet(),
sessionDurationLimits = sessionDurationLimitSlots[categoryId] ?: emptySet()
)
},
trustedTimestamp = trustedTimestamp
),
appLogic = appLogic,
ignoreIfDeviceIsNotConfigured = true
)
} catch (ex: CategoryNotFoundException) {
if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "could not commit used times", ex)
} }
} else false
val makeCommitByDifferentBaseData = this.dayOfEpoch != dayOfEpoch
val makeCommitByCountedTime = countedTime >= 30 * 1000 || countedTime >= maxTimeToAdd
val makeCommit = makeCommitByDifferntHandling || makeCommitByDifferentBaseData || makeCommitByCountedTime
// this is a very rare case if a category is deleted while it is used; val madeCommit = if (makeCommit) {
// in this case there could be some lost time doCommitPrivate()
// changes for other categories, but it's no big problem } else {
false
} }
timeToAdd.clear() this.lastCategoryHandlings = handlings
extraTimeToSubtract.clear() this.trustedTimestamp = trustedTimestamp
sessionDurationLimitSlots.clear() this.dayOfEpoch = dayOfEpoch
trustedTimestamp = 0
additionalSlots.clear() return madeCommit
shouldDoAutoCommit = false }
suspend fun flush() {
doCommitPrivate()
lastCategoryHandlings = emptyList()
categoryIds = emptySet()
}
private suspend fun doCommitPrivate(): Boolean {
val makeCommit = lastCategoryHandlings.isNotEmpty() && countedTime > 0
if (makeCommit) {
try {
ApplyActionUtil.applyAppLogicAction(
action = AddUsedTimeActionVersion2(
dayOfEpoch = dayOfEpoch,
items = lastCategoryHandlings.map { handling ->
AddUsedTimeActionItem(
categoryId = handling.createdWithCategoryRelatedData.category.id,
timeToAdd = countedTime,
extraTimeToSubtract = if (handling.shouldCountExtraTime) countedTime else 0,
additionalCountingSlots = handling.additionalTimeCountingSlots,
sessionDurationLimits = handling.sessionDurationSlotsToCount
)
},
trustedTimestamp = if (lastCategoryHandlings.find { it.sessionDurationSlotsToCount.isNotEmpty() } != null) trustedTimestamp else 0
),
appLogic = appLogic,
ignoreIfDeviceIsNotConfigured = true
)
} catch (ex: CategoryNotFoundException) {
if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "could not commit used times", ex)
}
// this is a very rare case if a category is deleted while it is used;
// in this case there could be some lost time
// changes for other categories, but it's no big problem
}
}
countedTime = 0
// doing this would cause a commit very soon again
// lastCategoryHandlings = emptyList()
// categoryIds = emptySet()
return makeCommit
} }
} }

View file

@ -0,0 +1,127 @@
/*
* 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.blockingreason
import io.timelimit.android.BuildConfig
import io.timelimit.android.data.model.derived.CategoryRelatedData
import io.timelimit.android.data.model.derived.DeviceRelatedData
import io.timelimit.android.data.model.derived.UserRelatedData
import io.timelimit.android.integration.platform.android.AndroidIntegrationApps
import io.timelimit.android.logic.BlockingLevel
sealed class AppBaseHandling {
object Idle: AppBaseHandling()
object PauseLogic: AppBaseHandling()
object Whitelist: AppBaseHandling()
object TemporarilyAllowed: AppBaseHandling()
object BlockDueToNoCategory: AppBaseHandling()
data class UseCategories(
val categoryIds: Set<String>,
val shouldCount: Boolean,
val level: BlockingLevel
): AppBaseHandling() {
init {
if (categoryIds.isEmpty()) {
throw IllegalStateException()
}
}
}
companion object {
fun calculate(
foregroundAppPackageName: String?,
foregroundAppActivityName: String?,
pauseForegroundAppBackgroundLoop: Boolean,
pauseCounting: Boolean,
userRelatedData: UserRelatedData,
deviceRelatedData: DeviceRelatedData
): AppBaseHandling {
if (pauseForegroundAppBackgroundLoop) {
return PauseLogic
} 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))
) {
return Whitelist
} else if (foregroundAppPackageName != null && deviceRelatedData.temporarilyAllowedApps.contains(foregroundAppPackageName)) {
return TemporarilyAllowed
} else if (foregroundAppPackageName != null) {
val appCategory = run {
val tryActivityLevelBlocking = deviceRelatedData.deviceEntry.enableActivityLevelBlocking && foregroundAppActivityName != null
val appLevelCategory = userRelatedData.findCategoryApp(foregroundAppPackageName)
(if (tryActivityLevelBlocking) {
userRelatedData.findCategoryApp("$foregroundAppPackageName:$foregroundAppActivityName")
} else {
null
}) ?: appLevelCategory
}
val startCategory = userRelatedData.categoryById[appCategory?.categoryId]
?: userRelatedData.categoryById[userRelatedData.user.categoryForNotAssignedApps]
if (startCategory == null) {
return BlockDueToNoCategory
} else {
val categoryIds = mutableSetOf(startCategory.category.id)
run {
// get parent category ids
var currentCategory: CategoryRelatedData? = userRelatedData.categoryById[startCategory.category.parentCategoryId]
while (currentCategory != null && categoryIds.add(currentCategory.category.id)) {
currentCategory = userRelatedData.categoryById[currentCategory.category.parentCategoryId]
}
}
return UseCategories(
categoryIds = categoryIds,
shouldCount = !pauseCounting,
level = when (appCategory?.specifiesActivity) {
null -> BlockingLevel.Activity // occurs when using a default category
true -> BlockingLevel.Activity
false -> BlockingLevel.App
}
)
}
} else {
return Idle
}
}
fun getCategoriesForCounting(a: AppBaseHandling, b: AppBaseHandling): Set<String> {
return if (a is UseCategories && b is UseCategories && a.shouldCount && b.shouldCount) {
a.categoryIds + b.categoryIds
} else if (a is UseCategories && a.shouldCount) {
a.categoryIds
} else if (b is UseCategories && b.shouldCount) {
b.categoryIds
} else {
emptySet()
}
}
}
}

View file

@ -0,0 +1,81 @@
/*
* 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.blockingreason
import io.timelimit.android.data.model.derived.UserRelatedData
import io.timelimit.android.integration.platform.BatteryStatus
class CategoryHandlingCache {
private val cachedItems = mutableMapOf<String, CategoryItselfHandling>()
private lateinit var user: UserRelatedData
private lateinit var batteryStatus: BatteryStatus
private var shouldTrustTimeTemporarily: Boolean = false
private var timeInMillis: Long = 0
private var assumeCurrentDevice: Boolean = false
fun reportStatus(
user: UserRelatedData,
batteryStatus: BatteryStatus,
shouldTrustTimeTemporarily: Boolean,
timeInMillis: Long,
assumeCurrentDevice: Boolean
) {
this.user = user
this.batteryStatus = batteryStatus
this.shouldTrustTimeTemporarily = shouldTrustTimeTemporarily
this.timeInMillis = timeInMillis
this.assumeCurrentDevice = assumeCurrentDevice
val iterator = cachedItems.iterator()
for (item in iterator) {
val category = user.categoryById[item.key]
if (
category == null ||
!item.value.isValid(
categoryRelatedData = category,
user = user,
batteryStatus = batteryStatus,
assumeCurrentDevice = assumeCurrentDevice,
shouldTrustTimeTemporarily = shouldTrustTimeTemporarily,
timeInMillis = timeInMillis
)
) {
iterator.remove()
}
}
}
fun get(categoryId: String): CategoryItselfHandling {
if (!cachedItems.containsKey(categoryId)) {
cachedItems[categoryId] = calculate(categoryId)
}
return cachedItems[categoryId]!!
}
private fun calculate(categoryId: String): CategoryItselfHandling = CategoryItselfHandling.calculate(
categoryRelatedData = user.categoryById[categoryId]!!,
user = user,
batteryStatus = batteryStatus,
assumeCurrentDevice = assumeCurrentDevice,
shouldTrustTimeTemporarily = shouldTrustTimeTemporarily,
timeInMillis = timeInMillis
)
}

View file

@ -0,0 +1,274 @@
/*
* 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.blockingreason
import io.timelimit.android.data.model.derived.CategoryRelatedData
import io.timelimit.android.data.model.derived.UserRelatedData
import io.timelimit.android.date.DateInTimezone
import io.timelimit.android.date.getMinuteOfWeek
import io.timelimit.android.extensions.MinuteOfDay
import io.timelimit.android.integration.platform.BatteryStatus
import io.timelimit.android.logic.BlockingReason
import io.timelimit.android.logic.RemainingSessionDuration
import io.timelimit.android.logic.RemainingTime
import io.timelimit.android.sync.actions.AddUsedTimeActionItemAdditionalCountingSlot
import io.timelimit.android.sync.actions.AddUsedTimeActionItemSessionDurationLimitSlot
import org.threeten.bp.ZoneId
data class CategoryItselfHandling (
val shouldCountTime: Boolean,
val shouldCountExtraTime: Boolean,
val maxTimeToAdd: Long,
val sessionDurationSlotsToCount: Set<AddUsedTimeActionItemSessionDurationLimitSlot>,
val additionalTimeCountingSlots: Set<AddUsedTimeActionItemAdditionalCountingSlot>,
val areLimitsTemporarilyDisabled: Boolean,
val okByBattery: Boolean,
val okByTempBlocking: Boolean,
val okByBlockedTimeAreas: Boolean,
val okByTimeLimitRules: Boolean,
val okBySessionDurationLimits: Boolean,
val okByCurrentDevice: Boolean,
val missingNetworkTime: Boolean,
val blockAllNotifications: Boolean,
val remainingTime: RemainingTime?,
val remainingSessionDuration: Long?,
val dependsOnMinTime: Long,
val dependsOnMaxTime: Long,
val dependsOnBatteryCharging: Boolean,
val dependsOnMinBatteryLevel: Int,
val dependsOnMaxBatteryLevel: Int,
val createdWithCategoryRelatedData: CategoryRelatedData,
val createdWithUserRelatedData: UserRelatedData,
val createdWithBatteryStatus: BatteryStatus,
val createdWithTemporarilyTrustTime: Boolean,
val createdWithAssumeCurrentDevice: Boolean
) {
companion object {
fun calculate(
categoryRelatedData: CategoryRelatedData,
user: UserRelatedData,
batteryStatus: BatteryStatus,
shouldTrustTimeTemporarily: Boolean,
timeInMillis: Long,
assumeCurrentDevice: Boolean
): CategoryItselfHandling {
val dependsOnMinTime = timeInMillis
val dateInTimezone = DateInTimezone.newInstance(timeInMillis, user.timeZone)
val minuteInWeek = getMinuteOfWeek(timeInMillis, user.timeZone)
val dayOfWeek = dateInTimezone.dayOfWeek
val dayOfEpoch = dateInTimezone.dayOfEpoch
val firstDayOfWeekAsEpochDay = dayOfEpoch - dayOfWeek
val localDate = dateInTimezone.localDate
val minRequiredBatteryLevel = if (batteryStatus.charging) categoryRelatedData.category.minBatteryLevelWhileCharging else categoryRelatedData.category.minBatteryLevelMobile
val okByBattery = batteryStatus.level >= minRequiredBatteryLevel
val dependsOnBatteryCharging = categoryRelatedData.category.minBatteryLevelWhileCharging != categoryRelatedData.category.minBatteryLevelMobile
val dependsOnMinBatteryLevel = if (okByBattery) minRequiredBatteryLevel else Int.MIN_VALUE
val dependsOnMaxBatteryLevel = if (okByBattery) Int.MAX_VALUE else minRequiredBatteryLevel - 1
val okByTempBlocking = !categoryRelatedData.category.temporarilyBlocked || (
shouldTrustTimeTemporarily && categoryRelatedData.category.temporarilyBlockedEndTime != 0L && categoryRelatedData.category.temporarilyBlockedEndTime < timeInMillis )
val dependsOnMaxTimeByTempBlocking = if (okByTempBlocking || !shouldTrustTimeTemporarily || categoryRelatedData.category.temporarilyBlockedEndTime == 0L) Long.MAX_VALUE else categoryRelatedData.category.temporarilyBlockedEndTime
val missingNetworkTimeForDisableTempBlocking = categoryRelatedData.category.temporarilyBlocked && categoryRelatedData.category.temporarilyBlockedEndTime != 0L
val areLimitsTemporarilyDisabled = shouldTrustTimeTemporarily && timeInMillis < user.user.disableLimitsUntil
val dependsOnMaxTimeByTemporarilyDisabledLimits = if (areLimitsTemporarilyDisabled) user.user.disableLimitsUntil else Long.MAX_VALUE
// ignore it for this case: val requiresTrustedTimeForTempLimitsDisabled = user.user.disableLimitsUntil != 0L
val missingNetworkTimeForBlockedTimeAreas = !categoryRelatedData.category.blockedMinutesInWeek.dataNotToModify.isEmpty
val okByBlockedTimeAreas = areLimitsTemporarilyDisabled || !categoryRelatedData.category.blockedMinutesInWeek.read(minuteInWeek)
val relatedRules = if (areLimitsTemporarilyDisabled)
emptyList()
else
RemainingTime.getRulesRelatedToDay(
dayOfWeek = dayOfWeek,
minuteOfDay = minuteInWeek % MinuteOfDay.LENGTH,
rules = categoryRelatedData.rules
)
val remainingTime = RemainingTime.getRemainingTime(
usedTimes = categoryRelatedData.usedTimes,
extraTime = categoryRelatedData.category.extraTimeInMillis,
rules = relatedRules,
dayOfWeek = dayOfWeek,
minuteOfDay = minuteInWeek % MinuteOfDay.LENGTH,
firstDayOfWeekAsEpochDay = firstDayOfWeekAsEpochDay
)
val remainingSessionDuration = RemainingSessionDuration.getRemainingSessionDuration(
rules = relatedRules,
minuteOfDay = minuteInWeek % MinuteOfDay.LENGTH,
dayOfWeek = dayOfWeek,
timestamp = timeInMillis,
durationsOfCategory = categoryRelatedData.durations
)
val missingNetworkTimeForRules = categoryRelatedData.rules.isNotEmpty()
val okByTimeLimitRules = relatedRules.isEmpty() || (remainingTime != null && remainingTime.hasRemainingTime)
val dependsOnMaxTimeByMinuteOfDay = (relatedRules.minBy { it.endMinuteOfDay }?.endMinuteOfDay ?: Int.MAX_VALUE).coerceAtMost(
categoryRelatedData.rules
.filter {
// related to today
it.dayMask.toInt() and (1 shl dayOfWeek) != 0 &&
// will be applied later at this day
it.startMinuteOfDay > minuteInWeek % MinuteOfDay.LENGTH
}
.minBy { it.startMinuteOfDay }?.startMinuteOfDay ?: Int.MAX_VALUE
)
val dependsOnMaxTimeByRules = if (dependsOnMaxTimeByMinuteOfDay != Int.MAX_VALUE) {
localDate.atStartOfDay(ZoneId.of(user.user.timeZone)).plusMinutes(dependsOnMaxTimeByMinuteOfDay.toLong()).toEpochSecond() * 1000
} else {
localDate.plusDays(1).atStartOfDay(ZoneId.of(user.user.timeZone)).toEpochSecond() * 1000
}
val dependsOnMaxTimeBySessionDurationLimitItems = (
categoryRelatedData.durations.map { it.lastUsage + it.sessionPauseDuration } +
categoryRelatedData.durations.map { it.lastUsage + it.maxSessionDuration - it.lastSessionDuration }
)
.filter { it > timeInMillis }
.min() ?: Long.MAX_VALUE
val okBySessionDurationLimits = remainingSessionDuration == null || remainingSessionDuration > 0
val okByCurrentDevice = assumeCurrentDevice || (remainingTime == null && remainingSessionDuration == null)
val dependsOnMaxTime = dependsOnMaxTimeByTempBlocking
.coerceAtMost(dependsOnMaxTimeByTemporarilyDisabledLimits)
.coerceAtMost(dependsOnMaxTimeByRules)
.coerceAtMost(dependsOnMaxTimeBySessionDurationLimitItems)
val missingNetworkTime = !shouldTrustTimeTemporarily &&
(missingNetworkTimeForDisableTempBlocking || missingNetworkTimeForBlockedTimeAreas || missingNetworkTimeForRules)
val shouldCountTime = relatedRules.isNotEmpty()
val shouldCountExtraTime = remainingTime?.usingExtraTime == true
val sessionDurationSlotsToCount = if (remainingSessionDuration != null && remainingSessionDuration <= 0)
emptySet()
else
relatedRules.filter { it.sessionDurationLimitEnabled }.map {
AddUsedTimeActionItemSessionDurationLimitSlot(
startMinuteOfDay = it.startMinuteOfDay,
endMinuteOfDay = it.endMinuteOfDay,
maxSessionDuration = it.sessionDurationMilliseconds,
sessionPauseDuration = it.sessionPauseMilliseconds
)
}.toSet()
val maxTimeToAddByRegularTime = if (!shouldCountTime || remainingTime == null)
Long.MAX_VALUE
else if (shouldCountExtraTime)
remainingTime.includingExtraTime
else
remainingTime.default
val maxTimeToAddBySessionDuration = remainingSessionDuration ?: Long.MAX_VALUE
val maxTimeToAdd = maxTimeToAddByRegularTime.coerceAtMost(maxTimeToAddBySessionDuration)
val additionalTimeCountingSlots = if (shouldCountTime)
relatedRules
.filterNot { it.appliesToWholeDay }
.map { AddUsedTimeActionItemAdditionalCountingSlot(it.startMinuteOfDay, it.endMinuteOfDay) }
.toSet()
else
emptySet()
val blockAllNotifications = categoryRelatedData.category.blockAllNotifications
return CategoryItselfHandling(
shouldCountTime = shouldCountTime,
shouldCountExtraTime = shouldCountExtraTime,
maxTimeToAdd = maxTimeToAdd,
sessionDurationSlotsToCount = sessionDurationSlotsToCount,
areLimitsTemporarilyDisabled = areLimitsTemporarilyDisabled,
okByBattery = okByBattery,
okByTempBlocking = okByTempBlocking,
okByBlockedTimeAreas = okByBlockedTimeAreas,
okByTimeLimitRules = okByTimeLimitRules,
okBySessionDurationLimits = okBySessionDurationLimits,
okByCurrentDevice = okByCurrentDevice,
missingNetworkTime = missingNetworkTime,
blockAllNotifications = blockAllNotifications,
remainingTime = remainingTime,
remainingSessionDuration = remainingSessionDuration,
additionalTimeCountingSlots = additionalTimeCountingSlots,
dependsOnMinTime = dependsOnMinTime,
dependsOnMaxTime = dependsOnMaxTime,
dependsOnBatteryCharging = dependsOnBatteryCharging,
dependsOnMinBatteryLevel = dependsOnMinBatteryLevel,
dependsOnMaxBatteryLevel = dependsOnMaxBatteryLevel,
createdWithCategoryRelatedData = categoryRelatedData,
createdWithBatteryStatus = batteryStatus,
createdWithTemporarilyTrustTime = shouldTrustTimeTemporarily,
createdWithAssumeCurrentDevice = assumeCurrentDevice,
createdWithUserRelatedData = user
)
}
}
val okBasic = okByBattery && okByTempBlocking && okByBlockedTimeAreas && okByTimeLimitRules && okBySessionDurationLimits && !missingNetworkTime
val okAll = okBasic && okByCurrentDevice
val shouldBlockActivities = !okAll
val activityBlockingReason: BlockingReason = if (!okByBattery)
BlockingReason.BatteryLimit
else if (!okByTempBlocking)
BlockingReason.TemporarilyBlocked
else if (!okByBlockedTimeAreas)
BlockingReason.BlockedAtThisTime
else if (!okByTimeLimitRules)
if (remainingTime?.hasRemainingTime == true)
BlockingReason.TimeOverExtraTimeCanBeUsedLater
else
BlockingReason.TimeOver
else if (!okBySessionDurationLimits)
BlockingReason.SessionDurationLimit
else if (!okByCurrentDevice)
BlockingReason.RequiresCurrentDevice
else if (missingNetworkTime)
BlockingReason.MissingNetworkTime
else
BlockingReason.None
// blockAllNotifications is only relevant if premium or local mode
// val shouldBlockNotifications = !okAll || blockAllNotifications
val shouldBlockAtSystemLevel = !okBasic
fun isValid(
categoryRelatedData: CategoryRelatedData,
user: UserRelatedData,
batteryStatus: BatteryStatus,
shouldTrustTimeTemporarily: Boolean,
timeInMillis: Long,
assumeCurrentDevice: Boolean
): Boolean {
if (
categoryRelatedData != createdWithCategoryRelatedData || user != createdWithUserRelatedData ||
shouldTrustTimeTemporarily != createdWithTemporarilyTrustTime || assumeCurrentDevice != createdWithAssumeCurrentDevice
) {
return false
}
if (timeInMillis < dependsOnMinTime || timeInMillis > dependsOnMaxTime) {
return false
}
if (batteryStatus.charging != this.createdWithBatteryStatus.charging && this.dependsOnBatteryCharging) {
return false
}
if (batteryStatus.level < dependsOnMinBatteryLevel || batteryStatus.level > dependsOnMaxBatteryLevel) {
return false
}
return true
}
}

View file

@ -25,20 +25,21 @@ import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.Transformations
import com.google.android.material.snackbar.Snackbar
import io.timelimit.android.R import io.timelimit.android.R
import io.timelimit.android.async.Threads import io.timelimit.android.async.Threads
import io.timelimit.android.coroutines.executeAndWait import io.timelimit.android.data.extensions.sortedCategories
import io.timelimit.android.coroutines.runAsync
import io.timelimit.android.data.extensions.mapToTimezone
import io.timelimit.android.data.extensions.sorted
import io.timelimit.android.data.model.* import io.timelimit.android.data.model.*
import io.timelimit.android.data.model.derived.DeviceAndUserRelatedData
import io.timelimit.android.data.model.derived.DeviceRelatedData
import io.timelimit.android.data.model.derived.UserRelatedData
import io.timelimit.android.databinding.LockFragmentBinding import io.timelimit.android.databinding.LockFragmentBinding
import io.timelimit.android.databinding.LockFragmentCategoryButtonBinding import io.timelimit.android.databinding.LockFragmentCategoryButtonBinding
import io.timelimit.android.date.DateInTimezone import io.timelimit.android.date.DateInTimezone
import io.timelimit.android.integration.platform.BatteryStatus
import io.timelimit.android.livedata.* import io.timelimit.android.livedata.*
import io.timelimit.android.logic.* import io.timelimit.android.logic.*
import io.timelimit.android.logic.blockingreason.AppBaseHandling
import io.timelimit.android.logic.blockingreason.CategoryHandlingCache
import io.timelimit.android.sync.actions.AddCategoryAppsAction import io.timelimit.android.sync.actions.AddCategoryAppsAction
import io.timelimit.android.sync.actions.IncrementCategoryExtraTimeAction import io.timelimit.android.sync.actions.IncrementCategoryExtraTimeAction
import io.timelimit.android.sync.actions.UpdateCategoryTemporarilyBlockedAction import io.timelimit.android.sync.actions.UpdateCategoryTemporarilyBlockedAction
@ -55,11 +56,13 @@ import io.timelimit.android.ui.manage.child.category.create.CreateCategoryDialog
import io.timelimit.android.ui.manage.child.primarydevice.UpdatePrimaryDeviceDialogFragment import io.timelimit.android.ui.manage.child.primarydevice.UpdatePrimaryDeviceDialogFragment
import io.timelimit.android.ui.payment.RequiresPurchaseDialogFragment import io.timelimit.android.ui.payment.RequiresPurchaseDialogFragment
import io.timelimit.android.ui.view.SelectTimeSpanViewListener import io.timelimit.android.ui.view.SelectTimeSpanViewListener
import java.util.*
class LockFragment : Fragment() { class LockFragment : Fragment() {
companion object { companion object {
private const val EXTRA_PACKAGE_NAME = "pkg" private const val EXTRA_PACKAGE_NAME = "pkg"
private const val EXTRA_ACTIVITY = "activitiy" private const val EXTRA_ACTIVITY = "activitiy"
private const val STATUS_DID_OPEN_SET_CURRENT_DEVICE_SCREEN = "didOpenSetCurrentDeviceScreen"
fun newInstance(packageName: String, activity: String?): LockFragment { fun newInstance(packageName: String, activity: String?): LockFragment {
val result = LockFragment() val result = LockFragment()
@ -76,6 +79,7 @@ class LockFragment : Fragment() {
} }
} }
private var didOpenSetCurrentDeviceScreen = false
private val packageName: String by lazy { arguments!!.getString(EXTRA_PACKAGE_NAME)!! } private val packageName: String by lazy { arguments!!.getString(EXTRA_PACKAGE_NAME)!! }
private val activityName: String? by lazy { private val activityName: String? by lazy {
if (arguments!!.containsKey(EXTRA_ACTIVITY)) if (arguments!!.containsKey(EXTRA_ACTIVITY))
@ -86,189 +90,273 @@ class LockFragment : Fragment() {
private val auth: ActivityViewModel by lazy { getActivityViewModel(activity!!) } private val auth: ActivityViewModel by lazy { getActivityViewModel(activity!!) }
private val logic: AppLogic by lazy { DefaultAppLogic.with(context!!) } private val logic: AppLogic by lazy { DefaultAppLogic.with(context!!) }
private val title: String? by lazy { logic.platformIntegration.getLocalAppTitle(packageName) } private val title: String? by lazy { logic.platformIntegration.getLocalAppTitle(packageName) }
private val blockingReason: LiveData<BlockingReasonDetail> by lazy { BlockingReasonUtil(logic).getBlockingReason(packageName, activityName) } private val deviceAndUserRelatedData: LiveData<DeviceAndUserRelatedData?> by lazy {
logic.database.derivedDataDao().getUserAndDeviceRelatedDataLive()
}
private val batteryStatus: LiveData<BatteryStatus> by lazy {
logic.platformIntegration.getBatteryStatusLive()
}
private lateinit var binding: LockFragmentBinding
private val handlingCache = CategoryHandlingCache()
private val realTime = RealTime.newInstance()
private val timeModificationListener: () -> Unit = { update() }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { private val updateRunnable = Runnable { update() }
val binding = LockFragmentBinding.inflate(layoutInflater, container, false)
AuthenticationFab.manageAuthenticationFab( fun scheduleUpdate(delay: Long) {
fab = binding.fab, logic.timeApi.cancelScheduledAction(updateRunnable)
shouldHighlight = auth.shouldHighlightAuthenticationButton, logic.timeApi.runDelayedByUptime(updateRunnable, delay)
authenticatedUser = auth.authenticatedUser, }
fragment = this,
doesSupportAuth = liveDataFromValue(true) fun unscheduleUpdate() {
logic.timeApi.cancelScheduledAction(updateRunnable)
}
private fun update() {
val deviceAndUserRelatedData = deviceAndUserRelatedData.value ?: return
val batteryStatus = batteryStatus.value ?: return
logic.realTimeLogic.getRealTime(realTime)
if (deviceAndUserRelatedData.userRelatedData?.user?.type != UserType.Child) {
binding.reason = BlockingReason.None
binding.handlers = null
activity?.finish()
return
}
handlingCache.reportStatus(
user = deviceAndUserRelatedData.userRelatedData,
assumeCurrentDevice = CurrentDeviceLogic.handleDeviceAsCurrentDevice(deviceAndUserRelatedData.deviceRelatedData, deviceAndUserRelatedData.userRelatedData),
batteryStatus = batteryStatus,
timeInMillis = realTime.timeInMillis,
shouldTrustTimeTemporarily = realTime.shouldTrustTimeTemporarily
) )
liveDataFromFunction { logic.timeApi.getCurrentTimeInMillis() }.observe(this, Observer { val appBaseHandling = AppBaseHandling.calculate(
systemTime -> foregroundAppPackageName = packageName,
foregroundAppActivityName = activityName,
deviceRelatedData = deviceAndUserRelatedData.deviceRelatedData,
userRelatedData = deviceAndUserRelatedData.userRelatedData,
pauseForegroundAppBackgroundLoop = false,
pauseCounting = false
)
binding.currentTime = DateUtils.formatDateTime( binding.activityName = if (deviceAndUserRelatedData.deviceRelatedData.deviceEntry.enableActivityLevelBlocking)
context, activityName?.removePrefix(packageName)
systemTime!!, else
DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_TIME or null
DateUtils.FORMAT_SHOW_YEAR or DateUtils.FORMAT_SHOW_WEEKDAY
if (appBaseHandling is AppBaseHandling.UseCategories) {
val categoryHandlings = appBaseHandling.categoryIds.map { handlingCache.get(it) }
val blockingHandling = categoryHandlings.find { it.shouldBlockActivities }
if (blockingHandling == null) {
binding.reason = BlockingReason.None
binding.handlers = null
activity?.finish()
return
}
binding.appCategoryTitle = blockingHandling.createdWithCategoryRelatedData.category.title
binding.reason = blockingHandling.activityBlockingReason
binding.blockedKindLabel = when (appBaseHandling.level) {
BlockingLevel.Activity -> "Activity"
BlockingLevel.App -> "App"
}
setupHandlers(
deviceId = deviceAndUserRelatedData.deviceRelatedData.deviceEntry.id,
blockedCategoryId = blockingHandling.createdWithCategoryRelatedData.category.id,
userRelatedData = deviceAndUserRelatedData.userRelatedData
)
bindExtraTimeView(
deviceRelatedData = deviceAndUserRelatedData.deviceRelatedData,
categoryId = blockingHandling.createdWithCategoryRelatedData.category.id,
timeZone = deviceAndUserRelatedData.userRelatedData.timeZone
)
binding.manageDisableTimeLimits.handlers = ManageDisableTimelimitsViewHelper.createHandlers(
childId = deviceAndUserRelatedData.userRelatedData.user.id,
childTimezone = deviceAndUserRelatedData.userRelatedData.user.timeZone,
activity = activity!!,
hasFullVersion = deviceAndUserRelatedData.deviceRelatedData.isConnectedAndHasPremium || deviceAndUserRelatedData.deviceRelatedData.isLocalMode
) )
})
val enableActivityLevelBlocking = logic.deviceEntry.map { it?.enableActivityLevelBlocking ?: false } if (blockingHandling.activityBlockingReason == BlockingReason.RequiresCurrentDevice && !didOpenSetCurrentDeviceScreen && isResumed) {
setThisDeviceAsCurrentDevice()
}
binding.packageName = packageName scheduleUpdate((blockingHandling.dependsOnMaxTime - realTime.timeInMillis))
} else if (appBaseHandling is AppBaseHandling.BlockDueToNoCategory) {
binding.appCategoryTitle = null
binding.reason = BlockingReason.NotPartOfAnCategory
binding.blockedKindLabel = "App"
setupHandlers(
deviceId = deviceAndUserRelatedData.deviceRelatedData.deviceEntry.id,
blockedCategoryId = null,
userRelatedData = deviceAndUserRelatedData.userRelatedData
)
enableActivityLevelBlocking.observe(this, Observer { bindAddToCategoryOptions(deviceAndUserRelatedData.userRelatedData)
binding.activityName = if (it) activityName?.removePrefix(packageName) else null
})
if (title != null) {
binding.appTitle = title
} else { } else {
binding.appTitle = "???" binding.reason = BlockingReason.None
binding.handlers = null
activity?.finish()
return
} }
}
binding.appIcon.setImageDrawable(logic.platformIntegration.getAppIcon(packageName)) private fun setupHandlers(deviceId: String, userRelatedData: UserRelatedData, blockedCategoryId: String?) {
binding.handlers = object: Handlers {
blockingReason.observe(this, Observer { override fun openMainApp() {
when (it) { startActivity(Intent(context, MainActivity::class.java))
is NoBlockingReason -> activity!!.finish()
is BlockedReasonDetails -> {
binding.reason = it.reason
binding.blockedKindLabel = when (it.level) {
BlockingLevel.Activity -> "Activity"
BlockingLevel.App -> "App"
}
}
}.let { /* require handling all cases */ }
})
val categories = logic.deviceUserEntry.switchMap {
user ->
if (user != null && user.type == UserType.Child) {
Transformations.map(logic.database.category().getCategoriesByChildId(user.id)) {
categories ->
user to categories
}
} else {
liveDataFromValue(null as Pair<User, List<Category>>?)
} }
}.ignoreUnchanged()
// bind category name of the app override fun allowTemporarily() {
val appCategory = categories.switchMap { if (auth.requestAuthenticationOrReturnTrue()) {
status -> val database = logic.database
if (status == null) { // this accesses the database directly because it is not synced
liveDataFromValue(null as Category?) Threads.database.submit {
} else { try {
val (_, categoryItems) = status database.temporarilyAllowedApp().addTemporarilyAllowedAppSync(TemporarilyAllowedApp(
deviceId = deviceId,
blockingReason.map { reason -> packageName = packageName
if (reason is BlockedReasonDetails) { ))
reason.categoryId } catch (ex: SQLiteConstraintException) {
} else { // ignore this
null //
} // this happens when touching that option more than once very fast
}.map { categoryId -> // or if the device is under load
categoryItems.find { it.id == categoryId }
}
}
}
appCategory.observe(this, Observer {
binding.appCategoryTitle = it?.title
})
// bind add to category list
categories.observe(this, Observer {
status ->
binding.addToCategoryOptions.removeAllViews()
if (status == null) {
// nothing to do
} else {
val (user, categoryEntries) = status
categoryEntries.sorted().forEach {
category ->
val button = LockFragmentCategoryButtonBinding.inflate(LayoutInflater.from(context), binding.addToCategoryOptions, true)
button.title = category.title
button.button.setOnClickListener {
_ ->
auth.tryDispatchParentAction(
AddCategoryAppsAction(
categoryId = category.id,
packageNames = listOf(packageName)
)
)
}
}
run {
val button = LockFragmentCategoryButtonBinding.inflate(LayoutInflater.from(context), binding.addToCategoryOptions, true)
button.title = getString(R.string.create_category_title)
button.button.setOnClickListener {
if (auth.requestAuthenticationOrReturnTrue()) {
CreateCategoryDialogFragment
.newInstance(ManageChildFragmentArgs(childId = user.id))
.show(fragmentManager!!)
} }
} }
} }
} }
})
// bind adding extra time controls override fun confirmLocalTime() {
if (auth.requestAuthenticationOrReturnTrue()) {
logic.realTimeLogic.confirmLocalTime()
}
}
override fun disableTimeVerification() {
auth.tryDispatchParentAction(
UpdateNetworkTimeVerificationAction(
deviceId = deviceId,
mode = NetworkTime.IfPossible
)
)
}
override fun disableTemporarilyLockForAllCategories() {
auth.tryDispatchParentActions(
userRelatedData.categories
.filter { it.category.temporarilyBlocked }
.map {
UpdateCategoryTemporarilyBlockedAction(
categoryId = it.category.id,
blocked = false,
endTime = null
)
}
)
}
override fun disableTemporarilyLockForCurrentCategory() {
blockedCategoryId ?: return
auth.tryDispatchParentAction(
UpdateCategoryTemporarilyBlockedAction(
categoryId = blockedCategoryId,
blocked = false,
endTime = null
)
)
}
override fun showAuthenticationScreen() {
(activity as LockActivity).showAuthenticationScreen()
}
override fun setThisDeviceAsCurrentDevice() = this@LockFragment.setThisDeviceAsCurrentDevice()
}
}
private fun setThisDeviceAsCurrentDevice() {
didOpenSetCurrentDeviceScreen = true
UpdatePrimaryDeviceDialogFragment
.newInstance(UpdatePrimaryDeviceRequestType.SetThisDevice)
.show(parentFragmentManager)
}
private fun bindAddToCategoryOptions(userRelatedData: UserRelatedData) {
binding.addToCategoryOptions.removeAllViews()
userRelatedData.categories.sortedCategories().forEach { category ->
LockFragmentCategoryButtonBinding.inflate(LayoutInflater.from(context), binding.addToCategoryOptions, true).let { button ->
button.title = category.category.title
button.button.setOnClickListener { _ ->
auth.tryDispatchParentAction(
AddCategoryAppsAction(
categoryId = category.category.id,
packageNames = listOf(packageName)
)
)
}
}
}
LockFragmentCategoryButtonBinding.inflate(LayoutInflater.from(context), binding.addToCategoryOptions, true).let { button ->
button.title = getString(R.string.create_category_title)
button.button.setOnClickListener {
if (auth.requestAuthenticationOrReturnTrue()) {
CreateCategoryDialogFragment
.newInstance(ManageChildFragmentArgs(childId = userRelatedData.user.id))
.show(fragmentManager!!)
}
}
}
}
private fun bindExtraTimeView(deviceRelatedData: DeviceRelatedData, categoryId: String, timeZone: TimeZone) {
val hasFullVersion = deviceRelatedData.isConnectedAndHasPremium || deviceRelatedData.isLocalMode
binding.extraTimeBtnOk.setOnClickListener {
binding.extraTimeSelection.clearNumberPickerFocus()
if (hasFullVersion) {
if (auth.requestAuthenticationOrReturnTrue()) {
val extraTimeToAdd = binding.extraTimeSelection.timeInMillis
if (extraTimeToAdd > 0) {
binding.extraTimeBtnOk.isEnabled = false
val date = DateInTimezone.newInstance(auth.logic.timeApi.getCurrentTimeInMillis(), timeZone)
auth.tryDispatchParentAction(IncrementCategoryExtraTimeAction(
categoryId = categoryId,
addedExtraTime = extraTimeToAdd,
extraTimeDay = if (binding.switchLimitExtraTimeToToday.isChecked) date.dayOfEpoch else -1
))
binding.extraTimeBtnOk.isEnabled = true
}
}
} else {
RequiresPurchaseDialogFragment().show(parentFragmentManager)
}
}
}
private fun initExtraTimeView() {
binding.extraTimeTitle.setOnClickListener { binding.extraTimeTitle.setOnClickListener {
HelpDialogFragment.newInstance( HelpDialogFragment.newInstance(
title = R.string.lock_extratime_title, title = R.string.lock_extratime_title,
text = R.string.lock_extratime_text text = R.string.lock_extratime_text
).show(fragmentManager!!) ).show(parentFragmentManager)
} }
logic.fullVersion.shouldProvideFullVersionFunctions.observe(this, Observer { hasFullVersion -> logic.database.config().getEnableAlternativeDurationSelectionAsync().observe(viewLifecycleOwner, Observer {
binding.extraTimeBtnOk.setOnClickListener {
binding.extraTimeSelection.clearNumberPickerFocus()
if (hasFullVersion) {
if (auth.isParentAuthenticated()) {
runAsync {
val extraTimeToAdd = binding.extraTimeSelection.timeInMillis
if (extraTimeToAdd > 0) {
binding.extraTimeBtnOk.isEnabled = false
val categoryId = appCategory.waitForNullableValue()?.id
val timezone = logic.deviceUserEntry.mapToTimezone().waitForNonNullValue()
val date = DateInTimezone.newInstance(auth.logic.timeApi.getCurrentTimeInMillis(), timezone)
if (categoryId != null) {
auth.tryDispatchParentAction(IncrementCategoryExtraTimeAction(
categoryId = categoryId,
addedExtraTime = extraTimeToAdd,
extraTimeDay = if (binding.switchLimitExtraTimeToToday.isChecked) date.dayOfEpoch else -1
))
} else {
Snackbar.make(binding.root, R.string.error_general, Snackbar.LENGTH_SHORT).show()
}
binding.extraTimeBtnOk.isEnabled = true
}
}
} else {
auth.requestAuthentication()
}
} else {
RequiresPurchaseDialogFragment().show(fragmentManager!!)
}
}
})
logic.database.config().getEnableAlternativeDurationSelectionAsync().observe(this, Observer {
binding.extraTimeSelection.enablePickerMode(it) binding.extraTimeSelection.enablePickerMode(it)
}) })
@ -283,150 +371,76 @@ class LockFragment : Fragment() {
} }
} }
} }
}
// bind disable time limits override fun onCreate(savedInstanceState: Bundle?) {
mergeLiveData(logic.deviceUserEntry, logic.fullVersion.shouldProvideFullVersionFunctions).observe(this, Observer { super.onCreate(savedInstanceState)
(child, hasFullVersion) ->
if (child != null) { if (savedInstanceState != null) {
binding.manageDisableTimeLimits.handlers = ManageDisableTimelimitsViewHelper.createHandlers( didOpenSetCurrentDeviceScreen = savedInstanceState.getBoolean(STATUS_DID_OPEN_SET_CURRENT_DEVICE_SCREEN)
childId = child.id, }
childTimezone = child.timeZone, }
activity = activity!!,
hasFullVersion = hasFullVersion == true override fun onSaveInstanceState(outState: Bundle) {
) super.onSaveInstanceState(outState)
}
outState.putBoolean(STATUS_DID_OPEN_SET_CURRENT_DEVICE_SCREEN, didOpenSetCurrentDeviceScreen)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = LockFragmentBinding.inflate(layoutInflater, container, false)
AuthenticationFab.manageAuthenticationFab(
fab = binding.fab,
shouldHighlight = auth.shouldHighlightAuthenticationButton,
authenticatedUser = auth.authenticatedUser,
fragment = this,
doesSupportAuth = liveDataFromValue(true)
)
liveDataFromFunction { logic.timeApi.getCurrentTimeInMillis() }.observe(viewLifecycleOwner, Observer {
systemTime ->
binding.currentTime = DateUtils.formatDateTime(
context,
systemTime!!,
DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_TIME or
DateUtils.FORMAT_SHOW_YEAR or DateUtils.FORMAT_SHOW_WEEKDAY
)
}) })
mergeLiveData(logic.deviceUserEntry, liveDataFromFunction { logic.realTimeLogic.getCurrentTimeInMillis() }).map { deviceAndUserRelatedData.observe(viewLifecycleOwner, Observer { update() })
(child, time) -> batteryStatus.observe(viewLifecycleOwner, Observer { update() })
if (time == null || child == null) { binding.packageName = packageName
null
} else {
ManageDisableTimelimitsViewHelper.getDisabledUntilString(child, time, context!!)
}
}.observe(this, Observer {
binding.manageDisableTimeLimits.disableTimeLimitsUntilString = it
})
binding.handlers = object: Handlers { binding.appTitle = title ?: "???"
override fun openMainApp() { binding.appIcon.setImageDrawable(logic.platformIntegration.getAppIcon(packageName))
startActivity(Intent(context, MainActivity::class.java))
}
override fun allowTemporarily() { initExtraTimeView()
if (auth.requestAuthenticationOrReturnTrue()) {
val database = logic.database
val deviceIdLive = logic.deviceId
// this accesses the database directly because it is not synced
runAsync {
val deviceId = deviceIdLive.waitForNullableValue()
if (deviceId != null) {
Threads.database.executeAndWait(Runnable {
try {
database.temporarilyAllowedApp().addTemporarilyAllowedAppSync(TemporarilyAllowedApp(
deviceId = deviceId,
packageName = packageName
))
} catch (ex: SQLiteConstraintException) {
// ignore this
//
// this happens when touching that option more than once very fast
// or if the device is under load
}
})
}
}
}
}
override fun confirmLocalTime() {
if (auth.requestAuthenticationOrReturnTrue()) {
logic.realTimeLogic.confirmLocalTime()
}
}
override fun disableTimeVerification() {
if (auth.requestAuthenticationOrReturnTrue()) {
runAsync {
val deviceId = logic.deviceId.waitForNullableValue()
if (deviceId != null) {
auth.tryDispatchParentAction(
UpdateNetworkTimeVerificationAction(
deviceId = deviceId,
mode = NetworkTime.IfPossible
)
)
}
}
}
}
override fun disableTemporarilyLockForAllCategories() {
if (auth.requestAuthenticationOrReturnTrue()) {
runAsync {
categories.waitForNullableValue()?.second?.filter { it.temporarilyBlocked }?.map { it.id }?.forEach {
categoryId ->
auth.tryDispatchParentAction(
UpdateCategoryTemporarilyBlockedAction(
categoryId = categoryId,
blocked = false,
endTime = null
)
)
}
}
}
}
override fun disableTemporarilyLockForCurrentCategory() {
if (auth.requestAuthenticationOrReturnTrue()) {
runAsync {
val category = appCategory.waitForNullableValue()
if (category != null) {
auth.tryDispatchParentAction(
UpdateCategoryTemporarilyBlockedAction(
categoryId = category.id,
blocked = false,
endTime = null
)
)
}
}
}
}
override fun showAuthenticationScreen() {
(activity as LockActivity).showAuthenticationScreen()
}
override fun setThisDeviceAsCurrentDevice() {
UpdatePrimaryDeviceDialogFragment
.newInstance(UpdatePrimaryDeviceRequestType.SetThisDevice)
.show(fragmentManager!!)
}
}
if (savedInstanceState == null) {
runAsync {
val reason = blockingReason.waitForNonNullValue()
if (reason is BlockedReasonDetails && reason.reason == BlockingReason.RequiresCurrentDevice) {
if (isResumed) {
binding.handlers!!.setThisDeviceAsCurrentDevice()
}
}
}
}
return binding.root return binding.root
} }
override fun onResume() {
super.onResume()
logic.realTimeLogic.registerTimeModificationListener(timeModificationListener)
update()
}
override fun onPause() {
super.onPause()
logic.realTimeLogic.unregisterTimeModificationListener(timeModificationListener)
}
override fun onDestroy() {
super.onDestroy()
unscheduleUpdate()
}
} }
interface Handlers { interface Handlers {