mirror of
https://codeberg.org/timelimit/timelimit-android.git
synced 2025-10-05 02:39:30 +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 sessionDuration(): SessionDurationDao
|
||||
fun derivedDataDao(): DerivedDataDao
|
||||
fun userLimitLoginCategoryDao(): UserLimitLoginCategoryDao
|
||||
|
||||
fun <T> runInTransaction(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")
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
AllowedContact::class,
|
||||
UserKey::class,
|
||||
SessionDuration::class
|
||||
], version = 30)
|
||||
SessionDuration::class,
|
||||
UserLimitLoginCategory::class
|
||||
], version = 31)
|
||||
abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database {
|
||||
companion 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_V28,
|
||||
DatabaseMigrations.MIGRATE_TO_V29,
|
||||
DatabaseMigrations.MIGRATE_TO_V30
|
||||
DatabaseMigrations.MIGRATE_TO_V30,
|
||||
DatabaseMigrations.MIGRATE_TO_V31
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
|
|
@ -42,6 +42,7 @@ object DatabaseBackupLowlevel {
|
|||
private const val ALLOWED_CONTACT = "allowedContact"
|
||||
private const val USER_KEY = "userKey"
|
||||
private const val SESSION_DURATION = "sessionDuration"
|
||||
private const val USER_LIMIT_LOGIN_CATEGORY = "userLimitLoginCategory"
|
||||
|
||||
fun outputAsBackupJson(database: Database, outputStream: OutputStream) {
|
||||
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(USER_KEY) { offset, pageSize -> database.userKey().getUserKeyPageSync(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()
|
||||
}
|
||||
|
@ -95,6 +97,8 @@ object DatabaseBackupLowlevel {
|
|||
fun restoreFromBackupJson(database: Database, inputStream: InputStream) {
|
||||
val reader = JsonReader(InputStreamReader(inputStream, Charsets.UTF_8))
|
||||
|
||||
var userLoginLimitCategories = emptyList<UserLimitLoginCategory>()
|
||||
|
||||
database.runInTransaction {
|
||||
database.deleteAllData()
|
||||
|
||||
|
@ -234,10 +238,25 @@ object DatabaseBackupLowlevel {
|
|||
|
||||
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()
|
||||
}
|
||||
}
|
||||
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.cache.multi.*
|
||||
import io.timelimit.android.data.cache.single.*
|
||||
import io.timelimit.android.data.model.derived.DeviceAndUserRelatedData
|
||||
import io.timelimit.android.data.model.derived.DeviceRelatedData
|
||||
import io.timelimit.android.data.model.derived.UserRelatedData
|
||||
import io.timelimit.android.data.model.derived.*
|
||||
|
||||
class DerivedDataDao (private val database: Database) {
|
||||
private val userRelatedDataCache = object : DataCacheHelperInterface<String, UserRelatedData?, UserRelatedData?> {
|
||||
|
@ -32,16 +30,8 @@ class DerivedDataDao (private val database: Database) {
|
|||
return UserRelatedData.load(user, database)
|
||||
}
|
||||
|
||||
override fun updateItemSync(key: String, item: UserRelatedData?): UserRelatedData? {
|
||||
return if (item != null) {
|
||||
item.update(database)
|
||||
} else {
|
||||
openItemSync(key)
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateItemSync(key: String, item: UserRelatedData?): UserRelatedData? = if (item != null) item.update(database) else openItemSync(key)
|
||||
override fun <R> wrapOpenOrUpdate(block: () -> R): R = database.runInUnobservedTransaction { block() }
|
||||
|
||||
override fun disposeItemFast(key: String, item: UserRelatedData?) = Unit
|
||||
override fun prepareForUser(item: UserRelatedData?): UserRelatedData? = item
|
||||
override fun close() = Unit
|
||||
|
@ -49,27 +39,30 @@ class DerivedDataDao (private val database: Database) {
|
|||
|
||||
private val deviceRelatedDataCache = object: SingleItemDataCacheHelperInterface<DeviceRelatedData?, DeviceRelatedData?> {
|
||||
override fun openItemSync(): DeviceRelatedData? = DeviceRelatedData.load(database)
|
||||
|
||||
override fun updateItemSync(item: DeviceRelatedData?): DeviceRelatedData? = if (item != null) {
|
||||
item.update(database)
|
||||
} else {
|
||||
openItemSync()
|
||||
}
|
||||
|
||||
override fun updateItemSync(item: DeviceRelatedData?): DeviceRelatedData? = if (item != null) item.update(database) else openItemSync()
|
||||
override fun <R> wrapOpenOrUpdate(block: () -> R): R = database.runInUnobservedTransaction { block() }
|
||||
|
||||
override fun prepareForUser(item: DeviceRelatedData?): DeviceRelatedData? = item
|
||||
override fun disposeItemFast(item: DeviceRelatedData?): Unit = Unit
|
||||
}.createCache()
|
||||
|
||||
private val 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 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?> {
|
||||
override fun openItemSync(): DeviceAndUserRelatedData? {
|
||||
val deviceRelatedData = usableDeviceRelatedData.openSync(null) ?: return null
|
||||
val userRelatedData = if (deviceRelatedData.deviceEntry.currentUserId.isNotEmpty())
|
||||
usableUserRelatedData.openSync(deviceRelatedData.deviceEntry.currentUserId, null)
|
||||
usableUserRelatedData.openSync(deviceRelatedData.deviceEntry.currentUserId, null)
|
||||
else
|
||||
null
|
||||
|
||||
|
@ -80,27 +73,12 @@ class DerivedDataDao (private val database: Database) {
|
|||
}
|
||||
|
||||
override fun updateItemSync(item: DeviceAndUserRelatedData?): DeviceAndUserRelatedData? {
|
||||
val deviceRelatedData = usableDeviceRelatedData.openSync(null) ?: run {
|
||||
// close old listener instances
|
||||
try {
|
||||
val newItem = openItemSync()
|
||||
|
||||
return if (newItem != item) newItem else item
|
||||
} finally {
|
||||
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 disposeItemFast(item: DeviceAndUserRelatedData?) {
|
||||
if (item != null) {
|
||||
usableDeviceRelatedData.close(null)
|
||||
item.userRelatedData?.user?.let { usableUserRelatedData.close(it.id, null) }
|
||||
usableDeviceRelatedData.close(null)
|
||||
item?.deviceRelatedData?.deviceEntry?.currentUserId?.let {
|
||||
if (it.isNotEmpty()) {
|
||||
usableUserRelatedData.close(it, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.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 usableCompleteUserLoginRelatedData = completeUserLoginRelatedData.userInterface.delayClosingItems(5000)
|
||||
|
||||
private val deviceAndUserRelatedDataLive = usableDeviceAndUserRelatedDataCache.openLiveAtDatabaseThread()
|
||||
|
||||
init {
|
||||
database.registerTransactionCommitListener {
|
||||
userRelatedDataCache.ownerInterface.updateSync()
|
||||
deviceRelatedDataCache.ownerInterface.updateSync()
|
||||
userLoginRelatedDataCache.ownerInterface.updateSync()
|
||||
deviceAndUserRelatedDataCache.ownerInterface.updateSync()
|
||||
completeUserLoginRelatedData.ownerInterface.updateSync()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -135,7 +164,17 @@ class DerivedDataDao (private val database: Database) {
|
|||
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 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
|
||||
}
|
||||
|
||||
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> {
|
||||
if (this.find { it.id == categoryId } != null) {
|
||||
return emptySet()
|
||||
|
|
|
@ -31,7 +31,8 @@ enum class Table {
|
|||
TimeLimitRule,
|
||||
UsedTimeItem,
|
||||
User,
|
||||
UserKey
|
||||
UserKey,
|
||||
UserLimitLoginCategory
|
||||
}
|
||||
|
||||
object TableNames {
|
||||
|
@ -50,6 +51,7 @@ object TableNames {
|
|||
const val USED_TIME_ITEM = "used_time"
|
||||
const val USER = "user"
|
||||
const val USER_KEY = "user_key"
|
||||
const val USER_LIMIT_LOGIN_CATEGORY = "user_limit_login_category"
|
||||
}
|
||||
|
||||
object TableUtil {
|
||||
|
@ -69,6 +71,7 @@ object TableUtil {
|
|||
Table.UsedTimeItem -> TableNames.USED_TIME_ITEM
|
||||
Table.User -> TableNames.USER
|
||||
Table.UserKey -> TableNames.USER_KEY
|
||||
Table.UserLimitLoginCategory -> TableNames.USER_LIMIT_LOGIN_CATEGORY
|
||||
}
|
||||
|
||||
fun toEnum(value: String): Table = when (value) {
|
||||
|
@ -87,6 +90,7 @@ object TableUtil {
|
|||
TableNames.USED_TIME_ITEM -> Table.UsedTimeItem
|
||||
TableNames.USER -> Table.User
|
||||
TableNames.USER_KEY -> Table.UserKey
|
||||
TableNames.USER_LIMIT_LOGIN_CATEGORY -> Table.UserLimitLoginCategory
|
||||
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
|
||||
|
||||
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.DeviceRelatedData
|
||||
import io.timelimit.android.data.model.derived.UserRelatedData
|
||||
|
@ -85,20 +86,8 @@ sealed class AppBaseHandling {
|
|||
if (startCategory == null) {
|
||||
return BlockDueToNoCategory
|
||||
} else {
|
||||
val categoryIds = mutableSetOf(startCategory.category.id)
|
||||
|
||||
run {
|
||||
// get parent category ids
|
||||
|
||||
var currentCategory: CategoryRelatedData? = userRelatedData.categoryById[startCategory.category.parentCategoryId]
|
||||
|
||||
while (currentCategory != null && categoryIds.add(currentCategory.category.id)) {
|
||||
currentCategory = userRelatedData.categoryById[currentCategory.category.parentCategoryId]
|
||||
}
|
||||
}
|
||||
|
||||
return UseCategories(
|
||||
categoryIds = categoryIds,
|
||||
categoryIds = userRelatedData.getCategoryWithParentCategories(startCategoryId = startCategory.category.id),
|
||||
shouldCount = !pauseCounting,
|
||||
level = when (appCategory?.specifiesActivity) {
|
||||
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
|
||||
// val shouldBlockNotifications = !okAll || blockAllNotifications
|
||||
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(
|
||||
categoryRelatedData: CategoryRelatedData,
|
||||
|
|
|
@ -52,8 +52,24 @@ object ApplyServerDataStatus {
|
|||
run {
|
||||
// update/ create entries (first because there must be always one parent user)
|
||||
|
||||
newUserList.data.forEach {
|
||||
newData ->
|
||||
newUserList.data.forEach { newEntry ->
|
||||
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 }
|
||||
|
||||
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
|
||||
object ChildSignInAction: ChildAction() {
|
||||
private const val TYPE_VALUE = "CHILD_SIGN_IN"
|
||||
|
|
|
@ -69,6 +69,7 @@ object ActionParser {
|
|||
// UpdateCategoryBatteryLimit
|
||||
// UpdateCategorySorting
|
||||
// UpdateUserFlagsAction
|
||||
// UpdateUserLimitLoginCategory
|
||||
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.customtypes.ImmutableBitmask
|
||||
import io.timelimit.android.data.extensions.getChildCategories
|
||||
import io.timelimit.android.data.model.Category
|
||||
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.data.model.*
|
||||
import io.timelimit.android.sync.actions.*
|
||||
import java.util.*
|
||||
|
||||
|
@ -259,6 +256,10 @@ object LocalDatabaseParentActionDispatcher {
|
|||
if (currentParents.size <= 1) {
|
||||
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) {
|
||||
|
@ -595,6 +596,29 @@ object LocalDatabaseParentActionDispatcher {
|
|||
|
||||
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 { }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
package io.timelimit.android.sync.network
|
||||
|
||||
import android.util.JsonReader
|
||||
import android.util.JsonToken
|
||||
import io.timelimit.android.data.customtypes.ImmutableBitmask
|
||||
import io.timelimit.android.data.customtypes.ImmutableBitmaskJson
|
||||
import io.timelimit.android.data.model.*
|
||||
|
@ -130,7 +131,7 @@ data class ServerDeviceList(
|
|||
|
||||
data class ServerUserList(
|
||||
val version: String,
|
||||
val data: List<User>
|
||||
val data: List<ServerUserData>
|
||||
) {
|
||||
companion object {
|
||||
private const val VERSION = "version"
|
||||
|
@ -138,13 +139,13 @@ data class ServerUserList(
|
|||
|
||||
fun parse(reader: JsonReader): ServerUserList {
|
||||
var version: String? = null
|
||||
var data: List<User>? = null
|
||||
var data: List<ServerUserData>? = null
|
||||
|
||||
reader.beginObject()
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
VERSION -> version = reader.nextString()
|
||||
DATA -> data = User.parseList(reader)
|
||||
DATA -> data = ServerUserData.parseList(reader)
|
||||
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(
|
||||
val deviceId: 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.data.model.User
|
||||
import io.timelimit.android.data.model.UserType
|
||||
import io.timelimit.android.data.model.derived.CompleteUserLoginRelatedData
|
||||
import io.timelimit.android.livedata.*
|
||||
import io.timelimit.android.logic.BlockingReason
|
||||
import io.timelimit.android.logic.BlockingReasonUtil
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
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 kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.util.*
|
||||
|
||||
class LoginDialogFragmentModel(application: Application): AndroidViewModel(application) {
|
||||
val selectedUserId = MutableLiveData<String?>().apply { value = null }
|
||||
private val logic = DefaultAppLogic.with(application)
|
||||
private val blockingReasonUtil = BlockingReasonUtil(logic)
|
||||
private val users = logic.database.user().getAllUsersLive()
|
||||
private val isConnectedMode = logic.fullVersion.isLocalMode.invert()
|
||||
private val selectedUser = users.switchMap { users ->
|
||||
selectedUserId.map { userId ->
|
||||
users.find { it.id == userId }
|
||||
}
|
||||
private val selectedUser = selectedUserId.switchMap { selectedUserId ->
|
||||
if (selectedUserId != null)
|
||||
logic.database.derivedDataDao().getUserLoginRelatedDataLive(selectedUserId)
|
||||
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 wasPasswordWrong = 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) {
|
||||
liveDataFromValue(LoginDialogDone as LoginDialogStatus)
|
||||
} else {
|
||||
selectedUser.switchMap { selectedUser ->
|
||||
selectedUser.switchMap { selectedUserInfo ->
|
||||
val selectedUser = selectedUserInfo?.loginRelatedData?.user
|
||||
|
||||
when (selectedUser?.type) {
|
||||
UserType.Parent -> {
|
||||
val isAlreadyCurrentUser = currentDeviceUser.map { it == selectedUser.id }.ignoreUnchanged()
|
||||
val loginScreen = isConnectedMode.switchMap { isConnectedMode ->
|
||||
isAlreadyCurrentUser.switchMap { isAlreadyCurrentUser ->
|
||||
isCheckingPassword.switchMap { isCheckingPassword ->
|
||||
wasPasswordWrong.map { wasPasswordWrong ->
|
||||
ParentUserLogin(
|
||||
isConnectedMode = isConnectedMode,
|
||||
isAlreadyCurrentDeviceUser = isAlreadyCurrentUser,
|
||||
isCheckingPassword = isCheckingPassword,
|
||||
wasPasswordWrong = wasPasswordWrong
|
||||
) as LoginDialogStatus
|
||||
}
|
||||
}
|
||||
val isAlreadyCurrentUser = selectedUserInfo.deviceRelatedData.deviceEntry.currentUserId == selectedUser.id
|
||||
val isConnectedMode = !selectedUserInfo.deviceRelatedData.isLocalMode
|
||||
val loginScreen = isCheckingPassword.switchMap { isCheckingPassword ->
|
||||
wasPasswordWrong.map { wasPasswordWrong ->
|
||||
ParentUserLogin(
|
||||
isConnectedMode = isConnectedMode,
|
||||
isAlreadyCurrentDeviceUser = isAlreadyCurrentUser,
|
||||
isCheckingPassword = isCheckingPassword,
|
||||
wasPasswordWrong = wasPasswordWrong
|
||||
) as LoginDialogStatus
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedUser.blockedTimes.dataNotToModify.isEmpty) {
|
||||
loginScreen
|
||||
} else {
|
||||
logic.fullVersion.shouldProvideFullVersionFunctions.switchMap { hasPremium ->
|
||||
if (hasPremium) {
|
||||
trustedTime.switchMap { time ->
|
||||
if (time == null) {
|
||||
liveDataFromValue(ParentUserLoginMissingTrustedTime as LoginDialogStatus)
|
||||
} else if (selectedUser.blockedTimes.dataNotToModify[time]) {
|
||||
liveDataFromValue(ParentUserLoginBlockedTime as LoginDialogStatus)
|
||||
} else {
|
||||
loginScreen
|
||||
}
|
||||
}
|
||||
} else {
|
||||
loginScreen
|
||||
}
|
||||
AllowUserLoginStatusUtil.calculateLive(logic, selectedUser.id).switchMap { status ->
|
||||
if (status is AllowUserLoginStatus.Allow) {
|
||||
loginScreen
|
||||
} else if (
|
||||
status is AllowUserLoginStatus.ForbidByCurrentTime ||
|
||||
(status is AllowUserLoginStatus.ForbidByCategory && status.blockingReason == BlockingReason.MissingNetworkTime)
|
||||
) {
|
||||
liveDataFromValue(ParentUserLoginMissingTrustedTime as LoginDialogStatus)
|
||||
} else if (status is AllowUserLoginStatus.ForbidByCurrentTime) {
|
||||
liveDataFromValue(ParentUserLoginBlockedTime as LoginDialogStatus)
|
||||
} else if (status is AllowUserLoginStatus.ForbidByCategory) {
|
||||
liveDataFromValue(
|
||||
ParentUserLoginBlockedByCategory(
|
||||
categoryTitle = status.categoryTitle,
|
||||
reason = status.blockingReason
|
||||
) as LoginDialogStatus
|
||||
)
|
||||
} else {
|
||||
loginScreen
|
||||
}
|
||||
}
|
||||
}
|
||||
UserType.Child -> {
|
||||
logic.fullVersion.shouldProvideFullVersionFunctions.switchMap { fullversion ->
|
||||
if (fullversion) {
|
||||
if (selectedUser.password.isEmpty()) {
|
||||
liveDataFromValue(CanNotSignInChildHasNoPassword(childName = selectedUser.name) as LoginDialogStatus)
|
||||
} else {
|
||||
val isAlreadyCurrentUser = currentDeviceUser.map { it == selectedUser.id }.ignoreUnchanged()
|
||||
val hasPremium = selectedUserInfo.deviceRelatedData.isLocalMode || selectedUserInfo.deviceRelatedData.isConnectedAndHasPremium
|
||||
|
||||
isAlreadyCurrentUser.switchMap { isSignedIn ->
|
||||
if (isSignedIn) {
|
||||
liveDataFromValue(ChildAlreadyDeviceUser as LoginDialogStatus)
|
||||
} else {
|
||||
isCheckingPassword.switchMap { isCheckingPassword ->
|
||||
wasPasswordWrong.map { wasPasswordWrong ->
|
||||
ChildUserLogin(
|
||||
isCheckingPassword = isCheckingPassword,
|
||||
wasPasswordWrong = wasPasswordWrong
|
||||
) as LoginDialogStatus
|
||||
}
|
||||
}
|
||||
if (hasPremium) {
|
||||
if (selectedUser.password.isEmpty()) {
|
||||
liveDataFromValue(CanNotSignInChildHasNoPassword(childName = selectedUser.name) as LoginDialogStatus)
|
||||
} else {
|
||||
val isAlreadyCurrentUser = selectedUserInfo.deviceRelatedData.deviceEntry.currentUserId == selectedUser.id
|
||||
|
||||
if (isAlreadyCurrentUser) {
|
||||
liveDataFromValue(ChildAlreadyDeviceUser as LoginDialogStatus)
|
||||
} else {
|
||||
isCheckingPassword.switchMap { isCheckingPassword ->
|
||||
wasPasswordWrong.map { wasPasswordWrong ->
|
||||
ChildUserLogin(
|
||||
isCheckingPassword = isCheckingPassword,
|
||||
wasPasswordWrong = wasPasswordWrong
|
||||
) as LoginDialogStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
liveDataFromValue(ChildLoginRequiresPremiumStatus as LoginDialogStatus)
|
||||
}
|
||||
} else {
|
||||
liveDataFromValue(ChildLoginRequiresPremiumStatus as LoginDialogStatus)
|
||||
}
|
||||
}
|
||||
null -> {
|
||||
|
@ -155,24 +153,14 @@ class LoginDialogFragmentModel(application: Application): AndroidViewModel(appli
|
|||
loginLock.withLock {
|
||||
logic.database.user().getParentUsersLive().waitForNonNullValue().singleOrNull()?.let { user ->
|
||||
val emptyPasswordValid = Threads.crypto.executeAndWait { PasswordHashing.validateSync("", user.password) }
|
||||
val hasBlockedTimes = !user.blockedTimes.dataNotToModify.isEmpty
|
||||
|
||||
val shouldSignIn = if (emptyPasswordValid) {
|
||||
val hasBlockedTimes = !user.blockedTimes.dataNotToModify.isEmpty
|
||||
|
||||
if (hasBlockedTimes) {
|
||||
val hasPremium = logic.fullVersion.shouldProvideFullVersionFunctions.waitForNonNullValue()
|
||||
|
||||
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
|
||||
Threads.database.executeAndWait {
|
||||
AllowUserLoginStatusUtil.calculateSync(
|
||||
logic = logic,
|
||||
userId = user.id
|
||||
) is AllowUserLoginStatus.Allow
|
||||
}
|
||||
} else {
|
||||
false
|
||||
|
@ -185,6 +173,10 @@ class LoginDialogFragmentModel(application: Application): AndroidViewModel(appli
|
|||
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
|
||||
}
|
||||
}
|
||||
|
@ -230,20 +222,11 @@ class LoginDialogFragmentModel(application: Application): AndroidViewModel(appli
|
|||
if (user != null && user.type == UserType.Parent) {
|
||||
val hasBlockedTimes = !user.blockedTimes.dataNotToModify.isEmpty
|
||||
|
||||
val shouldSignIn = if (hasBlockedTimes) {
|
||||
val hasPremium = logic.fullVersion.shouldProvideFullVersionFunctions.waitForNonNullValue()
|
||||
|
||||
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
|
||||
val shouldSignIn = Threads.database.executeAndWait {
|
||||
AllowUserLoginStatusUtil.calculateSync(
|
||||
logic = logic,
|
||||
userId = user.id
|
||||
) is AllowUserLoginStatus.Allow
|
||||
}
|
||||
|
||||
if (shouldSignIn) {
|
||||
|
@ -254,6 +237,10 @@ class LoginDialogFragmentModel(application: Application): AndroidViewModel(appli
|
|||
secondPasswordHash = "device"
|
||||
))
|
||||
|
||||
if (hasBlockedTimes) {
|
||||
Toast.makeText(getApplication(), R.string.manage_parent_blocked_times_toast, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
isLoginDone.value = true
|
||||
} else {
|
||||
Toast.makeText(getApplication(), R.string.login_blocked_time, Toast.LENGTH_SHORT).show()
|
||||
|
@ -274,15 +261,17 @@ class LoginDialogFragmentModel(application: Application): AndroidViewModel(appli
|
|||
try {
|
||||
isCheckingPassword.value = true
|
||||
|
||||
val userEntry = selectedUser.waitForNullableValue()
|
||||
val ownDeviceId = logic.deviceId.waitForNullableValue()
|
||||
val userEntryInfo = selectedUser.waitForNullableValue()
|
||||
val userEntry = userEntryInfo?.loginRelatedData?.user
|
||||
|
||||
if (userEntry?.type != UserType.Parent || ownDeviceId == null) {
|
||||
if (userEntry?.type != UserType.Parent) {
|
||||
selectedUserId.value = null
|
||||
|
||||
return@runAsync
|
||||
}
|
||||
|
||||
val ownDeviceId = userEntryInfo.deviceRelatedData.deviceEntry.id
|
||||
|
||||
val passwordValid = Threads.crypto.executeAndWait { PasswordHashing.validateSync(password, userEntry.password) }
|
||||
|
||||
if (!passwordValid) {
|
||||
|
@ -299,12 +288,28 @@ class LoginDialogFragmentModel(application: Application): AndroidViewModel(appli
|
|||
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)
|
||||
|
||||
if (setAsDeviceUser) {
|
||||
val deviceEntry = logic.deviceEntry.waitForNonNullValue()!!
|
||||
if (hasBlockedTimes) {
|
||||
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(
|
||||
SetDeviceUserAction(
|
||||
deviceId = ownDeviceId,
|
||||
|
@ -319,7 +324,7 @@ class LoginDialogFragmentModel(application: Application): AndroidViewModel(appli
|
|||
if (keepSignedIn) {
|
||||
if (
|
||||
setAsDeviceUser ||
|
||||
(currentDeviceUser.waitForNullableValue() == userEntry.id)
|
||||
userEntryInfo.deviceRelatedData.deviceEntry.currentUserId == userEntry.id
|
||||
) {
|
||||
ActivityViewModel.dispatchWithoutCheckOrCatching(
|
||||
SetKeepSignedInAction(
|
||||
|
@ -341,18 +346,17 @@ class LoginDialogFragmentModel(application: Application): AndroidViewModel(appli
|
|||
}
|
||||
|
||||
fun tryChildLogin(
|
||||
password: String,
|
||||
model: ActivityViewModel
|
||||
password: String
|
||||
) {
|
||||
runAsync {
|
||||
loginLock.withLock {
|
||||
try {
|
||||
isCheckingPassword.value = true
|
||||
|
||||
val userEntry = selectedUser.waitForNullableValue()
|
||||
val ownDeviceId = logic.deviceId.waitForNullableValue()
|
||||
val userEntryInfo = selectedUser.waitForNullableValue()
|
||||
val userEntry = userEntryInfo?.loginRelatedData?.user
|
||||
|
||||
if (userEntry?.type != UserType.Child || ownDeviceId == null) {
|
||||
if (userEntry?.type != UserType.Child) {
|
||||
selectedUserId.value = null
|
||||
|
||||
return@runAsync
|
||||
|
@ -409,6 +413,7 @@ sealed class LoginDialogStatus
|
|||
data class UserListLoginDialogStatus(val usersToShow: List<User>, val isLocalMode: Boolean): LoginDialogStatus()
|
||||
object ParentUserLoginMissingTrustedTime: LoginDialogStatus()
|
||||
object ParentUserLoginBlockedTime: LoginDialogStatus()
|
||||
data class ParentUserLoginBlockedByCategory(val categoryTitle: String, val reason: BlockingReason): LoginDialogStatus()
|
||||
data class ParentUserLogin(
|
||||
val isConnectedMode: 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.databinding.NewLoginFragmentBinding
|
||||
import io.timelimit.android.extensions.setOnEnterListenr
|
||||
import io.timelimit.android.logic.BlockingReason
|
||||
import io.timelimit.android.ui.main.getActivityViewModel
|
||||
import io.timelimit.android.ui.manage.parent.key.ScannedKey
|
||||
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 BLOCKED_LOGIN_TIME = 6
|
||||
private const val UNVERIFIED_TIME = 7
|
||||
private const val PARENT_LOGIN_BLOCKED = 8
|
||||
}
|
||||
|
||||
private val model: LoginDialogFragmentModel by lazy {
|
||||
|
@ -170,8 +172,7 @@ class NewLoginFragment: DialogFragment() {
|
|||
binding.childPassword.apply {
|
||||
password.setOnEnterListenr {
|
||||
model.tryChildLogin(
|
||||
password = password.text.toString(),
|
||||
model = getActivityViewModel(activity!!)
|
||||
password = password.text.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -307,6 +308,30 @@ class NewLoginFragment: DialogFragment() {
|
|||
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
|
||||
}
|
||||
}.let { /* require handling all cases */ }
|
||||
|
|
|
@ -28,6 +28,7 @@ import androidx.navigation.Navigation
|
|||
import io.timelimit.android.R
|
||||
import io.timelimit.android.data.model.User
|
||||
import io.timelimit.android.databinding.FragmentManageParentBinding
|
||||
import io.timelimit.android.databinding.ParentLimitLoginViewBinding
|
||||
import io.timelimit.android.extensions.safeNavigate
|
||||
import io.timelimit.android.livedata.liveDataFromValue
|
||||
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.parent.delete.DeleteParentView
|
||||
import io.timelimit.android.ui.manage.parent.key.ManageUserKeyView
|
||||
import io.timelimit.android.ui.manage.parent.limitlogin.ParentLimitLoginView
|
||||
|
||||
class ManageParentFragment : Fragment(), FragmentWithCustomTitle {
|
||||
private val activity: ActivityViewModelHolder by lazy { getActivity() as ActivityViewModelHolder }
|
||||
|
@ -124,6 +126,14 @@ class ManageParentFragment : Fragment(), FragmentWithCustomTitle {
|
|||
fragmentManager = parentFragmentManager
|
||||
)
|
||||
|
||||
ParentLimitLoginView.bind(
|
||||
view = binding.parentLimitLogin,
|
||||
lifecycleOwner = viewLifecycleOwner,
|
||||
userId = params.parentId,
|
||||
auth = activity.getActivityViewModel(),
|
||||
fragmentManager = parentFragmentManager
|
||||
)
|
||||
|
||||
binding.handlers = object: ManageParentFragmentHandlers {
|
||||
override fun onChangePasswordClicked() {
|
||||
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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -48,14 +48,19 @@ class DeleteParentModel(application: Application): AndroidViewModel(application)
|
|||
} != null
|
||||
}
|
||||
}
|
||||
private val isLastWithoutLoginLimit = parentUserIdLive.switchMap { userId ->
|
||||
database.userLimitLoginCategoryDao().countOtherUsersWithoutLimitLoginCategoryLive(userId).map { it == 0L }
|
||||
}
|
||||
|
||||
private val statusIgnoringLinkingLive = parentUserIdLive.switchMap { parentUserId ->
|
||||
authenticatedUserLive.map { authenticatedUser ->
|
||||
if (authenticatedUser?.second?.type != UserType.Parent) {
|
||||
Status.NotAuthenticated
|
||||
} else {
|
||||
if (authenticatedUser.second.id == parentUserId) {
|
||||
authenticatedUserLive.switchMap { authenticatedUser ->
|
||||
isLastWithoutLoginLimit.map { lastWithoutLoginLimit ->
|
||||
if (authenticatedUser?.second?.type != UserType.Parent) {
|
||||
Status.NotAuthenticated
|
||||
} else if (authenticatedUser.second.id == parentUserId) {
|
||||
Status.WrongAccount
|
||||
} else if (lastWithoutLoginLimit) {
|
||||
Status.LastWihtoutLoginLimit
|
||||
} else {
|
||||
Status.Ready
|
||||
}
|
||||
|
@ -142,5 +147,6 @@ enum class Status {
|
|||
LastLinked,
|
||||
NotAuthenticated,
|
||||
WrongAccount,
|
||||
LastWihtoutLoginLimit,
|
||||
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
|
||||
* 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.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.LastWihtoutLoginLimit -> context.getString(R.string.manage_parent_remove_user_status_last_without_login_limit)
|
||||
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"
|
||||
layout="@layout/user_timezone_view" />
|
||||
|
||||
<include android:id="@+id/parent_limit_login"
|
||||
layout="@layout/parent_limit_login_view" />
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
app:cardUseCompatPadding="true"
|
||||
android:onClick="@{() -> handlers.onManageBlockedTimesClicked()}"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?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
|
||||
it under the terms of the GNU General Public License as published by
|
||||
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_parent_login_blocked"
|
||||
android:id="@+id/parent_login_blocked" />
|
||||
|
||||
</io.timelimit.android.ui.view.SafeViewFlipper>
|
||||
</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_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_category_blocked">Die Kategorie %1$s ist momentan gesperrt (%2$s), sodass Sie sich nicht mit diesem Benutzer anmelden können</string>
|
||||
</resources>
|
|
@ -1,6 +1,6 @@
|
|||
<?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
|
||||
it under the terms of the GNU General Public License as published by
|
||||
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/>.
|
||||
-->
|
||||
<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">
|
||||
Sie können anstelle dieser Funktion eine Anmeldeverhinderungskategorie verwenden.
|
||||
Hiermit können Zeiten festgelegt werden, an denen sich ein Elternteil nicht anmelden kann.
|
||||
Das ist bei der Selbstbeschränkung relevant.
|
||||
</string>
|
||||
|
@ -28,4 +29,9 @@
|
|||
<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_blocked_times_toast">
|
||||
Sie verwenden das veraltete Zeitschloss für Ihren Elternbenutzer.
|
||||
Sie sollten stattdessen die Anmeldeverhinderungskategorie verwenden.
|
||||
</string>
|
||||
</resources>
|
|
@ -1,6 +1,6 @@
|
|||
<?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
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation version 3 of the License.
|
||||
|
@ -48,6 +48,9 @@
|
|||
<string name="manage_parent_remove_user_status_ready">
|
||||
Geben Sie das Passwort von %1$s ein, um %1$s zu löschen.
|
||||
</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_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_err_expired">This code is old</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>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?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
|
||||
it under the terms of the GNU General Public License as published by
|
||||
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/>.
|
||||
-->
|
||||
<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">
|
||||
You can use a limit login category instead.
|
||||
This allows to set times at which the parent can not sign in.
|
||||
This is relevant for limiting ones own usage.
|
||||
</string>
|
||||
|
@ -28,4 +29,9 @@
|
|||
<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_blocked_times_toast">
|
||||
You are using the deprecated time lock for your parent user.
|
||||
You should use a limit login category instead.
|
||||
</string>
|
||||
</resources>
|
|
@ -1,6 +1,6 @@
|
|||
<?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
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation version 3 of the License.
|
||||
|
@ -49,6 +49,9 @@
|
|||
<string name="manage_parent_remove_user_status_ready">
|
||||
Enter the password of %1$s to delete %1$s.
|
||||
</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_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