Add user limit login category feature

This commit is contained in:
Jonas Lochmann 2020-06-29 02:00:00 +02:00
parent 3f7e34a175
commit 7c8c00b539
No known key found for this signature in database
GPG key ID: 8B8C9AEE10FA5B36
40 changed files with 2502 additions and 187 deletions

File diff suppressed because it is too large Load diff

View file

@ -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

View file

@ -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`)")
}
}
}

View file

@ -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()
}

View file

@ -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)
}
}
}

View file

@ -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,21 +39,24 @@ 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? {
@ -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) }
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)
}

View file

@ -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)
}

View file

@ -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()

View file

@ -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()
}
}

View file

@ -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
)

View file

@ -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?
)

View file

@ -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))
}
}
}
}

View file

@ -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

View file

@ -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,

View file

@ -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
)
)
}
}
}
}
}
}

View file

@ -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"

View file

@ -69,6 +69,7 @@ object ActionParser {
// UpdateCategoryBatteryLimit
// UpdateCategorySorting
// UpdateUserFlagsAction
// UpdateUserLimitLoginCategory
else -> throw IllegalStateException()
}
}

View file

@ -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 { }
}
}

View file

@ -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,

View file

@ -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)
}
}
}

View file

@ -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,13 +63,14 @@ 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 ->
val isAlreadyCurrentUser = selectedUserInfo.deviceRelatedData.deviceEntry.currentUserId == selectedUser.id
val isConnectedMode = !selectedUserInfo.deviceRelatedData.isLocalMode
val loginScreen = isCheckingPassword.switchMap { isCheckingPassword ->
wasPasswordWrong.map { wasPasswordWrong ->
ParentUserLogin(
isConnectedMode = isConnectedMode,
@ -80,39 +80,39 @@ class LoginDialogFragmentModel(application: Application): AndroidViewModel(appli
) as LoginDialogStatus
}
}
}
}
if (selectedUser.blockedTimes.dataNotToModify.isEmpty) {
AllowUserLoginStatusUtil.calculateLive(logic, selectedUser.id).switchMap { status ->
if (status is AllowUserLoginStatus.Allow) {
loginScreen
} else {
logic.fullVersion.shouldProvideFullVersionFunctions.switchMap { hasPremium ->
if (hasPremium) {
trustedTime.switchMap { time ->
if (time == null) {
} else if (
status is AllowUserLoginStatus.ForbidByCurrentTime ||
(status is AllowUserLoginStatus.ForbidByCategory && status.blockingReason == BlockingReason.MissingNetworkTime)
) {
liveDataFromValue(ParentUserLoginMissingTrustedTime as LoginDialogStatus)
} else if (selectedUser.blockedTimes.dataNotToModify[time]) {
} 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
}
}
} else {
loginScreen
}
}
}
}
UserType.Child -> {
logic.fullVersion.shouldProvideFullVersionFunctions.switchMap { fullversion ->
if (fullversion) {
val hasPremium = selectedUserInfo.deviceRelatedData.isLocalMode || selectedUserInfo.deviceRelatedData.isConnectedAndHasPremium
if (hasPremium) {
if (selectedUser.password.isEmpty()) {
liveDataFromValue(CanNotSignInChildHasNoPassword(childName = selectedUser.name) as LoginDialogStatus)
} else {
val isAlreadyCurrentUser = currentDeviceUser.map { it == selectedUser.id }.ignoreUnchanged()
val isAlreadyCurrentUser = selectedUserInfo.deviceRelatedData.deviceEntry.currentUserId == selectedUser.id
isAlreadyCurrentUser.switchMap { isSignedIn ->
if (isSignedIn) {
if (isAlreadyCurrentUser) {
liveDataFromValue(ChildAlreadyDeviceUser as LoginDialogStatus)
} else {
isCheckingPassword.switchMap { isCheckingPassword ->
@ -125,12 +125,10 @@ class LoginDialogFragmentModel(application: Application): AndroidViewModel(appli
}
}
}
}
} else {
liveDataFromValue(ChildLoginRequiresPremiumStatus as LoginDialogStatus)
}
}
}
null -> {
logic.fullVersion.isLocalMode.switchMap { isLocalMode ->
users.map { users ->
@ -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 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
val shouldSignIn = if (emptyPasswordValid) {
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,

View file

@ -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 */ }

View file

@ -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(

View file

@ -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 ->
authenticatedUserLive.switchMap { authenticatedUser ->
isLastWithoutLoginLimit.map { lastWithoutLoginLimit ->
if (authenticatedUser?.second?.type != UserType.Parent) {
Status.NotAuthenticated
} else {
if (authenticatedUser.second.id == parentUserId) {
} 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
}

View file

@ -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 -> ""
}
})

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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)
}
})
}
}

View file

@ -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()}"

View file

@ -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>

View file

@ -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>

View 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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View 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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View 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>