Add option to restrict viewing child users

This commit is contained in:
Jonas Lochmann 2020-06-01 02:00:00 +02:00
parent 7139c87fec
commit a0a1c20e10
No known key found for this signature in database
GPG key ID: 8B8C9AEE10FA5B36
15 changed files with 1274 additions and 13 deletions

File diff suppressed because it is too large Load diff

View file

@ -218,4 +218,10 @@ object DatabaseMigrations {
database.execSQL("CREATE INDEX IF NOT EXISTS `session_duration_index_category_id` ON `session_duration` (`category_id`)")
}
}
val MIGRATE_TO_V30 = object: Migration(29, 30) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `user` ADD COLUMN `flags` INTEGER NOT NULL DEFAULT 0")
}
}
}

View file

@ -37,7 +37,7 @@ import io.timelimit.android.data.model.*
AllowedContact::class,
UserKey::class,
SessionDuration::class
], version = 29)
], version = 30)
abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database {
companion object {
private val lock = Object()
@ -100,7 +100,8 @@ abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database
DatabaseMigrations.MIGRATE_TO_V26,
DatabaseMigrations.MIGRATE_TO_V27,
DatabaseMigrations.MIGRATE_TO_V28,
DatabaseMigrations.MIGRATE_TO_V29
DatabaseMigrations.MIGRATE_TO_V29,
DatabaseMigrations.MIGRATE_TO_V30
)
.build()
}

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
@ -62,7 +62,9 @@ data class User(
@ColumnInfo(name = "mail_notification_flags")
val mailNotificationFlags: Int,
@ColumnInfo(name = "blocked_times")
val blockedTimes: ImmutableBitmask
val blockedTimes: ImmutableBitmask,
@ColumnInfo(name = "flags")
val flags: Long
): JsonSerializable {
companion object {
private const val ID = "id"
@ -78,6 +80,7 @@ data class User(
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"
fun parse(reader: JsonReader): User {
var id: String? = null
@ -93,6 +96,7 @@ data class User(
var relaxPrimaryDevice = false
var mailNotificationFlags = 0
var blockedTimes = ImmutableBitmask(BitSet())
var flags = 0L
reader.beginObject()
while (reader.hasNext()) {
@ -110,6 +114,7 @@ data class User(
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()
else -> reader.skipValue()
}
}
@ -128,7 +133,8 @@ data class User(
categoryForNotAssignedApps = categoryForNotAssignedApps,
relaxPrimaryDevice = relaxPrimaryDevice,
mailNotificationFlags = mailNotificationFlags,
blockedTimes = blockedTimes
blockedTimes = blockedTimes,
flags = flags
)
}
@ -159,6 +165,9 @@ data class User(
}
}
val restrictViewingToParents: Boolean
get() = flags and UserFlags.RESTRICT_VIEWING_TO_PARENTS == UserFlags.RESTRICT_VIEWING_TO_PARENTS
override fun serialize(writer: JsonWriter) {
writer.beginObject()
@ -175,6 +184,7 @@ data class User(
writer.name(RELAX_PRIMARY_DEVICE).value(relaxPrimaryDevice)
writer.name(MAIL_NOTIFICATION_FLAGS).value(mailNotificationFlags)
writer.name(BLOCKED_TIMES).value(ImmutableBitmaskJson.serialize(blockedTimes))
writer.name(FLAGS).value(flags)
writer.endObject()
}
@ -207,3 +217,8 @@ class UserTypeConverter {
@TypeConverter
fun toString(value: UserType) = UserTypeJson.serialize(value)
}
object UserFlags {
const val RESTRICT_VIEWING_TO_PARENTS = 1L
const val ALL_FLAGS = RESTRICT_VIEWING_TO_PARENTS
}

View file

@ -128,7 +128,8 @@ class AppSetupLogic(private val appLogic: AppLogic) {
categoryForNotAssignedApps = "",
relaxPrimaryDevice = false,
mailNotificationFlags = 0,
blockedTimes = ImmutableBitmask(BitSet())
blockedTimes = ImmutableBitmask(BitSet()),
flags = 0
)
appLogic.database.user().addUserSync(child)
@ -150,7 +151,8 @@ class AppSetupLogic(private val appLogic: AppLogic) {
categoryForNotAssignedApps = "",
relaxPrimaryDevice = false,
mailNotificationFlags = 0,
blockedTimes = ImmutableBitmask(BitSet())
blockedTimes = ImmutableBitmask(BitSet()),
flags = 0
)
appLogic.database.user().addUserSync(parent)

View file

@ -1826,6 +1826,34 @@ data class ResetParentBlockedTimesAction(val parentId: String): ParentAction() {
}
}
data class UpdateUserFlagsAction(val userId: String, val modifiedBits: Long, val newValues: Long): ParentAction() {
companion object {
private const val TYPE_VALUE = "UPDATE_USER_FLAGS"
private const val USER_ID = "userId"
private const val MODIFIED_BITS = "modified"
private const val NEW_VALUES = "values"
}
init {
IdGenerator.assertIdValid(userId)
if (modifiedBits or UserFlags.ALL_FLAGS != UserFlags.ALL_FLAGS || modifiedBits or newValues != modifiedBits) {
throw IllegalArgumentException()
}
}
override fun serialize(writer: JsonWriter) {
writer.beginObject()
writer.name(TYPE).value(TYPE_VALUE)
writer.name(USER_ID).value(userId)
writer.name(MODIFIED_BITS).value(modifiedBits)
writer.name(NEW_VALUES).value(newValues)
writer.endObject()
}
}
// child actions
object ChildSignInAction: ChildAction() {
private const val TYPE_VALUE = "CHILD_SIGN_IN"

View file

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

View file

@ -176,7 +176,8 @@ object LocalDatabaseParentActionDispatcher {
categoryForNotAssignedApps = "",
relaxPrimaryDevice = false,
mailNotificationFlags = 0,
blockedTimes = ImmutableBitmask(BitSet())
blockedTimes = ImmutableBitmask(BitSet()),
flags = 0
))
}
is UpdateCategoryBlockedTimesAction -> {
@ -586,6 +587,15 @@ object LocalDatabaseParentActionDispatcher {
database.category().updateCategorySorting(categoryId, index)
}
}
is UpdateUserFlagsAction -> {
val user = database.user().getUserByIdSync(action.userId)!!
val updatedUser = user.copy(
flags = (user.flags and action.modifiedBits.inv()) or action.newValues
)
database.user().updateUserSync(updatedUser)
}
}.let { }
}
}

