mirror of
https://codeberg.org/timelimit/timelimit-android.git
synced 2025-10-05 10:49:26 +02:00
Refactor blocking logic
This commit is contained in:
parent
5f6a1edd1a
commit
099c781f18
45 changed files with 2786 additions and 1950 deletions
|
@ -16,7 +16,9 @@
|
||||||
package io.timelimit.android.data
|
package io.timelimit.android.data
|
||||||
|
|
||||||
import io.timelimit.android.data.dao.*
|
import io.timelimit.android.data.dao.*
|
||||||
import java.io.Closeable
|
import io.timelimit.android.data.invalidation.Observer
|
||||||
|
import io.timelimit.android.data.invalidation.Table
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
interface Database {
|
interface Database {
|
||||||
fun app(): AppDao
|
fun app(): AppDao
|
||||||
|
@ -34,8 +36,13 @@ interface Database {
|
||||||
fun allowedContact(): AllowedContactDao
|
fun allowedContact(): AllowedContactDao
|
||||||
fun userKey(): UserKeyDao
|
fun userKey(): UserKeyDao
|
||||||
fun sessionDuration(): SessionDurationDao
|
fun sessionDuration(): SessionDurationDao
|
||||||
|
fun derivedDataDao(): DerivedDataDao
|
||||||
|
|
||||||
fun <T> runInTransaction(block: () -> T): T
|
fun <T> runInTransaction(block: () -> T): T
|
||||||
|
fun <T> runInUnobservedTransaction(block: () -> T): T
|
||||||
|
fun registerWeakObserver(tables: Array<Table>, observer: WeakReference<Observer>)
|
||||||
|
fun registerTransactionCommitListener(listener: () -> Unit)
|
||||||
|
fun unregisterTransactionCommitListener(listener: () -> Unit)
|
||||||
|
|
||||||
fun deleteAllData()
|
fun deleteAllData()
|
||||||
fun close()
|
fun close()
|
||||||
|
|
|
@ -15,11 +15,19 @@
|
||||||
*/
|
*/
|
||||||
package io.timelimit.android.data
|
package io.timelimit.android.data
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
|
import androidx.room.InvalidationTracker
|
||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
|
import io.timelimit.android.data.dao.DerivedDataDao
|
||||||
|
import io.timelimit.android.data.invalidation.Observer
|
||||||
|
import io.timelimit.android.data.invalidation.Table
|
||||||
|
import io.timelimit.android.data.invalidation.TableUtil
|
||||||
import io.timelimit.android.data.model.*
|
import io.timelimit.android.data.model.*
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
|
||||||
@Database(entities = [
|
@Database(entities = [
|
||||||
User::class,
|
User::class,
|
||||||
|
@ -107,11 +115,84 @@ abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val derivedDataDao: DerivedDataDao by lazy { DerivedDataDao(this) }
|
||||||
|
override fun derivedDataDao(): DerivedDataDao = derivedDataDao
|
||||||
|
|
||||||
|
private val transactionCommitListeners = mutableSetOf<() -> Unit>()
|
||||||
|
|
||||||
|
override fun registerTransactionCommitListener(listener: () -> Unit): Unit = synchronized(transactionCommitListeners) {
|
||||||
|
transactionCommitListeners.add(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun unregisterTransactionCommitListener(listener: () -> Unit): Unit = synchronized(transactionCommitListeners) {
|
||||||
|
transactionCommitListeners.remove(listener)
|
||||||
|
}
|
||||||
|
|
||||||
// the room compiler needs this
|
// the room compiler needs this
|
||||||
override fun <T> runInTransaction(block: () -> T): T {
|
override fun <T> runInTransaction(block: () -> T): T {
|
||||||
return super.runInTransaction(block)
|
return super.runInTransaction(block)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun <T> runInUnobservedTransaction(block: () -> T): T {
|
||||||
|
openHelper.readableDatabase.beginTransaction()
|
||||||
|
try {
|
||||||
|
val result = block()
|
||||||
|
|
||||||
|
openHelper.readableDatabase.setTransactionSuccessful()
|
||||||
|
|
||||||
|
return result
|
||||||
|
} finally {
|
||||||
|
openHelper.readableDatabase.endTransaction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("RestrictedApi")
|
||||||
|
override fun endTransaction() {
|
||||||
|
openHelper.writableDatabase.endTransaction()
|
||||||
|
|
||||||
|
if (!inTransaction()) {
|
||||||
|
// block the query thread of room until this is done
|
||||||
|
val latch = CountDownLatch(1)
|
||||||
|
|
||||||
|
try {
|
||||||
|
queryExecutor.execute { latch.await() }
|
||||||
|
|
||||||
|
// without requesting a async refresh, no sync refresh will happen
|
||||||
|
invalidationTracker.refreshVersionsAsync()
|
||||||
|
invalidationTracker.refreshVersionsSync()
|
||||||
|
|
||||||
|
openHelper.readableDatabase.beginTransaction()
|
||||||
|
try {
|
||||||
|
synchronized(transactionCommitListeners) { transactionCommitListeners.toList() }.forEach { it() }
|
||||||
|
} finally {
|
||||||
|
openHelper.readableDatabase.endTransaction()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
latch.countDown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun registerWeakObserver(tables: Array<Table>, observer: WeakReference<Observer>) {
|
||||||
|
val tableNames = arrayOfNulls<String>(tables.size)
|
||||||
|
|
||||||
|
tables.forEachIndexed { index, table ->
|
||||||
|
tableNames[index] = TableUtil.toName(table)
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidationTracker.addObserver(object: InvalidationTracker.Observer(tableNames) {
|
||||||
|
override fun onInvalidated(tables: MutableSet<String>) {
|
||||||
|
val item = observer.get()
|
||||||
|
|
||||||
|
if (item != null) {
|
||||||
|
item.onInvalidated(tables.map { TableUtil.toEnum(it) }.toSet())
|
||||||
|
} else {
|
||||||
|
invalidationTracker.removeObserver(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
override fun deleteAllData() {
|
override fun deleteAllData() {
|
||||||
clearAllTables()
|
clearAllTables()
|
||||||
}
|
}
|
||||||
|
|
132
app/src/main/java/io/timelimit/android/data/cache/multi/DataCacheCloseDelay.kt
vendored
Normal file
132
app/src/main/java/io/timelimit/android/data/cache/multi/DataCacheCloseDelay.kt
vendored
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
154
app/src/main/java/io/timelimit/android/data/cache/multi/DataCacheImplementation.kt
vendored
Normal file
154
app/src/main/java/io/timelimit/android/data/cache/multi/DataCacheImplementation.kt
vendored
Normal 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)
|
||||||
|
}
|
43
app/src/main/java/io/timelimit/android/data/cache/multi/DataCacheInterfaces.kt
vendored
Normal file
43
app/src/main/java/io/timelimit/android/data/cache/multi/DataCacheInterfaces.kt
vendored
Normal 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
|
||||||
|
}
|
51
app/src/main/java/io/timelimit/android/data/cache/multi/DataCacheLiveData.kt
vendored
Normal file
51
app/src/main/java/io/timelimit/android/data/cache/multi/DataCacheLiveData.kt
vendored
Normal 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)
|
64
app/src/main/java/io/timelimit/android/data/cache/single/SingleItemDataCacheCloseDelay.kt
vendored
Normal file
64
app/src/main/java/io/timelimit/android/data/cache/single/SingleItemDataCacheCloseDelay.kt
vendored
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
121
app/src/main/java/io/timelimit/android/data/cache/single/SingleItemDataCacheImplementation.kt
vendored
Normal file
121
app/src/main/java/io/timelimit/android/data/cache/single/SingleItemDataCacheImplementation.kt
vendored
Normal 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)
|
||||||
|
}
|
43
app/src/main/java/io/timelimit/android/data/cache/single/SingleItemDataCacheInterfaces.kt
vendored
Normal file
43
app/src/main/java/io/timelimit/android/data/cache/single/SingleItemDataCacheInterfaces.kt
vendored
Normal 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
|
||||||
|
}
|
51
app/src/main/java/io/timelimit/android/data/cache/single/SingleItemDataCacheLiveData.kt
vendored
Normal file
51
app/src/main/java/io/timelimit/android/data/cache/single/SingleItemDataCacheLiveData.kt
vendored
Normal 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)
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -32,6 +32,9 @@ abstract class CategoryAppDao {
|
||||||
@Query("SELECT * FROM category_app WHERE category_id IN (:categoryIds)")
|
@Query("SELECT * FROM category_app WHERE category_id IN (:categoryIds)")
|
||||||
abstract fun getCategoryApps(categoryIds: List<String>): LiveData<List<CategoryApp>>
|
abstract fun getCategoryApps(categoryIds: List<String>): LiveData<List<CategoryApp>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM category_app WHERE category_id IN (SELECT category_id FROM category WHERE child_id = :userId)")
|
||||||
|
abstract fun getCategoryAppsByUserIdSync(userId: String): List<CategoryApp>
|
||||||
|
|
||||||
@Insert
|
@Insert
|
||||||
abstract fun addCategoryAppsSync(items: Collection<CategoryApp>)
|
abstract fun addCategoryAppsSync(items: Collection<CategoryApp>)
|
||||||
|
|
||||||
|
|
|
@ -250,7 +250,7 @@ abstract class ConfigDao {
|
||||||
|
|
||||||
val experimentalFlags: LiveData<Long> by lazy { getExperimentalFlagsLive() }
|
val experimentalFlags: LiveData<Long> by lazy { getExperimentalFlagsLive() }
|
||||||
|
|
||||||
private fun getExperimentalFlagsSync(): Long {
|
fun getExperimentalFlagsSync(): Long {
|
||||||
val v = getValueOfKeySync(ConfigurationItemType.ExperimentalFlags)
|
val v = getValueOfKeySync(ConfigurationItemType.ExperimentalFlags)
|
||||||
|
|
||||||
if (v == null) {
|
if (v == null) {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -16,7 +16,6 @@
|
||||||
|
|
||||||
package io.timelimit.android.data.dao
|
package io.timelimit.android.data.dao
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.room.*
|
import androidx.room.*
|
||||||
import io.timelimit.android.data.model.SessionDuration
|
import io.timelimit.android.data.model.SessionDuration
|
||||||
|
|
||||||
|
@ -34,7 +33,7 @@ interface SessionDurationDao {
|
||||||
): SessionDuration?
|
): SessionDuration?
|
||||||
|
|
||||||
@Query("SELECT * FROM session_duration WHERE category_id = :categoryId")
|
@Query("SELECT * FROM session_duration WHERE category_id = :categoryId")
|
||||||
fun getSessionDurationItemsByCategoryId(categoryId: String): LiveData<List<SessionDuration>>
|
fun getSessionDurationItemsByCategoryIdSync(categoryId: String): List<SessionDuration>
|
||||||
|
|
||||||
@Insert
|
@Insert
|
||||||
fun insertSessionDurationItemSync(item: SessionDuration)
|
fun insertSessionDurationItemSync(item: SessionDuration)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -15,8 +15,6 @@
|
||||||
*/
|
*/
|
||||||
package io.timelimit.android.data.dao
|
package io.timelimit.android.data.dao
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.Transformations
|
|
||||||
import androidx.room.Dao
|
import androidx.room.Dao
|
||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
|
@ -24,12 +22,8 @@ import io.timelimit.android.data.model.TemporarilyAllowedApp
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
abstract class TemporarilyAllowedAppDao {
|
abstract class TemporarilyAllowedAppDao {
|
||||||
@Query("SELECT * FROM temporarily_allowed_app WHERE device_id = :deviceId")
|
@Query("SELECT package_name FROM temporarily_allowed_app")
|
||||||
abstract fun getTemporarilyAllowedAppsInternal(deviceId: String): LiveData<List<TemporarilyAllowedApp>>
|
abstract fun getTemporarilyAllowedAppsSync(): List<String>
|
||||||
|
|
||||||
fun getTemporarilyAllowedApps(deviceId: String): LiveData<List<String>> {
|
|
||||||
return Transformations.map(getTemporarilyAllowedAppsInternal(deviceId)) { it.map { it.packageName } }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Insert
|
@Insert
|
||||||
abstract fun addTemporarilyAllowedAppSync(app: TemporarilyAllowedApp)
|
abstract fun addTemporarilyAllowedAppSync(app: TemporarilyAllowedApp)
|
||||||
|
|
|
@ -57,6 +57,9 @@ abstract class UsedTimeDao {
|
||||||
@Query("SELECT * FROM used_time WHERE category_id IN (:categoryIds) AND day_of_epoch >= :startingDayOfEpoch AND day_of_epoch <= :endDayOfEpoch")
|
@Query("SELECT * FROM used_time WHERE category_id IN (:categoryIds) AND day_of_epoch >= :startingDayOfEpoch AND day_of_epoch <= :endDayOfEpoch")
|
||||||
abstract fun getUsedTimesByDayAndCategoryIds(categoryIds: List<String>, startingDayOfEpoch: Int, endDayOfEpoch: Int): LiveData<List<UsedTimeItem>>
|
abstract fun getUsedTimesByDayAndCategoryIds(categoryIds: List<String>, startingDayOfEpoch: Int, endDayOfEpoch: Int): LiveData<List<UsedTimeItem>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM used_time WHERE category_id = :categoryId")
|
||||||
|
abstract fun getUsedTimeItemsByCategoryId(categoryId: String): List<UsedTimeItem>
|
||||||
|
|
||||||
@Query("SELECT * FROM used_time")
|
@Query("SELECT * FROM used_time")
|
||||||
abstract fun getAllUsedTimeItemsSync(): List<UsedTimeItem>
|
abstract fun getAllUsedTimeItemsSync(): List<UsedTimeItem>
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,9 @@
|
||||||
package io.timelimit.android.data.extensions
|
package io.timelimit.android.data.extensions
|
||||||
|
|
||||||
import io.timelimit.android.data.model.Category
|
import io.timelimit.android.data.model.Category
|
||||||
|
import io.timelimit.android.data.model.derived.CategoryRelatedData
|
||||||
|
|
||||||
|
// TODO: remove this
|
||||||
fun List<Category>.sorted(): List<Category> {
|
fun List<Category>.sorted(): List<Category> {
|
||||||
val categoryIds = this.map { it.id }.toSet()
|
val categoryIds = this.map { it.id }.toSet()
|
||||||
|
|
||||||
|
@ -31,5 +33,22 @@ fun List<Category>.sorted(): List<Category> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return sortedCategories.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun List<CategoryRelatedData>.sortedCategories(): List<CategoryRelatedData> {
|
||||||
|
val categoryIds = this.map { it.category.id }.toSet()
|
||||||
|
|
||||||
|
val sortedCategories = mutableListOf<CategoryRelatedData>()
|
||||||
|
val childCategories = this.filter { categoryIds.contains(it.category.parentCategoryId) }.groupBy { it.category.parentCategoryId }
|
||||||
|
|
||||||
|
this.filterNot { categoryIds.contains(it.category.parentCategoryId) }.sortedBy { it.category.sort }.forEach { category ->
|
||||||
|
sortedCategories.add(category)
|
||||||
|
|
||||||
|
childCategories[category.category.id]?.sortedBy { it.category.sort }?.let { items ->
|
||||||
|
sortedCategories.addAll(items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return sortedCategories.toList()
|
return sortedCategories.toList()
|
||||||
}
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -13,17 +13,9 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
package io.timelimit.android.logic.extension
|
|
||||||
|
|
||||||
import io.timelimit.android.data.model.Category
|
package io.timelimit.android.data.invalidation
|
||||||
import io.timelimit.android.integration.platform.BatteryStatus
|
|
||||||
|
|
||||||
fun BatteryStatus.isCategoryAllowed(category: Category?): Boolean {
|
interface Observer {
|
||||||
return if (category == null) {
|
fun onInvalidated(tables: Set<Table>)
|
||||||
true
|
|
||||||
} else if (this.charging) {
|
|
||||||
this.level >= category.minBatteryLevelWhileCharging
|
|
||||||
} else {
|
|
||||||
this.level >= category.minBatteryLevelMobile
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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?
|
||||||
|
)
|
|
@ -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
|
||||||
|
}
|
|
@ -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)) }
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -15,11 +15,12 @@
|
||||||
*/
|
*/
|
||||||
package io.timelimit.android.date
|
package io.timelimit.android.date
|
||||||
|
|
||||||
|
import org.threeten.bp.DayOfWeek
|
||||||
import org.threeten.bp.LocalDate
|
import org.threeten.bp.LocalDate
|
||||||
import org.threeten.bp.temporal.ChronoUnit
|
import org.threeten.bp.temporal.ChronoUnit
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
data class DateInTimezone(val dayOfWeek: Int, val dayOfEpoch: Int) {
|
data class DateInTimezone(val dayOfWeek: Int, val dayOfEpoch: Int, val localDate: LocalDate) {
|
||||||
companion object {
|
companion object {
|
||||||
fun convertDayOfWeek(dayOfWeek: Int) = when(dayOfWeek) {
|
fun convertDayOfWeek(dayOfWeek: Int) = when(dayOfWeek) {
|
||||||
Calendar.MONDAY -> 0
|
Calendar.MONDAY -> 0
|
||||||
|
@ -32,7 +33,18 @@ data class DateInTimezone(val dayOfWeek: Int, val dayOfEpoch: Int) {
|
||||||
else -> throw IllegalStateException()
|
else -> throw IllegalStateException()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun newInstance(timeInMillis: Long, timeZone: TimeZone): DateInTimezone {
|
fun convertDayOfWeek(dayOfWeek: DayOfWeek) = when(dayOfWeek) {
|
||||||
|
DayOfWeek.MONDAY -> 0
|
||||||
|
DayOfWeek.TUESDAY -> 1
|
||||||
|
DayOfWeek.WEDNESDAY -> 2
|
||||||
|
DayOfWeek.THURSDAY -> 3
|
||||||
|
DayOfWeek.FRIDAY -> 4
|
||||||
|
DayOfWeek.SATURDAY -> 5
|
||||||
|
DayOfWeek.SUNDAY -> 6
|
||||||
|
else -> throw IllegalStateException()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLocalDate(timeInMillis: Long, timeZone: TimeZone): LocalDate {
|
||||||
val calendar = CalendarCache.getCalendar()
|
val calendar = CalendarCache.getCalendar()
|
||||||
|
|
||||||
calendar.firstDayOfWeek = Calendar.MONDAY
|
calendar.firstDayOfWeek = Calendar.MONDAY
|
||||||
|
@ -40,17 +52,19 @@ data class DateInTimezone(val dayOfWeek: Int, val dayOfEpoch: Int) {
|
||||||
calendar.timeZone = timeZone
|
calendar.timeZone = timeZone
|
||||||
calendar.timeInMillis = timeInMillis
|
calendar.timeInMillis = timeInMillis
|
||||||
|
|
||||||
val dayOfWeek = convertDayOfWeek(calendar.get(Calendar.DAY_OF_WEEK))
|
return LocalDate.of(
|
||||||
|
|
||||||
val localDate = LocalDate.of(
|
|
||||||
calendar.get(Calendar.YEAR),
|
calendar.get(Calendar.YEAR),
|
||||||
calendar.get(Calendar.MONTH) + 1,
|
calendar.get(Calendar.MONTH) + 1,
|
||||||
calendar.get(Calendar.DAY_OF_MONTH)
|
calendar.get(Calendar.DAY_OF_MONTH)
|
||||||
)
|
)
|
||||||
|
|
||||||
val dayOfEpoch = ChronoUnit.DAYS.between(LocalDate.ofEpochDay(0), localDate).toInt()
|
|
||||||
|
|
||||||
return DateInTimezone(dayOfWeek, dayOfEpoch)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun newInstance(localDate: LocalDate): DateInTimezone = DateInTimezone(
|
||||||
|
dayOfEpoch = localDate.toEpochDay().toInt(),
|
||||||
|
dayOfWeek = convertDayOfWeek(localDate.dayOfWeek),
|
||||||
|
localDate = localDate
|
||||||
|
)
|
||||||
|
|
||||||
|
fun newInstance(timeInMillis: Long, timeZone: TimeZone) = newInstance(getLocalDate(timeInMillis, timeZone))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,6 +73,7 @@ abstract class PlatformIntegration(
|
||||||
abstract fun setForceNetworkTime(enable: Boolean)
|
abstract fun setForceNetworkTime(enable: Boolean)
|
||||||
|
|
||||||
var installedAppsChangeListener: Runnable? = null
|
var installedAppsChangeListener: Runnable? = null
|
||||||
|
var systemClockChangeListener: Runnable? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ForegroundAppSpec(var packageName: String?, var activityName: String?) {
|
data class ForegroundAppSpec(var packageName: String?, var activityName: String?) {
|
||||||
|
|
|
@ -87,6 +87,12 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio
|
||||||
installedAppsChangeListener?.run()
|
installedAppsChangeListener?.run()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
context.registerReceiver(object: BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
|
systemClockChangeListener?.run()
|
||||||
|
}
|
||||||
|
}, IntentFilter(Intent.ACTION_TIME_CHANGED))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLocalApps(deviceId: String): Collection<App> {
|
override fun getLocalApps(deviceId: String): Collection<App> {
|
||||||
|
|
|
@ -27,9 +27,13 @@ import android.util.Log
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import io.timelimit.android.BuildConfig
|
import io.timelimit.android.BuildConfig
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
|
import io.timelimit.android.async.Threads
|
||||||
|
import io.timelimit.android.coroutines.executeAndWait
|
||||||
import io.timelimit.android.coroutines.runAsync
|
import io.timelimit.android.coroutines.runAsync
|
||||||
import io.timelimit.android.livedata.waitForNonNullValue
|
import io.timelimit.android.data.model.UserType
|
||||||
import io.timelimit.android.logic.*
|
import io.timelimit.android.logic.*
|
||||||
|
import io.timelimit.android.logic.blockingreason.AppBaseHandling
|
||||||
|
import io.timelimit.android.logic.blockingreason.CategoryItselfHandling
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
class NotificationListener: NotificationListenerService() {
|
class NotificationListener: NotificationListenerService() {
|
||||||
|
@ -39,7 +43,6 @@ class NotificationListener: NotificationListenerService() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private val appLogic: AppLogic by lazy { DefaultAppLogic.with(this) }
|
private val appLogic: AppLogic by lazy { DefaultAppLogic.with(this) }
|
||||||
private val blockingReasonUtil: BlockingReasonUtil by lazy { BlockingReasonUtil(appLogic) }
|
|
||||||
private val notificationManager: NotificationManager by lazy { getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager }
|
private val notificationManager: NotificationManager by lazy { getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager }
|
||||||
private val queryAppTitleCache: QueryAppTitleCache by lazy { QueryAppTitleCache(appLogic.platformIntegration) }
|
private val queryAppTitleCache: QueryAppTitleCache by lazy { QueryAppTitleCache(appLogic.platformIntegration) }
|
||||||
private val lastOngoingNotificationHidden = mutableSetOf<String>()
|
private val lastOngoingNotificationHidden = mutableSetOf<String>()
|
||||||
|
@ -140,31 +143,53 @@ class NotificationListener: NotificationListenerService() {
|
||||||
return BlockingReason.None
|
return BlockingReason.None
|
||||||
}
|
}
|
||||||
|
|
||||||
val blockingReason = blockingReasonUtil.getBlockingReason(
|
val deviceAndUserRelatedData = Threads.database.executeAndWait {
|
||||||
packageName = sbn.packageName,
|
appLogic.database.derivedDataDao().getUserAndDeviceRelatedDataSync()
|
||||||
activityName = null
|
|
||||||
).waitForNonNullValue()
|
|
||||||
|
|
||||||
if (blockingReason.areNotificationsBlocked) {
|
|
||||||
if (BuildConfig.DEBUG) {
|
|
||||||
Log.d(LOG_TAG, "blocking notification of ${sbn.packageName} because notifications are blocked")
|
|
||||||
}
|
|
||||||
|
|
||||||
return BlockingReason.NotificationsAreBlocked
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return when (blockingReason) {
|
return if (deviceAndUserRelatedData?.userRelatedData?.user?.type != UserType.Child) {
|
||||||
is NoBlockingReason -> BlockingReason.None
|
BlockingReason.None
|
||||||
is BlockedReasonDetails -> {
|
} else {
|
||||||
if (isSystemApp(sbn.packageName) && blockingReason.reason == BlockingReason.NotPartOfAnCategory) {
|
val appHandling = AppBaseHandling.calculate(
|
||||||
return BlockingReason.None
|
foregroundAppPackageName = sbn.packageName,
|
||||||
|
foregroundAppActivityName = null,
|
||||||
|
pauseCounting = false,
|
||||||
|
pauseForegroundAppBackgroundLoop = false,
|
||||||
|
userRelatedData = deviceAndUserRelatedData.userRelatedData,
|
||||||
|
deviceRelatedData = deviceAndUserRelatedData.deviceRelatedData
|
||||||
|
)
|
||||||
|
|
||||||
|
if (appHandling is AppBaseHandling.BlockDueToNoCategory && !isSystemApp(sbn.packageName)) {
|
||||||
|
BlockingReason.NotPartOfAnCategory
|
||||||
|
} else if (appHandling is AppBaseHandling.UseCategories) {
|
||||||
|
val time = RealTime.newInstance()
|
||||||
|
val battery = appLogic.platformIntegration.getBatteryStatus()
|
||||||
|
val allowNotificationFilter = deviceAndUserRelatedData.deviceRelatedData.isConnectedAndHasPremium || deviceAndUserRelatedData.deviceRelatedData.isLocalMode
|
||||||
|
|
||||||
|
appLogic.realTimeLogic.getRealTime(time)
|
||||||
|
|
||||||
|
val categoryHandlings = appHandling.categoryIds.map { categoryId ->
|
||||||
|
CategoryItselfHandling.calculate(
|
||||||
|
categoryRelatedData = deviceAndUserRelatedData.userRelatedData.categoryById[categoryId]!!,
|
||||||
|
user = deviceAndUserRelatedData.userRelatedData,
|
||||||
|
assumeCurrentDevice = CurrentDeviceLogic.handleDeviceAsCurrentDevice(
|
||||||
|
device = deviceAndUserRelatedData.deviceRelatedData,
|
||||||
|
user = deviceAndUserRelatedData.userRelatedData
|
||||||
|
),
|
||||||
|
batteryStatus = battery,
|
||||||
|
shouldTrustTimeTemporarily = time.shouldTrustTimeTemporarily,
|
||||||
|
timeInMillis = time.timeInMillis
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (BuildConfig.DEBUG) {
|
if (allowNotificationFilter && categoryHandlings.find { it.blockAllNotifications } != null) {
|
||||||
Log.d(LOG_TAG, "blocking notification of ${sbn.packageName} because ${blockingReason.reason}")
|
BlockingReason.NotificationsAreBlocked
|
||||||
|
} else {
|
||||||
|
categoryHandlings.find { it.shouldBlockActivities }?.activityBlockingReason
|
||||||
|
?: BlockingReason.None
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
return blockingReason.reason
|
BlockingReason.None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,6 +42,8 @@ class DummyTimeApi(var timeStepSizeInMillis: Long): TimeApi() {
|
||||||
scheduledActions.add(ScheduledAction(currentUptime + delayInMillis, runnable))
|
scheduledActions.add(ScheduledAction(currentUptime + delayInMillis, runnable))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun runDelayedByUptime(runnable: Runnable, delayInMillis: Long) = runDelayed(runnable, delayInMillis)
|
||||||
|
|
||||||
override fun cancelScheduledAction(runnable: Runnable) {
|
override fun cancelScheduledAction(runnable: Runnable) {
|
||||||
scheduledActions.removeAll { it.action === runnable }
|
scheduledActions.removeAll { it.action === runnable }
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -21,7 +21,47 @@ import android.os.SystemClock
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
object RealTimeApi: TimeApi() {
|
object RealTimeApi: TimeApi() {
|
||||||
|
internal class QueueItem(val runnable: Runnable, val targetUptime: Long): Comparable<QueueItem> {
|
||||||
|
override fun compareTo(other: QueueItem): Int = this.targetUptime.compareTo(other.targetUptime)
|
||||||
|
}
|
||||||
|
|
||||||
private val handler = Handler(Looper.getMainLooper())
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
|
private val queue = PriorityQueue<QueueItem>()
|
||||||
|
|
||||||
|
// why this? because the handler does not use elapsedRealtime
|
||||||
|
// this is a workaround
|
||||||
|
private val queueProcessor = Runnable {
|
||||||
|
synchronized(queue) {
|
||||||
|
try {
|
||||||
|
val now = getCurrentUptimeInMillis()
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
val head = queue.peek()
|
||||||
|
|
||||||
|
if (head == null || head.targetUptime > now) break
|
||||||
|
|
||||||
|
queue.remove()
|
||||||
|
|
||||||
|
head.runnable.run()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
scheduleQueue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scheduleQueue() {
|
||||||
|
synchronized(queue) {
|
||||||
|
handler.removeCallbacks(queueProcessor)
|
||||||
|
|
||||||
|
queue.peek()?.let { head ->
|
||||||
|
val delay = head.targetUptime - getCurrentTimeInMillis()
|
||||||
|
|
||||||
|
// at most 5 seconds so that sleeps don't cause trouble
|
||||||
|
handler.postDelayed(queueProcessor, delay.coerceAtLeast(0).coerceAtMost(5 * 1000))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun getCurrentTimeInMillis(): Long {
|
override fun getCurrentTimeInMillis(): Long {
|
||||||
return System.currentTimeMillis()
|
return System.currentTimeMillis()
|
||||||
|
@ -35,8 +75,21 @@ object RealTimeApi: TimeApi() {
|
||||||
handler.postDelayed(runnable, delayInMillis)
|
handler.postDelayed(runnable, delayInMillis)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun runDelayedByUptime(runnable: Runnable, delayInMillis: Long) {
|
||||||
|
synchronized(queue) {
|
||||||
|
queue.add(QueueItem(runnable = runnable, targetUptime = getCurrentUptimeInMillis() + delayInMillis))
|
||||||
|
scheduleQueue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun cancelScheduledAction(runnable: Runnable) {
|
override fun cancelScheduledAction(runnable: Runnable) {
|
||||||
handler.removeCallbacks(runnable)
|
handler.removeCallbacks(runnable)
|
||||||
|
|
||||||
|
synchronized(queue) {
|
||||||
|
val itemsToRemove = queue.filter { it.runnable === runnable }
|
||||||
|
|
||||||
|
itemsToRemove.forEach { queue.remove(it) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getSystemTimeZone() = TimeZone.getDefault()
|
override fun getSystemTimeZone() = TimeZone.getDefault()
|
||||||
|
|
|
@ -26,6 +26,7 @@ abstract class TimeApi {
|
||||||
abstract fun getCurrentUptimeInMillis(): Long
|
abstract fun getCurrentUptimeInMillis(): Long
|
||||||
// function to run something delayed at the UI Thread
|
// function to run something delayed at the UI Thread
|
||||||
abstract fun runDelayed(runnable: Runnable, delayInMillis: Long)
|
abstract fun runDelayed(runnable: Runnable, delayInMillis: Long)
|
||||||
|
abstract fun runDelayedByUptime(runnable: Runnable, delayInMillis: Long)
|
||||||
abstract fun cancelScheduledAction(runnable: Runnable)
|
abstract fun cancelScheduledAction(runnable: Runnable)
|
||||||
suspend fun sleep(timeInMillis: Long) = suspendCoroutine<Void?> {
|
suspend fun sleep(timeInMillis: Long) = suspendCoroutine<Void?> {
|
||||||
runDelayed(Runnable {
|
runDelayed(Runnable {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -15,24 +15,28 @@
|
||||||
*/
|
*/
|
||||||
package io.timelimit.android.logic
|
package io.timelimit.android.logic
|
||||||
|
|
||||||
|
import io.timelimit.android.async.Threads
|
||||||
|
import io.timelimit.android.coroutines.executeAndWait
|
||||||
import io.timelimit.android.data.model.UserType
|
import io.timelimit.android.data.model.UserType
|
||||||
import io.timelimit.android.integration.platform.ForegroundAppSpec
|
import io.timelimit.android.integration.platform.ForegroundAppSpec
|
||||||
import io.timelimit.android.livedata.waitForNonNullValue
|
import io.timelimit.android.livedata.waitForNonNullValue
|
||||||
import io.timelimit.android.livedata.waitForNullableValue
|
import io.timelimit.android.livedata.waitForNullableValue
|
||||||
|
import io.timelimit.android.logic.blockingreason.AppBaseHandling
|
||||||
|
|
||||||
object AppAffectedByPrimaryDeviceUtil {
|
object AppAffectedByPrimaryDeviceUtil {
|
||||||
suspend fun isCurrentAppAffectedByPrimaryDevice(
|
suspend fun isCurrentAppAffectedByPrimaryDevice(
|
||||||
logic: AppLogic
|
logic: AppLogic
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val user = logic.deviceUserEntry.waitForNullableValue()
|
val deviceAndUserRelatedData = Threads.database.executeAndWait {
|
||||||
?: throw NullPointerException("no user is signed in")
|
logic.database.derivedDataDao().getUserAndDeviceRelatedDataSync()
|
||||||
|
|
||||||
if (user.type != UserType.Child) {
|
|
||||||
throw IllegalStateException("no child is signed in")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.relaxPrimaryDevice) {
|
if (deviceAndUserRelatedData?.userRelatedData?.user?.type != UserType.Child) {
|
||||||
if (logic.fullVersion.shouldProvideFullVersionFunctions.waitForNonNullValue() == true) {
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deviceAndUserRelatedData.userRelatedData.user.relaxPrimaryDevice) {
|
||||||
|
if (deviceAndUserRelatedData.deviceRelatedData.isConnectedAndHasPremium) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,50 +53,26 @@ object AppAffectedByPrimaryDeviceUtil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
val categories = logic.database.category().getCategoriesByChildId(user.id).waitForNonNullValue()
|
val handling = AppBaseHandling.calculate(
|
||||||
val categoryId = run {
|
foregroundAppPackageName = currentApp.packageName,
|
||||||
val categoryIdAtAppLevel = logic.database.categoryApp().getCategoryApp(
|
foregroundAppActivityName = currentApp.activityName,
|
||||||
categoryIds = categories.map { it.id },
|
deviceRelatedData = deviceAndUserRelatedData.deviceRelatedData,
|
||||||
packageName = currentApp.packageName!!
|
userRelatedData = deviceAndUserRelatedData.userRelatedData,
|
||||||
).waitForNullableValue()?.categoryId
|
pauseCounting = false,
|
||||||
|
pauseForegroundAppBackgroundLoop = false
|
||||||
|
)
|
||||||
|
|
||||||
if (logic.deviceEntry.waitForNullableValue()?.enableActivityLevelBlocking == true) {
|
if (!(handling is AppBaseHandling.UseCategories)) {
|
||||||
val categoryIdAtActivityLevel = logic.database.categoryApp().getCategoryApp(
|
|
||||||
categoryIds = categories.map { it.id },
|
|
||||||
packageName = "${currentApp.packageName}:${currentApp.activityName}"
|
|
||||||
).waitForNullableValue()?.categoryId
|
|
||||||
|
|
||||||
categoryIdAtActivityLevel ?: categoryIdAtAppLevel
|
|
||||||
} else {
|
|
||||||
categoryIdAtAppLevel
|
|
||||||
}
|
|
||||||
} ?: user.categoryForNotAssignedApps
|
|
||||||
|
|
||||||
val category = categories.find { it.id == categoryId }
|
|
||||||
val parentCategory = categories.find { it.id == category?.parentCategoryId }
|
|
||||||
|
|
||||||
if (category == null) {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// check blocked time areas
|
return handling.categoryIds.find { categoryId ->
|
||||||
if (
|
val category = deviceAndUserRelatedData.userRelatedData.categoryById[categoryId]!!
|
||||||
(category.blockedMinutesInWeek.dataNotToModify.isEmpty == false) ||
|
|
||||||
(parentCategory?.blockedMinutesInWeek?.dataNotToModify?.isEmpty == false)
|
|
||||||
) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// check time limit rules
|
val hasBlockedTimeAreas = !category.category.blockedMinutesInWeek.dataNotToModify.isEmpty
|
||||||
val rules = logic.database.timeLimitRules().getTimeLimitRulesByCategories(
|
val hasRules = category.rules.isNotEmpty()
|
||||||
categoryIds = listOf(categoryId) +
|
|
||||||
(if (parentCategory != null) listOf(parentCategory.id) else emptyList())
|
|
||||||
).waitForNonNullValue()
|
|
||||||
|
|
||||||
if (rules.isNotEmpty()) {
|
hasBlockedTimeAreas || hasRules
|
||||||
return true
|
} != null
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -26,16 +26,15 @@ import io.timelimit.android.coroutines.runAsync
|
||||||
import io.timelimit.android.coroutines.runAsyncExpectForever
|
import io.timelimit.android.coroutines.runAsyncExpectForever
|
||||||
import io.timelimit.android.data.backup.DatabaseBackup
|
import io.timelimit.android.data.backup.DatabaseBackup
|
||||||
import io.timelimit.android.data.model.*
|
import io.timelimit.android.data.model.*
|
||||||
|
import io.timelimit.android.data.model.derived.UserRelatedData
|
||||||
import io.timelimit.android.date.DateInTimezone
|
import io.timelimit.android.date.DateInTimezone
|
||||||
import io.timelimit.android.date.getMinuteOfWeek
|
|
||||||
import io.timelimit.android.extensions.MinuteOfDay
|
|
||||||
import io.timelimit.android.integration.platform.AppStatusMessage
|
import io.timelimit.android.integration.platform.AppStatusMessage
|
||||||
import io.timelimit.android.integration.platform.ForegroundAppSpec
|
import io.timelimit.android.integration.platform.ForegroundAppSpec
|
||||||
import io.timelimit.android.integration.platform.ProtectionLevel
|
import io.timelimit.android.integration.platform.ProtectionLevel
|
||||||
import io.timelimit.android.integration.platform.android.AccessibilityService
|
import io.timelimit.android.integration.platform.android.AccessibilityService
|
||||||
import io.timelimit.android.livedata.*
|
import io.timelimit.android.livedata.*
|
||||||
import io.timelimit.android.sync.actions.AddUsedTimeActionItemAdditionalCountingSlot
|
import io.timelimit.android.logic.blockingreason.AppBaseHandling
|
||||||
import io.timelimit.android.sync.actions.AddUsedTimeActionItemSessionDurationLimitSlot
|
import io.timelimit.android.logic.blockingreason.CategoryHandlingCache
|
||||||
import io.timelimit.android.sync.actions.UpdateDeviceStatusAction
|
import io.timelimit.android.sync.actions.UpdateDeviceStatusAction
|
||||||
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
||||||
import io.timelimit.android.ui.IsAppInForeground
|
import io.timelimit.android.ui.IsAppInForeground
|
||||||
|
@ -64,14 +63,6 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
private const val MAX_USED_TIME_PER_ROUND_LONG = 2000 // 1 second
|
private const val MAX_USED_TIME_PER_ROUND_LONG = 2000 // 1 second
|
||||||
}
|
}
|
||||||
|
|
||||||
private val temporarilyAllowedApps = appLogic.deviceId.switchMap {
|
|
||||||
if (it != null) {
|
|
||||||
appLogic.database.temporarilyAllowedApp().getTemporarilyAllowedApps(it)
|
|
||||||
} else {
|
|
||||||
liveDataFromValue(Collections.emptyList<String>())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
runAsyncExpectForever { backgroundServiceLoop() }
|
runAsyncExpectForever { backgroundServiceLoop() }
|
||||||
runAsyncExpectForever { syncDeviceStatusLoop() }
|
runAsyncExpectForever { syncDeviceStatusLoop() }
|
||||||
|
@ -96,10 +87,6 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
temporarilyAllowedApps.map { it.isNotEmpty() }.ignoreUnchanged().observeForever {
|
|
||||||
appLogic.platformIntegration.setShowNotificationToRevokeTemporarilyAllowedApps(it!!)
|
|
||||||
}
|
|
||||||
|
|
||||||
appLogic.database.config().getEnableBackgroundSyncAsync().ignoreUnchanged().observeForever {
|
appLogic.database.config().getEnableBackgroundSyncAsync().ignoreUnchanged().observeForever {
|
||||||
if (it) {
|
if (it) {
|
||||||
PeriodicSyncInBackgroundWorker.enable()
|
PeriodicSyncInBackgroundWorker.enable()
|
||||||
|
@ -121,15 +108,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val cache = BackgroundTaskLogicCache(appLogic)
|
private val usedTimeUpdateHelper = UsedTimeUpdateHelper(appLogic)
|
||||||
private val deviceUserEntryLive = cache.deviceUserEntryLive
|
|
||||||
private val childCategories = cache.childCategories
|
|
||||||
private val timeLimitRules = cache.timeLimitRules
|
|
||||||
private val usedTimesOfCategoryAndWeekByFirstDayOfWeek = cache.usedTimesOfCategoryAndWeekByFirstDayOfWeek
|
|
||||||
private val shouldDoAutomaticSignOut = cache.shouldDoAutomaticSignOut
|
|
||||||
private val liveDataCaches = cache.liveDataCaches
|
|
||||||
|
|
||||||
private var usedTimeUpdateHelper: UsedTimeUpdateHelper? = null
|
|
||||||
private var previousMainLogicExecutionTime = 0
|
private var previousMainLogicExecutionTime = 0
|
||||||
private var previousMainLoopEndTime = 0L
|
private var previousMainLoopEndTime = 0L
|
||||||
private val dayChangeTracker = DayChangeTracker(
|
private val dayChangeTracker = DayChangeTracker(
|
||||||
|
@ -138,6 +117,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
)
|
)
|
||||||
|
|
||||||
private val appTitleCache = QueryAppTitleCache(appLogic.platformIntegration)
|
private val appTitleCache = QueryAppTitleCache(appLogic.platformIntegration)
|
||||||
|
private val categoryHandlingCache = CategoryHandlingCache()
|
||||||
|
|
||||||
private val isChromeOs = appLogic.context.packageManager.hasSystemFeature(PackageManager.FEATURE_PC)
|
private val isChromeOs = appLogic.context.packageManager.hasSystemFeature(PackageManager.FEATURE_PC)
|
||||||
|
|
||||||
|
@ -167,12 +147,19 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
appLogic.platformIntegration.showAppLockScreen(blockedAppPackageName, blockedAppActivityName)
|
appLogic.platformIntegration.showAppLockScreen(blockedAppPackageName, blockedAppActivityName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var showNotificationToRevokeTemporarilyAllowedApps: Boolean? = null
|
||||||
|
|
||||||
|
private fun setShowNotificationToRevokeTemporarilyAllowedApps(show: Boolean) {
|
||||||
|
if (showNotificationToRevokeTemporarilyAllowedApps != show) {
|
||||||
|
showNotificationToRevokeTemporarilyAllowedApps = show
|
||||||
|
appLogic.platformIntegration.setShowNotificationToRevokeTemporarilyAllowedApps(show)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val foregroundAppSpec = ForegroundAppSpec.newInstance()
|
private val foregroundAppSpec = ForegroundAppSpec.newInstance()
|
||||||
val foregroundAppHandling = BackgroundTaskRestrictionLogicResult()
|
|
||||||
val audioPlaybackHandling = BackgroundTaskRestrictionLogicResult()
|
|
||||||
|
|
||||||
private suspend fun commitUsedTimeUpdaters() {
|
private suspend fun commitUsedTimeUpdaters() {
|
||||||
usedTimeUpdateHelper?.forceCommit(appLogic)
|
usedTimeUpdateHelper.flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun backgroundServiceLoop() {
|
private suspend fun backgroundServiceLoop() {
|
||||||
|
@ -192,22 +179,30 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
// app must be enabled
|
// app must be enabled
|
||||||
if (!appLogic.enable.waitForNonNullValue()) {
|
if (!appLogic.enable.waitForNonNullValue()) {
|
||||||
commitUsedTimeUpdaters()
|
commitUsedTimeUpdaters()
|
||||||
liveDataCaches.removeAllItems()
|
|
||||||
appLogic.platformIntegration.setAppStatusMessage(null)
|
appLogic.platformIntegration.setAppStatusMessage(null)
|
||||||
appLogic.platformIntegration.setShowBlockingOverlay(false)
|
appLogic.platformIntegration.setShowBlockingOverlay(false)
|
||||||
|
setShowNotificationToRevokeTemporarilyAllowedApps(false)
|
||||||
appLogic.enable.waitUntilValueMatches { it == true }
|
appLogic.enable.waitUntilValueMatches { it == true }
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val deviceAndUSerRelatedData = Threads.database.executeAndWait {
|
||||||
|
appLogic.database.derivedDataDao().getUserAndDeviceRelatedDataSync()
|
||||||
|
}
|
||||||
|
|
||||||
|
val deviceRelatedData = deviceAndUSerRelatedData?.deviceRelatedData
|
||||||
|
val userRelatedData = deviceAndUSerRelatedData?.userRelatedData
|
||||||
|
|
||||||
|
setShowNotificationToRevokeTemporarilyAllowedApps(deviceRelatedData?.temporarilyAllowedApps?.isNotEmpty() ?: false)
|
||||||
|
|
||||||
// device must be used by a child
|
// device must be used by a child
|
||||||
val deviceUserEntry = deviceUserEntryLive.read().waitForNullableValue()
|
if (deviceRelatedData == null || userRelatedData == null || userRelatedData.user.type != UserType.Child) {
|
||||||
|
|
||||||
if (deviceUserEntry == null || deviceUserEntry.type != UserType.Child) {
|
|
||||||
commitUsedTimeUpdaters()
|
commitUsedTimeUpdaters()
|
||||||
val shouldDoAutomaticSignOut = shouldDoAutomaticSignOut.read()
|
|
||||||
|
|
||||||
if (shouldDoAutomaticSignOut.waitForNonNullValue()) {
|
val shouldDoAutomaticSignOut = deviceRelatedData != null && DefaultUserLogic.hasAutomaticSignOut(deviceRelatedData)
|
||||||
|
|
||||||
|
if (shouldDoAutomaticSignOut) {
|
||||||
appLogic.defaultUserLogic.reportScreenOn(appLogic.platformIntegration.isScreenOn())
|
appLogic.defaultUserLogic.reportScreenOn(appLogic.platformIntegration.isScreenOn())
|
||||||
|
|
||||||
appLogic.platformIntegration.setAppStatusMessage(
|
appLogic.platformIntegration.setAppStatusMessage(
|
||||||
|
@ -219,16 +214,12 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
)
|
)
|
||||||
appLogic.platformIntegration.setShowBlockingOverlay(false)
|
appLogic.platformIntegration.setShowBlockingOverlay(false)
|
||||||
|
|
||||||
liveDataCaches.reportLoopDone()
|
|
||||||
appLogic.timeApi.sleep(backgroundServiceInterval)
|
appLogic.timeApi.sleep(backgroundServiceInterval)
|
||||||
} else {
|
} else {
|
||||||
liveDataCaches.removeAllItems()
|
|
||||||
appLogic.platformIntegration.setAppStatusMessage(null)
|
appLogic.platformIntegration.setAppStatusMessage(null)
|
||||||
appLogic.platformIntegration.setShowBlockingOverlay(false)
|
appLogic.platformIntegration.setShowBlockingOverlay(false)
|
||||||
|
|
||||||
val isChildSignedIn = deviceUserEntryLive.read().map { it != null && it.type == UserType.Child }
|
appLogic.timeApi.sleep(backgroundServiceInterval)
|
||||||
|
|
||||||
isChildSignedIn.or(shouldDoAutomaticSignOut).waitUntilValueMatches { it == true }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
continue
|
continue
|
||||||
|
@ -240,18 +231,18 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
appLogic.realTimeLogic.getRealTime(realTime)
|
appLogic.realTimeLogic.getRealTime(realTime)
|
||||||
|
|
||||||
val nowTimestamp = realTime.timeInMillis
|
val nowTimestamp = realTime.timeInMillis
|
||||||
val nowTimezone = TimeZone.getTimeZone(deviceUserEntry.timeZone)
|
val nowTimezone = TimeZone.getTimeZone(userRelatedData.user.timeZone)
|
||||||
|
|
||||||
val nowDate = DateInTimezone.newInstance(nowTimestamp, nowTimezone)
|
val nowDate = DateInTimezone.getLocalDate(nowTimestamp, nowTimezone)
|
||||||
val minuteOfWeek = getMinuteOfWeek(nowTimestamp, nowTimezone)
|
val dayOfEpoch = nowDate.toEpochDay().toInt()
|
||||||
|
|
||||||
// eventually remove old used time data
|
// eventually remove old used time data
|
||||||
if (realTime.shouldTrustTimePermanently) {
|
if (realTime.shouldTrustTimePermanently) {
|
||||||
val dayChange = dayChangeTracker.reportDayChange(nowDate.dayOfEpoch)
|
val dayChange = dayChangeTracker.reportDayChange(dayOfEpoch)
|
||||||
|
|
||||||
fun deleteOldUsedTimes() = UsedTimeDeleter.deleteOldUsedTimeItems(
|
fun deleteOldUsedTimes() = UsedTimeDeleter.deleteOldUsedTimeItems(
|
||||||
database = appLogic.database,
|
database = appLogic.database,
|
||||||
date = nowDate,
|
date = DateInTimezone.newInstance(nowDate),
|
||||||
timestamp = nowTimestamp
|
timestamp = nowTimestamp
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -266,10 +257,6 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the categories
|
|
||||||
val categories = childCategories.get(deviceUserEntry.id).waitForNonNullValue()
|
|
||||||
val temporarilyAllowedApps = temporarilyAllowedApps.waitForNonNullValue()
|
|
||||||
|
|
||||||
// get the current status
|
// get the current status
|
||||||
val isScreenOn = appLogic.platformIntegration.isScreenOn()
|
val isScreenOn = appLogic.platformIntegration.isScreenOn()
|
||||||
val batteryStatus = appLogic.platformIntegration.getBatteryStatus()
|
val batteryStatus = appLogic.platformIntegration.getBatteryStatus()
|
||||||
|
@ -277,293 +264,104 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
appLogic.defaultUserLogic.reportScreenOn(isScreenOn)
|
appLogic.defaultUserLogic.reportScreenOn(isScreenOn)
|
||||||
|
|
||||||
if (!isScreenOn) {
|
if (!isScreenOn) {
|
||||||
if (temporarilyAllowedApps.isNotEmpty()) {
|
if (deviceRelatedData.temporarilyAllowedApps.isNotEmpty()) {
|
||||||
resetTemporarilyAllowedApps()
|
resetTemporarilyAllowedApps()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun reportStatusToCategoryHandlingCache(userRelatedData: UserRelatedData) {
|
||||||
|
categoryHandlingCache.reportStatus(
|
||||||
|
user = userRelatedData,
|
||||||
|
timeInMillis = nowTimestamp,
|
||||||
|
shouldTrustTimeTemporarily = realTime.shouldTrustTimeTemporarily,
|
||||||
|
assumeCurrentDevice = CurrentDeviceLogic.handleDeviceAsCurrentDevice(deviceRelatedData, userRelatedData),
|
||||||
|
batteryStatus = batteryStatus
|
||||||
|
)
|
||||||
|
}; reportStatusToCategoryHandlingCache(userRelatedData)
|
||||||
|
|
||||||
appLogic.platformIntegration.getForegroundApp(foregroundAppSpec, appLogic.getForegroundAppQueryInterval())
|
appLogic.platformIntegration.getForegroundApp(foregroundAppSpec, appLogic.getForegroundAppQueryInterval())
|
||||||
val foregroundAppPackageName = foregroundAppSpec.packageName
|
val foregroundAppPackageName = foregroundAppSpec.packageName
|
||||||
val foregroundAppActivityName = foregroundAppSpec.activityName
|
val foregroundAppActivityName = foregroundAppSpec.activityName
|
||||||
val audioPlaybackPackageName = appLogic.platformIntegration.getMusicPlaybackPackage()
|
val audioPlaybackPackageName = appLogic.platformIntegration.getMusicPlaybackPackage()
|
||||||
val activityLevelBlocking = appLogic.deviceEntry.value?.enableActivityLevelBlocking ?: false
|
val activityLevelBlocking = appLogic.deviceEntry.value?.enableActivityLevelBlocking ?: false
|
||||||
|
|
||||||
foregroundAppHandling.reset()
|
val foregroundAppBaseHandling = AppBaseHandling.calculate(
|
||||||
audioPlaybackHandling.reset()
|
|
||||||
|
|
||||||
BackgroundTaskRestrictionLogic.getHandling(
|
|
||||||
foregroundAppPackageName = foregroundAppPackageName,
|
foregroundAppPackageName = foregroundAppPackageName,
|
||||||
foregroundAppActivityName = foregroundAppActivityName,
|
foregroundAppActivityName = foregroundAppActivityName,
|
||||||
pauseForegroundAppBackgroundLoop = pauseForegroundAppBackgroundLoop,
|
pauseForegroundAppBackgroundLoop = pauseForegroundAppBackgroundLoop,
|
||||||
temporarilyAllowedApps = temporarilyAllowedApps,
|
userRelatedData = userRelatedData,
|
||||||
categories = categories,
|
deviceRelatedData = deviceRelatedData,
|
||||||
activityLevelBlocking = activityLevelBlocking,
|
pauseCounting = !isScreenOn
|
||||||
deviceUserEntry = deviceUserEntry,
|
|
||||||
batteryStatus = batteryStatus,
|
|
||||||
shouldTrustTimeTemporarily = realTime.shouldTrustTimeTemporarily,
|
|
||||||
nowTimestamp = nowTimestamp,
|
|
||||||
minuteOfWeek = minuteOfWeek,
|
|
||||||
cache = cache,
|
|
||||||
result = foregroundAppHandling
|
|
||||||
)
|
)
|
||||||
|
|
||||||
BackgroundTaskRestrictionLogic.getHandling(
|
val backgroundAppBaseHandling = AppBaseHandling.calculate(
|
||||||
foregroundAppPackageName = audioPlaybackPackageName,
|
foregroundAppPackageName = audioPlaybackPackageName,
|
||||||
foregroundAppActivityName = null,
|
foregroundAppActivityName = null,
|
||||||
pauseForegroundAppBackgroundLoop = false,
|
pauseForegroundAppBackgroundLoop = false,
|
||||||
temporarilyAllowedApps = temporarilyAllowedApps,
|
userRelatedData = userRelatedData,
|
||||||
categories = categories,
|
deviceRelatedData = deviceRelatedData,
|
||||||
activityLevelBlocking = activityLevelBlocking,
|
pauseCounting = false
|
||||||
deviceUserEntry = deviceUserEntry,
|
|
||||||
batteryStatus = batteryStatus,
|
|
||||||
shouldTrustTimeTemporarily = realTime.shouldTrustTimeTemporarily,
|
|
||||||
nowTimestamp = nowTimestamp,
|
|
||||||
minuteOfWeek = minuteOfWeek,
|
|
||||||
cache = cache,
|
|
||||||
result = audioPlaybackHandling
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// update used time helper if date does not match
|
// check if should be blocked
|
||||||
if (usedTimeUpdateHelper?.date != nowDate) {
|
val blockForegroundApp = foregroundAppBaseHandling is AppBaseHandling.BlockDueToNoCategory ||
|
||||||
usedTimeUpdateHelper?.forceCommit(appLogic)
|
(foregroundAppBaseHandling is AppBaseHandling.UseCategories && foregroundAppBaseHandling.categoryIds.find {
|
||||||
usedTimeUpdateHelper = UsedTimeUpdateHelper(nowDate)
|
categoryHandlingCache.get(it).shouldBlockActivities
|
||||||
}
|
} != null)
|
||||||
|
|
||||||
val usedTimeUpdateHelper = usedTimeUpdateHelper!!
|
val blockAudioPlayback = backgroundAppBaseHandling is AppBaseHandling.BlockDueToNoCategory ||
|
||||||
|
(backgroundAppBaseHandling is AppBaseHandling.UseCategories && backgroundAppBaseHandling.categoryIds.find {
|
||||||
|
val handling = categoryHandlingCache.get(it)
|
||||||
|
val hasPremium = deviceRelatedData.isLocalMode || deviceRelatedData.isConnectedAndHasPremium
|
||||||
|
val blockAllNotifications = handling.blockAllNotifications && hasPremium
|
||||||
|
|
||||||
// check times
|
handling.shouldBlockActivities || blockAllNotifications
|
||||||
fun buildDummyUsedTimeItems(categoryId: String): List<UsedTimeItem> {
|
} != null)
|
||||||
if (!usedTimeUpdateHelper.timeToAdd.containsKey(categoryId)) {
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (usedTimeUpdateHelper.additionalSlots[categoryId] ?: emptySet()).map {
|
|
||||||
UsedTimeItem(
|
|
||||||
categoryId = categoryId,
|
|
||||||
startTimeOfDay = it.start,
|
|
||||||
endTimeOfDay = it.end,
|
|
||||||
dayOfEpoch = usedTimeUpdateHelper.date.dayOfEpoch,
|
|
||||||
usedMillis = (usedTimeUpdateHelper.timeToAdd[categoryId] ?: 0).toLong()
|
|
||||||
)
|
|
||||||
} + listOf(
|
|
||||||
UsedTimeItem(
|
|
||||||
categoryId = categoryId,
|
|
||||||
startTimeOfDay = MinuteOfDay.MIN,
|
|
||||||
endTimeOfDay = MinuteOfDay.MAX,
|
|
||||||
dayOfEpoch = usedTimeUpdateHelper.date.dayOfEpoch,
|
|
||||||
usedMillis = (usedTimeUpdateHelper.timeToAdd[categoryId] ?: 0).toLong()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getRemainingTime(categoryId: String?): RemainingTime? {
|
|
||||||
categoryId ?: return null
|
|
||||||
|
|
||||||
val category = categories.find { it.id == categoryId } ?: return null
|
|
||||||
val rules = timeLimitRules.get(category.id).waitForNonNullValue()
|
|
||||||
|
|
||||||
if (rules.isEmpty()) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
val firstDayOfWeekAsEpochDay = nowDate.dayOfEpoch - nowDate.dayOfWeek
|
|
||||||
val usedTimes = usedTimesOfCategoryAndWeekByFirstDayOfWeek.get(Pair(category.id, firstDayOfWeekAsEpochDay)).waitForNonNullValue()
|
|
||||||
|
|
||||||
return RemainingTime.getRemainingTime(
|
|
||||||
nowDate.dayOfWeek,
|
|
||||||
minuteOfWeek % MinuteOfDay.LENGTH,
|
|
||||||
usedTimes + buildDummyUsedTimeItems(categoryId),
|
|
||||||
rules,
|
|
||||||
Math.max(0, category.getExtraTime(dayOfEpoch = nowDate.dayOfEpoch) - (usedTimeUpdateHelper.extraTimeToSubtract.get(categoryId) ?: 0)),
|
|
||||||
firstDayOfWeekAsEpochDay
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getRemainingSessionDuration(categoryId: String?): Long? {
|
|
||||||
categoryId ?: return null
|
|
||||||
|
|
||||||
val category = categories.find { it.id == categoryId } ?: return null
|
|
||||||
val rules = timeLimitRules.get(category.id).waitForNonNullValue()
|
|
||||||
val durations = cache.usedSessionDurationsByCategoryId.get(categoryId).waitForNonNullValue()
|
|
||||||
val timeToAdd = usedTimeUpdateHelper.timeToAdd[categoryId] ?: 0
|
|
||||||
|
|
||||||
val result = RemainingSessionDuration.getRemainingSessionDuration(
|
|
||||||
rules = rules,
|
|
||||||
durationsOfCategory = durations,
|
|
||||||
timestamp = nowTimestamp,
|
|
||||||
dayOfWeek = nowDate.dayOfWeek,
|
|
||||||
minuteOfDay = minuteOfWeek % MinuteOfDay.LENGTH
|
|
||||||
)
|
|
||||||
|
|
||||||
if (result == null) {
|
|
||||||
return null
|
|
||||||
} else {
|
|
||||||
return (result - timeToAdd).coerceAtLeast(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// note: remainingTime != null implicates that there are limits and they are currently not ignored
|
|
||||||
val remainingTimeForegroundAppChild = if (foregroundAppHandling.status == BackgroundTaskLogicAppStatus.AllowedCountAndCheckTime) getRemainingTime(foregroundAppHandling.categoryId) else null
|
|
||||||
val remainingTimeForegroundAppParent = if (foregroundAppHandling.status == BackgroundTaskLogicAppStatus.AllowedCountAndCheckTime) getRemainingTime(foregroundAppHandling.parentCategoryId) else null
|
|
||||||
val remainingTimeForegroundApp = RemainingTime.min(remainingTimeForegroundAppChild, remainingTimeForegroundAppParent)
|
|
||||||
val remainingSessionDurationForegroundAppChild = if (foregroundAppHandling.status == BackgroundTaskLogicAppStatus.AllowedCountAndCheckTime) getRemainingSessionDuration(foregroundAppHandling.categoryId) else null
|
|
||||||
val remainingSessionDurationForegroundAppParent = if (foregroundAppHandling.status == BackgroundTaskLogicAppStatus.AllowedCountAndCheckTime) getRemainingSessionDuration(foregroundAppHandling.parentCategoryId) else null
|
|
||||||
val remainingSessionDurationForegroundApp = RemainingSessionDuration.min(remainingSessionDurationForegroundAppChild, remainingSessionDurationForegroundAppParent)
|
|
||||||
|
|
||||||
val remainingTimeBackgroundAppChild = if (audioPlaybackHandling.status == BackgroundTaskLogicAppStatus.AllowedCountAndCheckTime) getRemainingTime(audioPlaybackHandling.categoryId) else null
|
|
||||||
val remainingTimeBackgroundAppParent = if (audioPlaybackHandling.status == BackgroundTaskLogicAppStatus.AllowedCountAndCheckTime) getRemainingTime(audioPlaybackHandling.parentCategoryId) else null
|
|
||||||
val remainingTimeBackgroundApp = RemainingTime.min(remainingTimeBackgroundAppChild, remainingTimeBackgroundAppParent)
|
|
||||||
val remainingSessionDurationBackgroundAppChild = if (audioPlaybackHandling.status == BackgroundTaskLogicAppStatus.AllowedCountAndCheckTime) getRemainingSessionDuration(audioPlaybackHandling.categoryId) else null
|
|
||||||
val remainingSessionDurationBackgroundAppParent = if (audioPlaybackHandling.status == BackgroundTaskLogicAppStatus.AllowedCountAndCheckTime) getRemainingSessionDuration(audioPlaybackHandling.parentCategoryId) else null
|
|
||||||
val remainingSessionDurationBackgroundApp = RemainingSessionDuration.min(remainingSessionDurationBackgroundAppChild, remainingSessionDurationBackgroundAppParent)
|
|
||||||
|
|
||||||
val sessionDurationLimitReachedForegroundApp = (remainingSessionDurationForegroundApp != null && remainingSessionDurationForegroundApp == 0L)
|
|
||||||
val sessionDurationLimitReachedBackgroundApp = (remainingSessionDurationBackgroundApp != null && remainingSessionDurationBackgroundApp == 0L)
|
|
||||||
|
|
||||||
// eventually block
|
|
||||||
if (remainingTimeForegroundApp?.hasRemainingTime == false || sessionDurationLimitReachedForegroundApp) {
|
|
||||||
foregroundAppHandling.status = BackgroundTaskLogicAppStatus.ShouldBlock
|
|
||||||
}
|
|
||||||
|
|
||||||
if (remainingTimeBackgroundApp?.hasRemainingTime == false || sessionDurationLimitReachedBackgroundApp) {
|
|
||||||
audioPlaybackHandling.status = BackgroundTaskLogicAppStatus.ShouldBlock
|
|
||||||
}
|
|
||||||
|
|
||||||
// update times
|
// update times
|
||||||
val timeToSubtract = Math.min(previousMainLogicExecutionTime, maxUsedTimeToAdd)
|
val timeToSubtract = Math.min(previousMainLogicExecutionTime, maxUsedTimeToAdd)
|
||||||
|
|
||||||
// see note above declaration of remainingTimeForegroundAppChild
|
val categoryHandlingsToCount = AppBaseHandling.getCategoriesForCounting(foregroundAppBaseHandling, backgroundAppBaseHandling)
|
||||||
val shouldCountForegroundApp = remainingTimeForegroundApp != null && isScreenOn && remainingTimeForegroundApp.hasRemainingTime
|
.map { categoryHandlingCache.get(it) }
|
||||||
val shouldCountBackgroundApp = remainingTimeBackgroundApp != null && remainingTimeBackgroundApp.hasRemainingTime
|
.filter { it.shouldCountTime }
|
||||||
|
|
||||||
val categoriesToCount = mutableSetOf<String>()
|
if (
|
||||||
val categoriesToCountExtraTime = mutableSetOf<String>()
|
usedTimeUpdateHelper.report(
|
||||||
val categoriesToCountSessionDurations = mutableSetOf<String>()
|
duration = timeToSubtract,
|
||||||
|
dayOfEpoch = dayOfEpoch,
|
||||||
if (shouldCountForegroundApp) {
|
|
||||||
remainingTimeForegroundAppChild?.let { remainingTime ->
|
|
||||||
foregroundAppHandling.categoryId?.let { categoryId ->
|
|
||||||
categoriesToCount.add(categoryId)
|
|
||||||
|
|
||||||
if (remainingTime.usingExtraTime) {
|
|
||||||
categoriesToCountExtraTime.add(categoryId)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sessionDurationLimitReachedForegroundApp) {
|
|
||||||
categoriesToCountSessionDurations.add(categoryId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
remainingTimeForegroundAppParent?.let { remainingTime ->
|
|
||||||
foregroundAppHandling.parentCategoryId?.let {
|
|
||||||
categoriesToCount.add(it)
|
|
||||||
|
|
||||||
if (remainingTime.usingExtraTime) {
|
|
||||||
categoriesToCountExtraTime.add(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sessionDurationLimitReachedForegroundApp) {
|
|
||||||
categoriesToCountSessionDurations.add(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldCountBackgroundApp) {
|
|
||||||
remainingTimeBackgroundAppChild?.let { remainingTime ->
|
|
||||||
audioPlaybackHandling.categoryId?.let {
|
|
||||||
categoriesToCount.add(it)
|
|
||||||
|
|
||||||
if (remainingTime.usingExtraTime) {
|
|
||||||
categoriesToCountExtraTime.add(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sessionDurationLimitReachedBackgroundApp) {
|
|
||||||
categoriesToCountSessionDurations.add(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
remainingTimeBackgroundAppParent?.let { remainingTime ->
|
|
||||||
audioPlaybackHandling.parentCategoryId?.let {
|
|
||||||
categoriesToCount.add(it)
|
|
||||||
|
|
||||||
if (remainingTime.usingExtraTime) {
|
|
||||||
categoriesToCountExtraTime.add(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sessionDurationLimitReachedBackgroundApp) {
|
|
||||||
categoriesToCountSessionDurations.add(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (categoriesToCount.isNotEmpty()) {
|
|
||||||
categoriesToCount.forEach { categoryId ->
|
|
||||||
// only handle rules which are related at today and the current time
|
|
||||||
val rules = RemainingTime.getRulesRelatedToDay(
|
|
||||||
dayOfWeek = nowDate.dayOfWeek,
|
|
||||||
minuteOfDay = minuteOfWeek % MinuteOfDay.LENGTH,
|
|
||||||
rules = timeLimitRules.get(categoryId).waitForNonNullValue()
|
|
||||||
)
|
|
||||||
|
|
||||||
usedTimeUpdateHelper.add(
|
|
||||||
categoryId = categoryId,
|
|
||||||
time = timeToSubtract,
|
|
||||||
includingExtraTime = categoriesToCountExtraTime.contains(categoryId),
|
|
||||||
slots = run {
|
|
||||||
val slots = mutableSetOf<AddUsedTimeActionItemAdditionalCountingSlot>()
|
|
||||||
|
|
||||||
rules.forEach { rule ->
|
|
||||||
if (!rule.appliesToWholeDay) {
|
|
||||||
slots.add(
|
|
||||||
AddUsedTimeActionItemAdditionalCountingSlot(
|
|
||||||
rule.startMinuteOfDay, rule.endMinuteOfDay
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
slots
|
|
||||||
},
|
|
||||||
trustedTimestamp = if (realTime.shouldTrustTimePermanently) realTime.timeInMillis else 0,
|
trustedTimestamp = if (realTime.shouldTrustTimePermanently) realTime.timeInMillis else 0,
|
||||||
sessionDurationLimits = run {
|
handlings = categoryHandlingsToCount
|
||||||
val slots = mutableSetOf<AddUsedTimeActionItemSessionDurationLimitSlot>()
|
|
||||||
|
|
||||||
if (categoriesToCountSessionDurations.contains(categoryId)) {
|
|
||||||
rules.forEach { rule ->
|
|
||||||
if (rule.sessionDurationLimitEnabled) {
|
|
||||||
slots.add(
|
|
||||||
AddUsedTimeActionItemSessionDurationLimitSlot(
|
|
||||||
startMinuteOfDay = rule.startMinuteOfDay,
|
|
||||||
endMinuteOfDay = rule.endMinuteOfDay,
|
|
||||||
sessionPauseDuration = rule.sessionPauseMilliseconds,
|
|
||||||
maxSessionDuration = rule.sessionDurationMilliseconds
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
slots
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
) {
|
||||||
|
val newDeviceAndUserRelatedData = Threads.database.executeAndWait {
|
||||||
|
appLogic.database.derivedDataDao().getUserAndDeviceRelatedDataSync()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
newDeviceAndUserRelatedData?.userRelatedData?.user?.id != deviceAndUSerRelatedData.userRelatedData.user.id ||
|
||||||
|
newDeviceAndUserRelatedData.userRelatedData.categoryById.keys != deviceAndUSerRelatedData.userRelatedData.categoryById.keys
|
||||||
|
) {
|
||||||
|
// start the loop directly again
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
reportStatusToCategoryHandlingCache(userRelatedData = newDeviceAndUserRelatedData.userRelatedData)
|
||||||
}
|
}
|
||||||
|
|
||||||
usedTimeUpdateHelper.reportCurrentCategories(categoriesToCount)
|
val categoriesToCount = categoryHandlingsToCount.map { it.createdWithCategoryRelatedData.category.id }
|
||||||
|
|
||||||
if (usedTimeUpdateHelper.shouldDoAutoCommit) {
|
fun timeToSubtractForCategory(categoryId: String): Int {
|
||||||
usedTimeUpdateHelper.forceCommit(appLogic)
|
return if (usedTimeUpdateHelper.getCountedCategoryIds().contains(categoryId)) usedTimeUpdateHelper.getCountedTime() else 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// trigger time warnings
|
// trigger time warnings
|
||||||
fun eventuallyTriggerTimeWarning(remaining: RemainingTime, categoryId: String?) {
|
categoriesToCount.forEach { categoryId ->
|
||||||
val category = categories.find { it.id == categoryId } ?: return
|
val category = userRelatedData.categoryById[categoryId]!!.category
|
||||||
val oldRemainingTime = remaining.includingExtraTime
|
val handling = categoryHandlingCache.get(categoryId)
|
||||||
val newRemainingTime = oldRemainingTime - timeToSubtract
|
val nowRemaining = handling.remainingTime ?: return@forEach // category is not limited anymore
|
||||||
|
|
||||||
|
val newRemainingTime = nowRemaining.includingExtraTime - timeToSubtractForCategory(categoryId)
|
||||||
|
val oldRemainingTime = newRemainingTime + timeToSubtract
|
||||||
|
|
||||||
if (oldRemainingTime / (1000 * 60) != newRemainingTime / (1000 * 60)) {
|
if (oldRemainingTime / (1000 * 60) != newRemainingTime / (1000 * 60)) {
|
||||||
// eventually show remaining time warning
|
// eventually show remaining time warning
|
||||||
|
@ -579,11 +377,6 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
remainingTimeForegroundAppChild?.let { eventuallyTriggerTimeWarning(it, foregroundAppHandling.categoryId) }
|
|
||||||
remainingTimeForegroundAppParent?.let { eventuallyTriggerTimeWarning(it, foregroundAppHandling.parentCategoryId) }
|
|
||||||
remainingTimeBackgroundAppChild?.let { eventuallyTriggerTimeWarning(it, audioPlaybackHandling.categoryId) }
|
|
||||||
remainingTimeBackgroundAppParent?.let { eventuallyTriggerTimeWarning(it, audioPlaybackHandling.parentCategoryId) }
|
|
||||||
|
|
||||||
// show notification
|
// show notification
|
||||||
fun buildStatusMessageWithCurrentAppTitle(
|
fun buildStatusMessageWithCurrentAppTitle(
|
||||||
text: String,
|
text: String,
|
||||||
|
@ -597,107 +390,156 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
if (appActivityToShow != null && appPackageName != null) appActivityToShow.removePrefix(appPackageName) else null
|
if (appActivityToShow != null && appPackageName != null) appActivityToShow.removePrefix(appPackageName) else null
|
||||||
)
|
)
|
||||||
|
|
||||||
fun getCategoryTitle(categoryId: String?): String = categories.find { it.id == categoryId }?.title ?: categoryId.toString()
|
fun getCategoryTitle(categoryId: String?): String = categoryId.let { userRelatedData.categoryById[it]?.category?.title } ?: categoryId.toString()
|
||||||
|
|
||||||
fun buildStatusMessage(
|
fun buildNotificationForAppWithCategoryUsage(
|
||||||
handling: BackgroundTaskRestrictionLogicResult,
|
suffix: String,
|
||||||
remainingTime: RemainingTime?,
|
appPackageName: String?,
|
||||||
remainingSessionDuration: Long?,
|
appActivityToShow: String?,
|
||||||
|
categoryId: String
|
||||||
|
): AppStatusMessage {
|
||||||
|
val handling = categoryHandlingCache.get(categoryId)
|
||||||
|
|
||||||
|
return if (handling.areLimitsTemporarilyDisabled) {
|
||||||
|
buildStatusMessageWithCurrentAppTitle(
|
||||||
|
text = appLogic.context.getString(R.string.background_logic_limits_disabled),
|
||||||
|
titleSuffix = suffix,
|
||||||
|
appPackageName = appPackageName,
|
||||||
|
appActivityToShow = appActivityToShow
|
||||||
|
)
|
||||||
|
} else if (handling.remainingTime == null) {
|
||||||
|
buildStatusMessageWithCurrentAppTitle(
|
||||||
|
text = appLogic.context.getString(R.string.background_logic_no_timelimit),
|
||||||
|
titlePrefix = getCategoryTitle(categoryId) + " - ",
|
||||||
|
titleSuffix = suffix,
|
||||||
|
appPackageName = appPackageName,
|
||||||
|
appActivityToShow = appActivityToShow
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val remainingTimeFromCache = handling.remainingTime
|
||||||
|
val timeSubtractedFromThisCategory = timeToSubtractForCategory(categoryId)
|
||||||
|
val realRemainingTimeDefault = (remainingTimeFromCache.default - timeSubtractedFromThisCategory).coerceAtLeast(0)
|
||||||
|
val realRemainingTimeWithExtraTime = (remainingTimeFromCache.includingExtraTime - timeSubtractedFromThisCategory).coerceAtLeast(0)
|
||||||
|
val realRemainingTimeUsingExtraTime = realRemainingTimeDefault == 0L && realRemainingTimeWithExtraTime > 0
|
||||||
|
|
||||||
|
val remainingSessionDuration = handling.remainingSessionDuration?.let { (it - timeToSubtractForCategory(categoryId)).coerceAtLeast(0) }
|
||||||
|
|
||||||
|
buildStatusMessageWithCurrentAppTitle(
|
||||||
|
text = if (realRemainingTimeUsingExtraTime)
|
||||||
|
appLogic.context.getString(R.string.background_logic_using_extra_time, TimeTextUtil.remaining(realRemainingTimeWithExtraTime.toInt(), appLogic.context))
|
||||||
|
else if (remainingSessionDuration != null && remainingSessionDuration < realRemainingTimeDefault)
|
||||||
|
TimeTextUtil.pauseIn(remainingSessionDuration.toInt(), appLogic.context)
|
||||||
|
else
|
||||||
|
TimeTextUtil.remaining(realRemainingTimeDefault.toInt() ?: 0, appLogic.context),
|
||||||
|
titlePrefix = getCategoryTitle(categoryId) + " - ",
|
||||||
|
titleSuffix = suffix,
|
||||||
|
appPackageName = appPackageName,
|
||||||
|
appActivityToShow = appActivityToShow
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildNotificationForAppWithoutCategoryUsage(
|
||||||
|
handling: AppBaseHandling,
|
||||||
suffix: String,
|
suffix: String,
|
||||||
appPackageName: String?,
|
appPackageName: String?,
|
||||||
appActivityToShow: String?
|
appActivityToShow: String?
|
||||||
): AppStatusMessage = when (handling.status) {
|
): AppStatusMessage = when (handling) {
|
||||||
BackgroundTaskLogicAppStatus.ShouldBlock -> buildStatusMessageWithCurrentAppTitle(
|
is AppBaseHandling.UseCategories -> throw IllegalArgumentException()
|
||||||
text = appLogic.context.getString(R.string.background_logic_opening_lockscreen),
|
AppBaseHandling.BlockDueToNoCategory -> throw IllegalArgumentException()
|
||||||
titleSuffix = suffix,
|
AppBaseHandling.PauseLogic -> AppStatusMessage(
|
||||||
appPackageName = appPackageName,
|
|
||||||
appActivityToShow = appActivityToShow
|
|
||||||
)
|
|
||||||
BackgroundTaskLogicAppStatus.BackgroundLogicPaused -> AppStatusMessage(
|
|
||||||
title = appLogic.context.getString(R.string.background_logic_paused_title) + suffix,
|
title = appLogic.context.getString(R.string.background_logic_paused_title) + suffix,
|
||||||
text = appLogic.context.getString(R.string.background_logic_paused_text)
|
text = appLogic.context.getString(R.string.background_logic_paused_text)
|
||||||
)
|
)
|
||||||
BackgroundTaskLogicAppStatus.InternalWhitelist -> buildStatusMessageWithCurrentAppTitle(
|
AppBaseHandling.Whitelist -> buildStatusMessageWithCurrentAppTitle(
|
||||||
text = appLogic.context.getString(R.string.background_logic_whitelisted),
|
text = appLogic.context.getString(R.string.background_logic_whitelisted),
|
||||||
titleSuffix = suffix,
|
titleSuffix = suffix,
|
||||||
appPackageName = appPackageName,
|
appPackageName = appPackageName,
|
||||||
appActivityToShow = appActivityToShow
|
appActivityToShow = appActivityToShow
|
||||||
)
|
)
|
||||||
BackgroundTaskLogicAppStatus.TemporarilyAllowed -> buildStatusMessageWithCurrentAppTitle(
|
AppBaseHandling.TemporarilyAllowed -> buildStatusMessageWithCurrentAppTitle(
|
||||||
text = appLogic.context.getString(R.string.background_logic_temporarily_allowed),
|
text = appLogic.context.getString(R.string.background_logic_temporarily_allowed),
|
||||||
titleSuffix = suffix,
|
titleSuffix = suffix,
|
||||||
appPackageName = appPackageName,
|
appPackageName = appPackageName,
|
||||||
appActivityToShow = appActivityToShow
|
appActivityToShow = appActivityToShow
|
||||||
)
|
)
|
||||||
BackgroundTaskLogicAppStatus.LimitsDisabled -> buildStatusMessageWithCurrentAppTitle(
|
AppBaseHandling.Idle -> AppStatusMessage(
|
||||||
text = appLogic.context.getString(R.string.background_logic_limits_disabled),
|
|
||||||
titleSuffix = suffix,
|
|
||||||
appPackageName = appPackageName,
|
|
||||||
appActivityToShow = appActivityToShow
|
|
||||||
)
|
|
||||||
BackgroundTaskLogicAppStatus.AllowedNoTimelimit -> buildStatusMessageWithCurrentAppTitle(
|
|
||||||
text = appLogic.context.getString(R.string.background_logic_no_timelimit),
|
|
||||||
titlePrefix = getCategoryTitle(handling.categoryId) + " - ",
|
|
||||||
titleSuffix = suffix,
|
|
||||||
appPackageName = appPackageName,
|
|
||||||
appActivityToShow = appActivityToShow
|
|
||||||
)
|
|
||||||
BackgroundTaskLogicAppStatus.AllowedCountAndCheckTime -> buildStatusMessageWithCurrentAppTitle(
|
|
||||||
text = if (remainingTime?.usingExtraTime == true)
|
|
||||||
appLogic.context.getString(R.string.background_logic_using_extra_time, TimeTextUtil.remaining(remainingTime.includingExtraTime.toInt(), appLogic.context))
|
|
||||||
else if (remainingTime != null && remainingSessionDuration != null && remainingSessionDuration < remainingTime.default)
|
|
||||||
TimeTextUtil.pauseIn(remainingSessionDuration.toInt(), appLogic.context)
|
|
||||||
else
|
|
||||||
TimeTextUtil.remaining(remainingTime?.default?.toInt() ?: 0, appLogic.context),
|
|
||||||
titlePrefix = getCategoryTitle(handling.categoryId) + " - ",
|
|
||||||
titleSuffix = suffix,
|
|
||||||
appPackageName = appPackageName,
|
|
||||||
appActivityToShow = appActivityToShow
|
|
||||||
)
|
|
||||||
BackgroundTaskLogicAppStatus.Idle -> AppStatusMessage(
|
|
||||||
appLogic.context.getString(R.string.background_logic_idle_title) + suffix,
|
appLogic.context.getString(R.string.background_logic_idle_title) + suffix,
|
||||||
appLogic.context.getString(R.string.background_logic_idle_text)
|
appLogic.context.getString(R.string.background_logic_idle_text)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val showBackgroundStatus = audioPlaybackHandling.status != BackgroundTaskLogicAppStatus.Idle &&
|
val showBackgroundStatus = !(backgroundAppBaseHandling is AppBaseHandling.Idle) &&
|
||||||
audioPlaybackHandling.status != BackgroundTaskLogicAppStatus.ShouldBlock &&
|
!blockAudioPlayback &&
|
||||||
audioPlaybackPackageName != foregroundAppPackageName
|
audioPlaybackPackageName != foregroundAppPackageName
|
||||||
|
|
||||||
if (showBackgroundStatus && nowTimestamp % 6000 >= 3000) {
|
val statusMessage = if (blockForegroundApp) {
|
||||||
// show notification for music
|
buildStatusMessageWithCurrentAppTitle(
|
||||||
appLogic.platformIntegration.setAppStatusMessage(
|
text = appLogic.context.getString(R.string.background_logic_opening_lockscreen),
|
||||||
buildStatusMessage(
|
appPackageName = foregroundAppPackageName,
|
||||||
handling = audioPlaybackHandling,
|
appActivityToShow = if (activityLevelBlocking) foregroundAppActivityName else null
|
||||||
remainingTime = remainingTimeBackgroundApp,
|
|
||||||
suffix = " (2/2)",
|
|
||||||
appPackageName = audioPlaybackPackageName,
|
|
||||||
appActivityToShow = null,
|
|
||||||
remainingSessionDuration = remainingSessionDurationBackgroundApp
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// show regular notification
|
val pagesForTheForegroundApp = if (foregroundAppBaseHandling is AppBaseHandling.UseCategories) foregroundAppBaseHandling.categoryIds.size else 1
|
||||||
appLogic.platformIntegration.setAppStatusMessage(
|
val pagesForTheBackgroundApp = if (!showBackgroundStatus) 0 else if (backgroundAppBaseHandling is AppBaseHandling.UseCategories) backgroundAppBaseHandling.categoryIds.size else 1
|
||||||
buildStatusMessage(
|
val totalPages = pagesForTheForegroundApp + pagesForTheBackgroundApp
|
||||||
handling = foregroundAppHandling,
|
val currentPage = (nowTimestamp / 3000 % totalPages).toInt()
|
||||||
remainingTime = remainingTimeForegroundApp,
|
|
||||||
suffix = if (showBackgroundStatus) " (1/2)" else "",
|
val suffix = if (totalPages == 1) "" else " (${currentPage + 1} / $totalPages)"
|
||||||
|
|
||||||
|
if (currentPage < pagesForTheForegroundApp) {
|
||||||
|
val pageWithin = currentPage
|
||||||
|
|
||||||
|
if (foregroundAppBaseHandling is AppBaseHandling.UseCategories) {
|
||||||
|
val categoryId = foregroundAppBaseHandling.categoryIds.toList()[pageWithin]
|
||||||
|
|
||||||
|
buildNotificationForAppWithCategoryUsage(
|
||||||
appPackageName = foregroundAppPackageName,
|
appPackageName = foregroundAppPackageName,
|
||||||
appActivityToShow = if (activityLevelBlocking) foregroundAppActivityName else null,
|
appActivityToShow = if (activityLevelBlocking) foregroundAppActivityName else null,
|
||||||
remainingSessionDuration = remainingSessionDurationForegroundApp
|
suffix = suffix,
|
||||||
|
categoryId = categoryId
|
||||||
)
|
)
|
||||||
)
|
} else {
|
||||||
|
buildNotificationForAppWithoutCategoryUsage(
|
||||||
|
appPackageName = foregroundAppPackageName,
|
||||||
|
appActivityToShow = if (activityLevelBlocking) foregroundAppActivityName else null,
|
||||||
|
suffix = suffix,
|
||||||
|
handling = foregroundAppBaseHandling
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val pageWithin = currentPage - pagesForTheForegroundApp
|
||||||
|
|
||||||
|
if (backgroundAppBaseHandling is AppBaseHandling.UseCategories) {
|
||||||
|
val categoryId = backgroundAppBaseHandling.categoryIds.toList()[pageWithin]
|
||||||
|
|
||||||
|
buildNotificationForAppWithCategoryUsage(
|
||||||
|
appPackageName = audioPlaybackPackageName,
|
||||||
|
appActivityToShow = null,
|
||||||
|
suffix = suffix,
|
||||||
|
categoryId = categoryId
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
buildNotificationForAppWithoutCategoryUsage(
|
||||||
|
appPackageName = audioPlaybackPackageName,
|
||||||
|
appActivityToShow = null,
|
||||||
|
suffix = suffix,
|
||||||
|
handling = backgroundAppBaseHandling
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
appLogic.platformIntegration.setAppStatusMessage(statusMessage)
|
||||||
|
|
||||||
// handle blocking
|
// handle blocking
|
||||||
if (foregroundAppHandling.status == BackgroundTaskLogicAppStatus.ShouldBlock) {
|
if (blockForegroundApp) {
|
||||||
openLockscreen(foregroundAppPackageName!!, foregroundAppActivityName)
|
openLockscreen(foregroundAppPackageName!!, foregroundAppActivityName)
|
||||||
} else {
|
} else {
|
||||||
appLogic.platformIntegration.setShowBlockingOverlay(false)
|
appLogic.platformIntegration.setShowBlockingOverlay(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (audioPlaybackHandling.status == BackgroundTaskLogicAppStatus.ShouldBlock && audioPlaybackPackageName != null) {
|
if (blockAudioPlayback && audioPlaybackPackageName != null) {
|
||||||
appLogic.platformIntegration.muteAudioIfPossible(audioPlaybackPackageName)
|
appLogic.platformIntegration.muteAudioIfPossible(audioPlaybackPackageName)
|
||||||
}
|
}
|
||||||
} catch (ex: SecurityException) {
|
} catch (ex: SecurityException) {
|
||||||
|
@ -723,8 +565,6 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
appLogic.platformIntegration.setShowBlockingOverlay(false)
|
appLogic.platformIntegration.setShowBlockingOverlay(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
liveDataCaches.reportLoopDone()
|
|
||||||
|
|
||||||
// delay before running next time
|
// delay before running next time
|
||||||
val endTime = appLogic.timeApi.getCurrentUptimeInMillis()
|
val endTime = appLogic.timeApi.getCurrentUptimeInMillis()
|
||||||
previousMainLogicExecutionTime = (endTime - previousMainLoopEndTime).toInt()
|
previousMainLogicExecutionTime = (endTime - previousMainLoopEndTime).toInt()
|
||||||
|
|
|
@ -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
|
|
||||||
))
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -15,17 +15,10 @@
|
||||||
*/
|
*/
|
||||||
package io.timelimit.android.logic
|
package io.timelimit.android.logic
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.Transformations
|
|
||||||
import io.timelimit.android.BuildConfig
|
|
||||||
import io.timelimit.android.data.model.*
|
|
||||||
import io.timelimit.android.date.DateInTimezone
|
import io.timelimit.android.date.DateInTimezone
|
||||||
import io.timelimit.android.date.getMinuteOfWeek
|
import io.timelimit.android.date.getMinuteOfWeek
|
||||||
import io.timelimit.android.extensions.MinuteOfDay
|
|
||||||
import io.timelimit.android.integration.platform.android.AndroidIntegrationApps
|
|
||||||
import io.timelimit.android.livedata.*
|
import io.timelimit.android.livedata.*
|
||||||
import io.timelimit.android.logic.extension.isCategoryAllowed
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
enum class BlockingReason {
|
enum class BlockingReason {
|
||||||
|
@ -47,382 +40,8 @@ enum class BlockingLevel {
|
||||||
Activity
|
Activity
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class BlockingReasonDetail {
|
|
||||||
abstract val areNotificationsBlocked: Boolean
|
|
||||||
}
|
|
||||||
data class NoBlockingReason(
|
|
||||||
override val areNotificationsBlocked: Boolean
|
|
||||||
): BlockingReasonDetail() {
|
|
||||||
companion object {
|
|
||||||
private val instanceWithoutNotificationsBlocked = NoBlockingReason(areNotificationsBlocked = false)
|
|
||||||
private val instanceWithNotificationsBlocked = NoBlockingReason(areNotificationsBlocked = true)
|
|
||||||
|
|
||||||
fun getInstance(areNotificationsBlocked: Boolean) = if (areNotificationsBlocked)
|
|
||||||
instanceWithNotificationsBlocked
|
|
||||||
else
|
|
||||||
instanceWithoutNotificationsBlocked
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data class BlockedReasonDetails(
|
|
||||||
val reason: BlockingReason,
|
|
||||||
val level: BlockingLevel,
|
|
||||||
val categoryId: String?,
|
|
||||||
override val areNotificationsBlocked: Boolean
|
|
||||||
): BlockingReasonDetail()
|
|
||||||
|
|
||||||
class BlockingReasonUtil(private val appLogic: AppLogic) {
|
class BlockingReasonUtil(private val appLogic: AppLogic) {
|
||||||
companion object {
|
|
||||||
private const val LOG_TAG = "BlockingReason"
|
|
||||||
}
|
|
||||||
|
|
||||||
private val enableActivityLevelFiltering = appLogic.deviceEntry.map { it?.enableActivityLevelBlocking ?: false }
|
|
||||||
private val batteryLevel = appLogic.platformIntegration.getBatteryStatusLive()
|
|
||||||
|
|
||||||
fun getBlockingReason(packageName: String, activityName: String?): LiveData<BlockingReasonDetail> {
|
|
||||||
// check precondition that the app is running
|
|
||||||
|
|
||||||
return appLogic.enable.switchMap {
|
|
||||||
enabled ->
|
|
||||||
|
|
||||||
if (enabled == null || enabled == false) {
|
|
||||||
liveDataFromValue(NoBlockingReason.getInstance(areNotificationsBlocked = false) as BlockingReasonDetail)
|
|
||||||
} else {
|
|
||||||
appLogic.deviceUserEntry.switchMap {
|
|
||||||
user ->
|
|
||||||
|
|
||||||
if (user == null || user.type != UserType.Child) {
|
|
||||||
liveDataFromValue(NoBlockingReason.getInstance(areNotificationsBlocked = false) as BlockingReasonDetail)
|
|
||||||
} else {
|
|
||||||
getBlockingReasonStep2(packageName, activityName, user, TimeZone.getTimeZone(user.timeZone))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getBlockingReasonStep2(packageName: String, activityName: String?, child: User, timeZone: TimeZone): LiveData<BlockingReasonDetail> {
|
|
||||||
if (BuildConfig.DEBUG) {
|
|
||||||
Log.d(LOG_TAG, "step 2")
|
|
||||||
}
|
|
||||||
|
|
||||||
// check internal whitelist
|
|
||||||
if (packageName == BuildConfig.APPLICATION_ID) {
|
|
||||||
return liveDataFromValue(NoBlockingReason.getInstance(areNotificationsBlocked = false))
|
|
||||||
} else if (AndroidIntegrationApps.ignoredApps[packageName].let {
|
|
||||||
when (it) {
|
|
||||||
null -> false
|
|
||||||
AndroidIntegrationApps.IgnoredAppHandling.Ignore -> true
|
|
||||||
AndroidIntegrationApps.IgnoredAppHandling.IgnoreOnStoreOtherwiseWhitelistAndDontDisable -> BuildConfig.storeCompilant
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
return liveDataFromValue(NoBlockingReason.getInstance(areNotificationsBlocked = false))
|
|
||||||
} else {
|
|
||||||
return getBlockingReasonStep3(packageName, activityName, child, timeZone)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getBlockingReasonStep3(packageName: String, activityName: String?, child: User, timeZone: TimeZone): LiveData<BlockingReasonDetail> {
|
|
||||||
if (BuildConfig.DEBUG) {
|
|
||||||
Log.d(LOG_TAG, "step 3")
|
|
||||||
}
|
|
||||||
|
|
||||||
// check temporarily allowed Apps
|
|
||||||
return appLogic.deviceId.switchMap {
|
|
||||||
if (it != null) {
|
|
||||||
appLogic.database.temporarilyAllowedApp().getTemporarilyAllowedApps(deviceId = it)
|
|
||||||
} else {
|
|
||||||
liveDataFromValue(Collections.emptyList())
|
|
||||||
}
|
|
||||||
}.switchMap {
|
|
||||||
temporarilyAllowedApps ->
|
|
||||||
|
|
||||||
if (temporarilyAllowedApps.contains(packageName)) {
|
|
||||||
liveDataFromValue(NoBlockingReason.getInstance(areNotificationsBlocked = false) as BlockingReasonDetail)
|
|
||||||
} else {
|
|
||||||
getBlockingReasonStep4(packageName, activityName, child, timeZone)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getBlockingReasonStep4(packageName: String, activityName: String?, child: User, timeZone: TimeZone): LiveData<BlockingReasonDetail> {
|
|
||||||
if (BuildConfig.DEBUG) {
|
|
||||||
Log.d(LOG_TAG, "step 4")
|
|
||||||
}
|
|
||||||
|
|
||||||
return appLogic.database.category().getCategoriesByChildId(child.id).switchMap {
|
|
||||||
childCategories ->
|
|
||||||
|
|
||||||
val categoryAppLevel = appLogic.database.categoryApp().getCategoryApp(childCategories.map { it.id }, packageName)
|
|
||||||
val categoryAppActivityLevel = enableActivityLevelFiltering.switchMap {
|
|
||||||
if (it)
|
|
||||||
appLogic.database.categoryApp().getCategoryApp(childCategories.map { it.id }, "$packageName:$activityName")
|
|
||||||
else
|
|
||||||
liveDataFromValue(null as CategoryApp?)
|
|
||||||
}
|
|
||||||
|
|
||||||
val categoryApp = categoryAppLevel.switchMap { appLevel ->
|
|
||||||
categoryAppActivityLevel.map { activityLevel ->
|
|
||||||
activityLevel?.let { it to BlockingLevel.Activity } ?: appLevel?.let { it to BlockingLevel.App }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Transformations.map(categoryApp) {
|
|
||||||
categoryApp ->
|
|
||||||
|
|
||||||
if (categoryApp == null) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
childCategories.find { it.id == categoryApp.first.categoryId }?.let { it to categoryApp.second }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.switchMap {
|
|
||||||
categoryEntry ->
|
|
||||||
|
|
||||||
if (categoryEntry == null) {
|
|
||||||
val defaultCategory = if (child.categoryForNotAssignedApps.isEmpty())
|
|
||||||
liveDataFromValue(null as Category?)
|
|
||||||
else
|
|
||||||
appLogic.database.category().getCategoryByChildIdAndId(child.id, child.categoryForNotAssignedApps)
|
|
||||||
|
|
||||||
defaultCategory.switchMap { categoryEntry2 ->
|
|
||||||
if (categoryEntry2 == null) {
|
|
||||||
liveDataFromValue(
|
|
||||||
BlockedReasonDetails(
|
|
||||||
areNotificationsBlocked = false,
|
|
||||||
level = BlockingLevel.App,
|
|
||||||
reason = BlockingReason.NotPartOfAnCategory,
|
|
||||||
categoryId = null
|
|
||||||
) as BlockingReasonDetail
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
getBlockingReasonStep4Point5(categoryEntry2, child, timeZone, false, BlockingLevel.App)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
getBlockingReasonStep4Point5(categoryEntry.first, child, timeZone, false, categoryEntry.second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getBlockingReasonStep4Point5(category: Category, child: User, timeZone: TimeZone, isParentCategory: Boolean, blockingLevel: BlockingLevel): LiveData<BlockingReasonDetail> {
|
|
||||||
if (BuildConfig.DEBUG) {
|
|
||||||
Log.d(LOG_TAG, "step 4.5")
|
|
||||||
}
|
|
||||||
|
|
||||||
val shouldBlockNotifications = if (category.blockAllNotifications)
|
|
||||||
appLogic.fullVersion.shouldProvideFullVersionFunctions
|
|
||||||
else
|
|
||||||
liveDataFromValue(false)
|
|
||||||
|
|
||||||
val nextLevel = getBlockingReasonStep4Point6(category, child, timeZone, isParentCategory, blockingLevel)
|
|
||||||
|
|
||||||
return shouldBlockNotifications.switchMap { blockNotifications ->
|
|
||||||
nextLevel.map { blockingReason ->
|
|
||||||
if (blockingReason == BlockingReason.None) {
|
|
||||||
NoBlockingReason.getInstance(areNotificationsBlocked = blockNotifications)
|
|
||||||
} else {
|
|
||||||
BlockedReasonDetails(
|
|
||||||
areNotificationsBlocked = blockNotifications,
|
|
||||||
level = blockingLevel,
|
|
||||||
reason = blockingReason,
|
|
||||||
categoryId = category.id
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getBlockingReasonStep4Point6(category: Category, child: User, timeZone: TimeZone, isParentCategory: Boolean, blockingLevel: BlockingLevel): LiveData<BlockingReason> {
|
|
||||||
val next = getBlockingReasonStep4Point7(category, child, timeZone, isParentCategory, blockingLevel)
|
|
||||||
|
|
||||||
return if (category.minBatteryLevelWhileCharging == 0 && category.minBatteryLevelMobile == 0) {
|
|
||||||
next
|
|
||||||
} else {
|
|
||||||
val batteryLevelOk = batteryLevel.map { it.isCategoryAllowed(category) }.ignoreUnchanged()
|
|
||||||
|
|
||||||
batteryLevelOk.switchMap { ok ->
|
|
||||||
if (ok) {
|
|
||||||
next
|
|
||||||
} else {
|
|
||||||
liveDataFromValue(BlockingReason.BatteryLimit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getBlockingReasonStep4Point7(category: Category, child: User, timeZone: TimeZone, isParentCategory: Boolean, blockingLevel: BlockingLevel): LiveData<BlockingReason> {
|
|
||||||
if (BuildConfig.DEBUG) {
|
|
||||||
Log.d(LOG_TAG, "step 4.7")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (category.temporarilyBlocked) {
|
|
||||||
if (category.temporarilyBlockedEndTime == 0L) {
|
|
||||||
return liveDataFromValue(BlockingReason.TemporarilyBlocked)
|
|
||||||
} else {
|
|
||||||
return getTemporarilyTrustedTimeInMillis().switchMap { time ->
|
|
||||||
if (time == null) {
|
|
||||||
liveDataFromValue(BlockingReason.MissingNetworkTime)
|
|
||||||
} else if (time < category.temporarilyBlockedEndTime) {
|
|
||||||
liveDataFromValue(BlockingReason.TemporarilyBlocked)
|
|
||||||
} else {
|
|
||||||
getBlockingReasonStep4Point8(category, child, timeZone, isParentCategory, blockingLevel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return getBlockingReasonStep4Point8(category, child, timeZone, isParentCategory, blockingLevel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getBlockingReasonStep4Point8(category: Category, child: User, timeZone: TimeZone, isParentCategory: Boolean, blockingLevel: BlockingLevel): LiveData<BlockingReason> {
|
|
||||||
val areLimitsDisabled: LiveData<Boolean>
|
|
||||||
|
|
||||||
if (child.disableLimitsUntil == 0L) {
|
|
||||||
areLimitsDisabled = liveDataFromValue(false)
|
|
||||||
} else {
|
|
||||||
areLimitsDisabled = getTemporarilyTrustedTimeInMillis().map {
|
|
||||||
trustedTimeInMillis ->
|
|
||||||
|
|
||||||
trustedTimeInMillis != null && child.disableLimitsUntil > trustedTimeInMillis
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return areLimitsDisabled.switchMap {
|
|
||||||
limitsDisabled ->
|
|
||||||
|
|
||||||
if (limitsDisabled) {
|
|
||||||
liveDataFromValue(BlockingReason.None)
|
|
||||||
} else {
|
|
||||||
getBlockingReasonStep5(category, timeZone)
|
|
||||||
}
|
|
||||||
}.switchMap { result ->
|
|
||||||
if (result == BlockingReason.None && (!isParentCategory) && category.parentCategoryId.isNotEmpty()) {
|
|
||||||
appLogic.database.category().getCategoryByChildIdAndId(child.id, category.parentCategoryId).switchMap { parentCategory ->
|
|
||||||
if (parentCategory == null) {
|
|
||||||
liveDataFromValue(BlockingReason.None)
|
|
||||||
} else {
|
|
||||||
getBlockingReasonStep4Point6(parentCategory, child, timeZone, true, blockingLevel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
liveDataFromValue(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getBlockingReasonStep5(category: Category, timeZone: TimeZone): LiveData<BlockingReason> {
|
|
||||||
if (BuildConfig.DEBUG) {
|
|
||||||
Log.d(LOG_TAG, "step 5")
|
|
||||||
}
|
|
||||||
|
|
||||||
return Transformations.switchMap(getTrustedMinuteOfWeekLive(timeZone)) {
|
|
||||||
trustedMinuteOfWeek ->
|
|
||||||
|
|
||||||
if (category.blockedMinutesInWeek.dataNotToModify.isEmpty) {
|
|
||||||
getBlockingReasonStep6(category, timeZone, trustedMinuteOfWeek)
|
|
||||||
} else if (trustedMinuteOfWeek == null) {
|
|
||||||
liveDataFromValue(BlockingReason.MissingNetworkTime)
|
|
||||||
} else if (category.blockedMinutesInWeek.read(trustedMinuteOfWeek)) {
|
|
||||||
liveDataFromValue(BlockingReason.BlockedAtThisTime)
|
|
||||||
} else {
|
|
||||||
getBlockingReasonStep6(category, timeZone, trustedMinuteOfWeek)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getBlockingReasonStep6(category: Category, timeZone: TimeZone, trustedMinuteOfWeek: Int?): LiveData<BlockingReason> {
|
|
||||||
if (BuildConfig.DEBUG) {
|
|
||||||
Log.d(LOG_TAG, "step 6")
|
|
||||||
}
|
|
||||||
|
|
||||||
return getTrustedDateLive(timeZone).switchMap {
|
|
||||||
nowTrustedDate ->
|
|
||||||
|
|
||||||
appLogic.database.timeLimitRules().getTimeLimitRulesByCategory(category.id).switchMap {
|
|
||||||
rules ->
|
|
||||||
|
|
||||||
if (rules.isEmpty()) {
|
|
||||||
liveDataFromValue(BlockingReason.None)
|
|
||||||
} else if (nowTrustedDate == null || trustedMinuteOfWeek == null) {
|
|
||||||
liveDataFromValue(BlockingReason.MissingNetworkTime)
|
|
||||||
} else {
|
|
||||||
getBlockingReasonStep6(category, nowTrustedDate, trustedMinuteOfWeek, rules)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getBlockingReasonStep6(category: Category, nowTrustedDate: DateInTimezone, trustedMinuteOfWeek: Int, rules: List<TimeLimitRule>): LiveData<BlockingReason> {
|
|
||||||
if (BuildConfig.DEBUG) {
|
|
||||||
Log.d(LOG_TAG, "step 6 - 2")
|
|
||||||
}
|
|
||||||
|
|
||||||
return appLogic.currentDeviceLogic.isThisDeviceTheCurrentDevice.switchMap { isCurrentDevice ->
|
|
||||||
if (isCurrentDevice) {
|
|
||||||
getBlockingReasonStep7(category, nowTrustedDate, trustedMinuteOfWeek, rules)
|
|
||||||
} else {
|
|
||||||
liveDataFromValue(BlockingReason.RequiresCurrentDevice)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getBlockingReasonStep7(category: Category, nowTrustedDate: DateInTimezone, trustedMinuteOfWeek: Int, rules: List<TimeLimitRule>): LiveData<BlockingReason> {
|
|
||||||
if (BuildConfig.DEBUG) {
|
|
||||||
Log.d(LOG_TAG, "step 7")
|
|
||||||
}
|
|
||||||
|
|
||||||
val extraTime = category.getExtraTime(dayOfEpoch = nowTrustedDate.dayOfEpoch)
|
|
||||||
val firstDayOfWeekAsEpochDay = nowTrustedDate.dayOfEpoch - nowTrustedDate.dayOfWeek
|
|
||||||
|
|
||||||
return appLogic.database.usedTimes().getUsedTimesOfWeek(category.id, firstDayOfWeekAsEpochDay = firstDayOfWeekAsEpochDay).switchMap { usedTimes ->
|
|
||||||
val remaining = RemainingTime.getRemainingTime(nowTrustedDate.dayOfWeek, trustedMinuteOfWeek % MinuteOfDay.LENGTH, usedTimes, rules, extraTime, firstDayOfWeekAsEpochDay)
|
|
||||||
|
|
||||||
if (remaining == null || remaining.includingExtraTime > 0) {
|
|
||||||
appLogic.database.sessionDuration().getSessionDurationItemsByCategoryId(category.id).switchMap { durations ->
|
|
||||||
getTemporarilyTrustedTimeInMillis().map { timeInMillis ->
|
|
||||||
if (timeInMillis == null) {
|
|
||||||
BlockingReason.MissingNetworkTime
|
|
||||||
} else {
|
|
||||||
val remainingDuration = RemainingSessionDuration.getRemainingSessionDuration(
|
|
||||||
rules = rules,
|
|
||||||
dayOfWeek = nowTrustedDate.dayOfWeek,
|
|
||||||
durationsOfCategory = durations,
|
|
||||||
minuteOfDay = trustedMinuteOfWeek % MinuteOfDay.LENGTH,
|
|
||||||
timestamp = timeInMillis
|
|
||||||
)
|
|
||||||
|
|
||||||
if (remainingDuration == null || remainingDuration > 0) {
|
|
||||||
BlockingReason.None
|
|
||||||
} else {
|
|
||||||
BlockingReason.SessionDurationLimit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (extraTime > 0) {
|
|
||||||
liveDataFromValue(BlockingReason.TimeOverExtraTimeCanBeUsedLater)
|
|
||||||
} else {
|
|
||||||
liveDataFromValue(BlockingReason.TimeOver)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getTemporarilyTrustedTimeInMillis(): LiveData<Long?> {
|
|
||||||
val realTime = RealTime.newInstance()
|
|
||||||
|
|
||||||
return liveDataFromFunction {
|
|
||||||
appLogic.realTimeLogic.getRealTime(realTime)
|
|
||||||
|
|
||||||
if (realTime.shouldTrustTimeTemporarily) {
|
|
||||||
realTime.timeInMillis
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getTrustedMinuteOfWeekLive(timeZone: TimeZone): LiveData<Int?> {
|
fun getTrustedMinuteOfWeekLive(timeZone: TimeZone): LiveData<Int?> {
|
||||||
val realTime = RealTime.newInstance()
|
val realTime = RealTime.newInstance()
|
||||||
|
|
||||||
|
@ -468,50 +87,4 @@ class BlockingReasonUtil(private val appLogic: AppLogic) {
|
||||||
}
|
}
|
||||||
}.ignoreUnchanged()
|
}.ignoreUnchanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getTrustedDateLive(timeZone: TimeZone): LiveData<DateInTimezone?> {
|
|
||||||
val realTime = RealTime.newInstance()
|
|
||||||
|
|
||||||
return object: LiveData<DateInTimezone?>() {
|
|
||||||
fun update() {
|
|
||||||
appLogic.realTimeLogic.getRealTime(realTime)
|
|
||||||
|
|
||||||
if (realTime.shouldTrustTimeTemporarily) {
|
|
||||||
value = DateInTimezone.newInstance(realTime.timeInMillis, timeZone)
|
|
||||||
} else {
|
|
||||||
value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
update()
|
|
||||||
}
|
|
||||||
|
|
||||||
val scheduledUpdateRunnable = Runnable {
|
|
||||||
update()
|
|
||||||
scheduleUpdate()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun scheduleUpdate() {
|
|
||||||
appLogic.timeApi.runDelayed(scheduledUpdateRunnable, 1000L /* every second */)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun cancelScheduledUpdate() {
|
|
||||||
appLogic.timeApi.cancelScheduledAction(scheduledUpdateRunnable)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActive() {
|
|
||||||
super.onActive()
|
|
||||||
|
|
||||||
update()
|
|
||||||
scheduleUpdate()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onInactive() {
|
|
||||||
super.onInactive()
|
|
||||||
|
|
||||||
cancelScheduledUpdate()
|
|
||||||
}
|
|
||||||
}.ignoreUnchanged()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -16,14 +16,26 @@
|
||||||
package io.timelimit.android.logic
|
package io.timelimit.android.logic
|
||||||
|
|
||||||
import io.timelimit.android.data.model.Device
|
import io.timelimit.android.data.model.Device
|
||||||
|
import io.timelimit.android.data.model.derived.DeviceRelatedData
|
||||||
|
import io.timelimit.android.data.model.derived.UserRelatedData
|
||||||
import io.timelimit.android.livedata.*
|
import io.timelimit.android.livedata.*
|
||||||
|
|
||||||
class CurrentDeviceLogic(private val appLogic: AppLogic) {
|
class CurrentDeviceLogic(private val appLogic: AppLogic) {
|
||||||
private val disabledPrimaryDeviceCheck = appLogic.deviceUserEntry.switchMap { userEntry ->
|
companion object {
|
||||||
if (userEntry?.relaxPrimaryDevice == true) {
|
fun handleDeviceAsCurrentDevice(device: DeviceRelatedData, user: UserRelatedData): Boolean {
|
||||||
appLogic.fullVersion.shouldProvideFullVersionFunctions
|
if (device.isLocalMode) {
|
||||||
} else {
|
return true
|
||||||
liveDataFromValue(false)
|
}
|
||||||
|
|
||||||
|
if (user.user.currentDevice == device.deviceEntry.id) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.user.relaxPrimaryDevice && device.isConnectedAndHasPremium) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,19 +53,6 @@ class CurrentDeviceLogic(private val appLogic: AppLogic) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val isThisDeviceMarkedAsCurrentDevice = appLogic.deviceEntry
|
|
||||||
.map { it?.id }
|
|
||||||
.switchMap { ownDeviceId ->
|
|
||||||
appLogic.deviceUserEntry.map { userEntry ->
|
|
||||||
userEntry?.currentDevice == ownDeviceId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val isThisDeviceTheCurrentDevice = appLogic.fullVersion.isLocalMode
|
|
||||||
.or(isThisDeviceMarkedAsCurrentDevice)
|
|
||||||
.or(disabledPrimaryDeviceCheck)
|
|
||||||
.ignoreUnchanged()
|
|
||||||
|
|
||||||
val otherAssignedDevice = appLogic.deviceUserEntry.switchMap { userEntry ->
|
val otherAssignedDevice = appLogic.deviceUserEntry.switchMap { userEntry ->
|
||||||
if (userEntry?.currentDevice == null) {
|
if (userEntry?.currentDevice == null) {
|
||||||
liveDataFromValue(null as Device?)
|
liveDataFromValue(null as Device?)
|
||||||
|
|
|
@ -21,6 +21,7 @@ import io.timelimit.android.async.Threads
|
||||||
import io.timelimit.android.coroutines.executeAndWait
|
import io.timelimit.android.coroutines.executeAndWait
|
||||||
import io.timelimit.android.coroutines.runAsync
|
import io.timelimit.android.coroutines.runAsync
|
||||||
import io.timelimit.android.data.model.User
|
import io.timelimit.android.data.model.User
|
||||||
|
import io.timelimit.android.data.model.derived.DeviceRelatedData
|
||||||
import io.timelimit.android.livedata.*
|
import io.timelimit.android.livedata.*
|
||||||
import io.timelimit.android.sync.actions.SignOutAtDeviceAction
|
import io.timelimit.android.sync.actions.SignOutAtDeviceAction
|
||||||
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
||||||
|
@ -30,6 +31,8 @@ import kotlinx.coroutines.sync.withLock
|
||||||
class DefaultUserLogic(private val appLogic: AppLogic) {
|
class DefaultUserLogic(private val appLogic: AppLogic) {
|
||||||
companion object {
|
companion object {
|
||||||
private const val LOG_TAG = "DefaultUserLogic"
|
private const val LOG_TAG = "DefaultUserLogic"
|
||||||
|
|
||||||
|
fun hasAutomaticSignOut(device: DeviceRelatedData): Boolean = device.hasValidDefaultUser && device.deviceEntry.defaultUserTimeout > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun defaultUserEntry() = appLogic.deviceEntry.map { device ->
|
private fun defaultUserEntry() = appLogic.deviceEntry.map { device ->
|
||||||
|
@ -40,10 +43,7 @@ class DefaultUserLogic(private val appLogic: AppLogic) {
|
||||||
else
|
else
|
||||||
liveDataFromValue(null as User?)
|
liveDataFromValue(null as User?)
|
||||||
}
|
}
|
||||||
private fun hasDefaultUser() = defaultUserEntry().map { it != null }.ignoreUnchanged()
|
|
||||||
private fun defaultUserTimeout() = appLogic.deviceEntry.map { it?.defaultUserTimeout ?: 0 }.ignoreUnchanged()
|
private fun defaultUserTimeout() = appLogic.deviceEntry.map { it?.defaultUserTimeout ?: 0 }.ignoreUnchanged()
|
||||||
private fun hasDefaultUserTimeout() = defaultUserTimeout().map { it != 0 }.ignoreUnchanged()
|
|
||||||
fun hasAutomaticSignOut() = hasDefaultUser().and(hasDefaultUserTimeout())
|
|
||||||
|
|
||||||
private val logoutLock = Mutex()
|
private val logoutLock = Mutex()
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -17,6 +17,7 @@ package io.timelimit.android.logic
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import io.timelimit.android.BuildConfig
|
import io.timelimit.android.BuildConfig
|
||||||
|
import io.timelimit.android.async.Threads
|
||||||
import io.timelimit.android.coroutines.runAsync
|
import io.timelimit.android.coroutines.runAsync
|
||||||
import io.timelimit.android.data.model.NetworkTime
|
import io.timelimit.android.data.model.NetworkTime
|
||||||
import io.timelimit.android.livedata.ignoreUnchanged
|
import io.timelimit.android.livedata.ignoreUnchanged
|
||||||
|
@ -27,6 +28,7 @@ import java.io.IOException
|
||||||
class RealTimeLogic(private val appLogic: AppLogic) {
|
class RealTimeLogic(private val appLogic: AppLogic) {
|
||||||
companion object {
|
companion object {
|
||||||
private const val LOG_TAG = "RealTimeLogic"
|
private const val LOG_TAG = "RealTimeLogic"
|
||||||
|
private const val MISSING_NETWORK_TIME_GRACE_PERIOD = 5 * 1000L
|
||||||
}
|
}
|
||||||
|
|
||||||
private val deviceEntry = appLogic.deviceEntryIfEnabled
|
private val deviceEntry = appLogic.deviceEntryIfEnabled
|
||||||
|
@ -48,6 +50,10 @@ class RealTimeLogic(private val appLogic: AppLogic) {
|
||||||
|
|
||||||
requireRemoteTimeUptime = appLogic.timeApi.getCurrentUptimeInMillis()
|
requireRemoteTimeUptime = appLogic.timeApi.getCurrentUptimeInMillis()
|
||||||
tryQueryTime()
|
tryQueryTime()
|
||||||
|
|
||||||
|
Threads.mainThreadHandler.postDelayed({
|
||||||
|
callTimeModificationListeners()
|
||||||
|
}, MISSING_NETWORK_TIME_GRACE_PERIOD)
|
||||||
} else {
|
} else {
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
Log.d(LOG_TAG, "shouldQueryTime = false")
|
Log.d(LOG_TAG, "shouldQueryTime = false")
|
||||||
|
@ -56,6 +62,10 @@ class RealTimeLogic(private val appLogic: AppLogic) {
|
||||||
appLogic.timeApi.cancelScheduledAction(tryQueryTime)
|
appLogic.timeApi.cancelScheduledAction(tryQueryTime)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
appLogic.platformIntegration.systemClockChangeListener = Runnable {
|
||||||
|
callTimeModificationListeners()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var lastSuccessfullyTimeRequestUptime: Long? = null
|
private var lastSuccessfullyTimeRequestUptime: Long? = null
|
||||||
|
@ -65,6 +75,19 @@ class RealTimeLogic(private val appLogic: AppLogic) {
|
||||||
|
|
||||||
private val queryTimeLock = Mutex()
|
private val queryTimeLock = Mutex()
|
||||||
private val tryQueryTime = Runnable { tryQueryTime() }
|
private val tryQueryTime = Runnable { tryQueryTime() }
|
||||||
|
private val timeModificationListeners = mutableSetOf<() -> Unit>()
|
||||||
|
|
||||||
|
fun registerTimeModificationListener(listener: () -> Unit) = synchronized(timeModificationListeners) {
|
||||||
|
timeModificationListeners.add(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unregisterTimeModificationListener(listener: () -> Unit) = synchronized(timeModificationListeners) {
|
||||||
|
timeModificationListeners.remove(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun callTimeModificationListeners() = synchronized(timeModificationListeners) {
|
||||||
|
timeModificationListeners.forEach { it() }
|
||||||
|
}
|
||||||
|
|
||||||
fun tryQueryTime() {
|
fun tryQueryTime() {
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
|
@ -103,6 +126,8 @@ class RealTimeLogic(private val appLogic: AppLogic) {
|
||||||
|
|
||||||
// schedule refresh in 2 hours
|
// schedule refresh in 2 hours
|
||||||
appLogic.timeApi.runDelayed(tryQueryTime, 1000 * 60 * 60 * 2)
|
appLogic.timeApi.runDelayed(tryQueryTime, 1000 * 60 * 60 * 2)
|
||||||
|
|
||||||
|
callTimeModificationListeners()
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
if (uptimeRealTimeOffset == null) {
|
if (uptimeRealTimeOffset == null) {
|
||||||
// schedule next attempt in 10 seconds
|
// schedule next attempt in 10 seconds
|
||||||
|
@ -127,6 +152,8 @@ class RealTimeLogic(private val appLogic: AppLogic) {
|
||||||
val systemTime = appLogic.timeApi.getCurrentTimeInMillis()
|
val systemTime = appLogic.timeApi.getCurrentTimeInMillis()
|
||||||
|
|
||||||
confirmedUptimeSystemTimeOffset = systemTime - uptime
|
confirmedUptimeSystemTimeOffset = systemTime - uptime
|
||||||
|
|
||||||
|
callTimeModificationListeners()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getRealTime(time: RealTime) {
|
fun getRealTime(time: RealTime) {
|
||||||
|
@ -174,7 +201,7 @@ class RealTimeLogic(private val appLogic: AppLogic) {
|
||||||
} else {
|
} else {
|
||||||
time.timeInMillis = systemTime
|
time.timeInMillis = systemTime
|
||||||
// 5 seconds grace period
|
// 5 seconds grace period
|
||||||
time.shouldTrustTimeTemporarily = requireRemoteTimeUptime + 5000 > systemUptime
|
time.shouldTrustTimeTemporarily = requireRemoteTimeUptime + MISSING_NETWORK_TIME_GRACE_PERIOD > systemUptime
|
||||||
time.shouldTrustTimePermanently = false
|
time.shouldTrustTimePermanently = false
|
||||||
time.isNetworkTime = false
|
time.isNetworkTime = false
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -15,104 +15,154 @@
|
||||||
*/
|
*/
|
||||||
package io.timelimit.android.logic
|
package io.timelimit.android.logic
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import io.timelimit.android.async.Threads
|
||||||
import io.timelimit.android.data.model.Category
|
import io.timelimit.android.data.invalidation.Observer
|
||||||
|
import io.timelimit.android.data.invalidation.Table
|
||||||
import io.timelimit.android.data.model.CategoryApp
|
import io.timelimit.android.data.model.CategoryApp
|
||||||
import io.timelimit.android.data.model.ExperimentalFlags
|
import io.timelimit.android.data.model.ExperimentalFlags
|
||||||
import io.timelimit.android.data.model.UserType
|
import io.timelimit.android.data.model.UserType
|
||||||
|
import io.timelimit.android.data.model.derived.UserRelatedData
|
||||||
|
import io.timelimit.android.integration.platform.ProtectionLevel
|
||||||
import io.timelimit.android.integration.platform.android.AndroidIntegrationApps
|
import io.timelimit.android.integration.platform.android.AndroidIntegrationApps
|
||||||
import io.timelimit.android.livedata.ignoreUnchanged
|
import io.timelimit.android.logic.blockingreason.CategoryHandlingCache
|
||||||
import io.timelimit.android.livedata.liveDataFromValue
|
import java.lang.ref.WeakReference
|
||||||
import io.timelimit.android.livedata.map
|
import java.util.concurrent.CountDownLatch
|
||||||
import io.timelimit.android.livedata.switchMap
|
import java.util.concurrent.Executors
|
||||||
import java.util.*
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
class SuspendAppsLogic(private val appLogic: AppLogic) {
|
class SuspendAppsLogic(private val appLogic: AppLogic): Observer {
|
||||||
private val blockingAtActivityLevel = appLogic.deviceEntry.map { it?.enableActivityLevelBlocking ?: false }
|
private var lastDefaultCategory: String? = null
|
||||||
private val blockingReasonUtil = CategoriesBlockingReasonUtil(appLogic)
|
private var lastAllowedCategoryList = emptySet<String>()
|
||||||
|
private var lastCategoryApps = emptyList<CategoryApp>()
|
||||||
|
private val installedAppsModified = AtomicBoolean(false)
|
||||||
|
private val categoryHandlingCache = CategoryHandlingCache()
|
||||||
|
private val realTime = RealTime.newInstance()
|
||||||
|
private var batteryStatus = appLogic.platformIntegration.getBatteryStatus()
|
||||||
|
private val pendingSync = AtomicBoolean(true)
|
||||||
|
private val executor = Executors.newSingleThreadExecutor()
|
||||||
|
|
||||||
private val knownInstalledApps = appLogic.deviceId.switchMap { deviceId ->
|
private val backgroundRunnable = Runnable {
|
||||||
if (deviceId.isNullOrEmpty()) {
|
while (pendingSync.getAndSet(false)) {
|
||||||
liveDataFromValue(emptyList())
|
updateBlockingSync()
|
||||||
} else {
|
|
||||||
appLogic.database.app().getAppsByDeviceIdAsync(deviceId).map { apps ->
|
|
||||||
apps.map { it.packageName }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.ignoreUnchanged()
|
|
||||||
|
|
||||||
private val categoryData = appLogic.deviceUserEntry.switchMap { deviceUser ->
|
Thread.sleep(500)
|
||||||
if (deviceUser?.type == UserType.Child) {
|
|
||||||
appLogic.database.category().getCategoriesByChildId(deviceUser.id).switchMap { categories ->
|
|
||||||
appLogic.database.categoryApp().getCategoryApps(categories.map { it.id }).map { categoryApps ->
|
|
||||||
RealCategoryData(
|
|
||||||
categoryForUnassignedApps = deviceUser.categoryForNotAssignedApps,
|
|
||||||
categories = categories,
|
|
||||||
categoryApps = categoryApps
|
|
||||||
) as CategoryData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
liveDataFromValue(NoChildUser as CategoryData)
|
|
||||||
}
|
|
||||||
}.ignoreUnchanged()
|
|
||||||
|
|
||||||
private val categoryBlockingReasons: LiveData<Map<String, BlockingReason>?> = appLogic.deviceUserEntry.switchMap { deviceUser ->
|
|
||||||
if (deviceUser?.type == UserType.Child) {
|
|
||||||
appLogic.database.category().getCategoriesByChildId(deviceUser.id).switchMap { categories ->
|
|
||||||
blockingReasonUtil.getCategoryBlockingReasons(
|
|
||||||
childDisableLimitsUntil = liveDataFromValue(deviceUser.disableLimitsUntil),
|
|
||||||
timeZone = liveDataFromValue(TimeZone.getTimeZone(deviceUser.timeZone)),
|
|
||||||
categories = categories
|
|
||||||
) as LiveData<Map<String, BlockingReason>?>
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
liveDataFromValue(null as Map<String, BlockingReason>?)
|
|
||||||
}
|
|
||||||
}.ignoreUnchanged()
|
|
||||||
|
|
||||||
private val appsToBlock = categoryBlockingReasons.switchMap { blockingReasons ->
|
|
||||||
if (blockingReasons == null) {
|
|
||||||
liveDataFromValue(emptyList<String>())
|
|
||||||
} else {
|
|
||||||
categoryData.switchMap { categories ->
|
|
||||||
when (categories) {
|
|
||||||
is NoChildUser -> liveDataFromValue(emptyList<String>())
|
|
||||||
is RealCategoryData -> {
|
|
||||||
knownInstalledApps.switchMap { installedApps ->
|
|
||||||
blockingAtActivityLevel.map { blockingAtActivityLevel ->
|
|
||||||
val prepared = getAppsWithCategories(installedApps, categories, blockingAtActivityLevel)
|
|
||||||
val result = mutableListOf<String>()
|
|
||||||
|
|
||||||
installedApps.forEach { packageName ->
|
|
||||||
val appCategories = prepared[packageName] ?: emptySet()
|
|
||||||
|
|
||||||
if (appCategories.find { categoryId -> (blockingReasons[categoryId] ?: BlockingReason.None) == BlockingReason.None } == null) {
|
|
||||||
if (!AndroidIntegrationApps.appsToNotSuspend.contains(packageName)) {
|
|
||||||
result.add(packageName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} as LiveData<List<String>>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val realAppsToBlock = appLogic.database.config().isExperimentalFlagsSetAsync(ExperimentalFlags.SYSTEM_LEVEL_BLOCKING).switchMap { systemLevelBlocking ->
|
private val triggerRunnable = Runnable {
|
||||||
if (systemLevelBlocking) {
|
triggerUpdate()
|
||||||
appsToBlock
|
}
|
||||||
} else {
|
|
||||||
liveDataFromValue(emptyList())
|
|
||||||
}
|
|
||||||
}.ignoreUnchanged()
|
|
||||||
|
|
||||||
private fun getAppsWithCategories(packageNames: List<String>, data: RealCategoryData, blockingAtActivityLevel: Boolean): Map<String, Set<String>> {
|
private fun triggerUpdate() {
|
||||||
val categoryForUnassignedApps = if (data.categories.find { it.id == data.categoryForUnassignedApps } != null) data.categoryForUnassignedApps else null
|
pendingSync.set(true); executor.submit(backgroundRunnable)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scheduleUpdate(delay: Long) {
|
||||||
|
appLogic.timeApi.cancelScheduledAction(triggerRunnable)
|
||||||
|
appLogic.timeApi.runDelayedByUptime(triggerRunnable, delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
appLogic.database.registerWeakObserver(arrayOf(Table.App), WeakReference(this))
|
||||||
|
appLogic.platformIntegration.getBatteryStatusLive().observeForever { batteryStatus = it; triggerUpdate() }
|
||||||
|
appLogic.realTimeLogic.registerTimeModificationListener { triggerUpdate() }
|
||||||
|
appLogic.database.derivedDataDao().getUserAndDeviceRelatedDataLive().observeForever { triggerUpdate() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onInvalidated(tables: Set<Table>) {
|
||||||
|
installedAppsModified.set(true); triggerUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateBlockingSync() {
|
||||||
|
val userAndDeviceRelatedData = appLogic.database.derivedDataDao().getUserAndDeviceRelatedDataSync()
|
||||||
|
|
||||||
|
val isRestrictedUser = userAndDeviceRelatedData?.userRelatedData?.user?.type == UserType.Child
|
||||||
|
val enableBlockingAtSystemLevel = userAndDeviceRelatedData?.deviceRelatedData?.isExperimentalFlagSetSync(ExperimentalFlags.SYSTEM_LEVEL_BLOCKING) ?: false
|
||||||
|
val hasPermission = appLogic.platformIntegration.getCurrentProtectionLevel() == ProtectionLevel.DeviceOwner
|
||||||
|
val enableBlocking = isRestrictedUser && enableBlockingAtSystemLevel && hasPermission
|
||||||
|
|
||||||
|
if (!enableBlocking) {
|
||||||
|
appLogic.platformIntegration.stopSuspendingForAllApps()
|
||||||
|
|
||||||
|
lastAllowedCategoryList = emptySet()
|
||||||
|
lastCategoryApps = emptyList()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val userRelatedData = userAndDeviceRelatedData!!.userRelatedData!!
|
||||||
|
|
||||||
|
val latch = CountDownLatch(1)
|
||||||
|
|
||||||
|
Threads.mainThreadHandler.post { appLogic.realTimeLogic.getRealTime(realTime); latch.countDown() }
|
||||||
|
|
||||||
|
latch.await()
|
||||||
|
|
||||||
|
categoryHandlingCache.reportStatus(
|
||||||
|
user = userRelatedData,
|
||||||
|
shouldTrustTimeTemporarily = realTime.shouldTrustTimeTemporarily,
|
||||||
|
timeInMillis = realTime.timeInMillis,
|
||||||
|
batteryStatus = batteryStatus,
|
||||||
|
assumeCurrentDevice = CurrentDeviceLogic.handleDeviceAsCurrentDevice(
|
||||||
|
device = userAndDeviceRelatedData.deviceRelatedData,
|
||||||
|
user = userRelatedData
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val defaultCategory = userRelatedData.user.categoryForNotAssignedApps
|
||||||
|
val blockingAtActivityLevel = userAndDeviceRelatedData.deviceRelatedData.deviceEntry.enableActivityLevelBlocking
|
||||||
|
val categoryApps = userRelatedData.categoryApps
|
||||||
|
val categoryHandlings = userRelatedData.categoryById.keys.map { categoryHandlingCache.get(it) }
|
||||||
|
val categoryIdsToAllow = categoryHandlings.filterNot { it.shouldBlockAtSystemLevel }.map { it.createdWithCategoryRelatedData.category.id }.toMutableSet()
|
||||||
|
|
||||||
|
var didModify: Boolean
|
||||||
|
|
||||||
|
do {
|
||||||
|
didModify = false
|
||||||
|
|
||||||
|
val iterator = categoryIdsToAllow.iterator()
|
||||||
|
|
||||||
|
for (categoryId in iterator) {
|
||||||
|
val parentCategory = userRelatedData.categoryById[userRelatedData.categoryById[categoryId]?.category?.parentCategoryId]
|
||||||
|
|
||||||
|
if (parentCategory != null && !categoryIdsToAllow.contains(parentCategory.category.id)) {
|
||||||
|
iterator.remove(); didModify = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (didModify)
|
||||||
|
|
||||||
|
categoryHandlings.minBy { it.dependsOnMaxTime }?.let {
|
||||||
|
scheduleUpdate((it.dependsOnMaxTime - realTime.timeInMillis))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
categoryIdsToAllow != lastAllowedCategoryList || categoryApps != lastCategoryApps ||
|
||||||
|
installedAppsModified.getAndSet(false) || defaultCategory != lastDefaultCategory
|
||||||
|
) {
|
||||||
|
lastAllowedCategoryList = categoryIdsToAllow
|
||||||
|
lastCategoryApps = categoryApps
|
||||||
|
lastDefaultCategory = defaultCategory
|
||||||
|
|
||||||
|
val installedApps = appLogic.platformIntegration.getLocalAppPackageNames()
|
||||||
|
val prepared = getAppsWithCategories(installedApps, userRelatedData, blockingAtActivityLevel)
|
||||||
|
val appsToBlock = mutableListOf<String>()
|
||||||
|
|
||||||
|
installedApps.forEach { packageName ->
|
||||||
|
val appCategories = prepared[packageName] ?: emptySet()
|
||||||
|
|
||||||
|
if (appCategories.find { categoryId -> categoryIdsToAllow.contains(categoryId) } == null) {
|
||||||
|
if (!AndroidIntegrationApps.appsToNotSuspend.contains(packageName)) {
|
||||||
|
appsToBlock.add(packageName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applySuspendedApps(appsToBlock)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getAppsWithCategories(packageNames: List<String>, data: UserRelatedData, blockingAtActivityLevel: Boolean): Map<String, Set<String>> {
|
||||||
|
val categoryForUnassignedApps = data.categoryById[data.user.categoryForNotAssignedApps]
|
||||||
|
|
||||||
if (blockingAtActivityLevel) {
|
if (blockingAtActivityLevel) {
|
||||||
val categoriesByPackageName = data.categoryApps.groupBy { it.packageNameWithoutActivityName }
|
val categoriesByPackageName = data.categoryApps.groupBy { it.packageNameWithoutActivityName }
|
||||||
|
@ -126,7 +176,7 @@ class SuspendAppsLogic(private val appLogic: AppLogic) {
|
||||||
|
|
||||||
if (!isMainAppIncluded) {
|
if (!isMainAppIncluded) {
|
||||||
if (categoryForUnassignedApps != null) {
|
if (categoryForUnassignedApps != null) {
|
||||||
categories.add(categoryForUnassignedApps)
|
categories.add(categoryForUnassignedApps.category.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,7 +190,7 @@ class SuspendAppsLogic(private val appLogic: AppLogic) {
|
||||||
val result = mutableMapOf<String, Set<String>>()
|
val result = mutableMapOf<String, Set<String>>()
|
||||||
|
|
||||||
packageNames.forEach { packageName ->
|
packageNames.forEach { packageName ->
|
||||||
val category = categoryByPackageName[packageName]?.categoryId ?: categoryForUnassignedApps
|
val category = categoryByPackageName[packageName]?.categoryId ?: categoryForUnassignedApps?.category?.id
|
||||||
|
|
||||||
result[packageName] = if (category != null) setOf(category) else emptySet()
|
result[packageName] = if (category != null) setOf(category) else emptySet()
|
||||||
}
|
}
|
||||||
|
@ -160,16 +210,4 @@ class SuspendAppsLogic(private val appLogic: AppLogic) {
|
||||||
appLogic.platformIntegration.setSuspendedApps(packageNames, true)
|
appLogic.platformIntegration.setSuspendedApps(packageNames, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
init {
|
|
||||||
realAppsToBlock.observeForever { appsToBlock -> applySuspendedApps(appsToBlock) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class CategoryData
|
|
||||||
internal object NoChildUser: CategoryData()
|
|
||||||
internal class RealCategoryData(
|
|
||||||
val categoryForUnassignedApps: String,
|
|
||||||
val categories: List<Category>,
|
|
||||||
val categoryApps: List<CategoryApp>
|
|
||||||
): CategoryData()
|
|
|
@ -13,118 +13,134 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.timelimit.android.logic
|
package io.timelimit.android.logic
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import io.timelimit.android.BuildConfig
|
import io.timelimit.android.BuildConfig
|
||||||
import io.timelimit.android.date.DateInTimezone
|
import io.timelimit.android.logic.blockingreason.CategoryItselfHandling
|
||||||
import io.timelimit.android.sync.actions.AddUsedTimeActionItem
|
import io.timelimit.android.sync.actions.AddUsedTimeActionItem
|
||||||
import io.timelimit.android.sync.actions.AddUsedTimeActionItemAdditionalCountingSlot
|
|
||||||
import io.timelimit.android.sync.actions.AddUsedTimeActionItemSessionDurationLimitSlot
|
|
||||||
import io.timelimit.android.sync.actions.AddUsedTimeActionVersion2
|
import io.timelimit.android.sync.actions.AddUsedTimeActionVersion2
|
||||||
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
||||||
import io.timelimit.android.sync.actions.dispatch.CategoryNotFoundException
|
import io.timelimit.android.sync.actions.dispatch.CategoryNotFoundException
|
||||||
|
|
||||||
class UsedTimeUpdateHelper (val date: DateInTimezone) {
|
class UsedTimeUpdateHelper (private val appLogic: AppLogic) {
|
||||||
companion object {
|
companion object {
|
||||||
private const val LOG_TAG = "UsedTimeUpdateHelper"
|
private const val LOG_TAG = "NewUsedTimeUpdateHelper"
|
||||||
}
|
}
|
||||||
|
|
||||||
val timeToAdd = mutableMapOf<String, Int>()
|
private var countedTime = 0
|
||||||
val extraTimeToSubtract = mutableMapOf<String, Int>()
|
private var lastCategoryHandlings = emptyList<CategoryItselfHandling>()
|
||||||
val sessionDurationLimitSlots = mutableMapOf<String, Set<AddUsedTimeActionItemSessionDurationLimitSlot>>()
|
private var categoryIds = emptySet<String>()
|
||||||
var trustedTimestamp: Long = 0
|
private var trustedTimestamp = 0L
|
||||||
val additionalSlots = mutableMapOf<String, Set<AddUsedTimeActionItemAdditionalCountingSlot>>()
|
private var dayOfEpoch = 0
|
||||||
var shouldDoAutoCommit = false
|
private var maxTimeToAdd = Long.MAX_VALUE
|
||||||
|
|
||||||
fun add(
|
fun getCountedTime() = countedTime
|
||||||
categoryId: String, time: Int, slots: Set<AddUsedTimeActionItemAdditionalCountingSlot>,
|
fun getCountedCategoryIds() = categoryIds
|
||||||
includingExtraTime: Boolean, sessionDurationLimits: Set<AddUsedTimeActionItemSessionDurationLimitSlot>,
|
|
||||||
trustedTimestamp: Long
|
// returns true if it made a commit
|
||||||
) {
|
suspend fun report(duration: Int, handlings: List<CategoryItselfHandling>, trustedTimestamp: Long, dayOfEpoch: Int): Boolean {
|
||||||
if (time < 0) {
|
if (handlings.find { !it.shouldCountTime } != null || duration < 0) {
|
||||||
throw IllegalArgumentException()
|
throw IllegalArgumentException()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (time == 0) {
|
if (duration == 0) {
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
timeToAdd[categoryId] = (timeToAdd[categoryId] ?: 0) + time
|
// the time is counted for the previous categories
|
||||||
|
// otherwise, the time is so much that the previous session of a session duration limit
|
||||||
|
// will be extend which causes an unintended blocking
|
||||||
|
countedTime += duration
|
||||||
|
|
||||||
if (sessionDurationLimits.isNotEmpty()) {
|
val makeCommitByDifferntHandling = if (handlings != lastCategoryHandlings) {
|
||||||
this.sessionDurationLimitSlots[categoryId] = sessionDurationLimits
|
val newIds = handlings.map { it.createdWithCategoryRelatedData.category.id }.toSet()
|
||||||
}
|
val oldIds = categoryIds
|
||||||
|
|
||||||
if (sessionDurationLimits.isNotEmpty() && trustedTimestamp != 0L) {
|
maxTimeToAdd = handlings.minBy { it.maxTimeToAdd }?.maxTimeToAdd ?: Long.MAX_VALUE
|
||||||
this.trustedTimestamp = trustedTimestamp
|
categoryIds = newIds
|
||||||
}
|
|
||||||
|
|
||||||
if (includingExtraTime) {
|
if (lastCategoryHandlings.size != handlings.size) {
|
||||||
extraTimeToSubtract[categoryId] = (extraTimeToSubtract[categoryId] ?: 0) + time
|
true
|
||||||
}
|
} else {
|
||||||
|
if ((oldIds - newIds).isNotEmpty() || (newIds - oldIds).isNotEmpty()) {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
val oldHandlingById = lastCategoryHandlings.associateBy { it.createdWithCategoryRelatedData.category.id }
|
||||||
|
|
||||||
if (additionalSlots[categoryId] != null && slots != additionalSlots[categoryId]) {
|
handlings.find { newHandling ->
|
||||||
shouldDoAutoCommit = true
|
val oldHandling = oldHandlingById[newHandling.createdWithCategoryRelatedData.category.id]!!
|
||||||
} else if (slots.isNotEmpty()) {
|
|
||||||
additionalSlots[categoryId] = slots
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timeToAdd[categoryId]!! >= 1000 * 10) {
|
oldHandling.shouldCountExtraTime != newHandling.shouldCountExtraTime ||
|
||||||
shouldDoAutoCommit = true
|
oldHandling.additionalTimeCountingSlots != newHandling.additionalTimeCountingSlots ||
|
||||||
}
|
oldHandling.sessionDurationSlotsToCount != newHandling.sessionDurationSlotsToCount
|
||||||
}
|
} != null
|
||||||
|
}
|
||||||
fun reportCurrentCategories(categories: Set<String>) {
|
|
||||||
if (!categories.containsAll(timeToAdd.keys)) {
|
|
||||||
shouldDoAutoCommit = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!categories.containsAll(extraTimeToSubtract.keys)) {
|
|
||||||
shouldDoAutoCommit = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun forceCommit(appLogic: AppLogic) {
|
|
||||||
if (timeToAdd.isEmpty() && extraTimeToSubtract.isEmpty()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val categoryIds = timeToAdd.keys + extraTimeToSubtract.keys
|
|
||||||
|
|
||||||
try {
|
|
||||||
ApplyActionUtil.applyAppLogicAction(
|
|
||||||
action = AddUsedTimeActionVersion2(
|
|
||||||
dayOfEpoch = date.dayOfEpoch,
|
|
||||||
items = categoryIds.map { categoryId ->
|
|
||||||
AddUsedTimeActionItem(
|
|
||||||
categoryId = categoryId,
|
|
||||||
timeToAdd = timeToAdd[categoryId] ?: 0,
|
|
||||||
extraTimeToSubtract = extraTimeToSubtract[categoryId] ?: 0,
|
|
||||||
additionalCountingSlots = additionalSlots[categoryId] ?: emptySet(),
|
|
||||||
sessionDurationLimits = sessionDurationLimitSlots[categoryId] ?: emptySet()
|
|
||||||
)
|
|
||||||
},
|
|
||||||
trustedTimestamp = trustedTimestamp
|
|
||||||
),
|
|
||||||
appLogic = appLogic,
|
|
||||||
ignoreIfDeviceIsNotConfigured = true
|
|
||||||
)
|
|
||||||
} catch (ex: CategoryNotFoundException) {
|
|
||||||
if (BuildConfig.DEBUG) {
|
|
||||||
Log.d(LOG_TAG, "could not commit used times", ex)
|
|
||||||
}
|
}
|
||||||
|
} else false
|
||||||
|
val makeCommitByDifferentBaseData = this.dayOfEpoch != dayOfEpoch
|
||||||
|
val makeCommitByCountedTime = countedTime >= 30 * 1000 || countedTime >= maxTimeToAdd
|
||||||
|
val makeCommit = makeCommitByDifferntHandling || makeCommitByDifferentBaseData || makeCommitByCountedTime
|
||||||
|
|
||||||
// this is a very rare case if a category is deleted while it is used;
|
val madeCommit = if (makeCommit) {
|
||||||
// in this case there could be some lost time
|
doCommitPrivate()
|
||||||
// changes for other categories, but it's no big problem
|
} else {
|
||||||
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
timeToAdd.clear()
|
this.lastCategoryHandlings = handlings
|
||||||
extraTimeToSubtract.clear()
|
this.trustedTimestamp = trustedTimestamp
|
||||||
sessionDurationLimitSlots.clear()
|
this.dayOfEpoch = dayOfEpoch
|
||||||
trustedTimestamp = 0
|
|
||||||
additionalSlots.clear()
|
return madeCommit
|
||||||
shouldDoAutoCommit = false
|
}
|
||||||
|
|
||||||
|
suspend fun flush() {
|
||||||
|
doCommitPrivate()
|
||||||
|
|
||||||
|
lastCategoryHandlings = emptyList()
|
||||||
|
categoryIds = emptySet()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun doCommitPrivate(): Boolean {
|
||||||
|
val makeCommit = lastCategoryHandlings.isNotEmpty() && countedTime > 0
|
||||||
|
|
||||||
|
if (makeCommit) {
|
||||||
|
try {
|
||||||
|
ApplyActionUtil.applyAppLogicAction(
|
||||||
|
action = AddUsedTimeActionVersion2(
|
||||||
|
dayOfEpoch = dayOfEpoch,
|
||||||
|
items = lastCategoryHandlings.map { handling ->
|
||||||
|
AddUsedTimeActionItem(
|
||||||
|
categoryId = handling.createdWithCategoryRelatedData.category.id,
|
||||||
|
timeToAdd = countedTime,
|
||||||
|
extraTimeToSubtract = if (handling.shouldCountExtraTime) countedTime else 0,
|
||||||
|
additionalCountingSlots = handling.additionalTimeCountingSlots,
|
||||||
|
sessionDurationLimits = handling.sessionDurationSlotsToCount
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trustedTimestamp = if (lastCategoryHandlings.find { it.sessionDurationSlotsToCount.isNotEmpty() } != null) trustedTimestamp else 0
|
||||||
|
),
|
||||||
|
appLogic = appLogic,
|
||||||
|
ignoreIfDeviceIsNotConfigured = true
|
||||||
|
)
|
||||||
|
} catch (ex: CategoryNotFoundException) {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.d(LOG_TAG, "could not commit used times", ex)
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is a very rare case if a category is deleted while it is used;
|
||||||
|
// in this case there could be some lost time
|
||||||
|
// changes for other categories, but it's no big problem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
countedTime = 0
|
||||||
|
// doing this would cause a commit very soon again
|
||||||
|
// lastCategoryHandlings = emptyList()
|
||||||
|
// categoryIds = emptySet()
|
||||||
|
|
||||||
|
return makeCommit
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,20 +25,21 @@ import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import androidx.lifecycle.Transformations
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
import io.timelimit.android.async.Threads
|
import io.timelimit.android.async.Threads
|
||||||
import io.timelimit.android.coroutines.executeAndWait
|
import io.timelimit.android.data.extensions.sortedCategories
|
||||||
import io.timelimit.android.coroutines.runAsync
|
|
||||||
import io.timelimit.android.data.extensions.mapToTimezone
|
|
||||||
import io.timelimit.android.data.extensions.sorted
|
|
||||||
import io.timelimit.android.data.model.*
|
import io.timelimit.android.data.model.*
|
||||||
|
import io.timelimit.android.data.model.derived.DeviceAndUserRelatedData
|
||||||
|
import io.timelimit.android.data.model.derived.DeviceRelatedData
|
||||||
|
import io.timelimit.android.data.model.derived.UserRelatedData
|
||||||
import io.timelimit.android.databinding.LockFragmentBinding
|
import io.timelimit.android.databinding.LockFragmentBinding
|
||||||
import io.timelimit.android.databinding.LockFragmentCategoryButtonBinding
|
import io.timelimit.android.databinding.LockFragmentCategoryButtonBinding
|
||||||
import io.timelimit.android.date.DateInTimezone
|
import io.timelimit.android.date.DateInTimezone
|
||||||
|
import io.timelimit.android.integration.platform.BatteryStatus
|
||||||
import io.timelimit.android.livedata.*
|
import io.timelimit.android.livedata.*
|
||||||
import io.timelimit.android.logic.*
|
import io.timelimit.android.logic.*
|
||||||
|
import io.timelimit.android.logic.blockingreason.AppBaseHandling
|
||||||
|
import io.timelimit.android.logic.blockingreason.CategoryHandlingCache
|
||||||
import io.timelimit.android.sync.actions.AddCategoryAppsAction
|
import io.timelimit.android.sync.actions.AddCategoryAppsAction
|
||||||
import io.timelimit.android.sync.actions.IncrementCategoryExtraTimeAction
|
import io.timelimit.android.sync.actions.IncrementCategoryExtraTimeAction
|
||||||
import io.timelimit.android.sync.actions.UpdateCategoryTemporarilyBlockedAction
|
import io.timelimit.android.sync.actions.UpdateCategoryTemporarilyBlockedAction
|
||||||
|
@ -55,11 +56,13 @@ import io.timelimit.android.ui.manage.child.category.create.CreateCategoryDialog
|
||||||
import io.timelimit.android.ui.manage.child.primarydevice.UpdatePrimaryDeviceDialogFragment
|
import io.timelimit.android.ui.manage.child.primarydevice.UpdatePrimaryDeviceDialogFragment
|
||||||
import io.timelimit.android.ui.payment.RequiresPurchaseDialogFragment
|
import io.timelimit.android.ui.payment.RequiresPurchaseDialogFragment
|
||||||
import io.timelimit.android.ui.view.SelectTimeSpanViewListener
|
import io.timelimit.android.ui.view.SelectTimeSpanViewListener
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
class LockFragment : Fragment() {
|
class LockFragment : Fragment() {
|
||||||
companion object {
|
companion object {
|
||||||
private const val EXTRA_PACKAGE_NAME = "pkg"
|
private const val EXTRA_PACKAGE_NAME = "pkg"
|
||||||
private const val EXTRA_ACTIVITY = "activitiy"
|
private const val EXTRA_ACTIVITY = "activitiy"
|
||||||
|
private const val STATUS_DID_OPEN_SET_CURRENT_DEVICE_SCREEN = "didOpenSetCurrentDeviceScreen"
|
||||||
|
|
||||||
fun newInstance(packageName: String, activity: String?): LockFragment {
|
fun newInstance(packageName: String, activity: String?): LockFragment {
|
||||||
val result = LockFragment()
|
val result = LockFragment()
|
||||||
|
@ -76,6 +79,7 @@ class LockFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var didOpenSetCurrentDeviceScreen = false
|
||||||
private val packageName: String by lazy { arguments!!.getString(EXTRA_PACKAGE_NAME)!! }
|
private val packageName: String by lazy { arguments!!.getString(EXTRA_PACKAGE_NAME)!! }
|
||||||
private val activityName: String? by lazy {
|
private val activityName: String? by lazy {
|
||||||
if (arguments!!.containsKey(EXTRA_ACTIVITY))
|
if (arguments!!.containsKey(EXTRA_ACTIVITY))
|
||||||
|
@ -86,189 +90,273 @@ class LockFragment : Fragment() {
|
||||||
private val auth: ActivityViewModel by lazy { getActivityViewModel(activity!!) }
|
private val auth: ActivityViewModel by lazy { getActivityViewModel(activity!!) }
|
||||||
private val logic: AppLogic by lazy { DefaultAppLogic.with(context!!) }
|
private val logic: AppLogic by lazy { DefaultAppLogic.with(context!!) }
|
||||||
private val title: String? by lazy { logic.platformIntegration.getLocalAppTitle(packageName) }
|
private val title: String? by lazy { logic.platformIntegration.getLocalAppTitle(packageName) }
|
||||||
private val blockingReason: LiveData<BlockingReasonDetail> by lazy { BlockingReasonUtil(logic).getBlockingReason(packageName, activityName) }
|
private val deviceAndUserRelatedData: LiveData<DeviceAndUserRelatedData?> by lazy {
|
||||||
|
logic.database.derivedDataDao().getUserAndDeviceRelatedDataLive()
|
||||||
|
}
|
||||||
|
private val batteryStatus: LiveData<BatteryStatus> by lazy {
|
||||||
|
logic.platformIntegration.getBatteryStatusLive()
|
||||||
|
}
|
||||||
|
private lateinit var binding: LockFragmentBinding
|
||||||
|
private val handlingCache = CategoryHandlingCache()
|
||||||
|
private val realTime = RealTime.newInstance()
|
||||||
|
private val timeModificationListener: () -> Unit = { update() }
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
private val updateRunnable = Runnable { update() }
|
||||||
val binding = LockFragmentBinding.inflate(layoutInflater, container, false)
|
|
||||||
|
|
||||||
AuthenticationFab.manageAuthenticationFab(
|
fun scheduleUpdate(delay: Long) {
|
||||||
fab = binding.fab,
|
logic.timeApi.cancelScheduledAction(updateRunnable)
|
||||||
shouldHighlight = auth.shouldHighlightAuthenticationButton,
|
logic.timeApi.runDelayedByUptime(updateRunnable, delay)
|
||||||
authenticatedUser = auth.authenticatedUser,
|
}
|
||||||
fragment = this,
|
|
||||||
doesSupportAuth = liveDataFromValue(true)
|
fun unscheduleUpdate() {
|
||||||
|
logic.timeApi.cancelScheduledAction(updateRunnable)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun update() {
|
||||||
|
val deviceAndUserRelatedData = deviceAndUserRelatedData.value ?: return
|
||||||
|
val batteryStatus = batteryStatus.value ?: return
|
||||||
|
|
||||||
|
logic.realTimeLogic.getRealTime(realTime)
|
||||||
|
|
||||||
|
if (deviceAndUserRelatedData.userRelatedData?.user?.type != UserType.Child) {
|
||||||
|
binding.reason = BlockingReason.None
|
||||||
|
binding.handlers = null
|
||||||
|
activity?.finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handlingCache.reportStatus(
|
||||||
|
user = deviceAndUserRelatedData.userRelatedData,
|
||||||
|
assumeCurrentDevice = CurrentDeviceLogic.handleDeviceAsCurrentDevice(deviceAndUserRelatedData.deviceRelatedData, deviceAndUserRelatedData.userRelatedData),
|
||||||
|
batteryStatus = batteryStatus,
|
||||||
|
timeInMillis = realTime.timeInMillis,
|
||||||
|
shouldTrustTimeTemporarily = realTime.shouldTrustTimeTemporarily
|
||||||
)
|
)
|
||||||
|
|
||||||
liveDataFromFunction { logic.timeApi.getCurrentTimeInMillis() }.observe(this, Observer {
|
val appBaseHandling = AppBaseHandling.calculate(
|
||||||
systemTime ->
|
foregroundAppPackageName = packageName,
|
||||||
|
foregroundAppActivityName = activityName,
|
||||||
|
deviceRelatedData = deviceAndUserRelatedData.deviceRelatedData,
|
||||||
|
userRelatedData = deviceAndUserRelatedData.userRelatedData,
|
||||||
|
pauseForegroundAppBackgroundLoop = false,
|
||||||
|
pauseCounting = false
|
||||||
|
)
|
||||||
|
|
||||||
binding.currentTime = DateUtils.formatDateTime(
|
binding.activityName = if (deviceAndUserRelatedData.deviceRelatedData.deviceEntry.enableActivityLevelBlocking)
|
||||||
context,
|
activityName?.removePrefix(packageName)
|
||||||
systemTime!!,
|
else
|
||||||
DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_TIME or
|
null
|
||||||
DateUtils.FORMAT_SHOW_YEAR or DateUtils.FORMAT_SHOW_WEEKDAY
|
|
||||||
|
if (appBaseHandling is AppBaseHandling.UseCategories) {
|
||||||
|
val categoryHandlings = appBaseHandling.categoryIds.map { handlingCache.get(it) }
|
||||||
|
val blockingHandling = categoryHandlings.find { it.shouldBlockActivities }
|
||||||
|
|
||||||
|
if (blockingHandling == null) {
|
||||||
|
binding.reason = BlockingReason.None
|
||||||
|
binding.handlers = null
|
||||||
|
activity?.finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.appCategoryTitle = blockingHandling.createdWithCategoryRelatedData.category.title
|
||||||
|
binding.reason = blockingHandling.activityBlockingReason
|
||||||
|
binding.blockedKindLabel = when (appBaseHandling.level) {
|
||||||
|
BlockingLevel.Activity -> "Activity"
|
||||||
|
BlockingLevel.App -> "App"
|
||||||
|
}
|
||||||
|
setupHandlers(
|
||||||
|
deviceId = deviceAndUserRelatedData.deviceRelatedData.deviceEntry.id,
|
||||||
|
blockedCategoryId = blockingHandling.createdWithCategoryRelatedData.category.id,
|
||||||
|
userRelatedData = deviceAndUserRelatedData.userRelatedData
|
||||||
|
)
|
||||||
|
bindExtraTimeView(
|
||||||
|
deviceRelatedData = deviceAndUserRelatedData.deviceRelatedData,
|
||||||
|
categoryId = blockingHandling.createdWithCategoryRelatedData.category.id,
|
||||||
|
timeZone = deviceAndUserRelatedData.userRelatedData.timeZone
|
||||||
|
)
|
||||||
|
binding.manageDisableTimeLimits.handlers = ManageDisableTimelimitsViewHelper.createHandlers(
|
||||||
|
childId = deviceAndUserRelatedData.userRelatedData.user.id,
|
||||||
|
childTimezone = deviceAndUserRelatedData.userRelatedData.user.timeZone,
|
||||||
|
activity = activity!!,
|
||||||
|
hasFullVersion = deviceAndUserRelatedData.deviceRelatedData.isConnectedAndHasPremium || deviceAndUserRelatedData.deviceRelatedData.isLocalMode
|
||||||
)
|
)
|
||||||
})
|
|
||||||
|
|
||||||
val enableActivityLevelBlocking = logic.deviceEntry.map { it?.enableActivityLevelBlocking ?: false }
|
if (blockingHandling.activityBlockingReason == BlockingReason.RequiresCurrentDevice && !didOpenSetCurrentDeviceScreen && isResumed) {
|
||||||
|
setThisDeviceAsCurrentDevice()
|
||||||
|
}
|
||||||
|
|
||||||
binding.packageName = packageName
|
scheduleUpdate((blockingHandling.dependsOnMaxTime - realTime.timeInMillis))
|
||||||
|
} else if (appBaseHandling is AppBaseHandling.BlockDueToNoCategory) {
|
||||||
|
binding.appCategoryTitle = null
|
||||||
|
binding.reason = BlockingReason.NotPartOfAnCategory
|
||||||
|
binding.blockedKindLabel = "App"
|
||||||
|
setupHandlers(
|
||||||
|
deviceId = deviceAndUserRelatedData.deviceRelatedData.deviceEntry.id,
|
||||||
|
blockedCategoryId = null,
|
||||||
|
userRelatedData = deviceAndUserRelatedData.userRelatedData
|
||||||
|
)
|
||||||
|
|
||||||
enableActivityLevelBlocking.observe(this, Observer {
|
bindAddToCategoryOptions(deviceAndUserRelatedData.userRelatedData)
|
||||||
binding.activityName = if (it) activityName?.removePrefix(packageName) else null
|
|
||||||
})
|
|
||||||
|
|
||||||
if (title != null) {
|
|
||||||
binding.appTitle = title
|
|
||||||
} else {
|
} else {
|
||||||
binding.appTitle = "???"
|
binding.reason = BlockingReason.None
|
||||||
|
binding.handlers = null
|
||||||
|
activity?.finish()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
binding.appIcon.setImageDrawable(logic.platformIntegration.getAppIcon(packageName))
|
private fun setupHandlers(deviceId: String, userRelatedData: UserRelatedData, blockedCategoryId: String?) {
|
||||||
|
binding.handlers = object: Handlers {
|
||||||
blockingReason.observe(this, Observer {
|
override fun openMainApp() {
|
||||||
when (it) {
|
startActivity(Intent(context, MainActivity::class.java))
|
||||||
is NoBlockingReason -> activity!!.finish()
|
|
||||||
is BlockedReasonDetails -> {
|
|
||||||
binding.reason = it.reason
|
|
||||||
binding.blockedKindLabel = when (it.level) {
|
|
||||||
BlockingLevel.Activity -> "Activity"
|
|
||||||
BlockingLevel.App -> "App"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.let { /* require handling all cases */ }
|
|
||||||
})
|
|
||||||
|
|
||||||
val categories = logic.deviceUserEntry.switchMap {
|
|
||||||
user ->
|
|
||||||
|
|
||||||
if (user != null && user.type == UserType.Child) {
|
|
||||||
Transformations.map(logic.database.category().getCategoriesByChildId(user.id)) {
|
|
||||||
categories ->
|
|
||||||
|
|
||||||
user to categories
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
liveDataFromValue(null as Pair<User, List<Category>>?)
|
|
||||||
}
|
}
|
||||||
}.ignoreUnchanged()
|
|
||||||
|
|
||||||
// bind category name of the app
|
override fun allowTemporarily() {
|
||||||
val appCategory = categories.switchMap {
|
if (auth.requestAuthenticationOrReturnTrue()) {
|
||||||
status ->
|
val database = logic.database
|
||||||
|
|
||||||
if (status == null) {
|
// this accesses the database directly because it is not synced
|
||||||
liveDataFromValue(null as Category?)
|
Threads.database.submit {
|
||||||
} else {
|
try {
|
||||||
val (_, categoryItems) = status
|
database.temporarilyAllowedApp().addTemporarilyAllowedAppSync(TemporarilyAllowedApp(
|
||||||
|
deviceId = deviceId,
|
||||||
blockingReason.map { reason ->
|
packageName = packageName
|
||||||
if (reason is BlockedReasonDetails) {
|
))
|
||||||
reason.categoryId
|
} catch (ex: SQLiteConstraintException) {
|
||||||
} else {
|
// ignore this
|
||||||
null
|
//
|
||||||
}
|
// this happens when touching that option more than once very fast
|
||||||
}.map { categoryId ->
|
// or if the device is under load
|
||||||
categoryItems.find { it.id == categoryId }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
appCategory.observe(this, Observer {
|
|
||||||
binding.appCategoryTitle = it?.title
|
|
||||||
})
|
|
||||||
|
|
||||||
// bind add to category list
|
|
||||||
categories.observe(this, Observer {
|
|
||||||
status ->
|
|
||||||
|
|
||||||
binding.addToCategoryOptions.removeAllViews()
|
|
||||||
|
|
||||||
if (status == null) {
|
|
||||||
// nothing to do
|
|
||||||
} else {
|
|
||||||
val (user, categoryEntries) = status
|
|
||||||
|
|
||||||
categoryEntries.sorted().forEach {
|
|
||||||
category ->
|
|
||||||
|
|
||||||
val button = LockFragmentCategoryButtonBinding.inflate(LayoutInflater.from(context), binding.addToCategoryOptions, true)
|
|
||||||
|
|
||||||
button.title = category.title
|
|
||||||
button.button.setOnClickListener {
|
|
||||||
_ ->
|
|
||||||
|
|
||||||
auth.tryDispatchParentAction(
|
|
||||||
AddCategoryAppsAction(
|
|
||||||
categoryId = category.id,
|
|
||||||
packageNames = listOf(packageName)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
run {
|
|
||||||
val button = LockFragmentCategoryButtonBinding.inflate(LayoutInflater.from(context), binding.addToCategoryOptions, true)
|
|
||||||
|
|
||||||
button.title = getString(R.string.create_category_title)
|
|
||||||
button.button.setOnClickListener {
|
|
||||||
if (auth.requestAuthenticationOrReturnTrue()) {
|
|
||||||
CreateCategoryDialogFragment
|
|
||||||
.newInstance(ManageChildFragmentArgs(childId = user.id))
|
|
||||||
.show(fragmentManager!!)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
// bind adding extra time controls
|
override fun confirmLocalTime() {
|
||||||
|
if (auth.requestAuthenticationOrReturnTrue()) {
|
||||||
|
logic.realTimeLogic.confirmLocalTime()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun disableTimeVerification() {
|
||||||
|
auth.tryDispatchParentAction(
|
||||||
|
UpdateNetworkTimeVerificationAction(
|
||||||
|
deviceId = deviceId,
|
||||||
|
mode = NetworkTime.IfPossible
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun disableTemporarilyLockForAllCategories() {
|
||||||
|
auth.tryDispatchParentActions(
|
||||||
|
userRelatedData.categories
|
||||||
|
.filter { it.category.temporarilyBlocked }
|
||||||
|
.map {
|
||||||
|
UpdateCategoryTemporarilyBlockedAction(
|
||||||
|
categoryId = it.category.id,
|
||||||
|
blocked = false,
|
||||||
|
endTime = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun disableTemporarilyLockForCurrentCategory() {
|
||||||
|
blockedCategoryId ?: return
|
||||||
|
|
||||||
|
auth.tryDispatchParentAction(
|
||||||
|
UpdateCategoryTemporarilyBlockedAction(
|
||||||
|
categoryId = blockedCategoryId,
|
||||||
|
blocked = false,
|
||||||
|
endTime = null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showAuthenticationScreen() {
|
||||||
|
(activity as LockActivity).showAuthenticationScreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setThisDeviceAsCurrentDevice() = this@LockFragment.setThisDeviceAsCurrentDevice()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setThisDeviceAsCurrentDevice() {
|
||||||
|
didOpenSetCurrentDeviceScreen = true
|
||||||
|
|
||||||
|
UpdatePrimaryDeviceDialogFragment
|
||||||
|
.newInstance(UpdatePrimaryDeviceRequestType.SetThisDevice)
|
||||||
|
.show(parentFragmentManager)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindAddToCategoryOptions(userRelatedData: UserRelatedData) {
|
||||||
|
binding.addToCategoryOptions.removeAllViews()
|
||||||
|
|
||||||
|
userRelatedData.categories.sortedCategories().forEach { category ->
|
||||||
|
LockFragmentCategoryButtonBinding.inflate(LayoutInflater.from(context), binding.addToCategoryOptions, true).let { button ->
|
||||||
|
button.title = category.category.title
|
||||||
|
button.button.setOnClickListener { _ ->
|
||||||
|
auth.tryDispatchParentAction(
|
||||||
|
AddCategoryAppsAction(
|
||||||
|
categoryId = category.category.id,
|
||||||
|
packageNames = listOf(packageName)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LockFragmentCategoryButtonBinding.inflate(LayoutInflater.from(context), binding.addToCategoryOptions, true).let { button ->
|
||||||
|
button.title = getString(R.string.create_category_title)
|
||||||
|
button.button.setOnClickListener {
|
||||||
|
if (auth.requestAuthenticationOrReturnTrue()) {
|
||||||
|
CreateCategoryDialogFragment
|
||||||
|
.newInstance(ManageChildFragmentArgs(childId = userRelatedData.user.id))
|
||||||
|
.show(fragmentManager!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindExtraTimeView(deviceRelatedData: DeviceRelatedData, categoryId: String, timeZone: TimeZone) {
|
||||||
|
val hasFullVersion = deviceRelatedData.isConnectedAndHasPremium || deviceRelatedData.isLocalMode
|
||||||
|
|
||||||
|
binding.extraTimeBtnOk.setOnClickListener {
|
||||||
|
binding.extraTimeSelection.clearNumberPickerFocus()
|
||||||
|
|
||||||
|
if (hasFullVersion) {
|
||||||
|
if (auth.requestAuthenticationOrReturnTrue()) {
|
||||||
|
val extraTimeToAdd = binding.extraTimeSelection.timeInMillis
|
||||||
|
|
||||||
|
if (extraTimeToAdd > 0) {
|
||||||
|
binding.extraTimeBtnOk.isEnabled = false
|
||||||
|
|
||||||
|
val date = DateInTimezone.newInstance(auth.logic.timeApi.getCurrentTimeInMillis(), timeZone)
|
||||||
|
|
||||||
|
auth.tryDispatchParentAction(IncrementCategoryExtraTimeAction(
|
||||||
|
categoryId = categoryId,
|
||||||
|
addedExtraTime = extraTimeToAdd,
|
||||||
|
extraTimeDay = if (binding.switchLimitExtraTimeToToday.isChecked) date.dayOfEpoch else -1
|
||||||
|
))
|
||||||
|
|
||||||
|
binding.extraTimeBtnOk.isEnabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
RequiresPurchaseDialogFragment().show(parentFragmentManager)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initExtraTimeView() {
|
||||||
binding.extraTimeTitle.setOnClickListener {
|
binding.extraTimeTitle.setOnClickListener {
|
||||||
HelpDialogFragment.newInstance(
|
HelpDialogFragment.newInstance(
|
||||||
title = R.string.lock_extratime_title,
|
title = R.string.lock_extratime_title,
|
||||||
text = R.string.lock_extratime_text
|
text = R.string.lock_extratime_text
|
||||||
).show(fragmentManager!!)
|
).show(parentFragmentManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
logic.fullVersion.shouldProvideFullVersionFunctions.observe(this, Observer { hasFullVersion ->
|
logic.database.config().getEnableAlternativeDurationSelectionAsync().observe(viewLifecycleOwner, Observer {
|
||||||
binding.extraTimeBtnOk.setOnClickListener {
|
|
||||||
binding.extraTimeSelection.clearNumberPickerFocus()
|
|
||||||
|
|
||||||
if (hasFullVersion) {
|
|
||||||
if (auth.isParentAuthenticated()) {
|
|
||||||
runAsync {
|
|
||||||
val extraTimeToAdd = binding.extraTimeSelection.timeInMillis
|
|
||||||
|
|
||||||
if (extraTimeToAdd > 0) {
|
|
||||||
binding.extraTimeBtnOk.isEnabled = false
|
|
||||||
|
|
||||||
val categoryId = appCategory.waitForNullableValue()?.id
|
|
||||||
val timezone = logic.deviceUserEntry.mapToTimezone().waitForNonNullValue()
|
|
||||||
val date = DateInTimezone.newInstance(auth.logic.timeApi.getCurrentTimeInMillis(), timezone)
|
|
||||||
|
|
||||||
if (categoryId != null) {
|
|
||||||
auth.tryDispatchParentAction(IncrementCategoryExtraTimeAction(
|
|
||||||
categoryId = categoryId,
|
|
||||||
addedExtraTime = extraTimeToAdd,
|
|
||||||
extraTimeDay = if (binding.switchLimitExtraTimeToToday.isChecked) date.dayOfEpoch else -1
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
Snackbar.make(binding.root, R.string.error_general, Snackbar.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.extraTimeBtnOk.isEnabled = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
auth.requestAuthentication()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
RequiresPurchaseDialogFragment().show(fragmentManager!!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
logic.database.config().getEnableAlternativeDurationSelectionAsync().observe(this, Observer {
|
|
||||||
binding.extraTimeSelection.enablePickerMode(it)
|
binding.extraTimeSelection.enablePickerMode(it)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -283,150 +371,76 @@ class LockFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// bind disable time limits
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
mergeLiveData(logic.deviceUserEntry, logic.fullVersion.shouldProvideFullVersionFunctions).observe(this, Observer {
|
super.onCreate(savedInstanceState)
|
||||||
(child, hasFullVersion) ->
|
|
||||||
|
|
||||||
if (child != null) {
|
if (savedInstanceState != null) {
|
||||||
binding.manageDisableTimeLimits.handlers = ManageDisableTimelimitsViewHelper.createHandlers(
|
didOpenSetCurrentDeviceScreen = savedInstanceState.getBoolean(STATUS_DID_OPEN_SET_CURRENT_DEVICE_SCREEN)
|
||||||
childId = child.id,
|
}
|
||||||
childTimezone = child.timeZone,
|
}
|
||||||
activity = activity!!,
|
|
||||||
hasFullVersion = hasFullVersion == true
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
)
|
super.onSaveInstanceState(outState)
|
||||||
}
|
|
||||||
|
outState.putBoolean(STATUS_DID_OPEN_SET_CURRENT_DEVICE_SCREEN, didOpenSetCurrentDeviceScreen)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
binding = LockFragmentBinding.inflate(layoutInflater, container, false)
|
||||||
|
|
||||||
|
AuthenticationFab.manageAuthenticationFab(
|
||||||
|
fab = binding.fab,
|
||||||
|
shouldHighlight = auth.shouldHighlightAuthenticationButton,
|
||||||
|
authenticatedUser = auth.authenticatedUser,
|
||||||
|
fragment = this,
|
||||||
|
doesSupportAuth = liveDataFromValue(true)
|
||||||
|
)
|
||||||
|
|
||||||
|
liveDataFromFunction { logic.timeApi.getCurrentTimeInMillis() }.observe(viewLifecycleOwner, Observer {
|
||||||
|
systemTime ->
|
||||||
|
|
||||||
|
binding.currentTime = DateUtils.formatDateTime(
|
||||||
|
context,
|
||||||
|
systemTime!!,
|
||||||
|
DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_TIME or
|
||||||
|
DateUtils.FORMAT_SHOW_YEAR or DateUtils.FORMAT_SHOW_WEEKDAY
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
mergeLiveData(logic.deviceUserEntry, liveDataFromFunction { logic.realTimeLogic.getCurrentTimeInMillis() }).map {
|
deviceAndUserRelatedData.observe(viewLifecycleOwner, Observer { update() })
|
||||||
(child, time) ->
|
batteryStatus.observe(viewLifecycleOwner, Observer { update() })
|
||||||
|
|
||||||
if (time == null || child == null) {
|
binding.packageName = packageName
|
||||||
null
|
|
||||||
} else {
|
|
||||||
ManageDisableTimelimitsViewHelper.getDisabledUntilString(child, time, context!!)
|
|
||||||
}
|
|
||||||
}.observe(this, Observer {
|
|
||||||
binding.manageDisableTimeLimits.disableTimeLimitsUntilString = it
|
|
||||||
})
|
|
||||||
|
|
||||||
binding.handlers = object: Handlers {
|
binding.appTitle = title ?: "???"
|
||||||
override fun openMainApp() {
|
binding.appIcon.setImageDrawable(logic.platformIntegration.getAppIcon(packageName))
|
||||||
startActivity(Intent(context, MainActivity::class.java))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun allowTemporarily() {
|
initExtraTimeView()
|
||||||
if (auth.requestAuthenticationOrReturnTrue()) {
|
|
||||||
val database = logic.database
|
|
||||||
val deviceIdLive = logic.deviceId
|
|
||||||
|
|
||||||
// this accesses the database directly because it is not synced
|
|
||||||
runAsync {
|
|
||||||
val deviceId = deviceIdLive.waitForNullableValue()
|
|
||||||
|
|
||||||
if (deviceId != null) {
|
|
||||||
Threads.database.executeAndWait(Runnable {
|
|
||||||
try {
|
|
||||||
database.temporarilyAllowedApp().addTemporarilyAllowedAppSync(TemporarilyAllowedApp(
|
|
||||||
deviceId = deviceId,
|
|
||||||
packageName = packageName
|
|
||||||
))
|
|
||||||
} catch (ex: SQLiteConstraintException) {
|
|
||||||
// ignore this
|
|
||||||
//
|
|
||||||
// this happens when touching that option more than once very fast
|
|
||||||
// or if the device is under load
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun confirmLocalTime() {
|
|
||||||
if (auth.requestAuthenticationOrReturnTrue()) {
|
|
||||||
logic.realTimeLogic.confirmLocalTime()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun disableTimeVerification() {
|
|
||||||
if (auth.requestAuthenticationOrReturnTrue()) {
|
|
||||||
runAsync {
|
|
||||||
val deviceId = logic.deviceId.waitForNullableValue()
|
|
||||||
|
|
||||||
if (deviceId != null) {
|
|
||||||
auth.tryDispatchParentAction(
|
|
||||||
UpdateNetworkTimeVerificationAction(
|
|
||||||
deviceId = deviceId,
|
|
||||||
mode = NetworkTime.IfPossible
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun disableTemporarilyLockForAllCategories() {
|
|
||||||
if (auth.requestAuthenticationOrReturnTrue()) {
|
|
||||||
runAsync {
|
|
||||||
categories.waitForNullableValue()?.second?.filter { it.temporarilyBlocked }?.map { it.id }?.forEach {
|
|
||||||
categoryId ->
|
|
||||||
|
|
||||||
auth.tryDispatchParentAction(
|
|
||||||
UpdateCategoryTemporarilyBlockedAction(
|
|
||||||
categoryId = categoryId,
|
|
||||||
blocked = false,
|
|
||||||
endTime = null
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun disableTemporarilyLockForCurrentCategory() {
|
|
||||||
if (auth.requestAuthenticationOrReturnTrue()) {
|
|
||||||
runAsync {
|
|
||||||
val category = appCategory.waitForNullableValue()
|
|
||||||
|
|
||||||
if (category != null) {
|
|
||||||
auth.tryDispatchParentAction(
|
|
||||||
UpdateCategoryTemporarilyBlockedAction(
|
|
||||||
categoryId = category.id,
|
|
||||||
blocked = false,
|
|
||||||
endTime = null
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun showAuthenticationScreen() {
|
|
||||||
(activity as LockActivity).showAuthenticationScreen()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setThisDeviceAsCurrentDevice() {
|
|
||||||
UpdatePrimaryDeviceDialogFragment
|
|
||||||
.newInstance(UpdatePrimaryDeviceRequestType.SetThisDevice)
|
|
||||||
.show(fragmentManager!!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (savedInstanceState == null) {
|
|
||||||
runAsync {
|
|
||||||
val reason = blockingReason.waitForNonNullValue()
|
|
||||||
|
|
||||||
if (reason is BlockedReasonDetails && reason.reason == BlockingReason.RequiresCurrentDevice) {
|
|
||||||
if (isResumed) {
|
|
||||||
binding.handlers!!.setThisDeviceAsCurrentDevice()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
logic.realTimeLogic.registerTimeModificationListener(timeModificationListener)
|
||||||
|
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
|
||||||
|
logic.realTimeLogic.unregisterTimeModificationListener(timeModificationListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
|
||||||
|
unscheduleUpdate()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Handlers {
|
interface Handlers {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue