mirror of
https://codeberg.org/timelimit/timelimit-android.git
synced 2025-10-05 10:49:26 +02:00
Add user limit login category feature
This commit is contained in:
parent
3f7e34a175
commit
7c8c00b539
40 changed files with 2502 additions and 187 deletions
1070
app/schemas/io.timelimit.android.data.RoomDatabase/31.json
Normal file
1070
app/schemas/io.timelimit.android.data.RoomDatabase/31.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -37,6 +37,7 @@ interface Database {
|
||||||
fun userKey(): UserKeyDao
|
fun userKey(): UserKeyDao
|
||||||
fun sessionDuration(): SessionDurationDao
|
fun sessionDuration(): SessionDurationDao
|
||||||
fun derivedDataDao(): DerivedDataDao
|
fun derivedDataDao(): DerivedDataDao
|
||||||
|
fun userLimitLoginCategoryDao(): UserLimitLoginCategoryDao
|
||||||
|
|
||||||
fun <T> runInTransaction(block: () -> T): T
|
fun <T> runInTransaction(block: () -> T): T
|
||||||
fun <T> runInUnobservedTransaction(block: () -> T): T
|
fun <T> runInUnobservedTransaction(block: () -> T): T
|
||||||
|
|
|
@ -224,4 +224,11 @@ object DatabaseMigrations {
|
||||||
database.execSQL("ALTER TABLE `user` ADD COLUMN `flags` INTEGER NOT NULL DEFAULT 0")
|
database.execSQL("ALTER TABLE `user` ADD COLUMN `flags` INTEGER NOT NULL DEFAULT 0")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val MIGRATE_TO_V31 = object: Migration(30, 31) {
|
||||||
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
|
database.execSQL("CREATE TABLE IF NOT EXISTS `user_limit_login_category` (`user_id` TEXT NOT NULL, `category_id` TEXT NOT NULL, PRIMARY KEY(`user_id`), FOREIGN KEY(`user_id`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`category_id`) REFERENCES `category`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )")
|
||||||
|
database.execSQL("CREATE INDEX IF NOT EXISTS `user_limit_login_category_index_category_id` ON `user_limit_login_category` (`category_id`)")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,8 +44,9 @@ import java.util.concurrent.CountDownLatch
|
||||||
Notification::class,
|
Notification::class,
|
||||||
AllowedContact::class,
|
AllowedContact::class,
|
||||||
UserKey::class,
|
UserKey::class,
|
||||||
SessionDuration::class
|
SessionDuration::class,
|
||||||
], version = 30)
|
UserLimitLoginCategory::class
|
||||||
|
], version = 31)
|
||||||
abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database {
|
abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database {
|
||||||
companion object {
|
companion object {
|
||||||
private val lock = Object()
|
private val lock = Object()
|
||||||
|
@ -109,7 +110,8 @@ abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database
|
||||||
DatabaseMigrations.MIGRATE_TO_V27,
|
DatabaseMigrations.MIGRATE_TO_V27,
|
||||||
DatabaseMigrations.MIGRATE_TO_V28,
|
DatabaseMigrations.MIGRATE_TO_V28,
|
||||||
DatabaseMigrations.MIGRATE_TO_V29,
|
DatabaseMigrations.MIGRATE_TO_V29,
|
||||||
DatabaseMigrations.MIGRATE_TO_V30
|
DatabaseMigrations.MIGRATE_TO_V30,
|
||||||
|
DatabaseMigrations.MIGRATE_TO_V31
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,6 +42,7 @@ object DatabaseBackupLowlevel {
|
||||||
private const val ALLOWED_CONTACT = "allowedContact"
|
private const val ALLOWED_CONTACT = "allowedContact"
|
||||||
private const val USER_KEY = "userKey"
|
private const val USER_KEY = "userKey"
|
||||||
private const val SESSION_DURATION = "sessionDuration"
|
private const val SESSION_DURATION = "sessionDuration"
|
||||||
|
private const val USER_LIMIT_LOGIN_CATEGORY = "userLimitLoginCategory"
|
||||||
|
|
||||||
fun outputAsBackupJson(database: Database, outputStream: OutputStream) {
|
fun outputAsBackupJson(database: Database, outputStream: OutputStream) {
|
||||||
val writer = JsonWriter(OutputStreamWriter(outputStream, Charsets.UTF_8))
|
val writer = JsonWriter(OutputStreamWriter(outputStream, Charsets.UTF_8))
|
||||||
|
@ -88,6 +89,7 @@ object DatabaseBackupLowlevel {
|
||||||
handleCollection(ALLOWED_CONTACT) { offset, pageSize -> database.allowedContact().getAllowedContactPageSync(offset, pageSize) }
|
handleCollection(ALLOWED_CONTACT) { offset, pageSize -> database.allowedContact().getAllowedContactPageSync(offset, pageSize) }
|
||||||
handleCollection(USER_KEY) { offset, pageSize -> database.userKey().getUserKeyPageSync(offset, pageSize) }
|
handleCollection(USER_KEY) { offset, pageSize -> database.userKey().getUserKeyPageSync(offset, pageSize) }
|
||||||
handleCollection(SESSION_DURATION) { offset, pageSize -> database.sessionDuration().getSessionDurationPageSync(offset, pageSize) }
|
handleCollection(SESSION_DURATION) { offset, pageSize -> database.sessionDuration().getSessionDurationPageSync(offset, pageSize) }
|
||||||
|
handleCollection(USER_LIMIT_LOGIN_CATEGORY) { offset, pageSize -> database.userLimitLoginCategoryDao().getAllowedContactPageSync(offset, pageSize) }
|
||||||
|
|
||||||
writer.endObject().flush()
|
writer.endObject().flush()
|
||||||
}
|
}
|
||||||
|
@ -95,6 +97,8 @@ object DatabaseBackupLowlevel {
|
||||||
fun restoreFromBackupJson(database: Database, inputStream: InputStream) {
|
fun restoreFromBackupJson(database: Database, inputStream: InputStream) {
|
||||||
val reader = JsonReader(InputStreamReader(inputStream, Charsets.UTF_8))
|
val reader = JsonReader(InputStreamReader(inputStream, Charsets.UTF_8))
|
||||||
|
|
||||||
|
var userLoginLimitCategories = emptyList<UserLimitLoginCategory>()
|
||||||
|
|
||||||
database.runInTransaction {
|
database.runInTransaction {
|
||||||
database.deleteAllData()
|
database.deleteAllData()
|
||||||
|
|
||||||
|
@ -234,10 +238,25 @@ object DatabaseBackupLowlevel {
|
||||||
|
|
||||||
reader.endArray()
|
reader.endArray()
|
||||||
}
|
}
|
||||||
|
USER_LIMIT_LOGIN_CATEGORY -> {
|
||||||
|
reader.beginArray()
|
||||||
|
|
||||||
|
mutableListOf<UserLimitLoginCategory>().let { list ->
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
list.add(UserLimitLoginCategory.parse(reader))
|
||||||
|
}
|
||||||
|
|
||||||
|
userLoginLimitCategories = list
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.endArray()
|
||||||
|
}
|
||||||
else -> reader.skipValue()
|
else -> reader.skipValue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
reader.endObject()
|
reader.endObject()
|
||||||
|
|
||||||
|
database.userLimitLoginCategoryDao().addItemsSync(userLoginLimitCategories)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -20,9 +20,7 @@ import androidx.lifecycle.LiveData
|
||||||
import io.timelimit.android.data.Database
|
import io.timelimit.android.data.Database
|
||||||
import io.timelimit.android.data.cache.multi.*
|
import io.timelimit.android.data.cache.multi.*
|
||||||
import io.timelimit.android.data.cache.single.*
|
import io.timelimit.android.data.cache.single.*
|
||||||
import io.timelimit.android.data.model.derived.DeviceAndUserRelatedData
|
import io.timelimit.android.data.model.derived.*
|
||||||
import io.timelimit.android.data.model.derived.DeviceRelatedData
|
|
||||||
import io.timelimit.android.data.model.derived.UserRelatedData
|
|
||||||
|
|
||||||
class DerivedDataDao (private val database: Database) {
|
class DerivedDataDao (private val database: Database) {
|
||||||
private val userRelatedDataCache = object : DataCacheHelperInterface<String, UserRelatedData?, UserRelatedData?> {
|
private val userRelatedDataCache = object : DataCacheHelperInterface<String, UserRelatedData?, UserRelatedData?> {
|
||||||
|
@ -32,16 +30,8 @@ class DerivedDataDao (private val database: Database) {
|
||||||
return UserRelatedData.load(user, database)
|
return UserRelatedData.load(user, database)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateItemSync(key: String, item: UserRelatedData?): UserRelatedData? {
|
override fun updateItemSync(key: String, item: UserRelatedData?): UserRelatedData? = if (item != null) item.update(database) else openItemSync(key)
|
||||||
return if (item != null) {
|
|
||||||
item.update(database)
|
|
||||||
} else {
|
|
||||||
openItemSync(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun <R> wrapOpenOrUpdate(block: () -> R): R = database.runInUnobservedTransaction { block() }
|
override fun <R> wrapOpenOrUpdate(block: () -> R): R = database.runInUnobservedTransaction { block() }
|
||||||
|
|
||||||
override fun disposeItemFast(key: String, item: UserRelatedData?) = Unit
|
override fun disposeItemFast(key: String, item: UserRelatedData?) = Unit
|
||||||
override fun prepareForUser(item: UserRelatedData?): UserRelatedData? = item
|
override fun prepareForUser(item: UserRelatedData?): UserRelatedData? = item
|
||||||
override fun close() = Unit
|
override fun close() = Unit
|
||||||
|
@ -49,27 +39,30 @@ class DerivedDataDao (private val database: Database) {
|
||||||
|
|
||||||
private val deviceRelatedDataCache = object: SingleItemDataCacheHelperInterface<DeviceRelatedData?, DeviceRelatedData?> {
|
private val deviceRelatedDataCache = object: SingleItemDataCacheHelperInterface<DeviceRelatedData?, DeviceRelatedData?> {
|
||||||
override fun openItemSync(): DeviceRelatedData? = DeviceRelatedData.load(database)
|
override fun openItemSync(): DeviceRelatedData? = DeviceRelatedData.load(database)
|
||||||
|
override fun updateItemSync(item: DeviceRelatedData?): DeviceRelatedData? = if (item != null) item.update(database) else openItemSync()
|
||||||
override fun updateItemSync(item: DeviceRelatedData?): DeviceRelatedData? = if (item != null) {
|
|
||||||
item.update(database)
|
|
||||||
} else {
|
|
||||||
openItemSync()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun <R> wrapOpenOrUpdate(block: () -> R): R = database.runInUnobservedTransaction { block() }
|
override fun <R> wrapOpenOrUpdate(block: () -> R): R = database.runInUnobservedTransaction { block() }
|
||||||
|
|
||||||
override fun prepareForUser(item: DeviceRelatedData?): DeviceRelatedData? = item
|
override fun prepareForUser(item: DeviceRelatedData?): DeviceRelatedData? = item
|
||||||
override fun disposeItemFast(item: DeviceRelatedData?): Unit = Unit
|
override fun disposeItemFast(item: DeviceRelatedData?): Unit = Unit
|
||||||
}.createCache()
|
}.createCache()
|
||||||
|
|
||||||
|
private val userLoginRelatedDataCache = object: DataCacheHelperInterface<String, UserLoginRelatedData?, UserLoginRelatedData?> {
|
||||||
|
override fun openItemSync(key: String): UserLoginRelatedData? = UserLoginRelatedData.load(key, database)
|
||||||
|
override fun updateItemSync(key: String, item: UserLoginRelatedData?): UserLoginRelatedData? = if (item != null) item.update(database) else openItemSync(key)
|
||||||
|
override fun <R> wrapOpenOrUpdate(block: () -> R): R = database.runInUnobservedTransaction { block() }
|
||||||
|
override fun disposeItemFast(key: String, item: UserLoginRelatedData?) = Unit
|
||||||
|
override fun prepareForUser(item: UserLoginRelatedData?): UserLoginRelatedData? = item
|
||||||
|
override fun close() = Unit
|
||||||
|
}.createCache()
|
||||||
|
|
||||||
private val usableUserRelatedData = userRelatedDataCache.userInterface.delayClosingItems(15 * 1000 /* 15 seconds */)
|
private val usableUserRelatedData = userRelatedDataCache.userInterface.delayClosingItems(15 * 1000 /* 15 seconds */)
|
||||||
private val usableDeviceRelatedData = deviceRelatedDataCache.userInterface.delayClosingItem(60 * 1000 /* 1 minute */)
|
private val usableDeviceRelatedData = deviceRelatedDataCache.userInterface.delayClosingItem(60 * 1000 /* 1 minute */)
|
||||||
|
private val usableUserLoginRelatedDataCache = userLoginRelatedDataCache.userInterface.delayClosingItems(15 * 1000 /* 15 seconds */)
|
||||||
|
|
||||||
private val deviceAndUserRelatedDataCache = object: SingleItemDataCacheHelperInterface<DeviceAndUserRelatedData?, DeviceAndUserRelatedData?> {
|
private val deviceAndUserRelatedDataCache = object: SingleItemDataCacheHelperInterface<DeviceAndUserRelatedData?, DeviceAndUserRelatedData?> {
|
||||||
override fun openItemSync(): DeviceAndUserRelatedData? {
|
override fun openItemSync(): DeviceAndUserRelatedData? {
|
||||||
val deviceRelatedData = usableDeviceRelatedData.openSync(null) ?: return null
|
val deviceRelatedData = usableDeviceRelatedData.openSync(null) ?: return null
|
||||||
val userRelatedData = if (deviceRelatedData.deviceEntry.currentUserId.isNotEmpty())
|
val userRelatedData = if (deviceRelatedData.deviceEntry.currentUserId.isNotEmpty())
|
||||||
usableUserRelatedData.openSync(deviceRelatedData.deviceEntry.currentUserId, null)
|
usableUserRelatedData.openSync(deviceRelatedData.deviceEntry.currentUserId, null)
|
||||||
else
|
else
|
||||||
null
|
null
|
||||||
|
|
||||||
|
@ -80,27 +73,12 @@ class DerivedDataDao (private val database: Database) {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateItemSync(item: DeviceAndUserRelatedData?): DeviceAndUserRelatedData? {
|
override fun updateItemSync(item: DeviceAndUserRelatedData?): DeviceAndUserRelatedData? {
|
||||||
val deviceRelatedData = usableDeviceRelatedData.openSync(null) ?: run {
|
try {
|
||||||
// close old listener instances
|
val newItem = openItemSync()
|
||||||
|
|
||||||
|
return if (newItem != item) newItem else item
|
||||||
|
} finally {
|
||||||
disposeItemFast(item)
|
disposeItemFast(item)
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
val userRelatedData = if (deviceRelatedData.deviceEntry.currentUserId.isNotEmpty())
|
|
||||||
usableUserRelatedData.openSync(deviceRelatedData.deviceEntry.currentUserId, null)
|
|
||||||
else
|
|
||||||
null
|
|
||||||
|
|
||||||
// close old listener instances
|
|
||||||
disposeItemFast(item)
|
|
||||||
|
|
||||||
return if (deviceRelatedData == item?.deviceRelatedData && userRelatedData == item.userRelatedData) {
|
|
||||||
item
|
|
||||||
} else {
|
|
||||||
DeviceAndUserRelatedData(
|
|
||||||
deviceRelatedData = deviceRelatedData,
|
|
||||||
userRelatedData = userRelatedData
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,21 +87,72 @@ class DerivedDataDao (private val database: Database) {
|
||||||
override fun prepareForUser(item: DeviceAndUserRelatedData?): DeviceAndUserRelatedData? = item
|
override fun prepareForUser(item: DeviceAndUserRelatedData?): DeviceAndUserRelatedData? = item
|
||||||
|
|
||||||
override fun disposeItemFast(item: DeviceAndUserRelatedData?) {
|
override fun disposeItemFast(item: DeviceAndUserRelatedData?) {
|
||||||
if (item != null) {
|
usableDeviceRelatedData.close(null)
|
||||||
usableDeviceRelatedData.close(null)
|
item?.deviceRelatedData?.deviceEntry?.currentUserId?.let {
|
||||||
item.userRelatedData?.user?.let { usableUserRelatedData.close(it.id, null) }
|
if (it.isNotEmpty()) {
|
||||||
|
usableUserRelatedData.close(it, null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.createCache()
|
}.createCache()
|
||||||
|
|
||||||
|
private val completeUserLoginRelatedData = object: DataCacheHelperInterface<String, CompleteUserLoginRelatedData?, CompleteUserLoginRelatedData?> {
|
||||||
|
override fun openItemSync(key: String): CompleteUserLoginRelatedData? = database.runInUnobservedTransaction {
|
||||||
|
val userLoginRelatedData = usableUserLoginRelatedDataCache.openSync(key, null)
|
||||||
|
val deviceRelatedData = usableDeviceRelatedData.openSync(null)
|
||||||
|
|
||||||
|
val limitLoginCategoryUserRelatedData = if (userLoginRelatedData?.limitLoginCategory == null)
|
||||||
|
null
|
||||||
|
else {
|
||||||
|
usableUserRelatedData.openSync(userLoginRelatedData.limitLoginCategory.childId, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userLoginRelatedData == null || deviceRelatedData == null) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
CompleteUserLoginRelatedData(
|
||||||
|
loginRelatedData = userLoginRelatedData,
|
||||||
|
deviceRelatedData = deviceRelatedData,
|
||||||
|
limitLoginCategoryUserRelatedData = limitLoginCategoryUserRelatedData
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateItemSync(key: String, item: CompleteUserLoginRelatedData?): CompleteUserLoginRelatedData? {
|
||||||
|
try {
|
||||||
|
val newItem = openItemSync(key)
|
||||||
|
|
||||||
|
return if (newItem != item) newItem else item
|
||||||
|
} finally {
|
||||||
|
disposeItemFast(key, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun disposeItemFast(key: String, item: CompleteUserLoginRelatedData?) {
|
||||||
|
usableUserLoginRelatedDataCache.close(key, null)
|
||||||
|
usableDeviceRelatedData.close(null)
|
||||||
|
item?.loginRelatedData?.limitLoginCategory?.let { category ->
|
||||||
|
usableUserRelatedData.close(category.childId, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun <R> wrapOpenOrUpdate(block: () -> R): R = database.runInUnobservedTransaction { block() }
|
||||||
|
override fun prepareForUser(item: CompleteUserLoginRelatedData?): CompleteUserLoginRelatedData? = item
|
||||||
|
override fun close() = Unit
|
||||||
|
}.createCache()
|
||||||
|
|
||||||
private val usableDeviceAndUserRelatedDataCache = deviceAndUserRelatedDataCache.userInterface.delayClosingItem(5000)
|
private val usableDeviceAndUserRelatedDataCache = deviceAndUserRelatedDataCache.userInterface.delayClosingItem(5000)
|
||||||
|
private val usableCompleteUserLoginRelatedData = completeUserLoginRelatedData.userInterface.delayClosingItems(5000)
|
||||||
|
|
||||||
private val deviceAndUserRelatedDataLive = usableDeviceAndUserRelatedDataCache.openLiveAtDatabaseThread()
|
private val deviceAndUserRelatedDataLive = usableDeviceAndUserRelatedDataCache.openLiveAtDatabaseThread()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
database.registerTransactionCommitListener {
|
database.registerTransactionCommitListener {
|
||||||
userRelatedDataCache.ownerInterface.updateSync()
|
userRelatedDataCache.ownerInterface.updateSync()
|
||||||
deviceRelatedDataCache.ownerInterface.updateSync()
|
deviceRelatedDataCache.ownerInterface.updateSync()
|
||||||
|
userLoginRelatedDataCache.ownerInterface.updateSync()
|
||||||
deviceAndUserRelatedDataCache.ownerInterface.updateSync()
|
deviceAndUserRelatedDataCache.ownerInterface.updateSync()
|
||||||
|
completeUserLoginRelatedData.ownerInterface.updateSync()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,7 +164,17 @@ class DerivedDataDao (private val database: Database) {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getUserLoginRelatedDataSync(userId: String): CompleteUserLoginRelatedData? {
|
||||||
|
val result = usableCompleteUserLoginRelatedData.openSync(userId, null)
|
||||||
|
|
||||||
|
usableCompleteUserLoginRelatedData.close(userId, null)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
fun getUserAndDeviceRelatedDataLive(): LiveData<DeviceAndUserRelatedData?> = deviceAndUserRelatedDataLive
|
fun getUserAndDeviceRelatedDataLive(): LiveData<DeviceAndUserRelatedData?> = deviceAndUserRelatedDataLive
|
||||||
|
|
||||||
fun getUserRelatedDataLive(userId: String): LiveData<UserRelatedData?> = usableUserRelatedData.openLiveAtDatabaseThread(userId)
|
fun getUserRelatedDataLive(userId: String): LiveData<UserRelatedData?> = usableUserRelatedData.openLiveAtDatabaseThread(userId)
|
||||||
|
|
||||||
|
fun getUserLoginRelatedDataLive(userId: String) = usableCompleteUserLoginRelatedData.openLiveAtDatabaseThread(userId)
|
||||||
}
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation version 3 of the License.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.timelimit.android.data.dao
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import io.timelimit.android.data.model.UserLimitLoginCategory
|
||||||
|
import io.timelimit.android.data.model.UserLimitLoginCategoryWithChildId
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface UserLimitLoginCategoryDao {
|
||||||
|
@Query("SELECT * FROM user_limit_login_category LIMIT :pageSize OFFSET :offset")
|
||||||
|
fun getAllowedContactPageSync(offset: Int, pageSize: Int): List<UserLimitLoginCategory>
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
fun addItemsSync(item: List<UserLimitLoginCategory>)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
fun insertOrReplaceItemSync(item: UserLimitLoginCategory)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
fun insertOrIgnoreItemSync(item: UserLimitLoginCategory)
|
||||||
|
|
||||||
|
@Query("SELECT child_user.id AS child_id, child_user.name AS child_title, category.id AS category_id, category.title AS category_title, 1 AS selected FROM user_limit_login_category JOIN category ON (user_limit_login_category.category_id = category.id) JOIN user child_user ON (category.child_id = child_user.id) WHERE user_limit_login_category.user_id = :parentUserId")
|
||||||
|
fun getByParentUserIdLive(parentUserId: String): LiveData<UserLimitLoginCategoryWithChildId?>
|
||||||
|
|
||||||
|
@Query("SELECT child_user.id AS child_id, child_user.name AS child_title, category.id AS category_id, category.title AS category_title, 1 AS selected FROM user_limit_login_category JOIN category ON (user_limit_login_category.category_id = category.id) JOIN user child_user ON (category.child_id = child_user.id) WHERE user_limit_login_category.user_id = :parentUserId")
|
||||||
|
fun getByParentUserIdSync(parentUserId: String): UserLimitLoginCategoryWithChildId?
|
||||||
|
|
||||||
|
@Query("SELECT child_user.id AS child_id, child_user.name AS child_title, category.id AS category_id, category.title AS category_title, CASE WHEN category.id IN (SELECT user_limit_login_category.category_id FROM user_limit_login_category WHERE user_limit_login_category.user_id = :parentUserId) THEN 1 ELSE 0 END AS selected FROM user child_user JOIN category category ON (category.child_id = child_user.id)")
|
||||||
|
fun getLimitLoginCategoryOptions(parentUserId: String): LiveData<List<UserLimitLoginCategoryWithChildId>>
|
||||||
|
|
||||||
|
@Query("SELECT COUNT(*) FROM user WHERE user.id != :userId AND user.type = 'parent' AND user.id NOT IN (SELECT user_id FROM user_limit_login_category)")
|
||||||
|
fun countOtherUsersWithoutLimitLoginCategoryLive(userId: String): LiveData<Long>
|
||||||
|
|
||||||
|
@Query("SELECT COUNT(*) FROM user WHERE user.id != :userId AND user.type = 'parent' AND user.id NOT IN (SELECT user_id FROM user_limit_login_category)")
|
||||||
|
fun countOtherUsersWithoutLimitLoginCategorySync(userId: String): Long
|
||||||
|
|
||||||
|
@Query("DELETE FROM user_limit_login_category WHERE user_id = :userId")
|
||||||
|
fun removeItemSync(userId: String)
|
||||||
|
}
|
|
@ -66,6 +66,19 @@ fun UserRelatedData.getChildCategories(categoryId: String): Set<String> {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun UserRelatedData.getCategoryWithParentCategories(startCategoryId: String): Set<String> {
|
||||||
|
val startCategory = categoryById[startCategoryId]!!
|
||||||
|
val categoryIds = mutableSetOf(startCategoryId)
|
||||||
|
|
||||||
|
var currentCategory: CategoryRelatedData? = categoryById[startCategory.category.parentCategoryId]
|
||||||
|
|
||||||
|
while (currentCategory != null && categoryIds.add(currentCategory.category.id)) {
|
||||||
|
currentCategory = categoryById[currentCategory.category.parentCategoryId]
|
||||||
|
}
|
||||||
|
|
||||||
|
return categoryIds
|
||||||
|
}
|
||||||
|
|
||||||
fun List<Category>.getChildCategories(categoryId: String): Set<String> {
|
fun List<Category>.getChildCategories(categoryId: String): Set<String> {
|
||||||
if (this.find { it.id == categoryId } != null) {
|
if (this.find { it.id == categoryId } != null) {
|
||||||
return emptySet()
|
return emptySet()
|
||||||
|
|
|
@ -31,7 +31,8 @@ enum class Table {
|
||||||
TimeLimitRule,
|
TimeLimitRule,
|
||||||
UsedTimeItem,
|
UsedTimeItem,
|
||||||
User,
|
User,
|
||||||
UserKey
|
UserKey,
|
||||||
|
UserLimitLoginCategory
|
||||||
}
|
}
|
||||||
|
|
||||||
object TableNames {
|
object TableNames {
|
||||||
|
@ -50,6 +51,7 @@ object TableNames {
|
||||||
const val USED_TIME_ITEM = "used_time"
|
const val USED_TIME_ITEM = "used_time"
|
||||||
const val USER = "user"
|
const val USER = "user"
|
||||||
const val USER_KEY = "user_key"
|
const val USER_KEY = "user_key"
|
||||||
|
const val USER_LIMIT_LOGIN_CATEGORY = "user_limit_login_category"
|
||||||
}
|
}
|
||||||
|
|
||||||
object TableUtil {
|
object TableUtil {
|
||||||
|
@ -69,6 +71,7 @@ object TableUtil {
|
||||||
Table.UsedTimeItem -> TableNames.USED_TIME_ITEM
|
Table.UsedTimeItem -> TableNames.USED_TIME_ITEM
|
||||||
Table.User -> TableNames.USER
|
Table.User -> TableNames.USER
|
||||||
Table.UserKey -> TableNames.USER_KEY
|
Table.UserKey -> TableNames.USER_KEY
|
||||||
|
Table.UserLimitLoginCategory -> TableNames.USER_LIMIT_LOGIN_CATEGORY
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toEnum(value: String): Table = when (value) {
|
fun toEnum(value: String): Table = when (value) {
|
||||||
|
@ -87,6 +90,7 @@ object TableUtil {
|
||||||
TableNames.USED_TIME_ITEM -> Table.UsedTimeItem
|
TableNames.USED_TIME_ITEM -> Table.UsedTimeItem
|
||||||
TableNames.USER -> Table.User
|
TableNames.USER -> Table.User
|
||||||
TableNames.USER_KEY -> Table.UserKey
|
TableNames.USER_KEY -> Table.UserKey
|
||||||
|
TableNames.USER_LIMIT_LOGIN_CATEGORY -> Table.UserLimitLoginCategory
|
||||||
else -> throw IllegalArgumentException()
|
else -> throw IllegalArgumentException()
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,108 @@
|
||||||
|
/*
|
||||||
|
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation version 3 of the License.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.timelimit.android.data.model
|
||||||
|
|
||||||
|
import android.util.JsonReader
|
||||||
|
import android.util.JsonWriter
|
||||||
|
import androidx.room.*
|
||||||
|
import io.timelimit.android.data.IdGenerator
|
||||||
|
import io.timelimit.android.data.JsonSerializable
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "user_limit_login_category",
|
||||||
|
indices = [
|
||||||
|
Index(
|
||||||
|
name = "user_limit_login_category_index_category_id",
|
||||||
|
value = ["category_id"]
|
||||||
|
)
|
||||||
|
],
|
||||||
|
foreignKeys = [
|
||||||
|
ForeignKey(
|
||||||
|
entity = User::class,
|
||||||
|
childColumns = ["user_id"],
|
||||||
|
parentColumns = ["id"],
|
||||||
|
onDelete = ForeignKey.CASCADE,
|
||||||
|
onUpdate = ForeignKey.CASCADE
|
||||||
|
),
|
||||||
|
ForeignKey(
|
||||||
|
entity = Category::class,
|
||||||
|
childColumns = ["category_id"],
|
||||||
|
parentColumns = ["id"],
|
||||||
|
onDelete = ForeignKey.CASCADE,
|
||||||
|
onUpdate = ForeignKey.CASCADE
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
data class UserLimitLoginCategory(
|
||||||
|
@PrimaryKey
|
||||||
|
@ColumnInfo(name = "user_id")
|
||||||
|
val userId: String,
|
||||||
|
@ColumnInfo(name = "category_id")
|
||||||
|
val categoryId: String
|
||||||
|
): JsonSerializable {
|
||||||
|
companion object {
|
||||||
|
private const val USER_ID = "userId"
|
||||||
|
private const val CATEGORY_ID = "categoryId"
|
||||||
|
|
||||||
|
fun parse(reader: JsonReader): UserLimitLoginCategory {
|
||||||
|
var userId: String? = null
|
||||||
|
var categoryId: String? = null
|
||||||
|
|
||||||
|
reader.beginObject()
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
when (reader.nextName()) {
|
||||||
|
USER_ID -> userId = reader.nextString()
|
||||||
|
CATEGORY_ID -> categoryId = reader.nextString()
|
||||||
|
else -> reader.skipValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.endObject()
|
||||||
|
|
||||||
|
return UserLimitLoginCategory(
|
||||||
|
userId = userId!!,
|
||||||
|
categoryId = categoryId!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
IdGenerator.assertIdValid(userId)
|
||||||
|
IdGenerator.assertIdValid(categoryId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun serialize(writer: JsonWriter) {
|
||||||
|
writer.beginObject()
|
||||||
|
|
||||||
|
writer.name(USER_ID).value(userId)
|
||||||
|
writer.name(CATEGORY_ID).value(categoryId)
|
||||||
|
|
||||||
|
writer.endObject()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class UserLimitLoginCategoryWithChildId(
|
||||||
|
@ColumnInfo(name = "child_id")
|
||||||
|
val childId: String,
|
||||||
|
@ColumnInfo(name = "child_title")
|
||||||
|
val childTitle: String,
|
||||||
|
@ColumnInfo(name = "category_id")
|
||||||
|
val categoryId: String,
|
||||||
|
@ColumnInfo(name = "category_title")
|
||||||
|
val categoryTitle: String,
|
||||||
|
@ColumnInfo(name = "selected")
|
||||||
|
val selected: Boolean
|
||||||
|
)
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation version 3 of the License.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.timelimit.android.data.model.derived
|
||||||
|
|
||||||
|
data class CompleteUserLoginRelatedData(
|
||||||
|
val loginRelatedData: UserLoginRelatedData,
|
||||||
|
val deviceRelatedData: DeviceRelatedData,
|
||||||
|
val limitLoginCategoryUserRelatedData: UserRelatedData?
|
||||||
|
)
|
|
@ -0,0 +1,78 @@
|
||||||
|
/*
|
||||||
|
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation version 3 of the License.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.timelimit.android.data.model.derived
|
||||||
|
|
||||||
|
import io.timelimit.android.data.Database
|
||||||
|
import io.timelimit.android.data.invalidation.Observer
|
||||||
|
import io.timelimit.android.data.invalidation.Table
|
||||||
|
import io.timelimit.android.data.model.User
|
||||||
|
import io.timelimit.android.data.model.UserLimitLoginCategoryWithChildId
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
|
data class UserLoginRelatedData(
|
||||||
|
val user: User,
|
||||||
|
val limitLoginCategory: UserLimitLoginCategoryWithChildId?
|
||||||
|
): Observer {
|
||||||
|
companion object {
|
||||||
|
private val relatedTables = arrayOf(Table.User, Table.UserLimitLoginCategory, Table.Category)
|
||||||
|
|
||||||
|
fun load(userId: String, database: Database): UserLoginRelatedData? = database.runInUnobservedTransaction {
|
||||||
|
val user = database.user().getUserByIdSync(userId) ?: return@runInUnobservedTransaction null
|
||||||
|
val limitLoginCategory = database.userLimitLoginCategoryDao().getByParentUserIdSync(userId)
|
||||||
|
|
||||||
|
UserLoginRelatedData(
|
||||||
|
user = user,
|
||||||
|
limitLoginCategory = limitLoginCategory
|
||||||
|
).also {
|
||||||
|
database.registerWeakObserver(relatedTables, WeakReference(it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var userInvalidated = false
|
||||||
|
private var limitLoginCategoryInvalidated = false
|
||||||
|
|
||||||
|
override fun onInvalidated(tables: Set<Table>) {
|
||||||
|
tables.forEach { table ->
|
||||||
|
when (table) {
|
||||||
|
Table.User -> userInvalidated = true
|
||||||
|
Table.UserLimitLoginCategory -> limitLoginCategoryInvalidated = true
|
||||||
|
Table.Category -> limitLoginCategoryInvalidated = true
|
||||||
|
else -> {/* ignore */}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun update(database: Database): UserLoginRelatedData? {
|
||||||
|
if (!userInvalidated && !limitLoginCategoryInvalidated) {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
return database.runInUnobservedTransaction {
|
||||||
|
val userId = user.id
|
||||||
|
val user = if (userInvalidated) database.user().getUserByIdSync(userId) ?: return@runInUnobservedTransaction null else user
|
||||||
|
val limitLoginCategory = if (limitLoginCategoryInvalidated) database.userLimitLoginCategoryDao().getByParentUserIdSync(userId) else limitLoginCategory
|
||||||
|
|
||||||
|
UserLoginRelatedData(
|
||||||
|
user = user,
|
||||||
|
limitLoginCategory = limitLoginCategory
|
||||||
|
).also {
|
||||||
|
database.registerWeakObserver(relatedTables, WeakReference(it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,6 +17,7 @@
|
||||||
package io.timelimit.android.logic.blockingreason
|
package io.timelimit.android.logic.blockingreason
|
||||||
|
|
||||||
import io.timelimit.android.BuildConfig
|
import io.timelimit.android.BuildConfig
|
||||||
|
import io.timelimit.android.data.extensions.getCategoryWithParentCategories
|
||||||
import io.timelimit.android.data.model.derived.CategoryRelatedData
|
import io.timelimit.android.data.model.derived.CategoryRelatedData
|
||||||
import io.timelimit.android.data.model.derived.DeviceRelatedData
|
import io.timelimit.android.data.model.derived.DeviceRelatedData
|
||||||
import io.timelimit.android.data.model.derived.UserRelatedData
|
import io.timelimit.android.data.model.derived.UserRelatedData
|
||||||
|
@ -85,20 +86,8 @@ sealed class AppBaseHandling {
|
||||||
if (startCategory == null) {
|
if (startCategory == null) {
|
||||||
return BlockDueToNoCategory
|
return BlockDueToNoCategory
|
||||||
} else {
|
} else {
|
||||||
val categoryIds = mutableSetOf(startCategory.category.id)
|
|
||||||
|
|
||||||
run {
|
|
||||||
// get parent category ids
|
|
||||||
|
|
||||||
var currentCategory: CategoryRelatedData? = userRelatedData.categoryById[startCategory.category.parentCategoryId]
|
|
||||||
|
|
||||||
while (currentCategory != null && categoryIds.add(currentCategory.category.id)) {
|
|
||||||
currentCategory = userRelatedData.categoryById[currentCategory.category.parentCategoryId]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return UseCategories(
|
return UseCategories(
|
||||||
categoryIds = categoryIds,
|
categoryIds = userRelatedData.getCategoryWithParentCategories(startCategoryId = startCategory.category.id),
|
||||||
shouldCount = !pauseCounting,
|
shouldCount = !pauseCounting,
|
||||||
level = when (appCategory?.specifiesActivity) {
|
level = when (appCategory?.specifiesActivity) {
|
||||||
null -> BlockingLevel.Activity // occurs when using a default category
|
null -> BlockingLevel.Activity // occurs when using a default category
|
||||||
|
|
|
@ -241,6 +241,23 @@ data class CategoryItselfHandling (
|
||||||
// blockAllNotifications is only relevant if premium or local mode
|
// blockAllNotifications is only relevant if premium or local mode
|
||||||
// val shouldBlockNotifications = !okAll || blockAllNotifications
|
// val shouldBlockNotifications = !okAll || blockAllNotifications
|
||||||
val shouldBlockAtSystemLevel = !okBasic
|
val shouldBlockAtSystemLevel = !okBasic
|
||||||
|
val systemLevelBlockingReason: BlockingReason = if (!okByBattery)
|
||||||
|
BlockingReason.BatteryLimit
|
||||||
|
else if (!okByTempBlocking)
|
||||||
|
BlockingReason.TemporarilyBlocked
|
||||||
|
else if (!okByBlockedTimeAreas)
|
||||||
|
BlockingReason.BlockedAtThisTime
|
||||||
|
else if (!okByTimeLimitRules)
|
||||||
|
if (remainingTime?.hasRemainingTime == true)
|
||||||
|
BlockingReason.TimeOverExtraTimeCanBeUsedLater
|
||||||
|
else
|
||||||
|
BlockingReason.TimeOver
|
||||||
|
else if (!okBySessionDurationLimits)
|
||||||
|
BlockingReason.SessionDurationLimit
|
||||||
|
else if (missingNetworkTime)
|
||||||
|
BlockingReason.MissingNetworkTime
|
||||||
|
else
|
||||||
|
BlockingReason.None
|
||||||
|
|
||||||
fun isValid(
|
fun isValid(
|
||||||
categoryRelatedData: CategoryRelatedData,
|
categoryRelatedData: CategoryRelatedData,
|
||||||
|
|
|
@ -52,8 +52,24 @@ object ApplyServerDataStatus {
|
||||||
run {
|
run {
|
||||||
// update/ create entries (first because there must be always one parent user)
|
// update/ create entries (first because there must be always one parent user)
|
||||||
|
|
||||||
newUserList.data.forEach {
|
newUserList.data.forEach { newEntry ->
|
||||||
newData ->
|
val newData = User(
|
||||||
|
id = newEntry.id,
|
||||||
|
name = newEntry.name,
|
||||||
|
password = newEntry.password,
|
||||||
|
secondPasswordSalt = newEntry.secondPasswordSalt,
|
||||||
|
type = newEntry.type,
|
||||||
|
timeZone = newEntry.timeZone,
|
||||||
|
disableLimitsUntil = newEntry.disableLimitsUntil,
|
||||||
|
mail = newEntry.mail,
|
||||||
|
currentDevice = newEntry.currentDevice,
|
||||||
|
categoryForNotAssignedApps = newEntry.categoryForNotAssignedApps,
|
||||||
|
relaxPrimaryDevice = newEntry.relaxPrimaryDevice,
|
||||||
|
mailNotificationFlags = newEntry.mailNotificationFlags,
|
||||||
|
blockedTimes = newEntry.blockedTimes,
|
||||||
|
flags = newEntry.flags
|
||||||
|
)
|
||||||
|
|
||||||
val oldEntry = oldUserList.find { it.id == newData.id }
|
val oldEntry = oldUserList.find { it.id == newData.id }
|
||||||
|
|
||||||
if (oldEntry == null) {
|
if (oldEntry == null) {
|
||||||
|
@ -460,6 +476,24 @@ object ApplyServerDataStatus {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
status.newUserList?.data?.forEach { user ->
|
||||||
|
if (user.limitLoginCategory == null) {
|
||||||
|
database.userLimitLoginCategoryDao().removeItemSync(user.id)
|
||||||
|
} else {
|
||||||
|
val oldItem = database.userLimitLoginCategoryDao().getByParentUserIdSync(user.id)
|
||||||
|
|
||||||
|
if (oldItem == null || oldItem.categoryId != user.limitLoginCategory) {
|
||||||
|
database.userLimitLoginCategoryDao().removeItemSync(user.id)
|
||||||
|
database.userLimitLoginCategoryDao().insertOrIgnoreItemSync(
|
||||||
|
UserLimitLoginCategory(
|
||||||
|
userId = user.id,
|
||||||
|
categoryId = user.limitLoginCategory
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1854,6 +1854,32 @@ data class UpdateUserFlagsAction(val userId: String, val modifiedBits: Long, val
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class UpdateUserLimitLoginCategory(val userId: String, val categoryId: String?): ParentAction() {
|
||||||
|
companion object {
|
||||||
|
private const val TYPE_VALUE = "UPDATE_USER_LIMIT_LOGIN_CATEGORY"
|
||||||
|
private const val USER_ID = "userId"
|
||||||
|
private const val CATEGORY_ID = "categoryId"
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
IdGenerator.assertIdValid(userId)
|
||||||
|
categoryId?.let { IdGenerator.assertIdValid(categoryId) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun serialize(writer: JsonWriter) {
|
||||||
|
writer.beginObject()
|
||||||
|
|
||||||
|
writer.name(TYPE).value(TYPE_VALUE)
|
||||||
|
writer.name(USER_ID).value(userId)
|
||||||
|
|
||||||
|
if (categoryId != null) {
|
||||||
|
writer.name(CATEGORY_ID).value(categoryId)
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.endObject()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// child actions
|
// child actions
|
||||||
object ChildSignInAction: ChildAction() {
|
object ChildSignInAction: ChildAction() {
|
||||||
private const val TYPE_VALUE = "CHILD_SIGN_IN"
|
private const val TYPE_VALUE = "CHILD_SIGN_IN"
|
||||||
|
|
|
@ -69,6 +69,7 @@ object ActionParser {
|
||||||
// UpdateCategoryBatteryLimit
|
// UpdateCategoryBatteryLimit
|
||||||
// UpdateCategorySorting
|
// UpdateCategorySorting
|
||||||
// UpdateUserFlagsAction
|
// UpdateUserFlagsAction
|
||||||
|
// UpdateUserLimitLoginCategory
|
||||||
else -> throw IllegalStateException()
|
else -> throw IllegalStateException()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,10 +18,7 @@ package io.timelimit.android.sync.actions.dispatch
|
||||||
import io.timelimit.android.data.Database
|
import io.timelimit.android.data.Database
|
||||||
import io.timelimit.android.data.customtypes.ImmutableBitmask
|
import io.timelimit.android.data.customtypes.ImmutableBitmask
|
||||||
import io.timelimit.android.data.extensions.getChildCategories
|
import io.timelimit.android.data.extensions.getChildCategories
|
||||||
import io.timelimit.android.data.model.Category
|
import io.timelimit.android.data.model.*
|
||||||
import io.timelimit.android.data.model.CategoryApp
|
|
||||||
import io.timelimit.android.data.model.User
|
|
||||||
import io.timelimit.android.data.model.UserType
|
|
||||||
import io.timelimit.android.sync.actions.*
|
import io.timelimit.android.sync.actions.*
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
@ -259,6 +256,10 @@ object LocalDatabaseParentActionDispatcher {
|
||||||
if (currentParents.size <= 1) {
|
if (currentParents.size <= 1) {
|
||||||
throw IllegalStateException("would delete last parent")
|
throw IllegalStateException("would delete last parent")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (database.userLimitLoginCategoryDao().countOtherUsersWithoutLimitLoginCategorySync(action.userId) == 0L) {
|
||||||
|
throw IllegalStateException("would delete last user without login limit")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userToDelete.type == UserType.Child) {
|
if (userToDelete.type == UserType.Child) {
|
||||||
|
@ -595,6 +596,29 @@ object LocalDatabaseParentActionDispatcher {
|
||||||
|
|
||||||
database.user().updateUserSync(updatedUser)
|
database.user().updateUserSync(updatedUser)
|
||||||
}
|
}
|
||||||
|
is UpdateUserLimitLoginCategory -> {
|
||||||
|
val user = database.user().getUserByIdSync(action.userId)!!
|
||||||
|
|
||||||
|
if (user.type != UserType.Parent) {
|
||||||
|
throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.categoryId == null) {
|
||||||
|
database.userLimitLoginCategoryDao().removeItemSync(action.userId)
|
||||||
|
} else {
|
||||||
|
if (database.userLimitLoginCategoryDao().countOtherUsersWithoutLimitLoginCategorySync(action.userId) == 0L) {
|
||||||
|
throw IllegalStateException("there must be one user withou such limits")
|
||||||
|
}
|
||||||
|
|
||||||
|
database.category().getCategoryByIdSync(action.categoryId)!!
|
||||||
|
|
||||||
|
database.userLimitLoginCategoryDao().insertOrReplaceItemSync(
|
||||||
|
UserLimitLoginCategory(
|
||||||
|
userId = action.userId,
|
||||||
|
categoryId = action.categoryId
|
||||||
|
)
|
||||||
|
) }
|
||||||
|
}
|
||||||
}.let { }
|
}.let { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
package io.timelimit.android.sync.network
|
package io.timelimit.android.sync.network
|
||||||
|
|
||||||
import android.util.JsonReader
|
import android.util.JsonReader
|
||||||
|
import android.util.JsonToken
|
||||||
import io.timelimit.android.data.customtypes.ImmutableBitmask
|
import io.timelimit.android.data.customtypes.ImmutableBitmask
|
||||||
import io.timelimit.android.data.customtypes.ImmutableBitmaskJson
|
import io.timelimit.android.data.customtypes.ImmutableBitmaskJson
|
||||||
import io.timelimit.android.data.model.*
|
import io.timelimit.android.data.model.*
|
||||||
|
@ -130,7 +131,7 @@ data class ServerDeviceList(
|
||||||
|
|
||||||
data class ServerUserList(
|
data class ServerUserList(
|
||||||
val version: String,
|
val version: String,
|
||||||
val data: List<User>
|
val data: List<ServerUserData>
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
private const val VERSION = "version"
|
private const val VERSION = "version"
|
||||||
|
@ -138,13 +139,13 @@ data class ServerUserList(
|
||||||
|
|
||||||
fun parse(reader: JsonReader): ServerUserList {
|
fun parse(reader: JsonReader): ServerUserList {
|
||||||
var version: String? = null
|
var version: String? = null
|
||||||
var data: List<User>? = null
|
var data: List<ServerUserData>? = null
|
||||||
|
|
||||||
reader.beginObject()
|
reader.beginObject()
|
||||||
while (reader.hasNext()) {
|
while (reader.hasNext()) {
|
||||||
when (reader.nextName()) {
|
when (reader.nextName()) {
|
||||||
VERSION -> version = reader.nextString()
|
VERSION -> version = reader.nextString()
|
||||||
DATA -> data = User.parseList(reader)
|
DATA -> data = ServerUserData.parseList(reader)
|
||||||
else -> reader.skipValue()
|
else -> reader.skipValue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -158,6 +159,103 @@ data class ServerUserList(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class ServerUserData(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val password: String,
|
||||||
|
val secondPasswordSalt: String,
|
||||||
|
val type: UserType,
|
||||||
|
val timeZone: String,
|
||||||
|
val disableLimitsUntil: Long,
|
||||||
|
val mail: String,
|
||||||
|
val currentDevice: String,
|
||||||
|
val categoryForNotAssignedApps: String,
|
||||||
|
val relaxPrimaryDevice: Boolean,
|
||||||
|
val mailNotificationFlags: Int,
|
||||||
|
val blockedTimes: ImmutableBitmask,
|
||||||
|
val flags: Long,
|
||||||
|
val limitLoginCategory: String?
|
||||||
|
) {
|
||||||
|
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"
|
||||||
|
private const val BLOCKED_TIMES = "blockedTimes"
|
||||||
|
private const val FLAGS = "flags"
|
||||||
|
private const val USER_LIMIT_LOGIN_CATEGORY = "llc"
|
||||||
|
|
||||||
|
fun parse(reader: JsonReader): ServerUserData {
|
||||||
|
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
|
||||||
|
var blockedTimes = ImmutableBitmask(BitSet())
|
||||||
|
var flags = 0L
|
||||||
|
var limitLoginCategory: String? = null
|
||||||
|
|
||||||
|
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()
|
||||||
|
BLOCKED_TIMES -> blockedTimes = ImmutableBitmaskJson.parse(reader.nextString(), Category.BLOCKED_MINUTES_IN_WEEK_LENGTH)
|
||||||
|
FLAGS -> flags = reader.nextLong()
|
||||||
|
USER_LIMIT_LOGIN_CATEGORY -> if (reader.peek() == JsonToken.NULL) reader.nextNull() else limitLoginCategory = reader.nextString()
|
||||||
|
else -> reader.skipValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.endObject()
|
||||||
|
|
||||||
|
return ServerUserData(
|
||||||
|
id = id!!,
|
||||||
|
name = name!!,
|
||||||
|
password = password!!,
|
||||||
|
secondPasswordSalt = secondPasswordSalt!!,
|
||||||
|
type = type!!,
|
||||||
|
timeZone = timeZone!!,
|
||||||
|
disableLimitsUntil = disableLimitsUntil!!,
|
||||||
|
mail = mail!!,
|
||||||
|
currentDevice = currentDevice!!,
|
||||||
|
categoryForNotAssignedApps = categoryForNotAssignedApps,
|
||||||
|
relaxPrimaryDevice = relaxPrimaryDevice,
|
||||||
|
mailNotificationFlags = mailNotificationFlags,
|
||||||
|
blockedTimes = blockedTimes,
|
||||||
|
flags = flags,
|
||||||
|
limitLoginCategory = limitLoginCategory
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseList(reader: JsonReader) = parseJsonArray(reader) { parse(reader) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class ServerDeviceData(
|
data class ServerDeviceData(
|
||||||
val deviceId: String,
|
val deviceId: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
|
|
|
@ -0,0 +1,208 @@
|
||||||
|
/*
|
||||||
|
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation version 3 of the License.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.timelimit.android.ui.login
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MediatorLiveData
|
||||||
|
import io.timelimit.android.async.Threads
|
||||||
|
import io.timelimit.android.data.extensions.getCategoryWithParentCategories
|
||||||
|
import io.timelimit.android.data.model.derived.CompleteUserLoginRelatedData
|
||||||
|
import io.timelimit.android.date.getMinuteOfWeek
|
||||||
|
import io.timelimit.android.integration.platform.BatteryStatus
|
||||||
|
import io.timelimit.android.logic.AppLogic
|
||||||
|
import io.timelimit.android.logic.BlockingReason
|
||||||
|
import io.timelimit.android.logic.RealTime
|
||||||
|
import io.timelimit.android.logic.blockingreason.CategoryHandlingCache
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
|
||||||
|
sealed class AllowUserLoginStatus {
|
||||||
|
data class Allow(val maxTime: Long): AllowUserLoginStatus()
|
||||||
|
data class ForbidByCurrentTime(val missingNetworkTime: Boolean, val maxTime: Long): AllowUserLoginStatus()
|
||||||
|
data class ForbidByCategory(val categoryTitle: String, val blockingReason: BlockingReason, val maxTime: Long): AllowUserLoginStatus()
|
||||||
|
object ForbidUserNotFound: AllowUserLoginStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
object AllowUserLoginStatusUtil {
|
||||||
|
private fun calculate(data: CompleteUserLoginRelatedData, time: RealTime, cache: CategoryHandlingCache, batteryStatus: BatteryStatus): AllowUserLoginStatus = synchronized(cache) {
|
||||||
|
val hasPremium = data.deviceRelatedData.isConnectedAndHasPremium || data.deviceRelatedData.isLocalMode
|
||||||
|
|
||||||
|
if (!hasPremium) {
|
||||||
|
return AllowUserLoginStatus.Allow(maxTime = Long.MAX_VALUE)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.loginRelatedData.user.blockedTimes.dataNotToModify.isEmpty) {
|
||||||
|
if (!time.shouldTrustTimePermanently) {
|
||||||
|
return AllowUserLoginStatus.ForbidByCurrentTime(missingNetworkTime = true, maxTime = Long.MAX_VALUE)
|
||||||
|
} else {
|
||||||
|
val minuteOfWeek = getMinuteOfWeek(time.timeInMillis, TimeZone.getTimeZone(data.loginRelatedData.user.timeZone))
|
||||||
|
|
||||||
|
if (data.loginRelatedData.user.blockedTimes.dataNotToModify[minuteOfWeek]) {
|
||||||
|
val nextAllowedSlot = data.loginRelatedData.user.blockedTimes.dataNotToModify.nextClearBit(minuteOfWeek)
|
||||||
|
val minutesToWait: Long = (nextAllowedSlot - minuteOfWeek).toLong()
|
||||||
|
// not very nice but it works
|
||||||
|
val msToWait = if (minutesToWait <= 1) 5000 else (minutesToWait - 1) * 1000 * 60
|
||||||
|
|
||||||
|
return AllowUserLoginStatus.ForbidByCurrentTime(missingNetworkTime = false, maxTime = time.timeInMillis + msToWait)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (data.limitLoginCategoryUserRelatedData != null && data.loginRelatedData.limitLoginCategory != null) {
|
||||||
|
cache.reportStatus(
|
||||||
|
user = data.limitLoginCategoryUserRelatedData,
|
||||||
|
assumeCurrentDevice = true,
|
||||||
|
timeInMillis = time.timeInMillis,
|
||||||
|
batteryStatus = batteryStatus,
|
||||||
|
shouldTrustTimeTemporarily = time.shouldTrustTimeTemporarily
|
||||||
|
)
|
||||||
|
|
||||||
|
val categoryIds = data.limitLoginCategoryUserRelatedData.getCategoryWithParentCategories(data.loginRelatedData.limitLoginCategory.categoryId)
|
||||||
|
val handlings = categoryIds.map { cache.get(it) }
|
||||||
|
|
||||||
|
val blockingHandling = handlings.find { it.shouldBlockAtSystemLevel }
|
||||||
|
|
||||||
|
if (blockingHandling != null) {
|
||||||
|
AllowUserLoginStatus.ForbidByCategory(
|
||||||
|
categoryTitle = blockingHandling.createdWithCategoryRelatedData.category.title,
|
||||||
|
blockingReason = blockingHandling.systemLevelBlockingReason,
|
||||||
|
maxTime = blockingHandling.dependsOnMaxTime.coerceAtMost(
|
||||||
|
if (data.loginRelatedData.user.blockedTimes.dataNotToModify.isEmpty)
|
||||||
|
Long.MAX_VALUE
|
||||||
|
else
|
||||||
|
time.timeInMillis + 1000 * 5
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val maxTimeByCategories = handlings.minBy { it.dependsOnMaxTime }?.dependsOnMaxTime ?: Long.MAX_VALUE
|
||||||
|
|
||||||
|
AllowUserLoginStatus.Allow(
|
||||||
|
maxTime = maxTimeByCategories.coerceAtMost(
|
||||||
|
if (data.loginRelatedData.user.blockedTimes.dataNotToModify.isEmpty)
|
||||||
|
Long.MAX_VALUE
|
||||||
|
else
|
||||||
|
time.timeInMillis + 1000 * 5
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
AllowUserLoginStatus.Allow(
|
||||||
|
maxTime = if (data.loginRelatedData.user.blockedTimes.dataNotToModify.isEmpty)
|
||||||
|
Long.MAX_VALUE
|
||||||
|
else
|
||||||
|
time.timeInMillis + 1000 * 5
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun calculateSync(logic: AppLogic, userId: String): AllowUserLoginStatus {
|
||||||
|
val userRelatedData = logic.database.derivedDataDao().getUserLoginRelatedDataSync(userId) ?: return AllowUserLoginStatus.ForbidUserNotFound
|
||||||
|
val realTime = RealTime.newInstance()
|
||||||
|
val batteryStatus = logic.platformIntegration.getBatteryStatus()
|
||||||
|
val latch = CountDownLatch(1)
|
||||||
|
|
||||||
|
Threads.mainThreadHandler.post {
|
||||||
|
logic.realTimeLogic.getRealTime(realTime)
|
||||||
|
latch.countDown()
|
||||||
|
}
|
||||||
|
|
||||||
|
latch.await()
|
||||||
|
|
||||||
|
return calculate(
|
||||||
|
data = userRelatedData,
|
||||||
|
batteryStatus = batteryStatus,
|
||||||
|
time = realTime,
|
||||||
|
cache = CategoryHandlingCache()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun calculateLive(logic: AppLogic, userId: String): LiveData<AllowUserLoginStatus> = object : MediatorLiveData<AllowUserLoginStatus>() {
|
||||||
|
val cache = CategoryHandlingCache()
|
||||||
|
val time = RealTime.newInstance()
|
||||||
|
var batteryStatus: BatteryStatus? = null
|
||||||
|
var hasUserLoginRelatedData = false
|
||||||
|
var userLoginRelatedData: CompleteUserLoginRelatedData? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
addSource(logic.platformIntegration.getBatteryStatusLive(), androidx.lifecycle.Observer {
|
||||||
|
batteryStatus = it; update()
|
||||||
|
})
|
||||||
|
|
||||||
|
addSource(logic.database.derivedDataDao().getUserLoginRelatedDataLive(userId), androidx.lifecycle.Observer {
|
||||||
|
userLoginRelatedData = it; hasUserLoginRelatedData = true; update()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
val updateLambda: () -> Unit = { update() }
|
||||||
|
val updateRunnable = Runnable { update() }
|
||||||
|
|
||||||
|
fun update() {
|
||||||
|
val batteryStatus = batteryStatus
|
||||||
|
val userLoginRelatedData = userLoginRelatedData
|
||||||
|
|
||||||
|
if (batteryStatus == null || !hasUserLoginRelatedData) return
|
||||||
|
|
||||||
|
if (userLoginRelatedData == null) {
|
||||||
|
if (value !== AllowUserLoginStatus.ForbidUserNotFound) {
|
||||||
|
value = AllowUserLoginStatus.ForbidUserNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logic.realTimeLogic.getRealTime(time)
|
||||||
|
|
||||||
|
val result = calculate(
|
||||||
|
data = userLoginRelatedData,
|
||||||
|
batteryStatus = batteryStatus,
|
||||||
|
cache = cache,
|
||||||
|
time = time
|
||||||
|
)
|
||||||
|
|
||||||
|
if (result != value) {
|
||||||
|
value = result
|
||||||
|
}
|
||||||
|
|
||||||
|
val scheduledTime: Long = when (result) {
|
||||||
|
AllowUserLoginStatus.ForbidUserNotFound -> Long.MAX_VALUE
|
||||||
|
is AllowUserLoginStatus.ForbidByCurrentTime -> result.maxTime
|
||||||
|
is AllowUserLoginStatus.Allow -> result.maxTime
|
||||||
|
is AllowUserLoginStatus.ForbidByCategory -> result.maxTime
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scheduledTime != Long.MAX_VALUE) {
|
||||||
|
logic.timeApi.cancelScheduledAction(updateRunnable)
|
||||||
|
logic.timeApi.runDelayedByUptime(updateRunnable, scheduledTime - time.timeInMillis)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActive() {
|
||||||
|
super.onActive()
|
||||||
|
|
||||||
|
logic.realTimeLogic.registerTimeModificationListener(updateLambda)
|
||||||
|
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onInactive() {
|
||||||
|
super.onInactive()
|
||||||
|
|
||||||
|
logic.realTimeLogic.unregisterTimeModificationListener(updateLambda)
|
||||||
|
logic.timeApi.cancelScheduledAction(updateRunnable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,7 +27,9 @@ import io.timelimit.android.coroutines.runAsync
|
||||||
import io.timelimit.android.crypto.PasswordHashing
|
import io.timelimit.android.crypto.PasswordHashing
|
||||||
import io.timelimit.android.data.model.User
|
import io.timelimit.android.data.model.User
|
||||||
import io.timelimit.android.data.model.UserType
|
import io.timelimit.android.data.model.UserType
|
||||||
|
import io.timelimit.android.data.model.derived.CompleteUserLoginRelatedData
|
||||||
import io.timelimit.android.livedata.*
|
import io.timelimit.android.livedata.*
|
||||||
|
import io.timelimit.android.logic.BlockingReason
|
||||||
import io.timelimit.android.logic.BlockingReasonUtil
|
import io.timelimit.android.logic.BlockingReasonUtil
|
||||||
import io.timelimit.android.logic.DefaultAppLogic
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
import io.timelimit.android.sync.actions.ChildSignInAction
|
import io.timelimit.android.sync.actions.ChildSignInAction
|
||||||
|
@ -40,21 +42,18 @@ import io.timelimit.android.ui.main.AuthenticatedUser
|
||||||
import io.timelimit.android.ui.manage.parent.key.ScannedKey
|
import io.timelimit.android.ui.manage.parent.key.ScannedKey
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class LoginDialogFragmentModel(application: Application): AndroidViewModel(application) {
|
class LoginDialogFragmentModel(application: Application): AndroidViewModel(application) {
|
||||||
val selectedUserId = MutableLiveData<String?>().apply { value = null }
|
val selectedUserId = MutableLiveData<String?>().apply { value = null }
|
||||||
private val logic = DefaultAppLogic.with(application)
|
private val logic = DefaultAppLogic.with(application)
|
||||||
private val blockingReasonUtil = BlockingReasonUtil(logic)
|
private val blockingReasonUtil = BlockingReasonUtil(logic)
|
||||||
private val users = logic.database.user().getAllUsersLive()
|
private val users = logic.database.user().getAllUsersLive()
|
||||||
private val isConnectedMode = logic.fullVersion.isLocalMode.invert()
|
private val selectedUser = selectedUserId.switchMap { selectedUserId ->
|
||||||
private val selectedUser = users.switchMap { users ->
|
if (selectedUserId != null)
|
||||||
selectedUserId.map { userId ->
|
logic.database.derivedDataDao().getUserLoginRelatedDataLive(selectedUserId)
|
||||||
users.find { it.id == userId }
|
else
|
||||||
}
|
liveDataFromValue(null as CompleteUserLoginRelatedData?)
|
||||||
}
|
}
|
||||||
private val trustedTime = selectedUser.switchMap { blockingReasonUtil.getTrustedMinuteOfWeekLive(TimeZone.getTimeZone(it?.timeZone ?: "GMT")) }
|
|
||||||
private val currentDeviceUser = logic.deviceUserId
|
|
||||||
private val isCheckingPassword = MutableLiveData<Boolean>().apply { value = false }
|
private val isCheckingPassword = MutableLiveData<Boolean>().apply { value = false }
|
||||||
private val wasPasswordWrong = MutableLiveData<Boolean>().apply { value = false }
|
private val wasPasswordWrong = MutableLiveData<Boolean>().apply { value = false }
|
||||||
private val isLoginDone = MutableLiveData<Boolean>().apply { value = false }
|
private val isLoginDone = MutableLiveData<Boolean>().apply { value = false }
|
||||||
|
@ -64,71 +63,70 @@ class LoginDialogFragmentModel(application: Application): AndroidViewModel(appli
|
||||||
if (isLoginDone) {
|
if (isLoginDone) {
|
||||||
liveDataFromValue(LoginDialogDone as LoginDialogStatus)
|
liveDataFromValue(LoginDialogDone as LoginDialogStatus)
|
||||||
} else {
|
} else {
|
||||||
selectedUser.switchMap { selectedUser ->
|
selectedUser.switchMap { selectedUserInfo ->
|
||||||
|
val selectedUser = selectedUserInfo?.loginRelatedData?.user
|
||||||
|
|
||||||
when (selectedUser?.type) {
|
when (selectedUser?.type) {
|
||||||
UserType.Parent -> {
|
UserType.Parent -> {
|
||||||
val isAlreadyCurrentUser = currentDeviceUser.map { it == selectedUser.id }.ignoreUnchanged()
|
val isAlreadyCurrentUser = selectedUserInfo.deviceRelatedData.deviceEntry.currentUserId == selectedUser.id
|
||||||
val loginScreen = isConnectedMode.switchMap { isConnectedMode ->
|
val isConnectedMode = !selectedUserInfo.deviceRelatedData.isLocalMode
|
||||||
isAlreadyCurrentUser.switchMap { isAlreadyCurrentUser ->
|
val loginScreen = isCheckingPassword.switchMap { isCheckingPassword ->
|
||||||
isCheckingPassword.switchMap { isCheckingPassword ->
|
wasPasswordWrong.map { wasPasswordWrong ->
|
||||||
wasPasswordWrong.map { wasPasswordWrong ->
|
ParentUserLogin(
|
||||||
ParentUserLogin(
|
isConnectedMode = isConnectedMode,
|
||||||
isConnectedMode = isConnectedMode,
|
isAlreadyCurrentDeviceUser = isAlreadyCurrentUser,
|
||||||
isAlreadyCurrentDeviceUser = isAlreadyCurrentUser,
|
isCheckingPassword = isCheckingPassword,
|
||||||
isCheckingPassword = isCheckingPassword,
|
wasPasswordWrong = wasPasswordWrong
|
||||||
wasPasswordWrong = wasPasswordWrong
|
) as LoginDialogStatus
|
||||||
) as LoginDialogStatus
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedUser.blockedTimes.dataNotToModify.isEmpty) {
|
AllowUserLoginStatusUtil.calculateLive(logic, selectedUser.id).switchMap { status ->
|
||||||
loginScreen
|
if (status is AllowUserLoginStatus.Allow) {
|
||||||
} else {
|
loginScreen
|
||||||
logic.fullVersion.shouldProvideFullVersionFunctions.switchMap { hasPremium ->
|
} else if (
|
||||||
if (hasPremium) {
|
status is AllowUserLoginStatus.ForbidByCurrentTime ||
|
||||||
trustedTime.switchMap { time ->
|
(status is AllowUserLoginStatus.ForbidByCategory && status.blockingReason == BlockingReason.MissingNetworkTime)
|
||||||
if (time == null) {
|
) {
|
||||||
liveDataFromValue(ParentUserLoginMissingTrustedTime as LoginDialogStatus)
|
liveDataFromValue(ParentUserLoginMissingTrustedTime as LoginDialogStatus)
|
||||||
} else if (selectedUser.blockedTimes.dataNotToModify[time]) {
|
} else if (status is AllowUserLoginStatus.ForbidByCurrentTime) {
|
||||||
liveDataFromValue(ParentUserLoginBlockedTime as LoginDialogStatus)
|
liveDataFromValue(ParentUserLoginBlockedTime as LoginDialogStatus)
|
||||||
} else {
|
} else if (status is AllowUserLoginStatus.ForbidByCategory) {
|
||||||
loginScreen
|
liveDataFromValue(
|
||||||
}
|
ParentUserLoginBlockedByCategory(
|
||||||
}
|
categoryTitle = status.categoryTitle,
|
||||||
} else {
|
reason = status.blockingReason
|
||||||
loginScreen
|
) as LoginDialogStatus
|
||||||
}
|
)
|
||||||
|
} else {
|
||||||
|
loginScreen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
UserType.Child -> {
|
UserType.Child -> {
|
||||||
logic.fullVersion.shouldProvideFullVersionFunctions.switchMap { fullversion ->
|
val hasPremium = selectedUserInfo.deviceRelatedData.isLocalMode || selectedUserInfo.deviceRelatedData.isConnectedAndHasPremium
|
||||||
if (fullversion) {
|
|
||||||
if (selectedUser.password.isEmpty()) {
|
|
||||||
liveDataFromValue(CanNotSignInChildHasNoPassword(childName = selectedUser.name) as LoginDialogStatus)
|
|
||||||
} else {
|
|
||||||
val isAlreadyCurrentUser = currentDeviceUser.map { it == selectedUser.id }.ignoreUnchanged()
|
|
||||||
|
|
||||||
isAlreadyCurrentUser.switchMap { isSignedIn ->
|
if (hasPremium) {
|
||||||
if (isSignedIn) {
|
if (selectedUser.password.isEmpty()) {
|
||||||
liveDataFromValue(ChildAlreadyDeviceUser as LoginDialogStatus)
|
liveDataFromValue(CanNotSignInChildHasNoPassword(childName = selectedUser.name) as LoginDialogStatus)
|
||||||
} else {
|
} else {
|
||||||
isCheckingPassword.switchMap { isCheckingPassword ->
|
val isAlreadyCurrentUser = selectedUserInfo.deviceRelatedData.deviceEntry.currentUserId == selectedUser.id
|
||||||
wasPasswordWrong.map { wasPasswordWrong ->
|
|
||||||
ChildUserLogin(
|
if (isAlreadyCurrentUser) {
|
||||||
isCheckingPassword = isCheckingPassword,
|
liveDataFromValue(ChildAlreadyDeviceUser as LoginDialogStatus)
|
||||||
wasPasswordWrong = wasPasswordWrong
|
} else {
|
||||||
) as LoginDialogStatus
|
isCheckingPassword.switchMap { isCheckingPassword ->
|
||||||
}
|
wasPasswordWrong.map { wasPasswordWrong ->
|
||||||
}
|
ChildUserLogin(
|
||||||
|
isCheckingPassword = isCheckingPassword,
|
||||||
|
wasPasswordWrong = wasPasswordWrong
|
||||||
|
) as LoginDialogStatus
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
liveDataFromValue(ChildLoginRequiresPremiumStatus as LoginDialogStatus)
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
liveDataFromValue(ChildLoginRequiresPremiumStatus as LoginDialogStatus)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
null -> {
|
null -> {
|
||||||
|
@ -155,24 +153,14 @@ class LoginDialogFragmentModel(application: Application): AndroidViewModel(appli
|
||||||
loginLock.withLock {
|
loginLock.withLock {
|
||||||
logic.database.user().getParentUsersLive().waitForNonNullValue().singleOrNull()?.let { user ->
|
logic.database.user().getParentUsersLive().waitForNonNullValue().singleOrNull()?.let { user ->
|
||||||
val emptyPasswordValid = Threads.crypto.executeAndWait { PasswordHashing.validateSync("", user.password) }
|
val emptyPasswordValid = Threads.crypto.executeAndWait { PasswordHashing.validateSync("", user.password) }
|
||||||
|
val hasBlockedTimes = !user.blockedTimes.dataNotToModify.isEmpty
|
||||||
|
|
||||||
val shouldSignIn = if (emptyPasswordValid) {
|
val shouldSignIn = if (emptyPasswordValid) {
|
||||||
val hasBlockedTimes = !user.blockedTimes.dataNotToModify.isEmpty
|
Threads.database.executeAndWait {
|
||||||
|
AllowUserLoginStatusUtil.calculateSync(
|
||||||
if (hasBlockedTimes) {
|
logic = logic,
|
||||||
val hasPremium = logic.fullVersion.shouldProvideFullVersionFunctions.waitForNonNullValue()
|
userId = user.id
|
||||||
|
) is AllowUserLoginStatus.Allow
|
||||||
if (hasPremium) {
|
|
||||||
val isGoodTime = blockingReasonUtil.getTrustedMinuteOfWeekLive(TimeZone.getTimeZone(user.timeZone)).map { minuteOfWeek ->
|
|
||||||
minuteOfWeek != null && user.blockedTimes.dataNotToModify[minuteOfWeek] == false
|
|
||||||
}.waitForNonNullValue()
|
|
||||||
|
|
||||||
isGoodTime
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
|
@ -185,6 +173,10 @@ class LoginDialogFragmentModel(application: Application): AndroidViewModel(appli
|
||||||
secondPasswordHash = Threads.crypto.executeAndWait { PasswordHashing.hashSyncWithSalt("", user.secondPasswordSalt) }
|
secondPasswordHash = Threads.crypto.executeAndWait { PasswordHashing.hashSyncWithSalt("", user.secondPasswordSalt) }
|
||||||
))
|
))
|
||||||
|
|
||||||
|
if (hasBlockedTimes) {
|
||||||
|
Toast.makeText(getApplication(), R.string.manage_parent_blocked_times_toast, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
|
||||||
isLoginDone.value = true
|
isLoginDone.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -230,20 +222,11 @@ class LoginDialogFragmentModel(application: Application): AndroidViewModel(appli
|
||||||
if (user != null && user.type == UserType.Parent) {
|
if (user != null && user.type == UserType.Parent) {
|
||||||
val hasBlockedTimes = !user.blockedTimes.dataNotToModify.isEmpty
|
val hasBlockedTimes = !user.blockedTimes.dataNotToModify.isEmpty
|
||||||
|
|
||||||
val shouldSignIn = if (hasBlockedTimes) {
|
val shouldSignIn = Threads.database.executeAndWait {
|
||||||
val hasPremium = logic.fullVersion.shouldProvideFullVersionFunctions.waitForNonNullValue()
|
AllowUserLoginStatusUtil.calculateSync(
|
||||||
|
logic = logic,
|
||||||
if (hasPremium) {
|
userId = user.id
|
||||||
val isGoodTime = blockingReasonUtil.getTrustedMinuteOfWeekLive(TimeZone.getTimeZone(user.timeZone)).map { minuteOfWeek ->
|
) is AllowUserLoginStatus.Allow
|
||||||
minuteOfWeek != null && user.blockedTimes.dataNotToModify[minuteOfWeek] == false
|
|
||||||
}.waitForNonNullValue()
|
|
||||||
|
|
||||||
isGoodTime
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldSignIn) {
|
if (shouldSignIn) {
|
||||||
|
@ -254,6 +237,10 @@ class LoginDialogFragmentModel(application: Application): AndroidViewModel(appli
|
||||||
secondPasswordHash = "device"
|
secondPasswordHash = "device"
|
||||||
))
|
))
|
||||||
|
|
||||||
|
if (hasBlockedTimes) {
|
||||||
|
Toast.makeText(getApplication(), R.string.manage_parent_blocked_times_toast, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
|
||||||
isLoginDone.value = true
|
isLoginDone.value = true
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(getApplication(), R.string.login_blocked_time, Toast.LENGTH_SHORT).show()
|
Toast.makeText(getApplication(), R.string.login_blocked_time, Toast.LENGTH_SHORT).show()
|
||||||
|
@ -274,15 +261,17 @@ class LoginDialogFragmentModel(application: Application): AndroidViewModel(appli
|
||||||
try {
|
try {
|
||||||
isCheckingPassword.value = true
|
isCheckingPassword.value = true
|
||||||
|
|
||||||
val userEntry = selectedUser.waitForNullableValue()
|
val userEntryInfo = selectedUser.waitForNullableValue()
|
||||||
val ownDeviceId = logic.deviceId.waitForNullableValue()
|
val userEntry = userEntryInfo?.loginRelatedData?.user
|
||||||
|
|
||||||
if (userEntry?.type != UserType.Parent || ownDeviceId == null) {
|
if (userEntry?.type != UserType.Parent) {
|
||||||
selectedUserId.value = null
|
selectedUserId.value = null
|
||||||
|
|
||||||
return@runAsync
|
return@runAsync
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val ownDeviceId = userEntryInfo.deviceRelatedData.deviceEntry.id
|
||||||
|
|
||||||
val passwordValid = Threads.crypto.executeAndWait { PasswordHashing.validateSync(password, userEntry.password) }
|
val passwordValid = Threads.crypto.executeAndWait { PasswordHashing.validateSync(password, userEntry.password) }
|
||||||
|
|
||||||
if (!passwordValid) {
|
if (!passwordValid) {
|
||||||
|
@ -299,12 +288,28 @@ class LoginDialogFragmentModel(application: Application): AndroidViewModel(appli
|
||||||
secondPasswordHash = secondPasswordHash
|
secondPasswordHash = secondPasswordHash
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val hasBlockedTimes = !userEntry.blockedTimes.dataNotToModify.isEmpty
|
||||||
|
val shouldSignIn = Threads.database.executeAndWait {
|
||||||
|
AllowUserLoginStatusUtil.calculateSync(
|
||||||
|
logic = logic,
|
||||||
|
userId = userEntry.id
|
||||||
|
) is AllowUserLoginStatus.Allow
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shouldSignIn) {
|
||||||
|
Toast.makeText(getApplication(), R.string.login_blocked_time, Toast.LENGTH_SHORT).show()
|
||||||
|
|
||||||
|
return@runAsync
|
||||||
|
}
|
||||||
|
|
||||||
model.setAuthenticatedUser(authenticatedUser)
|
model.setAuthenticatedUser(authenticatedUser)
|
||||||
|
|
||||||
if (setAsDeviceUser) {
|
if (hasBlockedTimes) {
|
||||||
val deviceEntry = logic.deviceEntry.waitForNonNullValue()!!
|
Toast.makeText(getApplication(), R.string.manage_parent_blocked_times_toast, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
|
||||||
if (deviceEntry.currentUserId != userEntry.id) {
|
if (setAsDeviceUser) {
|
||||||
|
if (userEntryInfo.deviceRelatedData.deviceEntry.currentUserId != userEntry.id) {
|
||||||
ActivityViewModel.dispatchWithoutCheckOrCatching(
|
ActivityViewModel.dispatchWithoutCheckOrCatching(
|
||||||
SetDeviceUserAction(
|
SetDeviceUserAction(
|
||||||
deviceId = ownDeviceId,
|
deviceId = ownDeviceId,
|
||||||
|
@ -319,7 +324,7 @@ class LoginDialogFragmentModel(application: Application): AndroidViewModel(appli
|
||||||
if (keepSignedIn) {
|
if (keepSignedIn) {
|
||||||
if (
|
if (
|
||||||
setAsDeviceUser ||
|
setAsDeviceUser ||
|
||||||
(currentDeviceUser.waitForNullableValue() == userEntry.id)
|
userEntryInfo.deviceRelatedData.deviceEntry.currentUserId == userEntry.id
|
||||||
) {
|
) {
|
||||||
ActivityViewModel.dispatchWithoutCheckOrCatching(
|
ActivityViewModel.dispatchWithoutCheckOrCatching(
|
||||||
SetKeepSignedInAction(
|
SetKeepSignedInAction(
|
||||||
|
@ -341,18 +346,17 @@ class LoginDialogFragmentModel(application: Application): AndroidViewModel(appli
|
||||||
}
|
}
|
||||||
|
|
||||||
fun tryChildLogin(
|
fun tryChildLogin(
|
||||||
password: String,
|
password: String
|
||||||
model: ActivityViewModel
|
|
||||||
) {
|
) {
|
||||||
runAsync {
|
runAsync {
|
||||||
loginLock.withLock {
|
loginLock.withLock {
|
||||||
try {
|
try {
|
||||||
isCheckingPassword.value = true
|
isCheckingPassword.value = true
|
||||||
|
|
||||||
val userEntry = selectedUser.waitForNullableValue()
|
val userEntryInfo = selectedUser.waitForNullableValue()
|
||||||
val ownDeviceId = logic.deviceId.waitForNullableValue()
|
val userEntry = userEntryInfo?.loginRelatedData?.user
|
||||||
|
|
||||||
if (userEntry?.type != UserType.Child || ownDeviceId == null) {
|
if (userEntry?.type != UserType.Child) {
|
||||||
selectedUserId.value = null
|
selectedUserId.value = null
|
||||||
|
|
||||||
return@runAsync
|
return@runAsync
|
||||||
|
@ -409,6 +413,7 @@ sealed class LoginDialogStatus
|
||||||
data class UserListLoginDialogStatus(val usersToShow: List<User>, val isLocalMode: Boolean): LoginDialogStatus()
|
data class UserListLoginDialogStatus(val usersToShow: List<User>, val isLocalMode: Boolean): LoginDialogStatus()
|
||||||
object ParentUserLoginMissingTrustedTime: LoginDialogStatus()
|
object ParentUserLoginMissingTrustedTime: LoginDialogStatus()
|
||||||
object ParentUserLoginBlockedTime: LoginDialogStatus()
|
object ParentUserLoginBlockedTime: LoginDialogStatus()
|
||||||
|
data class ParentUserLoginBlockedByCategory(val categoryTitle: String, val reason: BlockingReason): LoginDialogStatus()
|
||||||
data class ParentUserLogin(
|
data class ParentUserLogin(
|
||||||
val isConnectedMode: Boolean,
|
val isConnectedMode: Boolean,
|
||||||
val isAlreadyCurrentDeviceUser: Boolean,
|
val isAlreadyCurrentDeviceUser: Boolean,
|
||||||
|
|
|
@ -34,6 +34,7 @@ import io.timelimit.android.async.Threads
|
||||||
import io.timelimit.android.data.model.User
|
import io.timelimit.android.data.model.User
|
||||||
import io.timelimit.android.databinding.NewLoginFragmentBinding
|
import io.timelimit.android.databinding.NewLoginFragmentBinding
|
||||||
import io.timelimit.android.extensions.setOnEnterListenr
|
import io.timelimit.android.extensions.setOnEnterListenr
|
||||||
|
import io.timelimit.android.logic.BlockingReason
|
||||||
import io.timelimit.android.ui.main.getActivityViewModel
|
import io.timelimit.android.ui.main.getActivityViewModel
|
||||||
import io.timelimit.android.ui.manage.parent.key.ScannedKey
|
import io.timelimit.android.ui.manage.parent.key.ScannedKey
|
||||||
import io.timelimit.android.ui.view.KeyboardViewListener
|
import io.timelimit.android.ui.view.KeyboardViewListener
|
||||||
|
@ -51,6 +52,7 @@ class NewLoginFragment: DialogFragment() {
|
||||||
private const val CHILD_LOGIN_REQUIRES_PREMIUM = 5
|
private const val CHILD_LOGIN_REQUIRES_PREMIUM = 5
|
||||||
private const val BLOCKED_LOGIN_TIME = 6
|
private const val BLOCKED_LOGIN_TIME = 6
|
||||||
private const val UNVERIFIED_TIME = 7
|
private const val UNVERIFIED_TIME = 7
|
||||||
|
private const val PARENT_LOGIN_BLOCKED = 8
|
||||||
}
|
}
|
||||||
|
|
||||||
private val model: LoginDialogFragmentModel by lazy {
|
private val model: LoginDialogFragmentModel by lazy {
|
||||||
|
@ -170,8 +172,7 @@ class NewLoginFragment: DialogFragment() {
|
||||||
binding.childPassword.apply {
|
binding.childPassword.apply {
|
||||||
password.setOnEnterListenr {
|
password.setOnEnterListenr {
|
||||||
model.tryChildLogin(
|
model.tryChildLogin(
|
||||||
password = password.text.toString(),
|
password = password.text.toString()
|
||||||
model = getActivityViewModel(activity!!)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -307,6 +308,30 @@ class NewLoginFragment: DialogFragment() {
|
||||||
binding.switcher.displayedChild = CHILD_LOGIN_REQUIRES_PREMIUM
|
binding.switcher.displayedChild = CHILD_LOGIN_REQUIRES_PREMIUM
|
||||||
}
|
}
|
||||||
|
|
||||||
|
null
|
||||||
|
}
|
||||||
|
is ParentUserLoginBlockedByCategory -> {
|
||||||
|
if (binding.switcher.displayedChild != PARENT_LOGIN_BLOCKED) {
|
||||||
|
binding.switcher.setInAnimation(context!!, R.anim.wizard_open_step_in)
|
||||||
|
binding.switcher.setOutAnimation(context!!, R.anim.wizard_open_step_out)
|
||||||
|
binding.switcher.displayedChild = PARENT_LOGIN_BLOCKED
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.parentLoginBlocked.categoryTitle = status.categoryTitle
|
||||||
|
binding.parentLoginBlocked.reason = when (status.reason) {
|
||||||
|
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.NotificationsAreBlocked -> getString(R.string.lock_reason_short_notification_blocking)
|
||||||
|
BlockingReason.BatteryLimit -> getString(R.string.lock_reason_short_battery_limit)
|
||||||
|
BlockingReason.SessionDurationLimit -> getString(R.string.lock_reason_short_session_duration)
|
||||||
|
BlockingReason.NotPartOfAnCategory -> "???"
|
||||||
|
BlockingReason.None -> "???"
|
||||||
|
}
|
||||||
|
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}.let { /* require handling all cases */ }
|
}.let { /* require handling all cases */ }
|
||||||
|
|
|
@ -28,6 +28,7 @@ import androidx.navigation.Navigation
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
import io.timelimit.android.data.model.User
|
import io.timelimit.android.data.model.User
|
||||||
import io.timelimit.android.databinding.FragmentManageParentBinding
|
import io.timelimit.android.databinding.FragmentManageParentBinding
|
||||||
|
import io.timelimit.android.databinding.ParentLimitLoginViewBinding
|
||||||
import io.timelimit.android.extensions.safeNavigate
|
import io.timelimit.android.extensions.safeNavigate
|
||||||
import io.timelimit.android.livedata.liveDataFromValue
|
import io.timelimit.android.livedata.liveDataFromValue
|
||||||
import io.timelimit.android.livedata.map
|
import io.timelimit.android.livedata.map
|
||||||
|
@ -39,6 +40,7 @@ import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
||||||
import io.timelimit.android.ui.manage.child.advanced.timezone.UserTimezoneView
|
import io.timelimit.android.ui.manage.child.advanced.timezone.UserTimezoneView
|
||||||
import io.timelimit.android.ui.manage.parent.delete.DeleteParentView
|
import io.timelimit.android.ui.manage.parent.delete.DeleteParentView
|
||||||
import io.timelimit.android.ui.manage.parent.key.ManageUserKeyView
|
import io.timelimit.android.ui.manage.parent.key.ManageUserKeyView
|
||||||
|
import io.timelimit.android.ui.manage.parent.limitlogin.ParentLimitLoginView
|
||||||
|
|
||||||
class ManageParentFragment : Fragment(), FragmentWithCustomTitle {
|
class ManageParentFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
private val activity: ActivityViewModelHolder by lazy { getActivity() as ActivityViewModelHolder }
|
private val activity: ActivityViewModelHolder by lazy { getActivity() as ActivityViewModelHolder }
|
||||||
|
@ -124,6 +126,14 @@ class ManageParentFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
fragmentManager = parentFragmentManager
|
fragmentManager = parentFragmentManager
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ParentLimitLoginView.bind(
|
||||||
|
view = binding.parentLimitLogin,
|
||||||
|
lifecycleOwner = viewLifecycleOwner,
|
||||||
|
userId = params.parentId,
|
||||||
|
auth = activity.getActivityViewModel(),
|
||||||
|
fragmentManager = parentFragmentManager
|
||||||
|
)
|
||||||
|
|
||||||
binding.handlers = object: ManageParentFragmentHandlers {
|
binding.handlers = object: ManageParentFragmentHandlers {
|
||||||
override fun onChangePasswordClicked() {
|
override fun onChangePasswordClicked() {
|
||||||
navigation.safeNavigate(
|
navigation.safeNavigate(
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -48,14 +48,19 @@ class DeleteParentModel(application: Application): AndroidViewModel(application)
|
||||||
} != null
|
} != null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private val isLastWithoutLoginLimit = parentUserIdLive.switchMap { userId ->
|
||||||
|
database.userLimitLoginCategoryDao().countOtherUsersWithoutLimitLoginCategoryLive(userId).map { it == 0L }
|
||||||
|
}
|
||||||
|
|
||||||
private val statusIgnoringLinkingLive = parentUserIdLive.switchMap { parentUserId ->
|
private val statusIgnoringLinkingLive = parentUserIdLive.switchMap { parentUserId ->
|
||||||
authenticatedUserLive.map { authenticatedUser ->
|
authenticatedUserLive.switchMap { authenticatedUser ->
|
||||||
if (authenticatedUser?.second?.type != UserType.Parent) {
|
isLastWithoutLoginLimit.map { lastWithoutLoginLimit ->
|
||||||
Status.NotAuthenticated
|
if (authenticatedUser?.second?.type != UserType.Parent) {
|
||||||
} else {
|
Status.NotAuthenticated
|
||||||
if (authenticatedUser.second.id == parentUserId) {
|
} else if (authenticatedUser.second.id == parentUserId) {
|
||||||
Status.WrongAccount
|
Status.WrongAccount
|
||||||
|
} else if (lastWithoutLoginLimit) {
|
||||||
|
Status.LastWihtoutLoginLimit
|
||||||
} else {
|
} else {
|
||||||
Status.Ready
|
Status.Ready
|
||||||
}
|
}
|
||||||
|
@ -142,5 +147,6 @@ enum class Status {
|
||||||
LastLinked,
|
LastLinked,
|
||||||
NotAuthenticated,
|
NotAuthenticated,
|
||||||
WrongAccount,
|
WrongAccount,
|
||||||
|
LastWihtoutLoginLimit,
|
||||||
Ready
|
Ready
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -41,6 +41,7 @@ object DeleteParentView {
|
||||||
Status.NotAuthenticated -> context.getString(R.string.manage_parent_remove_user_status_not_authenticated, userName)
|
Status.NotAuthenticated -> context.getString(R.string.manage_parent_remove_user_status_not_authenticated, userName)
|
||||||
Status.WrongAccount -> context.getString(R.string.manage_parent_remove_user_status_wrong_account, userName)
|
Status.WrongAccount -> context.getString(R.string.manage_parent_remove_user_status_wrong_account, userName)
|
||||||
Status.Ready -> context.getString(R.string.manage_parent_remove_user_status_ready, userName)
|
Status.Ready -> context.getString(R.string.manage_parent_remove_user_status_ready, userName)
|
||||||
|
Status.LastWihtoutLoginLimit -> context.getString(R.string.manage_parent_remove_user_status_last_without_login_limit)
|
||||||
null -> ""
|
null -> ""
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation version 3 of the License.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.ui.manage.parent.limitlogin
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import io.timelimit.android.R
|
||||||
|
import io.timelimit.android.extensions.showSafe
|
||||||
|
|
||||||
|
class LimitLoginRestrictedToUserItselfDialogFragment: DialogFragment() {
|
||||||
|
companion object {
|
||||||
|
private const val DIALOG_TAG = "LimitLoginRestrictedToUserItselfDialogFragment"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
return AlertDialog.Builder(context!!, theme)
|
||||||
|
.setMessage(R.string.parent_limit_login_error_user_itself)
|
||||||
|
.setPositiveButton(R.string.generic_ok, null)
|
||||||
|
.create()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun show(fragmentManager: FragmentManager) = showSafe(fragmentManager, DIALOG_TAG)
|
||||||
|
}
|
|
@ -0,0 +1,138 @@
|
||||||
|
/*
|
||||||
|
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation version 3 of the License.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.timelimit.android.ui.manage.parent.limitlogin
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.CheckedTextView
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
|
import io.timelimit.android.R
|
||||||
|
import io.timelimit.android.data.model.UserType
|
||||||
|
import io.timelimit.android.databinding.BottomSheetSelectionListBinding
|
||||||
|
import io.timelimit.android.extensions.showSafe
|
||||||
|
import io.timelimit.android.livedata.map
|
||||||
|
import io.timelimit.android.livedata.switchMap
|
||||||
|
import io.timelimit.android.sync.actions.UpdateUserLimitLoginCategory
|
||||||
|
import io.timelimit.android.ui.main.ActivityViewModelHolder
|
||||||
|
import io.timelimit.android.ui.payment.RequiresPurchaseDialogFragment
|
||||||
|
|
||||||
|
class ParentLimitLoginSelectCategoryDialogFragment: BottomSheetDialogFragment() {
|
||||||
|
companion object {
|
||||||
|
private const val DIALOG_TAG = "ParentLimitLoginSelectCategoryDialogFragment"
|
||||||
|
private const val USER_ID = "userId"
|
||||||
|
|
||||||
|
fun newInstance(userId: String) = ParentLimitLoginSelectCategoryDialogFragment().apply {
|
||||||
|
arguments = Bundle().apply {
|
||||||
|
putString(USER_ID, userId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
val userId = arguments!!.getString(USER_ID)!!
|
||||||
|
val auth = (activity as ActivityViewModelHolder).getActivityViewModel()
|
||||||
|
val logic = auth.logic
|
||||||
|
val options = logic.database.userLimitLoginCategoryDao().getLimitLoginCategoryOptions(userId)
|
||||||
|
val hasPremium = logic.fullVersion.shouldProvideFullVersionFunctions
|
||||||
|
|
||||||
|
val binding = BottomSheetSelectionListBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
|
binding.title = getString(R.string.parent_limit_login_title)
|
||||||
|
|
||||||
|
val list = binding.list
|
||||||
|
|
||||||
|
hasPremium.switchMap { a ->
|
||||||
|
options.switchMap { b ->
|
||||||
|
auth.authenticatedUser.map { c ->
|
||||||
|
Triple(a, b, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.observe(viewLifecycleOwner, Observer { (hasPremium, categoryList, user) ->
|
||||||
|
if (user?.second?.type != UserType.Parent) {
|
||||||
|
dismissAllowingStateLoss(); return@Observer
|
||||||
|
}
|
||||||
|
|
||||||
|
val isUserItself = user.second.id == userId
|
||||||
|
|
||||||
|
val hasSelection = categoryList.find { it.selected } != null
|
||||||
|
|
||||||
|
list.removeAllViews()
|
||||||
|
|
||||||
|
fun buildRow(): CheckedTextView = LayoutInflater.from(context!!).inflate(
|
||||||
|
android.R.layout.simple_list_item_single_choice,
|
||||||
|
list,
|
||||||
|
false
|
||||||
|
) as CheckedTextView
|
||||||
|
|
||||||
|
categoryList.forEach { category ->
|
||||||
|
val row = buildRow()
|
||||||
|
|
||||||
|
row.text = getString(R.string.parent_limit_login_dialog_item, category.childTitle, category.categoryTitle)
|
||||||
|
row.isChecked = category.selected
|
||||||
|
row.setOnClickListener {
|
||||||
|
if (!hasPremium) {
|
||||||
|
RequiresPurchaseDialogFragment().show(parentFragmentManager)
|
||||||
|
} else if (!row.isChecked) {
|
||||||
|
if (isUserItself) {
|
||||||
|
auth.tryDispatchParentAction(
|
||||||
|
UpdateUserLimitLoginCategory(
|
||||||
|
userId = userId,
|
||||||
|
categoryId = category.categoryId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
dismiss()
|
||||||
|
} else {
|
||||||
|
LimitLoginRestrictedToUserItselfDialogFragment().show(parentFragmentManager)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
list.addView(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
buildRow().let { row ->
|
||||||
|
row.setText(R.string.parent_limit_login_dialog_no_selection)
|
||||||
|
row.isChecked = !hasSelection
|
||||||
|
row.setOnClickListener {
|
||||||
|
if (!row.isChecked) {
|
||||||
|
auth.tryDispatchParentAction(
|
||||||
|
UpdateUserLimitLoginCategory(
|
||||||
|
userId = userId,
|
||||||
|
categoryId = null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
list.addView(row)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
fun show(fragmentManager: FragmentManager) = showSafe(fragmentManager, DIALOG_TAG)
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
/*
|
||||||
|
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation version 3 of the License.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.timelimit.android.ui.manage.parent.limitlogin
|
||||||
|
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import io.timelimit.android.R
|
||||||
|
import io.timelimit.android.databinding.ParentLimitLoginViewBinding
|
||||||
|
import io.timelimit.android.livedata.map
|
||||||
|
import io.timelimit.android.livedata.switchMap
|
||||||
|
import io.timelimit.android.ui.help.HelpDialogFragment
|
||||||
|
import io.timelimit.android.ui.main.ActivityViewModel
|
||||||
|
|
||||||
|
object ParentLimitLoginView {
|
||||||
|
fun bind(
|
||||||
|
view: ParentLimitLoginViewBinding,
|
||||||
|
lifecycleOwner: LifecycleOwner,
|
||||||
|
userId: String,
|
||||||
|
auth: ActivityViewModel,
|
||||||
|
fragmentManager: FragmentManager
|
||||||
|
) {
|
||||||
|
val database = auth.logic.database
|
||||||
|
val context = view.root.context
|
||||||
|
|
||||||
|
view.titleView.setOnClickListener {
|
||||||
|
HelpDialogFragment.newInstance(
|
||||||
|
title = R.string.parent_limit_login_title,
|
||||||
|
text = R.string.parent_limit_login_help
|
||||||
|
).show(fragmentManager)
|
||||||
|
}
|
||||||
|
|
||||||
|
view.changeButton.setOnClickListener {
|
||||||
|
if (auth.requestAuthenticationOrReturnTrue()) {
|
||||||
|
ParentLimitLoginSelectCategoryDialogFragment.newInstance(userId).show(fragmentManager)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
database.userLimitLoginCategoryDao().countOtherUsersWithoutLimitLoginCategoryLive(userId).switchMap { otherUsers ->
|
||||||
|
database.userLimitLoginCategoryDao().getByParentUserIdLive(userId).map { config ->
|
||||||
|
otherUsers to config
|
||||||
|
}
|
||||||
|
}.observe(lifecycleOwner, Observer { (otherUsers, config) ->
|
||||||
|
if (otherUsers == 0L) {
|
||||||
|
view.canConfigure = false
|
||||||
|
view.status = context.getString(R.string.parent_limit_login_status_needs_other_user)
|
||||||
|
} else {
|
||||||
|
view.canConfigure = true
|
||||||
|
view.status = if (config == null)
|
||||||
|
context.getString(R.string.parent_limit_login_status_disabled)
|
||||||
|
else
|
||||||
|
context.getString(R.string.parent_limit_login_status_enabled, config.categoryTitle, config.childTitle)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -163,6 +163,9 @@
|
||||||
<include android:id="@+id/timezone"
|
<include android:id="@+id/timezone"
|
||||||
layout="@layout/user_timezone_view" />
|
layout="@layout/user_timezone_view" />
|
||||||
|
|
||||||
|
<include android:id="@+id/parent_limit_login"
|
||||||
|
layout="@layout/parent_limit_login_view" />
|
||||||
|
|
||||||
<androidx.cardview.widget.CardView
|
<androidx.cardview.widget.CardView
|
||||||
app:cardUseCompatPadding="true"
|
app:cardUseCompatPadding="true"
|
||||||
android:onClick="@{() -> handlers.onManageBlockedTimesClicked()}"
|
android:onClick="@{() -> handlers.onManageBlockedTimesClicked()}"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!--
|
<!--
|
||||||
TimeLimit Copyright <C> 2019 Jonas Lochmann
|
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
the Free Software Foundation version 3 of the License.
|
the Free Software Foundation version 3 of the License.
|
||||||
|
@ -42,5 +42,8 @@
|
||||||
|
|
||||||
<include layout="@layout/new_login_fragment_missing_trusted_time" />
|
<include layout="@layout/new_login_fragment_missing_trusted_time" />
|
||||||
|
|
||||||
|
<include layout="@layout/new_login_fragment_parent_login_blocked"
|
||||||
|
android:id="@+id/parent_login_blocked" />
|
||||||
|
|
||||||
</io.timelimit.android.ui.view.SafeViewFlipper>
|
</io.timelimit.android.ui.view.SafeViewFlipper>
|
||||||
</layout>
|
</layout>
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation version 3 of the License.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<data>
|
||||||
|
<variable
|
||||||
|
name="categoryTitle"
|
||||||
|
type="String" />
|
||||||
|
|
||||||
|
<variable
|
||||||
|
name="reason"
|
||||||
|
type="String" />
|
||||||
|
</data>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:textAppearance="?android:textAppearanceMedium"
|
||||||
|
android:padding="8dp"
|
||||||
|
tools:text="@string/login_category_blocked"
|
||||||
|
android:text="@{@string/login_category_blocked(categoryTitle, reason)}"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
</layout>
|
74
app/src/main/res/layout/parent_limit_login_view.xml
Normal file
74
app/src/main/res/layout/parent_limit_login_view.xml
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation version 3 of the License.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<data>
|
||||||
|
<variable
|
||||||
|
name="status"
|
||||||
|
type="String" />
|
||||||
|
|
||||||
|
<variable
|
||||||
|
name="canConfigure"
|
||||||
|
type="boolean" />
|
||||||
|
</data>
|
||||||
|
|
||||||
|
<androidx.cardview.widget.CardView
|
||||||
|
app:cardUseCompatPadding="true"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="8dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:text="@string/parent_limit_login_title"
|
||||||
|
tools:ignore="UnusedAttribute"
|
||||||
|
android:drawableTint="?colorOnSurface"
|
||||||
|
android:id="@+id/title_view"
|
||||||
|
android:background="?selectableItemBackground"
|
||||||
|
android:drawableEnd="@drawable/ic_info_outline_black_24dp"
|
||||||
|
android:textAppearance="?android:textAppearanceLarge"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:textAppearance="?android:textAppearanceMedium"
|
||||||
|
tools:text="@string/parent_limit_login_status_needs_other_user"
|
||||||
|
android:text="@{status}"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:enabled="@{canConfigure}"
|
||||||
|
android:id="@+id/change_button"
|
||||||
|
android:text="@string/parent_limit_login_select_button"
|
||||||
|
style="?materialButtonOutlinedStyle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:textAppearance="?android:textAppearanceSmall"
|
||||||
|
android:text="@string/purchase_required_info_local_mode_free"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
|
</layout>
|
|
@ -32,4 +32,5 @@
|
||||||
<string name="login_scan_code">Code scannen</string>
|
<string name="login_scan_code">Code scannen</string>
|
||||||
<string name="login_scan_code_err_expired">Dieser Code ist alt</string>
|
<string name="login_scan_code_err_expired">Dieser Code ist alt</string>
|
||||||
<string name="login_scan_code_err_not_linked">Dieser Schlüssel ist nicht verknüpft</string>
|
<string name="login_scan_code_err_not_linked">Dieser Schlüssel ist nicht verknüpft</string>
|
||||||
|
<string name="login_category_blocked">Die Kategorie %1$s ist momentan gesperrt (%2$s), sodass Sie sich nicht mit diesem Benutzer anmelden können</string>
|
||||||
</resources>
|
</resources>
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!--
|
<!--
|
||||||
TimeLimit Copyright <C> 2019 Jonas Lochmann
|
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
the Free Software Foundation version 3 of the License.
|
the Free Software Foundation version 3 of the License.
|
||||||
|
@ -14,8 +14,9 @@
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-->
|
-->
|
||||||
<resources>
|
<resources>
|
||||||
<string name="manage_parent_blocked_times_title">Zeitschloss</string>
|
<string name="manage_parent_blocked_times_title">Zeitschloss (veraltet)</string>
|
||||||
<string name="manage_parent_blocked_times_description">
|
<string name="manage_parent_blocked_times_description">
|
||||||
|
Sie können anstelle dieser Funktion eine Anmeldeverhinderungskategorie verwenden.
|
||||||
Hiermit können Zeiten festgelegt werden, an denen sich ein Elternteil nicht anmelden kann.
|
Hiermit können Zeiten festgelegt werden, an denen sich ein Elternteil nicht anmelden kann.
|
||||||
Das ist bei der Selbstbeschränkung relevant.
|
Das ist bei der Selbstbeschränkung relevant.
|
||||||
</string>
|
</string>
|
||||||
|
@ -28,4 +29,9 @@
|
||||||
<string name="manage_parent_blocked_action_reset">Einstellungen zurücksetzen</string>
|
<string name="manage_parent_blocked_action_reset">Einstellungen zurücksetzen</string>
|
||||||
|
|
||||||
<string name="manage_parent_lockout_hour_rule">Um ein Aussperren zu verhindern können maximal 18 Stunden je Tag blockiert werden</string>
|
<string name="manage_parent_lockout_hour_rule">Um ein Aussperren zu verhindern können maximal 18 Stunden je Tag blockiert werden</string>
|
||||||
|
|
||||||
|
<string name="manage_parent_blocked_times_toast">
|
||||||
|
Sie verwenden das veraltete Zeitschloss für Ihren Elternbenutzer.
|
||||||
|
Sie sollten stattdessen die Anmeldeverhinderungskategorie verwenden.
|
||||||
|
</string>
|
||||||
</resources>
|
</resources>
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!--
|
<!--
|
||||||
TimeLimit Copyright <C> 2019 Jonas Lochmann
|
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
the Free Software Foundation version 3 of the License.
|
the Free Software Foundation version 3 of the License.
|
||||||
|
@ -48,6 +48,9 @@
|
||||||
<string name="manage_parent_remove_user_status_ready">
|
<string name="manage_parent_remove_user_status_ready">
|
||||||
Geben Sie das Passwort von %1$s ein, um %1$s zu löschen.
|
Geben Sie das Passwort von %1$s ein, um %1$s zu löschen.
|
||||||
</string>
|
</string>
|
||||||
|
<string name="manage_parent_remove_user_status_last_without_login_limit">Das ist der letzte Benutzer,
|
||||||
|
der sich jederzeit anmelden kann. Daher kann dieser nicht entfernt werden.
|
||||||
|
</string>
|
||||||
|
|
||||||
<string name="manage_parent_notifications_title">Benachrichtigungs-E-Mails</string>
|
<string name="manage_parent_notifications_title">Benachrichtigungs-E-Mails</string>
|
||||||
<string name="manage_parent_notifications_needs_linked_mail">Sie müssen eine E-Mail-Adresse hinterlegen, um diese Funktion zu benutzen</string>
|
<string name="manage_parent_notifications_needs_linked_mail">Sie müssen eine E-Mail-Adresse hinterlegen, um diese Funktion zu benutzen</string>
|
||||||
|
|
34
app/src/main/res/values-de/strings-parent-limit-login.xml
Normal file
34
app/src/main/res/values-de/strings-parent-limit-login.xml
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation version 3 of the License.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
<resources>
|
||||||
|
<string name="parent_limit_login_title">Anmeldeverhinderungskategorie</string>
|
||||||
|
<string name="parent_limit_login_help">Hier können Sie eine Kategorie wählen.
|
||||||
|
Sobald diese Kategorie gesperrt ist können Sie sich nicht mehr als Elternteil anmelden, sodass
|
||||||
|
Sie dann keine Einstellungen mehr ändern können.
|
||||||
|
Damit können Sie verhindern, dass Sie Einschränkungen abschalten, die Sie sich gesetzt haben,
|
||||||
|
während diese wirken.
|
||||||
|
</string>
|
||||||
|
<string name="parent_limit_login_status_disabled">Sie können sich jederzeit anmelden</string>
|
||||||
|
<string name="parent_limit_login_status_enabled">Sie können sich nicht anmelden, wenn %s$1 für %s$2 gesperrt ist</string>
|
||||||
|
<string name="parent_limit_login_status_needs_other_user">Sie müssen einen Elternteil-Benutzer als Backup (damit Sie
|
||||||
|
sich nicht aussperren) erstellen, bevor Sie diese Funktion verwenden können.</string>
|
||||||
|
<string name="parent_limit_login_select_button">Kategorie wählen</string>
|
||||||
|
<string name="parent_limit_login_dialog_no_selection">keine Kategorie</string>
|
||||||
|
<string name="parent_limit_login_dialog_item">%1$s/%2$s</string>
|
||||||
|
<string name="parent_limit_login_error_user_itself">Sie müssen als der entsprechende Benutzer angemeldet sein, um eine Kategorie
|
||||||
|
zu wählen. Andere Benutzer können diese Funktion nur wieder deaktivieren.
|
||||||
|
</string>
|
||||||
|
</resources>
|
|
@ -32,4 +32,5 @@
|
||||||
<string name="login_scan_code">Scan code</string>
|
<string name="login_scan_code">Scan code</string>
|
||||||
<string name="login_scan_code_err_expired">This code is old</string>
|
<string name="login_scan_code_err_expired">This code is old</string>
|
||||||
<string name="login_scan_code_err_not_linked">This key is not linked</string>
|
<string name="login_scan_code_err_not_linked">This key is not linked</string>
|
||||||
|
<string name="login_category_blocked">The category %1$s is blocked now (%2$s) so you can not sign in with this user now</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!--
|
<!--
|
||||||
TimeLimit Copyright <C> 2019 Jonas Lochmann
|
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
the Free Software Foundation version 3 of the License.
|
the Free Software Foundation version 3 of the License.
|
||||||
|
@ -14,8 +14,9 @@
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-->
|
-->
|
||||||
<resources>
|
<resources>
|
||||||
<string name="manage_parent_blocked_times_title">Time lock</string>
|
<string name="manage_parent_blocked_times_title">Time lock (deprecated)</string>
|
||||||
<string name="manage_parent_blocked_times_description">
|
<string name="manage_parent_blocked_times_description">
|
||||||
|
You can use a limit login category instead.
|
||||||
This allows to set times at which the parent can not sign in.
|
This allows to set times at which the parent can not sign in.
|
||||||
This is relevant for limiting ones own usage.
|
This is relevant for limiting ones own usage.
|
||||||
</string>
|
</string>
|
||||||
|
@ -28,4 +29,9 @@
|
||||||
<string name="manage_parent_blocked_action_reset">Reset limits</string>
|
<string name="manage_parent_blocked_action_reset">Reset limits</string>
|
||||||
|
|
||||||
<string name="manage_parent_lockout_hour_rule">To prevent losing access, you can only block 18 hours per day</string>
|
<string name="manage_parent_lockout_hour_rule">To prevent losing access, you can only block 18 hours per day</string>
|
||||||
|
|
||||||
|
<string name="manage_parent_blocked_times_toast">
|
||||||
|
You are using the deprecated time lock for your parent user.
|
||||||
|
You should use a limit login category instead.
|
||||||
|
</string>
|
||||||
</resources>
|
</resources>
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!--
|
<!--
|
||||||
TimeLimit Copyright <C> 2019 Jonas Lochmann
|
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
the Free Software Foundation version 3 of the License.
|
the Free Software Foundation version 3 of the License.
|
||||||
|
@ -49,6 +49,9 @@
|
||||||
<string name="manage_parent_remove_user_status_ready">
|
<string name="manage_parent_remove_user_status_ready">
|
||||||
Enter the password of %1$s to delete %1$s.
|
Enter the password of %1$s to delete %1$s.
|
||||||
</string>
|
</string>
|
||||||
|
<string name="manage_parent_remove_user_status_last_without_login_limit">This is the last user
|
||||||
|
who can sign in at any time. Due to that, you can not remove it.
|
||||||
|
</string>
|
||||||
|
|
||||||
<string name="manage_parent_notifications_title">Mail notifications</string>
|
<string name="manage_parent_notifications_title">Mail notifications</string>
|
||||||
<string name="manage_parent_notifications_needs_linked_mail">You have to link a mail address to use this feature</string>
|
<string name="manage_parent_notifications_needs_linked_mail">You have to link a mail address to use this feature</string>
|
||||||
|
|
31
app/src/main/res/values/strings-parent-limit-login.xml
Normal file
31
app/src/main/res/values/strings-parent-limit-login.xml
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation version 3 of the License.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
<resources>
|
||||||
|
<string name="parent_limit_login_title">Limit login category</string>
|
||||||
|
<string name="parent_limit_login_help">Here you can select a category. As soon as Apps of this
|
||||||
|
category are blocked, you can not sign in with your parent user. This
|
||||||
|
can be used to prevent you from disabling limits which you set for yourself while they are active.
|
||||||
|
</string>
|
||||||
|
<string name="parent_limit_login_status_disabled">You can sign in at any time</string>
|
||||||
|
<string name="parent_limit_login_status_enabled">You can not sign in when %1$s is blocked for %2$s</string>
|
||||||
|
<string name="parent_limit_login_status_needs_other_user">You must create another parent user as backup (in case you lock out yourself) before you can use this feature</string>
|
||||||
|
<string name="parent_limit_login_select_button">Select category</string>
|
||||||
|
<string name="parent_limit_login_dialog_no_selection">no category</string>
|
||||||
|
<string name="parent_limit_login_dialog_item">%1$s/%2$s</string>
|
||||||
|
<string name="parent_limit_login_error_user_itself">You must be signed in as the user
|
||||||
|
to select a category. Other users can only disable this feature.
|
||||||
|
</string>
|
||||||
|
</resources>
|
Loading…
Add table
Add a link
Reference in a new issue