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