diff --git a/app/src/main/java/io/timelimit/android/data/dao/CategoryAppDao.kt b/app/src/main/java/io/timelimit/android/data/dao/CategoryAppDao.kt index 983ce13..1b6f487 100644 --- a/app/src/main/java/io/timelimit/android/data/dao/CategoryAppDao.kt +++ b/app/src/main/java/io/timelimit/android/data/dao/CategoryAppDao.kt @@ -46,4 +46,7 @@ abstract class CategoryAppDao { @Query("SELECT * FROM category_app LIMIT :pageSize OFFSET :offset") abstract fun getCategoryAppPageSync(offset: Int, pageSize: Int): List + + @Query("SELECT * FROM category_app") + abstract fun getAllCategoryAppSync(): List } diff --git a/app/src/main/java/io/timelimit/android/data/dao/CategoryDao.kt b/app/src/main/java/io/timelimit/android/data/dao/CategoryDao.kt index 6f51131..35e0fc6 100644 --- a/app/src/main/java/io/timelimit/android/data/dao/CategoryDao.kt +++ b/app/src/main/java/io/timelimit/android/data/dao/CategoryDao.kt @@ -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 } data class CategoryWithVersionNumbers( diff --git a/app/src/main/java/io/timelimit/android/data/dao/TimeLimitRuleDao.kt b/app/src/main/java/io/timelimit/android/data/dao/TimeLimitRuleDao.kt index 7d821e9..8b0bc3b 100644 --- a/app/src/main/java/io/timelimit/android/data/dao/TimeLimitRuleDao.kt +++ b/app/src/main/java/io/timelimit/android/data/dao/TimeLimitRuleDao.kt @@ -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 + + @Query("SELECT * FROM time_limit_rule") + abstract fun getAllRulesSync(): List } diff --git a/app/src/main/java/io/timelimit/android/data/dao/UsedTimeDao.kt b/app/src/main/java/io/timelimit/android/data/dao/UsedTimeDao.kt index 7681aab..86864a1 100644 --- a/app/src/main/java/io/timelimit/android/data/dao/UsedTimeDao.kt +++ b/app/src/main/java/io/timelimit/android/data/dao/UsedTimeDao.kt @@ -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, startingDayOfEpoch: Int, endDayOfEpoch: Int): LiveData> + + @Query("SELECT * FROM used_time") + abstract fun getAllUsedTimeItemsSync(): List } diff --git a/app/src/main/java/io/timelimit/android/ui/authentication/AuthenticateByMailFragment.kt b/app/src/main/java/io/timelimit/android/ui/authentication/AuthenticateByMailFragment.kt index 97b0d2f..44d15bc 100644 --- a/app/src/main/java/io/timelimit/android/ui/authentication/AuthenticateByMailFragment.kt +++ b/app/src/main/java/io/timelimit/android/ui/authentication/AuthenticateByMailFragment.kt @@ -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 { diff --git a/app/src/main/java/io/timelimit/android/ui/main/ActivityViewModel.kt b/app/src/main/java/io/timelimit/android/ui/main/ActivityViewModel.kt index b465122..a3a180a 100644 --- a/app/src/main/java/io/timelimit/android/ui/main/ActivityViewModel.kt +++ b/app/src/main/java/io/timelimit/android/ui/main/ActivityViewModel.kt @@ -166,6 +166,8 @@ class ActivityViewModel(application: Application): AndroidViewModel(application) authenticatedUserMetadata.value = user } + fun getAuthenticatedUser() = authenticatedUserMetadata.value + fun logOut() { authenticatedUserMetadata.value = null } diff --git a/app/src/main/java/io/timelimit/android/ui/migrate_to_connected/MigrateToConnectedModeFragment.kt b/app/src/main/java/io/timelimit/android/ui/migrate_to_connected/MigrateToConnectedModeFragment.kt new file mode 100644 index 0000000..0c50744 --- /dev/null +++ b/app/src/main/java/io/timelimit/android/ui/migrate_to_connected/MigrateToConnectedModeFragment.kt @@ -0,0 +1,105 @@ +/* + * TimeLimit Copyright 2019 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package io.timelimit.android.ui.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) + } +} diff --git a/app/src/main/java/io/timelimit/android/ui/migrate_to_connected/MigrateToConnectedModeModel.kt b/app/src/main/java/io/timelimit/android/ui/migrate_to_connected/MigrateToConnectedModeModel.kt new file mode 100644 index 0000000..8bc4d22 --- /dev/null +++ b/app/src/main/java/io/timelimit/android/ui/migrate_to_connected/MigrateToConnectedModeModel.kt @@ -0,0 +1,189 @@ +/* + * TimeLimit Copyright 2019 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package io.timelimit.android.ui.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().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() \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/migrate_to_connected/OfflineModeStatus.kt b/app/src/main/java/io/timelimit/android/ui/migrate_to_connected/OfflineModeStatus.kt new file mode 100644 index 0000000..aac7b6d --- /dev/null +++ b/app/src/main/java/io/timelimit/android/ui/migrate_to_connected/OfflineModeStatus.kt @@ -0,0 +1,222 @@ +/* + * TimeLimit Copyright 2019 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package io.timelimit.android.ui.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, + val categories: List, + val categoryApps: List, + val device: Device, + val rules: List, + val usedTimes: List +) { + 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 + )) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/overview/main/MainFragment.kt b/app/src/main/java/io/timelimit/android/ui/overview/main/MainFragment.kt index 6e010e3..4ea4ef2 100644 --- a/app/src/main/java/io/timelimit/android/ui/overview/main/MainFragment.kt +++ b/app/src/main/java/io/timelimit/android/ui/overview/main/MainFragment.kt @@ -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 + ) + } } diff --git a/app/src/main/java/io/timelimit/android/ui/overview/overview/CanNotAddDevicesInLocalModeDialogFragment.kt b/app/src/main/java/io/timelimit/android/ui/overview/overview/CanNotAddDevicesInLocalModeDialogFragment.kt index b867b0b..2cec0a7 100644 --- a/app/src/main/java/io/timelimit/android/ui/overview/overview/CanNotAddDevicesInLocalModeDialogFragment.kt +++ b/app/src/main/java/io/timelimit/android/ui/overview/overview/CanNotAddDevicesInLocalModeDialogFragment.kt @@ -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() +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/overview/overview/OverviewFragment.kt b/app/src/main/java/io/timelimit/android/ui/overview/overview/OverviewFragment.kt index 2c01f38..5028ea4 100644 --- a/app/src/main/java/io/timelimit/android/ui/overview/overview/OverviewFragment.kt +++ b/app/src/main/java/io/timelimit/android/ui/overview/overview/OverviewFragment.kt @@ -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) diff --git a/app/src/main/res/layout/fragment_authenticate_by_mail.xml b/app/src/main/res/layout/fragment_authenticate_by_mail.xml index e644ca0..e7627b2 100644 --- a/app/src/main/res/layout/fragment_authenticate_by_mail.xml +++ b/app/src/main/res/layout/fragment_authenticate_by_mail.xml @@ -22,7 +22,7 @@ type="String" /> @@ -45,7 +45,7 @@ android:layout_height="wrap_content"> diff --git a/app/src/main/res/layout/migrate_to_connected_mode_done.xml b/app/src/main/res/layout/migrate_to_connected_mode_done.xml new file mode 100644 index 0000000..926d5af --- /dev/null +++ b/app/src/main/res/layout/migrate_to_connected_mode_done.xml @@ -0,0 +1,23 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/migrate_to_connected_mode_fragment.xml b/app/src/main/res/layout/migrate_to_connected_mode_fragment.xml new file mode 100644 index 0000000..af355ef --- /dev/null +++ b/app/src/main/res/layout/migrate_to_connected_mode_fragment.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + +