From 64aaa3ddd7575cefaea38683dae98e3c04fe61bc Mon Sep 17 00:00:00 2001 From: Jonas Lochmann Date: Mon, 23 Nov 2020 01:00:00 +0100 Subject: [PATCH] Add user duplication feature --- .../android/data/dao/ChildTaskDao.kt | 3 + .../android/data/dao/DerivedDataDao.kt | 8 + .../advanced/ManageChildAdvancedFragment.kt | 7 + .../duplicate/DuplicateChildActions.kt | 163 ++++++++++++++++++ .../duplicate/DuplicateChildDialogFragment.kt | 93 ++++++++++ .../advanced/duplicate/DuplicateChildModel.kt | 68 ++++++++ .../layout/fragment_manage_child_advanced.xml | 7 + app/src/main/res/values-de/strings.xml | 5 + app/src/main/res/values/strings.xml | 5 + 9 files changed, 359 insertions(+) create mode 100644 app/src/main/java/io/timelimit/android/ui/manage/child/advanced/duplicate/DuplicateChildActions.kt create mode 100644 app/src/main/java/io/timelimit/android/ui/manage/child/advanced/duplicate/DuplicateChildDialogFragment.kt create mode 100644 app/src/main/java/io/timelimit/android/ui/manage/child/advanced/duplicate/DuplicateChildModel.kt diff --git a/app/src/main/java/io/timelimit/android/data/dao/ChildTaskDao.kt b/app/src/main/java/io/timelimit/android/data/dao/ChildTaskDao.kt index a7b6d39..5e210c2 100644 --- a/app/src/main/java/io/timelimit/android/data/dao/ChildTaskDao.kt +++ b/app/src/main/java/io/timelimit/android/data/dao/ChildTaskDao.kt @@ -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> + @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 + @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> diff --git a/app/src/main/java/io/timelimit/android/data/dao/DerivedDataDao.kt b/app/src/main/java/io/timelimit/android/data/dao/DerivedDataDao.kt index d90345e..19e5cc1 100644 --- a/app/src/main/java/io/timelimit/android/data/dao/DerivedDataDao.kt +++ b/app/src/main/java/io/timelimit/android/data/dao/DerivedDataDao.kt @@ -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 = deviceAndUserRelatedDataLive fun getUserRelatedDataLive(userId: String): LiveData = usableUserRelatedData.openLiveAtDatabaseThread(userId) diff --git a/app/src/main/java/io/timelimit/android/ui/manage/child/advanced/ManageChildAdvancedFragment.kt b/app/src/main/java/io/timelimit/android/ui/manage/child/advanced/ManageChildAdvancedFragment.kt index 400070d..f2d3aed 100644 --- a/app/src/main/java/io/timelimit/android/ui/manage/child/advanced/ManageChildAdvancedFragment.kt +++ b/app/src/main/java/io/timelimit/android/ui/manage/child/advanced/ManageChildAdvancedFragment.kt @@ -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) diff --git a/app/src/main/java/io/timelimit/android/ui/manage/child/advanced/duplicate/DuplicateChildActions.kt b/app/src/main/java/io/timelimit/android/ui/manage/child/advanced/duplicate/DuplicateChildActions.kt new file mode 100644 index 0000000..761d196 --- /dev/null +++ b/app/src/main/java/io/timelimit/android/ui/manage/child/advanced/duplicate/DuplicateChildActions.kt @@ -0,0 +1,163 @@ +/* + * TimeLimit Copyright 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 . + */ + +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 { + 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().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() + + 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 + )) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/manage/child/advanced/duplicate/DuplicateChildDialogFragment.kt b/app/src/main/java/io/timelimit/android/ui/manage/child/advanced/duplicate/DuplicateChildDialogFragment.kt new file mode 100644 index 0000000..396aa6c --- /dev/null +++ b/app/src/main/java/io/timelimit/android/ui/manage/child/advanced/duplicate/DuplicateChildDialogFragment.kt @@ -0,0 +1,93 @@ +/* + * TimeLimit Copyright 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 . + */ + +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) +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/manage/child/advanced/duplicate/DuplicateChildModel.kt b/app/src/main/java/io/timelimit/android/ui/manage/child/advanced/duplicate/DuplicateChildModel.kt new file mode 100644 index 0000000..e5547e1 --- /dev/null +++ b/app/src/main/java/io/timelimit/android/ui/manage/child/advanced/duplicate/DuplicateChildModel.kt @@ -0,0 +1,68 @@ +/* + * TimeLimit Copyright 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 . + */ + +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().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().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): Status() + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_manage_child_advanced.xml b/app/src/main/res/layout/fragment_manage_child_advanced.xml index 5d2a67b..ad556b2 100644 --- a/app/src/main/res/layout/fragment_manage_child_advanced.xml +++ b/app/src/main/res/layout/fragment_manage_child_advanced.xml @@ -76,6 +76,13 @@ android:layout_width="match_parent" android:layout_height="wrap_content" /> +