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
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 {
fun app(): AppDao
@ -34,8 +36,13 @@ interface Database {
fun allowedContact(): AllowedContactDao
fun userKey(): UserKeyDao
fun sessionDuration(): SessionDurationDao
fun derivedDataDao(): DerivedDataDao
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 close()

View file

@ -15,11 +15,19 @@
*/
package io.timelimit.android.data
import android.annotation.SuppressLint
import android.content.Context
import androidx.room.Database
import androidx.room.InvalidationTracker
import androidx.room.Room
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 java.lang.ref.WeakReference
import java.util.concurrent.CountDownLatch
@Database(entities = [
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
override fun <T> runInTransaction(block: () -> T): T {
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() {
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
* 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)")
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
abstract fun addCategoryAppsSync(items: Collection<CategoryApp>)

View file

@ -250,7 +250,7 @@ abstract class ConfigDao {
val experimentalFlags: LiveData<Long> by lazy { getExperimentalFlagsLive() }
private fun getExperimentalFlagsSync(): Long {
fun getExperimentalFlagsSync(): Long {
val v = getValueOfKeySync(ConfigurationItemType.ExperimentalFlags)
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
import androidx.lifecycle.LiveData
import androidx.room.*
import io.timelimit.android.data.model.SessionDuration
@ -34,7 +33,7 @@ interface SessionDurationDao {
): SessionDuration?
@Query("SELECT * FROM session_duration WHERE category_id = :categoryId")
fun getSessionDurationItemsByCategoryId(categoryId: String): LiveData<List<SessionDuration>>
fun getSessionDurationItemsByCategoryIdSync(categoryId: String): List<SessionDuration>
@Insert
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
* it under the terms of the GNU General Public License as published by
@ -15,8 +15,6 @@
*/
package io.timelimit.android.data.dao
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
@ -24,12 +22,8 @@ import io.timelimit.android.data.model.TemporarilyAllowedApp
@Dao
abstract class TemporarilyAllowedAppDao {
@Query("SELECT * FROM temporarily_allowed_app WHERE device_id = :deviceId")
abstract fun getTemporarilyAllowedAppsInternal(deviceId: String): LiveData<List<TemporarilyAllowedApp>>
fun getTemporarilyAllowedApps(deviceId: String): LiveData<List<String>> {
return Transformations.map(getTemporarilyAllowedAppsInternal(deviceId)) { it.map { it.packageName } }
}
@Query("SELECT package_name FROM temporarily_allowed_app")
abstract fun getTemporarilyAllowedAppsSync(): List<String>
@Insert
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")
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")
abstract fun getAllUsedTimeItemsSync(): List<UsedTimeItem>

View file

@ -16,7 +16,9 @@
package io.timelimit.android.data.extensions
import io.timelimit.android.data.model.Category
import io.timelimit.android.data.model.derived.CategoryRelatedData
// TODO: remove this
fun List<Category>.sorted(): List<Category> {
val categoryIds = this.map { it.id }.toSet()
@ -33,3 +35,20 @@ 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()
}

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 Jonas Lochmann
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -13,17 +13,9 @@
* 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.extension
import io.timelimit.android.data.model.Category
import io.timelimit.android.integration.platform.BatteryStatus
package io.timelimit.android.data.invalidation
fun BatteryStatus.isCategoryAllowed(category: Category?): Boolean {
return if (category == null) {
true
} else if (this.charging) {
this.level >= category.minBatteryLevelWhileCharging
} else {
this.level >= category.minBatteryLevelMobile
}
interface Observer {
fun onInvalidated(tables: Set<Table>)
}

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
* it under the terms of the GNU General Public License as published by
@ -15,11 +15,12 @@
*/
package io.timelimit.android.date
import org.threeten.bp.DayOfWeek
import org.threeten.bp.LocalDate
import org.threeten.bp.temporal.ChronoUnit
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 {
fun convertDayOfWeek(dayOfWeek: Int) = when(dayOfWeek) {
Calendar.MONDAY -> 0
@ -32,7 +33,18 @@ data class DateInTimezone(val dayOfWeek: Int, val dayOfEpoch: Int) {
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()
calendar.firstDayOfWeek = Calendar.MONDAY
@ -40,17 +52,19 @@ data class DateInTimezone(val dayOfWeek: Int, val dayOfEpoch: Int) {
calendar.timeZone = timeZone
calendar.timeInMillis = timeInMillis
val dayOfWeek = convertDayOfWeek(calendar.get(Calendar.DAY_OF_WEEK))
val localDate = LocalDate.of(
return LocalDate.of(
calendar.get(Calendar.YEAR),
calendar.get(Calendar.MONTH) + 1,
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)
var installedAppsChangeListener: Runnable? = null
var systemClockChangeListener: Runnable? = null
}
data class ForegroundAppSpec(var packageName: String?, var activityName: String?) {

View file

@ -87,6 +87,12 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio
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> {

View file

@ -27,9 +27,13 @@ import android.util.Log
import androidx.core.app.NotificationCompat
import io.timelimit.android.BuildConfig
import io.timelimit.android.R
import io.timelimit.android.async.Threads
import io.timelimit.android.coroutines.executeAndWait
import io.timelimit.android.coroutines.runAsync
import io.timelimit.android.livedata.waitForNonNullValue
import io.timelimit.android.data.model.UserType
import io.timelimit.android.logic.*
import io.timelimit.android.logic.blockingreason.AppBaseHandling
import io.timelimit.android.logic.blockingreason.CategoryItselfHandling
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
class NotificationListener: NotificationListenerService() {
@ -39,7 +43,6 @@ class NotificationListener: NotificationListenerService() {
}
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 queryAppTitleCache: QueryAppTitleCache by lazy { QueryAppTitleCache(appLogic.platformIntegration) }
private val lastOngoingNotificationHidden = mutableSetOf<String>()
@ -140,31 +143,53 @@ class NotificationListener: NotificationListenerService() {
return BlockingReason.None
}
val blockingReason = blockingReasonUtil.getBlockingReason(
packageName = sbn.packageName,
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
val deviceAndUserRelatedData = Threads.database.executeAndWait {
appLogic.database.derivedDataDao().getUserAndDeviceRelatedDataSync()
}
return when (blockingReason) {
is NoBlockingReason -> BlockingReason.None
is BlockedReasonDetails -> {
if (isSystemApp(sbn.packageName) && blockingReason.reason == BlockingReason.NotPartOfAnCategory) {
return BlockingReason.None
return if (deviceAndUserRelatedData?.userRelatedData?.user?.type != UserType.Child) {
BlockingReason.None
} else {
val appHandling = AppBaseHandling.calculate(
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) {
Log.d(LOG_TAG, "blocking notification of ${sbn.packageName} because ${blockingReason.reason}")
if (allowNotificationFilter && categoryHandlings.find { it.blockAllNotifications } != null) {
BlockingReason.NotificationsAreBlocked
} else {
categoryHandlings.find { it.shouldBlockActivities }?.activityBlockingReason
?: BlockingReason.None
}
return blockingReason.reason
} else {
BlockingReason.None
}
}
}

View file

@ -42,6 +42,8 @@ class DummyTimeApi(var timeStepSizeInMillis: Long): TimeApi() {
scheduledActions.add(ScheduledAction(currentUptime + delayInMillis, runnable))
}
override fun runDelayedByUptime(runnable: Runnable, delayInMillis: Long) = runDelayed(runnable, delayInMillis)
override fun cancelScheduledAction(runnable: 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
* it under the terms of the GNU General Public License as published by
@ -21,7 +21,47 @@ import android.os.SystemClock
import java.util.*
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 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 {
return System.currentTimeMillis()
@ -35,8 +75,21 @@ object RealTimeApi: TimeApi() {
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) {
handler.removeCallbacks(runnable)
synchronized(queue) {
val itemsToRemove = queue.filter { it.runnable === runnable }
itemsToRemove.forEach { queue.remove(it) }
}
}
override fun getSystemTimeZone() = TimeZone.getDefault()

View file

@ -26,6 +26,7 @@ abstract class TimeApi {
abstract fun getCurrentUptimeInMillis(): Long
// function to run something delayed at the UI Thread
abstract fun runDelayed(runnable: Runnable, delayInMillis: Long)
abstract fun runDelayedByUptime(runnable: Runnable, delayInMillis: Long)
abstract fun cancelScheduledAction(runnable: Runnable)
suspend fun sleep(timeInMillis: Long) = suspendCoroutine<Void?> {
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
* it under the terms of the GNU General Public License as published by
@ -15,24 +15,28 @@
*/
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.integration.platform.ForegroundAppSpec
import io.timelimit.android.livedata.waitForNonNullValue
import io.timelimit.android.livedata.waitForNullableValue
import io.timelimit.android.logic.blockingreason.AppBaseHandling
object AppAffectedByPrimaryDeviceUtil {
suspend fun isCurrentAppAffectedByPrimaryDevice(
logic: AppLogic
): Boolean {
val user = logic.deviceUserEntry.waitForNullableValue()
?: throw NullPointerException("no user is signed in")
if (user.type != UserType.Child) {
throw IllegalStateException("no child is signed in")
val deviceAndUserRelatedData = Threads.database.executeAndWait {
logic.database.derivedDataDao().getUserAndDeviceRelatedDataSync()
}
if (user.relaxPrimaryDevice) {
if (logic.fullVersion.shouldProvideFullVersionFunctions.waitForNonNullValue() == true) {
if (deviceAndUserRelatedData?.userRelatedData?.user?.type != UserType.Child) {
return false
}
if (deviceAndUserRelatedData.userRelatedData.user.relaxPrimaryDevice) {
if (deviceAndUserRelatedData.deviceRelatedData.isConnectedAndHasPremium) {
return false
}
}
@ -49,50 +53,26 @@ object AppAffectedByPrimaryDeviceUtil {
return false
}
val categories = logic.database.category().getCategoriesByChildId(user.id).waitForNonNullValue()
val categoryId = run {
val categoryIdAtAppLevel = logic.database.categoryApp().getCategoryApp(
categoryIds = categories.map { it.id },
packageName = currentApp.packageName!!
).waitForNullableValue()?.categoryId
val handling = AppBaseHandling.calculate(
foregroundAppPackageName = currentApp.packageName,
foregroundAppActivityName = currentApp.activityName,
deviceRelatedData = deviceAndUserRelatedData.deviceRelatedData,
userRelatedData = deviceAndUserRelatedData.userRelatedData,
pauseCounting = false,
pauseForegroundAppBackgroundLoop = false
)
if (logic.deviceEntry.waitForNullableValue()?.enableActivityLevelBlocking == true) {
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) {
if (!(handling is AppBaseHandling.UseCategories)) {
return false
}
// check blocked time areas
if (
(category.blockedMinutesInWeek.dataNotToModify.isEmpty == false) ||
(parentCategory?.blockedMinutesInWeek?.dataNotToModify?.isEmpty == false)
) {
return true
}
return handling.categoryIds.find { categoryId ->
val category = deviceAndUserRelatedData.userRelatedData.categoryById[categoryId]!!
// check time limit rules
val rules = logic.database.timeLimitRules().getTimeLimitRulesByCategories(
categoryIds = listOf(categoryId) +
(if (parentCategory != null) listOf(parentCategory.id) else emptyList())
).waitForNonNullValue()
val hasBlockedTimeAreas = !category.category.blockedMinutesInWeek.dataNotToModify.isEmpty
val hasRules = category.rules.isNotEmpty()
if (rules.isNotEmpty()) {
return true
}
return false
hasBlockedTimeAreas || hasRules
} != null
}
}

View file

@ -26,16 +26,15 @@ import io.timelimit.android.coroutines.runAsync
import io.timelimit.android.coroutines.runAsyncExpectForever
import io.timelimit.android.data.backup.DatabaseBackup
import io.timelimit.android.data.model.*
import io.timelimit.android.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.AppStatusMessage
import io.timelimit.android.integration.platform.ForegroundAppSpec
import io.timelimit.android.integration.platform.ProtectionLevel
import io.timelimit.android.integration.platform.android.AccessibilityService
import io.timelimit.android.livedata.*
import io.timelimit.android.sync.actions.AddUsedTimeActionItemAdditionalCountingSlot
import io.timelimit.android.sync.actions.AddUsedTimeActionItemSessionDurationLimitSlot
import io.timelimit.android.logic.blockingreason.AppBaseHandling
import io.timelimit.android.logic.blockingreason.CategoryHandlingCache
import io.timelimit.android.sync.actions.UpdateDeviceStatusAction
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
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 val temporarilyAllowedApps = appLogic.deviceId.switchMap {
if (it != null) {
appLogic.database.temporarilyAllowedApp().getTemporarilyAllowedApps(it)
} else {
liveDataFromValue(Collections.emptyList<String>())
}
}
init {
runAsyncExpectForever { backgroundServiceLoop() }
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 {
if (it) {
PeriodicSyncInBackgroundWorker.enable()
@ -121,15 +108,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
}
}
private val cache = BackgroundTaskLogicCache(appLogic)
private val deviceUserEntryLive = cache.deviceUserEntryLive
private val childCategories = cache.childCategories
private val timeLimitRules = cache.timeLimitRules
private val usedTimesOfCategoryAndWeekByFirstDayOfWeek = cache.usedTimesOfCategoryAndWeekByFirstDayOfWeek
private val shouldDoAutomaticSignOut = cache.shouldDoAutomaticSignOut
private val liveDataCaches = cache.liveDataCaches
private var usedTimeUpdateHelper: UsedTimeUpdateHelper? = null
private val usedTimeUpdateHelper = UsedTimeUpdateHelper(appLogic)
private var previousMainLogicExecutionTime = 0
private var previousMainLoopEndTime = 0L
private val dayChangeTracker = DayChangeTracker(
@ -138,6 +117,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
)
private val appTitleCache = QueryAppTitleCache(appLogic.platformIntegration)
private val categoryHandlingCache = CategoryHandlingCache()
private val isChromeOs = appLogic.context.packageManager.hasSystemFeature(PackageManager.FEATURE_PC)
@ -167,12 +147,19 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
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()
val foregroundAppHandling = BackgroundTaskRestrictionLogicResult()
val audioPlaybackHandling = BackgroundTaskRestrictionLogicResult()
private suspend fun commitUsedTimeUpdaters() {
usedTimeUpdateHelper?.forceCommit(appLogic)
usedTimeUpdateHelper.flush()
}
private suspend fun backgroundServiceLoop() {
@ -192,22 +179,30 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
// app must be enabled
if (!appLogic.enable.waitForNonNullValue()) {
commitUsedTimeUpdaters()
liveDataCaches.removeAllItems()
appLogic.platformIntegration.setAppStatusMessage(null)
appLogic.platformIntegration.setShowBlockingOverlay(false)
setShowNotificationToRevokeTemporarilyAllowedApps(false)
appLogic.enable.waitUntilValueMatches { it == true }
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
val deviceUserEntry = deviceUserEntryLive.read().waitForNullableValue()
if (deviceUserEntry == null || deviceUserEntry.type != UserType.Child) {
if (deviceRelatedData == null || userRelatedData == null || userRelatedData.user.type != UserType.Child) {
commitUsedTimeUpdaters()
val shouldDoAutomaticSignOut = shouldDoAutomaticSignOut.read()
if (shouldDoAutomaticSignOut.waitForNonNullValue()) {
val shouldDoAutomaticSignOut = deviceRelatedData != null && DefaultUserLogic.hasAutomaticSignOut(deviceRelatedData)
if (shouldDoAutomaticSignOut) {
appLogic.defaultUserLogic.reportScreenOn(appLogic.platformIntegration.isScreenOn())
appLogic.platformIntegration.setAppStatusMessage(
@ -219,16 +214,12 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
)
appLogic.platformIntegration.setShowBlockingOverlay(false)
liveDataCaches.reportLoopDone()
appLogic.timeApi.sleep(backgroundServiceInterval)
} else {
liveDataCaches.removeAllItems()
appLogic.platformIntegration.setAppStatusMessage(null)
appLogic.platformIntegration.setShowBlockingOverlay(false)
val isChildSignedIn = deviceUserEntryLive.read().map { it != null && it.type == UserType.Child }
isChildSignedIn.or(shouldDoAutomaticSignOut).waitUntilValueMatches { it == true }
appLogic.timeApi.sleep(backgroundServiceInterval)
}
continue
@ -240,18 +231,18 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
appLogic.realTimeLogic.getRealTime(realTime)
val nowTimestamp = realTime.timeInMillis
val nowTimezone = TimeZone.getTimeZone(deviceUserEntry.timeZone)
val nowTimezone = TimeZone.getTimeZone(userRelatedData.user.timeZone)
val nowDate = DateInTimezone.newInstance(nowTimestamp, nowTimezone)
val minuteOfWeek = getMinuteOfWeek(nowTimestamp, nowTimezone)
val nowDate = DateInTimezone.getLocalDate(nowTimestamp, nowTimezone)
val dayOfEpoch = nowDate.toEpochDay().toInt()
// eventually remove old used time data
if (realTime.shouldTrustTimePermanently) {
val dayChange = dayChangeTracker.reportDayChange(nowDate.dayOfEpoch)
val dayChange = dayChangeTracker.reportDayChange(dayOfEpoch)
fun deleteOldUsedTimes() = UsedTimeDeleter.deleteOldUsedTimeItems(
database = appLogic.database,
date = nowDate,
date = DateInTimezone.newInstance(nowDate),
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
val isScreenOn = appLogic.platformIntegration.isScreenOn()
val batteryStatus = appLogic.platformIntegration.getBatteryStatus()
@ -277,293 +264,104 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
appLogic.defaultUserLogic.reportScreenOn(isScreenOn)
if (!isScreenOn) {
if (temporarilyAllowedApps.isNotEmpty()) {
if (deviceRelatedData.temporarilyAllowedApps.isNotEmpty()) {
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())
val foregroundAppPackageName = foregroundAppSpec.packageName
val foregroundAppActivityName = foregroundAppSpec.activityName
val audioPlaybackPackageName = appLogic.platformIntegration.getMusicPlaybackPackage()
val activityLevelBlocking = appLogic.deviceEntry.value?.enableActivityLevelBlocking ?: false
foregroundAppHandling.reset()
audioPlaybackHandling.reset()
BackgroundTaskRestrictionLogic.getHandling(
val foregroundAppBaseHandling = AppBaseHandling.calculate(
foregroundAppPackageName = foregroundAppPackageName,
foregroundAppActivityName = foregroundAppActivityName,
pauseForegroundAppBackgroundLoop = pauseForegroundAppBackgroundLoop,
temporarilyAllowedApps = temporarilyAllowedApps,
categories = categories,
activityLevelBlocking = activityLevelBlocking,
deviceUserEntry = deviceUserEntry,
batteryStatus = batteryStatus,
shouldTrustTimeTemporarily = realTime.shouldTrustTimeTemporarily,
nowTimestamp = nowTimestamp,
minuteOfWeek = minuteOfWeek,
cache = cache,
result = foregroundAppHandling
userRelatedData = userRelatedData,
deviceRelatedData = deviceRelatedData,
pauseCounting = !isScreenOn
)
BackgroundTaskRestrictionLogic.getHandling(
val backgroundAppBaseHandling = AppBaseHandling.calculate(
foregroundAppPackageName = audioPlaybackPackageName,
foregroundAppActivityName = null,
pauseForegroundAppBackgroundLoop = false,
temporarilyAllowedApps = temporarilyAllowedApps,
categories = categories,
activityLevelBlocking = activityLevelBlocking,
deviceUserEntry = deviceUserEntry,
batteryStatus = batteryStatus,
shouldTrustTimeTemporarily = realTime.shouldTrustTimeTemporarily,
nowTimestamp = nowTimestamp,
minuteOfWeek = minuteOfWeek,
cache = cache,
result = audioPlaybackHandling
userRelatedData = userRelatedData,
deviceRelatedData = deviceRelatedData,
pauseCounting = false
)
// update used time helper if date does not match
if (usedTimeUpdateHelper?.date != nowDate) {
usedTimeUpdateHelper?.forceCommit(appLogic)
usedTimeUpdateHelper = UsedTimeUpdateHelper(nowDate)
}
// check if should be blocked
val blockForegroundApp = foregroundAppBaseHandling is AppBaseHandling.BlockDueToNoCategory ||
(foregroundAppBaseHandling is AppBaseHandling.UseCategories && foregroundAppBaseHandling.categoryIds.find {
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
fun buildDummyUsedTimeItems(categoryId: String): List<UsedTimeItem> {
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
}
handling.shouldBlockActivities || blockAllNotifications
} != null)
// update times
val timeToSubtract = Math.min(previousMainLogicExecutionTime, maxUsedTimeToAdd)
// see note above declaration of remainingTimeForegroundAppChild
val shouldCountForegroundApp = remainingTimeForegroundApp != null && isScreenOn && remainingTimeForegroundApp.hasRemainingTime
val shouldCountBackgroundApp = remainingTimeBackgroundApp != null && remainingTimeBackgroundApp.hasRemainingTime
val categoryHandlingsToCount = AppBaseHandling.getCategoriesForCounting(foregroundAppBaseHandling, backgroundAppBaseHandling)
.map { categoryHandlingCache.get(it) }
.filter { it.shouldCountTime }
val categoriesToCount = mutableSetOf<String>()
val categoriesToCountExtraTime = mutableSetOf<String>()
val categoriesToCountSessionDurations = mutableSetOf<String>()
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
},
if (
usedTimeUpdateHelper.report(
duration = timeToSubtract,
dayOfEpoch = dayOfEpoch,
trustedTimestamp = if (realTime.shouldTrustTimePermanently) realTime.timeInMillis else 0,
sessionDurationLimits = run {
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
}
handlings = categoryHandlingsToCount
)
) {
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) {
usedTimeUpdateHelper.forceCommit(appLogic)
fun timeToSubtractForCategory(categoryId: String): Int {
return if (usedTimeUpdateHelper.getCountedCategoryIds().contains(categoryId)) usedTimeUpdateHelper.getCountedTime() else 0
}
// trigger time warnings
fun eventuallyTriggerTimeWarning(remaining: RemainingTime, categoryId: String?) {
val category = categories.find { it.id == categoryId } ?: return
val oldRemainingTime = remaining.includingExtraTime
val newRemainingTime = oldRemainingTime - timeToSubtract
categoriesToCount.forEach { categoryId ->
val category = userRelatedData.categoryById[categoryId]!!.category
val handling = categoryHandlingCache.get(categoryId)
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)) {
// 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
fun buildStatusMessageWithCurrentAppTitle(
text: String,
@ -597,107 +390,156 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
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(
handling: BackgroundTaskRestrictionLogicResult,
remainingTime: RemainingTime?,
remainingSessionDuration: Long?,
fun buildNotificationForAppWithCategoryUsage(
suffix: String,
appPackageName: String?,
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,
appPackageName: String?,
appActivityToShow: String?
): AppStatusMessage = when (handling.status) {
BackgroundTaskLogicAppStatus.ShouldBlock -> buildStatusMessageWithCurrentAppTitle(
text = appLogic.context.getString(R.string.background_logic_opening_lockscreen),
titleSuffix = suffix,
appPackageName = appPackageName,
appActivityToShow = appActivityToShow
)
BackgroundTaskLogicAppStatus.BackgroundLogicPaused -> AppStatusMessage(
): AppStatusMessage = when (handling) {
is AppBaseHandling.UseCategories -> throw IllegalArgumentException()
AppBaseHandling.BlockDueToNoCategory -> throw IllegalArgumentException()
AppBaseHandling.PauseLogic -> AppStatusMessage(
title = appLogic.context.getString(R.string.background_logic_paused_title) + suffix,
text = appLogic.context.getString(R.string.background_logic_paused_text)
)
BackgroundTaskLogicAppStatus.InternalWhitelist -> buildStatusMessageWithCurrentAppTitle(
AppBaseHandling.Whitelist -> buildStatusMessageWithCurrentAppTitle(
text = appLogic.context.getString(R.string.background_logic_whitelisted),
titleSuffix = suffix,
appPackageName = appPackageName,
appActivityToShow = appActivityToShow
)
BackgroundTaskLogicAppStatus.TemporarilyAllowed -> buildStatusMessageWithCurrentAppTitle(
AppBaseHandling.TemporarilyAllowed -> buildStatusMessageWithCurrentAppTitle(
text = appLogic.context.getString(R.string.background_logic_temporarily_allowed),
titleSuffix = suffix,
appPackageName = appPackageName,
appActivityToShow = appActivityToShow
)
BackgroundTaskLogicAppStatus.LimitsDisabled -> buildStatusMessageWithCurrentAppTitle(
text = appLogic.context.getString(R.string.background_logic_limits_disabled),
titleSuffix = suffix,
appPackageName = appPackageName,
appActivityToShow = appActivityToShow
)
BackgroundTaskLogicAppStatus.AllowedNoTimelimit -> buildStatusMessageWithCurrentAppTitle(
text = appLogic.context.getString(R.string.background_logic_no_timelimit),
titlePrefix = getCategoryTitle(handling.categoryId) + " - ",
titleSuffix = suffix,
appPackageName = appPackageName,
appActivityToShow = appActivityToShow
)
BackgroundTaskLogicAppStatus.AllowedCountAndCheckTime -> 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(
AppBaseHandling.Idle -> AppStatusMessage(
appLogic.context.getString(R.string.background_logic_idle_title) + suffix,
appLogic.context.getString(R.string.background_logic_idle_text)
)
}
val showBackgroundStatus = audioPlaybackHandling.status != BackgroundTaskLogicAppStatus.Idle &&
audioPlaybackHandling.status != BackgroundTaskLogicAppStatus.ShouldBlock &&
val showBackgroundStatus = !(backgroundAppBaseHandling is AppBaseHandling.Idle) &&
!blockAudioPlayback &&
audioPlaybackPackageName != foregroundAppPackageName
if (showBackgroundStatus && nowTimestamp % 6000 >= 3000) {
// show notification for music
appLogic.platformIntegration.setAppStatusMessage(
buildStatusMessage(
handling = audioPlaybackHandling,
remainingTime = remainingTimeBackgroundApp,
suffix = " (2/2)",
appPackageName = audioPlaybackPackageName,
appActivityToShow = null,
remainingSessionDuration = remainingSessionDurationBackgroundApp
)
val statusMessage = if (blockForegroundApp) {
buildStatusMessageWithCurrentAppTitle(
text = appLogic.context.getString(R.string.background_logic_opening_lockscreen),
appPackageName = foregroundAppPackageName,
appActivityToShow = if (activityLevelBlocking) foregroundAppActivityName else null
)
} else {
// show regular notification
appLogic.platformIntegration.setAppStatusMessage(
buildStatusMessage(
handling = foregroundAppHandling,
remainingTime = remainingTimeForegroundApp,
suffix = if (showBackgroundStatus) " (1/2)" else "",
val pagesForTheForegroundApp = if (foregroundAppBaseHandling is AppBaseHandling.UseCategories) foregroundAppBaseHandling.categoryIds.size else 1
val pagesForTheBackgroundApp = if (!showBackgroundStatus) 0 else if (backgroundAppBaseHandling is AppBaseHandling.UseCategories) backgroundAppBaseHandling.categoryIds.size else 1
val totalPages = pagesForTheForegroundApp + pagesForTheBackgroundApp
val currentPage = (nowTimestamp / 3000 % totalPages).toInt()
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,
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
if (foregroundAppHandling.status == BackgroundTaskLogicAppStatus.ShouldBlock) {
if (blockForegroundApp) {
openLockscreen(foregroundAppPackageName!!, foregroundAppActivityName)
} else {
appLogic.platformIntegration.setShowBlockingOverlay(false)
}
if (audioPlaybackHandling.status == BackgroundTaskLogicAppStatus.ShouldBlock && audioPlaybackPackageName != null) {
if (blockAudioPlayback && audioPlaybackPackageName != null) {
appLogic.platformIntegration.muteAudioIfPossible(audioPlaybackPackageName)
}
} catch (ex: SecurityException) {
@ -723,8 +565,6 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
appLogic.platformIntegration.setShowBlockingOverlay(false)
}
liveDataCaches.reportLoopDone()
// delay before running next time
val endTime = appLogic.timeApi.getCurrentUptimeInMillis()
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
import android.util.Log
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.getMinuteOfWeek
import io.timelimit.android.extensions.MinuteOfDay
import io.timelimit.android.integration.platform.android.AndroidIntegrationApps
import io.timelimit.android.livedata.*
import io.timelimit.android.logic.extension.isCategoryAllowed
import java.util.*
enum class BlockingReason {
@ -47,382 +40,8 @@ enum class BlockingLevel {
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) {
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?> {
val realTime = RealTime.newInstance()
@ -468,50 +87,4 @@ class BlockingReasonUtil(private val appLogic: AppLogic) {
}
}.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
* it under the terms of the GNU General Public License as published by
@ -16,14 +16,26 @@
package io.timelimit.android.logic
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.*
class CurrentDeviceLogic(private val appLogic: AppLogic) {
private val disabledPrimaryDeviceCheck = appLogic.deviceUserEntry.switchMap { userEntry ->
if (userEntry?.relaxPrimaryDevice == true) {
appLogic.fullVersion.shouldProvideFullVersionFunctions
} else {
liveDataFromValue(false)
companion object {
fun handleDeviceAsCurrentDevice(device: DeviceRelatedData, user: UserRelatedData): Boolean {
if (device.isLocalMode) {
return true
}
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 ->
if (userEntry?.currentDevice == null) {
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.runAsync
import io.timelimit.android.data.model.User
import io.timelimit.android.data.model.derived.DeviceRelatedData
import io.timelimit.android.livedata.*
import io.timelimit.android.sync.actions.SignOutAtDeviceAction
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
@ -30,6 +31,8 @@ import kotlinx.coroutines.sync.withLock
class DefaultUserLogic(private val appLogic: AppLogic) {
companion object {
private const val LOG_TAG = "DefaultUserLogic"
fun hasAutomaticSignOut(device: DeviceRelatedData): Boolean = device.hasValidDefaultUser && device.deviceEntry.defaultUserTimeout > 0
}
private fun defaultUserEntry() = appLogic.deviceEntry.map { device ->
@ -40,10 +43,7 @@ class DefaultUserLogic(private val appLogic: AppLogic) {
else
liveDataFromValue(null as User?)
}
private fun hasDefaultUser() = defaultUserEntry().map { it != null }.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()

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 Jonas Lochmann
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -17,6 +17,7 @@ package io.timelimit.android.logic
import android.util.Log
import io.timelimit.android.BuildConfig
import io.timelimit.android.async.Threads
import io.timelimit.android.coroutines.runAsync
import io.timelimit.android.data.model.NetworkTime
import io.timelimit.android.livedata.ignoreUnchanged
@ -27,6 +28,7 @@ import java.io.IOException
class RealTimeLogic(private val appLogic: AppLogic) {
companion object {
private const val LOG_TAG = "RealTimeLogic"
private const val MISSING_NETWORK_TIME_GRACE_PERIOD = 5 * 1000L
}
private val deviceEntry = appLogic.deviceEntryIfEnabled
@ -48,6 +50,10 @@ class RealTimeLogic(private val appLogic: AppLogic) {
requireRemoteTimeUptime = appLogic.timeApi.getCurrentUptimeInMillis()
tryQueryTime()
Threads.mainThreadHandler.postDelayed({
callTimeModificationListeners()
}, MISSING_NETWORK_TIME_GRACE_PERIOD)
} else {
if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "shouldQueryTime = false")
@ -56,6 +62,10 @@ class RealTimeLogic(private val appLogic: AppLogic) {
appLogic.timeApi.cancelScheduledAction(tryQueryTime)
}
}
appLogic.platformIntegration.systemClockChangeListener = Runnable {
callTimeModificationListeners()
}
}
private var lastSuccessfullyTimeRequestUptime: Long? = null
@ -65,6 +75,19 @@ class RealTimeLogic(private val appLogic: AppLogic) {
private val queryTimeLock = Mutex()
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() {
if (BuildConfig.DEBUG) {
@ -103,6 +126,8 @@ class RealTimeLogic(private val appLogic: AppLogic) {
// schedule refresh in 2 hours
appLogic.timeApi.runDelayed(tryQueryTime, 1000 * 60 * 60 * 2)
callTimeModificationListeners()
} catch (ex: Exception) {
if (uptimeRealTimeOffset == null) {
// schedule next attempt in 10 seconds
@ -127,6 +152,8 @@ class RealTimeLogic(private val appLogic: AppLogic) {
val systemTime = appLogic.timeApi.getCurrentTimeInMillis()
confirmedUptimeSystemTimeOffset = systemTime - uptime
callTimeModificationListeners()
}
fun getRealTime(time: RealTime) {
@ -174,7 +201,7 @@ class RealTimeLogic(private val appLogic: AppLogic) {
} else {
time.timeInMillis = systemTime
// 5 seconds grace period
time.shouldTrustTimeTemporarily = requireRemoteTimeUptime + 5000 > systemUptime
time.shouldTrustTimeTemporarily = requireRemoteTimeUptime + MISSING_NETWORK_TIME_GRACE_PERIOD > systemUptime
time.shouldTrustTimePermanently = 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
* it under the terms of the GNU General Public License as published by
@ -15,104 +15,154 @@
*/
package io.timelimit.android.logic
import androidx.lifecycle.LiveData
import io.timelimit.android.data.model.Category
import io.timelimit.android.async.Threads
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.ExperimentalFlags
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.livedata.ignoreUnchanged
import io.timelimit.android.livedata.liveDataFromValue
import io.timelimit.android.livedata.map
import io.timelimit.android.livedata.switchMap
import java.util.*
import io.timelimit.android.logic.blockingreason.CategoryHandlingCache
import java.lang.ref.WeakReference
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean
class SuspendAppsLogic(private val appLogic: AppLogic) {
private val blockingAtActivityLevel = appLogic.deviceEntry.map { it?.enableActivityLevelBlocking ?: false }
private val blockingReasonUtil = CategoriesBlockingReasonUtil(appLogic)
class SuspendAppsLogic(private val appLogic: AppLogic): Observer {
private var lastDefaultCategory: String? = null
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 ->
if (deviceId.isNullOrEmpty()) {
liveDataFromValue(emptyList())
} else {
appLogic.database.app().getAppsByDeviceIdAsync(deviceId).map { apps ->
apps.map { it.packageName }
}
}
}.ignoreUnchanged()
private val backgroundRunnable = Runnable {
while (pendingSync.getAndSet(false)) {
updateBlockingSync()
private val categoryData = appLogic.deviceUserEntry.switchMap { deviceUser ->
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>>
}
Thread.sleep(500)
}
}
private val realAppsToBlock = appLogic.database.config().isExperimentalFlagsSetAsync(ExperimentalFlags.SYSTEM_LEVEL_BLOCKING).switchMap { systemLevelBlocking ->
if (systemLevelBlocking) {
appsToBlock
} else {
liveDataFromValue(emptyList())
}
}.ignoreUnchanged()
private val triggerRunnable = Runnable {
triggerUpdate()
}
private fun getAppsWithCategories(packageNames: List<String>, data: RealCategoryData, blockingAtActivityLevel: Boolean): Map<String, Set<String>> {
val categoryForUnassignedApps = if (data.categories.find { it.id == data.categoryForUnassignedApps } != null) data.categoryForUnassignedApps else null
private fun triggerUpdate() {
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) {
val categoriesByPackageName = data.categoryApps.groupBy { it.packageNameWithoutActivityName }
@ -126,7 +176,7 @@ class SuspendAppsLogic(private val appLogic: AppLogic) {
if (!isMainAppIncluded) {
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>>()
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()
}
@ -160,16 +210,4 @@ class SuspendAppsLogic(private val appLogic: AppLogic) {
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
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.timelimit.android.logic
import android.util.Log
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.AddUsedTimeActionItemAdditionalCountingSlot
import io.timelimit.android.sync.actions.AddUsedTimeActionItemSessionDurationLimitSlot
import io.timelimit.android.sync.actions.AddUsedTimeActionVersion2
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
import io.timelimit.android.sync.actions.dispatch.CategoryNotFoundException
class UsedTimeUpdateHelper (val date: DateInTimezone) {
class UsedTimeUpdateHelper (private val appLogic: AppLogic) {
companion object {
private const val LOG_TAG = "UsedTimeUpdateHelper"
private const val LOG_TAG = "NewUsedTimeUpdateHelper"
}
val timeToAdd = mutableMapOf<String, Int>()
val extraTimeToSubtract = mutableMapOf<String, Int>()
val sessionDurationLimitSlots = mutableMapOf<String, Set<AddUsedTimeActionItemSessionDurationLimitSlot>>()
var trustedTimestamp: Long = 0
val additionalSlots = mutableMapOf<String, Set<AddUsedTimeActionItemAdditionalCountingSlot>>()
var shouldDoAutoCommit = false
private var countedTime = 0
private var lastCategoryHandlings = emptyList<CategoryItselfHandling>()
private var categoryIds = emptySet<String>()
private var trustedTimestamp = 0L
private var dayOfEpoch = 0
private var maxTimeToAdd = Long.MAX_VALUE
fun add(
categoryId: String, time: Int, slots: Set<AddUsedTimeActionItemAdditionalCountingSlot>,
includingExtraTime: Boolean, sessionDurationLimits: Set<AddUsedTimeActionItemSessionDurationLimitSlot>,
trustedTimestamp: Long
) {
if (time < 0) {
fun getCountedTime() = countedTime
fun getCountedCategoryIds() = categoryIds
// returns true if it made a commit
suspend fun report(duration: Int, handlings: List<CategoryItselfHandling>, trustedTimestamp: Long, dayOfEpoch: Int): Boolean {
if (handlings.find { !it.shouldCountTime } != null || duration < 0) {
throw IllegalArgumentException()
}
if (time == 0) {
return
if (duration == 0) {
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()) {
this.sessionDurationLimitSlots[categoryId] = sessionDurationLimits
}
val makeCommitByDifferntHandling = if (handlings != lastCategoryHandlings) {
val newIds = handlings.map { it.createdWithCategoryRelatedData.category.id }.toSet()
val oldIds = categoryIds
if (sessionDurationLimits.isNotEmpty() && trustedTimestamp != 0L) {
this.trustedTimestamp = trustedTimestamp
}
maxTimeToAdd = handlings.minBy { it.maxTimeToAdd }?.maxTimeToAdd ?: Long.MAX_VALUE
categoryIds = newIds
if (includingExtraTime) {
extraTimeToSubtract[categoryId] = (extraTimeToSubtract[categoryId] ?: 0) + time
}
if (lastCategoryHandlings.size != handlings.size) {
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]) {
shouldDoAutoCommit = true
} else if (slots.isNotEmpty()) {
additionalSlots[categoryId] = slots
}
handlings.find { newHandling ->
val oldHandling = oldHandlingById[newHandling.createdWithCategoryRelatedData.category.id]!!
if (timeToAdd[categoryId]!! >= 1000 * 10) {
shouldDoAutoCommit = true
}
}
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)
oldHandling.shouldCountExtraTime != newHandling.shouldCountExtraTime ||
oldHandling.additionalTimeCountingSlots != newHandling.additionalTimeCountingSlots ||
oldHandling.sessionDurationSlotsToCount != newHandling.sessionDurationSlotsToCount
} != null
}
}
} 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;
// in this case there could be some lost time
// changes for other categories, but it's no big problem
val madeCommit = if (makeCommit) {
doCommitPrivate()
} else {
false
}
timeToAdd.clear()
extraTimeToSubtract.clear()
sessionDurationLimitSlots.clear()
trustedTimestamp = 0
additionalSlots.clear()
shouldDoAutoCommit = false
this.lastCategoryHandlings = handlings
this.trustedTimestamp = trustedTimestamp
this.dayOfEpoch = dayOfEpoch
return madeCommit
}
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.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.Transformations
import com.google.android.material.snackbar.Snackbar
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.data.extensions.mapToTimezone
import io.timelimit.android.data.extensions.sorted
import io.timelimit.android.data.extensions.sortedCategories
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.LockFragmentCategoryButtonBinding
import io.timelimit.android.date.DateInTimezone
import io.timelimit.android.integration.platform.BatteryStatus
import io.timelimit.android.livedata.*
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.IncrementCategoryExtraTimeAction
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.payment.RequiresPurchaseDialogFragment
import io.timelimit.android.ui.view.SelectTimeSpanViewListener
import java.util.*
class LockFragment : Fragment() {
companion object {
private const val EXTRA_PACKAGE_NAME = "pkg"
private const val EXTRA_ACTIVITY = "activitiy"
private const val STATUS_DID_OPEN_SET_CURRENT_DEVICE_SCREEN = "didOpenSetCurrentDeviceScreen"
fun newInstance(packageName: String, activity: String?): 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 activityName: String? by lazy {
if (arguments!!.containsKey(EXTRA_ACTIVITY))
@ -86,189 +90,273 @@ class LockFragment : Fragment() {
private val auth: ActivityViewModel by lazy { getActivityViewModel(activity!!) }
private val logic: AppLogic by lazy { DefaultAppLogic.with(context!!) }
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 {
val binding = LockFragmentBinding.inflate(layoutInflater, container, false)
private val updateRunnable = Runnable { update() }
AuthenticationFab.manageAuthenticationFab(
fab = binding.fab,
shouldHighlight = auth.shouldHighlightAuthenticationButton,
authenticatedUser = auth.authenticatedUser,
fragment = this,
doesSupportAuth = liveDataFromValue(true)
fun scheduleUpdate(delay: Long) {
logic.timeApi.cancelScheduledAction(updateRunnable)
logic.timeApi.runDelayedByUptime(updateRunnable, delay)
}
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 {
systemTime ->
val appBaseHandling = AppBaseHandling.calculate(
foregroundAppPackageName = packageName,
foregroundAppActivityName = activityName,
deviceRelatedData = deviceAndUserRelatedData.deviceRelatedData,
userRelatedData = deviceAndUserRelatedData.userRelatedData,
pauseForegroundAppBackgroundLoop = false,
pauseCounting = false
)
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
binding.activityName = if (deviceAndUserRelatedData.deviceRelatedData.deviceEntry.enableActivityLevelBlocking)
activityName?.removePrefix(packageName)
else
null
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 {
binding.activityName = if (it) activityName?.removePrefix(packageName) else null
})
if (title != null) {
binding.appTitle = title
bindAddToCategoryOptions(deviceAndUserRelatedData.userRelatedData)
} else {
binding.appTitle = "???"
binding.reason = BlockingReason.None
binding.handlers = null
activity?.finish()
return
}
}
binding.appIcon.setImageDrawable(logic.platformIntegration.getAppIcon(packageName))
blockingReason.observe(this, Observer {
when (it) {
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>>?)
private fun setupHandlers(deviceId: String, userRelatedData: UserRelatedData, blockedCategoryId: String?) {
binding.handlers = object: Handlers {
override fun openMainApp() {
startActivity(Intent(context, MainActivity::class.java))
}
}.ignoreUnchanged()
// bind category name of the app
val appCategory = categories.switchMap {
status ->
override fun allowTemporarily() {
if (auth.requestAuthenticationOrReturnTrue()) {
val database = logic.database
if (status == null) {
liveDataFromValue(null as Category?)
} else {
val (_, categoryItems) = status
blockingReason.map { reason ->
if (reason is BlockedReasonDetails) {
reason.categoryId
} else {
null
}
}.map { categoryId ->
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!!)
// this accesses the database directly because it is not synced
Threads.database.submit {
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
}
}
}
}
})
// 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 {
HelpDialogFragment.newInstance(
title = R.string.lock_extratime_title,
text = R.string.lock_extratime_text
).show(fragmentManager!!)
).show(parentFragmentManager)
}
logic.fullVersion.shouldProvideFullVersionFunctions.observe(this, Observer { hasFullVersion ->
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 {
logic.database.config().getEnableAlternativeDurationSelectionAsync().observe(viewLifecycleOwner, Observer {
binding.extraTimeSelection.enablePickerMode(it)
})
@ -283,150 +371,76 @@ class LockFragment : Fragment() {
}
}
}
}
// bind disable time limits
mergeLiveData(logic.deviceUserEntry, logic.fullVersion.shouldProvideFullVersionFunctions).observe(this, Observer {
(child, hasFullVersion) ->
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (child != null) {
binding.manageDisableTimeLimits.handlers = ManageDisableTimelimitsViewHelper.createHandlers(
childId = child.id,
childTimezone = child.timeZone,
activity = activity!!,
hasFullVersion = hasFullVersion == true
)
}
if (savedInstanceState != null) {
didOpenSetCurrentDeviceScreen = savedInstanceState.getBoolean(STATUS_DID_OPEN_SET_CURRENT_DEVICE_SCREEN)
}
}
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 {
(child, time) ->
deviceAndUserRelatedData.observe(viewLifecycleOwner, Observer { update() })
batteryStatus.observe(viewLifecycleOwner, Observer { update() })
if (time == null || child == null) {
null
} else {
ManageDisableTimelimitsViewHelper.getDisabledUntilString(child, time, context!!)
}
}.observe(this, Observer {
binding.manageDisableTimeLimits.disableTimeLimitsUntilString = it
})
binding.packageName = packageName
binding.handlers = object: Handlers {
override fun openMainApp() {
startActivity(Intent(context, MainActivity::class.java))
}
binding.appTitle = title ?: "???"
binding.appIcon.setImageDrawable(logic.platformIntegration.getAppIcon(packageName))
override fun allowTemporarily() {
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()
}
}
}
}
initExtraTimeView()
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 {