From ae02d2fdfff629f10616e6b10be30c9045a9b41c Mon Sep 17 00:00:00 2001 From: Jonas Lochmann Date: Mon, 6 Mar 2023 01:00:00 +0100 Subject: [PATCH] Rebuild user selection screen --- .../io/timelimit/android/ui/MainActivity.kt | 1 - .../timelimit/android/ui/ScreenMultiplexer.kt | 4 +- .../defaultuser/ManageDeviceDefaultUser.kt | 117 ------- .../SetDeviceDefaultUserDialogFragment.kt | 131 -------- ...tDeviceDefaultUserTimeoutDialogFragment.kt | 125 -------- .../manage/user/ManageDeviceUserFragment.kt | 179 ----------- .../manage/user/ManageDeviceUserScreen.kt | 180 +++++++++++ .../timelimit/android/ui/model/MainModel.kt | 2 +- .../io/timelimit/android/ui/model/Screen.kt | 15 +- .../io/timelimit/android/ui/model/State.kt | 31 +- .../android/ui/model/UpdateStateCommand.kt | 8 +- .../android/ui/model/flow/SplitFlow.kt | 16 +- .../managedevice/ManageDeviceHandling.kt | 40 ++- .../ui/model/managedevice/ManageDeviceUser.kt | 293 ++++++++++++++++++ .../res/layout/manage_device_default_user.xml | 138 --------- .../layout/manage_device_user_fragment.xml | 81 ----- app/src/main/res/navigation/nav_graph.xml | 9 - app/src/main/res/values-de/strings.xml | 30 +- app/src/main/res/values/fragment_ids.xml | 1 - app/src/main/res/values/strings.xml | 24 +- 20 files changed, 557 insertions(+), 868 deletions(-) delete mode 100644 app/src/main/java/io/timelimit/android/ui/manage/device/manage/defaultuser/ManageDeviceDefaultUser.kt delete mode 100644 app/src/main/java/io/timelimit/android/ui/manage/device/manage/defaultuser/SetDeviceDefaultUserDialogFragment.kt delete mode 100644 app/src/main/java/io/timelimit/android/ui/manage/device/manage/defaultuser/SetDeviceDefaultUserTimeoutDialogFragment.kt delete mode 100644 app/src/main/java/io/timelimit/android/ui/manage/device/manage/user/ManageDeviceUserFragment.kt create mode 100644 app/src/main/java/io/timelimit/android/ui/manage/device/manage/user/ManageDeviceUserScreen.kt create mode 100644 app/src/main/java/io/timelimit/android/ui/model/managedevice/ManageDeviceUser.kt delete mode 100644 app/src/main/res/layout/manage_device_default_user.xml delete mode 100644 app/src/main/res/layout/manage_device_user_fragment.xml diff --git a/app/src/main/java/io/timelimit/android/ui/MainActivity.kt b/app/src/main/java/io/timelimit/android/ui/MainActivity.kt index 6e393e0..6b6e4d5 100644 --- a/app/src/main/java/io/timelimit/android/ui/MainActivity.kt +++ b/app/src/main/java/io/timelimit/android/ui/MainActivity.kt @@ -285,7 +285,6 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De content = { paddingValues -> ScreenMultiplexer( screen = screen, - executeCommand = ::execute, fragmentManager = supportFragmentManager, fragmentIds = mainModel.fragmentIds, modifier = Modifier diff --git a/app/src/main/java/io/timelimit/android/ui/ScreenMultiplexer.kt b/app/src/main/java/io/timelimit/android/ui/ScreenMultiplexer.kt index 5a40f6b..6793081 100644 --- a/app/src/main/java/io/timelimit/android/ui/ScreenMultiplexer.kt +++ b/app/src/main/java/io/timelimit/android/ui/ScreenMultiplexer.kt @@ -19,14 +19,13 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.fragment.app.FragmentManager import io.timelimit.android.ui.diagnose.deviceowner.DeviceOwnerScreen +import io.timelimit.android.ui.manage.device.manage.user.ManageDeviceUserScreen import io.timelimit.android.ui.model.Screen -import io.timelimit.android.ui.model.UpdateStateCommand import io.timelimit.android.ui.overview.overview.OverviewScreen @Composable fun ScreenMultiplexer( screen: Screen?, - executeCommand: (UpdateStateCommand) -> Unit, fragmentManager: FragmentManager, fragmentIds: MutableSet, modifier: Modifier = Modifier @@ -35,6 +34,7 @@ fun ScreenMultiplexer( null -> {/* nothing to do */ } is Screen.FragmentScreen -> FragmentScreen(screen, fragmentManager, fragmentIds, modifier = modifier) is Screen.OverviewScreen -> OverviewScreen(screen.content, modifier = modifier) + is Screen.ManageDeviceUserScreen -> ManageDeviceUserScreen(screen.items, screen.actions, screen.overlay, modifier) is Screen.DeviceOwnerScreen -> DeviceOwnerScreen(screen.content, modifier = modifier) } } \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/manage/device/manage/defaultuser/ManageDeviceDefaultUser.kt b/app/src/main/java/io/timelimit/android/ui/manage/device/manage/defaultuser/ManageDeviceDefaultUser.kt deleted file mode 100644 index 25e9e49..0000000 --- a/app/src/main/java/io/timelimit/android/ui/manage/device/manage/defaultuser/ManageDeviceDefaultUser.kt +++ /dev/null @@ -1,117 +0,0 @@ -/* - * 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.device.manage.defaultuser - -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.coroutines.runAsync -import io.timelimit.android.data.model.Device -import io.timelimit.android.data.model.User -import io.timelimit.android.databinding.ManageDeviceDefaultUserBinding -import io.timelimit.android.livedata.map -import io.timelimit.android.livedata.switchMap -import io.timelimit.android.sync.actions.SignOutAtDeviceAction -import io.timelimit.android.sync.actions.apply.ApplyActionUtil -import io.timelimit.android.ui.help.HelpDialogFragment -import io.timelimit.android.ui.main.ActivityViewModel -import io.timelimit.android.ui.payment.RequiresPurchaseDialogFragment -import io.timelimit.android.util.TimeTextUtil - -object ManageDeviceDefaultUser { - fun bind( - view: ManageDeviceDefaultUserBinding, - users: LiveData>, - lifecycleOwner: LifecycleOwner, - device: LiveData, - isThisDevice: LiveData, - auth: ActivityViewModel, - fragmentManager: FragmentManager - ) { - val context = view.root.context - - view.titleView.setOnClickListener { - HelpDialogFragment.newInstance( - title = R.string.manage_device_default_user_title, - text = R.string.manage_device_default_user_info - ).show(fragmentManager) - } - - device.switchMap { deviceEntry -> - users.map { users -> - deviceEntry to users.find { it.id == deviceEntry?.defaultUser } - } - }.observe(lifecycleOwner, Observer { (deviceEntry, defaultUser) -> - view.hasDefaultUser = defaultUser != null - view.isAlreadyUsingDefaultUser = defaultUser != null && deviceEntry?.currentUserId == defaultUser.id - view.defaultUserTitle = defaultUser?.name - }) - - isThisDevice.observe(lifecycleOwner, Observer { - view.isCurrentDevice = it - }) - - device.observe(lifecycleOwner, Observer { deviceEntry -> - view.setDefaultUserButton.setOnClickListener { - if (deviceEntry != null && auth.requestAuthenticationOrReturnTrue()) { - SetDeviceDefaultUserDialogFragment.newInstance( - deviceId = deviceEntry.id - ).show(fragmentManager) - } - } - - view.configureAutoLogoutButton.setOnClickListener { - if (deviceEntry != null && auth.requestAuthenticationOrReturnTrue()) { - SetDeviceDefaultUserTimeoutDialogFragment - .newInstance(deviceId = deviceEntry.id) - .show(fragmentManager) - } - } - - val defaultUserTimeout = deviceEntry?.defaultUserTimeout ?: 0 - - view.isAutomaticallySwitchingToDefaultUserEnabled = defaultUserTimeout != 0 - view.defaultUserSwitchText = if (defaultUserTimeout == 0) - context.getString(R.string.manage_device_default_user_timeout_off) - else - context.getString( - R.string.manage_device_default_user_timeout_on, - if (defaultUserTimeout < 1000 * 60) - TimeTextUtil.seconds(defaultUserTimeout / 1000, context) - else - TimeTextUtil.time(defaultUserTimeout, context) - ) - }) - - auth.logic.fullVersion.shouldProvideFullVersionFunctions.observe(lifecycleOwner, Observer { fullVersion -> - view.switchToDefaultUserButton.setOnClickListener { - if (fullVersion) { - runAsync { - ApplyActionUtil.applyAppLogicAction( - action = SignOutAtDeviceAction, - appLogic = auth.logic, - ignoreIfDeviceIsNotConfigured = true - ) - } - } else { - RequiresPurchaseDialogFragment().show(fragmentManager) - } - } - }) - } -} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/manage/device/manage/defaultuser/SetDeviceDefaultUserDialogFragment.kt b/app/src/main/java/io/timelimit/android/ui/manage/device/manage/defaultuser/SetDeviceDefaultUserDialogFragment.kt deleted file mode 100644 index ce90292..0000000 --- a/app/src/main/java/io/timelimit/android/ui/manage/device/manage/defaultuser/SetDeviceDefaultUserDialogFragment.kt +++ /dev/null @@ -1,131 +0,0 @@ -/* - * TimeLimit Copyright 2019 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.device.manage.defaultuser - -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.Database -import io.timelimit.android.data.model.UserType -import io.timelimit.android.databinding.BottomSheetSelectionListBinding -import io.timelimit.android.extensions.showSafe -import io.timelimit.android.livedata.ignoreUnchanged -import io.timelimit.android.livedata.map -import io.timelimit.android.livedata.switchMap -import io.timelimit.android.logic.AppLogic -import io.timelimit.android.logic.DefaultAppLogic -import io.timelimit.android.sync.actions.SetDeviceDefaultUserAction -import io.timelimit.android.ui.main.ActivityViewModel -import io.timelimit.android.ui.main.ActivityViewModelHolder - -class SetDeviceDefaultUserDialogFragment: BottomSheetDialogFragment() { - companion object { - private const val EXTRA_DEVICE_ID = "deviceId" - private const val DIALOG_TAG = "sddudf" - - fun newInstance(deviceId: String) = SetDeviceDefaultUserDialogFragment().apply { - arguments = Bundle().apply { - putString(EXTRA_DEVICE_ID, deviceId) - } - } - } - - val deviceId: String by lazy { arguments!!.getString(EXTRA_DEVICE_ID)!! } - val logic: AppLogic by lazy { DefaultAppLogic.with(context!!) } - val database: Database by lazy { logic.database } - val auth: ActivityViewModel by lazy { (activity as ActivityViewModelHolder).getActivityViewModel() } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - auth.authenticatedUser.observe(this, Observer { - if (it?.second?.type != UserType.Parent) { - dismissAllowingStateLoss() - } - }) - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - val binding = BottomSheetSelectionListBinding.inflate(inflater, container, false) - - binding.title = getString(R.string.manage_device_default_user_title) - - val list = binding.list - val users = database.user().getAllUsersLive() - val deviceEntry = database.device().getDeviceById(deviceId) - val currentDefaultUserId = deviceEntry.map { it?.defaultUser }.ignoreUnchanged() - - currentDefaultUserId.switchMap { v1 -> - users.map { v2 -> v1 to v2 } - }.observe(this, Observer { (defaultUserId, userList) -> - list.removeAllViews() - - fun buildRow(): CheckedTextView = LayoutInflater.from(context!!).inflate( - android.R.layout.simple_list_item_single_choice, - list, - false - ) as CheckedTextView - - val hasDefaultUser = userList.find { it.id == defaultUserId } != null - - userList.forEach { user -> - buildRow().let { row -> - row.text = user.name - row.isChecked = defaultUserId == user.id - row.setOnClickListener { - auth.tryDispatchParentAction( - SetDeviceDefaultUserAction( - deviceId = deviceId, - defaultUserId = user.id - ) - ) - - dismiss() - } - - list.addView(row) - } - } - - buildRow().let { row -> - row.setText(R.string.manage_device_default_user_selection_none) - row.isChecked = !hasDefaultUser - row.setOnClickListener { - auth.tryDispatchParentAction( - SetDeviceDefaultUserAction( - deviceId = deviceId, - defaultUserId = "" - ) - ) - - dismiss() - } - - list.addView(row) - } - }) - - return binding.root - } - - 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/device/manage/defaultuser/SetDeviceDefaultUserTimeoutDialogFragment.kt b/app/src/main/java/io/timelimit/android/ui/manage/device/manage/defaultuser/SetDeviceDefaultUserTimeoutDialogFragment.kt deleted file mode 100644 index e1c2680..0000000 --- a/app/src/main/java/io/timelimit/android/ui/manage/device/manage/defaultuser/SetDeviceDefaultUserTimeoutDialogFragment.kt +++ /dev/null @@ -1,125 +0,0 @@ -/* - * TimeLimit Copyright 2019 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.device.manage.defaultuser - -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.LiveData -import androidx.lifecycle.Observer -import com.google.android.material.bottomsheet.BottomSheetDialogFragment -import io.timelimit.android.R -import io.timelimit.android.data.model.Device -import io.timelimit.android.data.model.UserType -import io.timelimit.android.databinding.BottomSheetSelectionListBinding -import io.timelimit.android.extensions.showSafe -import io.timelimit.android.logic.DefaultAppLogic -import io.timelimit.android.sync.actions.SetDeviceDefaultUserTimeoutAction -import io.timelimit.android.ui.main.ActivityViewModel -import io.timelimit.android.ui.main.getActivityViewModel -import io.timelimit.android.util.TimeTextUtil - -class SetDeviceDefaultUserTimeoutDialogFragment: BottomSheetDialogFragment() { - companion object { - private const val EXTRA_DEVICE_ID = "deviceId" - private const val DIALOG_TAG = "sddutdf" - private val OPTIONS = listOf( - 0, - 1000 * 5, - 1000 * 60, - 1000 * 60 * 5, - 1000 * 60 * 15, - 1000 * 60 * 30, - 1000 * 60 * 60 - ) - - fun newInstance(deviceId: String) = SetDeviceDefaultUserTimeoutDialogFragment().apply { - arguments = Bundle().apply { - putString(EXTRA_DEVICE_ID, deviceId) - } - } - } - - val deviceId: String by lazy { arguments!!.getString(EXTRA_DEVICE_ID)!! } - val deviceEntry: LiveData by lazy { - DefaultAppLogic.with(context!!).database.device().getDeviceById(deviceId) - } - val auth: ActivityViewModel by lazy { getActivityViewModel(activity!!) } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - auth.authenticatedUser.observe(this, Observer { - if (it?.second?.type != UserType.Parent) { - dismissAllowingStateLoss() - } - }) - - deviceEntry.observe(this, Observer { - if (it == null) { - dismissAllowingStateLoss() - } - }) - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - val binding = BottomSheetSelectionListBinding.inflate(inflater, container, false) - binding.title = getString(R.string.manage_device_default_user_timeout_dialog_title) - val list = binding.list - - deviceEntry.observe(this, Observer { device -> - val timeout = device?.defaultUserTimeout ?: 0 - - fun buildRow(): CheckedTextView = LayoutInflater.from(context!!).inflate( - android.R.layout.simple_list_item_single_choice, - list, - false - ) as CheckedTextView - - list.removeAllViews() - - OPTIONS.forEach { option -> - buildRow().let { row -> - row.text = if (option == 0) - getString(R.string.manage_device_default_user_timeout_dialog_disable) - else if (option < 1000 * 60) - TimeTextUtil.seconds(option / 1000, context!!) - else - TimeTextUtil.time(option, context!!) - - row.isChecked = option == timeout - row.setOnClickListener { - auth.tryDispatchParentAction(SetDeviceDefaultUserTimeoutAction( - deviceId = deviceId, - timeout = option - )) - - dismiss() - } - - list.addView(row) - } - } - }) - - return binding.root - } - - 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/device/manage/user/ManageDeviceUserFragment.kt b/app/src/main/java/io/timelimit/android/ui/manage/device/manage/user/ManageDeviceUserFragment.kt deleted file mode 100644 index 04cfca8..0000000 --- a/app/src/main/java/io/timelimit/android/ui/manage/device/manage/user/ManageDeviceUserFragment.kt +++ /dev/null @@ -1,179 +0,0 @@ -/* - * TimeLimit Copyright 2019 - 2023 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.device.manage.user - - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.RadioButton -import androidx.fragment.app.Fragment -import androidx.lifecycle.LiveData -import androidx.lifecycle.Observer -import io.timelimit.android.R -import io.timelimit.android.data.model.Device -import io.timelimit.android.databinding.ManageDeviceUserFragmentBinding -import io.timelimit.android.livedata.ignoreUnchanged -import io.timelimit.android.livedata.liveDataFromNonNullValue -import io.timelimit.android.livedata.map -import io.timelimit.android.livedata.mergeLiveData -import io.timelimit.android.logic.AppLogic -import io.timelimit.android.logic.DefaultAppLogic -import io.timelimit.android.sync.actions.SetDeviceUserAction -import io.timelimit.android.ui.main.ActivityViewModel -import io.timelimit.android.ui.main.ActivityViewModelHolder -import io.timelimit.android.ui.main.AuthenticationFab -import io.timelimit.android.ui.main.FragmentWithCustomTitle -import io.timelimit.android.ui.manage.device.manage.defaultuser.ManageDeviceDefaultUser -import io.timelimit.android.ui.model.UpdateStateCommand -import io.timelimit.android.ui.model.execute - -class ManageDeviceUserFragment : Fragment(), FragmentWithCustomTitle { - private val activity: ActivityViewModelHolder by lazy { getActivity() as ActivityViewModelHolder } - private val logic: AppLogic by lazy { DefaultAppLogic.with(context!!) } - private val auth: ActivityViewModel by lazy { activity.getActivityViewModel() } - private val args: ManageDeviceUserFragmentArgs by lazy { ManageDeviceUserFragmentArgs.fromBundle(arguments!!) } - private val deviceEntry: LiveData by lazy { - logic.database.device().getDeviceById(args.deviceId) - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - val binding = ManageDeviceUserFragmentBinding.inflate(inflater, container, false) - val userEntries = logic.database.user().getAllUsersLive() - - var isUpdatingSelectedUser = false - - // auth - AuthenticationFab.manageAuthenticationFab( - fab = binding.fab, - shouldHighlight = auth.shouldHighlightAuthenticationButton, - authenticatedUser = auth.authenticatedUser, - fragment = this, - doesSupportAuth = liveDataFromNonNullValue(true) - ) - - // label, id - val userListItems = ArrayList>() - - fun bindUserListItems() { - userListItems.forEachIndexed { index, listItem -> - val oldRadio = binding.userList.getChildAt(index) as RadioButton? - val radio = oldRadio ?: RadioButton(requireContext()) - - radio.text = listItem.first - - if (oldRadio == null) { - radio.layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ) - radio.id = index - - binding.userList.addView(radio) - } - } - - while (binding.userList.childCount > userListItems.size) { - binding.userList.removeViewAt(userListItems.size) - } - } - - fun bindUserListSelection() { - isUpdatingSelectedUser = true - - val selectedUserId = deviceEntry.value?.currentUserId - val selectedIndex = userListItems.indexOfFirst { it.second == selectedUserId } - - if (selectedIndex != -1) { - binding.userList.check(selectedIndex) - } else { - val fallbackSelectedIndex = userListItems.indexOfFirst { it.second == "" } - - if (fallbackSelectedIndex != -1) { - binding.userList.check(fallbackSelectedIndex) - } - } - - isUpdatingSelectedUser = false - } - - binding.handlers = object: ManageDeviceUserFragmentHandlers { - override fun showAuthenticationScreen() { - activity.showAuthenticationScreen() - } - } - - binding.userList.setOnCheckedChangeListener { _, checkedId -> - val userId = userListItems[checkedId].second - val device = deviceEntry.value - - if (device != null && device.currentUserId != userId && !isUpdatingSelectedUser) { - if (!auth.tryDispatchParentAction( - SetDeviceUserAction( - deviceId = args.deviceId, - userId = userId - ) - )) { - bindUserListSelection() - } - } - } - - deviceEntry.observe(this, Observer { - device -> - - if (device == null) { - requireActivity().execute(UpdateStateCommand.ManageDevice.Leave) - } - }) - - val isThisDevice = logic.deviceId.map { ownDeviceId -> ownDeviceId == args.deviceId }.ignoreUnchanged() - - mergeLiveData(deviceEntry, userEntries).observe(this, Observer { - val (device, users) = it!! - - if (device != null && users != null) { - userListItems.clear() - userListItems.addAll( - users.map { user -> Pair(user.name, user.id) } - ) - userListItems.add(Pair(getString(R.string.manage_device_current_user_none), "")) - - bindUserListItems() - bindUserListSelection() - } - }) - - ManageDeviceDefaultUser.bind( - view = binding.defaultUser, - device = deviceEntry, - users = userEntries, - lifecycleOwner = this, - isThisDevice = isThisDevice, - auth = auth, - fragmentManager = parentFragmentManager - ) - - return binding.root - } - - override fun getCustomTitle(): LiveData = deviceEntry.map { "${getString(R.string.manage_device_card_user_title)} < ${it?.name} < ${getString(R.string.main_tab_overview)}" } -} - -interface ManageDeviceUserFragmentHandlers { - fun showAuthenticationScreen() -} diff --git a/app/src/main/java/io/timelimit/android/ui/manage/device/manage/user/ManageDeviceUserScreen.kt b/app/src/main/java/io/timelimit/android/ui/manage/device/manage/user/ManageDeviceUserScreen.kt new file mode 100644 index 0000000..7a9aeee --- /dev/null +++ b/app/src/main/java/io/timelimit/android/ui/manage/device/manage/user/ManageDeviceUserScreen.kt @@ -0,0 +1,180 @@ +/* + * TimeLimit Copyright 2019 - 2023 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.device.manage.user + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.clickable +import io.timelimit.android.R +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import io.timelimit.android.data.model.UserType +import io.timelimit.android.ui.model.managedevice.ManageDeviceUser +import io.timelimit.android.util.TimeTextUtil + +@OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class) +@Composable +fun ManageDeviceUserScreen( + items: List, + actions: ManageDeviceUser.Actions, + overlay: ManageDeviceUser.Overlay?, + modifier: Modifier = Modifier +) { + LazyColumn( + modifier, + contentPadding = PaddingValues(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(items, key = { it.id }) { item -> + Card( + onClick = { actions.select(item) }, + modifier = Modifier + .animateItemPlacement() + .fillMaxWidth(), + backgroundColor = when (item.selected) { + true -> MaterialTheme.colors.secondary + false -> MaterialTheme.colors.surface + } + ) { + val buttonColors = + if (item.selected) ButtonDefaults.textButtonColors( + contentColor = MaterialTheme.colors.onSecondary.copy(alpha = .8f), + disabledContentColor = MaterialTheme.colors.onSecondary + ) + else ButtonDefaults.textButtonColors() + + Column( + Modifier.padding(8.dp) + ) { + Text( + item.name, + style = MaterialTheme.typography.h5 + ) + + if (item.defaultUser is ManageDeviceUser.UserItem.DefaultUser.Yes) { + Text(stringResource(R.string.manage_device_user_is_default_user)) + + if (item.defaultUser.timeout > 0) { + Text(stringResource( + R.string.manage_device_user_default_user_timeout, + if (item.defaultUser.timeout < 1000 * 60) TimeTextUtil.seconds(item.defaultUser.timeout / 1000, LocalContext.current) + else TimeTextUtil.time(item.defaultUser.timeout, LocalContext.current) + )) + } + + TextButton(onClick = actions.disableDefaultUser, colors = buttonColors) { + Text(stringResource(R.string.manage_device_user_disable_default_user)) + } + + TextButton(onClick = actions.configureAutoSwitching, colors = buttonColors) { + Text(stringResource( + if (item.defaultUser.timeout == 0) R.string.manage_device_default_user_timeout_btn_enable + else R.string.manage_device_default_user_timeout_btn_change + )) + } + } else if (item.selected) { + TextButton(onClick = { actions.makeDefaultUser(item) }, colors = buttonColors) { + Text(stringResource(R.string.manage_device_user_make_default_user)) + } + } else { + Text(when (item.type) { + UserType.Child -> stringResource(R.string.add_user_type_child) + UserType.Parent -> stringResource(R.string.add_user_type_parent) + }) + } + } + } + } + } + + when (overlay) { + is ManageDeviceUser.Overlay.EnableDefaultUser -> AlertDialog( + title = { Text(stringResource(R.string.manage_device_default_user_title)) }, + text = { + Column ( + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text(stringResource(R.string.manage_device_default_user_info)) + Text(stringResource(R.string.manage_device_default_user_confirm, overlay.userTitle)) + Text(stringResource(R.string.purchase_required_info_local_mode_free)) + } + }, + confirmButton = { + TextButton(onClick = overlay.confirm) { + Text(stringResource(R.string.generic_set)) + } + }, + dismissButton = { + TextButton(onClick = overlay.cancel) { + Text(stringResource(R.string.generic_cancel)) + } + }, + onDismissRequest = overlay.cancel + ) + is ManageDeviceUser.Overlay.ConfigureTimeout -> AlertDialog( + title = { Text(stringResource(R.string.manage_device_default_user_timeout_dialog_title)) }, + text = { + val options = listOf( + 0, + 1000 * 5, + 1000 * 60, + 1000 * 60 * 5, + 1000 * 60 * 15, + 1000 * 60 * 30, + 1000 * 60 * 60 + ) + + Column { + for (option in options) { + val onClick = { overlay.confirm(option) } + + val label = + if (option == 0) stringResource(R.string.manage_device_default_user_timeout_dialog_disable) + else if (option < 1000 * 60) TimeTextUtil.seconds(option / 1000, LocalContext.current) + else TimeTextUtil.time(option, LocalContext.current) + + Row( + Modifier + .fillMaxWidth() + .clickable(onClickLabel = label, onClick = onClick), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton(selected = option == overlay.currentValue, onClick = onClick) + Text(label) + } + } + + Text(stringResource(R.string.purchase_required_info_local_mode_free)) + } + }, + confirmButton = {}, + dismissButton = { + TextButton(onClick = overlay.cancel) { + Text(stringResource(R.string.generic_cancel)) + } + }, + onDismissRequest = overlay.cancel + ) + null -> Unit + } +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/model/MainModel.kt b/app/src/main/java/io/timelimit/android/ui/model/MainModel.kt index e3a515e..665ac03 100644 --- a/app/src/main/java/io/timelimit/android/ui/model/MainModel.kt +++ b/app/src/main/java/io/timelimit/android/ui/model/MainModel.kt @@ -110,7 +110,7 @@ class MainModel(application: Application): AndroidViewModel(application) { Case.simple<_, _, State.LaunchState> { LaunchHandling.processLaunchState(state, logic) }, Case.simple<_, _, State.Overview> { OverviewHandling.processState(logic, scope, activityCommandInternal, authenticationModelApi, state) }, Case.simple<_, _, State.ManageChild> { state -> ManageChildHandling.processState(logic, state, updateMethod(::updateState)) }, - Case.simple<_, _, State.ManageDevice> { state -> ManageDeviceHandling.processState(logic, state, updateMethod(::updateState)) }, + Case.simple<_, _, State.ManageDevice> { state -> ManageDeviceHandling.processState(logic, activityCommandInternal, authenticationModelApi, state, updateMethod(::updateState)) }, Case.simple<_, _, State.DiagnoseScreen.DeviceOwner> { DeviceOwnerHandling.processState(logic, scope, authenticationModelApi, state) }, Case.simple<_, _, FragmentState> { state -> state.transform { diff --git a/app/src/main/java/io/timelimit/android/ui/model/Screen.kt b/app/src/main/java/io/timelimit/android/ui/model/Screen.kt index 7982761..aea2526 100644 --- a/app/src/main/java/io/timelimit/android/ui/model/Screen.kt +++ b/app/src/main/java/io/timelimit/android/ui/model/Screen.kt @@ -21,6 +21,7 @@ import androidx.compose.material.icons.outlined.Info import io.timelimit.android.R import io.timelimit.android.ui.model.diagnose.DeviceOwnerHandling import io.timelimit.android.ui.model.main.OverviewHandling +import io.timelimit.android.ui.model.managedevice.ManageDeviceUser sealed class Screen( val state: State, @@ -163,14 +164,14 @@ sealed class Screen( override val title = Title.Plain(deviceName) } - class ManageDeviceUser( + class ManageDeviceUserScreen( state: State, - toolbarIcons: List, - toolbarOptions: List, - fragment: FragmentState, - containerId: Int, - override val backStack: List - ): FragmentScreen(state, toolbarIcons, toolbarOptions, fragment, containerId), ScreenWithBackStack, ScreenWithTitle { + override val backStack: List, + override val snackbarHostState: SnackbarHostState, + val items: List, + val actions: ManageDeviceUser.Actions, + val overlay: ManageDeviceUser.Overlay? + ): Screen(state), ScreenWithBackStack, ScreenWithTitle, ScreenWithAuthenticationFab, ScreenWithSnackbar { override val title = Title.StringResource(R.string.manage_device_card_user_title) } diff --git a/app/src/main/java/io/timelimit/android/ui/model/State.kt b/app/src/main/java/io/timelimit/android/ui/model/State.kt index a0e1a1f9..dfe56d4 100644 --- a/app/src/main/java/io/timelimit/android/ui/model/State.kt +++ b/app/src/main/java/io/timelimit/android/ui/model/State.kt @@ -37,8 +37,6 @@ import io.timelimit.android.ui.manage.device.manage.feature.ManageDeviceFeatures import io.timelimit.android.ui.manage.device.manage.feature.ManageDeviceFeaturesFragmentArgs import io.timelimit.android.ui.manage.device.manage.permission.ManageDevicePermissionsFragment import io.timelimit.android.ui.manage.device.manage.permission.ManageDevicePermissionsFragmentArgs -import io.timelimit.android.ui.manage.device.manage.user.ManageDeviceUserFragment -import io.timelimit.android.ui.manage.device.manage.user.ManageDeviceUserFragmentArgs import io.timelimit.android.ui.manage.parent.ManageParentFragment import io.timelimit.android.ui.manage.parent.ManageParentFragmentArgs import io.timelimit.android.ui.manage.parent.link.LinkParentMailFragment @@ -239,31 +237,24 @@ sealed class State (val previous: State?): Serializable { fragmentClass ) - class User( - previousMain: Main, - deviceId: String - ): Sub(previousMain, ManageDeviceUserFragment::class.java) { - @Transient - override val arguments: Bundle = ManageDeviceUserFragmentArgs(deviceId).toBundle() - } - class Permissions( + data class User( val previousMain: Main, - deviceId: String - ): Sub(previousMain, ManageDevicePermissionsFragment::class.java) { + val overlay: Overlay? = null + ): Sub(previousMain, Fragment::class.java) { + sealed class Overlay: Serializable { + data class EnableDefaultUserDialog(val userId: String): Overlay() + object AdjustDefaultUserTimeout: Overlay() + } + } + class Permissions(previousMain: Main): Sub(previousMain, ManageDevicePermissionsFragment::class.java) { @Transient override val arguments: Bundle = ManageDevicePermissionsFragmentArgs(deviceId).toBundle() } - class Features( - val previousMain: Main, - deviceId: String - ): Sub(previousMain, ManageDeviceFeaturesFragment::class.java) { + class Features(previousMain: Main): Sub(previousMain, ManageDeviceFeaturesFragment::class.java) { @Transient override val arguments: Bundle = ManageDeviceFeaturesFragmentArgs(deviceId).toBundle() } - class Advanced( - val previousMain: Main, - deviceId: String - ): Sub(previousMain, ManageDeviceAdvancedFragment::class.java) { + class Advanced(previousMain: Main): Sub(previousMain, ManageDeviceAdvancedFragment::class.java) { @Transient override val arguments: Bundle = ManageDeviceAdvancedFragmentArgs(deviceId).toBundle() } diff --git a/app/src/main/java/io/timelimit/android/ui/model/UpdateStateCommand.kt b/app/src/main/java/io/timelimit/android/ui/model/UpdateStateCommand.kt index cd94dbf..8967828 100644 --- a/app/src/main/java/io/timelimit/android/ui/model/UpdateStateCommand.kt +++ b/app/src/main/java/io/timelimit/android/ui/model/UpdateStateCommand.kt @@ -161,22 +161,22 @@ sealed class UpdateStateCommand { object ManageDevice { data class User(val childId: String): UpdateStateCommand() { override fun transform(state: State): State? = - if (state is State.ManageDevice.Main) State.ManageDevice.User(state, childId) + if (state is State.ManageDevice.Main) State.ManageDevice.User(state) else null } data class Permissions(val childId: String): UpdateStateCommand() { override fun transform(state: State): State? = - if (state is State.ManageDevice.Main) State.ManageDevice.Permissions(state, childId) + if (state is State.ManageDevice.Main) State.ManageDevice.Permissions(state) else null } data class Features(val childId: String): UpdateStateCommand() { override fun transform(state: State): State? = - if (state is State.ManageDevice.Main) State.ManageDevice.Features(state, childId) + if (state is State.ManageDevice.Main) State.ManageDevice.Features(state) else null } data class Advanced(val childId: String): UpdateStateCommand() { override fun transform(state: State): State? = - if (state is State.ManageDevice.Main) State.ManageDevice.Advanced(state, childId) + if (state is State.ManageDevice.Main) State.ManageDevice.Advanced(state) else null } object Leave: UpdateStateCommand() { diff --git a/app/src/main/java/io/timelimit/android/ui/model/flow/SplitFlow.kt b/app/src/main/java/io/timelimit/android/ui/model/flow/SplitFlow.kt index 7bc231d..678a681 100644 --- a/app/src/main/java/io/timelimit/android/ui/model/flow/SplitFlow.kt +++ b/app/src/main/java/io/timelimit/android/ui/model/flow/SplitFlow.kt @@ -24,13 +24,14 @@ import kotlinx.coroutines.launch class CaseScope( val scope: CoroutineScope, - val className: Class + val className: Class? ) { inline fun updateMethod( crossinline parent: ((SuperStateType) -> SuperStateType) -> Unit ): ((LocalStateType) -> SuperStateType) -> Unit = { request -> parent { oldState -> - if (scope.isActive && className.isInstance(oldState)) request(oldState as LocalStateType) + if (!scope.isActive) oldState + else if (className != null && className.isInstance(oldState)) request(oldState as LocalStateType) else oldState } } @@ -39,11 +40,16 @@ class CaseScope( } class Case( - val className: Class, + val className: Class?, val key: (T) -> Any?, val producer: CaseScope.(Flow, Any?) -> Flow ) { companion object { + fun nil(producer: CaseScope.(Flow) -> Flow) = Case( + className = null, + key = {} + ) { flow, _ -> producer(this as CaseScope, flow.map { Unit }) } + inline fun simple( crossinline producer: CaseScope.(Flow) -> Flow ) = Case( @@ -60,7 +66,7 @@ class Case( ) { flow, key -> producer(this as CaseScope, key as K, flow as Flow) } } - internal fun doesMatch(value: Any?): Boolean = className.isInstance(value) + internal fun doesMatch(value: Any?): Boolean = (className == null && value == null) || (className != null && className.isInstance(value)) } fun Flow.splitConflated(vararg cases: Case): Flow { @@ -78,7 +84,7 @@ fun Flow.splitConflated(vararg cases: Case): Flow { val key = case.key(value) val relayChannel = Channel(Channel.CONFLATED) val job = launch { - val scope = CaseScope(this, case.className as Class) + val scope = CaseScope(this, case.className as Class?) val inputFlow = flow { relayChannel.consumeEach { emit(it) } } case.producer(scope, inputFlow, key).collect { send(it) } diff --git a/app/src/main/java/io/timelimit/android/ui/model/managedevice/ManageDeviceHandling.kt b/app/src/main/java/io/timelimit/android/ui/model/managedevice/ManageDeviceHandling.kt index 5c6753c..c3c9ea2 100644 --- a/app/src/main/java/io/timelimit/android/ui/model/managedevice/ManageDeviceHandling.kt +++ b/app/src/main/java/io/timelimit/android/ui/model/managedevice/ManageDeviceHandling.kt @@ -18,17 +18,17 @@ package io.timelimit.android.ui.model.managedevice import io.timelimit.android.R import io.timelimit.android.data.model.Device import io.timelimit.android.logic.AppLogic -import io.timelimit.android.ui.model.BackStackItem -import io.timelimit.android.ui.model.Screen -import io.timelimit.android.ui.model.State -import io.timelimit.android.ui.model.Title +import io.timelimit.android.ui.model.* import io.timelimit.android.ui.model.flow.Case import io.timelimit.android.ui.model.flow.splitConflated +import kotlinx.coroutines.channels.SendChannel import kotlinx.coroutines.flow.* object ManageDeviceHandling { fun processState( logic: AppLogic, + activityCommand: SendChannel, + authentication: AuthenticationModelApi, state: Flow, updateState: ((State.ManageDevice) -> State) -> Unit ): Flow = state.splitConflated( @@ -60,6 +60,9 @@ object ManageDeviceHandling { }, Case.simple<_, _, State.ManageDevice.Sub> { processSubState( + logic, + activityCommand, + authentication, it, baseBackStackLive, share(foundDeviceLive), @@ -90,6 +93,9 @@ object ManageDeviceHandling { } private fun processSubState( + logic: AppLogic, + activityCommand: SendChannel, + authentication: AuthenticationModelApi, stateLive: Flow, parentBackStackLive: Flow>, deviceLive: SharedFlow, @@ -101,10 +107,15 @@ object ManageDeviceHandling { return stateLive.splitConflated( Case.simple<_, _, State.ManageDevice.User> { - processUserState( - it, + ManageDeviceUser.processUserState( + logic, + scope, + activityCommand, + authentication, + share(it), subBackStackLive, - deviceLive + deviceLive, + updateMethod(updateState) ) }, Case.simple<_, _, State.ManageDevice.Permissions> { @@ -131,21 +142,6 @@ object ManageDeviceHandling { ) } - private fun processUserState( - stateLive: Flow, - parentBackStackLive: Flow>, - deviceLive: Flow - ): Flow = combine(stateLive, deviceLive, parentBackStackLive) { state, device, backStack -> - Screen.ManageDeviceUser( - state, - state.toolbarIcons, - state.toolbarOptions, - state, - R.id.fragment_manage_device_user, - backStack - ) - } - private fun processPermissionsState( stateLive: Flow, parentBackStackLive: Flow>, diff --git a/app/src/main/java/io/timelimit/android/ui/model/managedevice/ManageDeviceUser.kt b/app/src/main/java/io/timelimit/android/ui/model/managedevice/ManageDeviceUser.kt new file mode 100644 index 0000000..09cfc74 --- /dev/null +++ b/app/src/main/java/io/timelimit/android/ui/model/managedevice/ManageDeviceUser.kt @@ -0,0 +1,293 @@ +/* + * TimeLimit Copyright 2019 - 2023 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.model.managedevice + +import androidx.compose.material.SnackbarHostState +import androidx.compose.material.SnackbarResult +import io.timelimit.android.R +import io.timelimit.android.data.model.Device +import io.timelimit.android.data.model.UserType +import io.timelimit.android.logic.AppLogic +import io.timelimit.android.sync.actions.SetDeviceDefaultUserAction +import io.timelimit.android.sync.actions.SetDeviceDefaultUserTimeoutAction +import io.timelimit.android.sync.actions.SetDeviceUserAction +import io.timelimit.android.sync.actions.SignOutAtDeviceAction +import io.timelimit.android.sync.actions.apply.ApplyActionUtil +import io.timelimit.android.ui.model.* +import io.timelimit.android.ui.model.flow.Case +import io.timelimit.android.ui.model.flow.splitConflated +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.SendChannel +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch + +object ManageDeviceUser { + data class UserItem( + val id: String, + val type: UserType, + val name: String, + val selected: Boolean, + val defaultUser: DefaultUser + ) { + sealed class DefaultUser { + object No: DefaultUser() + data class Yes(val timeout: Int): DefaultUser() + } + } + + data class Actions( + val select: (UserItem) -> Unit, + val makeDefaultUser: (UserItem) -> Unit, + val disableDefaultUser: () -> Unit, + val configureAutoSwitching: () -> Unit + ) + + sealed class Overlay { + data class EnableDefaultUser( + val userTitle: String, + val confirm: () -> Unit, + val cancel: () -> Unit + ): Overlay() + + data class ConfigureTimeout( + val currentValue: Int, + val confirm: (Int) -> Unit, + val cancel: () -> Unit + ): Overlay() + } + + fun processUserState( + logic: AppLogic, + scope: CoroutineScope, + activityCommand: SendChannel, + authentication: AuthenticationModelApi, + stateLive: SharedFlow, + parentBackStackLive: Flow>, + deviceLive: Flow, + updateState: ((State.ManageDevice.User) -> State) -> Unit + ): Flow { + val snackbar = SnackbarHostState() + + fun launch(action: suspend () -> Unit) { + scope.launch { + try { + action() + } catch (ex: Exception) { + snackbar.showSnackbar(logic.context.getString(R.string.error_general)) + } + } + } + + val actions = Actions( + select = { user -> launch { + if (user.selected) { + val result = snackbar.showSnackbar( + logic.context.getString(R.string.manage_device_user_already_selected_text), + logic.context.getString(R.string.manage_device_user_already_selected_action) + ) + + if (result == SnackbarResult.ActionPerformed) { + val userEntry = logic.database.user().getUserByIdFlow(user.id).first()!! + + updateState { state -> + when (userEntry.type) { + UserType.Child -> State.ManageChild.Main(state.previousOverview, user.id, false) + UserType.Parent -> State.ManageParent.Main(state.previousOverview, user.id) + } + } + } + } else if ( + user.defaultUser is UserItem.DefaultUser.Yes && + logic.fullVersion.shouldProvideFullVersionFunctions() && + deviceLive.first().id == logic.database.config().getOwnDeviceIdFlow().first() + ) { + ApplyActionUtil.applyAppLogicAction( + SignOutAtDeviceAction, + logic, + false + ) + } else { + val parent = authentication.authenticatedParentOnly.firstOrNull() + + if (parent != null) { + ApplyActionUtil.applyParentAction( + SetDeviceUserAction( + deviceId = deviceLive.first().id, + userId = user.id + ), + parent.authentication, + logic + ) + } else authentication.triggerAuthenticationScreen() + } + } }, + makeDefaultUser = { user -> launch { + if (authentication.doParentAuthentication() != null) { + updateState { it.copy(overlay = State.ManageDevice.User.Overlay.EnableDefaultUserDialog(user.id)) } + } + } }, + disableDefaultUser = { launch { + val parent = authentication.authenticatedParentOnly.firstOrNull() + + if (parent != null) { + val currentDefaultUser = deviceLive.first().defaultUser + + ApplyActionUtil.applyParentAction( + SetDeviceDefaultUserAction( + deviceId = deviceLive.first().id, + defaultUserId = "" + ), + parent.authentication, + logic + ) + + val result = snackbar.showSnackbar( + logic.context.getString(R.string.manage_device_user_disable_default_user_toast), + logic.context.getString(R.string.generic_undo) + ) + + if (result == SnackbarResult.ActionPerformed) { + ApplyActionUtil.applyParentAction( + SetDeviceDefaultUserAction( + deviceId = deviceLive.first().id, + defaultUserId = currentDefaultUser + ), + parent.authentication, + logic + ) + } + } else authentication.triggerAuthenticationScreen() + } }, + configureAutoSwitching = { launch { + if (authentication.doParentAuthentication() != null) { + updateState { it.copy(overlay = State.ManageDevice.User.Overlay.AdjustDefaultUserTimeout) } + } + } } + ) + + val usersLive = listUsers(logic, deviceLive) + + val overlayLive: Flow = stateLive + .map { it.overlay } + .splitConflated( + Case.withKey<_, _, State.ManageDevice.User.Overlay.EnableDefaultUserDialog, _>( + withKey = { it.userId }, + producer = { userId, _ -> + val userLive = logic.database.user().getUserByIdFlow(userId) + val closeOverlay = { updateState { + if (it.overlay is State.ManageDevice.User.Overlay.EnableDefaultUserDialog) it.copy(overlay = null) + else it + } } + + userLive.transformLatest { user -> + if (user == null) closeOverlay() + else emit(Overlay.EnableDefaultUser( + userTitle = user.name, + cancel = closeOverlay, + confirm = { launch { + closeOverlay() + + if (logic.fullVersion.shouldProvideFullVersionFunctions()) authentication.authenticatedParentOnly.first()!!.let { parent -> + ApplyActionUtil.applyParentAction( + SetDeviceDefaultUserAction( + deviceId = deviceLive.first().id, + defaultUserId = userId + ), + parent.authentication, + logic + ) + + snackbar.showSnackbar(logic.context.getString(R.string.manage_device_user_make_default_user_toast)) + } + else activityCommand.send(ActivityCommand.ShowMissingPremiumDialog) + } } + )) + } + } + ), + Case.simple<_, _, State.ManageDevice.User.Overlay.AdjustDefaultUserTimeout> { + val closeOverlay = { updateState { + if (it.overlay is State.ManageDevice.User.Overlay.AdjustDefaultUserTimeout) it.copy(overlay = null) + else it + } } + + deviceLive.transform { device -> + if (device.defaultUser == "") closeOverlay() + else emit(Overlay.ConfigureTimeout( + currentValue = device.defaultUserTimeout, + cancel = closeOverlay, + confirm = { newValue -> launch { + closeOverlay() + + if (logic.fullVersion.shouldProvideFullVersionFunctions()) authentication.authenticatedParentOnly.first()!!.let { parent -> + ApplyActionUtil.applyParentAction( + SetDeviceDefaultUserTimeoutAction( + deviceId = deviceLive.first().id, + timeout = newValue + ), + parent.authentication, + logic + ) + } + else activityCommand.send(ActivityCommand.ShowMissingPremiumDialog) + } } + )) + } + }, + Case.nil { flowOf(null) } + ) + + return combine(stateLive, parentBackStackLive, usersLive, overlayLive) { state, parentBackStack, users, overlay -> + Screen.ManageDeviceUserScreen( + state, + parentBackStack, + snackbar, + users, + actions, + overlay + ) + } + } + + private fun listUsers( + logic: AppLogic, + deviceLive: Flow + ): Flow> { + val usersLive = logic.database.user().getAllUsersFlow() + + return combine(usersLive, deviceLive) { users, device -> + users.map { user -> + val selected = device.currentUserId == user.id + val defaultUser = + if (device.defaultUser == user.id) UserItem.DefaultUser.Yes( + timeout = device.defaultUserTimeout + ) + else UserItem.DefaultUser.No + + UserItem( + id = user.id, + type = user.type, + name = user.name, + selected = selected, + defaultUser = defaultUser + ) + }.sortedBy { when (it.defaultUser) { + is UserItem.DefaultUser.Yes -> 0 + UserItem.DefaultUser.No -> 1 + } } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/manage_device_default_user.xml b/app/src/main/res/layout/manage_device_default_user.xml deleted file mode 100644 index 1f99cb5..0000000 --- a/app/src/main/res/layout/manage_device_default_user.xml +++ /dev/null @@ -1,138 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - -