Rebuild user selection screen

This commit is contained in:
Jonas Lochmann 2023-03-06 01:00:00 +01:00
parent 35ccf48507
commit ae02d2fdff
No known key found for this signature in database
GPG key ID: 8B8C9AEE10FA5B36
20 changed files with 557 additions and 868 deletions

View file

@ -285,7 +285,6 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
content = { paddingValues ->
ScreenMultiplexer(
screen = screen,
executeCommand = ::execute,
fragmentManager = supportFragmentManager,
fragmentIds = mainModel.fragmentIds,
modifier = Modifier

View file

@ -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<Int>,
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)
}
}

View file

@ -1,117 +0,0 @@
/*
* 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.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<List<User>>,
lifecycleOwner: LifecycleOwner,
device: LiveData<Device?>,
isThisDevice: LiveData<Boolean>,
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)
}
}
})
}
}

View file

@ -1,131 +0,0 @@
/*
* TimeLimit Copyright <C> 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 <https://www.gnu.org/licenses/>.
*/
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)
}

View file

@ -1,125 +0,0 @@
/*
* TimeLimit Copyright <C> 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 <https://www.gnu.org/licenses/>.
*/
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<Device?> 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)
}

View file

@ -1,179 +0,0 @@
/*
* TimeLimit Copyright <C> 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 <https://www.gnu.org/licenses/>.
*/
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<Device?> 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<Pair<String, String>>()
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<String?> = deviceEntry.map { "${getString(R.string.manage_device_card_user_title)} < ${it?.name} < ${getString(R.string.main_tab_overview)}" }
}
interface ManageDeviceUserFragmentHandlers {
fun showAuthenticationScreen()
}

View file

@ -0,0 +1,180 @@
/*
* TimeLimit Copyright <C> 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 <https://www.gnu.org/licenses/>.
*/
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<ManageDeviceUser.UserItem>,
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
}
}

View file

@ -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 {

View file

@ -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<Menu.Icon>,
toolbarOptions: List<Menu.Dropdown>,
fragment: FragmentState,
containerId: Int,
override val backStack: List<BackStackItem>
): FragmentScreen(state, toolbarIcons, toolbarOptions, fragment, containerId), ScreenWithBackStack, ScreenWithTitle {
override val backStack: List<BackStackItem>,
override val snackbarHostState: SnackbarHostState,
val items: List<ManageDeviceUser.UserItem>,
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)
}

View file

@ -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()
}

View file

@ -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() {

View file

@ -24,13 +24,14 @@ import kotlinx.coroutines.launch
class CaseScope<LocalStateType>(
val scope: CoroutineScope,
val className: Class<LocalStateType>
val className: Class<LocalStateType>?
) {
inline fun <SuperStateType, LocalStateType : SuperStateType> 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<LocalStateType>(
}
class Case<T, R>(
val className: Class<out Any>,
val className: Class<out Any>?,
val key: (T) -> Any?,
val producer: CaseScope<T>.(Flow<T>, Any?) -> Flow<R>
) {
companion object {
fun <T, R> nil(producer: CaseScope<Unit>.(Flow<Unit>) -> Flow<R>) = Case<T, R>(
className = null,
key = {}
) { flow, _ -> producer(this as CaseScope<Unit>, flow.map { Unit }) }
inline fun <T, R, reified C : Any> simple(
crossinline producer: CaseScope<C>.(Flow<C>) -> Flow<R>
) = Case<T, R>(
@ -60,7 +66,7 @@ class Case<T, R>(
) { flow, key -> producer(this as CaseScope<C>, key as K, flow as Flow<C>) }
}
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 <T, R> Flow<T>.splitConflated(vararg cases: Case<T, R>): Flow<R> {
@ -78,7 +84,7 @@ fun <T, R> Flow<T>.splitConflated(vararg cases: Case<T, R>): Flow<R> {
val key = case.key(value)
val relayChannel = Channel<T>(Channel.CONFLATED)
val job = launch {
val scope = CaseScope<T>(this, case.className as Class<T>)
val scope = CaseScope<T>(this, case.className as Class<T>?)
val inputFlow = flow { relayChannel.consumeEach { emit(it) } }
case.producer(scope, inputFlow, key).collect { send(it) }

View file

@ -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<ActivityCommand>,
authentication: AuthenticationModelApi,
state: Flow<State.ManageDevice>,
updateState: ((State.ManageDevice) -> State) -> Unit
): Flow<Screen> = 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<ActivityCommand>,
authentication: AuthenticationModelApi,
stateLive: Flow<State.ManageDevice.Sub>,
parentBackStackLive: Flow<List<BackStackItem>>,
deviceLive: SharedFlow<Device>,
@ -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<State.ManageDevice.User>,
parentBackStackLive: Flow<List<BackStackItem>>,
deviceLive: Flow<Device>
): Flow<Screen> = 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<State.ManageDevice.Permissions>,
parentBackStackLive: Flow<List<BackStackItem>>,

View file

@ -0,0 +1,293 @@
/*
* TimeLimit Copyright <C> 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 <https://www.gnu.org/licenses/>.
*/
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<ActivityCommand>,
authentication: AuthenticationModelApi,
stateLive: SharedFlow<State.ManageDevice.User>,
parentBackStackLive: Flow<List<BackStackItem>>,
deviceLive: Flow<Device>,
updateState: ((State.ManageDevice.User) -> State) -> Unit
): Flow<Screen> {
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<Overlay?> = 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<Device>
): Flow<List<UserItem>> {
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
} }
}
}
}

View file

@ -1,138 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="defaultUserTitle"
type="String" />
<variable
name="hasDefaultUser"
type="boolean" />
<variable
name="isCurrentDevice"
type="boolean" />
<variable
name="isAlreadyUsingDefaultUser"
type="boolean" />
<variable
name="defaultUserSwitchText"
type="String" />
<variable
name="isAutomaticallySwitchingToDefaultUserEnabled"
type="boolean" />
<import type="android.view.View" />
</data>
<androidx.cardview.widget.CardView
app:cardUseCompatPadding="true"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:padding="8dp"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
tools:ignore="UnusedAttribute"
android:drawableTint="?colorOnSurface"
android:id="@+id/title_view"
android:drawableEnd="@drawable/ic_info_outline_black_24dp"
android:background="?selectableItemBackground"
android:textAppearance="?android:textAppearanceLarge"
android:text="@string/manage_device_default_user_title"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:visibility="@{hasDefaultUser ? View.VISIBLE : View.GONE}"
android:textAppearance="?android:textAppearanceMedium"
tools:text="@string/manage_device_default_user_status"
android:text="@{@string/manage_device_default_user_status(defaultUserTitle)}"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
style="?materialButtonOutlinedStyle"
android:id="@+id/set_default_user_button"
tools:text="@string/manage_device_default_user_set_btn_set"
android:text="@{hasDefaultUser ? @string/manage_device_default_user_set_btn_change : @string/manage_device_default_user_set_btn_set}"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:id="@+id/switch_to_default_user_button"
android:enabled="@{hasDefaultUser &amp;&amp; isCurrentDevice &amp;&amp; (!isAlreadyUsingDefaultUser)}"
android:text="@string/manage_device_default_user_switch_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:visibility="@{hasDefaultUser ? View.GONE : View.VISIBLE}"
android:textAppearance="?android:textAppearanceSmall"
android:text="@string/manage_device_default_user_switch_reason_unset"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:visibility="@{isCurrentDevice ? View.GONE : View.VISIBLE}"
android:textAppearance="?android:textAppearanceSmall"
android:text="@string/manage_device_default_user_switch_reason_remote"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:visibility="@{isAlreadyUsingDefaultUser ? View.VISIBLE : View.GONE}"
android:textAppearance="?android:textAppearanceSmall"
android:text="@string/manage_device_default_user_switch_reason_already_done"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:visibility="@{hasDefaultUser ? View.VISIBLE : View.GONE}"
android:textAppearance="?android:textAppearanceMedium"
android:text="@{defaultUserSwitchText}"
tools:text="@string/manage_device_default_user_timeout_off"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
style="?materialButtonOutlinedStyle"
android:visibility="@{hasDefaultUser ? View.VISIBLE : View.GONE}"
android:id="@+id/configure_auto_logout_button"
android:text="@{isAutomaticallySwitchingToDefaultUserEnabled ? @string/manage_device_default_user_timeout_btn_change : @string/manage_device_default_user_timeout_btn_enable}"
tools:text="@string/manage_device_default_user_timeout_btn_enable"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:textAppearance="?android:textAppearanceSmall"
android:text="@string/purchase_required_info_local_mode_free"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</layout>

View file

@ -1,81 +0,0 @@
<!--
TimeLimit Copyright <C> 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 <https://www.gnu.org/licenses/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context="io.timelimit.android.ui.manage.device.manage.user.ManageDeviceUserFragment">
<data>
<variable
name="handlers"
type="io.timelimit.android.ui.manage.device.manage.user.ManageDeviceUserFragmentHandlers" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:id="@+id/scroll"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:padding="8dp"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.cardview.widget.CardView
app:cardUseCompatPadding="true"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:padding="8dp"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:textAppearance="?android:textAppearanceLarge"
android:text="@string/manage_device_current_user_title"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<RadioGroup
android:orientation="vertical"
android:id="@+id/user_list"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<include android:id="@+id/default_user"
layout="@layout/manage_device_default_user" />
</LinearLayout>
</ScrollView>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:onClick="@{() -> handlers.showAuthenticationScreen()}"
android:id="@+id/fab"
app:fabSize="normal"
android:src="@drawable/ic_lock_open_white_24dp"
android:layout_margin="16dp"
android:layout_gravity="end|bottom"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

View file

@ -102,15 +102,6 @@
android:name="deviceId"
app:argType="string" />
</fragment>
<fragment
android:id="@+id/manageDeviceUserFragment"
android:name="io.timelimit.android.ui.manage.device.manage.user.ManageDeviceUserFragment"
android:label="manage_device_user_fragment"
tools:layout="@layout/manage_device_user_fragment" >
<argument
android:name="deviceId"
app:argType="string" />
</fragment>
<fragment
android:id="@+id/manageDeviceAdvancedFragment"
android:name="io.timelimit.android.ui.manage.device.manage.advanced.ManageDeviceAdvancedFragment"

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
TimeLimit Copyright <C> 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.
@ -19,6 +19,7 @@
<string name="generic_help">Hilfe</string>
<string name="generic_undo">Rückgängig</string>
<string name="generic_save">Speichern</string>
<string name="generic_set">Festlegen</string>
<string name="generic_create">Erstellen</string>
<string name="generic_delete">Löschen</string>
<string name="generic_retry">Erneut versuchen</string>
@ -830,30 +831,30 @@
</string>
<string name="manage_device_activity_level_blocking_checkbox">Sperren auf Activity-Ebene aktivieren</string>
<string name="manage_device_user_is_default_user">Standardbenutzer</string>
<string name="manage_device_user_default_user_timeout">wird nach %s aktiviert</string>
<string name="manage_device_user_make_default_user">zum Standardbenutzer machen</string>
<string name="manage_device_user_make_default_user_toast">Standardbenutzer gesetzt</string>
<string name="manage_device_user_disable_default_user">Festlegung als Standardbenutzer aufheben</string>
<string name="manage_device_user_disable_default_user_toast">Standardbenutzer entfernt</string>
<string name="manage_device_user_already_selected_text">Dieser Benutzer ist bereits ausgewählt</string>
<string name="manage_device_user_already_selected_action">Benutzereinstellungen aufrufen</string>
<string name="manage_device_default_user_title">Standardbenutzer</string>
<string name="manage_device_default_user_confirm">Möchten Sie %s als Standardbenutzer festlegen?</string>
<string name="manage_device_default_user_info">Der Standardbenutzer ist ein Benutzer,
zu dem jeder an dem Gerät ohne Eingabe eines Passworts wechseln kann (um sich abzumelden).
Das ist sinnvoll bei Geräten, die von verschiedenen Benutzern genutzt werden können.
</string>
<string name="manage_device_default_user_status">Der Standardbenutzer ist %s</string>
<string name="manage_device_default_user_switch_btn">Zum Standardbenutzer wechseln</string>
<string name="manage_device_default_user_switch_reason_remote">Sie können nur am Gerät selbst zum Standardbenutzer wechseln</string>
<string name="manage_device_default_user_switch_reason_unset">Sie können nicht zum Standardbenutzer wechseln,
solange es keinen Standardbenutzer gibt</string>
<string name="manage_device_default_user_switch_reason_already_done">Sie können
nicht zum Standardbenutzer wechseln, wenn der Standardbenutzer bereits der aktuelle Benutzer ist
</string>
<string name="manage_device_default_user_set_btn_set">Standardbenutzer wählen</string>
<string name="manage_device_default_user_set_btn_change">Standardbenutzer ändern</string>
<string name="manage_device_default_user_selection_none">kein Standardbenutzer</string>
<string name="manage_device_default_user_timeout_off">Dieses Gerät wechselt nicht automatisch zum Standardbenutzer</string>
<string name="manage_device_default_user_timeout_on">Dieses Gerät wechselt automatisch zum Standardbenutzer, wenn es %s nicht benutzt wurde</string>
<string name="manage_device_default_user_timeout_btn_enable">automatischen Wechsel zum Standardbenutzer aktivieren</string>
<string name="manage_device_default_user_timeout_btn_change">automatischen Wechsel zum Standardbenutzer konfigurieren</string>
<string name="manage_device_default_user_timeout_dialog_title">automatisch zum Standardbenutzer wechseln</string>
<string name="manage_device_default_user_timeout_dialog_disable">nie</string>
<string name="manage_device_introduction_title">Was ist das?</string>
<string name="manage_device_introduction_text">
Das ist die Ansicht, um ein Gerät zu verwalten.
@ -910,7 +911,6 @@
<string name="manage_device_is_this_device">Das ist das Gerät, das Sie gerade vor sich haben</string>
<string name="manage_device_signing_key">Signaturschlüssel: %s</string>
<string name="manage_device_current_user_title">Benutzer des Geräts</string>
<string name="manage_device_current_user_none">Keine Angabe - keine Begrenzungen</string>
<string name="manage_device_feature_summary_none">Keine Funktionen aktiviert</string>

View file

@ -40,7 +40,6 @@
<item name="fragment_manage_category_advanced" type="id" />
<item name="fragment_manage_category_blocked_times" type="id" />
<item name="fragment_manage_device_main" type="id" />
<item name="fragment_manage_device_user" type="id" />
<item name="fragment_manage_device_permissions" type="id" />
<item name="fragment_manage_device_features" type="id" />
<item name="fragment_manage_device_advanced" type="id" />

View file

@ -22,6 +22,7 @@
<string name="generic_help">Help</string>
<string name="generic_undo">Undo</string>
<string name="generic_save">Save</string>
<string name="generic_set">Set</string>
<string name="generic_create">Create</string>
<string name="generic_delete">Delete</string>
<string name="generic_retry">Retry</string>
@ -882,21 +883,25 @@
</string>
<string name="manage_device_activity_level_blocking_checkbox">Enable activity level blocking</string>
<string name="manage_device_user_is_default_user">Default User</string>
<string name="manage_device_user_default_user_timeout">enabled after %s</string>
<string name="manage_device_user_make_default_user">make default user</string>
<string name="manage_device_user_make_default_user_toast">default user set</string>
<string name="manage_device_user_disable_default_user">disable default user role</string>
<string name="manage_device_user_disable_default_user_toast">default user removed</string>
<string name="manage_device_user_already_selected_text">This is the current user</string>
<string name="manage_device_user_already_selected_action">Open user settings</string>
<string name="manage_device_default_user_title">Default user</string>
<string name="manage_device_default_user_confirm">Would you like to set %s as default user?</string>
<string name="manage_device_default_user_info">The default user is a user
to which any user at the device can switch without a password (to sign out).
This is useful at devices which can be used by multiple different users.
</string>
<string name="manage_device_default_user_status">The current default user is %s</string>
<string name="manage_device_default_user_switch_btn">Switch to the default user</string>
<string name="manage_device_default_user_switch_reason_remote">You can only switch to the default user at the device itself</string>
<string name="manage_device_default_user_switch_reason_unset">You can only switch to the default user if a default user was configured</string>
<string name="manage_device_default_user_switch_reason_already_done">You can not switch to the default user if the default user is already the current user</string>
<string name="manage_device_default_user_set_btn_set">Set default user</string>
<string name="manage_device_default_user_set_btn_change">Change default user</string>
<string name="manage_device_default_user_selection_none">No default user</string>
<string name="manage_device_default_user_timeout_off">The device will not switch automatically to the default user</string>
<string name="manage_device_default_user_timeout_on">The device will switch automatically to the default user when it was not used for %s</string>
<string name="manage_device_default_user_timeout_btn_enable">enable switching automatically to default user</string>
<string name="manage_device_default_user_timeout_btn_change">configure switching automatically to default user</string>
<string name="manage_device_default_user_timeout_dialog_title">Automatically switch to the default user</string>
@ -955,7 +960,6 @@
<string name="manage_device_is_this_device">This is the device which is in front of you</string>
<string name="manage_device_signing_key">Signing Key: %s</string>
<string name="manage_device_current_user_title">User of the device</string>
<string name="manage_device_current_user_none">No selection - no limits</string>
<string name="manage_device_feature_summary_none">No features enabled</string>