View file

@ -34,11 +34,13 @@ import io.timelimit.android.ui.help.HelpDialogFragment
import io.timelimit.android.ui.main.ActivityViewModel
import io.timelimit.android.ui.main.getActivityViewModel
import io.timelimit.android.ui.manage.child.ManageChildFragmentArgs
import io.timelimit.android.ui.manage.child.advanced.limituserviewing.LimitUserViewingView
import io.timelimit.android.ui.manage.child.advanced.manageblocktemporarily.ManageBlockTemporarilyView
import io.timelimit.android.ui.manage.child.advanced.managedisabletimelimits.ManageDisableTimelimitsViewHelper
import io.timelimit.android.ui.manage.child.advanced.password.ManageChildPassword
import io.timelimit.android.ui.manage.child.advanced.timezone.UserTimezoneView
import io.timelimit.android.ui.manage.child.primarydevice.PrimaryDeviceView
import kotlin.concurrent.fixedRateTimer
class ManageChildAdvancedFragment : Fragment() {
companion object {
@ -149,6 +151,15 @@ class ManageChildAdvancedFragment : Fragment() {
fragmentManager = fragmentManager!!
)
LimitUserViewingView.bind(
view = binding.limitViewing,
auth = auth,
lifecycleOwner = viewLifecycleOwner,
fragmentManager = parentFragmentManager,
userEntry = childEntry,
userId = params.childId
)
return binding.root
}
}

View file

@ -0,0 +1,69 @@
/*
* 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.child.advanced.limituserviewing
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import io.timelimit.android.R
import io.timelimit.android.data.model.User
import io.timelimit.android.data.model.UserFlags
import io.timelimit.android.databinding.LimitUserViewingViewBinding
import io.timelimit.android.livedata.ignoreUnchanged
import io.timelimit.android.livedata.map
import io.timelimit.android.sync.actions.UpdateUserFlagsAction
import io.timelimit.android.ui.help.HelpDialogFragment
import io.timelimit.android.ui.main.ActivityViewModel
object LimitUserViewingView {
fun bind(
view: LimitUserViewingViewBinding,
auth: ActivityViewModel,
lifecycleOwner: LifecycleOwner,
fragmentManager: FragmentManager,
userEntry: LiveData<User?>,
userId: String
) {
view.titleView.setOnClickListener {
HelpDialogFragment.newInstance(
title = R.string.limit_user_viewing_title,
text = R.string.limit_user_viewing_help
).show(fragmentManager)
}
userEntry.map { it?.restrictViewingToParents ?: false }.ignoreUnchanged().observe(lifecycleOwner, Observer { checked ->
view.enableSwitch.setOnCheckedChangeListener { buttonView, isChecked -> /* ignore */ }
view.enableSwitch.isChecked = checked
view.enableSwitch.setOnCheckedChangeListener { buttonView, isChecked ->
if (isChecked != checked) {
if (
!auth.tryDispatchParentAction(
UpdateUserFlagsAction(
userId = userId,
modifiedBits = UserFlags.RESTRICT_VIEWING_TO_PARENTS,
newValues = if (isChecked) UserFlags.RESTRICT_VIEWING_TO_PARENTS else 0
)
)
) {
view.enableSwitch.isChecked = checked
}
}
}
})
}
}

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
@ -69,10 +69,18 @@ class OverviewFragment : CoroutineFragment(), CanNotAddDevicesInLocalModeDialogF
}
override fun onUserClicked(user: User) {
when (user.type) {
UserType.Child -> handlers.openManageChildScreen(childId = user.id)
UserType.Parent -> handlers.openManageParentScreen(parentId = user.id)
}.let { }
if (
user.restrictViewingToParents &&
logic.deviceUserId.value != user.id &&
!auth.requestAuthenticationOrReturnTrue()
) {
// do "nothing"/ request authentication
} else {
when (user.type) {
UserType.Child -> handlers.openManageChildScreen(childId = user.id)
UserType.Parent -> handlers.openManageParentScreen(parentId = user.id)
}.let { }
}
}
override fun onAddDeviceClicked() {

View file

@ -98,6 +98,9 @@
<include android:id="@+id/password"
layout="@layout/manage_child_password" />
<include android:id="@+id/limit_viewing"
layout="@layout/limit_user_viewing_view" />
<androidx.cardview.widget.CardView
app:cardUseCompatPadding="true"
android:layout_width="match_parent"

View file

@ -0,0 +1,48 @@
<?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">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardUseCompatPadding="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp">
<TextView
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:text="@string/limit_user_viewing_title"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/enable_switch"
android:text="@string/limit_user_viewing_checkbox"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</layout>

View file

@ -0,0 +1,24 @@
<?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="limit_user_viewing_title">Nutzersichtbarkeit einschränken</string>
<string name="limit_user_viewing_checkbox">Nur dem Benutzer selbst und den Eltern erlauben, die Benutzerdetails anzuzeigen</string>
<string name="limit_user_viewing_help">Damit kann verhindert werden, dass andere Kinder der Familie
die Einstellungen und Nutzungsdauern dieses Kindes sehen können. Das ist nicht als
Sicherheitsfunktion gedacht und leicht umgehbar, z.B. mit der Nutzung einer älteren TimeLimit-
Version.
</string>
</resources>

View file

@ -0,0 +1,23 @@
<?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="limit_user_viewing_title">Restrict viewing the user</string>
<string name="limit_user_viewing_checkbox">Only allow parents and the user itself to view its details</string>
<string name="limit_user_viewing_help">This allows preventing other child users to
view the configuration and used times of this child user. This is not intended as security feature
and it is not one - using an older version is enough to circumvent this.
</string>
</resources>