mirror of
https://codeberg.org/timelimit/timelimit-android.git
synced 2025-10-03 09:49:25 +02:00
Add user duplication feature
This commit is contained in:
parent
caf7703863
commit
64aaa3ddd7
9 changed files with 359 additions and 0 deletions
|
@ -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>>
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue