mirror of
https://codeberg.org/timelimit/timelimit-android.git
synced 2025-10-03 09:49:25 +02:00
Add new parent setup UI
This commit is contained in:
parent
be894e876f
commit
7a3418ae1d
24 changed files with 1108 additions and 630 deletions
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -16,13 +16,14 @@
|
||||||
package io.timelimit.android.sync.network
|
package io.timelimit.android.sync.network
|
||||||
|
|
||||||
import android.util.JsonReader
|
import android.util.JsonReader
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
data class StatusOfMailAddressResponse(
|
data class StatusOfMailAddressResponse(
|
||||||
val mail: String,
|
val mail: String,
|
||||||
val status: StatusOfMailAddress,
|
val status: StatusOfMailAddress,
|
||||||
val canCreateFamily: Boolean,
|
val canCreateFamily: Boolean,
|
||||||
val alwaysPro: Boolean
|
val alwaysPro: Boolean
|
||||||
) {
|
): Serializable {
|
||||||
companion object {
|
companion object {
|
||||||
fun parse(reader: JsonReader): StatusOfMailAddressResponse {
|
fun parse(reader: JsonReader): StatusOfMailAddressResponse {
|
||||||
var mail: String? = null
|
var mail: String? = null
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
package io.timelimit.android.ui
|
package io.timelimit.android.ui
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
@ -26,6 +27,7 @@ import android.provider.Settings
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
@ -123,6 +125,10 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
|
||||||
override var ignoreStop: Boolean = false
|
override var ignoreStop: Boolean = false
|
||||||
override val showPasswordRecovery: Boolean = true
|
override val showPasswordRecovery: Boolean = true
|
||||||
|
|
||||||
|
private val requestNotifyPermission = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
||||||
|
if (granted) mainModel.reportPermissionsChanged()
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalAnimationApi::class)
|
@OptIn(ExperimentalAnimationApi::class)
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
@ -162,6 +168,7 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
message.errorHandler()
|
message.errorHandler()
|
||||||
}
|
}
|
||||||
|
ActivityCommand.RequestNotifyPermission -> requestNotifyPermission.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import io.timelimit.android.ui.account.DeleteRegistrationScreen
|
import io.timelimit.android.ui.account.DeleteRegistrationScreen
|
||||||
|
import io.timelimit.android.ui.authentication.AuthenticateByMailScreen
|
||||||
import io.timelimit.android.ui.diagnose.deviceowner.DeviceOwnerScreen
|
import io.timelimit.android.ui.diagnose.deviceowner.DeviceOwnerScreen
|
||||||
import io.timelimit.android.ui.manage.category.blocked_times.BlockedTimesScreen
|
import io.timelimit.android.ui.manage.category.blocked_times.BlockedTimesScreen
|
||||||
import io.timelimit.android.ui.manage.child.usagehistory.UsageHistoryScreen
|
import io.timelimit.android.ui.manage.child.usagehistory.UsageHistoryScreen
|
||||||
|
@ -28,6 +29,11 @@ import io.timelimit.android.ui.model.Screen
|
||||||
import io.timelimit.android.ui.overview.overview.OverviewScreen
|
import io.timelimit.android.ui.overview.overview.OverviewScreen
|
||||||
import io.timelimit.android.ui.setup.selectmode.SelectConnectedModeScreen
|
import io.timelimit.android.ui.setup.selectmode.SelectConnectedModeScreen
|
||||||
import io.timelimit.android.ui.setup.SetupDevicePermissionsScreen
|
import io.timelimit.android.ui.setup.SetupDevicePermissionsScreen
|
||||||
|
import io.timelimit.android.ui.setup.parent.ConfirmNewParentAccount
|
||||||
|
import io.timelimit.android.ui.setup.parent.ParentBaseConfiguration
|
||||||
|
import io.timelimit.android.ui.setup.parent.ParentSetupConsent
|
||||||
|
import io.timelimit.android.ui.setup.parent.SignInWrongMailAddress
|
||||||
|
import io.timelimit.android.ui.setup.parent.SignupBlockedScreen
|
||||||
import io.timelimit.android.ui.setup.privacy.SetupConnectedModePrivacyScreen
|
import io.timelimit.android.ui.setup.privacy.SetupConnectedModePrivacyScreen
|
||||||
import io.timelimit.android.ui.setup.selectmode.SelectModeScreen
|
import io.timelimit.android.ui.setup.selectmode.SelectModeScreen
|
||||||
|
|
||||||
|
@ -52,5 +58,11 @@ fun ScreenMultiplexer(
|
||||||
is Screen.DeleteRegistration -> DeleteRegistrationScreen(screen.content, modifier)
|
is Screen.DeleteRegistration -> DeleteRegistrationScreen(screen.content, modifier)
|
||||||
is Screen.ManageBlockedTimes -> BlockedTimesScreen(screen.content, screen.intro, modifier)
|
is Screen.ManageBlockedTimes -> BlockedTimesScreen(screen.content, screen.intro, modifier)
|
||||||
is Screen.ChildUsageHistory -> UsageHistoryScreen(screen.content, modifier)
|
is Screen.ChildUsageHistory -> UsageHistoryScreen(screen.content, modifier)
|
||||||
|
is Screen.SetupParentMailAuthentication -> AuthenticateByMailScreen(screen.content, modifier)
|
||||||
|
is Screen.SignupBlocked -> SignupBlockedScreen(modifier)
|
||||||
|
is Screen.SignInWrongMailAddress -> SignInWrongMailAddress(modifier)
|
||||||
|
is Screen.ConfirmNewParentAccount -> ConfirmNewParentAccount(confirm = screen.confirm, reject = screen.reject, modifier = modifier)
|
||||||
|
is Screen.ParentBaseConfiguration -> ParentBaseConfiguration(content = screen.content, modifier = modifier)
|
||||||
|
is Screen.ParentSetupConsent -> ParentSetupConsent(content = screen.content, errorDialog = screen.errorDialog, modifier = modifier)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -24,4 +24,5 @@ sealed class ActivityCommand {
|
||||||
object ShowMissingPremiumDialog: ActivityCommand()
|
object ShowMissingPremiumDialog: ActivityCommand()
|
||||||
class LaunchSystemSettings(val permission: SystemPermission): ActivityCommand()
|
class LaunchSystemSettings(val permission: SystemPermission): ActivityCommand()
|
||||||
class TriggerUninstall(val packageName: String, val errorHandler: () -> Unit): ActivityCommand()
|
class TriggerUninstall(val packageName: String, val errorHandler: () -> Unit): ActivityCommand()
|
||||||
|
object RequestNotifyPermission: ActivityCommand()
|
||||||
}
|
}
|
|
@ -63,6 +63,7 @@ class MainModel(application: Application): AndroidViewModel(application) {
|
||||||
|
|
||||||
private val activityCommandInternal = Channel<ActivityCommand>()
|
private val activityCommandInternal = Channel<ActivityCommand>()
|
||||||
private val authenticationScreenClosed = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
|
private val authenticationScreenClosed = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
|
||||||
|
private val permissionsChanged = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
|
||||||
|
|
||||||
private val authenticationModelApi = object: AuthenticationModelApi {
|
private val authenticationModelApi = object: AuthenticationModelApi {
|
||||||
override val authenticatedParentOnly: Flow<AuthenticationModelApi.Parent?> =
|
override val authenticatedParentOnly: Flow<AuthenticationModelApi.Parent?> =
|
||||||
|
@ -114,7 +115,7 @@ class MainModel(application: Application): AndroidViewModel(application) {
|
||||||
Case.simple<_, _, State.ManageChild> { state -> ManageChildHandling.processState(logic, activityCommandInternal, authenticationModelApi, state, updateMethod(::updateState)) },
|
Case.simple<_, _, State.ManageChild> { state -> ManageChildHandling.processState(logic, activityCommandInternal, authenticationModelApi, state, updateMethod(::updateState)) },
|
||||||
Case.simple<_, _, State.ManageDevice> { state -> ManageDeviceHandling.processState(logic, activityCommandInternal, authenticationModelApi, 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<_, _, State.DiagnoseScreen.DeviceOwner> { DeviceOwnerHandling.processState(logic, scope, authenticationModelApi, state) },
|
||||||
Case.simple<_, _, State.Setup> { state -> SetupHandling.handle(logic, activityCommandInternal, state, updateMethod(::updateState)) },
|
Case.simple<_, _, State.Setup> { state -> SetupHandling.handle(logic, activityCommandInternal, permissionsChanged, state, updateMethod(::updateState)) },
|
||||||
Case.simple<_, _, State.DeleteAccount> { AccountDeletion.handle(logic, scope, share(it), updateMethod(::updateState)) },
|
Case.simple<_, _, State.DeleteAccount> { AccountDeletion.handle(logic, scope, share(it), updateMethod(::updateState)) },
|
||||||
Case.simple<_, _, FragmentState> { state ->
|
Case.simple<_, _, FragmentState> { state ->
|
||||||
state.transform {
|
state.transform {
|
||||||
|
@ -141,5 +142,9 @@ class MainModel(application: Application): AndroidViewModel(application) {
|
||||||
authenticationScreenClosed.tryEmit(Unit)
|
authenticationScreenClosed.tryEmit(Unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun reportPermissionsChanged() {
|
||||||
|
permissionsChanged.tryEmit(Unit)
|
||||||
|
}
|
||||||
|
|
||||||
private fun updateState(method: (State) -> State): Unit = state.update(method)
|
private fun updateState(method: (State) -> State): Unit = state.update(method)
|
||||||
}
|
}
|
|
@ -24,10 +24,12 @@ import io.timelimit.android.ui.manage.device.manage.permission.PermissionScreenC
|
||||||
import io.timelimit.android.ui.model.account.AccountDeletion
|
import io.timelimit.android.ui.model.account.AccountDeletion
|
||||||
import io.timelimit.android.ui.model.diagnose.DeviceOwnerHandling
|
import io.timelimit.android.ui.model.diagnose.DeviceOwnerHandling
|
||||||
import io.timelimit.android.ui.model.intro.IntroHandling
|
import io.timelimit.android.ui.model.intro.IntroHandling
|
||||||
|
import io.timelimit.android.ui.model.mailauthentication.MailAuthentication
|
||||||
import io.timelimit.android.ui.model.main.OverviewHandling
|
import io.timelimit.android.ui.model.main.OverviewHandling
|
||||||
import io.timelimit.android.ui.model.managechild.ManageCategoryBlockedTimes
|
import io.timelimit.android.ui.model.managechild.ManageCategoryBlockedTimes
|
||||||
import io.timelimit.android.ui.model.managechild.ManageChildUsageHistory
|
import io.timelimit.android.ui.model.managechild.ManageChildUsageHistory
|
||||||
import io.timelimit.android.ui.model.managedevice.ManageDeviceUser
|
import io.timelimit.android.ui.model.managedevice.ManageDeviceUser
|
||||||
|
import io.timelimit.android.ui.model.setup.SetupParentHandling
|
||||||
|
|
||||||
sealed class Screen(
|
sealed class Screen(
|
||||||
val state: State,
|
val state: State,
|
||||||
|
@ -270,6 +272,37 @@ sealed class Screen(
|
||||||
): Screen(state), ScreenWithSnackbar, ScreenWithTitle {
|
): Screen(state), ScreenWithSnackbar, ScreenWithTitle {
|
||||||
override val title: Title = Title.StringResource(R.string.account_deletion_title)
|
override val title: Title = Title.StringResource(R.string.account_deletion_title)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SetupParentMailAuthentication(
|
||||||
|
state: State.Setup.ParentMailAuthentication,
|
||||||
|
val content: MailAuthentication.Screen,
|
||||||
|
override val snackbarHostState: SnackbarHostState
|
||||||
|
): Screen(state), ScreenWithSnackbar
|
||||||
|
|
||||||
|
class SignupBlocked(state: State.Setup.SignUpBlocked): Screen(state)
|
||||||
|
|
||||||
|
class SignInWrongMailAddress(state: State.Setup.SignInWrongMailAddress): Screen(state)
|
||||||
|
class ConfirmNewParentAccount(
|
||||||
|
state: State.Setup.ConfirmNewParentAccount,
|
||||||
|
val reject: () -> Unit,
|
||||||
|
val confirm: () -> Unit
|
||||||
|
): Screen(state)
|
||||||
|
|
||||||
|
class ParentBaseConfiguration(
|
||||||
|
state: State.Setup.ParentBaseConfiguration,
|
||||||
|
val content: SetupParentHandling.ParentBaseConfiguration
|
||||||
|
): Screen(state), ScreenWithTitle {
|
||||||
|
override val title: Title get() =
|
||||||
|
if (content.newUserDetails != null) Title.StringResource(R.string.setup_parent_mode_create_family)
|
||||||
|
else Title.StringResource(R.string.setup_parent_mode_add_device)
|
||||||
|
}
|
||||||
|
|
||||||
|
class ParentSetupConsent(
|
||||||
|
state: State.Setup.ParentConsent,
|
||||||
|
val content: SetupParentHandling.ParentSetupConsent,
|
||||||
|
override val snackbarHostState: SnackbarHostState,
|
||||||
|
val errorDialog: Pair<String, () -> Unit>?
|
||||||
|
): Screen(state), ScreenWithSnackbar
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ScreenWithAuthenticationFab
|
interface ScreenWithAuthenticationFab
|
||||||
|
|
|
@ -22,6 +22,8 @@ import androidx.compose.material.icons.filled.Phone
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
import io.timelimit.android.integration.platform.SystemPermission
|
import io.timelimit.android.integration.platform.SystemPermission
|
||||||
|
import io.timelimit.android.sync.network.StatusOfMailAddress
|
||||||
|
import io.timelimit.android.sync.network.StatusOfMailAddressResponse
|
||||||
import io.timelimit.android.ui.contacts.ContactsFragment
|
import io.timelimit.android.ui.contacts.ContactsFragment
|
||||||
import io.timelimit.android.ui.diagnose.*
|
import io.timelimit.android.ui.diagnose.*
|
||||||
import io.timelimit.android.ui.diagnose.exitreason.DiagnoseExitReasonFragment
|
import io.timelimit.android.ui.diagnose.exitreason.DiagnoseExitReasonFragment
|
||||||
|
@ -48,9 +50,11 @@ import io.timelimit.android.ui.manage.parent.u2fkey.ManageParentU2FKeyFragment
|
||||||
import io.timelimit.android.ui.manage.parent.u2fkey.ManageParentU2FKeyFragmentArgs
|
import io.timelimit.android.ui.manage.parent.u2fkey.ManageParentU2FKeyFragmentArgs
|
||||||
import io.timelimit.android.ui.model.account.AccountDeletion
|
import io.timelimit.android.ui.model.account.AccountDeletion
|
||||||
import io.timelimit.android.ui.model.diagnose.DeviceOwnerHandling
|
import io.timelimit.android.ui.model.diagnose.DeviceOwnerHandling
|
||||||
|
import io.timelimit.android.ui.model.mailauthentication.MailAuthentication
|
||||||
import io.timelimit.android.ui.model.main.OverviewHandling
|
import io.timelimit.android.ui.model.main.OverviewHandling
|
||||||
import io.timelimit.android.ui.model.managechild.ManageCategoryBlockedTimes
|
import io.timelimit.android.ui.model.managechild.ManageCategoryBlockedTimes
|
||||||
import io.timelimit.android.ui.model.managechild.ManageChildUsageHistory
|
import io.timelimit.android.ui.model.managechild.ManageChildUsageHistory
|
||||||
|
import io.timelimit.android.ui.model.setup.SetupParentHandling
|
||||||
import io.timelimit.android.ui.overview.uninstall.UninstallFragment
|
import io.timelimit.android.ui.overview.uninstall.UninstallFragment
|
||||||
import io.timelimit.android.ui.parentmode.ParentModeFragment
|
import io.timelimit.android.ui.parentmode.ParentModeFragment
|
||||||
import io.timelimit.android.ui.payment.PurchaseFragment
|
import io.timelimit.android.ui.payment.PurchaseFragment
|
||||||
|
@ -58,8 +62,8 @@ import io.timelimit.android.ui.payment.StayAwesomeFragment
|
||||||
import io.timelimit.android.ui.setup.*
|
import io.timelimit.android.ui.setup.*
|
||||||
import io.timelimit.android.ui.setup.child.SetupRemoteChildFragment
|
import io.timelimit.android.ui.setup.child.SetupRemoteChildFragment
|
||||||
import io.timelimit.android.ui.setup.device.SetupDeviceFragment
|
import io.timelimit.android.ui.setup.device.SetupDeviceFragment
|
||||||
import io.timelimit.android.ui.setup.parent.SetupParentModeFragment
|
|
||||||
import io.timelimit.android.ui.user.create.AddUserFragment
|
import io.timelimit.android.ui.user.create.AddUserFragment
|
||||||
|
import io.timelimit.android.ui.view.NotifyPermissionCard
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
sealed class State (val previous: State?): Serializable {
|
sealed class State (val previous: State?): Serializable {
|
||||||
|
@ -270,7 +274,42 @@ sealed class State (val previous: State?): Serializable {
|
||||||
class ConnectedPrivacy(previousSelectMode: SelectMode): Setup(previousSelectMode)
|
class ConnectedPrivacy(previousSelectMode: SelectMode): Setup(previousSelectMode)
|
||||||
class SelectConnectedMode(previousConnectedPrivacy: ConnectedPrivacy): Setup(previousConnectedPrivacy)
|
class SelectConnectedMode(previousConnectedPrivacy: ConnectedPrivacy): Setup(previousConnectedPrivacy)
|
||||||
class RemoteChild(previous: SelectConnectedMode): FragmentStateLegacy(previous = previous, fragmentClass = SetupRemoteChildFragment::class.java)
|
class RemoteChild(previous: SelectConnectedMode): FragmentStateLegacy(previous = previous, fragmentClass = SetupRemoteChildFragment::class.java)
|
||||||
class ParentMode(previous: SelectConnectedMode): FragmentStateLegacy(previous = previous, fragmentClass = SetupParentModeFragment::class.java)
|
sealed class ParentModeSetup(previous: State): Setup(previous)
|
||||||
|
data class ParentMailAuthentication(
|
||||||
|
val previousSelectConnectedMode: SelectConnectedMode,
|
||||||
|
val content: MailAuthentication.State = MailAuthentication.State.initial
|
||||||
|
): ParentModeSetup(previous = previousSelectConnectedMode)
|
||||||
|
class SignUpBlocked(previous: ParentMailAuthentication): ParentModeSetup(previous)
|
||||||
|
class SignInWrongMailAddress(previous: ConfirmNewParentAccount): ParentModeSetup(previous)
|
||||||
|
data class ConfirmNewParentAccount(
|
||||||
|
val previousParentMailAuthentication: ParentMailAuthentication,
|
||||||
|
val mailAuthToken: String,
|
||||||
|
val mailStatus: StatusOfMailAddressResponse
|
||||||
|
): ParentModeSetup(previous = previousParentMailAuthentication)
|
||||||
|
data class ParentBaseConfiguration(
|
||||||
|
val previousState: State,
|
||||||
|
val previousParentMailAuthentication: ParentMailAuthentication,
|
||||||
|
val mailAuthToken: String,
|
||||||
|
val mailStatus: StatusOfMailAddressResponse,
|
||||||
|
val deviceName: String,
|
||||||
|
val newUser: SetupParentHandling.NewUserDetails?
|
||||||
|
): ParentModeSetup(previousState) {
|
||||||
|
init {
|
||||||
|
if ((newUser != null) != (mailStatus.status == StatusOfMailAddress.MailAddressWithoutFamily))
|
||||||
|
throw IllegalStateException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data class ParentConsent(
|
||||||
|
val baseConfig: ParentBaseConfiguration,
|
||||||
|
val backgroundSync: Boolean,
|
||||||
|
val notificationAccess: NotifyPermissionCard.Status,
|
||||||
|
val enableUpdates: Boolean,
|
||||||
|
val error: String?
|
||||||
|
): ParentModeSetup(baseConfig) {
|
||||||
|
init {
|
||||||
|
if (baseConfig.newUser != null && !baseConfig.newUser.ready) throw IllegalStateException()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
class ParentMode: FragmentStateLegacy(previous = null, fragmentClass = ParentModeFragment::class.java)
|
class ParentMode: FragmentStateLegacy(previous = null, fragmentClass = ParentModeFragment::class.java)
|
||||||
object Purchase {
|
object Purchase {
|
||||||
|
|
|
@ -35,6 +35,10 @@ import java.io.Serializable
|
||||||
|
|
||||||
object MailAuthentication {
|
object MailAuthentication {
|
||||||
sealed class State: java.io.Serializable {
|
sealed class State: java.io.Serializable {
|
||||||
|
companion object {
|
||||||
|
val initial = State.EnterMailAddress()
|
||||||
|
}
|
||||||
|
|
||||||
abstract val error: ErrorDialog?
|
abstract val error: ErrorDialog?
|
||||||
abstract fun withError(error: ErrorDialog?): State
|
abstract fun withError(error: ErrorDialog?): State
|
||||||
|
|
||||||
|
|
|
@ -23,11 +23,13 @@ import io.timelimit.android.ui.model.flow.Case
|
||||||
import io.timelimit.android.ui.model.flow.splitConflated
|
import io.timelimit.android.ui.model.flow.splitConflated
|
||||||
import kotlinx.coroutines.channels.SendChannel
|
import kotlinx.coroutines.channels.SendChannel
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
|
||||||
object SetupHandling {
|
object SetupHandling {
|
||||||
fun handle(
|
fun handle(
|
||||||
logic: AppLogic,
|
logic: AppLogic,
|
||||||
activityCommand: SendChannel<ActivityCommand>,
|
activityCommand: SendChannel<ActivityCommand>,
|
||||||
|
permissionsChanged: SharedFlow<Unit>,
|
||||||
stateLive: Flow<State.Setup>,
|
stateLive: Flow<State.Setup>,
|
||||||
updateState: ((State.Setup) -> State) -> Unit
|
updateState: ((State.Setup) -> State) -> Unit
|
||||||
): Flow<Screen> = stateLive.splitConflated(
|
): Flow<Screen> = stateLive.splitConflated(
|
||||||
|
@ -35,5 +37,6 @@ object SetupHandling {
|
||||||
Case.simple<_, _, State.Setup.DevicePermissions> { SetupLocalModePermissions.handle(logic, scope, activityCommand, it, updateMethod(updateState)) },
|
Case.simple<_, _, State.Setup.DevicePermissions> { SetupLocalModePermissions.handle(logic, scope, activityCommand, it, updateMethod(updateState)) },
|
||||||
Case.simple<_, _, State.Setup.ConnectedPrivacy> { SetupConnectedModePrivacy.handle(logic, it, updateMethod(updateState)) },
|
Case.simple<_, _, State.Setup.ConnectedPrivacy> { SetupConnectedModePrivacy.handle(logic, it, updateMethod(updateState)) },
|
||||||
Case.simple<_, _, State.Setup.SelectConnectedMode> { SetupSelectConnectedMode.handle(it, updateMethod(updateState)) },
|
Case.simple<_, _, State.Setup.SelectConnectedMode> { SetupSelectConnectedMode.handle(it, updateMethod(updateState)) },
|
||||||
|
Case.simple<_, _, State.Setup.ParentModeSetup> { SetupParentHandling.handle(logic, activityCommand, permissionsChanged, it, updateMethod(updateState)) }
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -0,0 +1,411 @@
|
||||||
|
/*
|
||||||
|
* 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.setup
|
||||||
|
|
||||||
|
import androidx.compose.material.SnackbarDuration
|
||||||
|
import androidx.compose.material.SnackbarHostState
|
||||||
|
import androidx.compose.material.SnackbarResult
|
||||||
|
import io.timelimit.android.R
|
||||||
|
import io.timelimit.android.async.Threads
|
||||||
|
import io.timelimit.android.coroutines.executeAndWait
|
||||||
|
import io.timelimit.android.data.backup.DatabaseBackup
|
||||||
|
import io.timelimit.android.data.devicename.DeviceName
|
||||||
|
import io.timelimit.android.logic.AppLogic
|
||||||
|
import io.timelimit.android.sync.ApplyServerDataStatus
|
||||||
|
import io.timelimit.android.sync.network.NewDeviceInfo
|
||||||
|
import io.timelimit.android.sync.network.ParentPassword
|
||||||
|
import io.timelimit.android.sync.network.ServerDataStatus
|
||||||
|
import io.timelimit.android.sync.network.StatusOfMailAddress
|
||||||
|
import io.timelimit.android.sync.network.api.ConflictHttpError
|
||||||
|
import io.timelimit.android.sync.network.api.UnauthorizedHttpError
|
||||||
|
import io.timelimit.android.ui.diagnose.exception.ExceptionUtil
|
||||||
|
import io.timelimit.android.ui.model.ActivityCommand
|
||||||
|
import io.timelimit.android.ui.model.Screen
|
||||||
|
import io.timelimit.android.ui.model.State
|
||||||
|
import io.timelimit.android.ui.model.flow.Case
|
||||||
|
import io.timelimit.android.ui.model.flow.splitConflated
|
||||||
|
import io.timelimit.android.ui.model.mailauthentication.MailAuthentication
|
||||||
|
import io.timelimit.android.ui.setup.SetupUnprovisionedCheck
|
||||||
|
import io.timelimit.android.ui.view.NotifyPermissionCard
|
||||||
|
import io.timelimit.android.update.UpdateIntegration
|
||||||
|
import io.timelimit.android.update.UpdateUtil
|
||||||
|
import io.timelimit.android.work.PeriodicSyncInBackgroundWorker
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.channels.SendChannel
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.emitAll
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
object SetupParentHandling {
|
||||||
|
data class NewUserDetails(
|
||||||
|
val parentName: String,
|
||||||
|
val password: String,
|
||||||
|
val password2: String
|
||||||
|
): Serializable {
|
||||||
|
companion object {
|
||||||
|
val empty = NewUserDetails("", "", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
private val nameReady get() = parentName.isNotBlank()
|
||||||
|
private val passwordReady get() = password == password2 && password.isNotEmpty()
|
||||||
|
val ready get() = nameReady && passwordReady
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ParentBaseConfiguration(
|
||||||
|
val mail: String,
|
||||||
|
val showLimitedProInfo: Boolean,
|
||||||
|
val deviceName: String,
|
||||||
|
val newUserDetails: NewUserDetails?,
|
||||||
|
val actions: Actions
|
||||||
|
) {
|
||||||
|
data class Actions(
|
||||||
|
val newUserActions: NewUserActions?,
|
||||||
|
val updateDeviceName: (String) -> Unit,
|
||||||
|
val next: (() -> Unit)?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class NewUserActions(
|
||||||
|
val updateParentName: (String) -> Unit,
|
||||||
|
val updatePassword: (String) -> Unit,
|
||||||
|
val updatePassword2: (String) -> Unit,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ParentSetupConsent(
|
||||||
|
val backgroundSync: Boolean,
|
||||||
|
val notificationAccess: NotifyPermissionCard.Status,
|
||||||
|
val showEnableUpdates: Boolean,
|
||||||
|
val enableUpdates: Boolean,
|
||||||
|
val actions: Actions?
|
||||||
|
) {
|
||||||
|
data class Actions(
|
||||||
|
val updateBackgroundSync: (Boolean) -> Unit,
|
||||||
|
val updateNotificationAccess: (NotifyPermissionCard.Status) -> Unit,
|
||||||
|
val updateEnableUpdates: (Boolean) -> Unit,
|
||||||
|
val requestNotifyPermission: () -> Unit,
|
||||||
|
val skipNotifyPermission: () -> Unit,
|
||||||
|
val next: (() -> Unit)?
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handle(
|
||||||
|
logic: AppLogic,
|
||||||
|
activityCommand: SendChannel<ActivityCommand>,
|
||||||
|
permissionsChanged: SharedFlow<Unit>,
|
||||||
|
stateLive: Flow<State.Setup.ParentModeSetup>,
|
||||||
|
updateState: ((State.Setup.ParentModeSetup) -> State) -> Unit
|
||||||
|
): Flow<Screen> = stateLive.splitConflated(
|
||||||
|
Case.simple<_, _, State.Setup.ParentMailAuthentication> { handleMailAuthentication(logic, scope, share(it), updateMethod(updateState)) },
|
||||||
|
Case.simple<_, _, State.Setup.SignUpBlocked> { handleSignUpBlocked(it) },
|
||||||
|
Case.simple<_, _, State.Setup.SignInWrongMailAddress> { handleSignUpWrongMailAddress(it) },
|
||||||
|
Case.simple<_, _, State.Setup.ConfirmNewParentAccount> { handleConfirmNewParentAccount(logic, it, updateMethod(updateState)) },
|
||||||
|
Case.simple<_, _, State.Setup.ParentBaseConfiguration> { handleParentBaseConfiguration(it, updateMethod(updateState)) },
|
||||||
|
Case.simple<_, _, State.Setup.ParentConsent> { handleParentConsent(logic, scope, activityCommand, permissionsChanged, share(it), updateMethod(updateState)) },
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun handleMailAuthentication(
|
||||||
|
logic: AppLogic,
|
||||||
|
scope: CoroutineScope,
|
||||||
|
stateLive: SharedFlow<State.Setup.ParentMailAuthentication>,
|
||||||
|
updateState: ((State.Setup.ParentMailAuthentication) -> State) -> Unit
|
||||||
|
): Flow<Screen> = flow {
|
||||||
|
val snackbarHostState = SnackbarHostState()
|
||||||
|
|
||||||
|
val nestedLice = MailAuthentication.handle(
|
||||||
|
logic = logic,
|
||||||
|
scope = scope,
|
||||||
|
snackbarHostState = snackbarHostState,
|
||||||
|
stateLive = stateLive.map { it.content },
|
||||||
|
updateState = { modifier -> updateState { it.copy(content = modifier(it.content)) } },
|
||||||
|
processAuthToken = { token ->
|
||||||
|
val status = logic.serverLogic.getServerConfigCoroutine().api.getStatusByMailToken(token)
|
||||||
|
|
||||||
|
val deviceName = Threads.database.executeAndWait {
|
||||||
|
DeviceName.getDeviceNameSync(logic.context)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.status == StatusOfMailAddress.MailAddressWithFamily) updateState {
|
||||||
|
val prev = it.copy(content = MailAuthentication.State.initial)
|
||||||
|
|
||||||
|
State.Setup.ParentBaseConfiguration(
|
||||||
|
previousState = prev,
|
||||||
|
previousParentMailAuthentication = prev,
|
||||||
|
mailAuthToken = token,
|
||||||
|
mailStatus = status,
|
||||||
|
deviceName = deviceName,
|
||||||
|
newUser = null
|
||||||
|
)
|
||||||
|
} else if (status.canCreateFamily) updateState { State.Setup.ConfirmNewParentAccount(it, token, status) }
|
||||||
|
else updateState { State.Setup.SignUpBlocked(it.copy(content = MailAuthentication.State.initial)) }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
emitAll(combine(stateLive, nestedLice) { state, nested ->
|
||||||
|
Screen.SetupParentMailAuthentication(state, nested, snackbarHostState)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleSignUpBlocked(
|
||||||
|
stateLive: Flow<State.Setup.SignUpBlocked>
|
||||||
|
): Flow<Screen> = stateLive.map { Screen.SignupBlocked(it) }
|
||||||
|
|
||||||
|
private fun handleSignUpWrongMailAddress(
|
||||||
|
stateLive: Flow<State.Setup.SignInWrongMailAddress>
|
||||||
|
): Flow<Screen> = stateLive.map { Screen.SignInWrongMailAddress(it) }
|
||||||
|
|
||||||
|
private fun handleConfirmNewParentAccount(
|
||||||
|
logic: AppLogic,
|
||||||
|
stateLive: Flow<State.Setup.ConfirmNewParentAccount>,
|
||||||
|
updateState: ((State.Setup.ConfirmNewParentAccount) -> State) -> Unit
|
||||||
|
): Flow<Screen> = flow {
|
||||||
|
val deviceName = Threads.database.executeAndWait {
|
||||||
|
DeviceName.getDeviceNameSync(logic.context)
|
||||||
|
}
|
||||||
|
|
||||||
|
val confirm = { updateState {
|
||||||
|
State.Setup.ParentBaseConfiguration(
|
||||||
|
previousState = it,
|
||||||
|
previousParentMailAuthentication = it.previousParentMailAuthentication,
|
||||||
|
mailAuthToken = it.mailAuthToken,
|
||||||
|
mailStatus = it.mailStatus,
|
||||||
|
deviceName = deviceName,
|
||||||
|
newUser = NewUserDetails.empty
|
||||||
|
)
|
||||||
|
} }
|
||||||
|
|
||||||
|
val reject = { updateState {
|
||||||
|
State.Setup.SignInWrongMailAddress(it)
|
||||||
|
} }
|
||||||
|
|
||||||
|
emitAll(stateLive.map { Screen.ConfirmNewParentAccount(it, confirm = confirm, reject = reject) })
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleParentBaseConfiguration(
|
||||||
|
stateLive: Flow<State.Setup.ParentBaseConfiguration>,
|
||||||
|
updateState: ((State.Setup.ParentBaseConfiguration) -> State) -> Unit
|
||||||
|
): Flow<Screen> {
|
||||||
|
fun deviceReady(state: State.Setup.ParentBaseConfiguration) = state.deviceName.isNotBlank()
|
||||||
|
fun newUserReady(state: State.Setup.ParentBaseConfiguration) = state.newUser == null || state.newUser.ready
|
||||||
|
|
||||||
|
fun ready(state: State.Setup.ParentBaseConfiguration) = newUserReady(state) && deviceReady(state)
|
||||||
|
|
||||||
|
val next = { updateState {
|
||||||
|
if (ready(it)) State.Setup.ParentConsent(
|
||||||
|
it,
|
||||||
|
backgroundSync = false,
|
||||||
|
notificationAccess = NotifyPermissionCard.Status.Unknown,
|
||||||
|
enableUpdates = false,
|
||||||
|
error = null
|
||||||
|
)
|
||||||
|
else it
|
||||||
|
}}
|
||||||
|
|
||||||
|
return stateLive.map { state ->
|
||||||
|
val newUserDetails = if (state.newUser != null) NewUserDetails(
|
||||||
|
parentName = state.newUser.parentName,
|
||||||
|
password = state.newUser.password,
|
||||||
|
password2 = state.newUser.password2
|
||||||
|
) to ParentBaseConfiguration.NewUserActions(
|
||||||
|
updateParentName = { v -> updateState { s -> s.copy(newUser = s.newUser?.copy(parentName = v)) } },
|
||||||
|
updatePassword = { v -> updateState { s -> s.copy(newUser = s.newUser?.copy(password = v)) } },
|
||||||
|
updatePassword2 = { v -> updateState { s -> s.copy(newUser = s.newUser?.copy(password2 = v)) } }
|
||||||
|
) else null
|
||||||
|
|
||||||
|
val actions = ParentBaseConfiguration.Actions(
|
||||||
|
newUserActions = newUserDetails?.second,
|
||||||
|
updateDeviceName = { v -> updateState { it.copy(deviceName = v) } },
|
||||||
|
next = if (ready(state)) next else null
|
||||||
|
)
|
||||||
|
|
||||||
|
val content = ParentBaseConfiguration(
|
||||||
|
mail = state.mailStatus.mail,
|
||||||
|
showLimitedProInfo = !state.mailStatus.alwaysPro,
|
||||||
|
deviceName = state.deviceName,
|
||||||
|
newUserDetails = newUserDetails?.first,
|
||||||
|
actions = actions
|
||||||
|
)
|
||||||
|
|
||||||
|
Screen.ParentBaseConfiguration(
|
||||||
|
state = state,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleParentConsent(
|
||||||
|
logic: AppLogic,
|
||||||
|
scope: CoroutineScope,
|
||||||
|
activityCommand: SendChannel<ActivityCommand>,
|
||||||
|
permissionsChanged: SharedFlow<Unit>,
|
||||||
|
stateLive: SharedFlow<State.Setup.ParentConsent>,
|
||||||
|
updateState: ((State.Setup.ParentConsent) -> State) -> Unit
|
||||||
|
): Flow<Screen> = flow {
|
||||||
|
val isWorkingLive = MutableStateFlow(false)
|
||||||
|
val snackbarHostState = SnackbarHostState()
|
||||||
|
var lastError: Job? = null
|
||||||
|
|
||||||
|
val actions = ParentSetupConsent.Actions(
|
||||||
|
updateBackgroundSync = { v -> updateState { it.copy(backgroundSync = v) } },
|
||||||
|
updateNotificationAccess = { v -> updateState { it.copy(notificationAccess = v) } },
|
||||||
|
updateEnableUpdates = { v -> updateState { it.copy(enableUpdates = v) } },
|
||||||
|
requestNotifyPermission = {
|
||||||
|
activityCommand.trySend(ActivityCommand.RequestNotifyPermission)
|
||||||
|
},
|
||||||
|
skipNotifyPermission = { updateState { it.copy(notificationAccess = NotifyPermissionCard.Status.SkipGrant) } },
|
||||||
|
next = { scope.launch { if (isWorkingLive.compareAndSet(expect = false, update = true)) try {
|
||||||
|
val state = stateLive.first()
|
||||||
|
val database = logic.database
|
||||||
|
val serverConfig = logic.serverLogic.getServerConfigCoroutine()
|
||||||
|
val api = serverConfig.api
|
||||||
|
|
||||||
|
lastError?.cancel(); lastError = null
|
||||||
|
|
||||||
|
val deviceModelName = Threads.database.executeAndWait {
|
||||||
|
DeviceName.getDeviceNameSync(logic.context)
|
||||||
|
}
|
||||||
|
|
||||||
|
val mailToken = state.baseConfig.mailAuthToken
|
||||||
|
val parentDevice = NewDeviceInfo(model = deviceModelName)
|
||||||
|
val deviceName = state.baseConfig.deviceName
|
||||||
|
|
||||||
|
val result = if (state.baseConfig.newUser != null) {
|
||||||
|
val parentPassword = ParentPassword.createCoroutine(state.baseConfig.newUser.password, null)
|
||||||
|
val parentName = state.baseConfig.newUser.parentName
|
||||||
|
val timeZone = logic.timeApi.getSystemTimeZone().id
|
||||||
|
|
||||||
|
val registerResponse = api.createFamilyByMailToken(
|
||||||
|
mailToken = mailToken,
|
||||||
|
parentPassword = parentPassword,
|
||||||
|
parentDevice = parentDevice,
|
||||||
|
deviceName = deviceName,
|
||||||
|
parentName = parentName,
|
||||||
|
timeZone = timeZone
|
||||||
|
)
|
||||||
|
|
||||||
|
Result(
|
||||||
|
deviceAuthToken = registerResponse.deviceAuthToken,
|
||||||
|
ownDeviceId = registerResponse.ownDeviceId,
|
||||||
|
serverDataStatus = registerResponse.data
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val signInResponse = api.signInToFamilyByMailToken(
|
||||||
|
mailToken = mailToken,
|
||||||
|
parentDevice = parentDevice,
|
||||||
|
deviceName = deviceName
|
||||||
|
)
|
||||||
|
|
||||||
|
Result(
|
||||||
|
deviceAuthToken = signInResponse.deviceAuthToken,
|
||||||
|
ownDeviceId = signInResponse.ownDeviceId,
|
||||||
|
serverDataStatus = signInResponse.data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Threads.database.executeAndWait {
|
||||||
|
database.runInTransaction {
|
||||||
|
SetupUnprovisionedCheck.checkSync(logic.database)
|
||||||
|
|
||||||
|
database.deleteAllData()
|
||||||
|
|
||||||
|
database.config().setCustomServerUrlSync(serverConfig.customServerUrl)
|
||||||
|
database.config().setOwnDeviceIdSync(result.ownDeviceId)
|
||||||
|
database.config().setDeviceAuthTokenSync(result.deviceAuthToken)
|
||||||
|
database.config().setEnableBackgroundSync(state.backgroundSync)
|
||||||
|
|
||||||
|
ApplyServerDataStatus.applyServerDataStatusSync(result.serverDataStatus, logic.database, logic.platformIntegration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DatabaseBackup.with(logic.context).tryCreateDatabaseBackupAsync()
|
||||||
|
|
||||||
|
if (state.backgroundSync) {
|
||||||
|
PeriodicSyncInBackgroundWorker.enable(logic.context)
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateUtil.setEnableChecks(logic.context, state.enableUpdates)
|
||||||
|
|
||||||
|
updateState { State.LaunchState }
|
||||||
|
} catch (ex: ConflictHttpError) {
|
||||||
|
snackbarHostState.showSnackbar(logic.context.getString(R.string.error_server_rejected))
|
||||||
|
|
||||||
|
updateState { it.baseConfig.previousParentMailAuthentication }
|
||||||
|
} catch (ex: UnauthorizedHttpError) {
|
||||||
|
snackbarHostState.showSnackbar(logic.context.getString(R.string.error_server_rejected))
|
||||||
|
|
||||||
|
updateState { it.baseConfig.previousParentMailAuthentication }
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
lastError = launch {
|
||||||
|
val result = snackbarHostState.showSnackbar(
|
||||||
|
logic.context.getString(R.string.error_network),
|
||||||
|
logic.context.getString(R.string.generic_show_details),
|
||||||
|
SnackbarDuration.Long
|
||||||
|
)
|
||||||
|
|
||||||
|
if (result == SnackbarResult.ActionPerformed) updateState {
|
||||||
|
it.copy(error = ExceptionUtil.format(ex))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isWorkingLive.value = false
|
||||||
|
} } }
|
||||||
|
)
|
||||||
|
|
||||||
|
scope.launch { permissionsChanged.collect {
|
||||||
|
updateState {
|
||||||
|
it.copy(notificationAccess = NotifyPermissionCard.updateStatus(it.notificationAccess, logic.context))
|
||||||
|
}
|
||||||
|
} }
|
||||||
|
|
||||||
|
val showEnableUpdates = UpdateIntegration.doesSupportUpdates(logic.context)
|
||||||
|
|
||||||
|
emitAll(combine(stateLive, isWorkingLive) { state, isWorking ->
|
||||||
|
val notificationAccess = NotifyPermissionCard.updateStatus(state.notificationAccess, logic.context)
|
||||||
|
|
||||||
|
val content = ParentSetupConsent(
|
||||||
|
backgroundSync = state.backgroundSync,
|
||||||
|
notificationAccess = notificationAccess,
|
||||||
|
enableUpdates = state.enableUpdates,
|
||||||
|
showEnableUpdates = showEnableUpdates,
|
||||||
|
actions = if (isWorking) null else actions.copy(
|
||||||
|
next = if (NotifyPermissionCard.canProceed(notificationAccess)) actions.next else null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Screen.ParentSetupConsent(
|
||||||
|
state = state,
|
||||||
|
content = content,
|
||||||
|
snackbarHostState = snackbarHostState,
|
||||||
|
errorDialog = state.error?.let { error ->
|
||||||
|
Pair(error) { updateState { it.copy(error = null) } }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
internal data class Result(
|
||||||
|
val deviceAuthToken: String,
|
||||||
|
val ownDeviceId: String,
|
||||||
|
val serverDataStatus: ServerDataStatus
|
||||||
|
)
|
||||||
|
}
|
|
@ -28,7 +28,7 @@ object SetupSelectConnectedMode {
|
||||||
return stateLive.map { state ->
|
return stateLive.map { state ->
|
||||||
Screen.SetupSelectConnectedModeScreen(
|
Screen.SetupSelectConnectedModeScreen(
|
||||||
state = state,
|
state = state,
|
||||||
mailLogin = { updateState { State.Setup.ParentMode(it) } },
|
mailLogin = { updateState { State.Setup.ParentMailAuthentication(it) } },
|
||||||
codeLogin = { updateState { State.Setup.RemoteChild(it) } }
|
codeLogin = { updateState { State.Setup.RemoteChild(it) } }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
/*
|
||||||
|
* 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.setup.parent
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.Button
|
||||||
|
import androidx.compose.material.OutlinedButton
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.timelimit.android.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ConfirmNewParentAccount(
|
||||||
|
confirm: () -> Unit,
|
||||||
|
reject: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.setup_parent_confirm_new_account_text),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterHorizontally)
|
||||||
|
) {
|
||||||
|
OutlinedButton(onClick = confirm) {
|
||||||
|
Text(stringResource(R.string.generic_no))
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(onClick = reject) {
|
||||||
|
Text(stringResource(R.string.generic_yes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,105 @@
|
||||||
|
/*
|
||||||
|
* 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.setup.parent
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.Button
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.TextField
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.timelimit.android.R
|
||||||
|
import io.timelimit.android.ui.model.setup.SetupParentHandling
|
||||||
|
import io.timelimit.android.ui.view.EnterTextField
|
||||||
|
import io.timelimit.android.ui.view.SetPassword
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ParentBaseConfiguration(
|
||||||
|
content: SetupParentHandling.ParentBaseConfiguration,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
if (content.newUserDetails != null) {
|
||||||
|
run {
|
||||||
|
Text(stringResource(R.string.setup_parent_mode_explaination_user_name))
|
||||||
|
|
||||||
|
TextField(
|
||||||
|
value = content.newUserDetails.parentName,
|
||||||
|
onValueChange = content.actions.newUserActions?.updateParentName ?: {},
|
||||||
|
enabled = content.actions.newUserActions?.updateParentName != null,
|
||||||
|
label = {
|
||||||
|
Text(stringResource(R.string.setup_parent_mode_field_name_hint))
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true,
|
||||||
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
run {
|
||||||
|
Text(stringResource(R.string.setup_parent_mode_explaination_password, content.mail))
|
||||||
|
|
||||||
|
SetPassword(
|
||||||
|
password1 = content.newUserDetails.password,
|
||||||
|
password2 = content.newUserDetails.password2,
|
||||||
|
updatePassword1 = content.actions.newUserActions?.updatePassword ?: {},
|
||||||
|
updatePassword2 = content.actions.newUserActions?.updatePassword2 ?: {},
|
||||||
|
enabled = content.actions.newUserActions != null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run {
|
||||||
|
Text(stringResource(R.string.setup_parent_mode_explaination_device_title))
|
||||||
|
|
||||||
|
EnterTextField(
|
||||||
|
value = content.deviceName,
|
||||||
|
onValueChange = content.actions.updateDeviceName,
|
||||||
|
label = {
|
||||||
|
Text(stringResource(R.string.setup_parent_mode_explaination_device_title_field_hint))
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
onConfirmInput = content.actions.next ?: {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.showLimitedProInfo) Text(stringResource(R.string.purchase_demo_temporarily_notice))
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = content.actions.next ?: {},
|
||||||
|
enabled = content.actions.next != null,
|
||||||
|
modifier = Modifier.align(Alignment.End)
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.wiazrd_next))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,123 @@
|
||||||
|
/*
|
||||||
|
* 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.setup.parent
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.Button
|
||||||
|
import androidx.compose.material.Card
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.timelimit.android.BuildConfig
|
||||||
|
import io.timelimit.android.R
|
||||||
|
import io.timelimit.android.ui.diagnose.exception.DiagnoseExceptionDialog
|
||||||
|
import io.timelimit.android.ui.model.setup.SetupParentHandling
|
||||||
|
import io.timelimit.android.ui.view.NotifyPermissionCard
|
||||||
|
import io.timelimit.android.ui.view.SwitchRow
|
||||||
|
import io.timelimit.android.update.UpdateUtil
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ParentSetupConsent(
|
||||||
|
content: SetupParentHandling.ParentSetupConsent,
|
||||||
|
errorDialog: Pair<String, () -> Unit>?,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
ConsentCard(
|
||||||
|
stringResource(R.string.device_background_sync_title),
|
||||||
|
stringResource(R.string.device_background_sync_text),
|
||||||
|
stringResource(R.string.device_background_sync_checkbox),
|
||||||
|
checked = content.backgroundSync,
|
||||||
|
onCheckedChanged = content.actions?.updateBackgroundSync ?: {/* do nothing */},
|
||||||
|
enabled = content.actions?.updateBackgroundSync != null
|
||||||
|
)
|
||||||
|
|
||||||
|
if (content.showEnableUpdates) ConsentCard(
|
||||||
|
stringResource(R.string.update),
|
||||||
|
stringResource(R.string.update_privacy, BuildConfig.updateServer),
|
||||||
|
stringResource(R.string.update_enable_switch),
|
||||||
|
checked = content.enableUpdates,
|
||||||
|
onCheckedChanged = content.actions?.updateEnableUpdates ?: {/* do nothing */},
|
||||||
|
enabled = content.actions?.updateEnableUpdates != null
|
||||||
|
)
|
||||||
|
|
||||||
|
NotifyPermissionCard.View(
|
||||||
|
status = content.notificationAccess,
|
||||||
|
listener = object: NotifyPermissionCard.Listener {
|
||||||
|
override fun onGrantClicked() { content.actions?.requestNotifyPermission?.invoke() }
|
||||||
|
override fun onSkipClicked() { content.actions?.skipNotifyPermission?.invoke() }
|
||||||
|
},
|
||||||
|
enabled = content.actions != null
|
||||||
|
)
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = content.actions?.next ?: {},
|
||||||
|
enabled = content.actions?.next != null,
|
||||||
|
modifier = Modifier.align(Alignment.End)
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.wiazrd_next))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorDialog != null) DiagnoseExceptionDialog(errorDialog.first, errorDialog.second)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ConsentCard(
|
||||||
|
title: String,
|
||||||
|
text: String,
|
||||||
|
switch: String,
|
||||||
|
checked: Boolean,
|
||||||
|
onCheckedChanged: (Boolean) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
enabled: Boolean = true
|
||||||
|
) {
|
||||||
|
Card {
|
||||||
|
Column(
|
||||||
|
modifier.fillMaxWidth().padding(8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style = MaterialTheme.typography.h5
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(text)
|
||||||
|
|
||||||
|
SwitchRow(
|
||||||
|
label = switch,
|
||||||
|
checked = checked,
|
||||||
|
enabled = enabled,
|
||||||
|
onCheckedChange = onCheckedChanged,
|
||||||
|
reverse = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,221 +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.setup.parent
|
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.lifecycle.*
|
|
||||||
import io.timelimit.android.R
|
|
||||||
import io.timelimit.android.async.Threads
|
|
||||||
import io.timelimit.android.coroutines.executeAndWait
|
|
||||||
import io.timelimit.android.coroutines.runAsync
|
|
||||||
import io.timelimit.android.data.devicename.DeviceName
|
|
||||||
import io.timelimit.android.databinding.FragmentSetupParentModeBinding
|
|
||||||
import io.timelimit.android.livedata.*
|
|
||||||
import io.timelimit.android.logic.DefaultAppLogic
|
|
||||||
import io.timelimit.android.sync.network.StatusOfMailAddress
|
|
||||||
import io.timelimit.android.ui.authentication.AuthenticateByMailFragment
|
|
||||||
import io.timelimit.android.ui.authentication.AuthenticateByMailFragmentListener
|
|
||||||
import io.timelimit.android.ui.update.UpdateConsentCard
|
|
||||||
import io.timelimit.android.ui.view.NotifyPermissionCard
|
|
||||||
|
|
||||||
class SetupParentModeFragment : Fragment(), AuthenticateByMailFragmentListener {
|
|
||||||
companion object {
|
|
||||||
private const val STATUS_NOTIFY_PERMISSION = "notify permission"
|
|
||||||
}
|
|
||||||
|
|
||||||
private val model: SetupParentModeModel by lazy { ViewModelProviders.of(this).get(SetupParentModeModel::class.java) }
|
|
||||||
private var notifyPermission = MutableLiveData<NotifyPermissionCard.Status>()
|
|
||||||
|
|
||||||
private val requestNotifyPermission = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
|
|
||||||
if (isGranted) notifyPermission.value = NotifyPermissionCard.Status.Granted
|
|
||||||
else Toast.makeText(requireContext(), R.string.notify_permission_rejected_toast, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
if (savedInstanceState != null) {
|
|
||||||
notifyPermission.value = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
|
|
||||||
savedInstanceState.getSerializable(STATUS_NOTIFY_PERMISSION, NotifyPermissionCard.Status::class.java)!!
|
|
||||||
else
|
|
||||||
savedInstanceState.getSerializable(STATUS_NOTIFY_PERMISSION)!! as NotifyPermissionCard.Status
|
|
||||||
}
|
|
||||||
|
|
||||||
notifyPermission.value = NotifyPermissionCard.updateStatus(notifyPermission.value ?: NotifyPermissionCard.Status.Unknown, requireContext())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
|
|
||||||
notifyPermission.value = NotifyPermissionCard.updateStatus(notifyPermission.value ?: NotifyPermissionCard.Status.Unknown, requireContext())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
|
||||||
super.onSaveInstanceState(outState)
|
|
||||||
|
|
||||||
outState.putSerializable(STATUS_NOTIFY_PERMISSION, notifyPermission.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
|
||||||
val binding = FragmentSetupParentModeBinding.inflate(layoutInflater, container, false)
|
|
||||||
|
|
||||||
model.mailAuthToken.switchMap {
|
|
||||||
mailAuthToken ->
|
|
||||||
|
|
||||||
if (mailAuthToken == null) {
|
|
||||||
liveDataFromNonNullValue(1) // show login screen
|
|
||||||
} else {
|
|
||||||
// show form or loading indicator or error screen
|
|
||||||
model.statusOfMailAddress.switchMap {
|
|
||||||
status ->
|
|
||||||
|
|
||||||
if (status == null) {
|
|
||||||
liveDataFromNonNullValue(2) // loading screen
|
|
||||||
} else if (status.status == StatusOfMailAddress.MailAddressWithoutFamily && status.canCreateFamily == false) {
|
|
||||||
liveDataFromNonNullValue(3) // signup disabled screen
|
|
||||||
} else {
|
|
||||||
model.isDoingSetup.map {
|
|
||||||
if (it!!) {
|
|
||||||
2 // loading screen
|
|
||||||
} else {
|
|
||||||
0 // the form
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.observe(viewLifecycleOwner, Observer {
|
|
||||||
binding.switcher.displayedChild = it!!
|
|
||||||
})
|
|
||||||
|
|
||||||
model.statusOfMailAddress.observe(viewLifecycleOwner, Observer {
|
|
||||||
if (it != null) {
|
|
||||||
binding.isNewFamily = when (it.status) {
|
|
||||||
StatusOfMailAddress.MailAddressWithoutFamily -> true
|
|
||||||
StatusOfMailAddress.MailAddressWithFamily -> false
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.showLimitedProInfo = !it.alwaysPro
|
|
||||||
binding.mail = it.mail
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
val isPasswordValid = model.statusOfMailAddress.switchMap {
|
|
||||||
if (it == null) {
|
|
||||||
liveDataFromNonNullValue(false)
|
|
||||||
} else {
|
|
||||||
when (it.status) {
|
|
||||||
StatusOfMailAddress.MailAddressWithFamily -> liveDataFromNonNullValue(true)
|
|
||||||
StatusOfMailAddress.MailAddressWithoutFamily -> binding.password.passwordOk
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val isNotifyPermissionValid = notifyPermission.map { NotifyPermissionCard.canProceed(it) }
|
|
||||||
|
|
||||||
val isPreNameValid = model.statusOfMailAddress.switchMap {
|
|
||||||
if (it == null) {
|
|
||||||
liveDataFromNonNullValue(false)
|
|
||||||
} else {
|
|
||||||
when (it.status) {
|
|
||||||
StatusOfMailAddress.MailAddressWithFamily -> liveDataFromNonNullValue(true)
|
|
||||||
StatusOfMailAddress.MailAddressWithoutFamily -> binding.prename.getTextLive().map { prename -> prename.isNotBlank() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val isDeviceNameValid = binding.deviceName.getTextLive().map { it.isNotBlank() }
|
|
||||||
|
|
||||||
val isInputValid = isPasswordValid.and(isNotifyPermissionValid).and(isPreNameValid).and(isDeviceNameValid)
|
|
||||||
|
|
||||||
isInputValid.ignoreUnchanged().observe(viewLifecycleOwner, Observer {
|
|
||||||
binding.enableOkButton = it!!
|
|
||||||
})
|
|
||||||
|
|
||||||
if (savedInstanceState == null) {
|
|
||||||
val ctx = requireContext()
|
|
||||||
|
|
||||||
runAsync {
|
|
||||||
// provide an useful default value
|
|
||||||
val deviceName = Threads.database.executeAndWait { DeviceName.getDeviceNameSync(ctx) }
|
|
||||||
|
|
||||||
binding.deviceName.setText(deviceName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.ok.setOnClickListener {
|
|
||||||
val status = model.statusOfMailAddress.value
|
|
||||||
|
|
||||||
if (status == null) {
|
|
||||||
throw IllegalStateException()
|
|
||||||
}
|
|
||||||
|
|
||||||
when (status.status) {
|
|
||||||
StatusOfMailAddress.MailAddressWithoutFamily -> {
|
|
||||||
model.createFamily(
|
|
||||||
parentPassword = binding.password.readPassword(),
|
|
||||||
parentName = binding.prename.text.toString(),
|
|
||||||
deviceName = binding.deviceName.text.toString(),
|
|
||||||
enableBackgroundSync = binding.backgroundSyncCheckbox.isChecked,
|
|
||||||
enableUpdateChecks = binding.update.enableSwitch.isChecked
|
|
||||||
)
|
|
||||||
}
|
|
||||||
StatusOfMailAddress.MailAddressWithFamily -> {
|
|
||||||
model.addDeviceToFamily(
|
|
||||||
deviceName = binding.deviceName.text.toString(),
|
|
||||||
enableBackgroundSync = binding.backgroundSyncCheckbox.isChecked,
|
|
||||||
enableUpdateChecks = binding.update.enableSwitch.isChecked
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateConsentCard.bind(
|
|
||||||
view = binding.update,
|
|
||||||
lifecycleOwner = viewLifecycleOwner,
|
|
||||||
database = DefaultAppLogic.with(requireContext()).database
|
|
||||||
)
|
|
||||||
|
|
||||||
NotifyPermissionCard.bind(object: NotifyPermissionCard.Listener {
|
|
||||||
override fun onGrantClicked() { requestNotifyPermission.launch(Manifest.permission.POST_NOTIFICATIONS) }
|
|
||||||
override fun onSkipClicked() { notifyPermission.value = NotifyPermissionCard.Status.SkipGrant }
|
|
||||||
}, binding.notifyPermissionCard)
|
|
||||||
|
|
||||||
notifyPermission.observe(viewLifecycleOwner) { NotifyPermissionCard.bind(it, binding.notifyPermissionCard) }
|
|
||||||
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLoginSucceeded(mailAuthToken: String) = model.setMailToken(mailAuthToken)
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
if (savedInstanceState == null) {
|
|
||||||
childFragmentManager.beginTransaction()
|
|
||||||
.replace(R.id.mail_auth_container, AuthenticateByMailFragment())
|
|
||||||
.commit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,225 +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.setup.parent
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.util.Log
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.lifecycle.AndroidViewModel
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.map
|
|
||||||
import io.timelimit.android.BuildConfig
|
|
||||||
import io.timelimit.android.R
|
|
||||||
import io.timelimit.android.async.Threads
|
|
||||||
import io.timelimit.android.coroutines.executeAndWait
|
|
||||||
import io.timelimit.android.coroutines.runAsync
|
|
||||||
import io.timelimit.android.data.backup.DatabaseBackup
|
|
||||||
import io.timelimit.android.data.devicename.DeviceName
|
|
||||||
import io.timelimit.android.livedata.castDown
|
|
||||||
import io.timelimit.android.logic.DefaultAppLogic
|
|
||||||
import io.timelimit.android.sync.ApplyServerDataStatus
|
|
||||||
import io.timelimit.android.sync.network.NewDeviceInfo
|
|
||||||
import io.timelimit.android.sync.network.ParentPassword
|
|
||||||
import io.timelimit.android.sync.network.StatusOfMailAddressResponse
|
|
||||||
import io.timelimit.android.sync.network.api.ConflictHttpError
|
|
||||||
import io.timelimit.android.sync.network.api.UnauthorizedHttpError
|
|
||||||
import io.timelimit.android.ui.setup.SetupUnprovisionedCheck
|
|
||||||
import io.timelimit.android.update.UpdateUtil
|
|
||||||
import io.timelimit.android.work.PeriodicSyncInBackgroundWorker
|
|
||||||
|
|
||||||
class SetupParentModeModel(application: Application): AndroidViewModel(application) {
|
|
||||||
companion object {
|
|
||||||
private const val LOG_TAG = "SetupParentModeModel"
|
|
||||||
}
|
|
||||||
|
|
||||||
private val logic = DefaultAppLogic.with(application)
|
|
||||||
|
|
||||||
private val mailAuthTokenInternal = MutableLiveData<String?>().apply { value = null }
|
|
||||||
private val statusOfMailAddressInternal = MutableLiveData<StatusOfMailAddressResponse?>().apply { value = null }
|
|
||||||
private val isDoingSetupInternal = MutableLiveData<Boolean>().apply { value = false }
|
|
||||||
|
|
||||||
val mailAuthToken = mailAuthTokenInternal.castDown()
|
|
||||||
val statusOfMailAddress = statusOfMailAddressInternal.castDown()
|
|
||||||
val isDoingSetup = isDoingSetupInternal.castDown()
|
|
||||||
val isSetupDone = logic.database.config().getOwnDeviceId().map { it != null }
|
|
||||||
|
|
||||||
fun setMailToken(mailAuthToken: String) {
|
|
||||||
if (this.mailAuthTokenInternal.value == null) {
|
|
||||||
this.mailAuthTokenInternal.value = mailAuthToken
|
|
||||||
this.statusOfMailAddressInternal.value = null
|
|
||||||
|
|
||||||
runAsync {
|
|
||||||
try {
|
|
||||||
val api = logic.serverLogic.getServerConfigCoroutine().api
|
|
||||||
val status = api.getStatusByMailToken(mailAuthToken)
|
|
||||||
|
|
||||||
statusOfMailAddressInternal.value = status
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
Toast.makeText(getApplication(), R.string.error_network, Toast.LENGTH_SHORT).show()
|
|
||||||
|
|
||||||
mailAuthTokenInternal.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createFamily(parentPassword: String, parentName: String, deviceName: String, enableBackgroundSync: Boolean, enableUpdateChecks: Boolean) {
|
|
||||||
val database = logic.database
|
|
||||||
|
|
||||||
if (isDoingSetup.value!!) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
isDoingSetupInternal.value = true
|
|
||||||
|
|
||||||
runAsync {
|
|
||||||
try {
|
|
||||||
val api = logic.serverLogic.getServerConfigCoroutine().api
|
|
||||||
val deviceModelName = Threads.database.executeAndWait { DeviceName.getDeviceNameSync(getApplication()) }
|
|
||||||
|
|
||||||
val registerResponse = api.createFamilyByMailToken(
|
|
||||||
mailToken = mailAuthToken.value!!,
|
|
||||||
parentPassword = ParentPassword.createCoroutine(parentPassword, null),
|
|
||||||
parentDevice = NewDeviceInfo(
|
|
||||||
model = deviceModelName
|
|
||||||
),
|
|
||||||
deviceName = deviceName,
|
|
||||||
parentName = parentName,
|
|
||||||
timeZone = logic.timeApi.getSystemTimeZone().id
|
|
||||||
)
|
|
||||||
|
|
||||||
val clientStatusResponse = registerResponse.data
|
|
||||||
|
|
||||||
Threads.database.executeAndWait {
|
|
||||||
logic.database.runInTransaction {
|
|
||||||
val customServerUrl = logic.database.config().getCustomServerUrlSync()
|
|
||||||
|
|
||||||
SetupUnprovisionedCheck.checkSync(logic.database)
|
|
||||||
|
|
||||||
database.deleteAllData()
|
|
||||||
|
|
||||||
database.config().setCustomServerUrlSync(customServerUrl)
|
|
||||||
database.config().setOwnDeviceIdSync(registerResponse.ownDeviceId)
|
|
||||||
database.config().setDeviceAuthTokenSync(registerResponse.deviceAuthToken)
|
|
||||||
database.config().setEnableBackgroundSync(enableBackgroundSync)
|
|
||||||
|
|
||||||
ApplyServerDataStatus.applyServerDataStatusSync(clientStatusResponse, logic.database, logic.platformIntegration)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DatabaseBackup.with(getApplication()).tryCreateDatabaseBackupAsync()
|
|
||||||
|
|
||||||
if (enableBackgroundSync) {
|
|
||||||
PeriodicSyncInBackgroundWorker.enable(getApplication())
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateUtil.setEnableChecks(getApplication(), enableUpdateChecks)
|
|
||||||
|
|
||||||
// the fragment detects the success and leaves this screen
|
|
||||||
} catch (ex: ConflictHttpError) {
|
|
||||||
mailAuthTokenInternal.value = null
|
|
||||||
isDoingSetupInternal.value = false
|
|
||||||
|
|
||||||
Toast.makeText(getApplication(), R.string.error_server_rejected, Toast.LENGTH_SHORT).show()
|
|
||||||
} catch (ex: UnauthorizedHttpError) {
|
|
||||||
isDoingSetupInternal.value = false
|
|
||||||
mailAuthTokenInternal.value = null
|
|
||||||
|
|
||||||
Toast.makeText(getApplication(), R.string.error_server_rejected, Toast.LENGTH_SHORT).show()
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
if (BuildConfig.DEBUG) {
|
|
||||||
Log.w(LOG_TAG, "error during setup", ex)
|
|
||||||
}
|
|
||||||
|
|
||||||
isDoingSetupInternal.value = false
|
|
||||||
|
|
||||||
Toast.makeText(getApplication(), R.string.error_network, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addDeviceToFamily(deviceName: String, enableBackgroundSync: Boolean, enableUpdateChecks: Boolean) {
|
|
||||||
val database = logic.database
|
|
||||||
|
|
||||||
if (isDoingSetup.value!!) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
isDoingSetupInternal.value = true
|
|
||||||
|
|
||||||
runAsync {
|
|
||||||
try {
|
|
||||||
val api = logic.serverLogic.getServerConfigCoroutine().api
|
|
||||||
val deviceModelName = Threads.database.executeAndWait { DeviceName.getDeviceNameSync(getApplication()) }
|
|
||||||
|
|
||||||
val registerResponse = api.signInToFamilyByMailToken(
|
|
||||||
mailToken = mailAuthToken.value!!,
|
|
||||||
parentDevice = NewDeviceInfo(
|
|
||||||
model = deviceModelName
|
|
||||||
),
|
|
||||||
deviceName = deviceName
|
|
||||||
)
|
|
||||||
|
|
||||||
val clientStatusResponse = registerResponse.data
|
|
||||||
|
|
||||||
Threads.database.executeAndWait {
|
|
||||||
logic.database.runInTransaction {
|
|
||||||
val customServerUrl = logic.database.config().getCustomServerUrlSync()
|
|
||||||
|
|
||||||
SetupUnprovisionedCheck.checkSync(logic.database)
|
|
||||||
|
|
||||||
database.deleteAllData()
|
|
||||||
|
|
||||||
database.config().setCustomServerUrlSync(customServerUrl)
|
|
||||||
database.config().setOwnDeviceIdSync(registerResponse.ownDeviceId)
|
|
||||||
database.config().setDeviceAuthTokenSync(registerResponse.deviceAuthToken)
|
|
||||||
database.config().setEnableBackgroundSync(enableBackgroundSync)
|
|
||||||
|
|
||||||
ApplyServerDataStatus.applyServerDataStatusSync(clientStatusResponse, logic.database, logic.platformIntegration)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DatabaseBackup.with(getApplication()).tryCreateDatabaseBackupAsync()
|
|
||||||
|
|
||||||
if (enableBackgroundSync) {
|
|
||||||
PeriodicSyncInBackgroundWorker.enable(getApplication())
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateUtil.setEnableChecks(getApplication(), enableUpdateChecks)
|
|
||||||
|
|
||||||
// the fragment detects the success and leaves this screen
|
|
||||||
} catch (ex: ConflictHttpError) {
|
|
||||||
isDoingSetupInternal.value = false
|
|
||||||
mailAuthTokenInternal.value = null
|
|
||||||
|
|
||||||
Toast.makeText(getApplication(), R.string.error_server_rejected, Toast.LENGTH_SHORT).show()
|
|
||||||
} catch (ex: UnauthorizedHttpError) {
|
|
||||||
isDoingSetupInternal.value = false
|
|
||||||
mailAuthTokenInternal.value = null
|
|
||||||
|
|
||||||
Toast.makeText(getApplication(), R.string.error_server_rejected, Toast.LENGTH_SHORT).show()
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
if (BuildConfig.DEBUG) {
|
|
||||||
Log.w(LOG_TAG, "error during setup", ex)
|
|
||||||
}
|
|
||||||
|
|
||||||
isDoingSetupInternal.value = false
|
|
||||||
|
|
||||||
Toast.makeText(getApplication(), R.string.error_network, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
* 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.setup.parent
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.timelimit.android.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SignInWrongMailAddress(modifier: Modifier) {
|
||||||
|
Column(
|
||||||
|
modifier.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.setup_parent_mode_error_wrong_mail_address_title),
|
||||||
|
style = MaterialTheme.typography.h5,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.setup_parent_mode_error_wrong_mail_address_text),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* 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.setup.parent
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.timelimit.android.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SignupBlockedScreen(modifier: Modifier) {
|
||||||
|
Column(
|
||||||
|
modifier.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.SpaceAround
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.setup_parent_mode_error_signup_disabled),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* 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
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -18,7 +18,26 @@ package io.timelimit.android.ui.view
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.Button
|
||||||
|
import androidx.compose.material.Card
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
|
import io.timelimit.android.R
|
||||||
import io.timelimit.android.databinding.NotifyPermissionCardBinding
|
import io.timelimit.android.databinding.NotifyPermissionCardBinding
|
||||||
|
|
||||||
object NotifyPermissionCard {
|
object NotifyPermissionCard {
|
||||||
|
@ -29,6 +48,49 @@ object NotifyPermissionCard {
|
||||||
Granted
|
Granted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun View(
|
||||||
|
status: Status,
|
||||||
|
listener: Listener,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
enabled: Boolean = true
|
||||||
|
) {
|
||||||
|
Card { Column(
|
||||||
|
modifier.padding(8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.notify_permission_title),
|
||||||
|
style = MaterialTheme.typography.h5
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.notify_permission_text)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (status == Status.Granted) Text(
|
||||||
|
stringResource(R.string.notify_permission_text_granted),
|
||||||
|
color = Color(ContextCompat.getColor(LocalContext.current, R.color.text_green)).copy(alpha = 1f),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) else if (status == Status.WaitingForInteraction) TextButton(
|
||||||
|
onClick = { listener.onSkipClicked() },
|
||||||
|
modifier = Modifier.align(Alignment.End),
|
||||||
|
enabled = enabled
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.notify_permission_btn_later))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status != Status.Granted) Button(
|
||||||
|
onClick = { listener.onGrantClicked() },
|
||||||
|
modifier = Modifier.align(Alignment.End),
|
||||||
|
enabled = enabled
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.notify_permission_btn_grant))
|
||||||
|
}
|
||||||
|
} }
|
||||||
|
}
|
||||||
|
|
||||||
fun bind(status: Status, view: NotifyPermissionCardBinding) {
|
fun bind(status: Status, view: NotifyPermissionCardBinding) {
|
||||||
view.showGrantButton = status != Status.Granted
|
view.showGrantButton = status != Status.Granted
|
||||||
view.showGrantedMessage = status == Status.Granted
|
view.showGrantedMessage = status == Status.Granted
|
||||||
|
|
100
app/src/main/java/io/timelimit/android/ui/view/SetPassword.kt
Normal file
100
app/src/main/java/io/timelimit/android/ui/view/SetPassword.kt
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
/*
|
||||||
|
* 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.view
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.TextField
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.timelimit.android.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SetPassword(
|
||||||
|
password1: String,
|
||||||
|
password2: String,
|
||||||
|
updatePassword1: (String) -> Unit,
|
||||||
|
updatePassword2: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
enabled: Boolean
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
TextField(
|
||||||
|
value = password1,
|
||||||
|
onValueChange = updatePassword1,
|
||||||
|
enabled = enabled,
|
||||||
|
label = {
|
||||||
|
Text(stringResource(R.string.set_password_view_label_password))
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true,
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
keyboardType = KeyboardType.Password,
|
||||||
|
imeAction = ImeAction.Next
|
||||||
|
),
|
||||||
|
visualTransformation = PasswordVisualTransformation()
|
||||||
|
)
|
||||||
|
|
||||||
|
AnimatedVisibility(visible = password1.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.password_validator_empty),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
color = MaterialTheme.colors.secondary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
TextField(
|
||||||
|
value = password2,
|
||||||
|
onValueChange = updatePassword2,
|
||||||
|
enabled = enabled,
|
||||||
|
label = {
|
||||||
|
Text(stringResource(R.string.set_password_view_label_password_repeat))
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true,
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
keyboardType = KeyboardType.Password,
|
||||||
|
imeAction = ImeAction.Next
|
||||||
|
),
|
||||||
|
visualTransformation = PasswordVisualTransformation()
|
||||||
|
)
|
||||||
|
|
||||||
|
AnimatedVisibility(visible = password1 != password2) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.set_password_view_not_identical),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
color = MaterialTheme.colors.secondary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,6 +17,7 @@ package io.timelimit.android.ui.view
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
@ -36,7 +37,8 @@ fun SwitchRow(
|
||||||
checked: Boolean,
|
checked: Boolean,
|
||||||
onCheckedChange: (Boolean) -> Unit,
|
onCheckedChange: (Boolean) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
enabled: Boolean = true
|
enabled: Boolean = true,
|
||||||
|
reverse: Boolean = false
|
||||||
) {
|
) {
|
||||||
Row (
|
Row (
|
||||||
modifier
|
modifier
|
||||||
|
@ -51,14 +53,21 @@ fun SwitchRow(
|
||||||
),
|
),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
|
if (reverse) {
|
||||||
|
Text(label)
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
|
||||||
Switch(
|
Switch(
|
||||||
checked = checked,
|
checked = checked,
|
||||||
onCheckedChange = onCheckedChange,
|
onCheckedChange = onCheckedChange,
|
||||||
enabled = enabled
|
enabled = enabled
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(Modifier.width(8.dp))
|
if (!reverse) {
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
Text(label)
|
Text(label)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,173 +0,0 @@
|
||||||
<!--
|
|
||||||
TimeLimit Copyright <C> 2019 - 2022 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.setup.parent.SetupParentModeFragment">
|
|
||||||
|
|
||||||
<data>
|
|
||||||
<variable
|
|
||||||
name="isNewFamily"
|
|
||||||
type="Boolean" />
|
|
||||||
|
|
||||||
<variable
|
|
||||||
name="mail"
|
|
||||||
type="String" />
|
|
||||||
|
|
||||||
<variable
|
|
||||||
name="enableOkButton"
|
|
||||||
type="Boolean" />
|
|
||||||
|
|
||||||
<variable
|
|
||||||
name="showLimitedProInfo"
|
|
||||||
type="boolean" />
|
|
||||||
|
|
||||||
<import type="android.view.View" />
|
|
||||||
</data>
|
|
||||||
|
|
||||||
<io.timelimit.android.ui.view.SafeViewFlipper
|
|
||||||
android:id="@+id/switcher"
|
|
||||||
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:orientation="vertical"
|
|
||||||
android:padding="8dp"
|
|
||||||
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"
|
|
||||||
tools:text="@string/setup_parent_mode_create_family"
|
|
||||||
android:text="@{safeUnbox(isNewFamily) ? @string/setup_parent_mode_create_family : @string/setup_parent_mode_add_device}"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:visibility="@{safeUnbox(isNewFamily) ? View.VISIBLE : View.GONE}"
|
|
||||||
android:textAppearance="?android:textAppearanceMedium"
|
|
||||||
android:text="@string/setup_parent_mode_explaination_user_name"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:inputType="textPersonName"
|
|
||||||
android:id="@+id/prename"
|
|
||||||
android:visibility="@{safeUnbox(isNewFamily) ? View.VISIBLE : View.GONE}"
|
|
||||||
android:hint="@string/setup_parent_mode_field_name_hint"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
<requestFocus />
|
|
||||||
</EditText>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:textAppearance="?android:textAppearanceMedium"
|
|
||||||
android:visibility="@{safeUnbox(isNewFamily) ? View.VISIBLE : View.GONE}"
|
|
||||||
tools:text="@string/setup_parent_mode_explaination_password"
|
|
||||||
android:text="@{@string/setup_parent_mode_explaination_password(mail)}"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<io.timelimit.android.ui.view.SetPasswordView
|
|
||||||
android:visibility="@{safeUnbox(isNewFamily) ? View.VISIBLE : View.GONE}"
|
|
||||||
android:id="@+id/password"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:text="@string/setup_parent_mode_explaination_device_title"
|
|
||||||
android:textAppearance="?android:textAppearanceMedium"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:inputType="text"
|
|
||||||
android:id="@+id/device_name"
|
|
||||||
android:hint="@string/setup_parent_mode_explaination_device_title_field_hint"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:textAppearance="?android:textAppearanceMedium"
|
|
||||||
android:text="@string/device_background_sync_text"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<CheckBox
|
|
||||||
android:text="@string/device_background_sync_checkbox"
|
|
||||||
android:id="@+id/background_sync_checkbox"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:visibility="@{showLimitedProInfo ? View.VISIBLE : View.GONE}"
|
|
||||||
android:textAppearance="?android:textAppearanceSmall"
|
|
||||||
android:text="@string/purchase_demo_temporarily_notice"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
</androidx.cardview.widget.CardView>
|
|
||||||
|
|
||||||
<include layout="@layout/notify_permission_card"
|
|
||||||
android:id="@+id/notify_permission_card" />
|
|
||||||
|
|
||||||
<include
|
|
||||||
layout="@layout/update_consent_card"
|
|
||||||
android:id="@+id/update" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:layout_marginEnd="4dp"
|
|
||||||
android:enabled="@{safeUnbox(enableOkButton)}"
|
|
||||||
android:id="@+id/ok"
|
|
||||||
android:layout_gravity="end"
|
|
||||||
android:text="@string/generic_ok"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
</ScrollView>
|
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
android:id="@+id/mail_auth_container"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent" />
|
|
||||||
|
|
||||||
<include layout="@layout/circular_progress_indicator" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:text="@string/setup_parent_mode_error_signup_disabled"
|
|
||||||
android:textAppearance="?android:textAppearanceMedium"
|
|
||||||
android:padding="16dp"
|
|
||||||
android:gravity="center"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent" />
|
|
||||||
|
|
||||||
</io.timelimit.android.ui.view.SafeViewFlipper>
|
|
||||||
</layout>
|
|
|
@ -1296,6 +1296,7 @@
|
||||||
dann installieren Sie TimeLimit erneut.
|
dann installieren Sie TimeLimit erneut.
|
||||||
</string>
|
</string>
|
||||||
|
|
||||||
|
<string name="password_validator_empty">Das Passwort darf nicht leer sein</string>
|
||||||
<string name="password_validator_too_short">Das Passwort muss mindestens <xliff:g example="5" id="number of chars">%d</xliff:g> Zeichen haben</string>
|
<string name="password_validator_too_short">Das Passwort muss mindestens <xliff:g example="5" id="number of chars">%d</xliff:g> Zeichen haben</string>
|
||||||
|
|
||||||
<string name="primary_device_title">Aktuelles Gerät</string>
|
<string name="primary_device_title">Aktuelles Gerät</string>
|
||||||
|
@ -1471,6 +1472,18 @@
|
||||||
|
|
||||||
<string name="setup_parent_mode_error_signup_disabled">Momentan sind keine Neuanmeldungen möglich</string>
|
<string name="setup_parent_mode_error_signup_disabled">Momentan sind keine Neuanmeldungen möglich</string>
|
||||||
|
|
||||||
|
<string name="setup_parent_mode_error_wrong_mail_address_title">falsche E-Mail-Adresse</string>
|
||||||
|
<string name="setup_parent_mode_error_wrong_mail_address_text">Es liegt keine Registrierung mit
|
||||||
|
der von Ihnen angegebenen E-Mail-Adresse vor. Finden Sie heraus, mit welcher E-Mail-Adresse
|
||||||
|
Sie sich ursprünglich angemeldet haben. Ansonsten gehen die bestehenden Verknüpfungen mit
|
||||||
|
anderen Geräten und eine evtl. vorhandene Vollversion verloren. Sie können die E-Mail-Adresse
|
||||||
|
bei noch bestehenden TimeLimit-Installationen einsehen oder in Ihren Postfächern nachsehen,
|
||||||
|
wohin damals die Anmelde-E-Mail gesendet wurde. Der Support kann Ihnen aus Datenschutzgründen
|
||||||
|
nicht mitteilen, welche E-Mail-Adresse Ihren Namen trägt und registriert ist.
|
||||||
|
</string>
|
||||||
|
|
||||||
|
<string name="setup_parent_confirm_new_account_text">Haben Sie sich bereits für TimeLimit mit Vernetzung registriert oder haben Sie bereits die Vollversion erworben?</string>
|
||||||
|
|
||||||
<string name="setup_privacy_connected_title">Vernetzung und Datenschutz</string>
|
<string name="setup_privacy_connected_title">Vernetzung und Datenschutz</string>
|
||||||
<string name="setup_privacy_connected_text_general_intro">Für den vernetzten Modus
|
<string name="setup_privacy_connected_text_general_intro">Für den vernetzten Modus
|
||||||
gibt es eine Zentrale - den Server. Auf diesen werden die Einstellungen,
|
gibt es eine Zentrale - den Server. Auf diesen werden die Einstellungen,
|
||||||
|
|
|
@ -1342,6 +1342,7 @@
|
||||||
reset/reinstall TimeLimit.
|
reset/reinstall TimeLimit.
|
||||||
</string>
|
</string>
|
||||||
|
|
||||||
|
<string name="password_validator_empty">The password must not be empty</string>
|
||||||
<string name="password_validator_too_short">The password must have at least <xliff:g example="5" id="number of chars">%d</xliff:g> characters</string>
|
<string name="password_validator_too_short">The password must have at least <xliff:g example="5" id="number of chars">%d</xliff:g> characters</string>
|
||||||
|
|
||||||
<string name="primary_device_title">Current device</string>
|
<string name="primary_device_title">Current device</string>
|
||||||
|
@ -1513,6 +1514,18 @@
|
||||||
|
|
||||||
<string name="setup_parent_mode_error_signup_disabled">It is currently not possible to create new accounts</string>
|
<string name="setup_parent_mode_error_signup_disabled">It is currently not possible to create new accounts</string>
|
||||||
|
|
||||||
|
<string name="setup_parent_mode_error_wrong_mail_address_title">wrong mail address</string>
|
||||||
|
<string name="setup_parent_mode_error_wrong_mail_address_text">There is no registration
|
||||||
|
for the mail address that you provided. Do some research to find the correct one.
|
||||||
|
Otherwise, you will loose any premium version and linked devices. You can see the
|
||||||
|
mail address at other devices if TimeLimit is still installed. Moreover, you can check
|
||||||
|
your mailboxes to see at which mail address the login mail was sent in the past.
|
||||||
|
For privacy reasons, the support cannot tell you which registered mail addresses
|
||||||
|
with your name exist.
|
||||||
|
</string>
|
||||||
|
|
||||||
|
<string name="setup_parent_confirm_new_account_text">Did you already sign up for the connected mode/did you buy the premium version already?</string>
|
||||||
|
|
||||||
<string name="setup_privacy_connected_title">Networking and Privacy</string>
|
<string name="setup_privacy_connected_title">Networking and Privacy</string>
|
||||||
<string name="setup_privacy_connected_text_general_intro">For the connected mode,
|
<string name="setup_privacy_connected_text_general_intro">For the connected mode,
|
||||||
there is a central unit - the server. This server saves the settings, the usage durations
|
there is a central unit - the server. This server saves the settings, the usage durations
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue