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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -16,13 +16,14 @@
|
|||
package io.timelimit.android.sync.network
|
||||
|
||||
import android.util.JsonReader
|
||||
import java.io.Serializable
|
||||
|
||||
data class StatusOfMailAddressResponse(
|
||||
val mail: String,
|
||||
val status: StatusOfMailAddress,
|
||||
val canCreateFamily: Boolean,
|
||||
val alwaysPro: Boolean
|
||||
) {
|
||||
): Serializable {
|
||||
companion object {
|
||||
fun parse(reader: JsonReader): StatusOfMailAddressResponse {
|
||||
var mail: String? = null
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
package io.timelimit.android.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
|
@ -26,6 +27,7 @@ import android.provider.Settings
|
|||
import android.util.Log
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
|
@ -123,6 +125,10 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
|
|||
override var ignoreStop: Boolean = false
|
||||
override val showPasswordRecovery: Boolean = true
|
||||
|
||||
private val requestNotifyPermission = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
||||
if (granted) mainModel.reportPermissionsChanged()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
@ -162,6 +168,7 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
|
|||
} catch (ex: Exception) {
|
||||
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.fragment.app.FragmentManager
|
||||
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.manage.category.blocked_times.BlockedTimesScreen
|
||||
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.setup.selectmode.SelectConnectedModeScreen
|
||||
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.selectmode.SelectModeScreen
|
||||
|
||||
|
@ -52,5 +58,11 @@ fun ScreenMultiplexer(
|
|||
is Screen.DeleteRegistration -> DeleteRegistrationScreen(screen.content, modifier)
|
||||
is Screen.ManageBlockedTimes -> BlockedTimesScreen(screen.content, screen.intro, 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()
|
||||
class LaunchSystemSettings(val permission: SystemPermission): 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 authenticationScreenClosed = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
|
||||
private val permissionsChanged = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
|
||||
|
||||
private val authenticationModelApi = object: AuthenticationModelApi {
|
||||
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.ManageDevice> { state -> ManageDeviceHandling.processState(logic, activityCommandInternal, authenticationModelApi, state, updateMethod(::updateState)) },
|
||||
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<_, _, FragmentState> { state ->
|
||||
state.transform {
|
||||
|
@ -141,5 +142,9 @@ class MainModel(application: Application): AndroidViewModel(application) {
|
|||
authenticationScreenClosed.tryEmit(Unit)
|
||||
}
|
||||
|
||||
fun reportPermissionsChanged() {
|
||||
permissionsChanged.tryEmit(Unit)
|
||||
}
|
||||
|
||||
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.diagnose.DeviceOwnerHandling
|
||||
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.managechild.ManageCategoryBlockedTimes
|
||||
import io.timelimit.android.ui.model.managechild.ManageChildUsageHistory
|
||||
import io.timelimit.android.ui.model.managedevice.ManageDeviceUser
|
||||
import io.timelimit.android.ui.model.setup.SetupParentHandling
|
||||
|
||||
sealed class Screen(
|
||||
val state: State,
|
||||
|
@ -270,6 +272,37 @@ sealed class Screen(
|
|||
): Screen(state), ScreenWithSnackbar, ScreenWithTitle {
|
||||
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
|
||||
|
|
|
@ -22,6 +22,8 @@ import androidx.compose.material.icons.filled.Phone
|
|||
import androidx.fragment.app.Fragment
|
||||
import io.timelimit.android.R
|
||||
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.diagnose.*
|
||||
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.model.account.AccountDeletion
|
||||
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.managechild.ManageCategoryBlockedTimes
|
||||
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.parentmode.ParentModeFragment
|
||||
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.child.SetupRemoteChildFragment
|
||||
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.view.NotifyPermissionCard
|
||||
import java.io.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 SelectConnectedMode(previousConnectedPrivacy: ConnectedPrivacy): Setup(previousConnectedPrivacy)
|
||||
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)
|
||||
object Purchase {
|
||||
|
|
|
@ -35,6 +35,10 @@ import java.io.Serializable
|
|||
|
||||
object MailAuthentication {
|
||||
sealed class State: java.io.Serializable {
|
||||
companion object {
|
||||
val initial = State.EnterMailAddress()
|
||||
}
|
||||
|
||||
abstract val error: ErrorDialog?
|
||||
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 kotlinx.coroutines.channels.SendChannel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
|
||||
object SetupHandling {
|
||||
fun handle(
|
||||
logic: AppLogic,
|
||||
activityCommand: SendChannel<ActivityCommand>,
|
||||
permissionsChanged: SharedFlow<Unit>,
|
||||
stateLive: Flow<State.Setup>,
|
||||
updateState: ((State.Setup) -> State) -> Unit
|
||||
): 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.ConnectedPrivacy> { SetupConnectedModePrivacy.handle(logic, 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 ->
|
||||
Screen.SetupSelectConnectedModeScreen(
|
||||
state = state,
|
||||
mailLogin = { updateState { State.Setup.ParentMode(it) } },
|
||||
mailLogin = { updateState { State.Setup.ParentMailAuthentication(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
|
||||
* 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.content.Context
|
||||
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 io.timelimit.android.R
|
||||
import io.timelimit.android.databinding.NotifyPermissionCardBinding
|
||||
|
||||
object NotifyPermissionCard {
|
||||
|
@ -29,6 +48,49 @@ object NotifyPermissionCard {
|
|||
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) {
|
||||
view.showGrantButton = 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.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
|
@ -36,7 +37,8 @@ fun SwitchRow(
|
|||
checked: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true
|
||||
enabled: Boolean = true,
|
||||
reverse: Boolean = false
|
||||
) {
|
||||
Row (
|
||||
modifier
|
||||
|
@ -51,14 +53,21 @@ fun SwitchRow(
|
|||
),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (reverse) {
|
||||
Text(label)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Spacer(Modifier.weight(1f))
|
||||
}
|
||||
|
||||
Switch(
|
||||
checked = checked,
|
||||
onCheckedChange = onCheckedChange,
|
||||
enabled = enabled
|
||||
)
|
||||
|
||||
if (!reverse) {
|
||||
Spacer(Modifier.width(8.dp))
|
||||
|
||||
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.
|
||||
</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="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_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_text_general_intro">Für den vernetzten Modus
|
||||
gibt es eine Zentrale - den Server. Auf diesen werden die Einstellungen,
|
||||
|
|
|
@ -1342,6 +1342,7 @@
|
|||
reset/reinstall TimeLimit.
|
||||
</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="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_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_text_general_intro">For the connected mode,
|
||||
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