mirror of
https://codeberg.org/timelimit/timelimit-android.git
synced 2025-10-03 17:59:51 +02:00
Initial commit
This commit is contained in:
commit
4d322f6798
648 changed files with 52974 additions and 0 deletions
32
app/src/main/java/io/timelimit/android/Application.kt
Normal file
32
app/src/main/java/io/timelimit/android/Application.kt
Normal file
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android
|
||||
|
||||
import android.app.Application
|
||||
import com.jakewharton.threetenabp.AndroidThreeTen
|
||||
import org.solovyev.android.checkout.Billing
|
||||
|
||||
class Application : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
AndroidThreeTen.init(this)
|
||||
}
|
||||
|
||||
val billing = Billing(this, object: Billing.DefaultConfiguration() {
|
||||
override fun getPublicKey() = BuildConfig.googlePlayKey
|
||||
})
|
||||
}
|
27
app/src/main/java/io/timelimit/android/async/Threads.kt
Normal file
27
app/src/main/java/io/timelimit/android/async/Threads.kt
Normal file
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.async
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
object Threads {
|
||||
val database = Executors.newSingleThreadExecutor()!!
|
||||
val mainThreadHandler = Handler(Looper.getMainLooper())
|
||||
val network = Executors.newSingleThreadExecutor()
|
||||
val crypto = Executors.newSingleThreadExecutor()
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.coroutines
|
||||
|
||||
import java.util.concurrent.Executor
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
suspend fun Executor.executeAndWait(runnable: Runnable) {
|
||||
suspendCoroutine<Void?> {
|
||||
this.execute {
|
||||
try {
|
||||
runnable.run()
|
||||
|
||||
it.resume(null)
|
||||
} catch (ex: Exception) {
|
||||
it.resumeWithException(ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <R> Executor.executeAndWait(function: () -> R) = suspendCoroutine<R> {
|
||||
this.execute {
|
||||
try {
|
||||
it.resume(function())
|
||||
} catch (ex: Exception) {
|
||||
it.resumeWithException(ex)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.coroutines
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
open class CoroutineFragment: Fragment(), CoroutineScope {
|
||||
private val job = Job()
|
||||
|
||||
override val coroutineContext: CoroutineContext
|
||||
get() = job + Dispatchers.Main
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
}
|
38
app/src/main/java/io/timelimit/android/coroutines/OkHttp.kt
Normal file
38
app/src/main/java/io/timelimit/android/coroutines/OkHttp.kt
Normal file
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.coroutines
|
||||
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.Response
|
||||
import java.io.IOException
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
suspend fun Call.waitForResponse(): Response {
|
||||
return suspendCoroutine {
|
||||
this.enqueue(object: Callback {
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
it.resumeWithException(e)
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
it.resume(response)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.coroutines
|
||||
|
||||
import io.timelimit.android.async.Threads
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun <T> runAsync(block: suspend CoroutineScope.() -> T) {
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
block()
|
||||
}.invokeOnCompletion {
|
||||
if (it != null && (!(it is CancellationException))) {
|
||||
Threads.mainThreadHandler.post {
|
||||
throw it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> runAsyncExpectForever(block: suspend CoroutineScope.() -> T) {
|
||||
runAsync {
|
||||
block()
|
||||
|
||||
throw IllegalStateException()
|
||||
}
|
||||
}
|
21
app/src/main/java/io/timelimit/android/crypto/AES.kt
Normal file
21
app/src/main/java/io/timelimit/android/crypto/AES.kt
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.crypto
|
||||
|
||||
object AES {
|
||||
const val KEY_LENGTH_BYTES = 16
|
||||
const val IV_LENGTH_BYTES = 16
|
||||
}
|
61
app/src/main/java/io/timelimit/android/crypto/HexString.kt
Normal file
61
app/src/main/java/io/timelimit/android/crypto/HexString.kt
Normal file
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.crypto
|
||||
|
||||
object HexString {
|
||||
fun toHex(data: ByteArray): String {
|
||||
return StringBuilder(data.size * 2).apply {
|
||||
for (i in data.indices) {
|
||||
append(Integer.toString(data[i].toInt() shr 4 and 15, 16))
|
||||
append(Integer.toString(data[i].toInt() and 15, 16))
|
||||
}
|
||||
}.toString()
|
||||
}
|
||||
|
||||
fun fromHex(value: String): ByteArray {
|
||||
if (value.length % 2 != 0) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
val result = ByteArray(value.length / 2)
|
||||
|
||||
for (index in result.indices) {
|
||||
result.set(
|
||||
index = index,
|
||||
value = (
|
||||
(Integer.parseInt(value[index * 2 + 0].toString(), 16) shl 4) or
|
||||
Integer.parseInt(value[index * 2 + 1].toString(), 16)
|
||||
).toByte()
|
||||
)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
fun assertIsHexString(value: String) {
|
||||
if (value.length % 2 != 0) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
value.forEach {
|
||||
char ->
|
||||
|
||||
if ("0123456789abcdef".indexOf(char) == -1) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.crypto
|
||||
|
||||
import org.mindrot.jbcrypt.BCrypt
|
||||
|
||||
object PasswordHashing {
|
||||
fun hashSync(password: String) = hashSyncWithSalt(password, generateSalt())
|
||||
|
||||
fun hashSyncWithSalt(password: String, salt: String): String = BCrypt.hashpw(password, salt)
|
||||
fun generateSalt(): String = BCrypt.gensalt()
|
||||
|
||||
fun validateSync(password: String, hashed: String): Boolean {
|
||||
try {
|
||||
return BCrypt.checkpw(password, hashed)
|
||||
} catch (ex: Exception) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
30
app/src/main/java/io/timelimit/android/crypto/RandomBytes.kt
Normal file
30
app/src/main/java/io/timelimit/android/crypto/RandomBytes.kt
Normal file
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.crypto
|
||||
|
||||
import java.security.SecureRandom
|
||||
|
||||
object RandomBytes {
|
||||
private val random: SecureRandom by lazy { SecureRandom() }
|
||||
|
||||
fun randomBytes(length: Int): ByteArray {
|
||||
val result = ByteArray(length)
|
||||
|
||||
random.nextBytes(result)
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
28
app/src/main/java/io/timelimit/android/crypto/Sha512.kt
Normal file
28
app/src/main/java/io/timelimit/android/crypto/Sha512.kt
Normal file
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.crypto
|
||||
|
||||
import java.security.MessageDigest
|
||||
|
||||
object Sha512 {
|
||||
private val messageDigest: MessageDigest by lazy { MessageDigest.getInstance("SHA-512") }
|
||||
|
||||
fun hashSync(data: String): String {
|
||||
return HexString.toHex(hashSync(data.toByteArray(charset("UTF-8"))))
|
||||
}
|
||||
|
||||
fun hashSync(data: ByteArray) = messageDigest.digest(data)
|
||||
}
|
59
app/src/main/java/io/timelimit/android/data/Database.kt
Normal file
59
app/src/main/java/io/timelimit/android/data/Database.kt
Normal file
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.data
|
||||
|
||||
import io.timelimit.android.data.dao.*
|
||||
import java.io.Closeable
|
||||
|
||||
interface Database {
|
||||
fun app(): AppDao
|
||||
fun categoryApp(): CategoryAppDao
|
||||
fun category(): CategoryDao
|
||||
fun config(): ConfigDao
|
||||
fun device(): DeviceDao
|
||||
fun timeLimitRules(): TimeLimitRuleDao
|
||||
fun usedTimes(): UsedTimeDao
|
||||
fun user(): UserDao
|
||||
fun temporarilyAllowedApp(): TemporarilyAllowedAppDao
|
||||
fun pendingSyncAction(): PendingSyncActionDao
|
||||
|
||||
fun beginTransaction()
|
||||
fun setTransactionSuccessful()
|
||||
fun endTransaction()
|
||||
|
||||
fun deleteAllData()
|
||||
fun close()
|
||||
}
|
||||
|
||||
fun Database.transaction(): Transaction {
|
||||
val db = this
|
||||
|
||||
db.beginTransaction()
|
||||
|
||||
return object: Transaction {
|
||||
override fun setSuccess() {
|
||||
db.setTransactionSuccessful()
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
db.endTransaction()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface Transaction: Closeable {
|
||||
fun setSuccess()
|
||||
}
|
54
app/src/main/java/io/timelimit/android/data/IdGenerator.kt
Normal file
54
app/src/main/java/io/timelimit/android/data/IdGenerator.kt
Normal file
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.data
|
||||
|
||||
import java.security.SecureRandom
|
||||
|
||||
object IdGenerator {
|
||||
private const val LENGTH = 6
|
||||
private const val CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
private val random: SecureRandom by lazy { SecureRandom() }
|
||||
|
||||
fun generateId(): String {
|
||||
val output = StringBuilder(LENGTH)
|
||||
|
||||
for (i in 1..LENGTH) {
|
||||
output.append(CHARS[random.nextInt(CHARS.length)])
|
||||
}
|
||||
|
||||
return output.toString()
|
||||
}
|
||||
|
||||
private fun isIdValid(id: String): Boolean {
|
||||
if (id.length != LENGTH) {
|
||||
return false
|
||||
}
|
||||
|
||||
for (char in id) {
|
||||
if (!CHARS.contains(char)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun assertIdValid(id: String) {
|
||||
if (!isIdValid(id)) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.data
|
||||
|
||||
import android.util.JsonWriter
|
||||
|
||||
interface JsonSerializable {
|
||||
fun serialize(writer: JsonWriter)
|
||||
}
|
88
app/src/main/java/io/timelimit/android/data/Migrations.kt
Normal file
88
app/src/main/java/io/timelimit/android/data/Migrations.kt
Normal file
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.data
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
object DatabaseMigrations {
|
||||
val MIGRATE_TO_V2 = object: Migration(1, 2) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE device ADD COLUMN did_report_uninstall INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V3 = object: Migration(2, 3) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE device ADD COLUMN is_user_kept_signed_in INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V4 = object: Migration(3, 4) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE `user` ADD COLUMN `category_for_not_assigned_apps` TEXT NOT NULL DEFAULT \"\"")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V5 = object: Migration(4, 5) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE `category` ADD COLUMN `parent_category_id` TEXT NOT NULL DEFAULT \"\"")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V6 = object: Migration(5, 6) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE `device` ADD COLUMN `show_device_connected` INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V7 = object: Migration(6, 7) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE `device` ADD COLUMN `default_user` TEXT NOT NULL DEFAULT \"\"")
|
||||
database.execSQL("ALTER TABLE `device` ADD COLUMN `default_user_timeout` INTEGER NOT NULL DEFAULT 0")
|
||||
database.execSQL("ALTER TABLE `user` ADD COLUMN `relax_primary_device` INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V8 = object: Migration(7, 8) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
// this is empty
|
||||
//
|
||||
// a new possible enum value was added, the version upgrade enables the downgrade mechanism
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V9 = object: Migration(8, 9) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE `device` ADD COLUMN `did_reboot` INTEGER NOT NULL DEFAULT 0")
|
||||
database.execSQL("ALTER TABLE `device` ADD COLUMN `consider_reboot_manipulation` INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V10 = object: Migration(9, 10) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
// this is empty
|
||||
//
|
||||
// a new possible enum value was added, the version upgrade enables the downgrade mechanism
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V11 = object: Migration(10, 11) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE `user` ADD COLUMN `mail_notification_flags` INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
}
|
106
app/src/main/java/io/timelimit/android/data/RoomDatabase.kt
Normal file
106
app/src/main/java/io/timelimit/android/data/RoomDatabase.kt
Normal file
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.data
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import io.timelimit.android.data.model.*
|
||||
|
||||
@Database(entities = [
|
||||
User::class,
|
||||
Device::class,
|
||||
App::class,
|
||||
CategoryApp::class,
|
||||
Category::class,
|
||||
UsedTimeItem::class,
|
||||
TimeLimitRule::class,
|
||||
ConfigurationItem::class,
|
||||
TemporarilyAllowedApp::class,
|
||||
PendingSyncAction::class
|
||||
], version = 11)
|
||||
abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database {
|
||||
companion object {
|
||||
private val lock = Object()
|
||||
private var instance: io.timelimit.android.data.Database? = null
|
||||
const val DEFAULT_DB_NAME = "db"
|
||||
const val BACKUP_DB_NAME = "db2"
|
||||
|
||||
fun with(context: Context): io.timelimit.android.data.Database {
|
||||
if (instance == null) {
|
||||
synchronized(lock) {
|
||||
if (instance == null) {
|
||||
instance = createOrOpenLocalStorageInstance(context, DEFAULT_DB_NAME)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return instance!!
|
||||
}
|
||||
|
||||
fun createInMemoryInstance(context: Context): io.timelimit.android.data.Database {
|
||||
return Room.inMemoryDatabaseBuilder(
|
||||
context,
|
||||
io.timelimit.android.data.RoomDatabase::class.java
|
||||
).build()
|
||||
}
|
||||
|
||||
fun createOrOpenLocalStorageInstance(context: Context, name: String): io.timelimit.android.data.Database {
|
||||
return Room.databaseBuilder(
|
||||
context,
|
||||
io.timelimit.android.data.RoomDatabase::class.java,
|
||||
name
|
||||
)
|
||||
.setJournalMode(JournalMode.TRUNCATE)
|
||||
.fallbackToDestructiveMigration()
|
||||
.addMigrations(
|
||||
DatabaseMigrations.MIGRATE_TO_V2,
|
||||
DatabaseMigrations.MIGRATE_TO_V3,
|
||||
DatabaseMigrations.MIGRATE_TO_V4,
|
||||
DatabaseMigrations.MIGRATE_TO_V5,
|
||||
DatabaseMigrations.MIGRATE_TO_V6,
|
||||
DatabaseMigrations.MIGRATE_TO_V7,
|
||||
DatabaseMigrations.MIGRATE_TO_V8,
|
||||
DatabaseMigrations.MIGRATE_TO_V9,
|
||||
DatabaseMigrations.MIGRATE_TO_V10,
|
||||
DatabaseMigrations.MIGRATE_TO_V11
|
||||
)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
// the room compiler needs this
|
||||
override fun setTransactionSuccessful() {
|
||||
super.setTransactionSuccessful()
|
||||
}
|
||||
|
||||
override fun beginTransaction() {
|
||||
super.beginTransaction()
|
||||
}
|
||||
|
||||
override fun endTransaction() {
|
||||
super.endTransaction()
|
||||
}
|
||||
|
||||
override fun deleteAllData() {
|
||||
clearAllTables()
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
super.close()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.data.backup
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.core.util.AtomicFile
|
||||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.coroutines.executeAndWait
|
||||
import io.timelimit.android.data.RoomDatabase
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import okio.Okio
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
class DatabaseBackup(private val context: Context) {
|
||||
companion object {
|
||||
private const val CONFIG_FILE = "config.json"
|
||||
private const val LOG_TAG = "DatabaseBackup"
|
||||
|
||||
private var instance: DatabaseBackup? = null
|
||||
private val lock = Object()
|
||||
|
||||
fun with(context: Context): DatabaseBackup {
|
||||
if (instance == null) {
|
||||
synchronized(lock) {
|
||||
if (instance == null) {
|
||||
instance = DatabaseBackup(context.applicationContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return instance!!
|
||||
}
|
||||
}
|
||||
|
||||
private val executor = Executors.newSingleThreadExecutor()
|
||||
private val jsonFile = AtomicFile(context.getDatabasePath(CONFIG_FILE))
|
||||
private val databaseFile = context.getDatabasePath(RoomDatabase.DEFAULT_DB_NAME)
|
||||
private val databaseBackupFile = context.getDatabasePath(RoomDatabase.BACKUP_DB_NAME)
|
||||
private val lock = Mutex()
|
||||
|
||||
suspend fun tryRestoreDatabaseBackupAsyncAndWait() {
|
||||
executor.executeAndWait { tryRestoreDatabaseBackupSync() }
|
||||
}
|
||||
|
||||
private fun tryRestoreDatabaseBackupSync() {
|
||||
runBlocking {
|
||||
lock.withLock {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "try restoring backup")
|
||||
}
|
||||
|
||||
val database = RoomDatabase.with(context)
|
||||
|
||||
if (database.config().getOwnDeviceIdSync().orEmpty().isNotEmpty()) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "database is not empty -> don't restore backup")
|
||||
}
|
||||
|
||||
return@runBlocking
|
||||
}
|
||||
|
||||
try {
|
||||
jsonFile.openRead().use { inputStream ->
|
||||
|
||||
DatabaseBackupLowlevel.restoreFromBackupJson(database, inputStream)
|
||||
}
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "database was restored from backup")
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.w(LOG_TAG, "error during restoring database backup", ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun tryCreateDatabaseBackupAsync() {
|
||||
executor.submit { tryCreateDatabaseBackupSync() }
|
||||
}
|
||||
|
||||
private fun tryCreateDatabaseBackupSync() {
|
||||
runBlocking {
|
||||
lock.withLock {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "create backup")
|
||||
}
|
||||
|
||||
try {
|
||||
// create a temp copy of the database
|
||||
databaseBackupFile.delete()
|
||||
Okio.buffer(Okio.source(databaseFile)).readAll(Okio.sink(databaseBackupFile))
|
||||
|
||||
// open the temp copy
|
||||
val database = RoomDatabase.createOrOpenLocalStorageInstance(context, RoomDatabase.BACKUP_DB_NAME)
|
||||
|
||||
try {
|
||||
// open the output file
|
||||
val output = jsonFile.startWrite()
|
||||
|
||||
try {
|
||||
DatabaseBackupLowlevel.outputAsBackupJson(database, output)
|
||||
|
||||
jsonFile.finishWrite(output)
|
||||
} catch (ex: Exception) {
|
||||
jsonFile.failWrite(output)
|
||||
|
||||
throw ex
|
||||
}
|
||||
|
||||
null
|
||||
} finally {
|
||||
database.close()
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.w(LOG_TAG, "failed to create backup", ex)
|
||||
}
|
||||
|
||||
null
|
||||
} finally {
|
||||
// delete the temp copy
|
||||
databaseBackupFile.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,190 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.data.backup
|
||||
|
||||
import android.util.JsonReader
|
||||
import android.util.JsonWriter
|
||||
import io.timelimit.android.data.Database
|
||||
import io.timelimit.android.data.JsonSerializable
|
||||
import io.timelimit.android.data.model.*
|
||||
import io.timelimit.android.data.transaction
|
||||
import java.io.InputStream
|
||||
import java.io.InputStreamReader
|
||||
import java.io.OutputStream
|
||||
import java.io.OutputStreamWriter
|
||||
|
||||
object DatabaseBackupLowlevel {
|
||||
private const val PAGE_SIZE = 50
|
||||
|
||||
private const val APP = "app"
|
||||
private const val CATEGORY = "category"
|
||||
private const val CATEGORY_APP = "categoryApp"
|
||||
private const val CONFIG = "config"
|
||||
private const val DEVICE = "device"
|
||||
private const val PENDING_SYNC_ACTION = "pendingSyncAction"
|
||||
private const val TIME_LIMIT_RULE = "timelimitRule"
|
||||
private const val USED_TIME_ITEM = "usedTime"
|
||||
private const val USER = "user"
|
||||
|
||||
fun outputAsBackupJson(database: Database, outputStream: OutputStream) {
|
||||
val writer = JsonWriter(OutputStreamWriter(outputStream, Charsets.UTF_8))
|
||||
|
||||
writer.beginObject()
|
||||
|
||||
fun <T: JsonSerializable> handleCollection(
|
||||
name: String,
|
||||
readPage: (offset: Int, pageSize: Int) -> List<T>
|
||||
) {
|
||||
writer.name(name).beginArray()
|
||||
|
||||
var offset = 0
|
||||
|
||||
while (true) {
|
||||
val page = readPage(offset, PAGE_SIZE)
|
||||
offset += page.size
|
||||
|
||||
if (page.isEmpty()) {
|
||||
break
|
||||
}
|
||||
|
||||
page.forEach { it.serialize(writer) }
|
||||
}
|
||||
|
||||
writer.endArray()
|
||||
}
|
||||
|
||||
handleCollection(APP) {offset, pageSize -> database.app().getAppPageSync(offset, pageSize) }
|
||||
handleCollection(CATEGORY) {offset: Int, pageSize: Int -> database.category().getCategoryPageSync(offset, pageSize) }
|
||||
handleCollection(CATEGORY_APP) { offset, pageSize -> database.categoryApp().getCategoryAppPageSync(offset, pageSize) }
|
||||
|
||||
writer.name(CONFIG).beginArray()
|
||||
database.config().getConfigItemsSync().forEach { it.serialize(writer) }
|
||||
writer.endArray()
|
||||
|
||||
handleCollection(DEVICE) { offset, pageSize -> database.device().getDevicePageSync(offset, pageSize) }
|
||||
handleCollection(PENDING_SYNC_ACTION) { offset, pageSize -> database.pendingSyncAction().getPendingSyncActionPageSync(offset, pageSize) }
|
||||
handleCollection(TIME_LIMIT_RULE) { offset, pageSize -> database.timeLimitRules().getRulePageSync(offset, pageSize) }
|
||||
handleCollection(USED_TIME_ITEM) { offset, pageSize -> database.usedTimes().getUsedTimePageSync(offset, pageSize) }
|
||||
handleCollection(USER) { offset, pageSize -> database.user().getUserPageSync(offset, pageSize) }
|
||||
|
||||
writer.endObject().flush()
|
||||
}
|
||||
|
||||
fun restoreFromBackupJson(database: Database, inputStream: InputStream) {
|
||||
val reader = JsonReader(InputStreamReader(inputStream, Charsets.UTF_8))
|
||||
|
||||
database.transaction().use {
|
||||
transaction ->
|
||||
|
||||
database.deleteAllData()
|
||||
|
||||
reader.beginObject()
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
APP -> {
|
||||
reader.beginArray()
|
||||
|
||||
while (reader.hasNext()) {
|
||||
database.app().addAppSync(App.parse(reader))
|
||||
}
|
||||
|
||||
reader.endArray()
|
||||
}
|
||||
CATEGORY -> {
|
||||
reader.beginArray()
|
||||
|
||||
while (reader.hasNext()) {
|
||||
database.category().addCategory(Category.parse(reader))
|
||||
}
|
||||
|
||||
reader.endArray()
|
||||
}
|
||||
CATEGORY_APP -> {
|
||||
reader.beginArray()
|
||||
|
||||
while (reader.hasNext()) {
|
||||
database.categoryApp().addCategoryAppSync(CategoryApp.parse(reader))
|
||||
}
|
||||
|
||||
reader.endArray()
|
||||
}
|
||||
CONFIG -> {
|
||||
reader.beginArray()
|
||||
|
||||
while (reader.hasNext()) {
|
||||
val item = ConfigurationItem.parse(reader)
|
||||
|
||||
if (item != null) {
|
||||
database.config().updateValueOfKeySync(item)
|
||||
}
|
||||
}
|
||||
|
||||
reader.endArray()
|
||||
}
|
||||
DEVICE -> {
|
||||
reader.beginArray()
|
||||
|
||||
while (reader.hasNext()) {
|
||||
database.device().addDeviceSync(Device.parse(reader))
|
||||
}
|
||||
|
||||
reader.endArray()
|
||||
}
|
||||
PENDING_SYNC_ACTION -> {
|
||||
reader.beginArray()
|
||||
|
||||
while (reader.hasNext()) {
|
||||
database.pendingSyncAction().addSyncActionSync(PendingSyncAction.parse(reader))
|
||||
}
|
||||
|
||||
reader.endArray()
|
||||
}
|
||||
TIME_LIMIT_RULE -> {
|
||||
reader.beginArray()
|
||||
|
||||
while (reader.hasNext()) {
|
||||
database.timeLimitRules().addTimeLimitRule(TimeLimitRule.parse(reader))
|
||||
}
|
||||
|
||||
reader.endArray()
|
||||
}
|
||||
USED_TIME_ITEM -> {
|
||||
reader.beginArray()
|
||||
|
||||
while (reader.hasNext()) {
|
||||
database.usedTimes().insertUsedTime(UsedTimeItem.parse(reader))
|
||||
}
|
||||
|
||||
reader.endArray()
|
||||
}
|
||||
USER -> {
|
||||
reader.beginArray()
|
||||
|
||||
while (reader.hasNext()) {
|
||||
database.user().addUserSync(User.parse(reader))
|
||||
}
|
||||
|
||||
reader.endArray()
|
||||
}
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
reader.endObject()
|
||||
|
||||
transaction.setSuccess()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.data.customtypes
|
||||
|
||||
import android.os.Parcelable
|
||||
import android.text.TextUtils
|
||||
import androidx.room.TypeConverter
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
sealed class Bitmask(private val data: BitSet) {
|
||||
fun read(index: Int): Boolean {
|
||||
return data[index]
|
||||
}
|
||||
}
|
||||
|
||||
data class MutableBitmask (val data: BitSet): Bitmask(data) {
|
||||
fun write(index: Int, value: Boolean) {
|
||||
data[index] = value
|
||||
}
|
||||
|
||||
fun toImmutable(): ImmutableBitmask {
|
||||
return ImmutableBitmask(data.clone() as BitSet)
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class ImmutableBitmask(val dataNotToModify: BitSet): Bitmask(dataNotToModify), Parcelable {
|
||||
fun toMutable(): MutableBitmask {
|
||||
return MutableBitmask(dataNotToModify)
|
||||
}
|
||||
}
|
||||
|
||||
// format: index of start of set bits, index of stop of set bits, ...
|
||||
class ImmutableBitmaskAdapter {
|
||||
@TypeConverter
|
||||
fun toString(mask: ImmutableBitmask) = ImmutableBitmaskJson.serialize(mask)
|
||||
|
||||
@TypeConverter
|
||||
fun toImmutableBitmask(data: String) = ImmutableBitmaskJson.parse(data, null)
|
||||
}
|
||||
|
||||
object ImmutableBitmaskJson {
|
||||
fun serialize(mask: ImmutableBitmask): String {
|
||||
val output = ArrayList<Int>()
|
||||
|
||||
if (mask.read(0)) {
|
||||
// if first bit is set
|
||||
|
||||
output.add(0)
|
||||
} else {
|
||||
// if first bit is not set
|
||||
|
||||
val start = mask.dataNotToModify.nextSetBit(0)
|
||||
|
||||
if (start == -1) {
|
||||
// nothing is set
|
||||
return ""
|
||||
}
|
||||
|
||||
output.add(start)
|
||||
}
|
||||
|
||||
do {
|
||||
val startIndex = output.last()
|
||||
val stopIndex = mask.dataNotToModify.nextClearBit(startIndex)
|
||||
|
||||
output.add(stopIndex)
|
||||
|
||||
val nextStartIndex = mask.dataNotToModify.nextSetBit(stopIndex)
|
||||
if (nextStartIndex == -1) {
|
||||
break
|
||||
} else {
|
||||
output.add(nextStartIndex)
|
||||
}
|
||||
} while (true)
|
||||
|
||||
return TextUtils.join(",", output.map { it.toString() })
|
||||
}
|
||||
|
||||
fun parse(data: String, maxSize: Int?): ImmutableBitmask {
|
||||
val indexes = data.split(",").filter{ it.isNotBlank() }.map { it.toInt() }
|
||||
val iterator = indexes.iterator()
|
||||
val output = BitSet()
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
val start = iterator.next()
|
||||
val end = iterator.next()
|
||||
|
||||
if (maxSize != null) {
|
||||
if (start > maxSize || end > maxSize) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
|
||||
output.set(start, end)
|
||||
}
|
||||
|
||||
return ImmutableBitmask(output)
|
||||
}
|
||||
}
|
58
app/src/main/java/io/timelimit/android/data/dao/AppDao.kt
Normal file
58
app/src/main/java/io/timelimit/android/data/dao/AppDao.kt
Normal file
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.data.dao
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.*
|
||||
import io.timelimit.android.data.model.App
|
||||
import io.timelimit.android.data.model.AppRecommendation
|
||||
import io.timelimit.android.data.model.AppRecommendationConverter
|
||||
|
||||
@Dao
|
||||
@TypeConverters(
|
||||
AppRecommendationConverter::class
|
||||
)
|
||||
interface AppDao {
|
||||
@Query("DELETE FROM app WHERE device_id = :deviceId")
|
||||
fun deleteAllAppsByDeviceId(deviceId: String)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun addAppsSync(apps: Collection<App>)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun addAppSync(app: App)
|
||||
|
||||
@Query("DELETE FROM app WHERE device_id = :deviceId AND package_name IN (:packageNames)")
|
||||
fun removeAppsByDeviceIdAndPackageNamesSync(deviceId: String, packageNames: List<String>)
|
||||
|
||||
@Query("SELECT * FROM app WHERE device_id IN (:deviceIds)")
|
||||
fun getAppsByDeviceIds(deviceIds: List<String>): LiveData<List<App>>
|
||||
|
||||
@Query("SELECT * FROM app")
|
||||
fun getAllApps(): LiveData<List<App>>
|
||||
|
||||
@Query("SELECT * FROM app WHERE device_id IN (:deviceIds) AND package_name = :packageName")
|
||||
fun getAppsByDeviceIdsAndPackageName(deviceIds: List<String>, packageName: String): LiveData<List<App>>
|
||||
|
||||
@Query("SELECT * FROM app WHERE device_id = :deviceId")
|
||||
fun getAppsByDeviceIdAsync(deviceId: String): LiveData<List<App>>
|
||||
|
||||
@Query("SELECT * FROM app LIMIT :pageSize OFFSET :offset")
|
||||
fun getAppPageSync(offset: Int, pageSize: Int): List<App>
|
||||
|
||||
@Query("SELECT * FROM app WHERE recommendation = :recommendation")
|
||||
fun getAppsByRecommendationLive(recommendation: AppRecommendation): LiveData<List<App>>
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.data.dao
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import io.timelimit.android.data.model.CategoryApp
|
||||
|
||||
@Dao
|
||||
abstract class CategoryAppDao {
|
||||
@Query("SELECT * FROM category_app WHERE category_id IN (:categoryIds) AND package_name = :packageName")
|
||||
abstract fun getCategoryApp(categoryIds: List<String>, packageName: String): LiveData<CategoryApp?>
|
||||
|
||||
@Query("SELECT * FROM category_app WHERE category_id = :categoryId")
|
||||
abstract fun getCategoryApps(categoryId: String): LiveData<List<CategoryApp>>
|
||||
|
||||
@Query("SELECT * FROM category_app WHERE category_id IN (:categoryIds)")
|
||||
abstract fun getCategoryApps(categoryIds: List<String>): LiveData<List<CategoryApp>>
|
||||
|
||||
@Insert
|
||||
abstract fun addCategoryAppsSync(items: Collection<CategoryApp>)
|
||||
|
||||
@Insert
|
||||
abstract fun addCategoryAppSync(item: CategoryApp)
|
||||
|
||||
@Query("DELETE FROM category_app WHERE category_id IN (:categoryIds) AND package_name IN (:packageNames)")
|
||||
abstract fun removeCategoryAppsSyncByCategoryIds(packageNames: List<String>, categoryIds: List<String>)
|
||||
|
||||
@Query("DELETE FROM category_app WHERE category_id = :categoryId")
|
||||
abstract fun deleteCategoryAppsByCategoryId(categoryId: String)
|
||||
|
||||
@Query("SELECT * FROM category_app LIMIT :pageSize OFFSET :offset")
|
||||
abstract fun getCategoryAppPageSync(offset: Int, pageSize: Int): List<CategoryApp>
|
||||
}
|
119
app/src/main/java/io/timelimit/android/data/dao/CategoryDao.kt
Normal file
119
app/src/main/java/io/timelimit/android/data/dao/CategoryDao.kt
Normal file
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.data.dao
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.*
|
||||
import io.timelimit.android.data.customtypes.ImmutableBitmask
|
||||
import io.timelimit.android.data.customtypes.ImmutableBitmaskAdapter
|
||||
import io.timelimit.android.data.model.Category
|
||||
import io.timelimit.android.livedata.map
|
||||
import java.util.*
|
||||
|
||||
@Dao
|
||||
abstract class CategoryDao {
|
||||
@Query("SELECT * FROM category WHERE child_id = :childId")
|
||||
abstract fun getCategoriesByChildId(childId: String): LiveData<List<Category>>
|
||||
|
||||
fun getCategoriesByChildIdMappedByCategoryId(childId: String): LiveData<Map<String, Category>> = getCategoriesByChildId(childId).map {
|
||||
val result = HashMap<String, Category>()
|
||||
it.forEach { result[it.id] = it }
|
||||
Collections.unmodifiableMap(result)
|
||||
}
|
||||
|
||||
@Query("SELECT * FROM category WHERE child_id = :childId AND id = :categoryId")
|
||||
abstract fun getCategoryByChildIdAndId(childId: String, categoryId: String): LiveData<Category?>
|
||||
|
||||
@Query("SELECT * FROM category WHERE id = :categoryId")
|
||||
abstract fun getCategoryByIdSync(categoryId: String): Category?
|
||||
|
||||
@Query("SELECT * FROM category WHERE child_id = :childId")
|
||||
abstract fun getCategoriesByChildIdSync(childId: String): List<Category>
|
||||
|
||||
@Query("DELETE FROM category WHERE id = :categoryId")
|
||||
abstract fun deleteCategory(categoryId: String)
|
||||
|
||||
@Insert
|
||||
abstract fun addCategory(category: Category)
|
||||
|
||||
@Query("UPDATE category SET title = :newTitle WHERE id = :categoryId")
|
||||
abstract fun updateCategoryTitle(categoryId: String, newTitle: String)
|
||||
|
||||
@Query("UPDATE category SET extra_time = :newExtraTime WHERE id = :categoryId")
|
||||
abstract fun updateCategoryExtraTime(categoryId: String, newExtraTime: Long)
|
||||
|
||||
@Query("UPDATE category SET extra_time = extra_time + :addedExtraTime WHERE id = :categoryId")
|
||||
abstract fun incrementCategoryExtraTime(categoryId: String, addedExtraTime: Long)
|
||||
|
||||
@Query("UPDATE category SET extra_time = MAX(0, extra_time - :removedExtraTime) WHERE id = :categoryId")
|
||||
abstract fun subtractCategoryExtraTime(categoryId: String, removedExtraTime: Int)
|
||||
|
||||
@TypeConverters(ImmutableBitmaskAdapter::class)
|
||||
@Query("UPDATE category SET blocked_times = :blockedMinutesInWeek WHERE id = :categoryId")
|
||||
abstract fun updateCategoryBlockedTimes(categoryId: String, blockedMinutesInWeek: ImmutableBitmask)
|
||||
|
||||
@Query("UPDATE category SET temporarily_blocked = :blocked WHERE id = :categoryId")
|
||||
abstract fun updateCategoryTemporarilyBlocked(categoryId: String, blocked: Boolean)
|
||||
|
||||
@Query("SELECT id, base_version, apps_version, rules_version, usedtimes_version FROM category")
|
||||
abstract fun getCategoriesWithVersionNumbers(): LiveData<List<CategoryWithVersionNumbers>>
|
||||
|
||||
@Query("UPDATE category SET apps_version = :assignedAppsVersion WHERE id = :categoryId")
|
||||
abstract fun updateCategoryAssignedAppsVersion(categoryId: String, assignedAppsVersion: String)
|
||||
|
||||
@Query("UPDATE category SET rules_version = :rulesVersion WHERE id = :categoryId")
|
||||
abstract fun updateCategoryRulesVersion(categoryId: String, rulesVersion: String)
|
||||
|
||||
@Query("UPDATE category SET usedtimes_version = :usedTimesVersion WHERE id = :categoryId")
|
||||
abstract fun updateCategoryUsedTimesVersion(categoryId: String, usedTimesVersion: String)
|
||||
|
||||
@Update
|
||||
abstract fun updateCategorySync(category: Category)
|
||||
|
||||
@Query("UPDATE category SET apps_version = \"\", rules_version = \"\", usedtimes_version = \"\", base_version = \"\"")
|
||||
abstract fun deleteAllCategoriesVersionNumbers()
|
||||
|
||||
@Query("SELECT * FROM category LIMIT :pageSize OFFSET :offset")
|
||||
abstract fun getCategoryPageSync(offset: Int, pageSize: Int): List<Category>
|
||||
|
||||
@Query("SELECT id, child_id, temporarily_blocked FROM category")
|
||||
abstract fun getAllCategoriesShortInfo(): LiveData<List<CategoryShortInfo>>
|
||||
|
||||
@Query("UPDATE category SET parent_category_id = :parentCategoryId WHERE id = :categoryId")
|
||||
abstract fun updateParentCategory(categoryId: String, parentCategoryId: String)
|
||||
}
|
||||
|
||||
data class CategoryWithVersionNumbers(
|
||||
@ColumnInfo(name = "id")
|
||||
val categoryId: String,
|
||||
@ColumnInfo(name = "base_version")
|
||||
val baseVersion: String,
|
||||
@ColumnInfo(name = "apps_version")
|
||||
val assignedAppsVersion: String,
|
||||
@ColumnInfo(name = "rules_version")
|
||||
val timeLimitRulesVersion: String,
|
||||
@ColumnInfo(name = "usedtimes_version")
|
||||
val usedTimeItemsVersion: String
|
||||
)
|
||||
|
||||
data class CategoryShortInfo(
|
||||
@ColumnInfo(name = "child_id")
|
||||
val childId: String,
|
||||
@ColumnInfo(name = "id")
|
||||
val categoryId: String,
|
||||
@ColumnInfo(name = "temporarily_blocked")
|
||||
val temporarilyBlocked: Boolean
|
||||
)
|
211
app/src/main/java/io/timelimit/android/data/dao/ConfigDao.kt
Normal file
211
app/src/main/java/io/timelimit/android/data/dao/ConfigDao.kt
Normal file
|
@ -0,0 +1,211 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.data.dao
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.room.*
|
||||
import io.timelimit.android.data.model.ConfigurationItem
|
||||
import io.timelimit.android.data.model.ConfigurationItemType
|
||||
import io.timelimit.android.data.model.ConfigurationItemTypeConverter
|
||||
import io.timelimit.android.data.model.ConfigurationItemTypeUtil
|
||||
import io.timelimit.android.livedata.ignoreUnchanged
|
||||
import io.timelimit.android.livedata.map
|
||||
|
||||
@Dao
|
||||
@TypeConverters(ConfigurationItemTypeConverter::class)
|
||||
abstract class ConfigDao {
|
||||
@Query("SELECT * FROM config WHERE id IN (:keys)")
|
||||
protected abstract fun getConfigItemsSync(keys: List<Int>): List<ConfigurationItem>
|
||||
|
||||
fun getConfigItemsSync() = getConfigItemsSync(ConfigurationItemTypeUtil.TYPES.map { ConfigurationItemTypeUtil.serialize(it) })
|
||||
|
||||
@Query("SELECT * FROM config WHERE id = :key")
|
||||
protected abstract fun getRowByKeyAsync(key: ConfigurationItemType): LiveData<ConfigurationItem?>
|
||||
|
||||
private fun getValueOfKeyAsync(key: ConfigurationItemType): LiveData<String?> {
|
||||
return Transformations.map(getRowByKeyAsync(key)) { it?.value }.ignoreUnchanged()
|
||||
}
|
||||
|
||||
@Query("SELECT * FROM config WHERE id = :key")
|
||||
protected abstract fun getRowByKeySync(key: ConfigurationItemType): ConfigurationItem?
|
||||
|
||||
private fun getValueOfKeySync(key: ConfigurationItemType): String? {
|
||||
return getRowByKeySync(key)?.value
|
||||
}
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
abstract fun updateValueOfKeySync(item: ConfigurationItem)
|
||||
|
||||
@Query("DELETE FROM config WHERE id = :key")
|
||||
protected abstract fun removeConfigItemSync(key: ConfigurationItemType)
|
||||
|
||||
private fun updateValueSync(key: ConfigurationItemType, value: String?) {
|
||||
if (value != null) {
|
||||
updateValueOfKeySync(ConfigurationItem(key, value))
|
||||
} else {
|
||||
removeConfigItemSync(key)
|
||||
}
|
||||
}
|
||||
|
||||
fun getOwnDeviceId(): LiveData<String?> {
|
||||
return getValueOfKeyAsync(ConfigurationItemType.OwnDeviceId)
|
||||
}
|
||||
|
||||
fun getOwnDeviceIdSync(): String? {
|
||||
return getValueOfKeySync(ConfigurationItemType.OwnDeviceId)
|
||||
}
|
||||
|
||||
fun setOwnDeviceIdSync(deviceId: String) {
|
||||
updateValueSync(ConfigurationItemType.OwnDeviceId, deviceId)
|
||||
}
|
||||
|
||||
fun getDeviceListVersion(): LiveData<String> {
|
||||
return getValueOfKeyAsync(ConfigurationItemType.DeviceListVersion).map {
|
||||
if (it == null) {
|
||||
""
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setDeviceListVersionSync(deviceListVersion: String) {
|
||||
updateValueSync(ConfigurationItemType.DeviceListVersion, deviceListVersion)
|
||||
}
|
||||
|
||||
fun getUserListVersion(): LiveData<String> {
|
||||
return getValueOfKeyAsync(ConfigurationItemType.UserListVersion).map {
|
||||
if (it == null) {
|
||||
""
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setUserListVersionSync(userListVersion: String) {
|
||||
updateValueSync(ConfigurationItemType.UserListVersion, userListVersion)
|
||||
}
|
||||
|
||||
private fun getNextSyncActionSequenceNumberSync(): Long {
|
||||
val row = getValueOfKeySync(ConfigurationItemType.NextSyncSequenceNumber)
|
||||
|
||||
if (row == null) {
|
||||
return 0
|
||||
} else {
|
||||
return row.toLong()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setNextSyncActionSequenceNumberSync(nextSyncSequenceNumber: Long) {
|
||||
updateValueSync(ConfigurationItemType.NextSyncSequenceNumber, nextSyncSequenceNumber.toString())
|
||||
}
|
||||
|
||||
fun getNextSyncActionSequenceActionAndIncrementIt(): Long {
|
||||
val current = getNextSyncActionSequenceNumberSync()
|
||||
setNextSyncActionSequenceNumberSync(current + 1)
|
||||
|
||||
return current
|
||||
}
|
||||
|
||||
fun getDeviceAuthTokenSync(): String {
|
||||
val value = getValueOfKeySync(ConfigurationItemType.DeviceAuthToken)
|
||||
|
||||
if (value == null) {
|
||||
return ""
|
||||
} else {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
fun getDeviceAuthTokenAsync(): LiveData<String> = getValueOfKeyAsync(ConfigurationItemType.DeviceAuthToken).map {
|
||||
if (it == null) {
|
||||
""
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
fun setDeviceAuthTokenSync(token: String) {
|
||||
updateValueSync(ConfigurationItemType.DeviceAuthToken, token)
|
||||
}
|
||||
|
||||
fun getFullVersionUntilAsync() = getValueOfKeyAsync(ConfigurationItemType.FullVersionUntil).map {
|
||||
if (it == null || it.isEmpty()) {
|
||||
0L
|
||||
} else {
|
||||
it.toLong()
|
||||
}
|
||||
}
|
||||
|
||||
fun setFullVersionUntilSync(fullVersionUntil: Long) {
|
||||
updateValueSync(ConfigurationItemType.FullVersionUntil, fullVersionUntil.toString())
|
||||
}
|
||||
|
||||
private fun getShownHintsLive(): LiveData<Long> {
|
||||
return getValueOfKeyAsync(ConfigurationItemType.ShownHints).map {
|
||||
if (it == null) {
|
||||
0
|
||||
} else {
|
||||
it.toLong(16)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getShownHintsSync(): Long {
|
||||
val v = getValueOfKeySync(ConfigurationItemType.ShownHints)
|
||||
|
||||
if (v == null) {
|
||||
return 0
|
||||
} else {
|
||||
return v.toLong(16)
|
||||
}
|
||||
}
|
||||
|
||||
fun wereHintsShown(flags: Long) = getShownHintsLive().map {
|
||||
(it and flags) == flags
|
||||
}.ignoreUnchanged()
|
||||
|
||||
fun wereAnyHintsShown() = getShownHintsLive().map { it != 0L }.ignoreUnchanged()
|
||||
|
||||
fun setHintsShownSync(flags: Long) {
|
||||
updateValueSync(
|
||||
ConfigurationItemType.ShownHints,
|
||||
(getShownHintsSync() or flags).toString(16)
|
||||
)
|
||||
}
|
||||
|
||||
fun resetShownHintsSync() {
|
||||
updateValueSync(ConfigurationItemType.ShownHints, null)
|
||||
}
|
||||
|
||||
fun wasDeviceLockedSync() = getValueOfKeySync(ConfigurationItemType.WasDeviceLocked) == "true"
|
||||
fun setWasDeviceLockedSync(value: Boolean) = updateValueSync(ConfigurationItemType.WasDeviceLocked, if (value) "true" else "false")
|
||||
|
||||
fun getLastAppVersionWhichSynced() = getValueOfKeySync(ConfigurationItemType.LastAppVersionWhichSynced)
|
||||
fun setLastAppVersionWhichSynced(version: String) = updateValueSync(ConfigurationItemType.LastAppVersionWhichSynced, version)
|
||||
|
||||
fun setLastScreenOnTime(time: Long) = updateValueSync(ConfigurationItemType.LastScreenOnTime, time.toString())
|
||||
fun getLastScreenOnTime() = getValueOfKeySync(ConfigurationItemType.LastScreenOnTime)?.toLong() ?: 0L
|
||||
|
||||
fun setServerMessage(message: String?) = updateValueSync(ConfigurationItemType.ServerMessage, message ?: "")
|
||||
fun getServerMessage() = getValueOfKeyAsync(ConfigurationItemType.ServerMessage).map { if (it.isNullOrBlank()) null else it }
|
||||
|
||||
fun getCustomServerUrlSync() = getValueOfKeySync(ConfigurationItemType.CustomServerUrl) ?: ""
|
||||
fun getCustomServerUrlAsync() = getValueOfKeyAsync(ConfigurationItemType.CustomServerUrl).map { it ?: "" }
|
||||
fun setCustomServerUrlSync(url: String) = updateValueSync(ConfigurationItemType.CustomServerUrl, url)
|
||||
}
|
95
app/src/main/java/io/timelimit/android/data/dao/DeviceDao.kt
Normal file
95
app/src/main/java/io/timelimit/android/data/dao/DeviceDao.kt
Normal file
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.data.dao
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.*
|
||||
import io.timelimit.android.data.model.Device
|
||||
import io.timelimit.android.data.model.NetworkTime
|
||||
import io.timelimit.android.data.model.NetworkTimeAdapter
|
||||
import io.timelimit.android.integration.platform.NewPermissionStatusConverter
|
||||
import io.timelimit.android.integration.platform.ProtectionLevelConverter
|
||||
import io.timelimit.android.integration.platform.RuntimePermissionStatusConverter
|
||||
|
||||
@Dao
|
||||
@TypeConverters(
|
||||
NetworkTimeAdapter::class,
|
||||
ProtectionLevelConverter::class,
|
||||
RuntimePermissionStatusConverter::class,
|
||||
NewPermissionStatusConverter::class
|
||||
)
|
||||
abstract class DeviceDao {
|
||||
@Query("SELECT * FROM device WHERE id = :deviceId")
|
||||
abstract fun getDeviceById(deviceId: String): LiveData<Device?>
|
||||
|
||||
@Query("SELECT * FROM device WHERE id = :deviceId")
|
||||
abstract fun getDeviceByIdSync(deviceId: String): Device?
|
||||
|
||||
@Query("SELECT * FROM device")
|
||||
abstract fun getAllDevicesLive(): LiveData<List<Device>>
|
||||
|
||||
@Query("SELECT * FROM device")
|
||||
abstract fun getAllDevicesSync(): List<Device>
|
||||
|
||||
@Insert
|
||||
abstract fun addDeviceSync(device: Device)
|
||||
|
||||
@Query("UPDATE device SET current_user_id = :userId, is_user_kept_signed_in = 0 WHERE id = :deviceId")
|
||||
abstract fun updateDeviceUser(deviceId: String, userId: String)
|
||||
|
||||
@Query("UPDATE device SET default_user = :defaultUserId, is_user_kept_signed_in = 0 WHERE id = :deviceId")
|
||||
abstract fun updateDeviceDefaultUser(deviceId: String, defaultUserId: String)
|
||||
|
||||
@Query("SELECT id, apps_version FROM device")
|
||||
abstract fun getInstalledAppsVersions(): LiveData<List<DeviceWithAppVersion>>
|
||||
|
||||
@Query("DELETE FROM device WHERE id IN (:deviceIds)")
|
||||
abstract fun removeDevicesById(deviceIds: List<String>)
|
||||
|
||||
@Update
|
||||
abstract fun updateDeviceEntry(device: Device)
|
||||
|
||||
@Query("UPDATE device SET apps_version = :appsVersion WHERE id = :deviceId")
|
||||
abstract fun updateAppsVersion(deviceId: String, appsVersion: String)
|
||||
|
||||
@Query("SELECT * FROM device WHERE current_user_id = :userId")
|
||||
abstract fun getDevicesByUserId(userId: String): LiveData<List<Device>>
|
||||
|
||||
@Query("UPDATE device SET apps_version = \"\"")
|
||||
abstract fun deleteAllInstalledAppsVersions()
|
||||
|
||||
@Query("UPDATE device SET network_time = :mode WHERE id = :deviceId")
|
||||
abstract fun updateNetworkTimeVerification(deviceId: String, mode: NetworkTime)
|
||||
|
||||
@Query("UPDATE device SET name = :name WHERE id = :deviceId")
|
||||
abstract fun updateDeviceName(deviceId: String, name: String): Int
|
||||
|
||||
@Query("SELECT * FROM device LIMIT :pageSize OFFSET :offset")
|
||||
abstract fun getDevicePageSync(offset: Int, pageSize: Int): List<Device>
|
||||
|
||||
@Query("UPDATE device SET current_user_id = \"\", is_user_kept_signed_in = 0 WHERE current_user_id = :userId")
|
||||
abstract fun unassignCurrentUserFromAllDevices(userId: String)
|
||||
|
||||
@Query("UPDATE device SET is_user_kept_signed_in = :keepSignedIn WHERE id = :deviceId")
|
||||
abstract fun updateKeepSignedIn(deviceId: String, keepSignedIn: Boolean)
|
||||
}
|
||||
|
||||
data class DeviceWithAppVersion(
|
||||
@ColumnInfo(name = "id")
|
||||
val deviceId: String,
|
||||
@ColumnInfo(name = "apps_version")
|
||||
val installedAppsVersions: String
|
||||
)
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.data.dao
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.paging.DataSource
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import io.timelimit.android.data.model.PendingSyncAction
|
||||
|
||||
@Dao
|
||||
interface PendingSyncActionDao {
|
||||
@Insert
|
||||
fun addSyncActionSync(action: PendingSyncAction)
|
||||
|
||||
@Query("DELETE FROM pending_sync_action WHERE sequence_number IN (:sequenceNumbers)")
|
||||
fun removeSyncActionsBySequenceNumbersSync(sequenceNumbers: List<Long>)
|
||||
|
||||
@Query("UPDATE pending_sync_action SET `action` = :action WHERE sequence_number = :sequenceNumber")
|
||||
fun updateEncodedActionSync(sequenceNumber: Long, action: String)
|
||||
|
||||
@Query("UPDATE pending_sync_action SET scheduled_for_upload = 1 WHERE sequence_number <= :highestSequenceNumberToMark")
|
||||
fun markSyncActionsAsScheduledForUpload(highestSequenceNumberToMark: Long)
|
||||
|
||||
@Query("SELECT * FROM pending_sync_action WHERE scheduled_for_upload = 0 ORDER BY sequence_number ASC LIMIT :limit")
|
||||
fun getNextUnscheduledActionsSync(limit: Int): List<PendingSyncAction>
|
||||
|
||||
@Query("SELECT * FROM pending_sync_action WHERE scheduled_for_upload = 1 ORDER BY sequence_number ASC LIMIT :limit")
|
||||
fun getScheduledActionsSync(limit: Int): List<PendingSyncAction>
|
||||
|
||||
@Query("SELECT COUNT(*) FROM pending_sync_action WHERE scheduled_for_upload = 1")
|
||||
fun countScheduledActionsSync(): Long
|
||||
|
||||
@Query("SELECT COUNT(*) FROM pending_sync_action WHERE scheduled_for_upload = 0")
|
||||
fun countUnscheduledActionsSync(): Long
|
||||
|
||||
@Query("SELECT COUNT(*) FROM pending_sync_action")
|
||||
fun countAllActionsLive(): LiveData<Long>
|
||||
|
||||
@Query("SELECT * FROM pending_sync_action WHERE scheduled_for_upload = 0 ORDER BY sequence_number DESC")
|
||||
fun getLatestUnscheduledActionSync(): PendingSyncAction?
|
||||
|
||||
@Query("SELECT * FROM pending_sync_action LIMIT :pageSize OFFSET :offset")
|
||||
fun getPendingSyncActionPageSync(offset: Int, pageSize: Int): List<PendingSyncAction>
|
||||
|
||||
@Query("SELECT * FROM pending_sync_action ORDER BY sequence_number ASC")
|
||||
fun getAllPendingSyncActionsPaged(): DataSource.Factory<Int, PendingSyncAction>
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.data.dao
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
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 } }
|
||||
}
|
||||
|
||||
@Insert
|
||||
abstract fun addTemporarilyAllowedAppSync(app: TemporarilyAllowedApp)
|
||||
|
||||
@Query("DELETE FROM temporarily_allowed_app WHERE device_id = :deviceId")
|
||||
abstract fun removeAllTemporarilyAllowedAppsSync(deviceId: String)
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.data.dao
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.*
|
||||
import io.timelimit.android.data.model.TimeLimitRule
|
||||
|
||||
@Dao
|
||||
abstract class TimeLimitRuleDao {
|
||||
@Query("SELECT * FROM time_limit_rule WHERE category_id = :categoryId")
|
||||
abstract fun getTimeLimitRulesByCategory(categoryId: String): LiveData<List<TimeLimitRule>>
|
||||
|
||||
@Query("SELECT * FROM time_limit_rule WHERE category_id IN (:categoryIds)")
|
||||
abstract fun getTimeLimitRulesByCategories(categoryIds: List<String>): LiveData<List<TimeLimitRule>>
|
||||
|
||||
@Query("SELECT * FROM time_limit_rule WHERE category_id = :categoryId")
|
||||
abstract fun getTimeLimitRulesByCategorySync(categoryId: String): List<TimeLimitRule>
|
||||
|
||||
@Query("DELETE FROM time_limit_rule WHERE category_id = :categoryId")
|
||||
abstract fun deleteTimeLimitRulesByCategory(categoryId: String)
|
||||
|
||||
@Insert
|
||||
abstract fun addTimeLimitRule(rule: TimeLimitRule)
|
||||
|
||||
@Update
|
||||
abstract fun updateTimeLimitRule(rule: TimeLimitRule)
|
||||
|
||||
@Delete
|
||||
abstract fun deleteTimeLimitRule(rule: TimeLimitRule)
|
||||
|
||||
@Query("SELECT * FROM time_limit_rule WHERE id = :timeLimitRuleId")
|
||||
abstract fun getTimeLimitRuleByIdSync(timeLimitRuleId: String): TimeLimitRule?
|
||||
|
||||
@Query("SELECT * FROM time_limit_rule WHERE id = :timeLimitRuleId")
|
||||
abstract fun getTimeLimitRuleByIdLive(timeLimitRuleId: String): LiveData<TimeLimitRule?>
|
||||
|
||||
@Query("DELETE FROM time_limit_rule WHERE id = :timeLimitRuleId")
|
||||
abstract fun deleteTimeLimitRuleByIdSync(timeLimitRuleId: String)
|
||||
|
||||
@Query("DELETE FROM time_limit_rule WHERE id IN (:timeLimitRuleIds)")
|
||||
abstract fun deleteTimeLimitRulesByIdsSync(timeLimitRuleIds: List<String>)
|
||||
|
||||
@Query("SELECT * FROM time_limit_rule LIMIT :pageSize OFFSET :offset")
|
||||
abstract fun getRulePageSync(offset: Int, pageSize: Int): List<TimeLimitRule>
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.data.dao
|
||||
|
||||
import android.util.SparseArray
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.paging.DataSource
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import io.timelimit.android.data.model.UsedTimeItem
|
||||
import io.timelimit.android.livedata.ignoreUnchanged
|
||||
|
||||
@Dao
|
||||
abstract class UsedTimeDao {
|
||||
@Query("SELECT * FROM used_time WHERE category_id = :categoryId AND day_of_epoch >= :startingDayOfEpoch AND day_of_epoch <= :endDayOfEpoch")
|
||||
protected abstract fun getUsedTimesOfWeekInternal(categoryId: String, startingDayOfEpoch: Int, endDayOfEpoch: Int): LiveData<List<UsedTimeItem>>
|
||||
|
||||
fun getUsedTimesOfWeek(categoryId: String, firstDayOfWeekAsEpochDay: Int): LiveData<SparseArray<UsedTimeItem>> {
|
||||
return Transformations.map(getUsedTimesOfWeekInternal(categoryId, firstDayOfWeekAsEpochDay, firstDayOfWeekAsEpochDay + 6).ignoreUnchanged()) {
|
||||
val result = SparseArray<UsedTimeItem>()
|
||||
|
||||
it.forEach {
|
||||
result.put(it.dayOfEpoch - firstDayOfWeekAsEpochDay, it)
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
@Insert
|
||||
abstract fun insertUsedTime(item: UsedTimeItem)
|
||||
|
||||
@Insert
|
||||
abstract fun insertUsedTimes(item: List<UsedTimeItem>)
|
||||
|
||||
@Query("UPDATE used_time SET used_time = :newUsedTime WHERE category_id = :categoryId AND day_of_epoch = :dayOfEpoch")
|
||||
abstract fun updateUsedTime(categoryId: String, dayOfEpoch: Int, newUsedTime: Long)
|
||||
|
||||
@Query("UPDATE used_time SET used_time = used_time + :timeToAdd WHERE category_id = :categoryId AND day_of_epoch = :dayOfEpoch")
|
||||
abstract fun addUsedTime(categoryId: String, dayOfEpoch: Int, timeToAdd: Int): Int
|
||||
|
||||
@Query("SELECT * FROM used_time WHERE category_id = :categoryId AND day_of_epoch = :dayOfEpoch")
|
||||
abstract fun getUsedTimeItem(categoryId: String, dayOfEpoch: Int): LiveData<UsedTimeItem?>
|
||||
|
||||
@Query("DELETE FROM used_time WHERE category_id = :categoryId")
|
||||
abstract fun deleteUsedTimeItems(categoryId: String)
|
||||
|
||||
@Query("SELECT * FROM used_time LIMIT :pageSize OFFSET :offset")
|
||||
abstract fun getUsedTimePageSync(offset: Int, pageSize: Int): List<UsedTimeItem>
|
||||
|
||||
@Query("SELECT * FROM used_time WHERE category_id = :categoryId ORDER BY day_of_epoch DESC")
|
||||
abstract fun getUsedTimesByCategoryId(categoryId: String): DataSource.Factory<Int, UsedTimeItem>
|
||||
|
||||
@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>>
|
||||
}
|
74
app/src/main/java/io/timelimit/android/data/dao/UserDao.kt
Normal file
74
app/src/main/java/io/timelimit/android/data/dao/UserDao.kt
Normal file
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.data.dao
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import androidx.room.Update
|
||||
import io.timelimit.android.data.model.User
|
||||
|
||||
@Dao
|
||||
abstract class UserDao {
|
||||
@Query("SELECT * from user WHERE id = :userId")
|
||||
abstract fun getUserByIdLive(userId: String): LiveData<User?>
|
||||
|
||||
@Query("SELECT * from user WHERE id = :userId AND type = \"child\"")
|
||||
abstract fun getChildUserByIdLive(userId: String): LiveData<User?>
|
||||
|
||||
@Query("SELECT * from user WHERE id = :userId AND type = \"parent\"")
|
||||
abstract fun getParentUserByIdLive(userId: String): LiveData<User?>
|
||||
|
||||
@Query("SELECT * from user WHERE id = :userId")
|
||||
abstract fun getUserByIdSync(userId: String): User?
|
||||
|
||||
@Insert
|
||||
abstract fun addUserSync(user: User)
|
||||
|
||||
@Query("SELECT * FROM user")
|
||||
abstract fun getAllUsersLive(): LiveData<List<User>>
|
||||
|
||||
@Query("SELECT * FROM user")
|
||||
abstract fun getAllUsersSync(): List<User>
|
||||
|
||||
@Query("SELECT * FROM user WHERE type = \"parent\"")
|
||||
abstract fun getParentUsersLive(): LiveData<List<User>>
|
||||
|
||||
@Query("SELECT * FROM user WHERE type = \"parent\"")
|
||||
abstract fun getParentUsersSync(): List<User>
|
||||
|
||||
@Query("DELETE FROM user WHERE id IN (:userIds)")
|
||||
abstract fun deleteUsersByIds(userIds: List<String>)
|
||||
|
||||
@Update
|
||||
abstract fun updateUserSync(user: User)
|
||||
|
||||
@Query("UPDATE user SET disable_limits_until = :timestamp WHERE id = :childId AND type = \"child\"")
|
||||
abstract fun updateDisableChildUserLimitsUntil(childId: String, timestamp: Long): Int
|
||||
|
||||
@Query("SELECT * FROM user LIMIT :pageSize OFFSET :offset")
|
||||
abstract fun getUserPageSync(offset: Int, pageSize: Int): List<User>
|
||||
|
||||
@Query("UPDATE user SET category_for_not_assigned_apps = :categoryId WHERE id = :childId")
|
||||
abstract fun updateCategoryForUnassignedApps(childId: String, categoryId: String)
|
||||
|
||||
@Query("UPDATE user SET category_for_not_assigned_apps = \"\" WHERE category_for_not_assigned_apps = :categoryId")
|
||||
abstract fun removeAsCategoryForUnassignedApps(categoryId: String)
|
||||
|
||||
@Query("UPDATE user SET timezone = :timezone WHERE id = :userId")
|
||||
abstract fun updateUserTimezone(userId: String, timezone: String)
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.data.extensions
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import io.timelimit.android.data.model.User
|
||||
import io.timelimit.android.date.DateInTimezone
|
||||
import io.timelimit.android.livedata.ignoreUnchanged
|
||||
import io.timelimit.android.livedata.liveDataFromFunction
|
||||
import io.timelimit.android.livedata.switchMap
|
||||
import io.timelimit.android.logic.RealTimeLogic
|
||||
|
||||
fun LiveData<User?>.getDateLive(realTimeLogic: RealTimeLogic) = this.mapToTimezone().switchMap {
|
||||
timeZone ->
|
||||
|
||||
liveDataFromFunction {
|
||||
DateInTimezone.newInstance(realTimeLogic.getCurrentTimeInMillis(), timeZone)
|
||||
}
|
||||
}.ignoreUnchanged()
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.data.extensions
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import io.timelimit.android.data.model.User
|
||||
import io.timelimit.android.livedata.ignoreUnchanged
|
||||
import io.timelimit.android.livedata.map
|
||||
import java.util.*
|
||||
|
||||
fun User?.getTimezone(): TimeZone {
|
||||
return if (this != null) {
|
||||
TimeZone.getTimeZone(this.timeZone)
|
||||
} else {
|
||||
null
|
||||
} ?: TimeZone.getDefault()
|
||||
}
|
||||
|
||||
fun LiveData<User?>.mapToTimezone() = this.map { it.getTimezone() }.ignoreUnchanged()
|
122
app/src/main/java/io/timelimit/android/data/model/App.kt
Normal file
122
app/src/main/java/io/timelimit/android/data/model/App.kt
Normal file
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.data.model
|
||||
|
||||
import android.util.JsonReader
|
||||
import android.util.JsonWriter
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.TypeConverter
|
||||
import androidx.room.TypeConverters
|
||||
import io.timelimit.android.data.IdGenerator
|
||||
import io.timelimit.android.data.JsonSerializable
|
||||
|
||||
@Entity(primaryKeys = ["device_id", "package_name"], tableName = "app")
|
||||
@TypeConverters(AppRecommendationConverter::class)
|
||||
data class App (
|
||||
@ColumnInfo(index = true, name = "device_id")
|
||||
val deviceId: String,
|
||||
@ColumnInfo(index = true, name = "package_name")
|
||||
val packageName: String,
|
||||
@ColumnInfo(name = "title")
|
||||
val title: String,
|
||||
@ColumnInfo(name = "launchable")
|
||||
val isLaunchable: Boolean,
|
||||
@ColumnInfo(name = "recommendation")
|
||||
val recommendation: AppRecommendation
|
||||
): JsonSerializable {
|
||||
companion object {
|
||||
private const val DEVICE_ID = "d"
|
||||
private const val PACKAGE_NAME = "p"
|
||||
private const val TITLE = "t"
|
||||
private const val IS_LAUNCHABLE = "l"
|
||||
private const val RECOMMENDATION = "r"
|
||||
|
||||
fun parse(reader: JsonReader): App {
|
||||
var deviceId: String? = null
|
||||
var packageName: String? = null
|
||||
var title: String? = null
|
||||
var isLaunchable: Boolean? = null
|
||||
var recommendation: AppRecommendation? = null
|
||||
|
||||
reader.beginObject()
|
||||
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
DEVICE_ID -> deviceId = reader.nextString()
|
||||
PACKAGE_NAME -> packageName = reader.nextString()
|
||||
TITLE -> title = reader.nextString()
|
||||
IS_LAUNCHABLE -> isLaunchable = reader.nextBoolean()
|
||||
RECOMMENDATION -> recommendation = AppRecommendationJson.parse(reader.nextString())
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
|
||||
reader.endObject()
|
||||
|
||||
return App(
|
||||
deviceId = deviceId!!,
|
||||
packageName = packageName!!,
|
||||
title = title!!,
|
||||
isLaunchable = isLaunchable!!,
|
||||
recommendation = recommendation!!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
IdGenerator.assertIdValid(deviceId)
|
||||
}
|
||||
|
||||
override fun serialize(writer: JsonWriter) {
|
||||
writer.beginObject()
|
||||
|
||||
writer.name(DEVICE_ID).value(deviceId)
|
||||
writer.name(PACKAGE_NAME).value(packageName)
|
||||
writer.name(TITLE).value(title)
|
||||
writer.name(IS_LAUNCHABLE).value(isLaunchable)
|
||||
writer.name(RECOMMENDATION).value(AppRecommendationJson.serialize(recommendation))
|
||||
|
||||
writer.endObject()
|
||||
}
|
||||
}
|
||||
|
||||
enum class AppRecommendation {
|
||||
None, Whitelist, Blacklist
|
||||
}
|
||||
|
||||
class AppRecommendationConverter {
|
||||
@TypeConverter
|
||||
fun toAppRecommendation(value: String) = AppRecommendationJson.parse(value)
|
||||
|
||||
@TypeConverter
|
||||
fun toString(value: AppRecommendation) = AppRecommendationJson.serialize(value)
|
||||
}
|
||||
|
||||
object AppRecommendationJson {
|
||||
fun parse(value: String) = when(value) {
|
||||
"whitelist" -> AppRecommendation.Whitelist
|
||||
"blacklist" -> AppRecommendation.Blacklist
|
||||
"none" -> AppRecommendation.None
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
fun serialize(value: AppRecommendation) = when(value) {
|
||||
AppRecommendation.None -> "none"
|
||||
AppRecommendation.Blacklist -> "blacklist"
|
||||
AppRecommendation.Whitelist -> "whitelist"
|
||||
}
|
||||
}
|
154
app/src/main/java/io/timelimit/android/data/model/Category.kt
Normal file
154
app/src/main/java/io/timelimit/android/data/model/Category.kt
Normal file
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.data.model
|
||||
|
||||
import android.util.JsonReader
|
||||
import android.util.JsonWriter
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.TypeConverters
|
||||
import io.timelimit.android.data.IdGenerator
|
||||
import io.timelimit.android.data.JsonSerializable
|
||||
import io.timelimit.android.data.customtypes.ImmutableBitmask
|
||||
import io.timelimit.android.data.customtypes.ImmutableBitmaskAdapter
|
||||
import io.timelimit.android.data.customtypes.ImmutableBitmaskJson
|
||||
|
||||
@Entity(tableName = "category")
|
||||
@TypeConverters(ImmutableBitmaskAdapter::class)
|
||||
data class Category(
|
||||
@PrimaryKey
|
||||
@ColumnInfo(name = "id")
|
||||
val id: String,
|
||||
@ColumnInfo(name = "child_id")
|
||||
val childId: String,
|
||||
@ColumnInfo(name = "title")
|
||||
val title: String,
|
||||
@ColumnInfo(name = "blocked_times")
|
||||
val blockedMinutesInWeek: ImmutableBitmask, // 10080 bit -> ~10 KB
|
||||
@ColumnInfo(name = "extra_time")
|
||||
val extraTimeInMillis: Long,
|
||||
@ColumnInfo(name = "temporarily_blocked")
|
||||
val temporarilyBlocked: Boolean,
|
||||
@ColumnInfo(name = "base_version")
|
||||
val baseVersion: String,
|
||||
@ColumnInfo(name = "apps_version")
|
||||
val assignedAppsVersion: String,
|
||||
@ColumnInfo(name = "rules_version")
|
||||
val timeLimitRulesVersion: String,
|
||||
@ColumnInfo(name = "usedtimes_version")
|
||||
val usedTimesVersion: String,
|
||||
@ColumnInfo(name = "parent_category_id")
|
||||
val parentCategoryId: String
|
||||
): JsonSerializable {
|
||||
companion object {
|
||||
const val MINUTES_PER_DAY = 60 * 24
|
||||
const val BLOCKED_MINUTES_IN_WEEK_LENGTH = MINUTES_PER_DAY * 7
|
||||
|
||||
private const val ID = "id"
|
||||
private const val CHILD_ID = "cid"
|
||||
private const val TITLE = "T"
|
||||
private const val BLOCKED_MINUTES_IN_WEEK = "b"
|
||||
private const val EXTRA_TIME_IN_MILLIS = "et"
|
||||
private const val TEMPORARILY_BLOCKED = "tb"
|
||||
private const val BASE_VERSION = "vb"
|
||||
private const val ASSIGNED_APPS_VERSION = "va"
|
||||
private const val RULES_VERSION = "vr"
|
||||
private const val USED_TIMES_VERSION = "vu"
|
||||
private const val PARENT_CATEGORY_ID = "pc"
|
||||
|
||||
fun parse(reader: JsonReader): Category {
|
||||
var id: String? = null
|
||||
var childId: String? = null
|
||||
var title: String? = null
|
||||
var blockedMinutesInWeek: ImmutableBitmask? = null
|
||||
var extraTimeInMillis: Long? = null
|
||||
var temporarilyBlocked: Boolean? = null
|
||||
var baseVersion: String? = null
|
||||
var assignedAppsVersion: String? = null
|
||||
var timeLimitRulesVersion: String? = null
|
||||
var usedTimesVersion: String? = null
|
||||
// this field was added later so it has got a default value
|
||||
var parentCategoryId = ""
|
||||
|
||||
reader.beginObject()
|
||||
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
ID -> id = reader.nextString()
|
||||
CHILD_ID -> childId = reader.nextString()
|
||||
TITLE -> title = reader.nextString()
|
||||
BLOCKED_MINUTES_IN_WEEK -> blockedMinutesInWeek = ImmutableBitmaskJson.parse(reader.nextString(), BLOCKED_MINUTES_IN_WEEK_LENGTH)
|
||||
EXTRA_TIME_IN_MILLIS -> extraTimeInMillis = reader.nextLong()
|
||||
TEMPORARILY_BLOCKED -> temporarilyBlocked = reader.nextBoolean()
|
||||
BASE_VERSION -> baseVersion = reader.nextString()
|
||||
ASSIGNED_APPS_VERSION -> assignedAppsVersion = reader.nextString()
|
||||
RULES_VERSION -> timeLimitRulesVersion = reader.nextString()
|
||||
USED_TIMES_VERSION -> usedTimesVersion = reader.nextString()
|
||||
PARENT_CATEGORY_ID -> parentCategoryId = reader.nextString()
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
|
||||
reader.endObject()
|
||||
|
||||
return Category(
|
||||
id = id!!,
|
||||
childId = childId!!,
|
||||
title = title!!,
|
||||
blockedMinutesInWeek = blockedMinutesInWeek!!,
|
||||
extraTimeInMillis = extraTimeInMillis!!,
|
||||
temporarilyBlocked = temporarilyBlocked!!,
|
||||
baseVersion = baseVersion!!,
|
||||
assignedAppsVersion = assignedAppsVersion!!,
|
||||
timeLimitRulesVersion = timeLimitRulesVersion!!,
|
||||
usedTimesVersion = usedTimesVersion!!,
|
||||
parentCategoryId = parentCategoryId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
IdGenerator.assertIdValid(id)
|
||||
IdGenerator.assertIdValid(childId)
|
||||
|
||||
if (extraTimeInMillis < 0) {
|
||||
throw IllegalStateException()
|
||||
}
|
||||
|
||||
if (title.isEmpty()) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
|
||||
override fun serialize(writer: JsonWriter) {
|
||||
writer.beginObject()
|
||||
|
||||
writer.name(ID).value(id)
|
||||
writer.name(CHILD_ID).value(childId)
|
||||
writer.name(TITLE).value(title)
|
||||
writer.name(BLOCKED_MINUTES_IN_WEEK).value(ImmutableBitmaskJson.serialize(blockedMinutesInWeek))
|
||||
writer.name(EXTRA_TIME_IN_MILLIS).value(extraTimeInMillis)
|
||||
writer.name(TEMPORARILY_BLOCKED).value(temporarilyBlocked)
|
||||
writer.name(BASE_VERSION).value(baseVersion)
|
||||
writer.name(ASSIGNED_APPS_VERSION).value(assignedAppsVersion)
|
||||
writer.name(RULES_VERSION).value(timeLimitRulesVersion)
|
||||
writer.name(USED_TIMES_VERSION).value(usedTimesVersion)
|
||||
writer.name(PARENT_CATEGORY_ID).value(parentCategoryId)
|
||||
|
||||
writer.endObject()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.data.model
|
||||
|
||||
import android.util.JsonReader
|
||||
import android.util.JsonWriter
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import io.timelimit.android.data.IdGenerator
|
||||
import io.timelimit.android.data.JsonSerializable
|
||||
|
||||
@Entity(primaryKeys = ["category_id", "package_name"], tableName = "category_app")
|
||||
data class CategoryApp(
|
||||
@ColumnInfo(index = true, name = "category_id")
|
||||
val categoryId: String,
|
||||
@ColumnInfo(index = true, name = "package_name")
|
||||
val packageName: String
|
||||
): JsonSerializable {
|
||||
companion object {
|
||||
private const val CATEGORY_ID = "c"
|
||||
private const val PACKAGE_NAME = "p"
|
||||
|
||||
fun parse(reader: JsonReader): CategoryApp {
|
||||
var categoryId: String? = null
|
||||
var packageName: String? = null
|
||||
|
||||
reader.beginObject()
|
||||
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
CATEGORY_ID -> categoryId = reader.nextString()
|
||||
PACKAGE_NAME -> packageName = reader.nextString()
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
|
||||
reader.endObject()
|
||||
|
||||
return CategoryApp(
|
||||
categoryId = categoryId!!,
|
||||
packageName = packageName!!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
IdGenerator.assertIdValid(categoryId)
|
||||
|
||||
if (packageName.isEmpty()) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
|
||||
override fun serialize(writer: JsonWriter) {
|
||||
writer.beginObject()
|
||||
|
||||
writer.name(CATEGORY_ID).value(categoryId)
|
||||
writer.name(PACKAGE_NAME).value(packageName)
|
||||
|
||||
writer.endObject()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,168 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.data.model
|
||||
|
||||
import android.util.JsonReader
|
||||
import android.util.JsonWriter
|
||||
import androidx.room.*
|
||||
import io.timelimit.android.data.JsonSerializable
|
||||
|
||||
@Entity(tableName = "config")
|
||||
@TypeConverters(ConfigurationItemTypeConverter::class)
|
||||
data class ConfigurationItem(
|
||||
@PrimaryKey
|
||||
@ColumnInfo(name = "id")
|
||||
val key: ConfigurationItemType,
|
||||
@ColumnInfo(name = "value")
|
||||
val value: String
|
||||
): JsonSerializable {
|
||||
companion object {
|
||||
private const val KEY = "k"
|
||||
private const val VALUE = "v"
|
||||
|
||||
// returns null if parsing failed
|
||||
fun parse(reader: JsonReader): ConfigurationItem? {
|
||||
var key: Int? = null
|
||||
var value: String? = null
|
||||
|
||||
reader.beginObject()
|
||||
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
KEY -> key = reader.nextInt()
|
||||
VALUE -> value = reader.nextString()
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
|
||||
reader.endObject()
|
||||
|
||||
key!!
|
||||
value!!
|
||||
|
||||
try {
|
||||
return ConfigurationItem(
|
||||
key = ConfigurationItemTypeUtil.parse(key),
|
||||
value = value
|
||||
)
|
||||
} catch (ex: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun serialize(writer: JsonWriter) {
|
||||
writer.beginObject()
|
||||
|
||||
writer.name(KEY).value(ConfigurationItemTypeUtil.serialize(key))
|
||||
writer.name(VALUE).value(value)
|
||||
|
||||
writer.endObject()
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: validate config item values
|
||||
|
||||
enum class ConfigurationItemType {
|
||||
OwnDeviceId,
|
||||
UserListVersion,
|
||||
DeviceListVersion,
|
||||
NextSyncSequenceNumber,
|
||||
DeviceAuthToken,
|
||||
FullVersionUntil,
|
||||
ShownHints,
|
||||
WasDeviceLocked,
|
||||
LastAppVersionWhichSynced,
|
||||
LastScreenOnTime,
|
||||
ServerMessage,
|
||||
CustomServerUrl
|
||||
}
|
||||
|
||||
object ConfigurationItemTypeUtil {
|
||||
private const val OWN_DEVICE_ID = 1
|
||||
private const val USER_LIST_VERSION = 2
|
||||
private const val DEVICE_LIST_VERSION = 3
|
||||
private const val NEXT_SYNC_SEQUENCE_NUMBER = 4
|
||||
private const val DEVICE_AUTH_TOKEN = 5
|
||||
private const val FULL_VERSION_UNTIL = 6
|
||||
private const val SHOWN_HINTS = 7
|
||||
private const val WAS_DEVICE_LOCKED = 9
|
||||
private const val LAST_APP_VERSION_WHICH_SYNCED = 10
|
||||
private const val LAST_SCREEN_ON_TIME = 11
|
||||
private const val SERVER_MESSAGE = 12
|
||||
private const val CUSTOM_SERVER_URL = 13
|
||||
|
||||
val TYPES = listOf(
|
||||
ConfigurationItemType.OwnDeviceId,
|
||||
ConfigurationItemType.UserListVersion,
|
||||
ConfigurationItemType.DeviceListVersion,
|
||||
ConfigurationItemType.NextSyncSequenceNumber,
|
||||
ConfigurationItemType.DeviceAuthToken,
|
||||
ConfigurationItemType.FullVersionUntil,
|
||||
ConfigurationItemType.ShownHints,
|
||||
ConfigurationItemType.WasDeviceLocked,
|
||||
ConfigurationItemType.LastAppVersionWhichSynced,
|
||||
ConfigurationItemType.LastScreenOnTime,
|
||||
ConfigurationItemType.ServerMessage,
|
||||
ConfigurationItemType.CustomServerUrl
|
||||
)
|
||||
|
||||
fun serialize(value: ConfigurationItemType) = when(value) {
|
||||
ConfigurationItemType.OwnDeviceId -> OWN_DEVICE_ID
|
||||
ConfigurationItemType.UserListVersion -> USER_LIST_VERSION
|
||||
ConfigurationItemType.DeviceListVersion -> DEVICE_LIST_VERSION
|
||||
ConfigurationItemType.NextSyncSequenceNumber -> NEXT_SYNC_SEQUENCE_NUMBER
|
||||
ConfigurationItemType.DeviceAuthToken -> DEVICE_AUTH_TOKEN
|
||||
ConfigurationItemType.FullVersionUntil -> FULL_VERSION_UNTIL
|
||||
ConfigurationItemType.ShownHints -> SHOWN_HINTS
|
||||
ConfigurationItemType.WasDeviceLocked -> WAS_DEVICE_LOCKED
|
||||
ConfigurationItemType.LastAppVersionWhichSynced -> LAST_APP_VERSION_WHICH_SYNCED
|
||||
ConfigurationItemType.LastScreenOnTime -> LAST_SCREEN_ON_TIME
|
||||
ConfigurationItemType.ServerMessage -> SERVER_MESSAGE
|
||||
ConfigurationItemType.CustomServerUrl -> CUSTOM_SERVER_URL
|
||||
}
|
||||
|
||||
fun parse(value: Int) = when(value) {
|
||||
OWN_DEVICE_ID -> ConfigurationItemType.OwnDeviceId
|
||||
USER_LIST_VERSION -> ConfigurationItemType.UserListVersion
|
||||
DEVICE_LIST_VERSION -> ConfigurationItemType.DeviceListVersion
|
||||
NEXT_SYNC_SEQUENCE_NUMBER -> ConfigurationItemType.NextSyncSequenceNumber
|
||||
DEVICE_AUTH_TOKEN -> ConfigurationItemType.DeviceAuthToken
|
||||
FULL_VERSION_UNTIL -> ConfigurationItemType.FullVersionUntil
|
||||
SHOWN_HINTS -> ConfigurationItemType.ShownHints
|
||||
WAS_DEVICE_LOCKED -> ConfigurationItemType.WasDeviceLocked
|
||||
LAST_APP_VERSION_WHICH_SYNCED -> ConfigurationItemType.LastAppVersionWhichSynced
|
||||
LAST_SCREEN_ON_TIME -> ConfigurationItemType.LastScreenOnTime
|
||||
SERVER_MESSAGE -> ConfigurationItemType.ServerMessage
|
||||
CUSTOM_SERVER_URL -> ConfigurationItemType.CustomServerUrl
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
|
||||
class ConfigurationItemTypeConverter {
|
||||
@TypeConverter
|
||||
fun toInt(value: ConfigurationItemType) = ConfigurationItemTypeUtil.serialize(value)
|
||||
|
||||
@TypeConverter
|
||||
fun toConfigurationItemType(value: Int) = ConfigurationItemTypeUtil.parse(value)
|
||||
}
|
||||
|
||||
object HintsToShow {
|
||||
const val OVERVIEW_INTRODUCTION = 1L
|
||||
const val DEVICE_SCREEN_INTRODUCTION = 2L
|
||||
const val CATEGORIES_INTRODUCTION = 4L
|
||||
const val TIME_LIMIT_RULE_INTRODUCTION = 8L
|
||||
}
|
311
app/src/main/java/io/timelimit/android/data/model/Device.kt
Normal file
311
app/src/main/java/io/timelimit/android/data/model/Device.kt
Normal file
|
@ -0,0 +1,311 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.data.model
|
||||
|
||||
import android.util.JsonReader
|
||||
import android.util.JsonWriter
|
||||
import androidx.room.*
|
||||
import io.timelimit.android.data.IdGenerator
|
||||
import io.timelimit.android.data.JsonSerializable
|
||||
import io.timelimit.android.integration.platform.*
|
||||
|
||||
@Entity(tableName = "device")
|
||||
@TypeConverters(
|
||||
NetworkTimeAdapter::class,
|
||||
ProtectionLevelConverter::class,
|
||||
RuntimePermissionStatusConverter::class,
|
||||
NewPermissionStatusConverter::class
|
||||
)
|
||||
data class Device(
|
||||
@PrimaryKey
|
||||
@ColumnInfo(name = "id")
|
||||
val id: String,
|
||||
@ColumnInfo(name = "name")
|
||||
val name: String,
|
||||
@ColumnInfo(name = "model")
|
||||
val model: String,
|
||||
@ColumnInfo(name = "added_at")
|
||||
val addedAt: Long,
|
||||
@ColumnInfo(name = "current_user_id")
|
||||
val currentUserId: String, // empty if not set
|
||||
@ColumnInfo(name = "apps_version")
|
||||
val installedAppsVersion: String,
|
||||
@ColumnInfo(name = "network_time")
|
||||
val networkTime: NetworkTime,
|
||||
@ColumnInfo(name = "current_protection_level")
|
||||
val currentProtectionLevel: ProtectionLevel,
|
||||
@ColumnInfo(name = "highest_permission_level")
|
||||
val highestProtectionLevel: ProtectionLevel,
|
||||
@ColumnInfo(name = "current_usage_stats_permission")
|
||||
val currentUsageStatsPermission: RuntimePermissionStatus,
|
||||
@ColumnInfo(name = "highest_usage_stats_permission")
|
||||
val highestUsageStatsPermission: RuntimePermissionStatus,
|
||||
@ColumnInfo(name = "current_notification_access_permission")
|
||||
val currentNotificationAccessPermission: NewPermissionStatus,
|
||||
@ColumnInfo(name = "highest_notification_access_permission")
|
||||
val highestNotificationAccessPermission: NewPermissionStatus,
|
||||
@ColumnInfo(name = "current_app_version")
|
||||
val currentAppVersion: Int,
|
||||
@ColumnInfo(name = "highest_app_version")
|
||||
val highestAppVersion: Int,
|
||||
@ColumnInfo(name = "tried_disabling_device_admin")
|
||||
val manipulationTriedDisablingDeviceAdmin: Boolean,
|
||||
@ColumnInfo(name = "did_reboot")
|
||||
val manipulationDidReboot: Boolean,
|
||||
@ColumnInfo(name = "had_manipulation")
|
||||
val hadManipulation: Boolean,
|
||||
@ColumnInfo(name = "did_report_uninstall")
|
||||
val didReportUninstall: Boolean,
|
||||
@ColumnInfo(name = "is_user_kept_signed_in")
|
||||
val isUserKeptSignedIn: Boolean,
|
||||
@ColumnInfo(name = "show_device_connected")
|
||||
val showDeviceConnected: Boolean,
|
||||
@ColumnInfo(name = "default_user")
|
||||
val defaultUser: String,
|
||||
@ColumnInfo(name = "default_user_timeout")
|
||||
val defaultUserTimeout: Int,
|
||||
@ColumnInfo(name = "consider_reboot_manipulation")
|
||||
val considerRebootManipulation: Boolean
|
||||
): JsonSerializable {
|
||||
companion object {
|
||||
private const val ID = "id"
|
||||
private const val NAME = "n"
|
||||
private const val MODEL = "m"
|
||||
private const val ADDED_AT = "aa"
|
||||
private const val CURRENT_USER_ID = "u"
|
||||
private const val INSTALLED_APPS_VERSION = "va"
|
||||
private const val NETWORK_TIME = "t"
|
||||
private const val CURRENT_PROTECTION_LEVEL = "pc"
|
||||
private const val HIGHEST_PROTECTION_LEVEL = "pm"
|
||||
private const val CURRENT_USAGE_STATS_PERMISSION = "uc"
|
||||
private const val HIGHEST_USAGE_STATS_PERMISSION = "um"
|
||||
private const val CURRENT_NOTIFICATION_ACCESS_PERMISSION = "nc"
|
||||
private const val HIGHEST_NOTIFICATION_ACCESS_PERMISSION = "nm"
|
||||
private const val CURRENT_APP_VERSION = "ac"
|
||||
private const val HIGHEST_APP_VERSION = "am"
|
||||
private const val TRIED_DISABLING_DEVICE_ADMIN = "tdda"
|
||||
private const val MANIPULATION_DID_REBOOT = "mdr"
|
||||
private const val HAD_MANIPULATION = "hm"
|
||||
private const val DID_REPORT_UNINSTALL = "dru"
|
||||
private const val IS_USER_KEPT_SIGNED_IN = "iuksi"
|
||||
private const val SHOW_DEVICE_CONNECTED = "sdc"
|
||||
private const val DEFAULT_USER = "du"
|
||||
private const val DEFAULT_USER_TIMEOUT = "dut"
|
||||
private const val CONSIDER_REBOOT_A_MANIPULATION = "cram"
|
||||
|
||||
fun parse(reader: JsonReader): Device {
|
||||
var id: String? = null
|
||||
var name: String? = null
|
||||
var model: String? = null
|
||||
var addedAt: Long? = null
|
||||
var currentUserId: String? = null
|
||||
var installedAppsVersion: String? = null
|
||||
var networkTime: NetworkTime? = null
|
||||
var currentProtectionLevel: ProtectionLevel? = null
|
||||
var highestProtectionLevel: ProtectionLevel? = null
|
||||
var currentUsageStatsPermission: RuntimePermissionStatus? = null
|
||||
var highestUsageStatsPermission: RuntimePermissionStatus? = null
|
||||
var currentNotificationAccessPermission: NewPermissionStatus? = null
|
||||
var highestNotificationAccessPermission: NewPermissionStatus? = null
|
||||
var currentAppVersion: Int? = null
|
||||
var highestAppVersion: Int? = null
|
||||
var manipulationTriedDisablingDeviceAdmin: Boolean? = null
|
||||
var manipulationDidReboot: Boolean = false
|
||||
var hadManipulation: Boolean? = null
|
||||
var didReportUninstall = false // this was added later, so it has got a default value
|
||||
var isUserKeptSignedIn = false
|
||||
var showDeviceConnected = false
|
||||
var defaultUser = ""
|
||||
var defaultUserTimeout = 0
|
||||
var considerRebootManipulation = false
|
||||
|
||||
reader.beginObject()
|
||||
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
ID -> id = reader.nextString()
|
||||
NAME -> name = reader.nextString()
|
||||
MODEL -> model = reader.nextString()
|
||||
ADDED_AT -> addedAt = reader.nextLong()
|
||||
CURRENT_USER_ID -> currentUserId = reader.nextString()
|
||||
INSTALLED_APPS_VERSION -> installedAppsVersion = reader.nextString()
|
||||
NETWORK_TIME -> networkTime = NetworkTimeJson.parse(reader.nextString())
|
||||
CURRENT_PROTECTION_LEVEL -> currentProtectionLevel = ProtectionLevelUtil.parse(reader.nextString())
|
||||
HIGHEST_PROTECTION_LEVEL -> highestProtectionLevel = ProtectionLevelUtil.parse(reader.nextString())
|
||||
CURRENT_USAGE_STATS_PERMISSION -> currentUsageStatsPermission = RuntimePermissionStatusUtil.parse(reader.nextString())
|
||||
HIGHEST_USAGE_STATS_PERMISSION -> highestUsageStatsPermission = RuntimePermissionStatusUtil.parse(reader.nextString())
|
||||
CURRENT_NOTIFICATION_ACCESS_PERMISSION -> currentNotificationAccessPermission = NewPermissionStatusUtil.parse(reader.nextString())
|
||||
HIGHEST_NOTIFICATION_ACCESS_PERMISSION -> highestNotificationAccessPermission = NewPermissionStatusUtil.parse(reader.nextString())
|
||||
CURRENT_APP_VERSION -> currentAppVersion = reader.nextInt()
|
||||
HIGHEST_APP_VERSION -> highestAppVersion = reader.nextInt()
|
||||
TRIED_DISABLING_DEVICE_ADMIN -> manipulationTriedDisablingDeviceAdmin = reader.nextBoolean()
|
||||
MANIPULATION_DID_REBOOT -> manipulationDidReboot = reader.nextBoolean()
|
||||
HAD_MANIPULATION -> hadManipulation = reader.nextBoolean()
|
||||
DID_REPORT_UNINSTALL -> didReportUninstall = reader.nextBoolean()
|
||||
IS_USER_KEPT_SIGNED_IN -> isUserKeptSignedIn = reader.nextBoolean()
|
||||
SHOW_DEVICE_CONNECTED -> showDeviceConnected = reader.nextBoolean()
|
||||
DEFAULT_USER -> defaultUser = reader.nextString()
|
||||
DEFAULT_USER_TIMEOUT -> defaultUserTimeout = reader.nextInt()
|
||||
CONSIDER_REBOOT_A_MANIPULATION -> considerRebootManipulation = reader.nextBoolean()
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
|
||||
reader.endObject()
|
||||
|
||||
return Device(
|
||||
id = id!!,
|
||||
name = name!!,
|
||||
model = model!!,
|
||||
addedAt = addedAt!!,
|
||||
currentUserId = currentUserId!!,
|
||||
installedAppsVersion = installedAppsVersion!!,
|
||||
networkTime = networkTime!!,
|
||||
currentProtectionLevel = currentProtectionLevel!!,
|
||||
highestProtectionLevel = highestProtectionLevel!!,
|
||||
currentUsageStatsPermission = currentUsageStatsPermission!!,
|
||||
highestUsageStatsPermission = highestUsageStatsPermission!!,
|
||||
currentNotificationAccessPermission = currentNotificationAccessPermission!!,
|
||||
highestNotificationAccessPermission = highestNotificationAccessPermission!!,
|
||||
currentAppVersion = currentAppVersion!!,
|
||||
highestAppVersion = highestAppVersion!!,
|
||||
manipulationTriedDisablingDeviceAdmin = manipulationTriedDisablingDeviceAdmin!!,
|
||||
manipulationDidReboot = manipulationDidReboot,
|
||||
hadManipulation = hadManipulation!!,
|
||||
didReportUninstall = didReportUninstall,
|
||||
isUserKeptSignedIn = isUserKeptSignedIn,
|
||||
showDeviceConnected = showDeviceConnected,
|
||||
defaultUser = defaultUser,
|
||||
defaultUserTimeout = defaultUserTimeout,
|
||||
considerRebootManipulation = considerRebootManipulation
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
IdGenerator.assertIdValid(id)
|
||||
|
||||
if (currentUserId.isNotEmpty()) {
|
||||
IdGenerator.assertIdValid(currentUserId)
|
||||
}
|
||||
|
||||
if (name.isEmpty()) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
if (model.isEmpty()) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
if (addedAt < 0) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
if (currentAppVersion < 0 || highestAppVersion < 0) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
if (defaultUser.isNotEmpty()) {
|
||||
IdGenerator.assertIdValid(defaultUser)
|
||||
}
|
||||
|
||||
if (defaultUserTimeout < 0) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
|
||||
override fun serialize(writer: JsonWriter) {
|
||||
writer.beginObject()
|
||||
|
||||
writer.name(ID).value(id)
|
||||
writer.name(NAME).value(name)
|
||||
writer.name(MODEL).value(model)
|
||||
writer.name(ADDED_AT).value(addedAt)
|
||||
writer.name(CURRENT_USER_ID).value(currentUserId)
|
||||
writer.name(INSTALLED_APPS_VERSION).value(installedAppsVersion)
|
||||
writer.name(NETWORK_TIME).value(NetworkTimeJson.serialize(networkTime))
|
||||
writer.name(CURRENT_PROTECTION_LEVEL).value(ProtectionLevelUtil.serialize(currentProtectionLevel))
|
||||
writer.name(HIGHEST_PROTECTION_LEVEL).value(ProtectionLevelUtil.serialize(highestProtectionLevel))
|
||||
writer.name(CURRENT_USAGE_STATS_PERMISSION).value(RuntimePermissionStatusUtil.serialize(currentUsageStatsPermission))
|
||||
writer.name(HIGHEST_USAGE_STATS_PERMISSION).value(RuntimePermissionStatusUtil.serialize(highestUsageStatsPermission))
|
||||
writer.name(CURRENT_NOTIFICATION_ACCESS_PERMISSION).value(NewPermissionStatusUtil.serialize(currentNotificationAccessPermission))
|
||||
writer.name(HIGHEST_NOTIFICATION_ACCESS_PERMISSION).value(NewPermissionStatusUtil.serialize(highestNotificationAccessPermission))
|
||||
writer.name(CURRENT_APP_VERSION).value(currentAppVersion)
|
||||
writer.name(HIGHEST_APP_VERSION).value(highestAppVersion)
|
||||
writer.name(TRIED_DISABLING_DEVICE_ADMIN).value(manipulationTriedDisablingDeviceAdmin)
|
||||
writer.name(MANIPULATION_DID_REBOOT).value(manipulationDidReboot)
|
||||
writer.name(HAD_MANIPULATION).value(hadManipulation)
|
||||
writer.name(DID_REPORT_UNINSTALL).value(didReportUninstall)
|
||||
writer.name(IS_USER_KEPT_SIGNED_IN).value(isUserKeptSignedIn)
|
||||
writer.name(SHOW_DEVICE_CONNECTED).value(showDeviceConnected)
|
||||
writer.name(DEFAULT_USER).value(defaultUser)
|
||||
writer.name(DEFAULT_USER_TIMEOUT).value(defaultUserTimeout)
|
||||
writer.name(CONSIDER_REBOOT_A_MANIPULATION).value(considerRebootManipulation)
|
||||
|
||||
writer.endObject()
|
||||
}
|
||||
|
||||
@Transient
|
||||
val manipulationOfProtectionLevel = currentProtectionLevel != highestProtectionLevel
|
||||
@Transient
|
||||
val manipulationOfUsageStats = currentUsageStatsPermission != highestUsageStatsPermission
|
||||
@Transient
|
||||
val manipulationOfNotificationAccess = currentNotificationAccessPermission != highestNotificationAccessPermission
|
||||
@Transient
|
||||
val manipulationOfAppVersion = currentAppVersion != highestAppVersion
|
||||
|
||||
@Transient
|
||||
val hasActiveManipulationWarning = manipulationOfProtectionLevel ||
|
||||
manipulationOfUsageStats ||
|
||||
manipulationOfNotificationAccess ||
|
||||
manipulationOfAppVersion ||
|
||||
manipulationTriedDisablingDeviceAdmin ||
|
||||
manipulationDidReboot
|
||||
|
||||
@Transient
|
||||
val hasAnyManipulation = hasActiveManipulationWarning || hadManipulation
|
||||
}
|
||||
|
||||
enum class NetworkTime {
|
||||
Disabled, IfPossible, Enabled
|
||||
}
|
||||
|
||||
object NetworkTimeJson {
|
||||
private const val DISABLED = "disabled"
|
||||
private const val IF_POSSIBLE = "if possible"
|
||||
private const val ENABLED = "enabled"
|
||||
|
||||
fun parse(value: String) = when(value) {
|
||||
DISABLED -> NetworkTime.Disabled
|
||||
IF_POSSIBLE -> NetworkTime.IfPossible
|
||||
ENABLED -> NetworkTime.Enabled
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
fun serialize(value: NetworkTime) = when(value) {
|
||||
NetworkTime.Disabled -> DISABLED
|
||||
NetworkTime.IfPossible -> IF_POSSIBLE
|
||||
NetworkTime.Enabled -> ENABLED
|
||||
}
|
||||
}
|
||||
|
||||
class NetworkTimeAdapter {
|
||||
@TypeConverter
|
||||
fun toString(networkTime: NetworkTime) = NetworkTimeJson.serialize(networkTime)
|
||||
|
||||
@TypeConverter
|
||||
fun toNetworkTime(value: String) = NetworkTimeJson.parse(value)
|
||||
}
|
|
@ -0,0 +1,160 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.data.model
|
||||
|
||||
import android.util.JsonReader
|
||||
import android.util.JsonWriter
|
||||
import androidx.room.*
|
||||
import io.timelimit.android.data.IdGenerator
|
||||
import io.timelimit.android.data.JsonSerializable
|
||||
|
||||
/*
|
||||
* parent and child actions must be signed
|
||||
* This happens with the following:
|
||||
* 1. The passwords for parents are stored 2 times
|
||||
* 1. encrypted with bcrypt
|
||||
* 2. encrypted with bcrypt, but the clients only keep the salt, only the server stores the full hash
|
||||
* 2. The full hash of the second sample (KEY) is used as a base for signing
|
||||
* 3. The integrity data is SHA512asHexString(sequenceNumber.toString() + deviceId + encodedAction + KEY).substring(0, 32)
|
||||
*
|
||||
* The integrity for child actions is empty
|
||||
*/
|
||||
|
||||
@Entity(tableName = "pending_sync_action")
|
||||
@TypeConverters(PendingSyncActionAdapter::class)
|
||||
data class PendingSyncAction(
|
||||
@ColumnInfo(name = "sequence_number")
|
||||
@PrimaryKey
|
||||
val sequenceNumber: Long,
|
||||
@ColumnInfo(name = "action")
|
||||
val encodedAction: String,
|
||||
@ColumnInfo(name = "integrity")
|
||||
val integrity: String,
|
||||
@ColumnInfo(name = "scheduled_for_upload", index = true)
|
||||
// actions can be modified/ merged if they were not yet scheduled for an upload
|
||||
// this merging is made by the upload function
|
||||
val scheduledForUpload: Boolean,
|
||||
@ColumnInfo(name = "type")
|
||||
val type: PendingSyncActionType,
|
||||
@ColumnInfo(name = "user_id")
|
||||
val userId: String
|
||||
): JsonSerializable {
|
||||
companion object {
|
||||
private const val SEQUENCE_NUMBER = "n"
|
||||
private const val ENCODED_ACTION = "a"
|
||||
private const val INTEGRITY = "i"
|
||||
private const val SCHEDULED_FOR_UPLOAD = "s"
|
||||
private const val TYPE = "t"
|
||||
private const val TYPE_NEW = "t2"
|
||||
private const val USER_ID = "u"
|
||||
|
||||
fun parse(reader: JsonReader): PendingSyncAction {
|
||||
var sequenceNumber: Long? = null
|
||||
var encodedAction: String? = null
|
||||
var integrity: String? = null
|
||||
var scheduledForUpload: Boolean? = null
|
||||
var type: PendingSyncActionType? = null
|
||||
var typeNew: PendingSyncActionType? = null
|
||||
var userId: String? = null
|
||||
|
||||
reader.beginObject()
|
||||
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
SEQUENCE_NUMBER -> sequenceNumber = reader.nextLong()
|
||||
ENCODED_ACTION -> encodedAction = reader.nextString()
|
||||
INTEGRITY -> integrity = reader.nextString()
|
||||
SCHEDULED_FOR_UPLOAD -> scheduledForUpload = reader.nextBoolean()
|
||||
TYPE -> type = PendingSyncActionTypeConverter.parse(reader.nextString())
|
||||
TYPE_NEW -> typeNew = PendingSyncActionTypeConverter.parse(reader.nextString())
|
||||
USER_ID -> userId = reader.nextString()
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
|
||||
reader.endObject()
|
||||
|
||||
return PendingSyncAction(
|
||||
sequenceNumber = sequenceNumber!!,
|
||||
encodedAction = encodedAction!!,
|
||||
integrity = integrity!!,
|
||||
scheduledForUpload = scheduledForUpload!!,
|
||||
type = (typeNew ?: type)!!,
|
||||
userId = userId!!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
if (userId.isNotEmpty()) {
|
||||
IdGenerator.assertIdValid(userId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun serialize(writer: JsonWriter) {
|
||||
writer.beginObject()
|
||||
|
||||
writer.name(SEQUENCE_NUMBER).value(sequenceNumber)
|
||||
writer.name(ENCODED_ACTION).value(encodedAction)
|
||||
writer.name(INTEGRITY).value(integrity)
|
||||
writer.name(SCHEDULED_FOR_UPLOAD).value(scheduledForUpload)
|
||||
writer.name(TYPE).value(
|
||||
PendingSyncActionTypeConverter.serialize(
|
||||
if (type != PendingSyncActionType.Child) {
|
||||
type
|
||||
} else {
|
||||
// this will make the actions fail, but prevents a crash after downgrades
|
||||
PendingSyncActionType.Parent
|
||||
}
|
||||
)
|
||||
)
|
||||
writer.name(TYPE_NEW).value(PendingSyncActionTypeConverter.serialize(type))
|
||||
writer.name(USER_ID).value(userId)
|
||||
|
||||
writer.endObject()
|
||||
}
|
||||
}
|
||||
|
||||
enum class PendingSyncActionType {
|
||||
Parent, AppLogic, Child
|
||||
}
|
||||
|
||||
object PendingSyncActionTypeConverter {
|
||||
private const val APP_LOGIC = "appLogic"
|
||||
private const val PARENT = "parent"
|
||||
private const val CHILD = "child"
|
||||
|
||||
fun serialize(type: PendingSyncActionType) = when(type) {
|
||||
PendingSyncActionType.Parent -> PARENT
|
||||
PendingSyncActionType.AppLogic -> APP_LOGIC
|
||||
PendingSyncActionType.Child -> CHILD
|
||||
}
|
||||
|
||||
fun parse(value: String) = when(value) {
|
||||
PARENT -> PendingSyncActionType.Parent
|
||||
APP_LOGIC -> PendingSyncActionType.AppLogic
|
||||
CHILD -> PendingSyncActionType.Child
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
class PendingSyncActionAdapter {
|
||||
@TypeConverter
|
||||
fun toString(value: PendingSyncActionType) = PendingSyncActionTypeConverter.serialize(value)
|
||||
|
||||
@TypeConverter
|
||||
fun toPendingSyncActionType(value: String) = PendingSyncActionTypeConverter.parse(value)
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.data.model
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import io.timelimit.android.data.IdGenerator
|
||||
|
||||
@Entity(tableName = "temporarily_allowed_app", primaryKeys = ["device_id", "package_name"])
|
||||
data class TemporarilyAllowedApp(
|
||||
@ColumnInfo(name = "device_id")
|
||||
val deviceId: String,
|
||||
@ColumnInfo(name = "package_name")
|
||||
val packageName: String
|
||||
) {
|
||||
init {
|
||||
IdGenerator.assertIdValid(deviceId)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.data.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import android.util.JsonReader
|
||||
import android.util.JsonWriter
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.TypeConverters
|
||||
import io.timelimit.android.data.IdGenerator
|
||||
import io.timelimit.android.data.JsonSerializable
|
||||
import io.timelimit.android.data.customtypes.ImmutableBitmaskAdapter
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Entity(tableName = "time_limit_rule")
|
||||
@TypeConverters(ImmutableBitmaskAdapter::class)
|
||||
@Parcelize
|
||||
data class TimeLimitRule(
|
||||
@PrimaryKey
|
||||
@ColumnInfo(name = "id")
|
||||
val id: String,
|
||||
@ColumnInfo(name = "category_id")
|
||||
val categoryId: String,
|
||||
@ColumnInfo(name = "apply_to_extra_time_usage")
|
||||
val applyToExtraTimeUsage: Boolean,
|
||||
@ColumnInfo(name = "day_mask")
|
||||
val dayMask: Byte,
|
||||
@ColumnInfo(name = "max_time")
|
||||
val maximumTimeInMillis: Int
|
||||
): Parcelable, JsonSerializable {
|
||||
companion object {
|
||||
private const val RULE_ID = "ruleId"
|
||||
private const val CATEGORY_ID = "categoryId"
|
||||
private const val MAX_TIME_IN_MILLIS = "time"
|
||||
private const val DAY_MASK = "days"
|
||||
private const val APPLY_TO_EXTRA_TIME_USAGE = "extraTime"
|
||||
|
||||
fun parse(reader: JsonReader): TimeLimitRule {
|
||||
var id: String? = null
|
||||
var categoryId: String? = null
|
||||
var applyToExtraTimeUsage: Boolean? = null
|
||||
var dayMask: Byte? = null
|
||||
var maximumTimeInMillis: Int? = null
|
||||
|
||||
reader.beginObject()
|
||||
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
RULE_ID -> id = reader.nextString()
|
||||
CATEGORY_ID -> categoryId = reader.nextString()
|
||||
MAX_TIME_IN_MILLIS -> maximumTimeInMillis = reader.nextInt()
|
||||
DAY_MASK -> dayMask = reader.nextInt().toByte()
|
||||
APPLY_TO_EXTRA_TIME_USAGE -> applyToExtraTimeUsage = reader.nextBoolean()
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
|
||||
reader.endObject()
|
||||
|
||||
return TimeLimitRule(
|
||||
id = id!!,
|
||||
categoryId = categoryId!!,
|
||||
applyToExtraTimeUsage = applyToExtraTimeUsage!!,
|
||||
dayMask = dayMask!!,
|
||||
maximumTimeInMillis = maximumTimeInMillis!!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
IdGenerator.assertIdValid(id)
|
||||
IdGenerator.assertIdValid(categoryId)
|
||||
|
||||
if (maximumTimeInMillis < 0) {
|
||||
throw IllegalArgumentException("maximumTimeInMillis $maximumTimeInMillis < 0")
|
||||
}
|
||||
|
||||
if (dayMask < 0 || dayMask > (1 or 2 or 4 or 8 or 16 or 32 or 64)) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
|
||||
override fun serialize(writer: JsonWriter) {
|
||||
writer.beginObject()
|
||||
|
||||
writer.name(RULE_ID).value(id)
|
||||
writer.name(CATEGORY_ID).value(categoryId)
|
||||
writer.name(MAX_TIME_IN_MILLIS).value(maximumTimeInMillis)
|
||||
writer.name(DAY_MASK).value(dayMask)
|
||||
writer.name(APPLY_TO_EXTRA_TIME_USAGE).value(applyToExtraTimeUsage)
|
||||
|
||||
writer.endObject()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.data.model
|
||||
|
||||
import android.util.JsonReader
|
||||
import android.util.JsonWriter
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import io.timelimit.android.data.IdGenerator
|
||||
import io.timelimit.android.data.JsonSerializable
|
||||
|
||||
@Entity(primaryKeys = ["category_id", "day_of_epoch"], tableName = "used_time")
|
||||
data class UsedTimeItem(
|
||||
@ColumnInfo(name = "day_of_epoch")
|
||||
val dayOfEpoch: Int,
|
||||
@ColumnInfo(name = "used_time")
|
||||
val usedMillis: Long,
|
||||
@ColumnInfo(name = "category_id")
|
||||
val categoryId: String
|
||||
): JsonSerializable {
|
||||
companion object {
|
||||
private const val DAY_OF_EPOCH = "day"
|
||||
private const val USED_TIME_MILLIS = "time"
|
||||
private const val CATEGORY_ID = "category"
|
||||
|
||||
fun parse(reader: JsonReader): UsedTimeItem {
|
||||
reader.beginObject()
|
||||
|
||||
var dayOfEpoch: Int? = null
|
||||
var usedMillis: Long? = null
|
||||
var categoryId: String? = null
|
||||
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
DAY_OF_EPOCH -> dayOfEpoch = reader.nextInt()
|
||||
USED_TIME_MILLIS -> usedMillis = reader.nextLong()
|
||||
CATEGORY_ID -> categoryId = reader.nextString()
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
|
||||
reader.endObject()
|
||||
|
||||
return UsedTimeItem(
|
||||
dayOfEpoch = dayOfEpoch!!,
|
||||
usedMillis = usedMillis!!,
|
||||
categoryId = categoryId!!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
IdGenerator.assertIdValid(categoryId)
|
||||
|
||||
if (dayOfEpoch < 0) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
if (usedMillis < 0) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
|
||||
override fun serialize(writer: JsonWriter) {
|
||||
writer.beginObject()
|
||||
|
||||
writer.name(DAY_OF_EPOCH).value(dayOfEpoch)
|
||||
writer.name(USED_TIME_MILLIS).value(usedMillis)
|
||||
writer.name(CATEGORY_ID).value(categoryId)
|
||||
|
||||
writer.endObject()
|
||||
}
|
||||
}
|
195
app/src/main/java/io/timelimit/android/data/model/User.kt
Normal file
195
app/src/main/java/io/timelimit/android/data/model/User.kt
Normal file
|
@ -0,0 +1,195 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.data.model
|
||||
|
||||
import android.util.JsonReader
|
||||
import android.util.JsonWriter
|
||||
import androidx.room.*
|
||||
import io.timelimit.android.data.IdGenerator
|
||||
import io.timelimit.android.data.JsonSerializable
|
||||
import io.timelimit.android.util.parseJsonArray
|
||||
|
||||
@Entity(tableName = "user")
|
||||
@TypeConverters(UserTypeConverter::class)
|
||||
data class User(
|
||||
@PrimaryKey
|
||||
@ColumnInfo(name = "id")
|
||||
val id: String,
|
||||
@ColumnInfo(name = "name")
|
||||
val name: String,
|
||||
@ColumnInfo(name = "password")
|
||||
val password: String, // protected using bcrypt, can be empty if not configured
|
||||
@ColumnInfo(name = "second_password_salt")
|
||||
val secondPasswordSalt: String,
|
||||
@ColumnInfo(name = "type")
|
||||
val type: UserType,
|
||||
@ColumnInfo(name = "timezone")
|
||||
val timeZone: String,
|
||||
// 0 = time limits enabled
|
||||
@ColumnInfo(name = "disable_limits_until")
|
||||
val disableLimitsUntil: Long,
|
||||
@ColumnInfo(name = "mail")
|
||||
val mail: String,
|
||||
// empty = unset; can contain an invalid device id or the id of an device which is not used by this user
|
||||
// in this case, it should be treated like unset
|
||||
@ColumnInfo(name = "current_device")
|
||||
val currentDevice: String,
|
||||
@ColumnInfo(name = "category_for_not_assigned_apps")
|
||||
// empty or invalid = no category
|
||||
val categoryForNotAssignedApps: String,
|
||||
@ColumnInfo(name = "relax_primary_device")
|
||||
val relaxPrimaryDevice: Boolean,
|
||||
@ColumnInfo(name = "mail_notification_flags")
|
||||
val mailNotificationFlags: Int
|
||||
): JsonSerializable {
|
||||
companion object {
|
||||
private const val ID = "id"
|
||||
private const val NAME = "name"
|
||||
private const val PASSWORD = "password"
|
||||
private const val SECOND_PASSWORD_SALT = "secondPasswordSalt"
|
||||
private const val TYPE = "type"
|
||||
private const val TIMEZONE = "timeZone"
|
||||
private const val DISABLE_LIMITS_UNTIL = "disableLimitsUntil"
|
||||
private const val MAIL = "mail"
|
||||
private const val CURRENT_DEVICE = "currentDevice"
|
||||
private const val CATEGORY_FOR_NOT_ASSIGNED_APPS = "categoryForNotAssignedApps"
|
||||
private const val RELAX_PRIMARY_DEVICE = "relaxPrimaryDevice"
|
||||
private const val MAIL_NOTIFICATION_FLAGS = "mailNotificationFlags"
|
||||
|
||||
fun parse(reader: JsonReader): User {
|
||||
var id: String? = null
|
||||
var name: String? = null
|
||||
var password: String? = null
|
||||
var secondPasswordSalt: String? = null
|
||||
var type: UserType? = null
|
||||
var timeZone: String? = null
|
||||
var disableLimitsUntil: Long? = null
|
||||
var mail: String? = null
|
||||
var currentDevice: String? = null
|
||||
var categoryForNotAssignedApps = ""
|
||||
var relaxPrimaryDevice = false
|
||||
var mailNotificationFlags = 0
|
||||
|
||||
reader.beginObject()
|
||||
while (reader.hasNext()) {
|
||||
when(reader.nextName()) {
|
||||
ID -> id = reader.nextString()
|
||||
NAME -> name = reader.nextString()
|
||||
PASSWORD -> password = reader.nextString()
|
||||
SECOND_PASSWORD_SALT -> secondPasswordSalt = reader.nextString()
|
||||
TYPE -> type = UserTypeJson.parse(reader.nextString())
|
||||
TIMEZONE -> timeZone = reader.nextString()
|
||||
DISABLE_LIMITS_UNTIL -> disableLimitsUntil = reader.nextLong()
|
||||
MAIL -> mail = reader.nextString()
|
||||
CURRENT_DEVICE -> currentDevice = reader.nextString()
|
||||
CATEGORY_FOR_NOT_ASSIGNED_APPS -> categoryForNotAssignedApps = reader.nextString()
|
||||
RELAX_PRIMARY_DEVICE -> relaxPrimaryDevice = reader.nextBoolean()
|
||||
MAIL_NOTIFICATION_FLAGS -> mailNotificationFlags = reader.nextInt()
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
reader.endObject()
|
||||
|
||||
return User(
|
||||
id = id!!,
|
||||
name = name!!,
|
||||
password = password!!,
|
||||
secondPasswordSalt = secondPasswordSalt!!,
|
||||
type = type!!,
|
||||
timeZone = timeZone!!,
|
||||
disableLimitsUntil = disableLimitsUntil!!,
|
||||
mail = mail!!,
|
||||
currentDevice = currentDevice!!,
|
||||
categoryForNotAssignedApps = categoryForNotAssignedApps,
|
||||
relaxPrimaryDevice = relaxPrimaryDevice,
|
||||
mailNotificationFlags = mailNotificationFlags
|
||||
)
|
||||
}
|
||||
|
||||
fun parseList(reader: JsonReader) = parseJsonArray(reader) { parse(reader) }
|
||||
}
|
||||
|
||||
init {
|
||||
IdGenerator.assertIdValid(id)
|
||||
|
||||
if (disableLimitsUntil < 0) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
if (name.isEmpty()) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
if (timeZone.isEmpty()) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
if (currentDevice.isNotEmpty()) {
|
||||
IdGenerator.assertIdValid(currentDevice)
|
||||
}
|
||||
|
||||
if (categoryForNotAssignedApps.isNotEmpty()) {
|
||||
IdGenerator.assertIdValid(categoryForNotAssignedApps)
|
||||
}
|
||||
}
|
||||
|
||||
override fun serialize(writer: JsonWriter) {
|
||||
writer.beginObject()
|
||||
|
||||
writer.name(ID).value(id)
|
||||
writer.name(NAME).value(name)
|
||||
writer.name(PASSWORD).value(password)
|
||||
writer.name(SECOND_PASSWORD_SALT).value(secondPasswordSalt)
|
||||
writer.name(TYPE).value(UserTypeJson.serialize(type))
|
||||
writer.name(TIMEZONE).value(timeZone)
|
||||
writer.name(DISABLE_LIMITS_UNTIL).value(disableLimitsUntil)
|
||||
writer.name(MAIL).value(mail)
|
||||
writer.name(CURRENT_DEVICE).value(currentDevice)
|
||||
writer.name(CATEGORY_FOR_NOT_ASSIGNED_APPS).value(categoryForNotAssignedApps)
|
||||
writer.name(RELAX_PRIMARY_DEVICE).value(relaxPrimaryDevice)
|
||||
writer.name(MAIL_NOTIFICATION_FLAGS).value(mailNotificationFlags)
|
||||
|
||||
writer.endObject()
|
||||
}
|
||||
}
|
||||
|
||||
enum class UserType {
|
||||
Parent, Child
|
||||
}
|
||||
|
||||
object UserTypeJson {
|
||||
private const val PARENT = "parent"
|
||||
private const val CHILD = "child"
|
||||
|
||||
fun parse(value: String) = when(value) {
|
||||
PARENT -> UserType.Parent
|
||||
CHILD -> UserType.Child
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
fun serialize(value: UserType) = when(value) {
|
||||
UserType.Parent -> PARENT
|
||||
UserType.Child -> CHILD
|
||||
}
|
||||
}
|
||||
|
||||
class UserTypeConverter {
|
||||
@TypeConverter
|
||||
fun toUserType(value: String) = UserTypeJson.parse(value)
|
||||
|
||||
@TypeConverter
|
||||
fun toString(value: UserType) = UserTypeJson.serialize(value)
|
||||
}
|
38
app/src/main/java/io/timelimit/android/date/CalendarCache.kt
Normal file
38
app/src/main/java/io/timelimit/android/date/CalendarCache.kt
Normal file
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.date
|
||||
|
||||
import java.util.*
|
||||
|
||||
object CalendarCache {
|
||||
private val cache = Collections.synchronizedMap(HashMap<Long, Calendar>())
|
||||
|
||||
fun getCalendar(): Calendar {
|
||||
val threadId = Thread.currentThread().id
|
||||
|
||||
val item = cache[threadId]
|
||||
|
||||
if (item != null) {
|
||||
return item
|
||||
} else {
|
||||
val newItem = GregorianCalendar()
|
||||
|
||||
cache[threadId] = newItem
|
||||
|
||||
return newItem
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.date
|
||||
|
||||
import org.threeten.bp.LocalDate
|
||||
import org.threeten.bp.temporal.ChronoUnit
|
||||
import java.util.*
|
||||
|
||||
data class DateInTimezone(val dayOfWeek: Int, val dayOfEpoch: Int) {
|
||||
companion object {
|
||||
fun convertDayOfWeek(dayOfWeek: Int) = when(dayOfWeek) {
|
||||
Calendar.MONDAY -> 0
|
||||
Calendar.TUESDAY -> 1
|
||||
Calendar.WEDNESDAY -> 2
|
||||
Calendar.THURSDAY -> 3
|
||||
Calendar.FRIDAY -> 4
|
||||
Calendar.SATURDAY -> 5
|
||||
Calendar.SUNDAY -> 6
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
|
||||
fun newInstance(timeInMillis: Long, timeZone: TimeZone): DateInTimezone {
|
||||
val calendar = CalendarCache.getCalendar()
|
||||
|
||||
calendar.firstDayOfWeek = Calendar.MONDAY
|
||||
|
||||
calendar.timeZone = timeZone
|
||||
calendar.timeInMillis = timeInMillis
|
||||
|
||||
val dayOfWeek = convertDayOfWeek(calendar.get(Calendar.DAY_OF_WEEK))
|
||||
|
||||
val localDate = 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)
|
||||
}
|
||||
}
|
||||
}
|
22
app/src/main/java/io/timelimit/android/date/DayOfWeek.kt
Normal file
22
app/src/main/java/io/timelimit/android/date/DayOfWeek.kt
Normal file
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.date
|
||||
|
||||
import java.util.*
|
||||
|
||||
fun getDayOfWeek(calendar: Calendar): Int {
|
||||
return DateInTimezone.convertDayOfWeek(calendar.get(Calendar.DAY_OF_WEEK))
|
||||
}
|
33
app/src/main/java/io/timelimit/android/date/MinuteOfWeek.kt
Normal file
33
app/src/main/java/io/timelimit/android/date/MinuteOfWeek.kt
Normal file
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.date
|
||||
|
||||
import java.util.*
|
||||
|
||||
fun getMinuteOfWeek(timeInMillis: Long, timeZone: TimeZone): Int {
|
||||
val calendar = CalendarCache.getCalendar()
|
||||
|
||||
calendar.firstDayOfWeek = Calendar.MONDAY
|
||||
|
||||
calendar.timeZone = timeZone
|
||||
calendar.timeInMillis = timeInMillis
|
||||
|
||||
val dayOfWeek = getDayOfWeek(calendar)
|
||||
val hourOfDay = calendar.get(Calendar.HOUR_OF_DAY)
|
||||
val minuteOfHour = calendar.get(Calendar.MINUTE)
|
||||
|
||||
return minuteOfHour + 60 * (hourOfDay + 24 * dayOfWeek)
|
||||
}
|
120
app/src/main/java/io/timelimit/android/extensions/Checkout.kt
Normal file
120
app/src/main/java/io/timelimit/android/extensions/Checkout.kt
Normal file
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.extensions
|
||||
|
||||
import org.solovyev.android.checkout.BillingRequests
|
||||
import org.solovyev.android.checkout.Checkout
|
||||
import org.solovyev.android.checkout.RequestListener
|
||||
import org.solovyev.android.checkout.Skus
|
||||
import java.io.Closeable
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
suspend fun Checkout.startAsync(): CheckoutStartResponse {
|
||||
val checkout = this
|
||||
var resumed = false
|
||||
|
||||
return suspendCoroutine<CheckoutStartResponse> {
|
||||
continuation ->
|
||||
|
||||
checkout.start(object: Checkout.EmptyListener() {
|
||||
override fun onReady(requests: BillingRequests, product: String, billingSupported: Boolean) {
|
||||
if (!resumed) {
|
||||
resumed = true
|
||||
|
||||
continuation.resume(CheckoutStartResponse(
|
||||
requests = requests,
|
||||
product = product,
|
||||
billingSupported = billingSupported,
|
||||
checkout = checkout
|
||||
))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun Checkout.waitUntilReady(): CheckoutStartResponse {
|
||||
val checkout = this
|
||||
var resumed = false
|
||||
|
||||
return suspendCoroutine {
|
||||
continuation ->
|
||||
|
||||
checkout.whenReady(object: Checkout.EmptyListener() {
|
||||
override fun onReady(requests: BillingRequests, product: String, billingSupported: Boolean) {
|
||||
if (!resumed) {
|
||||
resumed = true
|
||||
|
||||
continuation.resume(CheckoutStartResponse(
|
||||
requests = requests,
|
||||
product = product,
|
||||
billingSupported = billingSupported,
|
||||
checkout = checkout
|
||||
))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun BillingRequests.getSkusAsync(product: String, skus: List<String>): Skus {
|
||||
val requests = this
|
||||
|
||||
return suspendCoroutine {
|
||||
continuation ->
|
||||
|
||||
requests.getSkus(product, skus, object: RequestListener<Skus> {
|
||||
override fun onError(response: Int, e: Exception) {
|
||||
continuation.resumeWithException(e)
|
||||
}
|
||||
|
||||
override fun onSuccess(result: Skus) {
|
||||
continuation.resume(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun BillingRequests.consumeAsync(token: String) {
|
||||
val requests = this
|
||||
|
||||
suspendCoroutine<Any> {
|
||||
continuation ->
|
||||
|
||||
requests.consume(token, object: RequestListener<Any> {
|
||||
override fun onError(response: Int, e: java.lang.Exception) {
|
||||
continuation.resumeWithException(e)
|
||||
}
|
||||
|
||||
override fun onSuccess(result: Any) {
|
||||
continuation.resume(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class CheckoutStartResponse (
|
||||
val requests: BillingRequests,
|
||||
val product: String,
|
||||
val billingSupported: Boolean,
|
||||
private val checkout: Checkout
|
||||
): Closeable {
|
||||
override fun close() {
|
||||
checkout.stop()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.extensions
|
||||
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
|
||||
fun DialogFragment.showSafe(fragmentManager: FragmentManager, tag: String) {
|
||||
if (!fragmentManager.isStateSaved) {
|
||||
show(fragmentManager, tag)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.extensions
|
||||
|
||||
import android.view.KeyEvent
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.EditText
|
||||
|
||||
// TODO: use this everywhere
|
||||
fun EditText.setOnEnterListenr(listener: () -> Unit) {
|
||||
this.setOnEditorActionListener { _, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_GO) {
|
||||
listener()
|
||||
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
this.setOnKeyListener { _, keyCode, keyEvent ->
|
||||
if (keyEvent.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) {
|
||||
listener()
|
||||
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.extensions
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavDirections
|
||||
|
||||
fun NavController.safeNavigate(directions: NavDirections, currentScreen: Int) {
|
||||
if (this.currentDestination?.id == currentScreen) {
|
||||
navigate(directions)
|
||||
}
|
||||
}
|
24
app/src/main/java/io/timelimit/android/extensions/Set.kt
Normal file
24
app/src/main/java/io/timelimit/android/extensions/Set.kt
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.extensions
|
||||
|
||||
fun <T> MutableSet<T>.toggle(item: T) {
|
||||
synchronized(this) {
|
||||
if (!this.remove(item)) {
|
||||
this.add(item)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.extensions
|
||||
|
||||
import java.util.*
|
||||
|
||||
fun TimeZone.readableName() = "${this.displayName} (${this.id})"
|
|
@ -0,0 +1,174 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.integration.platform
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Parcelable
|
||||
import androidx.room.TypeConverter
|
||||
import io.timelimit.android.data.model.App
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
abstract class PlatformIntegration(
|
||||
val maximumProtectionLevel: ProtectionLevel
|
||||
) {
|
||||
abstract fun getLocalApps(deviceId: String): Collection<App>
|
||||
abstract fun getLocalAppTitle(packageName: String): String?
|
||||
abstract fun getAppIcon(packageName: String): Drawable?
|
||||
abstract fun getCurrentProtectionLevel(): ProtectionLevel
|
||||
abstract fun getForegroundAppPermissionStatus(): RuntimePermissionStatus
|
||||
abstract fun getDrawOverOtherAppsPermissionStatus(): RuntimePermissionStatus
|
||||
abstract fun getNotificationAccessPermissionStatus(): NewPermissionStatus
|
||||
abstract fun disableDeviceAdmin()
|
||||
abstract fun trySetLockScreenPassword(password: String): Boolean
|
||||
// this must have a fallback if the permission is not granted
|
||||
abstract fun showOverlayMessage(text: String)
|
||||
|
||||
abstract fun showAppLockScreen(currentPackageName: String)
|
||||
// this should throw an SecurityException if the permission is missing
|
||||
abstract suspend fun getForegroundAppPackageName(): String?
|
||||
abstract fun setAppStatusMessage(message: AppStatusMessage?)
|
||||
abstract fun isScreenOn(): Boolean
|
||||
abstract fun setShowNotificationToRevokeTemporarilyAllowedApps(show: Boolean)
|
||||
abstract fun showRemoteResetNotification()
|
||||
// returns package names for which it was set
|
||||
abstract fun setSuspendedApps(packageNames: List<String>, suspend: Boolean): List<String>
|
||||
abstract fun stopSuspendingForAllApps()
|
||||
|
||||
// returns true on success
|
||||
abstract fun setEnableSystemLockdown(enableLockdown: Boolean): Boolean
|
||||
// returns true on success
|
||||
abstract fun setLockTaskPackages(packageNames: List<String>): Boolean
|
||||
|
||||
var installedAppsChangeListener: Runnable? = null
|
||||
}
|
||||
|
||||
enum class ProtectionLevel {
|
||||
None, SimpleDeviceAdmin, PasswordDeviceAdmin, DeviceOwner
|
||||
}
|
||||
|
||||
object ProtectionLevelUtil {
|
||||
private const val NONE = "none"
|
||||
private const val SIMPLE_DEVICE_ADMIN = "simple device admin"
|
||||
private const val PASSWORD_DEVICE_ADMIN = "password device admin"
|
||||
private const val DEVICE_OWNER = "device owner"
|
||||
|
||||
fun serialize(level: ProtectionLevel) = when(level) {
|
||||
ProtectionLevel.None -> NONE
|
||||
ProtectionLevel.SimpleDeviceAdmin -> SIMPLE_DEVICE_ADMIN
|
||||
ProtectionLevel.PasswordDeviceAdmin -> PASSWORD_DEVICE_ADMIN
|
||||
ProtectionLevel.DeviceOwner -> DEVICE_OWNER
|
||||
}
|
||||
|
||||
fun parse(level: String) = when(level) {
|
||||
NONE -> ProtectionLevel.None
|
||||
SIMPLE_DEVICE_ADMIN -> ProtectionLevel.SimpleDeviceAdmin
|
||||
PASSWORD_DEVICE_ADMIN -> ProtectionLevel.PasswordDeviceAdmin
|
||||
DEVICE_OWNER -> ProtectionLevel.DeviceOwner
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
fun toInt(level: ProtectionLevel) = when(level) {
|
||||
ProtectionLevel.None -> 0
|
||||
ProtectionLevel.SimpleDeviceAdmin -> 1
|
||||
ProtectionLevel.PasswordDeviceAdmin -> 2
|
||||
ProtectionLevel.DeviceOwner -> 3
|
||||
}
|
||||
}
|
||||
|
||||
class ProtectionLevelConverter {
|
||||
@TypeConverter
|
||||
fun fromString(value: String) = ProtectionLevelUtil.parse(value)
|
||||
|
||||
@TypeConverter
|
||||
fun toString(value: ProtectionLevel) = ProtectionLevelUtil.serialize(value)
|
||||
}
|
||||
|
||||
enum class RuntimePermissionStatus {
|
||||
NotRequired, Granted, NotGranted
|
||||
}
|
||||
|
||||
object RuntimePermissionStatusUtil {
|
||||
private const val NOT_REQUIRED = "not required"
|
||||
private const val GRANTED = "granted"
|
||||
private const val NOT_GRANTED = "not granted"
|
||||
|
||||
fun serialize(value: RuntimePermissionStatus) = when(value) {
|
||||
RuntimePermissionStatus.NotRequired -> NOT_REQUIRED
|
||||
RuntimePermissionStatus.Granted -> GRANTED
|
||||
RuntimePermissionStatus.NotGranted -> NOT_GRANTED
|
||||
}
|
||||
|
||||
fun parse(value: String) = when(value) {
|
||||
NOT_REQUIRED -> RuntimePermissionStatus.NotRequired
|
||||
GRANTED -> RuntimePermissionStatus.Granted
|
||||
NOT_GRANTED -> RuntimePermissionStatus.NotGranted
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
fun toInt(value: RuntimePermissionStatus) = when(value) {
|
||||
RuntimePermissionStatus.NotGranted -> 0
|
||||
RuntimePermissionStatus.NotRequired -> 1
|
||||
RuntimePermissionStatus.Granted -> 2
|
||||
}
|
||||
}
|
||||
|
||||
class RuntimePermissionStatusConverter {
|
||||
@TypeConverter
|
||||
fun fromString(value: String) = RuntimePermissionStatusUtil.parse(value)
|
||||
|
||||
@TypeConverter
|
||||
fun toString(value: RuntimePermissionStatus) = RuntimePermissionStatusUtil.serialize(value)
|
||||
}
|
||||
|
||||
enum class NewPermissionStatus {
|
||||
NotSupported, Granted, NotGranted
|
||||
}
|
||||
|
||||
object NewPermissionStatusUtil {
|
||||
private const val NOT_SUPPORTED = "not supported"
|
||||
private const val GRANTED = "granted"
|
||||
private const val NOT_GRANTED = "not granted"
|
||||
|
||||
fun serialize(value: NewPermissionStatus) = when(value) {
|
||||
NewPermissionStatus.NotSupported -> NOT_SUPPORTED
|
||||
NewPermissionStatus.Granted -> GRANTED
|
||||
NewPermissionStatus.NotGranted -> NOT_GRANTED
|
||||
}
|
||||
|
||||
fun parse(value: String) = when(value) {
|
||||
NOT_SUPPORTED -> NewPermissionStatus.NotSupported
|
||||
GRANTED -> NewPermissionStatus.Granted
|
||||
NOT_GRANTED -> NewPermissionStatus.NotGranted
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
fun toInt(value: NewPermissionStatus) = when(value) {
|
||||
NewPermissionStatus.NotGranted -> 0
|
||||
NewPermissionStatus.NotSupported -> 1
|
||||
NewPermissionStatus.Granted -> 2
|
||||
}
|
||||
}
|
||||
|
||||
class NewPermissionStatusConverter {
|
||||
@TypeConverter
|
||||
fun fromString(value: String) = NewPermissionStatusUtil.parse(value)
|
||||
|
||||
@TypeConverter
|
||||
fun toString(value: NewPermissionStatus) = NewPermissionStatusUtil.serialize(value)
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class AppStatusMessage(val title: String, val text: String): Parcelable
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.integration.platform.android
|
||||
|
||||
import android.app.admin.DeviceAdminReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.UserHandle
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.coroutines.runAsync
|
||||
import io.timelimit.android.livedata.waitForNullableValue
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
import io.timelimit.android.sync.actions.TriedDisablingDeviceAdminAction
|
||||
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
||||
|
||||
class AdminReceiver: DeviceAdminReceiver() {
|
||||
override fun onEnabled(context: Context, intent: Intent?) {
|
||||
super.onEnabled(context, intent)
|
||||
|
||||
DefaultAppLogic.with(context).backgroundTaskLogic.syncDeviceStatusAsync()
|
||||
}
|
||||
|
||||
override fun onDisabled(context: Context, intent: Intent?) {
|
||||
super.onDisabled(context, intent)
|
||||
|
||||
DefaultAppLogic.with(context).backgroundTaskLogic.syncDeviceStatusAsync()
|
||||
}
|
||||
|
||||
override fun onDisableRequested(context: Context, intent: Intent?): CharSequence {
|
||||
runAsync {
|
||||
val logic = DefaultAppLogic.with(context)
|
||||
|
||||
if (logic.database.config().getOwnDeviceId().waitForNullableValue() != null) {
|
||||
ApplyActionUtil.applyAppLogicAction(
|
||||
TriedDisablingDeviceAdminAction,
|
||||
logic
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return context.getString(R.string.admin_disable_warning)
|
||||
}
|
||||
|
||||
override fun onPasswordSucceeded(context: Context, intent: Intent?) {
|
||||
super.onPasswordSucceeded(context, intent)
|
||||
|
||||
DefaultAppLogic.with(context).manipulationLogic.reportManualUnlock()
|
||||
}
|
||||
|
||||
override fun onPasswordSucceeded(context: Context, intent: Intent?, user: UserHandle?) {
|
||||
super.onPasswordSucceeded(context, intent, user)
|
||||
|
||||
DefaultAppLogic.with(context).manipulationLogic.reportManualUnlock()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.integration.platform.android
|
||||
|
||||
import android.app.admin.DevicePolicyManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.integration.platform.ProtectionLevel
|
||||
|
||||
object AdminStatus {
|
||||
fun getAdminStatus(context: Context, policyManager: DevicePolicyManager): ProtectionLevel {
|
||||
val component = ComponentName(context, AdminReceiver::class.java)
|
||||
|
||||
return if (BuildConfig.storeCompilant) {
|
||||
if (policyManager.isAdminActive(component)) {
|
||||
ProtectionLevel.SimpleDeviceAdmin
|
||||
} else {
|
||||
ProtectionLevel.None
|
||||
}
|
||||
} else {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
if (policyManager.isDeviceOwnerApp(context.packageName)) {
|
||||
ProtectionLevel.DeviceOwner
|
||||
} else if (policyManager.isAdminActive(component)) {
|
||||
ProtectionLevel.SimpleDeviceAdmin
|
||||
} else {
|
||||
ProtectionLevel.None
|
||||
}
|
||||
} else /* if below Lollipop */ {
|
||||
if (policyManager.isAdminActive(component)) {
|
||||
ProtectionLevel.PasswordDeviceAdmin
|
||||
} else {
|
||||
ProtectionLevel.None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,338 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.integration.platform.android
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.app.ActivityManager
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.admin.DevicePolicyManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import android.os.UserManager
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.data.model.App
|
||||
import io.timelimit.android.integration.platform.*
|
||||
import io.timelimit.android.integration.platform.android.foregroundapp.ForegroundAppHelper
|
||||
import io.timelimit.android.ui.lock.LockActivity
|
||||
|
||||
|
||||
class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectionLevel) {
|
||||
companion object {
|
||||
private const val LOG_TAG = "AndroidIntegration"
|
||||
|
||||
val maximumProtectionLevel: ProtectionLevel
|
||||
|
||||
init {
|
||||
if (BuildConfig.storeCompilant) {
|
||||
maximumProtectionLevel = ProtectionLevel.SimpleDeviceAdmin
|
||||
} else {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
maximumProtectionLevel = ProtectionLevel.DeviceOwner
|
||||
} else {
|
||||
maximumProtectionLevel = ProtectionLevel.PasswordDeviceAdmin
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val context = context.applicationContext
|
||||
private val policyManager = this.context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
|
||||
private val foregroundAppHelper = ForegroundAppHelper.with(this.context)
|
||||
private val powerManager = this.context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
private val activityManager = this.context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
||||
private val notificationManager = this.context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
private val deviceAdmin = ComponentName(context.applicationContext, AdminReceiver::class.java)
|
||||
|
||||
init {
|
||||
AppsChangeListener.registerBroadcastReceiver(this.context, object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
installedAppsChangeListener?.run()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun getLocalApps(deviceId: String): Collection<App> {
|
||||
return AndroidIntegrationApps.getLocalApps(deviceId, context)
|
||||
}
|
||||
|
||||
override fun getLocalAppTitle(packageName: String): String? {
|
||||
return AndroidIntegrationApps.getAppTitle(packageName, context)
|
||||
}
|
||||
|
||||
override fun getAppIcon(packageName: String): Drawable? {
|
||||
return AndroidIntegrationApps.getAppIcon(packageName, context)
|
||||
}
|
||||
|
||||
override fun getCurrentProtectionLevel(): ProtectionLevel {
|
||||
return AdminStatus.getAdminStatus(context, policyManager)
|
||||
}
|
||||
|
||||
override suspend fun getForegroundAppPackageName(): String? {
|
||||
return foregroundAppHelper.getForegroundAppPackage()
|
||||
}
|
||||
|
||||
override fun getForegroundAppPermissionStatus(): RuntimePermissionStatus {
|
||||
return foregroundAppHelper.getPermissionStatus()
|
||||
}
|
||||
|
||||
override fun showOverlayMessage(text: String) {
|
||||
Toast.makeText(context, text, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun getDrawOverOtherAppsPermissionStatus(): RuntimePermissionStatus {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (Settings.canDrawOverlays(context)) {
|
||||
return RuntimePermissionStatus.Granted
|
||||
} else {
|
||||
return RuntimePermissionStatus.NotGranted
|
||||
}
|
||||
} else {
|
||||
return RuntimePermissionStatus.NotRequired
|
||||
}
|
||||
}
|
||||
|
||||
override fun getNotificationAccessPermissionStatus(): NewPermissionStatus {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
if (activityManager.isLowRamDevice) {
|
||||
return NewPermissionStatus.NotSupported
|
||||
} else if (NotificationManagerCompat.getEnabledListenerPackages(context).contains(context.packageName)) {
|
||||
return NewPermissionStatus.Granted
|
||||
} else {
|
||||
return NewPermissionStatus.NotGranted
|
||||
}
|
||||
} else {
|
||||
return NewPermissionStatus.NotSupported
|
||||
}
|
||||
}
|
||||
|
||||
override fun trySetLockScreenPassword(password: String): Boolean {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "set password")
|
||||
}
|
||||
|
||||
if (!BuildConfig.storeCompilant) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
try {
|
||||
if (password.isBlank()) {
|
||||
return policyManager.resetPassword("", 0)
|
||||
} else if (policyManager.resetPassword(password, DevicePolicyManager.RESET_PASSWORD_REQUIRE_ENTRY)) {
|
||||
policyManager.lockNow()
|
||||
|
||||
return true
|
||||
}
|
||||
} catch (ex: SecurityException) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.w(LOG_TAG, "error setting password", ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private var lastAppStatusMessage: AppStatusMessage? = null
|
||||
|
||||
override fun setAppStatusMessage(message: AppStatusMessage?) {
|
||||
if (lastAppStatusMessage != message) {
|
||||
lastAppStatusMessage = message
|
||||
|
||||
BackgroundService.setStatusMessage(message, context)
|
||||
}
|
||||
}
|
||||
|
||||
override fun showAppLockScreen(currentPackageName: String) {
|
||||
LockActivity.start(context, currentPackageName)
|
||||
}
|
||||
|
||||
override fun isScreenOn(): Boolean {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
|
||||
return powerManager.isInteractive
|
||||
} else {
|
||||
return powerManager.isScreenOn
|
||||
}
|
||||
}
|
||||
|
||||
override fun setShowNotificationToRevokeTemporarilyAllowedApps(show: Boolean) {
|
||||
if (show) {
|
||||
NotificationChannels.createAppStatusChannel(notificationManager, context)
|
||||
|
||||
val actionIntent = PendingIntent.getService(
|
||||
context,
|
||||
PendingIntentIds.REVOKE_TEMPORARILY_ALLOWED,
|
||||
BackgroundService.prepareRevokeTemporarilyAllowed(context),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
val notification = NotificationCompat.Builder(context, NotificationChannels.APP_STATUS)
|
||||
.setSmallIcon(R.drawable.ic_stat_check)
|
||||
.setContentTitle(context.getString(R.string.background_logic_temporarily_allowed_title))
|
||||
.setContentText(context.getString(R.string.background_logic_temporarily_allowed_text))
|
||||
.setContentIntent(actionIntent)
|
||||
.setWhen(0)
|
||||
.setShowWhen(false)
|
||||
.setSound(null)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setLocalOnly(true)
|
||||
.setAutoCancel(false)
|
||||
.setOngoing(true)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.build()
|
||||
|
||||
notificationManager.notify(NotificationIds.REVOKE_TEMPORARILY_ALLOWED_APPS, notification)
|
||||
} else {
|
||||
notificationManager.cancel(NotificationIds.REVOKE_TEMPORARILY_ALLOWED_APPS)
|
||||
}
|
||||
}
|
||||
|
||||
override fun showRemoteResetNotification() {
|
||||
NotificationChannels.createAppStatusChannel(notificationManager, context)
|
||||
|
||||
notificationManager.notify(
|
||||
NotificationIds.APP_RESET,
|
||||
NotificationCompat.Builder(context, NotificationChannels.APP_STATUS)
|
||||
.setSmallIcon(R.drawable.ic_stat_timelapse)
|
||||
.setContentTitle(context.getString(R.string.remote_reset_notification_title))
|
||||
.setContentText(context.getString(R.string.remote_reset_notification_text))
|
||||
.setWhen(System.currentTimeMillis())
|
||||
.setShowWhen(true)
|
||||
.setSound(null)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setLocalOnly(true)
|
||||
.setAutoCancel(false)
|
||||
.setOngoing(false)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
override fun disableDeviceAdmin() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
if (policyManager.isDeviceOwnerApp(context.packageName)) {
|
||||
setEnableSystemLockdown(false)
|
||||
policyManager.clearDeviceOwnerApp(context.packageName)
|
||||
}
|
||||
}
|
||||
|
||||
policyManager.removeActiveAdmin(deviceAdmin)
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.N)
|
||||
override fun setSuspendedApps(packageNames: List<String>, suspend: Boolean): List<String> {
|
||||
if (
|
||||
(getCurrentProtectionLevel() == ProtectionLevel.DeviceOwner) &&
|
||||
(!BuildConfig.storeCompilant) &&
|
||||
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
|
||||
) {
|
||||
val failedApps = policyManager.setPackagesSuspended(
|
||||
deviceAdmin,
|
||||
packageNames.toTypedArray(),
|
||||
suspend
|
||||
)
|
||||
|
||||
return packageNames.filterNot { failedApps.contains(it) }
|
||||
} else {
|
||||
return emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
override fun setEnableSystemLockdown(enableLockdown: Boolean): Boolean {
|
||||
return if (
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &&
|
||||
policyManager.isDeviceOwnerApp(context.packageName) &&
|
||||
(!BuildConfig.storeCompilant)
|
||||
) {
|
||||
if (enableLockdown) {
|
||||
// disable problematic features
|
||||
policyManager.addUserRestriction(deviceAdmin, UserManager.DISALLOW_ADD_USER)
|
||||
policyManager.addUserRestriction(deviceAdmin, UserManager.DISALLOW_FACTORY_RESET)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
policyManager.addUserRestriction(deviceAdmin, UserManager.DISALLOW_SAFE_BOOT)
|
||||
}
|
||||
} else /* disable lockdown */ {
|
||||
// enable problematic features
|
||||
policyManager.clearUserRestriction(deviceAdmin, UserManager.DISALLOW_ADD_USER)
|
||||
policyManager.clearUserRestriction(deviceAdmin, UserManager.DISALLOW_FACTORY_RESET)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
policyManager.clearUserRestriction(deviceAdmin, UserManager.DISALLOW_SAFE_BOOT)
|
||||
}
|
||||
|
||||
enableSystemApps()
|
||||
stopSuspendingForAllApps()
|
||||
}
|
||||
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun enableSystemApps() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
return
|
||||
}
|
||||
|
||||
// disabled system apps (all apps - enabled apps)
|
||||
val allApps = context.packageManager.getInstalledApplications(
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1)
|
||||
PackageManager.GET_UNINSTALLED_PACKAGES
|
||||
else
|
||||
PackageManager.MATCH_UNINSTALLED_PACKAGES
|
||||
)
|
||||
val enabledAppsPackages = context.packageManager.getInstalledApplications(0).map { it.packageName }.toSet()
|
||||
|
||||
allApps
|
||||
.asSequence()
|
||||
.filterNot { enabledAppsPackages.contains(it.packageName) }
|
||||
.filter { it.flags and ApplicationInfo.FLAG_SYSTEM != 0 }
|
||||
.map { it.packageName }
|
||||
.forEach { policyManager.enableSystemApp(deviceAdmin, it) }
|
||||
}
|
||||
|
||||
override fun stopSuspendingForAllApps() {
|
||||
setSuspendedApps(context.packageManager.getInstalledApplications(0).map { it.packageName }, false)
|
||||
}
|
||||
|
||||
override fun setLockTaskPackages(packageNames: List<String>): Boolean {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
if (policyManager.isDeviceOwnerApp(context.packageName)) {
|
||||
policyManager.setLockTaskPackages(deviceAdmin, packageNames.toTypedArray())
|
||||
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,165 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.integration.platform.android
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.ResolveInfo
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Build
|
||||
import android.provider.ContactsContract
|
||||
import android.provider.Settings
|
||||
import android.provider.Telephony
|
||||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.data.model.App
|
||||
import io.timelimit.android.data.model.AppRecommendation
|
||||
|
||||
object AndroidIntegrationApps {
|
||||
private val mainIntent = Intent(Intent.ACTION_MAIN)
|
||||
.addCategory(Intent.CATEGORY_LAUNCHER)
|
||||
|
||||
private val launcherIntent = Intent(Intent.ACTION_MAIN)
|
||||
.addCategory(Intent.CATEGORY_DEFAULT)
|
||||
.addCategory(Intent.CATEGORY_HOME)
|
||||
|
||||
val ignoredApps = HashSet<String>()
|
||||
init {
|
||||
ignoredApps.add("com.android.systemui")
|
||||
ignoredApps.add("android")
|
||||
ignoredApps.add("com.android.packageinstaller")
|
||||
ignoredApps.add("com.google.android.packageinstaller")
|
||||
ignoredApps.add("com.android.bluetooth")
|
||||
ignoredApps.add("com.android.nfc")
|
||||
}
|
||||
|
||||
fun getLocalApps(deviceId: String, context: Context): Collection<App> {
|
||||
val packageManager = context.packageManager
|
||||
|
||||
val result = HashMap<String, App>()
|
||||
|
||||
// WHITELIST
|
||||
// add launcher
|
||||
add(map = result, resolveInfoList = packageManager.queryIntentActivities(launcherIntent, 0), deviceId = deviceId, recommendation = AppRecommendation.Whitelist, context = context)
|
||||
// add settings
|
||||
add(map = result, packageName = Intent(Settings.ACTION_SETTINGS).resolveActivity(packageManager)?.packageName, deviceId = deviceId, recommendation = AppRecommendation.Whitelist, context = context)
|
||||
// add dialer
|
||||
add(map = result, packageName = Intent(Intent.ACTION_DIAL).resolveActivity(packageManager)?.packageName, deviceId = deviceId, recommendation = AppRecommendation.Whitelist, context = context)
|
||||
// add SMS
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
val smsApp: String? = Telephony.Sms.getDefaultSmsPackage(context)
|
||||
|
||||
if (smsApp != null) {
|
||||
add(map = result, packageName = smsApp, deviceId = deviceId, recommendation = AppRecommendation.Whitelist, context = context)
|
||||
}
|
||||
} else {
|
||||
add(map = result, packageName = Intent(android.content.Intent.ACTION_VIEW).setType("vnd.android-dir/mms-sms").resolveActivity(packageManager)?.packageName, deviceId = deviceId, recommendation = AppRecommendation.Whitelist, context = context)
|
||||
}
|
||||
// add contacts
|
||||
add(map = result, packageName = Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI).resolveActivity(packageManager)?.packageName, deviceId = deviceId, recommendation = AppRecommendation.Whitelist, context = context)
|
||||
// add google play
|
||||
if (BuildConfig.storeCompilant) {
|
||||
add(map = result, packageName = "com.android.vending", deviceId = deviceId, recommendation = AppRecommendation.Whitelist, context = context)
|
||||
}
|
||||
// add all apps with launcher icon
|
||||
add(map = result, resolveInfoList = packageManager.queryIntentActivities(mainIntent, 0), deviceId = deviceId, recommendation = AppRecommendation.None, context = context)
|
||||
|
||||
val installedPackages = packageManager.getInstalledApplications(0)
|
||||
|
||||
for (applicationInfo in installedPackages) {
|
||||
val packageName = applicationInfo.packageName
|
||||
|
||||
if (!result.containsKey(packageName) && !ignoredApps.contains(packageName)) {
|
||||
result[packageName] = App(
|
||||
deviceId = deviceId,
|
||||
packageName = packageName,
|
||||
title = applicationInfo.loadLabel(packageManager).toString(),
|
||||
isLaunchable = false,
|
||||
recommendation = AppRecommendation.None
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return result.values
|
||||
}
|
||||
|
||||
private fun add(map: MutableMap<String, App>, resolveInfoList: List<ResolveInfo>, deviceId: String, recommendation: AppRecommendation, context: Context) {
|
||||
val packageManager = context.packageManager
|
||||
|
||||
for (info in resolveInfoList) {
|
||||
val packageName = info.activityInfo.applicationInfo.packageName
|
||||
if (ignoredApps.contains(packageName)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (!map.containsKey(packageName)) {
|
||||
map[packageName] = App(
|
||||
deviceId = deviceId,
|
||||
packageName = packageName,
|
||||
title = info.activityInfo.applicationInfo.loadLabel(packageManager).toString(),
|
||||
isLaunchable = true,
|
||||
recommendation = recommendation
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun add(map: MutableMap<String, App>, packageName: String?, deviceId: String, recommendation: AppRecommendation, context: Context) {
|
||||
val packageManager = context.packageManager
|
||||
|
||||
if (packageName == null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (ignoredApps.contains(packageName)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (map.containsKey(packageName)) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val packageInfo = context.packageManager.getApplicationInfo(packageName, 0)
|
||||
|
||||
map[packageName] = App(
|
||||
deviceId = deviceId,
|
||||
packageName = packageName,
|
||||
title = packageInfo.loadLabel(packageManager).toString(),
|
||||
isLaunchable = true,
|
||||
recommendation = recommendation
|
||||
)
|
||||
} catch (ex: PackageManager.NameNotFoundException) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
fun getAppTitle(packageName: String, context: Context): String? {
|
||||
try {
|
||||
return context.packageManager.getApplicationInfo(packageName, 0).loadLabel(context.packageManager).toString()
|
||||
} catch (ex: PackageManager.NameNotFoundException) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun getAppIcon(packageName: String, context: Context): Drawable? {
|
||||
try {
|
||||
return context.packageManager.getApplicationIcon(packageName)
|
||||
} catch (ex: PackageManager.NameNotFoundException) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.integration.platform.android
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
|
||||
object AppsChangeListener {
|
||||
private val changeFilter = IntentFilter()
|
||||
private val externalFilter = IntentFilter()
|
||||
|
||||
init {
|
||||
changeFilter.addAction(Intent.ACTION_PACKAGE_ADDED)
|
||||
changeFilter.addAction(Intent.ACTION_PACKAGE_CHANGED)
|
||||
changeFilter.addAction(Intent.ACTION_PACKAGE_REMOVED)
|
||||
changeFilter.addDataScheme("package")
|
||||
|
||||
externalFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE)
|
||||
externalFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE)
|
||||
}
|
||||
|
||||
fun registerBroadcastReceiver(context: Context, listener: BroadcastReceiver) {
|
||||
context.registerReceiver(listener, changeFilter)
|
||||
context.registerReceiver(listener, externalFilter)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.integration.platform.android
|
||||
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.coroutines.runAsync
|
||||
import io.timelimit.android.integration.platform.AppStatusMessage
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
import io.timelimit.android.ui.MainActivity
|
||||
|
||||
class BackgroundService: Service() {
|
||||
companion object {
|
||||
private const val ACTION = "a"
|
||||
private const val ACTION_SET_NOTIFICATION = "a"
|
||||
private const val ACTION_REVOKE_TEMPORARILY_ALLOWED_APPS = "b"
|
||||
private const val EXTRA_NOTIFICATION = "b"
|
||||
|
||||
fun setStatusMessage(status: AppStatusMessage?, context: Context) {
|
||||
val intent = Intent(context, BackgroundService::class.java)
|
||||
|
||||
if (status != null) {
|
||||
ContextCompat.startForegroundService(
|
||||
context,
|
||||
intent
|
||||
.putExtra(ACTION, ACTION_SET_NOTIFICATION)
|
||||
.putExtra(EXTRA_NOTIFICATION, status)
|
||||
)
|
||||
} else {
|
||||
context.stopService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
fun prepareRevokeTemporarilyAllowed(context: Context) = Intent(context, BackgroundService::class.java)
|
||||
.putExtra(ACTION, ACTION_REVOKE_TEMPORARILY_ALLOWED_APPS)
|
||||
}
|
||||
|
||||
private val notificationManager: NotificationManager by lazy {
|
||||
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
}
|
||||
|
||||
private var didPostNotification = false
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
// init the app logic if not yet done
|
||||
DefaultAppLogic.with(this)
|
||||
|
||||
// create the channel
|
||||
NotificationChannels.createAppStatusChannel(notificationManager, this)
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (intent != null) {
|
||||
val action = intent.getStringExtra(ACTION)
|
||||
|
||||
if (action == ACTION_SET_NOTIFICATION) {
|
||||
val appStatusMessage = intent.getParcelableExtra<AppStatusMessage>(EXTRA_NOTIFICATION)
|
||||
|
||||
val openAppIntent = PendingIntent.getActivity(
|
||||
this,
|
||||
PendingIntentIds.OPEN_MAIN_APP,
|
||||
Intent(this, MainActivity::class.java),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
val notification = NotificationCompat.Builder(this, NotificationChannels.APP_STATUS)
|
||||
.setSmallIcon(R.drawable.ic_stat_timelapse)
|
||||
.setContentTitle(appStatusMessage.title)
|
||||
.setContentText(appStatusMessage.text)
|
||||
.setContentIntent(openAppIntent)
|
||||
.setWhen(0)
|
||||
.setShowWhen(false)
|
||||
.setSound(null)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setLocalOnly(true)
|
||||
.setAutoCancel(false)
|
||||
.setOngoing(true)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.build()
|
||||
|
||||
if (didPostNotification) {
|
||||
notificationManager.notify(NotificationIds.APP_STATUS, notification)
|
||||
} else {
|
||||
startForeground(NotificationIds.APP_STATUS, notification)
|
||||
didPostNotification = true
|
||||
}
|
||||
} else if (action == ACTION_REVOKE_TEMPORARILY_ALLOWED_APPS) {
|
||||
runAsync {
|
||||
DefaultAppLogic.with(this@BackgroundService).backgroundTaskLogic.resetTemporarilyAllowedApps()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return START_REDELIVER_INTENT
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
stopForeground(true)
|
||||
didPostNotification = false
|
||||
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.integration.platform.android
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import io.timelimit.android.R
|
||||
|
||||
object NotificationIds {
|
||||
const val APP_STATUS = 1
|
||||
const val NOTIFICATION_BLOCKED = 2
|
||||
const val REVOKE_TEMPORARILY_ALLOWED_APPS = 3
|
||||
const val APP_RESET = 4
|
||||
}
|
||||
|
||||
object NotificationChannels {
|
||||
const val APP_STATUS = "app status"
|
||||
const val BLOCKED_NOTIFICATIONS_NOTIFICATION = "notification blocked notification"
|
||||
|
||||
fun createAppStatusChannel(notificationManager: NotificationManager, context: Context) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
notificationManager.createNotificationChannel(
|
||||
NotificationChannel(
|
||||
APP_STATUS,
|
||||
context.getString(R.string.notification_channel_app_status_title),
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
).apply {
|
||||
description = context.getString(R.string.notification_channel_app_status_description)
|
||||
enableLights(false)
|
||||
setSound(null, null)
|
||||
enableVibration(false)
|
||||
setShowBadge(false)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun createBlockedNotificationChannel(notificationManager: NotificationManager, context: Context) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
notificationManager.createNotificationChannel(
|
||||
NotificationChannel(
|
||||
NotificationChannels.BLOCKED_NOTIFICATIONS_NOTIFICATION,
|
||||
context.getString(R.string.notification_channel_blocked_notification_title),
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
).apply {
|
||||
description = context.getString(R.string.notification_channel_blocked_notification_text)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object PendingIntentIds {
|
||||
const val OPEN_MAIN_APP = 1
|
||||
const val REVOKE_TEMPORARILY_ALLOWED = 2
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.integration.platform.android
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.service.notification.NotificationListenerService
|
||||
import android.service.notification.StatusBarNotification
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.coroutines.runAsync
|
||||
import io.timelimit.android.livedata.waitForNonNullValue
|
||||
import io.timelimit.android.logic.*
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
class NotificationListener: NotificationListenerService() {
|
||||
companion object {
|
||||
private const val LOG_TAG = "NotificationListenerLog"
|
||||
}
|
||||
|
||||
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) }
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
NotificationChannels.createBlockedNotificationChannel(notificationManager, this)
|
||||
}
|
||||
|
||||
override fun onNotificationPosted(sbn: StatusBarNotification) {
|
||||
super.onNotificationPosted(sbn)
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, sbn.packageName)
|
||||
}
|
||||
|
||||
runAsync {
|
||||
val reason = shouldRemoveNotification(sbn)
|
||||
|
||||
if (reason != BlockingReason.None) {
|
||||
val success = try {
|
||||
cancelNotification(sbn.key)
|
||||
|
||||
true
|
||||
} catch (ex: SecurityException) {
|
||||
// this occurs when the notification access is revoked
|
||||
// while this function is running
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
notificationManager.notify(
|
||||
sbn.packageName,
|
||||
NotificationIds.NOTIFICATION_BLOCKED,
|
||||
NotificationCompat.Builder(this@NotificationListener, NotificationChannels.BLOCKED_NOTIFICATIONS_NOTIFICATION)
|
||||
.setDefaults(NotificationCompat.DEFAULT_ALL)
|
||||
.setSmallIcon(R.drawable.ic_stat_block)
|
||||
.setContentTitle(
|
||||
if (success)
|
||||
getString(R.string.notification_filter_not_blocked_title)
|
||||
else
|
||||
getString(R.string.notification_filter_blocking_failed_title)
|
||||
)
|
||||
.setContentText(
|
||||
queryAppTitleCache.query(sbn.packageName) +
|
||||
" - " +
|
||||
when (reason) {
|
||||
BlockingReason.NotPartOfAnCategory -> getString(R.string.lock_reason_short_no_category)
|
||||
BlockingReason.TemporarilyBlocked -> getString(R.string.lock_reason_short_temporarily_blocked)
|
||||
BlockingReason.TimeOver -> getString(R.string.lock_reason_short_time_over)
|
||||
BlockingReason.TimeOverExtraTimeCanBeUsedLater -> getString(R.string.lock_reason_short_time_over)
|
||||
BlockingReason.BlockedAtThisTime -> getString(R.string.lock_reason_short_blocked_time_area)
|
||||
BlockingReason.MissingNetworkTime -> getString(R.string.lock_reason_short_missing_network_time)
|
||||
BlockingReason.RequiresCurrentDevice -> getString(R.string.lock_reason_short_requires_current_device)
|
||||
BlockingReason.None -> throw IllegalStateException()
|
||||
}
|
||||
)
|
||||
.setLocalOnly(true)
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNotificationRemoved(sbn: StatusBarNotification) {
|
||||
super.onNotificationRemoved(sbn)
|
||||
|
||||
// not interesting but required for old android versions
|
||||
}
|
||||
|
||||
private suspend fun shouldRemoveNotification(sbn: StatusBarNotification): BlockingReason {
|
||||
if (sbn.packageName == packageName || sbn.isOngoing) {
|
||||
return BlockingReason.None
|
||||
}
|
||||
|
||||
val blockingReason = blockingReasonUtil.getBlockingReason(sbn.packageName).waitForNonNullValue()
|
||||
|
||||
if (blockingReason == BlockingReason.None) {
|
||||
return BlockingReason.None
|
||||
}
|
||||
|
||||
if (isSystemApp(sbn.packageName) && blockingReason == BlockingReason.NotPartOfAnCategory) {
|
||||
return BlockingReason.None
|
||||
}
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "blocking notification of ${sbn.packageName} because $blockingReason")
|
||||
}
|
||||
|
||||
return blockingReason
|
||||
}
|
||||
|
||||
private fun isSystemApp(packageName: String): Boolean {
|
||||
try {
|
||||
val appInfo = packageManager.getApplicationInfo(packageName, 0)
|
||||
|
||||
return appInfo.flags and ApplicationInfo.FLAG_SYSTEM == ApplicationInfo.FLAG_SYSTEM
|
||||
} catch (ex: PackageManager.NameNotFoundException) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.integration.platform.android.foregroundapp
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.content.Context
|
||||
import io.timelimit.android.integration.platform.RuntimePermissionStatus
|
||||
|
||||
class CompatForegroundAppHelper(context: Context) : ForegroundAppHelper() {
|
||||
private val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
||||
|
||||
override suspend fun getForegroundAppPackage(): String? {
|
||||
return try {
|
||||
activityManager.getRunningTasks(1)[0].topActivity.packageName
|
||||
} catch (ex: NullPointerException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPermissionStatus(): RuntimePermissionStatus {
|
||||
return RuntimePermissionStatus.NotRequired
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.integration.platform.android.foregroundapp
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import io.timelimit.android.integration.platform.RuntimePermissionStatus
|
||||
|
||||
abstract class ForegroundAppHelper {
|
||||
abstract suspend fun getForegroundAppPackage(): String?
|
||||
abstract fun getPermissionStatus(): RuntimePermissionStatus
|
||||
|
||||
companion object {
|
||||
private val lock = Any()
|
||||
private var instance: ForegroundAppHelper? = null
|
||||
|
||||
fun with(context: Context): ForegroundAppHelper {
|
||||
if (instance == null) {
|
||||
synchronized(lock) {
|
||||
if (instance == null) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
instance = LollipopForegroundAppHelper(context.applicationContext)
|
||||
} else {
|
||||
instance = CompatForegroundAppHelper(context.applicationContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return instance!!
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.integration.platform.android.foregroundapp
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.app.AppOpsManager
|
||||
import android.app.usage.UsageEvents
|
||||
import android.app.usage.UsageStatsManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import io.timelimit.android.coroutines.executeAndWait
|
||||
import io.timelimit.android.integration.platform.RuntimePermissionStatus
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
class LollipopForegroundAppHelper(private val context: Context) : ForegroundAppHelper() {
|
||||
companion object {
|
||||
private val foregroundAppThread: Executor by lazy { Executors.newSingleThreadExecutor() }
|
||||
}
|
||||
|
||||
private val usageStatsManager = context.getSystemService(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) Context.USAGE_STATS_SERVICE else "usagestats") as UsageStatsManager
|
||||
private val appOpsManager = context.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager
|
||||
|
||||
private var lastQueryTime: Long = 0
|
||||
private var lastPackage: String? = null
|
||||
private var lastPackageTime: Long = 0
|
||||
private val event = UsageEvents.Event()
|
||||
|
||||
@Throws(SecurityException::class)
|
||||
override suspend fun getForegroundAppPackage(): String? {
|
||||
if (getPermissionStatus() == RuntimePermissionStatus.NotGranted) {
|
||||
throw SecurityException()
|
||||
}
|
||||
|
||||
return foregroundAppThread.executeAndWait {
|
||||
val now = System.currentTimeMillis()
|
||||
|
||||
if (lastQueryTime > now) {
|
||||
// if the time went backwards, forget everything
|
||||
lastQueryTime = 0
|
||||
lastPackage = null
|
||||
lastPackageTime = 0
|
||||
}
|
||||
|
||||
val queryStartTime = if (lastQueryTime == 0L) {
|
||||
// query data for last 7 days
|
||||
now - 1000 * 60 * 60 * 24 * 7
|
||||
} else {
|
||||
// query data since last query
|
||||
// note: when the duration is too small, Android returns no data
|
||||
// due to that, 1 second more than required is queried
|
||||
// which seems to provide all data
|
||||
// update: with 1 second, some App switching events were missed
|
||||
// it seems to always work with 1.5 seconds
|
||||
lastQueryTime - 1500
|
||||
}
|
||||
|
||||
usageStatsManager.queryEvents(queryStartTime, now)?.let { usageEvents ->
|
||||
while (usageEvents.hasNextEvent()) {
|
||||
usageEvents.getNextEvent(event)
|
||||
|
||||
if (event.eventType == UsageEvents.Event.MOVE_TO_FOREGROUND) {
|
||||
if (event.timeStamp > lastPackageTime) {
|
||||
lastPackageTime = event.timeStamp
|
||||
lastPackage = event.packageName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lastQueryTime = now
|
||||
|
||||
lastPackage
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPermissionStatus(): RuntimePermissionStatus {
|
||||
if(appOpsManager.checkOpNoThrow("android:get_usage_stats",
|
||||
android.os.Process.myUid(), context.packageName) == AppOpsManager.MODE_ALLOWED) {
|
||||
return RuntimePermissionStatus.Granted
|
||||
} else {
|
||||
return RuntimePermissionStatus.NotGranted
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.integration.platform.android.receiver
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
|
||||
class BootReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
if (intent?.action == Intent.ACTION_BOOT_COMPLETED) {
|
||||
// this starts the logic (if not yet done)
|
||||
DefaultAppLogic.with(context).backgroundTaskLogic.reportDeviceReboot()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.integration.platform.android.receiver
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
|
||||
class UpdateReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
if (intent?.action == Intent.ACTION_MY_PACKAGE_REPLACED) {
|
||||
// this starts the logic (if not yet done)
|
||||
DefaultAppLogic.with(context)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.integration.platform.dummy
|
||||
|
||||
import io.timelimit.android.data.model.App
|
||||
import io.timelimit.android.data.model.AppRecommendation
|
||||
|
||||
object DummyApps {
|
||||
val taskmanagerLocalApp = App(
|
||||
deviceId = "",
|
||||
packageName = "com.demo.taskkiller",
|
||||
title = "Task-Killer",
|
||||
isLaunchable = true,
|
||||
recommendation = AppRecommendation.Blacklist
|
||||
)
|
||||
|
||||
val launcherLocalApp = App(
|
||||
deviceId = "",
|
||||
packageName = "com.demo.home",
|
||||
title = "Launcher",
|
||||
isLaunchable = true,
|
||||
recommendation = AppRecommendation.Whitelist
|
||||
)
|
||||
|
||||
val messagingLocalApp = App(
|
||||
deviceId = "",
|
||||
packageName = "com.demo.messaging",
|
||||
title = "Messaging",
|
||||
isLaunchable = true,
|
||||
recommendation = AppRecommendation.None
|
||||
)
|
||||
|
||||
val gameLocalApp = App(
|
||||
deviceId = "",
|
||||
packageName = "com.demo.game",
|
||||
title = "Game",
|
||||
isLaunchable = true,
|
||||
recommendation = AppRecommendation.None
|
||||
)
|
||||
|
||||
val all = listOf(taskmanagerLocalApp, launcherLocalApp, messagingLocalApp, gameLocalApp)
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.integration.platform.dummy
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import io.timelimit.android.data.model.App
|
||||
import io.timelimit.android.integration.platform.*
|
||||
|
||||
class DummyIntegration(
|
||||
maximumProtectionLevel: ProtectionLevel
|
||||
): PlatformIntegration(maximumProtectionLevel) {
|
||||
val localApps = ArrayList<App>(DummyApps.all)
|
||||
var protectionLevel = ProtectionLevel.None
|
||||
var foregroundAppPermission: RuntimePermissionStatus = RuntimePermissionStatus.NotRequired
|
||||
var drawOverOtherApps: RuntimePermissionStatus = RuntimePermissionStatus.NotRequired
|
||||
var notificationAccess: NewPermissionStatus = NewPermissionStatus.NotSupported
|
||||
var foregroundApp: String? = null
|
||||
var screenOn = false
|
||||
var lastAppStatusMessage: AppStatusMessage? = null
|
||||
var launchLockScreenForPackage: String? = null
|
||||
var showRevokeTemporarilyAllowedNotification = false
|
||||
|
||||
override fun getLocalApps(deviceId: String): Collection<App> {
|
||||
return localApps.map{ it.copy(deviceId = deviceId) }
|
||||
}
|
||||
|
||||
override fun getLocalAppTitle(packageName: String): String? {
|
||||
return localApps.find { it.packageName == packageName }?.title
|
||||
}
|
||||
|
||||
override fun getAppIcon(packageName: String): Drawable? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun getCurrentProtectionLevel(): ProtectionLevel {
|
||||
return protectionLevel
|
||||
}
|
||||
|
||||
override fun getForegroundAppPermissionStatus(): RuntimePermissionStatus {
|
||||
return foregroundAppPermission
|
||||
}
|
||||
|
||||
override fun getDrawOverOtherAppsPermissionStatus(): RuntimePermissionStatus {
|
||||
return drawOverOtherApps
|
||||
}
|
||||
|
||||
override fun getNotificationAccessPermissionStatus(): NewPermissionStatus {
|
||||
return notificationAccess
|
||||
}
|
||||
|
||||
override fun trySetLockScreenPassword(password: String): Boolean {
|
||||
return false // it failed
|
||||
}
|
||||
override fun showOverlayMessage(text: String) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
override fun showAppLockScreen(currentPackageName: String) {
|
||||
launchLockScreenForPackage = currentPackageName
|
||||
}
|
||||
|
||||
fun getAndResetShowAppLockScreen(): String? {
|
||||
try {
|
||||
return launchLockScreenForPackage
|
||||
} finally {
|
||||
launchLockScreenForPackage = null
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getForegroundAppPackageName(): String? {
|
||||
if (foregroundAppPermission == RuntimePermissionStatus.NotGranted) {
|
||||
throw SecurityException()
|
||||
}
|
||||
|
||||
return foregroundApp
|
||||
}
|
||||
|
||||
override fun setAppStatusMessage(message: AppStatusMessage?) {
|
||||
lastAppStatusMessage = message
|
||||
}
|
||||
|
||||
fun getAppStatusMessage(): AppStatusMessage? {
|
||||
return lastAppStatusMessage
|
||||
}
|
||||
|
||||
fun notifyLocalAppsChanged() {
|
||||
installedAppsChangeListener?.run()
|
||||
}
|
||||
|
||||
override fun isScreenOn(): Boolean {
|
||||
return screenOn
|
||||
}
|
||||
|
||||
override fun setShowNotificationToRevokeTemporarilyAllowedApps(show: Boolean) {
|
||||
showRevokeTemporarilyAllowedNotification = show
|
||||
}
|
||||
|
||||
override fun showRemoteResetNotification() {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
override fun disableDeviceAdmin() {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
override fun setSuspendedApps(packageNames: List<String>, suspend: Boolean) = emptyList<String>()
|
||||
|
||||
override fun stopSuspendingForAllApps() {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
override fun setEnableSystemLockdown(enableLockdown: Boolean) = false
|
||||
|
||||
override fun setLockTaskPackages(packageNames: List<String>) = false
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.integration.time
|
||||
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
class DummyTimeApi(var timeStepSizeInMillis: Long): TimeApi() {
|
||||
private var currentTime: Long = System.currentTimeMillis()
|
||||
private var currentUptime: Long = 0
|
||||
private var scheduledActions = Collections.synchronizedList(ArrayList<ScheduledAction>())
|
||||
var timeZone = TimeZone.getDefault()
|
||||
|
||||
override fun getCurrentTimeInMillis(): Long {
|
||||
return currentTime
|
||||
}
|
||||
|
||||
fun setCurrentTimeInMillis(time: Long) {
|
||||
this.currentTime = time
|
||||
}
|
||||
|
||||
override fun getCurrentUptimeInMillis(): Long {
|
||||
return currentUptime
|
||||
}
|
||||
|
||||
override fun getSystemTimeZone() = timeZone
|
||||
|
||||
override fun runDelayed(runnable: Runnable, delayInMillis: Long) {
|
||||
scheduledActions.add(ScheduledAction(currentUptime + delayInMillis, runnable))
|
||||
}
|
||||
|
||||
override fun cancelScheduledAction(runnable: Runnable) {
|
||||
scheduledActions.removeAll { it.action === runnable }
|
||||
}
|
||||
|
||||
private fun emulateTimeAtOnce(timeInMillis: Long) {
|
||||
if (timeInMillis <= 0) {
|
||||
throw IllegalStateException()
|
||||
}
|
||||
|
||||
currentTime += timeInMillis
|
||||
currentUptime += timeInMillis
|
||||
|
||||
synchronized(scheduledActions) {
|
||||
val iterator = scheduledActions.iterator()
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
val action = iterator.next()
|
||||
|
||||
if (action.uptime <= currentUptime) {
|
||||
action.action.run()
|
||||
iterator.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun emulateTimePassing(timeInMillis: Long) {
|
||||
var emulatedTime: Long = 0
|
||||
|
||||
while (emulatedTime < timeInMillis) {
|
||||
val missingTime = timeInMillis - emulatedTime
|
||||
|
||||
if (missingTime >= timeStepSizeInMillis) {
|
||||
emulateTimeAtOnce(timeStepSizeInMillis)
|
||||
emulatedTime += timeStepSizeInMillis
|
||||
} else {
|
||||
emulateTimeAtOnce(missingTime)
|
||||
emulatedTime += missingTime
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ScheduledAction (
|
||||
val uptime: Long,
|
||||
val action: Runnable
|
||||
)
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.integration.time
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.SystemClock
|
||||
import java.util.*
|
||||
|
||||
object RealTimeApi: TimeApi() {
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
|
||||
override fun getCurrentTimeInMillis(): Long {
|
||||
return System.currentTimeMillis()
|
||||
}
|
||||
|
||||
override fun getCurrentUptimeInMillis(): Long {
|
||||
return SystemClock.elapsedRealtime()
|
||||
}
|
||||
|
||||
override fun runDelayed(runnable: Runnable, delayInMillis: Long) {
|
||||
handler.postDelayed(runnable, delayInMillis)
|
||||
}
|
||||
|
||||
override fun cancelScheduledAction(runnable: Runnable) {
|
||||
handler.removeCallbacks(runnable)
|
||||
}
|
||||
|
||||
override fun getSystemTimeZone() = TimeZone.getDefault()
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.integration.time
|
||||
|
||||
import java.util.*
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
abstract class TimeApi {
|
||||
// normal clock - can be modified by the user at any time
|
||||
abstract fun getCurrentTimeInMillis(): Long
|
||||
// clock which starts at 0 at boot
|
||||
abstract fun getCurrentUptimeInMillis(): Long
|
||||
// function to run something delayed at the UI Thread
|
||||
abstract fun runDelayed(runnable: Runnable, delayInMillis: Long)
|
||||
abstract fun cancelScheduledAction(runnable: Runnable)
|
||||
suspend fun sleep(timeInMillis: Long) = suspendCoroutine<Void?> {
|
||||
runDelayed(Runnable {
|
||||
it.resume(null)
|
||||
}, timeInMillis)
|
||||
}
|
||||
abstract fun getSystemTimeZone(): TimeZone
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.livedata
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
|
||||
fun LiveData<Boolean>.or(other: LiveData<Boolean>): LiveData<Boolean> {
|
||||
return this.switchMap { v1 ->
|
||||
other.map { v2 ->
|
||||
v1 || v2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun LiveData<Boolean>.and(other: LiveData<Boolean>): LiveData<Boolean> {
|
||||
return this.switchMap { v1 ->
|
||||
other.map { v2 ->
|
||||
v1 && v2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun LiveData<Boolean>.invert(): LiveData<Boolean> = this.map { !it }
|
20
app/src/main/java/io/timelimit/android/livedata/CastDown.kt
Normal file
20
app/src/main/java/io/timelimit/android/livedata/CastDown.kt
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.livedata
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
|
||||
fun <T> LiveData<T>.castDown(): LiveData<T> = this
|
26
app/src/main/java/io/timelimit/android/livedata/FromValue.kt
Normal file
26
app/src/main/java/io/timelimit/android/livedata/FromValue.kt
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.livedata
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
|
||||
fun <T> liveDataFromValue(value: T): LiveData<T> {
|
||||
val result = MediatorLiveData<T>()
|
||||
result.value = value
|
||||
|
||||
return result
|
||||
}
|
44
app/src/main/java/io/timelimit/android/livedata/FromView.kt
Normal file
44
app/src/main/java/io/timelimit/android/livedata/FromView.kt
Normal file
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.livedata
|
||||
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.widget.TextView
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
|
||||
fun TextView.getTextLive(): LiveData<String> {
|
||||
val result = MutableLiveData<String>()
|
||||
|
||||
result.value = this.text.toString()
|
||||
|
||||
this.addTextChangedListener(object: TextWatcher {
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
result.value = s.toString()
|
||||
}
|
||||
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
|
||||
// ignore
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
55
app/src/main/java/io/timelimit/android/livedata/GetValue.kt
Normal file
55
app/src/main/java/io/timelimit/android/livedata/GetValue.kt
Normal file
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.livedata
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
suspend fun <T> LiveData<T>.waitUntilValueMatches(check: (T?) -> Boolean): T? {
|
||||
val liveData = this
|
||||
var observer: Observer<T>? = null
|
||||
|
||||
fun removeObserver() {
|
||||
val currentObserver = observer
|
||||
|
||||
if (currentObserver != null) {
|
||||
liveData.removeObserver(currentObserver)
|
||||
}
|
||||
}
|
||||
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
continuation.invokeOnCancellation { removeObserver() }
|
||||
|
||||
observer = Observer { t ->
|
||||
if (check(t)) {
|
||||
removeObserver()
|
||||
continuation.resume(t)
|
||||
}
|
||||
}
|
||||
|
||||
liveData.observeForever(observer!!)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <T> LiveData<T>.waitForNullableValue(): T? {
|
||||
return waitUntilValueMatches { true }
|
||||
}
|
||||
|
||||
suspend fun <T> LiveData<T>.waitForNonNullValue(): T {
|
||||
return waitUntilValueMatches { it != null }!!
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.livedata
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
|
||||
fun <T> LiveData<T>.ignoreUnchanged(): LiveData<T> {
|
||||
val result = MediatorLiveData<T>()
|
||||
var hadValue = false
|
||||
|
||||
result.addSource(this) {
|
||||
if (it != result.value || !hadValue) {
|
||||
hadValue = true
|
||||
result.value = it
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.livedata
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import io.timelimit.android.async.Threads
|
||||
|
||||
fun <X> liveDataFromFunction(pollInterval: Long = 1000L, function: () -> X): LiveData<X> = object: LiveData<X>() {
|
||||
val refresh = Runnable {
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
value = function()
|
||||
|
||||
Threads.mainThreadHandler.postDelayed(refresh, pollInterval)
|
||||
}
|
||||
|
||||
override fun onActive() {
|
||||
super.onActive()
|
||||
|
||||
refresh()
|
||||
}
|
||||
|
||||
override fun onInactive() {
|
||||
super.onInactive()
|
||||
|
||||
Threads.mainThreadHandler.removeCallbacks(refresh)
|
||||
}
|
||||
}.ignoreUnchanged()
|
36
app/src/main/java/io/timelimit/android/livedata/Map.kt
Normal file
36
app/src/main/java/io/timelimit/android/livedata/Map.kt
Normal file
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.livedata
|
||||
|
||||
import androidx.arch.core.util.Function
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
|
||||
fun <X, Y> LiveData<X>.map(function: Function<X, Y>): LiveData<Y> {
|
||||
return Transformations.map(this, function)
|
||||
}
|
||||
|
||||
fun <X, Y> LiveData<X>.map(function: (X) -> Y): LiveData<Y> {
|
||||
return Transformations.map(this, function)
|
||||
}
|
||||
|
||||
fun <X, Y> LiveData<X>.switchMap(function: Function<X, LiveData<Y>>): LiveData<Y> {
|
||||
return Transformations.switchMap(this, function)
|
||||
}
|
||||
|
||||
fun <X, Y> LiveData<X>.switchMap(function: (X) -> LiveData<Y>): LiveData<Y> {
|
||||
return Transformations.switchMap(this, function)
|
||||
}
|
140
app/src/main/java/io/timelimit/android/livedata/MergeLiveData.kt
Normal file
140
app/src/main/java/io/timelimit/android/livedata/MergeLiveData.kt
Normal file
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.livedata
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
|
||||
fun <T1, T2> mergeLiveData(d1: LiveData<T1>, d2: LiveData<T2>): LiveData<Pair<T1?, T2?>> {
|
||||
val result = MediatorLiveData<Pair<T1?, T2?>>()
|
||||
result.value = Pair(null, null)
|
||||
|
||||
result.addSource(d1) {
|
||||
result.value = result.value!!.copy(first = it)
|
||||
}
|
||||
|
||||
result.addSource(d2) {
|
||||
result.value = result.value!!.copy(second = it)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
fun <T1, T2, T3> mergeLiveData(d1: LiveData<T1>, d2: LiveData<T2>, d3: LiveData<T3>): LiveData<Triple<T1?, T2?, T3?>> {
|
||||
val result = MediatorLiveData<Triple<T1?, T2?, T3?>>()
|
||||
result.value = Triple(null, null, null)
|
||||
|
||||
result.addSource(d1) {
|
||||
result.value = result.value!!.copy(first = it)
|
||||
}
|
||||
|
||||
result.addSource(d2) {
|
||||
result.value = result.value!!.copy(second = it)
|
||||
}
|
||||
|
||||
result.addSource(d3) {
|
||||
result.value = result.value!!.copy(third = it)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
fun <T1, T2, T3, T4> mergeLiveData(d1: LiveData<T1>, d2: LiveData<T2>, d3: LiveData<T3>, d4: LiveData<T4>): LiveData<FourTuple<T1?, T2?, T3?, T4?>> {
|
||||
val result = MediatorLiveData<FourTuple<T1?, T2?, T3?, T4?>>()
|
||||
result.value = FourTuple(null, null, null, null)
|
||||
|
||||
result.addSource(d1) {
|
||||
result.value = result.value!!.copy(first = it)
|
||||
}
|
||||
|
||||
result.addSource(d2) {
|
||||
result.value = result.value!!.copy(second = it)
|
||||
}
|
||||
|
||||
result.addSource(d3) {
|
||||
result.value = result.value!!.copy(third = it)
|
||||
}
|
||||
|
||||
result.addSource(d4) {
|
||||
result.value = result.value!!.copy(forth = it)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
data class FourTuple<A, B, C, D>(val first: A, val second: B, val third: C, val forth: D)
|
||||
|
||||
fun <T1, T2, T3, T4, T5> mergeLiveData(d1: LiveData<T1>, d2: LiveData<T2>, d3: LiveData<T3>, d4: LiveData<T4>, d5: LiveData<T5>): LiveData<FiveTuple<T1?, T2?, T3?, T4?, T5?>> {
|
||||
val result = MediatorLiveData<FiveTuple<T1?, T2?, T3?, T4?, T5?>>()
|
||||
result.value = FiveTuple(null, null, null, null, null)
|
||||
|
||||
result.addSource(d1) {
|
||||
result.value = result.value!!.copy(first = it)
|
||||
}
|
||||
|
||||
result.addSource(d2) {
|
||||
result.value = result.value!!.copy(second = it)
|
||||
}
|
||||
|
||||
result.addSource(d3) {
|
||||
result.value = result.value!!.copy(third = it)
|
||||
}
|
||||
|
||||
result.addSource(d4) {
|
||||
result.value = result.value!!.copy(forth = it)
|
||||
}
|
||||
|
||||
result.addSource(d5) {
|
||||
result.value = result.value!!.copy(fifth = it)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
data class FiveTuple<A, B, C, D, E>(val first: A, val second: B, val third: C, val forth: D, val fifth: E)
|
||||
|
||||
fun <T1, T2, T3, T4, T5, T6> mergeLiveData(d1: LiveData<T1>, d2: LiveData<T2>, d3: LiveData<T3>, d4: LiveData<T4>, d5: LiveData<T5>, d6: LiveData<T6>): LiveData<SixTuple<T1?, T2?, T3?, T4?, T5?, T6?>> {
|
||||
val result = MediatorLiveData<SixTuple<T1?, T2?, T3?, T4?, T5?, T6?>>()
|
||||
result.value = SixTuple(null, null, null, null, null, null)
|
||||
|
||||
result.addSource(d1) {
|
||||
result.value = result.value!!.copy(first = it)
|
||||
}
|
||||
|
||||
result.addSource(d2) {
|
||||
result.value = result.value!!.copy(second = it)
|
||||
}
|
||||
|
||||
result.addSource(d3) {
|
||||
result.value = result.value!!.copy(third = it)
|
||||
}
|
||||
|
||||
result.addSource(d4) {
|
||||
result.value = result.value!!.copy(forth = it)
|
||||
}
|
||||
|
||||
result.addSource(d5) {
|
||||
result.value = result.value!!.copy(fifth = it)
|
||||
}
|
||||
|
||||
result.addSource(d6) {
|
||||
result.value = result.value!!.copy(sixth = it)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
data class SixTuple<A, B, C, D, E, F>(val first: A, val second: B, val third: C, val forth: D, val fifth: E, val sixth: F)
|
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.livedata
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
abstract class LiveDataCache {
|
||||
abstract fun reportLoopDone()
|
||||
abstract fun removeAllItems()
|
||||
}
|
||||
|
||||
class SingleItemLiveDataCache<T>(private val liveData: LiveData<T>): LiveDataCache() {
|
||||
private val dummyObserver = Observer<T> {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
private var isObserving = false
|
||||
private var wasUsed = false
|
||||
|
||||
fun read(): LiveData<T> {
|
||||
if (!isObserving) {
|
||||
liveData.observeForever(dummyObserver)
|
||||
isObserving = true
|
||||
}
|
||||
|
||||
wasUsed = true
|
||||
|
||||
return liveData
|
||||
}
|
||||
|
||||
override fun removeAllItems() {
|
||||
if (isObserving) {
|
||||
liveData.removeObserver(dummyObserver)
|
||||
isObserving = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun reportLoopDone() {
|
||||
if (isObserving && !wasUsed) {
|
||||
removeAllItems()
|
||||
}
|
||||
|
||||
wasUsed = false
|
||||
}
|
||||
}
|
||||
|
||||
class SingleItemLiveDataCacheWithRequery<T>(private val liveDataCreator: () -> LiveData<T>): LiveDataCache() {
|
||||
private val dummyObserver = Observer<T> {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
private var wasUsed = false
|
||||
private var instance: LiveData<T>? = null
|
||||
|
||||
fun read(): LiveData<T> {
|
||||
if (instance == null) {
|
||||
instance = liveDataCreator()
|
||||
instance!!.observeForever(dummyObserver)
|
||||
}
|
||||
|
||||
wasUsed = true
|
||||
|
||||
return instance!!
|
||||
}
|
||||
|
||||
override fun removeAllItems() {
|
||||
if (instance != null) {
|
||||
instance!!.removeObserver(dummyObserver)
|
||||
instance = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun reportLoopDone() {
|
||||
if (instance != null && !wasUsed) {
|
||||
removeAllItems()
|
||||
}
|
||||
|
||||
wasUsed = false
|
||||
}
|
||||
}
|
||||
|
||||
abstract class MultiKeyLiveDataCache<R, K>: LiveDataCache() {
|
||||
class ItemWrapper<R>(val value: LiveData<R>, var used: Boolean)
|
||||
|
||||
private val items = ConcurrentHashMap<K, ItemWrapper<R>>()
|
||||
|
||||
private val dummyObserver = Observer<R> {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
protected abstract fun createValue(key: K): LiveData<R>
|
||||
|
||||
fun get(key: K): LiveData<R> {
|
||||
val oldItem = items[key]
|
||||
|
||||
if (oldItem != null) {
|
||||
oldItem.used = true
|
||||
|
||||
return oldItem.value
|
||||
} else {
|
||||
val newItem = ItemWrapper(createValue(key), true)
|
||||
newItem.value.observeForever(dummyObserver)
|
||||
|
||||
items[key] = newItem
|
||||
|
||||
return newItem.value
|
||||
}
|
||||
}
|
||||
|
||||
override fun reportLoopDone() {
|
||||
items.forEach {
|
||||
if (it.value.used) {
|
||||
it.value.used = false
|
||||
} else {
|
||||
it.value.value.removeObserver(dummyObserver)
|
||||
items.remove(it.key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun removeAllItems() {
|
||||
items.forEach {
|
||||
it.value.value.removeObserver(dummyObserver)
|
||||
}
|
||||
|
||||
items.clear()
|
||||
}
|
||||
}
|
||||
|
||||
class LiveDataCaches(private val caches: Array<LiveDataCache>) {
|
||||
fun reportLoopDone() {
|
||||
caches.forEach { it.reportLoopDone() }
|
||||
}
|
||||
|
||||
fun removeAllItems() {
|
||||
caches.forEach { it.removeAllItems() }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.livedata
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import java.io.Closeable
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
fun <T> MutableLiveData<T>.setTemporarily(newValue: T): Closeable {
|
||||
val data = this
|
||||
val oldValue = data.value
|
||||
val closed = AtomicBoolean(false)
|
||||
|
||||
this.value = newValue
|
||||
|
||||
return object: Closeable {
|
||||
override fun close() {
|
||||
if (closed.compareAndSet(false, true)) {
|
||||
data.value = oldValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.logic
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.coroutines.runAsync
|
||||
import io.timelimit.android.data.RoomDatabase
|
||||
import io.timelimit.android.data.backup.DatabaseBackup
|
||||
import io.timelimit.android.integration.platform.android.AndroidIntegration
|
||||
import io.timelimit.android.integration.time.RealTimeApi
|
||||
import io.timelimit.android.sync.network.api.HttpServerApi
|
||||
import io.timelimit.android.sync.websocket.NetworkStatusUtil
|
||||
import io.timelimit.android.sync.websocket.SocketIoWebsocketClient
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
object AndroidAppLogic {
|
||||
private var instance: AppLogic? = null
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
|
||||
fun with(context: Context): AppLogic {
|
||||
val safeContext = context.applicationContext
|
||||
|
||||
if (Looper.getMainLooper() == Looper.myLooper()) {
|
||||
// at the UI thread
|
||||
if (instance == null) {
|
||||
val isInitialized = MutableLiveData<Boolean>().apply { value = false }
|
||||
|
||||
instance = AppLogic(
|
||||
platformIntegration = AndroidIntegration(safeContext),
|
||||
timeApi = RealTimeApi,
|
||||
database = RoomDatabase.with(safeContext),
|
||||
serverCreator = { url ->
|
||||
HttpServerApi.createInstance(
|
||||
if (url.isEmpty()) {
|
||||
BuildConfig.serverUrl
|
||||
} else {
|
||||
url
|
||||
}
|
||||
)
|
||||
},
|
||||
networkStatus = NetworkStatusUtil.getSystemNetworkStatusLive(safeContext),
|
||||
websocketClientCreator = SocketIoWebsocketClient.creator,
|
||||
context = safeContext,
|
||||
isInitialized = isInitialized
|
||||
)
|
||||
|
||||
runAsync {
|
||||
DatabaseBackup.with(safeContext).tryRestoreDatabaseBackupAsyncAndWait()
|
||||
isInitialized.value = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// at a background thread
|
||||
if (instance == null) {
|
||||
val latch = CountDownLatch(1)
|
||||
|
||||
handler.post {
|
||||
with(context)
|
||||
latch.countDown()
|
||||
}
|
||||
|
||||
latch.await()
|
||||
}
|
||||
}
|
||||
|
||||
return instance!!
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.logic
|
||||
|
||||
import io.timelimit.android.data.model.UserType
|
||||
import io.timelimit.android.livedata.waitForNonNullValue
|
||||
import io.timelimit.android.livedata.waitForNullableValue
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
if (user.relaxPrimaryDevice) {
|
||||
if (logic.fullVersion.shouldProvideFullVersionFunctions.waitForNonNullValue() == true) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
val currentApp = try {
|
||||
logic.platformIntegration.getForegroundAppPackageName()
|
||||
} catch (ex: SecurityException) {
|
||||
null
|
||||
}
|
||||
|
||||
if (currentApp == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
val categories = logic.database.category().getCategoriesByChildId(user.id).waitForNonNullValue()
|
||||
val categoryId = logic.database.categoryApp().getCategoryApp(
|
||||
categoryIds = categories.map { it.id },
|
||||
packageName = currentApp
|
||||
).waitForNullableValue()?.categoryId ?: user.categoryForNotAssignedApps
|
||||
|
||||
val category = categories.find { it.id == categoryId }
|
||||
val parentCategory = categories.find { it.id == category?.parentCategoryId }
|
||||
|
||||
if (category == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
// check blocked time areas
|
||||
if (
|
||||
(category.blockedMinutesInWeek.dataNotToModify.isEmpty == false) ||
|
||||
(parentCategory?.blockedMinutesInWeek?.dataNotToModify?.isEmpty == false)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
// check time limit rules
|
||||
val rules = logic.database.timeLimitRules().getTimeLimitRulesByCategories(
|
||||
categoryIds = listOf(categoryId) +
|
||||
(if (parentCategory != null) listOf(parentCategory.id) else emptyList())
|
||||
).waitForNonNullValue()
|
||||
|
||||
if (rules.isNotEmpty()) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
102
app/src/main/java/io/timelimit/android/logic/AppLogic.kt
Normal file
102
app/src/main/java/io/timelimit/android/logic/AppLogic.kt
Normal file
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.logic
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import io.timelimit.android.data.Database
|
||||
import io.timelimit.android.data.model.Device
|
||||
import io.timelimit.android.data.model.User
|
||||
import io.timelimit.android.integration.platform.PlatformIntegration
|
||||
import io.timelimit.android.integration.time.TimeApi
|
||||
import io.timelimit.android.livedata.castDown
|
||||
import io.timelimit.android.livedata.ignoreUnchanged
|
||||
import io.timelimit.android.livedata.liveDataFromValue
|
||||
import io.timelimit.android.livedata.switchMap
|
||||
import io.timelimit.android.sync.SyncUtil
|
||||
import io.timelimit.android.sync.network.api.ServerApi
|
||||
import io.timelimit.android.sync.websocket.NetworkStatus
|
||||
import io.timelimit.android.sync.websocket.WebsocketClientCreator
|
||||
|
||||
class AppLogic(
|
||||
val platformIntegration: PlatformIntegration,
|
||||
val timeApi: TimeApi,
|
||||
val database: Database,
|
||||
val serverCreator: (serverUrl: String) -> ServerApi,
|
||||
val networkStatus: LiveData<NetworkStatus>,
|
||||
websocketClientCreator: WebsocketClientCreator,
|
||||
val context: Context,
|
||||
val isInitialized: LiveData<Boolean>
|
||||
) {
|
||||
val enable = MutableLiveData<Boolean>().apply { value = true }
|
||||
|
||||
val deviceId = database.config().getOwnDeviceId()
|
||||
|
||||
val deviceEntry = Transformations.switchMap<String?, Device?> (deviceId) {
|
||||
if (it == null) {
|
||||
liveDataFromValue(null)
|
||||
} else {
|
||||
database.device().getDeviceById(it)
|
||||
}
|
||||
}.ignoreUnchanged()
|
||||
|
||||
val deviceEntryIfEnabled = enable.switchMap {
|
||||
if (it == null || it == false) {
|
||||
liveDataFromValue(null as Device?)
|
||||
} else {
|
||||
deviceEntry
|
||||
}
|
||||
}
|
||||
|
||||
val deviceUserId: LiveData<String> = Transformations.map(deviceEntry) { it?.currentUserId ?: "" }
|
||||
|
||||
val deviceUserEntry = deviceUserId.switchMap {
|
||||
if (it == "") {
|
||||
liveDataFromValue(null as User?)
|
||||
} else {
|
||||
database.user().getUserByIdLive(it)
|
||||
}
|
||||
}.ignoreUnchanged()
|
||||
|
||||
val serverLogic = ServerLogic(this)
|
||||
val defaultUserLogic = DefaultUserLogic(this)
|
||||
val realTimeLogic = RealTimeLogic(this)
|
||||
val fullVersion = FullVersionLogic(this)
|
||||
val currentDeviceLogic = CurrentDeviceLogic(this)
|
||||
val backgroundTaskLogic = BackgroundTaskLogic(this)
|
||||
val appSetupLogic = AppSetupLogic(this)
|
||||
|
||||
private val isConnectedInternal = MutableLiveData<Boolean>().apply { value = false }
|
||||
val isConnected = isConnectedInternal.castDown()
|
||||
val syncUtil = SyncUtil(this)
|
||||
val websocket = WebsocketClientLogic(
|
||||
appLogic = this,
|
||||
isConnectedInternal = isConnectedInternal,
|
||||
websocketClientCreator = websocketClientCreator
|
||||
)
|
||||
|
||||
init {
|
||||
SyncInstalledAppsLogic(this)
|
||||
}
|
||||
|
||||
val manipulationLogic = ManipulationLogic(this)
|
||||
|
||||
fun shutdown() {
|
||||
enable.value = false
|
||||
}
|
||||
}
|
225
app/src/main/java/io/timelimit/android/logic/AppSetupLogic.kt
Normal file
225
app/src/main/java/io/timelimit/android/logic/AppSetupLogic.kt
Normal file
|
@ -0,0 +1,225 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.logic
|
||||
|
||||
import android.content.Context
|
||||
import com.jaredrummler.android.device.DeviceName
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.async.Threads
|
||||
import io.timelimit.android.coroutines.executeAndWait
|
||||
import io.timelimit.android.crypto.PasswordHashing
|
||||
import io.timelimit.android.data.IdGenerator
|
||||
import io.timelimit.android.data.backup.DatabaseBackup
|
||||
import io.timelimit.android.data.customtypes.ImmutableBitmask
|
||||
import io.timelimit.android.data.model.*
|
||||
import io.timelimit.android.integration.platform.NewPermissionStatus
|
||||
import io.timelimit.android.integration.platform.ProtectionLevel
|
||||
import io.timelimit.android.integration.platform.RuntimePermissionStatus
|
||||
import io.timelimit.android.ui.user.create.DefaultCategories
|
||||
import java.util.*
|
||||
|
||||
class AppSetupLogic(private val appLogic: AppLogic) {
|
||||
suspend fun setupForLocalUse(parentPassword: String, networkTimeVerification: NetworkTime, context: Context) {
|
||||
Threads.database.executeAndWait(Runnable {
|
||||
run {
|
||||
// assert that the device is not yet configured
|
||||
val oldDeviceId = appLogic.database.config().getOwnDeviceIdSync()
|
||||
|
||||
if (oldDeviceId != null) {
|
||||
throw IllegalStateException("already configured")
|
||||
}
|
||||
}
|
||||
|
||||
val ownDeviceId = IdGenerator.generateId()
|
||||
val parentUserId = IdGenerator.generateId()
|
||||
val childUserId = IdGenerator.generateId()
|
||||
val allowedAppsCategoryId = IdGenerator.generateId()
|
||||
val allowedGamesCategoryId = IdGenerator.generateId()
|
||||
|
||||
appLogic.database.beginTransaction()
|
||||
try {
|
||||
run {
|
||||
val customServerUrl = appLogic.database.config().getCustomServerUrlSync()
|
||||
|
||||
// just for safety: delete everything except the custom server url
|
||||
appLogic.database.deleteAllData()
|
||||
|
||||
appLogic.database.config().setCustomServerUrlSync(customServerUrl)
|
||||
}
|
||||
|
||||
run {
|
||||
// set device id
|
||||
appLogic.database.config().setOwnDeviceIdSync(ownDeviceId)
|
||||
}
|
||||
|
||||
val timeZone = appLogic.timeApi.getSystemTimeZone().id
|
||||
|
||||
run {
|
||||
// add device
|
||||
val deviceName = DeviceName.getDeviceName()
|
||||
|
||||
val device = Device(
|
||||
id = ownDeviceId,
|
||||
name = deviceName,
|
||||
model = deviceName,
|
||||
addedAt = appLogic.timeApi.getCurrentTimeInMillis(),
|
||||
currentUserId = childUserId,
|
||||
installedAppsVersion = "",
|
||||
networkTime = networkTimeVerification,
|
||||
currentProtectionLevel = ProtectionLevel.None,
|
||||
highestProtectionLevel = ProtectionLevel.None,
|
||||
currentNotificationAccessPermission = NewPermissionStatus.NotGranted,
|
||||
highestNotificationAccessPermission = NewPermissionStatus.NotGranted,
|
||||
currentUsageStatsPermission = RuntimePermissionStatus.NotGranted,
|
||||
highestUsageStatsPermission = RuntimePermissionStatus.NotGranted,
|
||||
currentAppVersion = 0,
|
||||
highestAppVersion = 0,
|
||||
manipulationTriedDisablingDeviceAdmin = false,
|
||||
manipulationDidReboot = false,
|
||||
hadManipulation = false,
|
||||
didReportUninstall = false,
|
||||
isUserKeptSignedIn = false,
|
||||
showDeviceConnected = false,
|
||||
defaultUser = "",
|
||||
defaultUserTimeout = 0,
|
||||
considerRebootManipulation = false
|
||||
)
|
||||
|
||||
appLogic.database.device().addDeviceSync(device)
|
||||
}
|
||||
|
||||
run {
|
||||
// add child
|
||||
|
||||
val child = User(
|
||||
id = childUserId,
|
||||
name = context.getString(R.string.setup_username_child),
|
||||
password = "",
|
||||
secondPasswordSalt = "",
|
||||
type = UserType.Child,
|
||||
timeZone = timeZone,
|
||||
disableLimitsUntil = 0,
|
||||
mail = "",
|
||||
currentDevice = "",
|
||||
categoryForNotAssignedApps = "",
|
||||
relaxPrimaryDevice = false,
|
||||
mailNotificationFlags = 0
|
||||
)
|
||||
|
||||
appLogic.database.user().addUserSync(child)
|
||||
}
|
||||
|
||||
run {
|
||||
// add parent
|
||||
|
||||
val parent = User(
|
||||
id = parentUserId,
|
||||
name = context.getString(R.string.setup_username_parent),
|
||||
password = PasswordHashing.hashSync(parentPassword),
|
||||
secondPasswordSalt = PasswordHashing.generateSalt(),
|
||||
type = UserType.Parent,
|
||||
timeZone = timeZone,
|
||||
disableLimitsUntil = 0,
|
||||
mail = "",
|
||||
currentDevice = "",
|
||||
categoryForNotAssignedApps = "",
|
||||
relaxPrimaryDevice = false,
|
||||
mailNotificationFlags = 0
|
||||
)
|
||||
|
||||
appLogic.database.user().addUserSync(parent)
|
||||
}
|
||||
|
||||
val installedApps = appLogic.platformIntegration.getLocalApps(ownDeviceId)
|
||||
|
||||
// add installed apps
|
||||
appLogic.database.app().addAppsSync(installedApps)
|
||||
|
||||
val defaultCategories = DefaultCategories.with(context)
|
||||
|
||||
// NOTE: the default config is created at the AddUserModel and at the AppSetupLogic
|
||||
run {
|
||||
// add starter categories
|
||||
appLogic.database.category().addCategory(Category(
|
||||
id = allowedAppsCategoryId,
|
||||
childId = childUserId,
|
||||
title = defaultCategories.allowedAppsTitle,
|
||||
blockedMinutesInWeek = ImmutableBitmask((BitSet())),
|
||||
extraTimeInMillis = 0,
|
||||
temporarilyBlocked = false,
|
||||
baseVersion = "",
|
||||
assignedAppsVersion = "",
|
||||
timeLimitRulesVersion = "",
|
||||
usedTimesVersion = "",
|
||||
parentCategoryId = ""
|
||||
))
|
||||
|
||||
appLogic.database.category().addCategory(Category(
|
||||
id = allowedGamesCategoryId,
|
||||
childId = childUserId,
|
||||
title = defaultCategories.allowedGamesTitle,
|
||||
blockedMinutesInWeek = defaultCategories.allowedGamesBlockedTimes,
|
||||
extraTimeInMillis = 0,
|
||||
temporarilyBlocked = false,
|
||||
baseVersion = "",
|
||||
assignedAppsVersion = "",
|
||||
timeLimitRulesVersion = "",
|
||||
usedTimesVersion = "",
|
||||
parentCategoryId = ""
|
||||
))
|
||||
|
||||
// add default allowed apps
|
||||
appLogic.database.categoryApp().addCategoryAppsSync(
|
||||
installedApps
|
||||
.filter { it.recommendation == AppRecommendation.Whitelist }
|
||||
.map {
|
||||
CategoryApp(
|
||||
categoryId = allowedAppsCategoryId,
|
||||
packageName = it.packageName
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
// add default time limit rules
|
||||
defaultCategories.generateGamesTimeLimitRules(allowedGamesCategoryId).forEach { rule ->
|
||||
appLogic.database.timeLimitRules().addTimeLimitRule(rule)
|
||||
}
|
||||
}
|
||||
|
||||
appLogic.database.setTransactionSuccessful()
|
||||
} finally {
|
||||
appLogic.database.endTransaction()
|
||||
}
|
||||
})
|
||||
|
||||
DatabaseBackup.with(appLogic.context).tryCreateDatabaseBackupAsync()
|
||||
}
|
||||
|
||||
suspend fun dangerousResetApp() {
|
||||
Threads.database.executeAndWait(Runnable {
|
||||
// this is already wrapped in a transaction
|
||||
appLogic.database.deleteAllData()
|
||||
})
|
||||
|
||||
// delete the old config
|
||||
DatabaseBackup.with(appLogic.context).tryCreateDatabaseBackupAsync()
|
||||
}
|
||||
|
||||
suspend fun dangerousRemoteReset() {
|
||||
appLogic.platformIntegration.showRemoteResetNotification()
|
||||
dangerousResetApp()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,574 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.logic
|
||||
|
||||
import android.util.Log
|
||||
import android.util.SparseArray
|
||||
import android.util.SparseLongArray
|
||||
import androidx.lifecycle.LiveData
|
||||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.async.Threads
|
||||
import io.timelimit.android.coroutines.executeAndWait
|
||||
import io.timelimit.android.coroutines.runAsync
|
||||
import io.timelimit.android.coroutines.runAsyncExpectForever
|
||||
import io.timelimit.android.data.backup.DatabaseBackup
|
||||
import io.timelimit.android.data.model.*
|
||||
import io.timelimit.android.date.DateInTimezone
|
||||
import io.timelimit.android.date.getMinuteOfWeek
|
||||
import io.timelimit.android.integration.platform.AppStatusMessage
|
||||
import io.timelimit.android.integration.platform.ProtectionLevel
|
||||
import io.timelimit.android.integration.platform.android.AndroidIntegrationApps
|
||||
import io.timelimit.android.livedata.*
|
||||
import io.timelimit.android.sync.actions.UpdateDeviceStatusAction
|
||||
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
||||
import io.timelimit.android.ui.IsAppInForeground
|
||||
import io.timelimit.android.util.TimeTextUtil
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.util.*
|
||||
|
||||
class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||
companion object {
|
||||
private const val CHECK_PERMISSION_INTERVAL = 10 * 1000L // all 10 seconds
|
||||
private const val BACKGROUND_SERVICE_INTERVAL = 100L // all 100 ms
|
||||
private const val MAX_USED_TIME_PER_ROUND = 1000 // 1 second
|
||||
private const val LOG_TAG = "BackgroundTaskLogic"
|
||||
}
|
||||
|
||||
private val temporarilyAllowedApps = appLogic.deviceId.switchMap {
|
||||
if (it != null) {
|
||||
appLogic.database.temporarilyAllowedApp().getTemporarilyAllowedApps(it)
|
||||
} else {
|
||||
liveDataFromValue(Collections.emptyList<String>())
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
runAsyncExpectForever { backgroundServiceLoop() }
|
||||
runAsyncExpectForever { syncDeviceStatusLoop() }
|
||||
runAsyncExpectForever { backupDatabaseLoop() }
|
||||
runAsync {
|
||||
// this is effective after an reboot
|
||||
|
||||
if (appLogic.deviceEntryIfEnabled.waitForNullableValue() != null) {
|
||||
appLogic.platformIntegration.setEnableSystemLockdown(true)
|
||||
}
|
||||
}
|
||||
|
||||
appLogic.deviceEntryIfEnabled
|
||||
.map { it?.id }
|
||||
.ignoreUnchanged()
|
||||
.observeForever {
|
||||
_ ->
|
||||
|
||||
runAsync {
|
||||
syncInstalledAppVersion()
|
||||
}
|
||||
}
|
||||
|
||||
temporarilyAllowedApps.map { it.isNotEmpty() }.ignoreUnchanged().observeForever {
|
||||
appLogic.platformIntegration.setShowNotificationToRevokeTemporarilyAllowedApps(it!!)
|
||||
}
|
||||
}
|
||||
|
||||
private val deviceUserEntryLive = SingleItemLiveDataCache(appLogic.deviceUserEntry.ignoreUnchanged())
|
||||
private val isThisDeviceTheCurrentDeviceLive = SingleItemLiveDataCache(appLogic.currentDeviceLogic.isThisDeviceTheCurrentDevice)
|
||||
private val childCategories = object: MultiKeyLiveDataCache<List<Category>, String?>() {
|
||||
// key = child id
|
||||
override fun createValue(key: String?): LiveData<List<Category>> {
|
||||
if (key == null) {
|
||||
// this should rarely happen
|
||||
return liveDataFromValue(Collections.emptyList())
|
||||
} else {
|
||||
return appLogic.database.category().getCategoriesByChildId(key).ignoreUnchanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
private val appCategories = object: MultiKeyLiveDataCache<CategoryApp?, Pair<String, List<String>>>() {
|
||||
// key = package name, category ids
|
||||
override fun createValue(key: Pair<String, List<String>>): LiveData<CategoryApp?> {
|
||||
return appLogic.database.categoryApp().getCategoryApp(key.second, key.first)
|
||||
}
|
||||
}
|
||||
private val timeLimitRules = object: MultiKeyLiveDataCache<List<TimeLimitRule>, String>() {
|
||||
override fun createValue(key: String): LiveData<List<TimeLimitRule>> {
|
||||
return appLogic.database.timeLimitRules().getTimeLimitRulesByCategory(key)
|
||||
}
|
||||
}
|
||||
private val usedTimesOfCategoryAndWeekByFirstDayOfWeek = object: MultiKeyLiveDataCache<SparseArray<UsedTimeItem>, Pair<String, Int>>() {
|
||||
override fun createValue(key: Pair<String, Int>): LiveData<SparseArray<UsedTimeItem>> {
|
||||
return appLogic.database.usedTimes().getUsedTimesOfWeek(key.first, key.second)
|
||||
}
|
||||
}
|
||||
private val shouldDoAutomaticSignOut = SingleItemLiveDataCacheWithRequery { -> appLogic.defaultUserLogic.hasAutomaticSignOut()}
|
||||
|
||||
private val liveDataCaches = LiveDataCaches(arrayOf(
|
||||
deviceUserEntryLive,
|
||||
childCategories,
|
||||
appCategories,
|
||||
timeLimitRules,
|
||||
usedTimesOfCategoryAndWeekByFirstDayOfWeek,
|
||||
shouldDoAutomaticSignOut
|
||||
))
|
||||
|
||||
private var usedTimeUpdateHelper: UsedTimeItemBatchUpdateHelper? = null
|
||||
private var previousMainLogicExecutionTime = 0
|
||||
private var previousMainLoopEndTime = 0L
|
||||
|
||||
private val appTitleCache = QueryAppTitleCache(appLogic.platformIntegration)
|
||||
|
||||
private suspend fun backgroundServiceLoop() {
|
||||
val realTime = RealTime.newInstance()
|
||||
|
||||
while (true) {
|
||||
// app must be enabled
|
||||
if (!appLogic.enable.waitForNonNullValue()) {
|
||||
usedTimeUpdateHelper?.commit(appLogic)
|
||||
liveDataCaches.removeAllItems()
|
||||
appLogic.platformIntegration.setAppStatusMessage(null)
|
||||
appLogic.enable.waitUntilValueMatches { it == true }
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// device must be used by a child
|
||||
val deviceUserEntry = deviceUserEntryLive.read().waitForNullableValue()
|
||||
|
||||
if (deviceUserEntry == null || deviceUserEntry.type != UserType.Child) {
|
||||
usedTimeUpdateHelper?.commit(appLogic)
|
||||
val shouldDoAutomaticSignOut = shouldDoAutomaticSignOut.read()
|
||||
|
||||
if (shouldDoAutomaticSignOut.waitForNonNullValue()) {
|
||||
appLogic.defaultUserLogic.reportScreenOn(appLogic.platformIntegration.isScreenOn())
|
||||
|
||||
appLogic.platformIntegration.setAppStatusMessage(
|
||||
if (IsAppInForeground.isRunning())
|
||||
null
|
||||
else
|
||||
AppStatusMessage(
|
||||
title = appLogic.context.getString(R.string.background_logic_timeout_title),
|
||||
text = appLogic.context.getString(R.string.background_logic_timeout_text)
|
||||
)
|
||||
)
|
||||
|
||||
liveDataCaches.reportLoopDone()
|
||||
appLogic.timeApi.sleep(BACKGROUND_SERVICE_INTERVAL)
|
||||
} else {
|
||||
liveDataCaches.removeAllItems()
|
||||
appLogic.platformIntegration.setAppStatusMessage(null)
|
||||
|
||||
val isChildSignedIn = deviceUserEntryLive.read().map { it != null && it.type == UserType.Child }
|
||||
|
||||
isChildSignedIn.or(shouldDoAutomaticSignOut).waitUntilValueMatches { it == true }
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// loop logic
|
||||
try {
|
||||
// get the current time
|
||||
appLogic.realTimeLogic.getRealTime(realTime)
|
||||
|
||||
// get the categories
|
||||
val categories = childCategories.get(deviceUserEntry.id).waitForNonNullValue()
|
||||
val temporarilyAllowedApps = temporarilyAllowedApps.waitForNonNullValue()
|
||||
|
||||
// get the current status
|
||||
val isScreenOn = appLogic.platformIntegration.isScreenOn()
|
||||
|
||||
appLogic.defaultUserLogic.reportScreenOn(isScreenOn)
|
||||
|
||||
if (!isScreenOn) {
|
||||
if (temporarilyAllowedApps.isNotEmpty()) {
|
||||
resetTemporarilyAllowedApps()
|
||||
}
|
||||
}
|
||||
|
||||
val foregroundAppPackageName = appLogic.platformIntegration.getForegroundAppPackageName()
|
||||
// the following is not executed if the permission is missing
|
||||
|
||||
if (foregroundAppPackageName == BuildConfig.APPLICATION_ID) {
|
||||
// this app itself runs now -> no need for an status message
|
||||
usedTimeUpdateHelper?.commit(appLogic)
|
||||
appLogic.platformIntegration.setAppStatusMessage(null)
|
||||
} else if (foregroundAppPackageName != null && AndroidIntegrationApps.ignoredApps.contains(foregroundAppPackageName)) {
|
||||
usedTimeUpdateHelper?.commit(appLogic)
|
||||
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||
appTitleCache.query(foregroundAppPackageName),
|
||||
appLogic.context.getString(R.string.background_logic_whitelisted)
|
||||
))
|
||||
} else if (foregroundAppPackageName != null && temporarilyAllowedApps.contains(foregroundAppPackageName)) {
|
||||
usedTimeUpdateHelper?.commit(appLogic)
|
||||
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||
appTitleCache.query(foregroundAppPackageName),
|
||||
appLogic.context.getString(R.string.background_logic_temporarily_allowed)
|
||||
))
|
||||
} else if (foregroundAppPackageName != null) {
|
||||
val appCategory = appCategories.get(Pair(foregroundAppPackageName, categories.map { it.id })).waitForNullableValue()
|
||||
val category = categories.find { it.id == appCategory?.categoryId }
|
||||
?: categories.find { it.id == deviceUserEntry.categoryForNotAssignedApps }
|
||||
val parentCategory = categories.find { it.id == category?.parentCategoryId }
|
||||
|
||||
if (category == null) {
|
||||
usedTimeUpdateHelper?.commit(appLogic)
|
||||
|
||||
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||
title = appTitleCache.query(foregroundAppPackageName),
|
||||
text = appLogic.context.getString(R.string.background_logic_opening_lockscreen)
|
||||
))
|
||||
appLogic.platformIntegration.setSuspendedApps(listOf(foregroundAppPackageName), true)
|
||||
appLogic.platformIntegration.showAppLockScreen(foregroundAppPackageName)
|
||||
} else if (category.temporarilyBlocked or (parentCategory?.temporarilyBlocked == true)) {
|
||||
usedTimeUpdateHelper?.commit(appLogic)
|
||||
|
||||
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||
title = appTitleCache.query(foregroundAppPackageName),
|
||||
text = appLogic.context.getString(R.string.background_logic_opening_lockscreen)
|
||||
))
|
||||
appLogic.platformIntegration.showAppLockScreen(foregroundAppPackageName)
|
||||
} else {
|
||||
val nowTimestamp = realTime.timeInMillis
|
||||
val nowTimezone = TimeZone.getTimeZone(deviceUserEntry.timeZone)
|
||||
|
||||
val nowDate = DateInTimezone.newInstance(nowTimestamp, nowTimezone)
|
||||
val minuteOfWeek = getMinuteOfWeek(nowTimestamp, nowTimezone)
|
||||
|
||||
// disable time limits temporarily feature
|
||||
if (realTime.shouldTrustTimeTemporarily && nowTimestamp < deviceUserEntry.disableLimitsUntil) {
|
||||
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||
title = appTitleCache.query(foregroundAppPackageName),
|
||||
text = appLogic.context.getString(R.string.background_logic_limits_disabled)
|
||||
))
|
||||
} else if (
|
||||
// check blocked time areas
|
||||
// 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)
|
||||
) &&
|
||||
(!realTime.shouldTrustTimeTemporarily)
|
||||
)
|
||||
) {
|
||||
usedTimeUpdateHelper?.commit(appLogic)
|
||||
|
||||
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||
title = appTitleCache.query(foregroundAppPackageName),
|
||||
text = appLogic.context.getString(R.string.background_logic_opening_lockscreen)
|
||||
))
|
||||
appLogic.platformIntegration.showAppLockScreen(foregroundAppPackageName)
|
||||
} else {
|
||||
// check time limits
|
||||
val rules = timeLimitRules.get(category.id).waitForNonNullValue()
|
||||
val parentRules = parentCategory?.let {
|
||||
timeLimitRules.get(it.id).waitForNonNullValue()
|
||||
} ?: emptyList()
|
||||
|
||||
if (rules.isEmpty() and parentRules.isEmpty()) {
|
||||
// unlimited
|
||||
usedTimeUpdateHelper?.commit(appLogic)
|
||||
|
||||
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||
category.title + " - " + appTitleCache.query(foregroundAppPackageName),
|
||||
appLogic.context.getString(R.string.background_logic_no_timelimit)
|
||||
))
|
||||
} else {
|
||||
val isCurrentDevice = isThisDeviceTheCurrentDeviceLive.read().waitForNonNullValue()
|
||||
|
||||
if (!isCurrentDevice) {
|
||||
usedTimeUpdateHelper?.commit(appLogic)
|
||||
|
||||
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||
title = appTitleCache.query(foregroundAppPackageName),
|
||||
text = appLogic.context.getString(R.string.background_logic_opening_lockscreen)
|
||||
))
|
||||
appLogic.platformIntegration.showAppLockScreen(foregroundAppPackageName)
|
||||
} else if (realTime.shouldTrustTimeTemporarily) {
|
||||
val usedTimes = usedTimesOfCategoryAndWeekByFirstDayOfWeek.get(Pair(category.id, nowDate.dayOfEpoch - nowDate.dayOfWeek)).waitForNonNullValue()
|
||||
val parentUsedTimes = parentCategory?.let {
|
||||
usedTimesOfCategoryAndWeekByFirstDayOfWeek.get(Pair(it.id, nowDate.dayOfEpoch - nowDate.dayOfWeek)).waitForNonNullValue()
|
||||
} ?: SparseArray()
|
||||
|
||||
val newUsedTimeItemBatchUpdateHelper = UsedTimeItemBatchUpdateHelper.eventuallyUpdateInstance(
|
||||
date = nowDate,
|
||||
childCategoryId = category.id,
|
||||
parentCategoryId = parentCategory?.id,
|
||||
oldInstance = usedTimeUpdateHelper,
|
||||
usedTimeItemForDayChild = usedTimes.get(nowDate.dayOfWeek),
|
||||
usedTimeItemForDayParent = parentUsedTimes.get(nowDate.dayOfWeek),
|
||||
logic = appLogic
|
||||
)
|
||||
usedTimeUpdateHelper = newUsedTimeItemBatchUpdateHelper
|
||||
|
||||
fun buildUsedTimesSparseArray(items: SparseArray<UsedTimeItem>, isParentCategory: Boolean): SparseLongArray {
|
||||
val result = SparseLongArray()
|
||||
|
||||
for (i in 0..6) {
|
||||
val usedTimesItem = items[i]?.usedMillis
|
||||
|
||||
if (newUsedTimeItemBatchUpdateHelper.date.dayOfWeek == i) {
|
||||
result.put(
|
||||
i,
|
||||
if (isParentCategory)
|
||||
newUsedTimeItemBatchUpdateHelper.getTotalUsedTimeParent()
|
||||
else
|
||||
newUsedTimeItemBatchUpdateHelper.getTotalUsedTimeChild()
|
||||
)
|
||||
} else {
|
||||
result.put(i, usedTimesItem ?: 0)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
val remainingChild = RemainingTime.getRemainingTime(
|
||||
nowDate.dayOfWeek,
|
||||
buildUsedTimesSparseArray(usedTimes, isParentCategory = false),
|
||||
rules,
|
||||
Math.max(0, category.extraTimeInMillis - newUsedTimeItemBatchUpdateHelper.getCachedExtraTimeToSubtract())
|
||||
)
|
||||
|
||||
val remainingParent = parentCategory?.let {
|
||||
RemainingTime.getRemainingTime(
|
||||
nowDate.dayOfWeek,
|
||||
buildUsedTimesSparseArray(parentUsedTimes, isParentCategory = true),
|
||||
parentRules,
|
||||
Math.max(0, parentCategory.extraTimeInMillis - newUsedTimeItemBatchUpdateHelper.getCachedExtraTimeToSubtract())
|
||||
)
|
||||
}
|
||||
|
||||
val remaining = RemainingTime.min(remainingChild, remainingParent)
|
||||
|
||||
if (remaining == null) {
|
||||
// unlimited
|
||||
|
||||
usedTimeUpdateHelper?.commit(appLogic)
|
||||
|
||||
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||
category.title + " - " + appTitleCache.query(foregroundAppPackageName),
|
||||
appLogic.context.getString(R.string.background_logic_no_timelimit)
|
||||
))
|
||||
} else {
|
||||
// time limited
|
||||
if (remaining.includingExtraTime > 0) {
|
||||
if (remaining.default == 0L) {
|
||||
// using extra time
|
||||
|
||||
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||
category.title + " - " + appTitleCache.query(foregroundAppPackageName),
|
||||
appLogic.context.getString(R.string.background_logic_using_extra_time, TimeTextUtil.remaining(remaining.includingExtraTime.toInt(), appLogic.context))
|
||||
))
|
||||
|
||||
if (isScreenOn) {
|
||||
newUsedTimeItemBatchUpdateHelper.addUsedTime(
|
||||
Math.min(previousMainLogicExecutionTime, MAX_USED_TIME_PER_ROUND), // never save more than a second of used time
|
||||
true,
|
||||
appLogic
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// using normal contingent
|
||||
|
||||
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||
category.title + " - " + appTitleCache.query(foregroundAppPackageName),
|
||||
TimeTextUtil.remaining(remaining.default.toInt(), appLogic.context)
|
||||
))
|
||||
|
||||
if (isScreenOn) {
|
||||
newUsedTimeItemBatchUpdateHelper.addUsedTime(
|
||||
Math.min(previousMainLogicExecutionTime, MAX_USED_TIME_PER_ROUND), // never save more than a second of used time
|
||||
false,
|
||||
appLogic
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// there is not time anymore
|
||||
|
||||
newUsedTimeItemBatchUpdateHelper.commit(appLogic)
|
||||
|
||||
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||
title = appTitleCache.query(foregroundAppPackageName),
|
||||
text = appLogic.context.getString(R.string.background_logic_opening_lockscreen)
|
||||
))
|
||||
appLogic.platformIntegration.showAppLockScreen(foregroundAppPackageName)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// if should not trust the time temporarily
|
||||
|
||||
usedTimeUpdateHelper?.commit(appLogic)
|
||||
|
||||
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||
title = appTitleCache.query(foregroundAppPackageName),
|
||||
text = appLogic.context.getString(R.string.background_logic_opening_lockscreen)
|
||||
))
|
||||
appLogic.platformIntegration.showAppLockScreen(foregroundAppPackageName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||
appLogic.context.getString(R.string.background_logic_idle_title),
|
||||
appLogic.context.getString(R.string.background_logic_idle_text)
|
||||
))
|
||||
}
|
||||
} catch (ex: SecurityException) {
|
||||
// this is handled by an other main loop (with a delay)
|
||||
|
||||
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||
appLogic.context.getString(R.string.background_logic_error),
|
||||
appLogic.context.getString(R.string.background_logic_error_permission)
|
||||
))
|
||||
} catch (ex: Exception) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.w(LOG_TAG, "exception during running main loop", ex)
|
||||
}
|
||||
|
||||
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||
appLogic.context.getString(R.string.background_logic_error),
|
||||
appLogic.context.getString(R.string.background_logic_error_internal)
|
||||
))
|
||||
}
|
||||
|
||||
liveDataCaches.reportLoopDone()
|
||||
|
||||
// delay before running next time
|
||||
val endTime = appLogic.timeApi.getCurrentUptimeInMillis()
|
||||
previousMainLogicExecutionTime = (endTime - previousMainLoopEndTime).toInt()
|
||||
previousMainLoopEndTime = endTime
|
||||
|
||||
val timeToWait = Math.max(10, BACKGROUND_SERVICE_INTERVAL - previousMainLogicExecutionTime)
|
||||
appLogic.timeApi.sleep(timeToWait)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun syncInstalledAppVersion() {
|
||||
val currentAppVersion = BuildConfig.VERSION_CODE
|
||||
val deviceEntry = appLogic.deviceEntry.waitForNullableValue()
|
||||
|
||||
if (deviceEntry != null) {
|
||||
if (deviceEntry.currentAppVersion != currentAppVersion) {
|
||||
ApplyActionUtil.applyAppLogicAction(
|
||||
UpdateDeviceStatusAction.empty.copy(
|
||||
newAppVersion = currentAppVersion
|
||||
),
|
||||
appLogic
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun syncDeviceStatusAsync() {
|
||||
runAsync {
|
||||
syncDeviceStatus()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun syncDeviceStatusLoop() {
|
||||
while (true) {
|
||||
appLogic.deviceEntryIfEnabled.waitUntilValueMatches { it != null }
|
||||
|
||||
syncDeviceStatus()
|
||||
|
||||
appLogic.timeApi.sleep(CHECK_PERMISSION_INTERVAL)
|
||||
}
|
||||
}
|
||||
|
||||
private val syncDeviceStatusLock = Mutex()
|
||||
|
||||
fun reportDeviceReboot() {
|
||||
runAsync {
|
||||
val deviceEntry = appLogic.deviceEntry.waitForNullableValue()
|
||||
|
||||
if (deviceEntry?.considerRebootManipulation == true) {
|
||||
ApplyActionUtil.applyAppLogicAction(
|
||||
UpdateDeviceStatusAction.empty.copy(
|
||||
didReboot = true
|
||||
),
|
||||
appLogic
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun syncDeviceStatus() {
|
||||
syncDeviceStatusLock.withLock {
|
||||
val deviceEntry = appLogic.deviceEntry.waitForNullableValue()
|
||||
|
||||
if (deviceEntry != null) {
|
||||
val protectionLevel = appLogic.platformIntegration.getCurrentProtectionLevel()
|
||||
val usageStatsPermission = appLogic.platformIntegration.getForegroundAppPermissionStatus()
|
||||
val notificationAccess = appLogic.platformIntegration.getNotificationAccessPermissionStatus()
|
||||
|
||||
var changes = UpdateDeviceStatusAction.empty
|
||||
|
||||
if (protectionLevel != deviceEntry.currentProtectionLevel) {
|
||||
changes = changes.copy(
|
||||
newProtectionLevel = protectionLevel
|
||||
)
|
||||
|
||||
if (protectionLevel == ProtectionLevel.DeviceOwner) {
|
||||
appLogic.platformIntegration.setEnableSystemLockdown(true)
|
||||
}
|
||||
}
|
||||
|
||||
if (usageStatsPermission != deviceEntry.currentUsageStatsPermission) {
|
||||
changes = changes.copy(
|
||||
newUsageStatsPermissionStatus = usageStatsPermission
|
||||
)
|
||||
}
|
||||
|
||||
if (notificationAccess != deviceEntry.currentNotificationAccessPermission) {
|
||||
changes = changes.copy(
|
||||
newNotificationAccessPermission = notificationAccess
|
||||
)
|
||||
}
|
||||
|
||||
if (changes != UpdateDeviceStatusAction.empty) {
|
||||
ApplyActionUtil.applyAppLogicAction(changes, appLogic)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun resetTemporarilyAllowedApps() {
|
||||
val deviceId = appLogic.deviceId.waitForNullableValue()
|
||||
|
||||
if (deviceId != null) {
|
||||
Threads.database.executeAndWait(Runnable {
|
||||
appLogic.database.temporarilyAllowedApp().removeAllTemporarilyAllowedAppsSync(deviceId)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun backupDatabaseLoop() {
|
||||
appLogic.timeApi.sleep(1000 * 60 * 5 /* 5 minutes */)
|
||||
|
||||
while (true) {
|
||||
DatabaseBackup.with(appLogic.context).tryCreateDatabaseBackupAsync()
|
||||
|
||||
appLogic.timeApi.sleep(1000 * 60 * 60 * 3 /* 3 hours */)
|
||||
}
|
||||
}
|
||||
}
|
382
app/src/main/java/io/timelimit/android/logic/BlockingReason.kt
Normal file
382
app/src/main/java/io/timelimit/android/logic/BlockingReason.kt
Normal file
|
@ -0,0 +1,382 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.logic
|
||||
|
||||
import android.util.Log
|
||||
import android.util.SparseLongArray
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.data.model.Category
|
||||
import io.timelimit.android.data.model.TimeLimitRule
|
||||
import io.timelimit.android.data.model.User
|
||||
import io.timelimit.android.data.model.UserType
|
||||
import io.timelimit.android.date.DateInTimezone
|
||||
import io.timelimit.android.date.getMinuteOfWeek
|
||||
import io.timelimit.android.integration.platform.android.AndroidIntegrationApps
|
||||
import io.timelimit.android.integration.time.TimeApi
|
||||
import io.timelimit.android.livedata.*
|
||||
import java.util.*
|
||||
|
||||
enum class BlockingReason {
|
||||
None,
|
||||
NotPartOfAnCategory,
|
||||
TemporarilyBlocked,
|
||||
BlockedAtThisTime,
|
||||
TimeOver,
|
||||
TimeOverExtraTimeCanBeUsedLater,
|
||||
MissingNetworkTime,
|
||||
RequiresCurrentDevice
|
||||
}
|
||||
|
||||
class BlockingReasonUtil(private val appLogic: AppLogic) {
|
||||
companion object {
|
||||
private const val LOG_TAG = "BlockingReason"
|
||||
}
|
||||
|
||||
fun getBlockingReason(packageName: String): LiveData<BlockingReason> {
|
||||
// check precondition that the app is running
|
||||
|
||||
return appLogic.enable.switchMap {
|
||||
enabled ->
|
||||
|
||||
if (enabled == null || enabled == false) {
|
||||
liveDataFromValue(BlockingReason.None)
|
||||
} else {
|
||||
appLogic.deviceUserEntry.switchMap {
|
||||
user ->
|
||||
|
||||
if (user == null || user.type != UserType.Child) {
|
||||
liveDataFromValue(BlockingReason.None)
|
||||
} else {
|
||||
getBlockingReasonStep2(packageName, user, TimeZone.getTimeZone(user.timeZone))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBlockingReasonStep2(packageName: String, child: User, timeZone: TimeZone): LiveData<BlockingReason> {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "step 2")
|
||||
}
|
||||
|
||||
// check internal whitelist
|
||||
if (packageName == BuildConfig.APPLICATION_ID) {
|
||||
return liveDataFromValue(BlockingReason.None)
|
||||
} else if (AndroidIntegrationApps.ignoredApps.contains(packageName)) {
|
||||
return liveDataFromValue(BlockingReason.None)
|
||||
} else {
|
||||
return getBlockingReasonStep3(packageName, child, timeZone)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBlockingReasonStep3(packageName: String, child: User, timeZone: TimeZone): LiveData<BlockingReason> {
|
||||
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(BlockingReason.None)
|
||||
} else {
|
||||
getBlockingReasonStep4(packageName, child, timeZone)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBlockingReasonStep4(packageName: String, child: User, timeZone: TimeZone): LiveData<BlockingReason> {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "step 4")
|
||||
}
|
||||
|
||||
return appLogic.database.category().getCategoriesByChildId(child.id).switchMap {
|
||||
childCategories ->
|
||||
|
||||
Transformations.map(appLogic.database.categoryApp().getCategoryApp(childCategories.map { it.id }, packageName)) {
|
||||
categoryApp ->
|
||||
|
||||
if (categoryApp == null) {
|
||||
null
|
||||
} else {
|
||||
childCategories.find { it.id == categoryApp.categoryId }
|
||||
}
|
||||
}
|
||||
}.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(BlockingReason.NotPartOfAnCategory)
|
||||
} else {
|
||||
getBlockingReasonStep4Point5(categoryEntry2, child, timeZone, false)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
getBlockingReasonStep4Point5(categoryEntry, child, timeZone, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBlockingReasonStep4Point5(category: Category, child: User, timeZone: TimeZone, isParentCategory: Boolean): LiveData<BlockingReason> {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "step 4.5")
|
||||
}
|
||||
|
||||
if (category.temporarilyBlocked) {
|
||||
return liveDataFromValue(BlockingReason.TemporarilyBlocked)
|
||||
}
|
||||
|
||||
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 {
|
||||
getBlockingReasonStep4Point5(parentCategory, child, timeZone, true)
|
||||
}
|
||||
}
|
||||
} 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(appLogic.timeApi, timeZone)) {
|
||||
trustedMinuteOfWeek ->
|
||||
|
||||
if (category.blockedMinutesInWeek.dataNotToModify.isEmpty) {
|
||||
getBlockingReasonStep6(category, timeZone)
|
||||
} else if (trustedMinuteOfWeek == null) {
|
||||
liveDataFromValue(BlockingReason.MissingNetworkTime)
|
||||
} else if (category.blockedMinutesInWeek.read(trustedMinuteOfWeek)) {
|
||||
liveDataFromValue(BlockingReason.BlockedAtThisTime)
|
||||
} else {
|
||||
getBlockingReasonStep6(category, timeZone)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBlockingReasonStep6(category: Category, timeZone: TimeZone): LiveData<BlockingReason> {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "step 6")
|
||||
}
|
||||
|
||||
return getTrustedDateLive(appLogic.timeApi, timeZone).switchMap {
|
||||
nowTrustedDate ->
|
||||
|
||||
appLogic.database.timeLimitRules().getTimeLimitRulesByCategory(category.id).switchMap {
|
||||
rules ->
|
||||
|
||||
if (rules.isEmpty()) {
|
||||
liveDataFromValue(BlockingReason.None)
|
||||
} else if (nowTrustedDate == null) {
|
||||
liveDataFromValue(BlockingReason.MissingNetworkTime)
|
||||
} else {
|
||||
getBlockingReasonStep6(category, nowTrustedDate, rules)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBlockingReasonStep6(category: Category, nowTrustedDate: DateInTimezone, 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, rules)
|
||||
} else {
|
||||
liveDataFromValue(BlockingReason.RequiresCurrentDevice)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBlockingReasonStep7(category: Category, nowTrustedDate: DateInTimezone, rules: List<TimeLimitRule>): LiveData<BlockingReason> {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "step 7")
|
||||
}
|
||||
|
||||
return appLogic.database.usedTimes().getUsedTimesOfWeek(category.id, nowTrustedDate.dayOfEpoch - nowTrustedDate.dayOfWeek).map {
|
||||
usedTimes ->
|
||||
val usedTimesSparseArray = SparseLongArray()
|
||||
|
||||
for (i in 0..6) {
|
||||
val usedTimesItem = usedTimes[i]?.usedMillis
|
||||
usedTimesSparseArray.put(i, (if (usedTimesItem != null) usedTimesItem else 0))
|
||||
}
|
||||
|
||||
val remaining = RemainingTime.getRemainingTime(nowTrustedDate.dayOfWeek, usedTimesSparseArray, rules, category.extraTimeInMillis)
|
||||
|
||||
if (remaining == null || remaining.includingExtraTime > 0) {
|
||||
BlockingReason.None
|
||||
} else {
|
||||
if (category.extraTimeInMillis > 0) {
|
||||
BlockingReason.TimeOverExtraTimeCanBeUsedLater
|
||||
} else {
|
||||
BlockingReason.TimeOver
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTemporarilyTrustedTimeInMillis(): LiveData<Long?> {
|
||||
val realTime = RealTime.newInstance()
|
||||
|
||||
return liveDataFromFunction {
|
||||
appLogic.realTimeLogic.getRealTime(realTime)
|
||||
|
||||
if (realTime.shouldTrustTimeTemporarily) {
|
||||
realTime.timeInMillis
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTrustedMinuteOfWeekLive(api: TimeApi, timeZone: TimeZone): LiveData<Int?> {
|
||||
val realTime = RealTime.newInstance()
|
||||
|
||||
return object: LiveData<Int?>() {
|
||||
fun update() {
|
||||
appLogic.realTimeLogic.getRealTime(realTime)
|
||||
|
||||
if (realTime.shouldTrustTimeTemporarily) {
|
||||
value = getMinuteOfWeek(realTime.timeInMillis, timeZone)
|
||||
} else {
|
||||
value = null
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
update()
|
||||
}
|
||||
|
||||
val scheduledUpdateRunnable = Runnable {
|
||||
update()
|
||||
scheduleUpdate()
|
||||
}
|
||||
|
||||
fun scheduleUpdate() {
|
||||
api.runDelayed(scheduledUpdateRunnable, 1000L /* every second */)
|
||||
}
|
||||
|
||||
fun cancelScheduledUpdate() {
|
||||
api.cancelScheduledAction(scheduledUpdateRunnable)
|
||||
}
|
||||
|
||||
override fun onActive() {
|
||||
super.onActive()
|
||||
|
||||
update()
|
||||
scheduleUpdate()
|
||||
}
|
||||
|
||||
override fun onInactive() {
|
||||
super.onInactive()
|
||||
|
||||
cancelScheduledUpdate()
|
||||
}
|
||||
}.ignoreUnchanged()
|
||||
}
|
||||
|
||||
private fun getTrustedDateLive(api: TimeApi, 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() {
|
||||
api.runDelayed(scheduledUpdateRunnable, 1000L /* every second */)
|
||||
}
|
||||
|
||||
fun cancelScheduledUpdate() {
|
||||
api.cancelScheduledAction(scheduledUpdateRunnable)
|
||||
}
|
||||
|
||||
override fun onActive() {
|
||||
super.onActive()
|
||||
|
||||
update()
|
||||
scheduleUpdate()
|
||||
}
|
||||
|
||||
override fun onInactive() {
|
||||
super.onInactive()
|
||||
|
||||
cancelScheduledUpdate()
|
||||
}
|
||||
}.ignoreUnchanged()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.logic
|
||||
|
||||
import io.timelimit.android.data.model.Device
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
private val userDeviceEntries = appLogic.deviceUserId.switchMap { deviceUserId ->
|
||||
if (deviceUserId == "") {
|
||||
liveDataFromValue(emptyList())
|
||||
} else {
|
||||
appLogic.database.device().getDevicesByUserId(deviceUserId)
|
||||
}
|
||||
}
|
||||
|
||||
private val otherUserDeviceEntries = appLogic.deviceEntry.switchMap { ownDeviceEntry ->
|
||||
userDeviceEntries.map { devices ->
|
||||
devices.filterNot { device -> device.id == ownDeviceEntry?.id }
|
||||
}
|
||||
}
|
||||
|
||||
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?)
|
||||
} else {
|
||||
otherUserDeviceEntries.map { otherDeviceEntries ->
|
||||
otherDeviceEntries.find { it.id == userEntry.currentDevice }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.logic
|
||||
|
||||
import android.content.Context
|
||||
|
||||
object DefaultAppLogic {
|
||||
private var instance: AppLogic? = null
|
||||
|
||||
fun with(context: Context): AppLogic {
|
||||
if (instance == null) {
|
||||
instance = AndroidAppLogic.with(context)
|
||||
}
|
||||
|
||||
return instance!!
|
||||
}
|
||||
}
|
186
app/src/main/java/io/timelimit/android/logic/DefaultUserLogic.kt
Normal file
186
app/src/main/java/io/timelimit/android/logic/DefaultUserLogic.kt
Normal file
|
@ -0,0 +1,186 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.logic
|
||||
|
||||
import android.util.Log
|
||||
import io.timelimit.android.BuildConfig
|
||||
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.livedata.*
|
||||
import io.timelimit.android.sync.actions.SignOutAtDeviceAction
|
||||
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
class DefaultUserLogic(private val appLogic: AppLogic) {
|
||||
companion object {
|
||||
private const val LOG_TAG = "DefaultUserLogic"
|
||||
}
|
||||
|
||||
private fun defaultUserEntry() = appLogic.deviceEntry.map { device ->
|
||||
device?.defaultUser
|
||||
}.ignoreUnchanged().switchMap {
|
||||
if (it != null)
|
||||
appLogic.database.user().getUserByIdLive(it)
|
||||
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()
|
||||
|
||||
private var lastScreenOnStatus = false
|
||||
private var lastScreenDisableTime = 0L
|
||||
private var lastScreenOnSaveTime = 0L
|
||||
private var restoredLastScreenOnTime: Long? = null
|
||||
private var didRestoreLastDisabledTime = false
|
||||
|
||||
fun reportScreenOn(isScreenOn: Boolean) {
|
||||
if (isScreenOn) {
|
||||
val now = appLogic.timeApi.getCurrentTimeInMillis()
|
||||
|
||||
if (lastScreenOnSaveTime + 1000 * 30 < now) {
|
||||
lastScreenOnSaveTime = now
|
||||
|
||||
Threads.database.submit {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "save last screen on time")
|
||||
}
|
||||
|
||||
if (restoredLastScreenOnTime == null) {
|
||||
restoredLastScreenOnTime = appLogic.database.config().getLastScreenOnTime()
|
||||
}
|
||||
|
||||
appLogic.database.config().setLastScreenOnTime(now)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isScreenOn != lastScreenOnStatus) {
|
||||
lastScreenOnStatus = isScreenOn
|
||||
|
||||
if (isScreenOn) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "screen was enabled")
|
||||
}
|
||||
|
||||
runAsync {
|
||||
logoutLock.withLock {
|
||||
if (lastScreenDisableTime == 0L) {
|
||||
if (!didRestoreLastDisabledTime) {
|
||||
didRestoreLastDisabledTime = true
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "screen disabling time is not known - try to restore time")
|
||||
}
|
||||
|
||||
val nowTime = appLogic.timeApi.getCurrentTimeInMillis()
|
||||
val nowUptime = appLogic.timeApi.getCurrentUptimeInMillis()
|
||||
val savedLastScreenOnTime = restoredLastScreenOnTime ?: kotlin.run {
|
||||
Threads.database.executeAndWait {
|
||||
restoredLastScreenOnTime = appLogic.database.config().getLastScreenOnTime()
|
||||
}
|
||||
|
||||
restoredLastScreenOnTime!!
|
||||
}
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "now: $nowTime; uptime: $nowUptime; last screen on time: $savedLastScreenOnTime")
|
||||
}
|
||||
|
||||
if (savedLastScreenOnTime == 0L) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "no saved value - can not restore")
|
||||
}
|
||||
} else if (savedLastScreenOnTime > nowTime) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "saved last screen on time is in the future - can not restore")
|
||||
}
|
||||
} else {
|
||||
val diffToNow = nowTime - savedLastScreenOnTime
|
||||
val theoreticallyUptimeValue = nowUptime - diffToNow
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "restored last screen on time: diff to now: ${diffToNow / 1000} s; theoretically uptime: ${theoreticallyUptimeValue / 1000} s")
|
||||
}
|
||||
|
||||
lastScreenDisableTime = theoreticallyUptimeValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lastScreenDisableTime != 0L) {
|
||||
val now = appLogic.timeApi.getCurrentUptimeInMillis()
|
||||
val diff = now - lastScreenDisableTime
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "screen was disabled for ${diff / 1000} seconds")
|
||||
}
|
||||
|
||||
val defaultUser = defaultUserEntry().waitForNullableValue()
|
||||
|
||||
if (defaultUser != null) {
|
||||
if (appLogic.deviceEntry.waitForNullableValue()?.currentUserId == defaultUser.id) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "default user already signed in")
|
||||
}
|
||||
} else {
|
||||
val timeout = defaultUserTimeout().waitForNonNullValue()
|
||||
|
||||
if (diff >= timeout && timeout != 0) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "much time - log out")
|
||||
}
|
||||
|
||||
if (appLogic.fullVersion.shouldProvideFullVersionFunctions.waitForNonNullValue()) {
|
||||
ApplyActionUtil.applyAppLogicAction(
|
||||
appLogic = appLogic,
|
||||
action = SignOutAtDeviceAction
|
||||
)
|
||||
} else {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "has not full version - cancel")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "no reason to log out")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "has no default user")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "screen was disabled")
|
||||
}
|
||||
|
||||
lastScreenDisableTime = appLogic.timeApi.getCurrentUptimeInMillis()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.logic
|
||||
|
||||
import io.timelimit.android.livedata.ignoreUnchanged
|
||||
import io.timelimit.android.livedata.map
|
||||
import io.timelimit.android.livedata.or
|
||||
|
||||
class FullVersionLogic(logic: AppLogic) {
|
||||
private val hasFullVersion = logic.database.config().getFullVersionUntilAsync().map { it != 0L }.ignoreUnchanged()
|
||||
val isLocalMode = logic.database.config().getDeviceAuthTokenAsync().map { it == "" }
|
||||
|
||||
val shouldProvideFullVersionFunctions = hasFullVersion.or(isLocalMode)
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.logic
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import io.timelimit.android.async.Threads
|
||||
import io.timelimit.android.coroutines.executeAndWait
|
||||
import io.timelimit.android.coroutines.runAsync
|
||||
import io.timelimit.android.data.transaction
|
||||
import io.timelimit.android.ui.MainActivity
|
||||
import io.timelimit.android.ui.manipulation.UnlockAfterManipulationActivity
|
||||
|
||||
class ManipulationLogic(val appLogic: AppLogic) {
|
||||
companion object {
|
||||
private const val LOG_TAG = "ManipulationLogic"
|
||||
}
|
||||
|
||||
init {
|
||||
runAsync {
|
||||
Threads.database.executeAndWait {
|
||||
if (appLogic.database.config().wasDeviceLockedSync()) {
|
||||
showManipulationScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun lockDeviceSync() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
if (appLogic.platformIntegration.setLockTaskPackages(listOf(appLogic.context.packageName))) {
|
||||
appLogic.database.config().setWasDeviceLockedSync(true)
|
||||
|
||||
showManipulationScreen()
|
||||
}
|
||||
} else {
|
||||
if (lockDeviceSync("12timelimit34")) {
|
||||
appLogic.database.config().setWasDeviceLockedSync(true)
|
||||
|
||||
showManipulationScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun lockDeviceSync(password: String) = appLogic.platformIntegration.trySetLockScreenPassword(password)
|
||||
|
||||
private fun showManipulationScreen() {
|
||||
appLogic.context.startActivity(
|
||||
Intent(appLogic.context, UnlockAfterManipulationActivity::class.java)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
)
|
||||
}
|
||||
|
||||
fun showManipulationUnlockedScreen() {
|
||||
appLogic.context.startActivity(
|
||||
Intent(appLogic.context, MainActivity::class.java)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
)
|
||||
}
|
||||
|
||||
fun unlockDeviceSync() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
appLogic.database.config().setWasDeviceLockedSync(false)
|
||||
} else {
|
||||
if (lockDeviceSync("")) {
|
||||
appLogic.database.config().setWasDeviceLockedSync(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun reportManualUnlock() {
|
||||
Threads.database.execute {
|
||||
appLogic.database.transaction().use { transaction ->
|
||||
if (appLogic.database.config().getOwnDeviceIdSync() != null) {
|
||||
if (appLogic.database.config().wasDeviceLockedSync()) {
|
||||
appLogic.database.config().setWasDeviceLockedSync(false)
|
||||
|
||||
showManipulationUnlockedScreen()
|
||||
}
|
||||
|
||||
transaction.setSuccess()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.logic
|
||||
|
||||
import io.timelimit.android.integration.platform.PlatformIntegration
|
||||
|
||||
class QueryAppTitleCache(val platformIntegration: PlatformIntegration) {
|
||||
private var lastPackageName: String? = null
|
||||
private var lastAppTitle: String? = null
|
||||
|
||||
fun query(packageName: String): String {
|
||||
if (packageName == lastPackageName) {
|
||||
return lastAppTitle!!
|
||||
} else {
|
||||
val title = platformIntegration.getLocalAppTitle(packageName)
|
||||
|
||||
lastAppTitle = when {
|
||||
title != null -> title
|
||||
else -> packageName
|
||||
}
|
||||
lastPackageName = packageName
|
||||
|
||||
return lastAppTitle!!
|
||||
}
|
||||
}
|
||||
}
|
207
app/src/main/java/io/timelimit/android/logic/RealTimeLogic.kt
Normal file
207
app/src/main/java/io/timelimit/android/logic/RealTimeLogic.kt
Normal file
|
@ -0,0 +1,207 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.logic
|
||||
|
||||
import android.util.Log
|
||||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.coroutines.runAsync
|
||||
import io.timelimit.android.data.model.NetworkTime
|
||||
import io.timelimit.android.livedata.ignoreUnchanged
|
||||
import io.timelimit.android.livedata.map
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import java.io.IOException
|
||||
|
||||
class RealTimeLogic(private val appLogic: AppLogic) {
|
||||
companion object {
|
||||
private const val LOG_TAG = "RealTimeLogic"
|
||||
}
|
||||
|
||||
private val deviceEntry = appLogic.deviceEntryIfEnabled
|
||||
val shouldQueryTime = deviceEntry.map {
|
||||
it != null &&
|
||||
(it.networkTime == NetworkTime.Enabled || it.networkTime == NetworkTime.IfPossible)
|
||||
}.ignoreUnchanged()
|
||||
|
||||
init {
|
||||
deviceEntry.ignoreUnchanged().observeForever {
|
||||
// this keeps the value fresh
|
||||
}
|
||||
|
||||
shouldQueryTime.observeForever {
|
||||
if (it != null && it) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "shouldQueryTime = true")
|
||||
}
|
||||
|
||||
requireRemoteTimeUptime = appLogic.timeApi.getCurrentUptimeInMillis()
|
||||
tryQueryTime()
|
||||
} else {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "shouldQueryTime = false")
|
||||
}
|
||||
|
||||
appLogic.timeApi.cancelScheduledAction(tryQueryTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var lastSuccessfullyTimeRequestUptime: Long? = null
|
||||
private var uptimeRealTimeOffset: Long? = null
|
||||
private var requireRemoteTimeUptime: Long = 0
|
||||
private var confirmedUptimeSystemTimeOffset: Long? = null
|
||||
|
||||
private val queryTimeLock = Mutex()
|
||||
private val tryQueryTime = Runnable { tryQueryTime() }
|
||||
|
||||
fun tryQueryTime() {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "tryQueryTime")
|
||||
}
|
||||
|
||||
runAsync {
|
||||
val owner = Object()
|
||||
|
||||
if (queryTimeLock.tryLock(owner)) {
|
||||
appLogic.timeApi.cancelScheduledAction(tryQueryTime)
|
||||
|
||||
try {
|
||||
val server = appLogic.serverLogic.getServerConfigCoroutine()
|
||||
|
||||
if (!server.isAppEnabled) {
|
||||
throw IOException("app during setup - time queries disabled")
|
||||
}
|
||||
|
||||
val uptimeBefore = appLogic.timeApi.getCurrentUptimeInMillis()
|
||||
val serverTime = server.api.getTimeInMillis()
|
||||
val uptime = appLogic.timeApi.getCurrentUptimeInMillis()
|
||||
|
||||
val uptimeOffset = uptime - uptimeBefore
|
||||
|
||||
if (uptimeOffset > 30 * 1000 /* 30 seconds */) {
|
||||
throw IOException("time request took too long")
|
||||
}
|
||||
|
||||
uptimeRealTimeOffset = serverTime - uptime
|
||||
lastSuccessfullyTimeRequestUptime = uptime
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "tryQueryTime was successfully in $uptimeOffset ms")
|
||||
}
|
||||
|
||||
// schedule refresh in 2 hours
|
||||
appLogic.timeApi.runDelayed(tryQueryTime, 1000 * 60 * 60 * 2)
|
||||
} catch (ex: Exception) {
|
||||
if (uptimeRealTimeOffset == null) {
|
||||
// schedule next attempt in 10 seconds
|
||||
appLogic.timeApi.runDelayed(tryQueryTime, 1000 * 10)
|
||||
} else {
|
||||
// schedule next attempt in 10 minutes
|
||||
appLogic.timeApi.runDelayed(tryQueryTime, 1000 * 60 * 10)
|
||||
}
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "tryQueryTime failed")
|
||||
}
|
||||
} finally {
|
||||
queryTimeLock.unlock(owner)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun confirmLocalTime() {
|
||||
val uptime = appLogic.timeApi.getCurrentUptimeInMillis()
|
||||
val systemTime = appLogic.timeApi.getCurrentTimeInMillis()
|
||||
|
||||
confirmedUptimeSystemTimeOffset = systemTime - uptime
|
||||
}
|
||||
|
||||
fun getRealTime(time: RealTime) {
|
||||
val systemUptime = appLogic.timeApi.getCurrentUptimeInMillis()
|
||||
val systemTime = appLogic.timeApi.getCurrentTimeInMillis()
|
||||
|
||||
val uptimeRealTimeOffset = uptimeRealTimeOffset
|
||||
val confirmedUptimeSystemTimeOffset = confirmedUptimeSystemTimeOffset
|
||||
|
||||
val deviceConfig = deviceEntry.value
|
||||
|
||||
if (deviceConfig == null) {
|
||||
time.timeInMillis = systemTime
|
||||
time.shouldTrustTimeTemporarily = true
|
||||
time.shouldTrustTimePermanently = false
|
||||
} else if (deviceConfig.networkTime == NetworkTime.Disabled) {
|
||||
time.timeInMillis = systemTime
|
||||
time.shouldTrustTimeTemporarily = true
|
||||
time.shouldTrustTimePermanently = true
|
||||
} else if (deviceConfig.networkTime == NetworkTime.IfPossible) {
|
||||
if (uptimeRealTimeOffset != null) {
|
||||
time.timeInMillis = systemUptime + uptimeRealTimeOffset
|
||||
time.shouldTrustTimeTemporarily = true
|
||||
time.shouldTrustTimePermanently = true
|
||||
} else {
|
||||
time.timeInMillis = systemTime
|
||||
time.shouldTrustTimeTemporarily = true
|
||||
time.shouldTrustTimePermanently = true
|
||||
}
|
||||
} else if (deviceConfig.networkTime == NetworkTime.Enabled) {
|
||||
if (uptimeRealTimeOffset != null) {
|
||||
time.timeInMillis = systemUptime + uptimeRealTimeOffset
|
||||
time.shouldTrustTimeTemporarily = true
|
||||
time.shouldTrustTimePermanently = true
|
||||
} else if (confirmedUptimeSystemTimeOffset != null) {
|
||||
time.timeInMillis = systemUptime + confirmedUptimeSystemTimeOffset
|
||||
time.shouldTrustTimeTemporarily = true
|
||||
time.shouldTrustTimePermanently = false
|
||||
} else {
|
||||
time.timeInMillis = systemTime
|
||||
// 5 seconds grace period
|
||||
time.shouldTrustTimeTemporarily = requireRemoteTimeUptime + 5000 > systemUptime
|
||||
time.shouldTrustTimePermanently = false
|
||||
}
|
||||
} else {
|
||||
throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
private val temp = RealTime.newInstance()
|
||||
|
||||
fun getCurrentTimeInMillis(): Long {
|
||||
getRealTime(temp)
|
||||
|
||||
return temp.timeInMillis
|
||||
}
|
||||
|
||||
val durationSinceLastSuccessfullyTimeSync: Long?
|
||||
get() {
|
||||
val now = appLogic.timeApi.getCurrentUptimeInMillis()
|
||||
val last = lastSuccessfullyTimeRequestUptime
|
||||
|
||||
return if (last == null)
|
||||
null
|
||||
else
|
||||
now - last
|
||||
}
|
||||
}
|
||||
|
||||
data class RealTime(
|
||||
var timeInMillis: Long,
|
||||
var shouldTrustTimeTemporarily: Boolean,
|
||||
var shouldTrustTimePermanently: Boolean
|
||||
) {
|
||||
companion object {
|
||||
fun newInstance() = RealTime(0, false, false)
|
||||
}
|
||||
}
|
100
app/src/main/java/io/timelimit/android/logic/RemainingTime.kt
Normal file
100
app/src/main/java/io/timelimit/android/logic/RemainingTime.kt
Normal file
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.logic
|
||||
|
||||
import android.util.SparseLongArray
|
||||
import io.timelimit.android.data.model.TimeLimitRule
|
||||
|
||||
data class RemainingTime(val includingExtraTime: Long, val default: Long) {
|
||||
init {
|
||||
if (includingExtraTime < 0 || default < 0) {
|
||||
throw IllegalStateException("time is < 0")
|
||||
}
|
||||
|
||||
if (includingExtraTime < default) {
|
||||
throw IllegalStateException("extra time < default time")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun min(a: RemainingTime?, b: RemainingTime?): RemainingTime? = if (a == null) {
|
||||
b
|
||||
} else if (b == null) {
|
||||
a
|
||||
} else {
|
||||
RemainingTime(
|
||||
includingExtraTime = Math.min(a.includingExtraTime, b.includingExtraTime),
|
||||
default = Math.min(a.default, b.default)
|
||||
)
|
||||
}
|
||||
|
||||
private fun getRulesRelatedToDay(dayOfWeek: Int, rules: List<TimeLimitRule>): List<TimeLimitRule> {
|
||||
return rules.filter { (it.dayMask.toInt() and (1 shl dayOfWeek)) != 0 }
|
||||
}
|
||||
|
||||
fun getRemainingTime(dayOfWeek: Int, usedTimes: SparseLongArray, rules: List<TimeLimitRule>, extraTime: Long): RemainingTime? {
|
||||
if (extraTime < 0) {
|
||||
throw IllegalStateException("extra time < 0")
|
||||
}
|
||||
|
||||
val relatedRules = getRulesRelatedToDay(dayOfWeek, rules)
|
||||
val withoutExtraTime = getRemainingTime(usedTimes, relatedRules, false)
|
||||
val withExtraTime = getRemainingTime(usedTimes, relatedRules, true)
|
||||
|
||||
if (withoutExtraTime == null && withExtraTime == null) {
|
||||
// no rules
|
||||
return null
|
||||
} else if (withoutExtraTime != null && withExtraTime != null) {
|
||||
// with rules for extra time
|
||||
val additionalTimeWithExtraTime = withExtraTime - withoutExtraTime
|
||||
|
||||
if (additionalTimeWithExtraTime < 0) {
|
||||
throw IllegalStateException("additional time with extra time < 0")
|
||||
}
|
||||
|
||||
return RemainingTime(
|
||||
includingExtraTime = withoutExtraTime + Math.min(extraTime, additionalTimeWithExtraTime),
|
||||
default = withoutExtraTime
|
||||
)
|
||||
} else if (withoutExtraTime != null) {
|
||||
// without rules for extra time
|
||||
return RemainingTime(
|
||||
includingExtraTime = withoutExtraTime + extraTime,
|
||||
default = withoutExtraTime
|
||||
)
|
||||
} else {
|
||||
throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getRemainingTime(usedTimes: SparseLongArray, relatedRules: List<TimeLimitRule>, assumeMaximalExtraTime: Boolean): Long? {
|
||||
return relatedRules.filter { (!assumeMaximalExtraTime) || it.applyToExtraTimeUsage }.map {
|
||||
var usedTime = 0L
|
||||
|
||||
for (day in 0..6) {
|
||||
if ((it.dayMask.toInt() and (1 shl day)) != 0) {
|
||||
usedTime += usedTimes[day]
|
||||
}
|
||||
}
|
||||
|
||||
val maxTime = it.maximumTimeInMillis
|
||||
val remaining = Math.max(0, maxTime - usedTime)
|
||||
|
||||
remaining
|
||||
}.min()
|
||||
}
|
||||
}
|
||||
}
|
67
app/src/main/java/io/timelimit/android/logic/ServerLogic.kt
Normal file
67
app/src/main/java/io/timelimit/android/logic/ServerLogic.kt
Normal file
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.logic
|
||||
|
||||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.async.Threads
|
||||
import io.timelimit.android.coroutines.executeAndWait
|
||||
import io.timelimit.android.sync.network.api.ServerApi
|
||||
|
||||
class ServerLogic(private val appLogic: AppLogic) {
|
||||
companion object {
|
||||
private fun getServerUrlFromCustomServerUrl(customServerUrl: String) = customServerUrl.let { savedUrl ->
|
||||
if (savedUrl.isEmpty()) {
|
||||
BuildConfig.serverUrl
|
||||
} else {
|
||||
savedUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getServerFromCustomServerUrl(customServerUrl: String) = appLogic.serverCreator(getServerUrlFromCustomServerUrl(customServerUrl))
|
||||
|
||||
suspend fun getServerConfigCoroutine(): ServerConfig {
|
||||
return Threads.database.executeAndWait {
|
||||
appLogic.database.beginTransaction()
|
||||
|
||||
try {
|
||||
val customServerUrl = appLogic.database.config().getCustomServerUrlSync()
|
||||
val deviceAuthToken = appLogic.database.config().getDeviceAuthTokenSync()
|
||||
val isAppEnabled = appLogic.database.config().getOwnDeviceIdSync() != null
|
||||
|
||||
ServerConfig(
|
||||
customServerUrl = customServerUrl,
|
||||
deviceAuthToken = deviceAuthToken,
|
||||
isAppEnabled = isAppEnabled,
|
||||
serverLogic = this
|
||||
)
|
||||
} finally {
|
||||
appLogic.database.endTransaction()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class ServerConfig(
|
||||
val customServerUrl: String,
|
||||
val deviceAuthToken: String,
|
||||
val isAppEnabled: Boolean,
|
||||
private val serverLogic: ServerLogic
|
||||
) {
|
||||
val hasAuthToken = deviceAuthToken != ""
|
||||
val serverUrl = ServerLogic.getServerUrlFromCustomServerUrl(customServerUrl)
|
||||
val api: ServerApi by lazy { serverLogic.getServerFromCustomServerUrl(serverUrl) }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.logic
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import io.timelimit.android.coroutines.runAsyncExpectForever
|
||||
import io.timelimit.android.data.model.UserType
|
||||
import io.timelimit.android.livedata.*
|
||||
import io.timelimit.android.sync.actions.AddInstalledAppsAction
|
||||
import io.timelimit.android.sync.actions.InstalledApp
|
||||
import io.timelimit.android.sync.actions.RemoveInstalledAppsAction
|
||||
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
class SyncInstalledAppsLogic(val appLogic: AppLogic) {
|
||||
private val doSyncLock = Mutex()
|
||||
private var requestSync = MutableLiveData<Boolean>().apply { value = false }
|
||||
|
||||
private fun requestSync() {
|
||||
requestSync.value = true
|
||||
}
|
||||
|
||||
init {
|
||||
appLogic.platformIntegration.installedAppsChangeListener = Runnable { requestSync() }
|
||||
appLogic.deviceEntryIfEnabled.map { it?.id + it?.currentUserId }.ignoreUnchanged().observeForever { requestSync() }
|
||||
|
||||
runAsyncExpectForever { syncLoop() }
|
||||
}
|
||||
|
||||
private suspend fun syncLoop() {
|
||||
while (true) {
|
||||
requestSync.waitUntilValueMatches { it == true }
|
||||
requestSync.value = false
|
||||
|
||||
doSyncNow()
|
||||
|
||||
// maximal 1 time per 5 seconds
|
||||
appLogic.timeApi.sleep(5 * 1000)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun doSyncNow() {
|
||||
doSyncLock.withLock {
|
||||
val deviceEntry = appLogic.deviceEntryIfEnabled.waitForNullableValue()
|
||||
|
||||
if (deviceEntry == null) {
|
||||
return
|
||||
}
|
||||
|
||||
val userEntry = appLogic.deviceUserEntry.waitForNullableValue()
|
||||
|
||||
if (userEntry == null || userEntry.type != UserType.Child) {
|
||||
return@withLock
|
||||
}
|
||||
|
||||
val deviceId = deviceEntry.id
|
||||
|
||||
val currentlyInstalled = appLogic.platformIntegration.getLocalApps(deviceId = deviceId).associateBy { app -> app.packageName }
|
||||
val currentlySaved = appLogic.database.app().getAppsByDeviceIdAsync(deviceId = deviceId).waitForNonNullValue().associateBy { app -> app.packageName }
|
||||
|
||||
// skip all items for removal which are still saved locally
|
||||
val itemsToRemove = HashMap(currentlySaved)
|
||||
currentlyInstalled.forEach { (packageName, _) -> itemsToRemove.remove(packageName) }
|
||||
|
||||
// only add items which are not the same locally
|
||||
val itemsToAdd = currentlyInstalled.filter { (packageName, app) -> currentlySaved[packageName] != app }
|
||||
|
||||
// save the changes
|
||||
if (itemsToRemove.isNotEmpty()) {
|
||||
ApplyActionUtil.applyAppLogicAction(
|
||||
RemoveInstalledAppsAction(packageNames = itemsToRemove.keys.toList()),
|
||||
appLogic
|
||||
)
|
||||
}
|
||||
|
||||
if (itemsToAdd.isNotEmpty()) {
|
||||
ApplyActionUtil.applyAppLogicAction(
|
||||
AddInstalledAppsAction(
|
||||
apps = itemsToAdd.map {
|
||||
(_, app) ->
|
||||
|
||||
InstalledApp(
|
||||
packageName = app.packageName,
|
||||
title = app.title,
|
||||
recommendation = app.recommendation,
|
||||
isLaunchable = app.isLaunchable
|
||||
)
|
||||
}
|
||||
),
|
||||
appLogic
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
46
app/src/main/java/io/timelimit/android/logic/TestAppLogic.kt
Normal file
46
app/src/main/java/io/timelimit/android/logic/TestAppLogic.kt
Normal file
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.logic
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import io.timelimit.android.data.RoomDatabase
|
||||
import io.timelimit.android.integration.platform.ProtectionLevel
|
||||
import io.timelimit.android.integration.platform.dummy.DummyIntegration
|
||||
import io.timelimit.android.integration.time.DummyTimeApi
|
||||
import io.timelimit.android.livedata.liveDataFromValue
|
||||
import io.timelimit.android.sync.network.api.DummyServerApi
|
||||
import io.timelimit.android.sync.websocket.DummyWebsocketClient
|
||||
import io.timelimit.android.sync.websocket.NetworkStatus
|
||||
|
||||
class TestAppLogic(maximumProtectionLevel: ProtectionLevel, context: Context) {
|
||||
val platformIntegration = DummyIntegration(maximumProtectionLevel)
|
||||
val timeApi = DummyTimeApi(100)
|
||||
val database = RoomDatabase.createInMemoryInstance(context)
|
||||
val server = DummyServerApi()
|
||||
val networkStatus = MutableLiveData<NetworkStatus>().apply { value = NetworkStatus.Offline }
|
||||
|
||||
val logic = AppLogic(
|
||||
platformIntegration = platformIntegration,
|
||||
timeApi = timeApi,
|
||||
database = database,
|
||||
serverCreator = { _ -> server },
|
||||
networkStatus = networkStatus,
|
||||
websocketClientCreator = DummyWebsocketClient.creator,
|
||||
context = context,
|
||||
isInitialized = liveDataFromValue(true)
|
||||
)
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.logic
|
||||
|
||||
import io.timelimit.android.data.Database
|
||||
import io.timelimit.android.data.model.UsedTimeItem
|
||||
import io.timelimit.android.date.DateInTimezone
|
||||
import io.timelimit.android.livedata.waitForNullableValue
|
||||
import io.timelimit.android.sync.actions.AddUsedTimeAction
|
||||
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
||||
|
||||
class UsedTimeItemBatchUpdateHelper(
|
||||
val date: DateInTimezone,
|
||||
val childCategoryId: String,
|
||||
val parentCategoryId: String?,
|
||||
var cachedItemChild: UsedTimeItem?,
|
||||
var cachedItemParent: UsedTimeItem?
|
||||
) {
|
||||
companion object {
|
||||
suspend fun eventuallyUpdateInstance(
|
||||
date: DateInTimezone,
|
||||
childCategoryId: String,
|
||||
parentCategoryId: String?,
|
||||
oldInstance: UsedTimeItemBatchUpdateHelper?,
|
||||
usedTimeItemForDayChild: UsedTimeItem?,
|
||||
usedTimeItemForDayParent: UsedTimeItem?,
|
||||
logic: AppLogic
|
||||
): UsedTimeItemBatchUpdateHelper {
|
||||
if (
|
||||
oldInstance != null &&
|
||||
oldInstance.date == date &&
|
||||
oldInstance.childCategoryId == childCategoryId &&
|
||||
oldInstance.parentCategoryId == parentCategoryId
|
||||
) {
|
||||
if (oldInstance.cachedItemChild != usedTimeItemForDayChild) {
|
||||
oldInstance.cachedItemChild = usedTimeItemForDayChild
|
||||
}
|
||||
|
||||
if (oldInstance.cachedItemParent != usedTimeItemForDayParent) {
|
||||
oldInstance.cachedItemParent = usedTimeItemForDayParent
|
||||
}
|
||||
|
||||
return oldInstance
|
||||
} else {
|
||||
if (oldInstance != null) {
|
||||
oldInstance.commit(logic)
|
||||
}
|
||||
|
||||
return UsedTimeItemBatchUpdateHelper(
|
||||
date = date,
|
||||
childCategoryId = childCategoryId,
|
||||
parentCategoryId = parentCategoryId,
|
||||
cachedItemChild = usedTimeItemForDayChild,
|
||||
cachedItemParent = usedTimeItemForDayParent
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var timeToAdd = 0
|
||||
private var extraTimeToSubtract = 0
|
||||
|
||||
suspend fun addUsedTime(time: Int, subtractExtraTime: Boolean, appLogic: AppLogic) {
|
||||
timeToAdd += time
|
||||
|
||||
if (subtractExtraTime) {
|
||||
extraTimeToSubtract += time
|
||||
}
|
||||
|
||||
if (Math.max(timeToAdd, extraTimeToSubtract) > 1000 * 10 /* 10 seconds */) {
|
||||
commit(appLogic)
|
||||
}
|
||||
}
|
||||
|
||||
fun getTotalUsedTimeChild(): Long = (cachedItemChild?.usedMillis ?: 0) + timeToAdd
|
||||
fun getTotalUsedTimeParent(): Long = (cachedItemParent?.usedMillis ?: 0) + timeToAdd
|
||||
|
||||
fun getCachedExtraTimeToSubtract(): Int {
|
||||
return extraTimeToSubtract
|
||||
}
|
||||
|
||||
suspend fun queryCurrentStatusFromDatabase(database: Database) {
|
||||
cachedItemChild = database.usedTimes().getUsedTimeItem(childCategoryId, date.dayOfEpoch).waitForNullableValue()
|
||||
cachedItemParent = parentCategoryId?.let {
|
||||
database.usedTimes().getUsedTimeItem(parentCategoryId, date.dayOfEpoch).waitForNullableValue()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun commit(logic: AppLogic) {
|
||||
if (timeToAdd == 0) {
|
||||
// do nothing
|
||||
} else {
|
||||
ApplyActionUtil.applyAppLogicAction(
|
||||
AddUsedTimeAction(
|
||||
categoryId = childCategoryId,
|
||||
timeToAdd = timeToAdd,
|
||||
dayOfEpoch = date.dayOfEpoch,
|
||||
extraTimeToSubtract = extraTimeToSubtract
|
||||
),
|
||||
logic
|
||||
)
|
||||
|
||||
timeToAdd = 0
|
||||
extraTimeToSubtract = 0
|
||||
|
||||
queryCurrentStatusFromDatabase(logic.database)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,204 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.logic
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.coroutines.runAsync
|
||||
import io.timelimit.android.coroutines.runAsyncExpectForever
|
||||
import io.timelimit.android.data.model.UserType
|
||||
import io.timelimit.android.livedata.*
|
||||
import io.timelimit.android.sync.websocket.NetworkStatus
|
||||
import io.timelimit.android.sync.websocket.WebsocketClient
|
||||
import io.timelimit.android.sync.websocket.WebsocketClientCreator
|
||||
import io.timelimit.android.sync.websocket.WebsocketClientListener
|
||||
import io.timelimit.android.ui.IsAppInForeground
|
||||
import io.timelimit.android.ui.manage.child.primarydevice.UpdatePrimaryDeviceModel
|
||||
|
||||
class WebsocketClientLogic(
|
||||
private val appLogic: AppLogic,
|
||||
private val isConnectedInternal: MutableLiveData<Boolean>,
|
||||
websocketClientCreator: WebsocketClientCreator
|
||||
) {
|
||||
companion object {
|
||||
private const val LOG_TAG = "WebsocketClientLogic"
|
||||
}
|
||||
|
||||
// it's not checked if the device is configured for the online mode here because this is caught below
|
||||
private val shouldConnectToWebsocket = appLogic.enable.switchMap { enabled ->
|
||||
|
||||
// app must be enabled
|
||||
if (enabled == true) {
|
||||
val okForCurrentUser = appLogic.deviceUserEntry.switchMap {
|
||||
if (it?.type == UserType.Child) {
|
||||
liveDataFromValue(true)
|
||||
} else {
|
||||
IsAppInForeground.isRunning
|
||||
}
|
||||
}
|
||||
|
||||
val okFromNetworkStatus = appLogic.networkStatus.map { networkStatus ->
|
||||
networkStatus == NetworkStatus.Online
|
||||
}
|
||||
|
||||
okForCurrentUser.and(okFromNetworkStatus)
|
||||
} else {
|
||||
liveDataFromValue(false)
|
||||
}
|
||||
}
|
||||
|
||||
private val deviceAuthTokenToConnectFor = shouldConnectToWebsocket.switchMap { shouldConnect ->
|
||||
|
||||
if (shouldConnect) {
|
||||
appLogic.database.config().getDeviceAuthTokenAsync()
|
||||
} else {
|
||||
liveDataFromValue("")
|
||||
}
|
||||
}
|
||||
|
||||
private val connectedDevicesInternal = MutableLiveData<Set<String>>().apply { value = emptySet() }
|
||||
val connectedDevices = connectedDevicesInternal.ignoreUnchanged()
|
||||
|
||||
init {
|
||||
runAsyncExpectForever {
|
||||
var previousDeviceAuthToken: String? = null
|
||||
var currentWebsocketClient: WebsocketClient? = null
|
||||
|
||||
while (true) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "wait for new device auth token")
|
||||
}
|
||||
|
||||
val deviceAuthToken = deviceAuthTokenToConnectFor.waitUntilValueMatches { it != previousDeviceAuthToken }!!
|
||||
previousDeviceAuthToken = deviceAuthToken
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "got new device auth token: $deviceAuthToken")
|
||||
}
|
||||
|
||||
// shutdown any current connection
|
||||
currentWebsocketClient?.shutdown()
|
||||
currentWebsocketClient = null
|
||||
|
||||
if (deviceAuthToken.isNotEmpty()) {
|
||||
val serverConfig = appLogic.serverLogic.getServerConfigCoroutine()
|
||||
|
||||
if (serverConfig.deviceAuthToken != deviceAuthToken) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "device auth token changed in the time between - don't connect")
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
val serverUrl = serverConfig.serverUrl
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "create new websocket client for $serverUrl")
|
||||
}
|
||||
|
||||
lateinit var newWebsocketClient: WebsocketClient
|
||||
|
||||
newWebsocketClient = websocketClientCreator.createWebsocketClient(
|
||||
deviceAuthTokenToConnectFor = deviceAuthToken,
|
||||
serverUrl = serverUrl,
|
||||
listener = object : WebsocketClientListener {
|
||||
override fun onConnectionEstablished() {
|
||||
if (currentWebsocketClient !== newWebsocketClient) {
|
||||
// we are not the current instance anymore
|
||||
return
|
||||
}
|
||||
|
||||
isConnectedInternal.postValue(true)
|
||||
connectedDevicesInternal.postValue(emptySet())
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "request important sync because websocket connection was established")
|
||||
}
|
||||
|
||||
appLogic.syncUtil.requestImportantSync()
|
||||
}
|
||||
|
||||
override fun onSyncRequestedByServer(important: Boolean) {
|
||||
if (currentWebsocketClient !== newWebsocketClient) {
|
||||
// we are not the current instance anymore
|
||||
return
|
||||
}
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "request sync because the server did it")
|
||||
}
|
||||
|
||||
if (important) {
|
||||
appLogic.syncUtil.requestImportantSync()
|
||||
} else {
|
||||
appLogic.syncUtil.requestUnimportantSync()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConnectionLost() {
|
||||
if (currentWebsocketClient !== newWebsocketClient) {
|
||||
// we are not the current instance anymore
|
||||
return
|
||||
}
|
||||
|
||||
isConnectedInternal.postValue(false)
|
||||
connectedDevicesInternal.postValue(emptySet())
|
||||
}
|
||||
|
||||
override fun onGotConnectedDeviceList(connectedDeviceIds: Set<String>) {
|
||||
if (currentWebsocketClient !== newWebsocketClient) {
|
||||
// we are not the current instance anymore
|
||||
return
|
||||
}
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "got connected device list: ${connectedDeviceIds.joinToString(", ")}")
|
||||
}
|
||||
|
||||
connectedDevicesInternal.postValue(connectedDeviceIds)
|
||||
}
|
||||
|
||||
override fun onGotSignOutRequest() {
|
||||
if (currentWebsocketClient !== newWebsocketClient) {
|
||||
// we are not the current instance anymore
|
||||
return
|
||||
}
|
||||
|
||||
runAsync {
|
||||
try {
|
||||
if (AppAffectedByPrimaryDeviceUtil.isCurrentAppAffectedByPrimaryDevice(appLogic)) {
|
||||
throw IllegalStateException("current device would be affected by primary device")
|
||||
}
|
||||
|
||||
UpdatePrimaryDeviceModel.unsetPrimaryDeviceInBackground(appLogic)
|
||||
} catch (ex: Exception) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.w(LOG_TAG, "could not unset this device as current device", ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
currentWebsocketClient = newWebsocketClient
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue