Squashed commit of the following:

commit 168ac15c6d07d01aa61f5d660e7f9118a002bee3
Author: Jonas L <jonas@determapp.de>
Date:   Mon Feb 25 00:00:00 2019 +0000

    Hide sign in with google button when migrating to the connected mode

commit e77f43017875633444ffb4f5a33304fc8366bf26
Author: Jonas L <jonas@determapp.de>
Date:   Mon Feb 25 00:00:00 2019 +0000

    Improve strings

commit 16d8ae49aecd79cdc46d1224b0e4e7b27f5eca53
Author: Jonas L <jonas@determapp.de>
Date:   Mon Feb 25 00:00:00 2019 +0000

    Fix migration time limit rules

commit 88368e16d726840db21177a1cc2a9bd0b4c8c7ae
Author: Jonas L <jonas@determapp.de>
Date:   Mon Feb 25 00:00:00 2019 +0000

    Handle empty name field

commit d7bf7fe950eaf476fd49be8f233ebc39f6334f53
Author: Jonas L <jonas@determapp.de>
Date:   Mon Feb 25 00:00:00 2019 +0000

    Implement basically migration to connected mode

commit d05274d1a4cfa866ff1dafd764bbd90951bfcaa9
Author: Jonas L <jonas@determapp.de>
Date:   Mon Feb 25 00:00:00 2019 +0000

    Add screen for migration

commit 429ae8ce84ebe1121050b94c51f15da6030e8b25
Author: Jonas L <jonas@determapp.de>
Date:   Mon Feb 25 00:00:00 2019 +0000

    Add entry point for migrating to the connected mode
This commit is contained in:
Jonas L 2019-02-25 00:00:00 +00:00
parent 43efaceb7e
commit d85bf2a699
21 changed files with 812 additions and 8 deletions

View file

@ -46,4 +46,7 @@ abstract class CategoryAppDao {
@Query("SELECT * FROM category_app LIMIT :pageSize OFFSET :offset")
abstract fun getCategoryAppPageSync(offset: Int, pageSize: Int): List<CategoryApp>
@Query("SELECT * FROM category_app")
abstract fun getAllCategoryAppSync(): List<CategoryApp>
}

View file

@ -94,6 +94,9 @@ abstract class CategoryDao {
@Query("UPDATE category SET parent_category_id = :parentCategoryId WHERE id = :categoryId")
abstract fun updateParentCategory(categoryId: String, parentCategoryId: String)
@Query("SELECT * FROM category")
abstract fun getAllCategoriesSync(): List<Category>
}
data class CategoryWithVersionNumbers(

View file

@ -56,4 +56,7 @@ abstract class TimeLimitRuleDao {
@Query("SELECT * FROM time_limit_rule LIMIT :pageSize OFFSET :offset")
abstract fun getRulePageSync(offset: Int, pageSize: Int): List<TimeLimitRule>
@Query("SELECT * FROM time_limit_rule")
abstract fun getAllRulesSync(): List<TimeLimitRule>
}

View file

@ -68,4 +68,7 @@ abstract class UsedTimeDao {
@Query("SELECT * FROM used_time WHERE category_id IN (:categoryIds) AND day_of_epoch >= :startingDayOfEpoch AND day_of_epoch <= :endDayOfEpoch")
abstract fun getUsedTimesByDayAndCategoryIds(categoryIds: List<String>, startingDayOfEpoch: Int, endDayOfEpoch: Int): LiveData<List<UsedTimeItem>>
@Query("SELECT * FROM used_time")
abstract fun getAllUsedTimeItemsSync(): List<UsedTimeItem>
}

View file

@ -34,17 +34,27 @@ import io.timelimit.android.ui.MainActivity
class AuthenticateByMailFragment : Fragment() {
companion object {
private const val REQUEST_SIGN_IN_WITH_GOOGLE = 1
private const val EXTRA_HIDE_SIGN_IN_WITH_GOOGLE_BUTTON = "hsiwgb"
fun newInstance(hideSignInWithGoogleButton: Boolean) = AuthenticateByMailFragment().apply {
arguments = Bundle().apply {
putBoolean(EXTRA_HIDE_SIGN_IN_WITH_GOOGLE_BUTTON, hideSignInWithGoogleButton)
}
}
}
private val listener: AuthenticateByMailFragmentListener by lazy { parentFragment as AuthenticateByMailFragmentListener }
private val googleAuthUtil: GoogleSignInUtil by lazy { (activity as MainActivity).googleSignInUtil }
private val model: AuthenticateByMailModel by lazy { ViewModelProviders.of(this).get(AuthenticateByMailModel::class.java) }
private val hideSignInWithGoogleButton: Boolean by lazy {
arguments?.getBoolean(EXTRA_HIDE_SIGN_IN_WITH_GOOGLE_BUTTON, false) ?: false
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val binding = FragmentAuthenticateByMailBinding.inflate(layoutInflater, container, false)
model.usingDefaultServer.observe(this, Observer {
binding.usingDefaultServer = it
binding.showSignInWithGoogleButton = it && (!hideSignInWithGoogleButton)
})
binding.signInWithGoogleButton.setOnClickListener {

View file

@ -166,6 +166,8 @@ class ActivityViewModel(application: Application): AndroidViewModel(application)
authenticatedUserMetadata.value = user
}
fun getAuthenticatedUser() = authenticatedUserMetadata.value
fun logOut() {
authenticatedUserMetadata.value = null
}

View file

@ -0,0 +1,105 @@
/*
* TimeLimit Copyright <C> 2019 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.timelimit.android.ui.migrate_to_connected
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.navigation.Navigation
import io.timelimit.android.R
import io.timelimit.android.data.model.UserType
import io.timelimit.android.databinding.MigrateToConnectedModeFragmentBinding
import io.timelimit.android.ui.authentication.AuthenticateByMailFragment
import io.timelimit.android.ui.authentication.AuthenticateByMailFragmentListener
import io.timelimit.android.ui.main.ActivityViewModelHolder
import io.timelimit.android.ui.main.getActivityViewModel
class MigrateToConnectedModeFragment : Fragment(), AuthenticateByMailFragmentListener {
companion object {
private const val PAGE_READY = 0
private const val PAGE_AUTH = 1
private const val PAGE_WORKING = 2
private const val PAGE_EXISTING_ACCOUNT = 3
private const val PAGE_DONE = 4
}
private val model: MigrateToConnectedModeModel by lazy {
ViewModelProviders.of(this).get(MigrateToConnectedModeModel::class.java)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { // Inflate the layout for this fragment
val binding = MigrateToConnectedModeFragmentBinding.inflate(inflater, container, false)
val navigation = Navigation.findNavController(container!!)
val auth = getActivityViewModel(activity!!)
auth.authenticatedUser.observe(this, Observer {
binding.isParentSignedIn = it?.second?.type == UserType.Parent
})
binding.parentAuthButton.setOnClickListener {
(activity as ActivityViewModelHolder).showAuthenticationScreen()
}
binding.goButton.setOnClickListener {
var name = binding.parentName.text.toString()
if (name.isBlank())
name = getString(R.string.setup_username_parent)
model.doMigration(
model = auth,
parentFirstName = name
)
}
model.status.observe(this, Observer { status ->
when (status) {
LeaveScreenMigrationStatus -> {
navigation.popBackStack()
null
}
WaitingForAuthMigrationStatus -> binding.flipper.displayedChild = PAGE_AUTH
WaitingForConfirmationByParentMigrationStatus -> binding.flipper.displayedChild = PAGE_READY
WorkingMigrationStatus -> binding.flipper.displayedChild = PAGE_WORKING
DoneMigrationStatus -> binding.flipper.displayedChild = PAGE_DONE
ConflictAlreadyHasAccountMigrationStatus -> binding.flipper.displayedChild = PAGE_EXISTING_ACCOUNT
}.let { /* require handling all cases */ }
})
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (savedInstanceState == null) {
childFragmentManager.beginTransaction()
.replace(R.id.mail_auth_container, AuthenticateByMailFragment.newInstance(
hideSignInWithGoogleButton = true
))
.commit()
}
}
override fun onLoginSucceeded(mailAuthToken: String) {
model.onLoginSucceeded(mailAuthToken)
}
}

View file

@ -0,0 +1,189 @@
/*
* TimeLimit Copyright <C> 2019 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.timelimit.android.ui.migrate_to_connected
import android.app.Application
import android.util.Log
import android.widget.Toast
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
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.transaction
import io.timelimit.android.livedata.castDown
import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.sync.ApplyServerDataStatus
import io.timelimit.android.sync.actions.apply.ApplyActionParentPasswordAuthentication
import io.timelimit.android.sync.network.ClientDataStatus
import io.timelimit.android.sync.network.NewDeviceInfo
import io.timelimit.android.sync.network.ParentPassword
import io.timelimit.android.sync.network.StatusOfMailAddress
import io.timelimit.android.sync.network.api.HttpError
import io.timelimit.android.ui.main.ActivityViewModel
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlin.Exception
class MigrateToConnectedModeModel(application: Application): AndroidViewModel(application) {
companion object {
private const val LOG_TAG = "MigrateToConnected"
}
private val logic = DefaultAppLogic.with(application)
private val database = logic.database
private var mailAuthToken: String? = null
private val lock = Mutex()
private val statusInternal = MutableLiveData<MigrateToConnectedModeStatus>().apply {
value = WaitingForAuthMigrationStatus
}
val status = statusInternal.castDown()
fun onLoginSucceeded(mailAuthToken: String) {
this.mailAuthToken = mailAuthToken
runAsync {
lock.withLock {
try {
statusInternal.value = WorkingMigrationStatus
val api = logic.serverLogic.getServerConfigCoroutine().api
val status = api.getStatusByMailToken(mailAuthToken).status
when (status) {
StatusOfMailAddress.MailAddressWithFamily -> statusInternal.value = ConflictAlreadyHasAccountMigrationStatus
StatusOfMailAddress.MailAddressWithoutFamily -> statusInternal.value = WaitingForConfirmationByParentMigrationStatus
}.let { /* require handling all paths */ }
} catch (ex: Exception) {
if (BuildConfig.DEBUG) {
Log.w(LOG_TAG, "error during checking mail", ex)
}
Toast.makeText(
getApplication(),
if (ex is HttpError)
R.string.error_server_rejected
else
R.string.error_network,
Toast.LENGTH_SHORT
).show()
statusInternal.value = LeaveScreenMigrationStatus
}
}
}
}
fun doMigration(
model: ActivityViewModel,
parentFirstName: String
) {
runAsync {
lock.withLock {
try {
statusInternal.value = WorkingMigrationStatus
if (!model.isParentAuthenticated()) {
throw IllegalStateException()
}
val auth = model.getAuthenticatedUser()!!
val currentConfig = Threads.database.executeAndWait {
database.transaction().use {
// check if not yet linked
if (database.config().getDeviceAuthTokenSync() != "") {
throw IllegalStateException("already linked")
}
OfflineModeStatus.query(database)
}
}
// create family at server
val server = logic.serverLogic.getServerConfigCoroutine()
val addDeviceResponse = server.api.createFamilyByMailToken(
mailToken = mailAuthToken!!,
parentPassword = ParentPassword(
parentPasswordHash = auth.firstPasswordHash,
parentPasswordSecondHash = auth.secondPasswordHash,
parentPasswordSecondSalt = currentConfig.users.find { it.id == auth.userId }!!.secondPasswordSalt
),
parentDevice = NewDeviceInfo(model = currentConfig.device.model),
deviceName = currentConfig.device.name,
parentName = parentFirstName,
timeZone = logic.timeApi.getSystemTimeZone().id
)
// sync from server
val clientStatusResponse = server.api.pullChanges(addDeviceResponse.deviceAuthToken, ClientDataStatus.empty)
val authentication = Threads.database.executeAndWait {
logic.database.transaction().use { transaction ->
val customServerUrl = logic.database.config().getCustomServerUrlSync()
val database = logic.database
database.deleteAllData()
database.config().setCustomServerUrlSync(customServerUrl)
database.config().setOwnDeviceIdSync(addDeviceResponse.ownDeviceId)
database.config().setDeviceAuthTokenSync(addDeviceResponse.deviceAuthToken)
ApplyServerDataStatus.applyServerDataStatusSync(clientStatusResponse, logic.database, logic.platformIntegration)
val newParentUser = database.user().getParentUsersSync().first()
val newParentUserAuth = ApplyActionParentPasswordAuthentication(
parentUserId = newParentUser.id,
secondPasswordHash = auth.secondPasswordHash
)
transaction.setSuccess()
newParentUserAuth
}
}
currentConfig.apply(authentication, logic, addDeviceResponse.ownDeviceId)
DatabaseBackup.with(getApplication()).tryCreateDatabaseBackupAsync()
statusInternal.value = DoneMigrationStatus
} catch (ex: Exception) {
if (BuildConfig.DEBUG) {
Log.w(LOG_TAG, "error migration to connected mode", ex)
}
Toast.makeText(getApplication(), R.string.error_general, Toast.LENGTH_SHORT).show()
statusInternal.value = LeaveScreenMigrationStatus
}
}
}
}
}
sealed class MigrateToConnectedModeStatus
object WaitingForAuthMigrationStatus: MigrateToConnectedModeStatus()
object LeaveScreenMigrationStatus: MigrateToConnectedModeStatus()
object WaitingForConfirmationByParentMigrationStatus: MigrateToConnectedModeStatus()
object ConflictAlreadyHasAccountMigrationStatus: MigrateToConnectedModeStatus()
object WorkingMigrationStatus: MigrateToConnectedModeStatus()
object DoneMigrationStatus: MigrateToConnectedModeStatus()

View file

@ -0,0 +1,222 @@
/*
* TimeLimit Copyright <C> 2019 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.timelimit.android.ui.migrate_to_connected
import android.util.Log
import io.timelimit.android.BuildConfig
import io.timelimit.android.data.Database
import io.timelimit.android.data.model.*
import io.timelimit.android.logic.AppLogic
import io.timelimit.android.sync.actions.*
import io.timelimit.android.sync.actions.apply.ApplyActionParentPasswordAuthentication
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
data class OfflineModeStatus(
val users: List<User>,
val categories: List<Category>,
val categoryApps: List<CategoryApp>,
val device: Device,
val rules: List<TimeLimitRule>,
val usedTimes: List<UsedTimeItem>
) {
companion object {
private const val LOG_TAG = "OfflineModeStatus"
fun query(database: Database): OfflineModeStatus = OfflineModeStatus(
users = database.user().getAllUsersSync(),
categories = database.category().getAllCategoriesSync(),
categoryApps = database.categoryApp().getAllCategoryAppSync(),
device = database.device().getDeviceByIdSync(database.config().getOwnDeviceIdSync()!!)!!,
rules = database.timeLimitRules().getAllRulesSync(),
usedTimes = database.usedTimes().getAllUsedTimeItemsSync()
)
}
// limitations:
// - child passwords are lost
// - all parent users except the migrating one are lost
suspend fun apply(
authentication: ApplyActionParentPasswordAuthentication,
appLogic: AppLogic,
newDeviceId: String
) {
suspend fun apply(action: ParentAction) {
try {
ApplyActionUtil.applyParentAction(
action = action,
database = appLogic.database,
authentication = authentication,
platformIntegration = appLogic.platformIntegration,
syncUtil = appLogic.syncUtil
)
} catch (ex: Exception) {
if (BuildConfig.DEBUG) {
Log.w(LOG_TAG, "could not apply action $action", ex)
}
}
}
suspend fun apply(action: AppLogicAction) {
try {
ApplyActionUtil.applyAppLogicAction(
action = action,
appLogic = appLogic
)
} catch (ex: Exception) {
if (BuildConfig.DEBUG) {
Log.w(LOG_TAG, "could not apply action $action", ex)
}
}
}
// create child users
users.filter { it.type == UserType.Child }.forEach { child ->
apply(
AddUserAction(
name = child.name,
timeZone = child.timeZone,
userId = child.id,
userType = UserType.Child,
password = null
)
)
// disable limits until
if (child.disableLimitsUntil != 0L) {
apply(
SetUserDisableLimitsUntilAction(
childId = child.id,
timestamp = child.disableLimitsUntil
)
)
}
// create categories
val childCategories = categories.filter { it.childId == child.id }
childCategories.forEach { category ->
apply(
CreateCategoryAction(
childId = child.id,
categoryId = category.id,
title = category.title
)
)
if (!category.blockedMinutesInWeek.dataNotToModify.isEmpty) {
apply(
UpdateCategoryBlockedTimesAction(
categoryId = category.id,
blockedTimes = category.blockedMinutesInWeek
)
)
}
if (category.extraTimeInMillis != 0L) {
apply(
SetCategoryExtraTimeAction(
categoryId = category.id,
newExtraTime = category.extraTimeInMillis
)
)
}
if (category.temporarilyBlocked) {
apply(
UpdateCategoryTemporarilyBlockedAction(
categoryId = category.id,
blocked = true
)
)
}
// add category apps
val thisCategoryApps = categoryApps.filter { it.categoryId == category.id }
if (thisCategoryApps.isNotEmpty()) {
apply(AddCategoryAppsAction(
categoryId = category.id,
packageNames = thisCategoryApps.map { it.packageName }
))
}
// add used times
val thisUsedTimes = usedTimes.filter { it.categoryId == category.id }
thisUsedTimes.forEach { usedTime ->
apply(AddUsedTimeAction(
categoryId = category.id,
extraTimeToSubtract = 0,
dayOfEpoch = usedTime.dayOfEpoch,
timeToAdd = usedTime.usedMillis.toInt()
))
}
// add time limit rules
val thisRules = rules.filter { it.categoryId == category.id }
thisRules.forEach { rule -> apply(CreateTimeLimitRuleAction(rule)) }
}
// parent categories
childCategories.forEach { category ->
if (category.parentCategoryId != "") {
apply(SetParentCategory(
categoryId = category.id,
parentCategory = category.parentCategoryId
))
}
}
// category for not assigned apps
if (child.categoryForNotAssignedApps != "") {
apply(SetCategoryForUnassignedApps(
childId = child.id,
categoryId = child.categoryForNotAssignedApps
))
}
}
// update device config
if (users.find { it.type == UserType.Child && it.id == device.currentUserId } != null) {
apply(SetDeviceUserAction(
deviceId = newDeviceId,
userId = device.currentUserId
))
}
apply(UpdateNetworkTimeVerificationAction(
deviceId = newDeviceId,
mode = device.networkTime
))
if (users.find { it.type == UserType.Child && it.id == device.defaultUser } != null) {
apply(SetDeviceDefaultUserAction(
deviceId = newDeviceId,
defaultUserId = device.defaultUser
))
}
apply(SetDeviceDefaultUserTimeoutAction(
deviceId = newDeviceId,
timeout = device.defaultUserTimeout
))
if (device.considerRebootManipulation) {
apply(SetConsiderRebootManipulationAction(
deviceId = newDeviceId,
considerRebootManipulation = true
))
}
}
}

View file

@ -40,10 +40,13 @@ import io.timelimit.android.ui.main.ActivityViewModelHolder
import io.timelimit.android.ui.main.AuthenticationFab
import io.timelimit.android.ui.manage.device.add.AddDeviceFragment
import io.timelimit.android.ui.overview.about.AboutFragmentParentHandlers
import io.timelimit.android.ui.overview.overview.CanNotAddDevicesInLocalModeDialogFragmentListener
import io.timelimit.android.ui.overview.overview.OverviewFragmentParentHandlers
import kotlinx.android.synthetic.main.fragment_main.*
class MainFragment : Fragment(), OverviewFragmentParentHandlers, AboutFragmentParentHandlers {
class MainFragment : Fragment(), OverviewFragmentParentHandlers, AboutFragmentParentHandlers,
CanNotAddDevicesInLocalModeDialogFragmentListener {
private val adapter: PagerAdapter by lazy { PagerAdapter(childFragmentManager) }
private val logic: AppLogic by lazy { DefaultAppLogic.with(context!!) }
private lateinit var navigation: NavController
@ -205,4 +208,11 @@ class MainFragment : Fragment(), OverviewFragmentParentHandlers, AboutFragmentPa
R.id.overviewFragment
)
}
override fun migrateToConnectedMode() {
navigation.safeNavigate(
MainFragmentDirections.actionOverviewFragmentToMigrateToConnectedModeFragment(),
R.id.overviewFragment
)
}
}

View file

@ -20,6 +20,7 @@ import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentManager
import io.timelimit.android.BuildConfig
import io.timelimit.android.R
import io.timelimit.android.extensions.showSafe
@ -32,7 +33,22 @@ class CanNotAddDevicesInLocalModeDialogFragment: DialogFragment() {
return AlertDialog.Builder(context!!, theme)
.setTitle(R.string.overview_add_device)
.setMessage(R.string.overview_add_error_local_mode)
.setPositiveButton(R.string.generic_ok, null)
.apply {
if (BuildConfig.hasServer) {
setNegativeButton(R.string.generic_cancel, null)
setPositiveButton(R.string.overview_add_device_migrate_to_connected) { _, _ ->
dismiss()
targetFragment.let { target ->
if (target is CanNotAddDevicesInLocalModeDialogFragmentListener) {
target.migrateToConnectedMode()
}
}
}
} else {
setPositiveButton(R.string.generic_ok, null)
}
}
.create()
}
@ -40,3 +56,7 @@ class CanNotAddDevicesInLocalModeDialogFragment: DialogFragment() {
showSafe(manager, DIALOG_TAG)
}
}
interface CanNotAddDevicesInLocalModeDialogFragmentListener {
fun migrateToConnectedMode()
}

View file

@ -39,7 +39,7 @@ import io.timelimit.android.ui.main.getActivityViewModel
import kotlinx.android.synthetic.main.fragment_overview.*
import kotlinx.coroutines.launch
class OverviewFragment : CoroutineFragment() {
class OverviewFragment : CoroutineFragment(), CanNotAddDevicesInLocalModeDialogFragmentListener {
private val handlers: OverviewFragmentParentHandlers by lazy { parentFragment as OverviewFragmentParentHandlers }
private val logic: AppLogic by lazy { DefaultAppLogic.with(context!!) }
private val auth: ActivityViewModel by lazy { getActivityViewModel(activity!!) }
@ -78,7 +78,9 @@ class OverviewFragment : CoroutineFragment() {
override fun onAddDeviceClicked() {
launch {
if (logic.database.config().getDeviceAuthTokenAsync().waitForNonNullValue().isEmpty()) {
CanNotAddDevicesInLocalModeDialogFragment().show(fragmentManager!!)
CanNotAddDevicesInLocalModeDialogFragment()
.apply { setTargetFragment(this@OverviewFragment, 0) }
.show(fragmentManager!!)
} else if (auth.requestAuthenticationOrReturnTrue()) {
handlers.openAddDeviceScreen()
}
@ -114,9 +116,13 @@ class OverviewFragment : CoroutineFragment() {
}
).attachToRecyclerView(recycler)
}
override fun migrateToConnectedMode() {
handlers.migrateToConnectedMode()
}
}
interface OverviewFragmentParentHandlers {
interface OverviewFragmentParentHandlers: CanNotAddDevicesInLocalModeDialogFragmentListener {
fun openAddUserScreen()
fun openAddDeviceScreen()
fun openManageDeviceScreen(deviceId: String)

View file

@ -22,7 +22,7 @@
type="String" />
<variable
name="usingDefaultServer"
name="showSignInWithGoogleButton"
type="boolean" />
<import type="android.text.TextUtils" />
@ -45,7 +45,7 @@
android:layout_height="wrap_content">
<LinearLayout
android:visibility="@{(usingDefaultServer &amp;&amp; BuildConfig.useGoogleApis &amp;&amp; TextUtils.isEmpty(mailAddressToWhichCodeWasSent)) ? View.VISIBLE : View.GONE}"
android:visibility="@{(showSignInWithGoogleButton &amp;&amp; BuildConfig.useGoogleApis &amp;&amp; TextUtils.isEmpty(mailAddressToWhichCodeWasSent)) ? View.VISIBLE : View.GONE}"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">

View file

@ -0,0 +1,23 @@
<!--
TimeLimit Copyright <C> 2019 Jonas Lochmann
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:padding="16dp"
android:textAppearance="?android:textAppearanceLarge"
android:gravity="center"
android:text="@string/migrate_to_connected_mode_done"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="io.timelimit.android.ui.migrate_to_connected.MigrateToConnectedModeFragment" />

View file

@ -0,0 +1,107 @@
<!--
TimeLimit Copyright <C> 2019 Jonas Lochmann
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context="io.timelimit.android.ui.migrate_to_connected.MigrateToConnectedModeFragment">
<data>
<variable
name="isParentSignedIn"
type="boolean" />
<import type="android.view.View" />
</data>
<io.timelimit.android.ui.view.SafeViewFlipper
android:id="@+id/flipper"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:id="@+id/scroll"
android:orientation="vertical"
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">
<androidx.cardview.widget.CardView
app:cardUseCompatPadding="true"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:orientation="vertical"
android:padding="8dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:textAppearance="?android:textAppearanceLarge"
android:text="@string/migrate_to_connected_mode_title"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:textAppearance="?android:textAppearanceMedium"
android:text="@string/migrate_to_connected_mode_info"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:visibility="@{isParentSignedIn ? View.GONE : View.VISIBLE}"
android:id="@+id/parent_auth_button"
android:background="?selectableItemBackground"
android:text="@string/add_user_authentication_required_btn"
android:layout_gravity="end"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<EditText
android:visibility="@{isParentSignedIn ? View.VISIBLE : View.GONE}"
android:id="@+id/parent_name"
android:inputType="textPersonName"
android:hint="@string/setup_parent_mode_field_name_hint"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:visibility="@{isParentSignedIn ? View.VISIBLE : View.GONE}"
android:id="@+id/go_button"
android:background="?selectableItemBackground"
android:text="@string/generic_go"
android:layout_gravity="end"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
</ScrollView>
<FrameLayout
android:id="@+id/mail_auth_container"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<include layout="@layout/circular_progress_indicator" />
<include layout="@layout/migrate_to_connected_mode_not_possible_due_to_existing_account" />
<include layout="@layout/migrate_to_connected_mode_done" />
</io.timelimit.android.ui.view.SafeViewFlipper>
</layout>

View file

@ -0,0 +1,23 @@
<!--
TimeLimit Copyright <C> 2019 Jonas Lochmann
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:padding="16dp"
android:textAppearance="?android:textAppearanceLarge"
android:gravity="center"
android:text="@string/migrate_to_connected_mode_other_account_linked"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="io.timelimit.android.ui.migrate_to_connected.MigrateToConnectedModeFragment" />

View file

@ -80,6 +80,13 @@
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_overviewFragment_to_migrateToConnectedModeFragment"
app:destination="@id/migrateToConnectedModeFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
</fragment>
<fragment
android:id="@+id/manageChildFragment"
@ -299,4 +306,9 @@
android:name="io.timelimit.android.ui.diagnose.DiagnoseSyncFragment"
android:label="diagnose_sync_fragment"
tools:layout="@layout/diagnose_sync_fragment" />
<fragment
android:id="@+id/migrateToConnectedModeFragment"
android:name="io.timelimit.android.ui.migrate_to_connected.MigrateToConnectedModeFragment"
android:label="migrate_to_connected_mode_fragment"
tools:layout="@layout/migrate_to_connected_mode_fragment" />
</navigation>

View file

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
TimeLimit Copyright <C> 2019 Jonas Lochmann
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<resources>
<string name="migrate_to_connected_mode_title">Migration zum vernetzten Modus</string>
<string name="migrate_to_connected_mode_info">Wenn Sie fortfahren, dann gehen die Passwörter von Kind-Benutzern und alle Elternbenutzer (außer dem, der sich jetzt anmeldet) verloren.</string>
<string name="migrate_to_connected_mode_other_account_linked">
Es gibt bereits ein Konto mit dieser E-Mail-Adresse.
Deshalb kann es nicht für eine Migration in den vernetzten Modus verwendet werden.
</string>
<string name="migrate_to_connected_mode_done">
Die Migration zum vernetzten Modus ist abgeschlossen. Sie können sich am anderen
Gerät mit der selben E-Mail-Adresse anmelden oder die Geräte-Hinzufüge-Funktion
an diesem Gerät verwenden.
Betätigen Sie die Zurück-Taste, um zur Übersicht zurückzukehren.
</string>
</resources>

View file

@ -21,6 +21,7 @@
Sie haben TimeLimit im lokalen Modus eingerichtet.
In diesem gibt es keine Vernetzung, sodass Sie kein Gerät hinzufügen können.
</string>
<string name="overview_add_device_migrate_to_connected">Zum vernetzten Modus migrieren</string>
<string name="overview_intro_title">Willkommen bei TimeLimit</string>
<string name="overview_intro_text">

View file

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
TimeLimit Copyright <C> 2019 Jonas Lochmann
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<resources>
<string name="migrate_to_connected_mode_title">Migrate to the connected mode</string>
<string name="migrate_to_connected_mode_info">When using this, password of child users and parent users other than the one who signs in for this process are lost.</string>
<string name="migrate_to_connected_mode_other_account_linked">
There is already an account with this mail address. Due to that,
migrating to the connected mode is not possible.
</string>
<string name="migrate_to_connected_mode_done">
The migration to the connected mode is done. You can sign in at your device
using the same mail address or use the add device procedure from this device.
Press back to go to the overview.
</string>
</resources>

View file

@ -21,6 +21,7 @@
You have set up TimeLimit in the local mode.
There is no linking, therefore you can not add devices.
</string>
<string name="overview_add_device_migrate_to_connected">Migrate to the connected mode</string>
<string name="overview_intro_title">Welcome to TimeLimit</string>
<string name="overview_intro_text">