Add user duplication feature

This commit is contained in:
Jonas Lochmann 2020-11-23 01:00:00 +01:00
parent caf7703863
commit 64aaa3ddd7
No known key found for this signature in database
GPG key ID: 8B8C9AEE10FA5B36
9 changed files with 359 additions and 0 deletions

View file

@ -42,6 +42,9 @@ interface ChildTaskDao {
@Query("SELECT child_task.*, category.title as category_title FROM child_task JOIN category ON (child_task.category_id = category.id) WHERE category.child_id = :userId")
fun getTasksByUserIdWithCategoryTitlesLive(userId: String): LiveData<List<ChildTaskWithCategoryTitle>>
@Query("SELECT child_task.* FROM child_task JOIN category ON (child_task.category_id = category.id) WHERE category.child_id = :userId")
fun getTasksByUserIdSync(userId: String): List<ChildTask>
@Query("SELECT child_task.*, category.title as category_title, user.name as child_name FROM child_task JOIN category ON (child_task.category_id = category.id) JOIN user ON (category.child_id = user.id) WHERE child_task.pending_request = 1")
fun getPendingTasks(): LiveData<List<FullChildTask>>

View file

@ -173,6 +173,14 @@ class DerivedDataDao (private val database: Database) {
return result
}
fun getUserRelatedDataSync(userId: String): UserRelatedData? {
val result = usableUserRelatedData.openSync(userId, null)
usableUserRelatedData.close(userId, null)
return result
}
fun getUserAndDeviceRelatedDataLive(): LiveData<DeviceAndUserRelatedData?> = deviceAndUserRelatedDataLive
fun getUserRelatedDataLive(userId: String): LiveData<UserRelatedData?> = usableUserRelatedData.openLiveAtDatabaseThread(userId)

View file

@ -31,6 +31,7 @@ import io.timelimit.android.logic.AppLogic
import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.ui.main.ActivityViewModel
import io.timelimit.android.ui.main.getActivityViewModel
import io.timelimit.android.ui.manage.child.advanced.duplicate.DuplicateChildDialogFragment
import io.timelimit.android.ui.manage.child.advanced.limituserviewing.LimitUserViewingView
import io.timelimit.android.ui.manage.child.advanced.managedisabletimelimits.ManageDisableTimelimitsViewHelper
import io.timelimit.android.ui.manage.child.advanced.password.ManageChildPassword
@ -99,6 +100,12 @@ class ManageChildAdvancedFragment : Fragment() {
}
}
binding.duplicateChildButton.setOnClickListener {
if (auth.requestAuthenticationOrReturnTrue()) {
DuplicateChildDialogFragment.newInstance(childId).show(parentFragmentManager)
}
}
binding.deleteUserButton.setOnClickListener {
if (auth.requestAuthenticationOrReturnTrue()) {
DeleteChildDialogFragment.newInstance(childId).show(parentFragmentManager)

View file

@ -0,0 +1,163 @@
/*
* 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.duplicate
import io.timelimit.android.async.Threads
import io.timelimit.android.coroutines.executeAndWait
import io.timelimit.android.data.Database
import io.timelimit.android.data.IdGenerator
import io.timelimit.android.data.extensions.sortedCategories
import io.timelimit.android.data.model.UserType
import io.timelimit.android.sync.actions.*
object DuplicateChildActions {
suspend fun calculateDuplicateChildActions(userId: String, database: Database, newUserName: String): List<ParentAction> {
val (data, oldTasks) = Threads.database.executeAndWait {
database.runInUnobservedTransaction {
val data = database.derivedDataDao().getUserRelatedDataSync(userId) ?: throw IllegalStateException("user not found")
val tasks = database.childTasks().getTasksByUserIdSync(userId)
data to tasks
}
}
return mutableListOf<ParentAction>().also { result ->
val sourceUser = data.user
if (sourceUser.type != UserType.Child) throw IllegalArgumentException()
val newUserId = IdGenerator.generateId()
result.add(AddUserAction(
userId = newUserId,
userType = UserType.Child,
timeZone = sourceUser.timeZone,
name = newUserName,
password = null
))
sourceUser.flags.let { flags ->
if (flags != 0L) result.add(UpdateUserFlagsAction(
userId = newUserId,
modifiedBits = flags,
newValues = flags
))
}
val newCategoryIds = mutableMapOf<String, String>()
data.categories.forEach { oldCategory ->
val newCategoryId = IdGenerator.generateId().also { newCategoryIds[oldCategory.category.id] = it }
result.add(CreateCategoryAction(
childId = newUserId,
categoryId = newCategoryId,
title = oldCategory.category.title
))
oldCategory.category.blockedMinutesInWeek.let { blockedTimes ->
if (!blockedTimes.dataNotToModify.isEmpty) {
result.add(UpdateCategoryBlockedTimesAction(
categoryId = newCategoryId,
blockedTimes = blockedTimes
))
}
}
if (oldCategory.category.blockAllNotifications) {
result.add(UpdateCategoryBlockAllNotificationsAction(
categoryId = newCategoryId,
blocked = true
))
}
oldCategory.category.timeWarnings.let { timeWarnings ->
if (timeWarnings != 0) {
result.add(UpdateCategoryTimeWarningsAction(
categoryId = newCategoryId,
enable = true,
flags = timeWarnings
))
}
}
if (oldCategory.category.minBatteryLevelWhileCharging != 0 || oldCategory.category.minBatteryLevelMobile != 0) {
result.add(UpdateCategoryBatteryLimit(
categoryId = newCategoryId,
chargingLimit = oldCategory.category.minBatteryLevelWhileCharging,
mobileLimit = oldCategory.category.minBatteryLevelMobile
))
}
oldCategory.rules.forEach { oldRule ->
result.add(CreateTimeLimitRuleAction(
rule = oldRule.copy(id = IdGenerator.generateId(), categoryId = newCategoryId)
))
}
oldCategory.networks.forEach { oldNetwork ->
result.add(AddCategoryNetworkId(
categoryId = newCategoryId,
itemId = oldNetwork.networkItemId,
hashedNetworkId = oldNetwork.hashedNetworkId
))
}
}
data.categoryApps.groupBy { it.categoryId }.forEach { (oldCategoryId, oldApps) ->
val newCategoryId = newCategoryIds[oldCategoryId]!!
result.add(AddCategoryAppsAction(
categoryId = newCategoryId,
packageNames = oldApps.map { it.packageName }
))
}
result.add(UpdateCategorySortingAction(
categoryIds = data.sortedCategories().map { newCategoryIds[it.second.category.id]!! }
))
newCategoryIds[sourceUser.categoryForNotAssignedApps]?.let { categoryForNotAssignedApps ->
result.add(SetCategoryForUnassignedApps(
childId = newUserId,
categoryId = categoryForNotAssignedApps
))
}
data.categories.forEach { oldCategory ->
newCategoryIds[oldCategory.category.parentCategoryId]?.let { newParentCategoryId ->
val newCategoryId = newCategoryIds[oldCategory.category.id]!!
result.add(SetParentCategory(
categoryId = newCategoryId,
parentCategory = newParentCategoryId
))
}
}
oldTasks.forEach { oldTask ->
result.add(UpdateChildTaskAction(
isNew = true,
taskId = IdGenerator.generateId(),
categoryId = newCategoryIds[oldTask.categoryId]!!,
taskTitle = oldTask.taskTitle,
extraTimeDuration = oldTask.extraTimeDuration
))
}
}
}
}

View file

@ -0,0 +1,93 @@
/*
* 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.duplicate
import android.app.Dialog
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
import io.timelimit.android.R
import io.timelimit.android.extensions.showSafe
import io.timelimit.android.ui.main.getActivityViewModel
class DuplicateChildDialogFragment: DialogFragment() {
companion object {
private const val DIALOG_TAG = "DuplicateChildDialogFragment"
private const val CHILD_ID = "childId"
fun newInstance(childId: String) = DuplicateChildDialogFragment().apply {
arguments = Bundle().apply { putString(CHILD_ID, childId) }
}
}
private val model: DuplicateChildModel by viewModels()
private val childId: String get() = requireArguments().getString(CHILD_ID)!!
private val auth get() = getActivityViewModel(requireActivity())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
auth.logic.database.user().getChildUserByIdLive(childId).observe(this) { if (it == null) dismissAllowingStateLoss() }
auth.authenticatedUser.observe(this) { if (it == null) dismissAllowingStateLoss() }
model.status.observe(this) { status ->
when (status) {
is DuplicateChildModel.Status.WaitingForConfirmation -> {/* do nothing */}
is DuplicateChildModel.Status.Preparing -> {/* do nothing */}
is DuplicateChildModel.Status.HasAction -> {
Toast.makeText(requireContext(), R.string.duplicate_child_done_toast, Toast.LENGTH_SHORT).show()
auth.tryDispatchParentActions(status.actions)
dismissAllowingStateLoss()
}
is DuplicateChildModel.Status.Failure -> {
Toast.makeText(requireContext(), R.string.error_general, Toast.LENGTH_SHORT).show()
dismissAllowingStateLoss()
}
}.let {/* require handling all paths */}
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = AlertDialog.Builder(requireContext(), theme)
.setMessage(R.string.duplicate_child_message)
.setNegativeButton(R.string.generic_no, null)
.setPositiveButton(R.string.generic_yes, null)
.create()
.also { dialog ->
dialog.setOnShowListener {
val yesButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
val noButton = dialog.getButton(AlertDialog.BUTTON_NEGATIVE)
yesButton.setOnClickListener { model.start(childId) }
model.status.observe(this) {
val enableButtons = it is DuplicateChildModel.Status.WaitingForConfirmation
isCancelable = enableButtons
noButton.isEnabled = enableButtons
yesButton.isEnabled = enableButtons
}
}
}
fun show(fragmentManager: FragmentManager) = showSafe(fragmentManager, DIALOG_TAG)
}

View file

@ -0,0 +1,68 @@
/*
* 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.duplicate
import android.app.Application
import android.util.Log
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import io.timelimit.android.BuildConfig
import io.timelimit.android.R
import io.timelimit.android.coroutines.runAsync
import io.timelimit.android.livedata.castDown
import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.sync.actions.ParentAction
class DuplicateChildModel (application: Application): AndroidViewModel(application) {
companion object {
private const val LOG_TAG = "DuplicateChildModel"
}
private val logic = DefaultAppLogic.with(application)
private val statusInternal = MutableLiveData<Status>().apply { value = Status.WaitingForConfirmation }
val status = statusInternal.castDown()
fun start(userId: String) {
if (statusInternal.value != Status.WaitingForConfirmation) return
statusInternal.value = Status.Preparing
runAsync {
try {
val result = DuplicateChildActions.calculateDuplicateChildActions(
userId = userId,
database = logic.database,
newUserName = getApplication<Application>().getString(R.string.duplicate_child_user_name)
)
statusInternal.value = Status.HasAction(result)
} catch (ex: Exception) {
if (BuildConfig.DEBUG) {
Log.w(LOG_TAG, "could not clone user $userId", ex)
}
statusInternal.value = Status.Failure
}
}
}
sealed class Status {
object WaitingForConfirmation: Status()
object Preparing: Status()
object Failure: Status()
class HasAction(val actions: List<ParentAction>): Status()
}
}

View file

@ -76,6 +76,13 @@
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
style="?materialButtonOutlinedStyle"
android:id="@+id/duplicate_child_button"
android:text="@string/duplicate_child_title"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
style="?materialButtonOutlinedStyle"
android:id="@+id/delete_user_button"

View file

@ -1215,6 +1215,11 @@
<string name="rename_child_title">Kind-Benutzer umbenennen</string>
<string name="duplicate_child_title">Benutzer duplizieren</string>
<string name="duplicate_child_message">Möchten Sie einen neuen Benutzer mit den gleichen Einstellungen erstellen?</string>
<string name="duplicate_child_done_toast">Der Benutzer wurde dupliziert</string>
<string name="duplicate_child_user_name">duplizierter Benutzer</string>
<string name="reset_hidden_hints_title">Angezeigte Hinweise zurücksetzen</string>
<string name="reset_hidden_hints_description">Sie können einige Hinweise ausblenden. Das ermöglicht es Ihnen, diese Hinweise erneut anzuzeigen.</string>
<string name="reset_hidden_hints_not_possible">Sie haben keine Hinweise ausgeblendet</string>

View file

@ -1257,6 +1257,11 @@
<string name="rename_child_title">Rename child user</string>
<string name="duplicate_child_title">Duplicate user</string>
<string name="duplicate_child_message">Do you want to create a new user with the same settings?</string>
<string name="duplicate_child_done_toast">The user was duplicated</string>
<string name="duplicate_child_user_name">duplicated user</string>
<string name="reset_hidden_hints_title">Reset hidden hints</string>
<string name="reset_hidden_hints_description">You can chose to hide some messages. This allows to show these messages again.</string>
<string name="reset_hidden_hints_not_possible">You did not hide any hints</string>