Add Jetpack Compose

This commit is contained in:
Jonas Lochmann 2023-02-06 01:00:00 +01:00
parent 16d23273f5
commit 19aa402014
No known key found for this signature in database
GPG key ID: 8B8C9AEE10FA5B36
75 changed files with 2545 additions and 2276 deletions

View file

@ -48,6 +48,7 @@ android {
buildConfigField 'int', 'minimumRecommendServerVersion', '5' buildConfigField 'int', 'minimumRecommendServerVersion', '5'
} }
buildFeatures { buildFeatures {
compose true
viewBinding true viewBinding true
} }
@ -151,6 +152,10 @@ android {
kotlinOptions { kotlinOptions {
jvmTarget = "1.8" jvmTarget = "1.8"
} }
composeOptions {
kotlinCompilerExtensionVersion = "1.4.0-alpha02"
}
} }
wire { wire {
@ -170,6 +175,9 @@ dependencies {
implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.gridlayout:gridlayout:1.0.0' implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation "com.google.android.material:material:1.7.0" implementation "com.google.android.material:material:1.7.0"
implementation 'androidx.compose.material:material:1.3.1'
implementation 'androidx.activity:activity-compose:1.6.1'
implementation 'androidx.compose.material:material-icons-extended:1.3.1'
implementation 'androidx.fragment:fragment-ktx:1.5.5' implementation 'androidx.fragment:fragment-ktx:1.5.5'
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -21,6 +21,7 @@ import androidx.room.*
import io.timelimit.android.data.model.ChildTask import io.timelimit.android.data.model.ChildTask
import io.timelimit.android.data.model.derived.ChildTaskWithCategoryTitle import io.timelimit.android.data.model.derived.ChildTaskWithCategoryTitle
import io.timelimit.android.data.model.derived.FullChildTask import io.timelimit.android.data.model.derived.FullChildTask
import kotlinx.coroutines.flow.Flow
@Dao @Dao
interface ChildTaskDao { interface ChildTaskDao {
@ -45,9 +46,12 @@ interface ChildTaskDao {
@Query("SELECT child_task.* FROM child_task JOIN category ON (child_task.category_id = category.id) WHERE category.child_id = :userId") @Query("SELECT child_task.* FROM child_task JOIN category ON (child_task.category_id = category.id) WHERE category.child_id = :userId")
fun getTasksByUserIdSync(userId: String): List<ChildTask> fun getTasksByUserIdSync(userId: String): List<ChildTask>
@Query("SELECT child_task.*, category.title as category_title, user.name as child_name, user.timezone AS child_timezone FROM child_task JOIN category ON (child_task.category_id = category.id) JOIN user ON (category.child_id = user.id) WHERE child_task.pending_request = 1") @Query("SELECT child_task.*, category.title as category_title, user.id AS child_id, user.name as child_name, user.timezone AS child_timezone FROM child_task JOIN category ON (child_task.category_id = category.id) JOIN user ON (category.child_id = user.id) WHERE child_task.pending_request = 1")
fun getPendingTasks(): LiveData<List<FullChildTask>> fun getPendingTasks(): LiveData<List<FullChildTask>>
@Query("SELECT child_task.*, category.title as category_title, user.id AS child_id, user.name as child_name, user.timezone AS child_timezone FROM child_task JOIN category ON (child_task.category_id = category.id) JOIN user ON (category.child_id = user.id) WHERE child_task.pending_request = 1")
fun getPendingTasksFlow(): Flow<List<FullChildTask>>
@Query("SELECT * FROM child_task WHERE category_id = :categoryId") @Query("SELECT * FROM child_task WHERE category_id = :categoryId")
fun getTasksByCategoryId(categoryId: String): LiveData<List<ChildTask>> fun getTasksByCategoryId(categoryId: String): LiveData<List<ChildTask>>

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -32,6 +32,8 @@ import io.timelimit.android.livedata.ignoreUnchanged
import io.timelimit.android.livedata.map import io.timelimit.android.livedata.map
import io.timelimit.android.sync.network.ServerDhKey import io.timelimit.android.sync.network.ServerDhKey
import io.timelimit.android.update.UpdateStatus import io.timelimit.android.update.UpdateStatus
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import java.io.StringWriter import java.io.StringWriter
@Dao @Dao
@ -59,6 +61,13 @@ abstract class ConfigDao {
@Query("SELECT * FROM config WHERE id = :key") @Query("SELECT * FROM config WHERE id = :key")
protected abstract suspend fun getRowCoroutine(key: ConfigurationItemType): ConfigurationItem? protected abstract suspend fun getRowCoroutine(key: ConfigurationItemType): ConfigurationItem?
@Query("SELECT * FROM config WHERE id = :key")
protected abstract fun getRowFlow(key: ConfigurationItemType): Flow<ConfigurationItem?>
private fun getValueOfKeyFlow(key: ConfigurationItemType): Flow<String?> {
return getRowFlow(key).map { it?.value }
}
private suspend fun getValueOfKeyCoroutine(key: ConfigurationItemType): String? { private suspend fun getValueOfKeyCoroutine(key: ConfigurationItemType): String? {
return getRowCoroutine(key)?.value return getRowCoroutine(key)?.value
} }
@ -81,6 +90,10 @@ abstract class ConfigDao {
return getValueOfKeyAsync(ConfigurationItemType.OwnDeviceId) return getValueOfKeyAsync(ConfigurationItemType.OwnDeviceId)
} }
fun getOwnDeviceIdFlow(): Flow<String?> {
return getValueOfKeyFlow(ConfigurationItemType.OwnDeviceId)
}
fun getOwnDeviceIdSync(): String? { fun getOwnDeviceIdSync(): String? {
return getValueOfKeySync(ConfigurationItemType.OwnDeviceId) return getValueOfKeySync(ConfigurationItemType.OwnDeviceId)
} }
@ -213,6 +226,7 @@ abstract class ConfigDao {
fun setServerMessage(message: String?) = updateValueSync(ConfigurationItemType.ServerMessage, message ?: "") fun setServerMessage(message: String?) = updateValueSync(ConfigurationItemType.ServerMessage, message ?: "")
fun getServerMessage() = getValueOfKeyAsync(ConfigurationItemType.ServerMessage).map { if (it.isNullOrBlank()) null else it } fun getServerMessage() = getValueOfKeyAsync(ConfigurationItemType.ServerMessage).map { if (it.isNullOrBlank()) null else it }
fun getServerMessageFlow() = getValueOfKeyFlow(ConfigurationItemType.ServerMessage).map { if (it.isNullOrBlank()) null else it }
fun getCustomServerUrlSync() = getValueOfKeySync(ConfigurationItemType.CustomServerUrl) ?: "" fun getCustomServerUrlSync() = getValueOfKeySync(ConfigurationItemType.CustomServerUrl) ?: ""
fun getCustomServerUrlAsync() = getValueOfKeyAsync(ConfigurationItemType.CustomServerUrl).map { it ?: "" } fun getCustomServerUrlAsync() = getValueOfKeyAsync(ConfigurationItemType.CustomServerUrl).map { it ?: "" }

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -21,13 +21,15 @@ import io.timelimit.android.data.model.*
import io.timelimit.android.integration.platform.NewPermissionStatusConverter import io.timelimit.android.integration.platform.NewPermissionStatusConverter
import io.timelimit.android.integration.platform.ProtectionLevelConverter import io.timelimit.android.integration.platform.ProtectionLevelConverter
import io.timelimit.android.integration.platform.RuntimePermissionStatusConverter import io.timelimit.android.integration.platform.RuntimePermissionStatusConverter
import kotlinx.coroutines.flow.Flow
@Dao @Dao
@TypeConverters( @TypeConverters(
NetworkTimeAdapter::class, NetworkTimeAdapter::class,
ProtectionLevelConverter::class, ProtectionLevelConverter::class,
RuntimePermissionStatusConverter::class, RuntimePermissionStatusConverter::class,
NewPermissionStatusConverter::class NewPermissionStatusConverter::class,
UserTypeConverter::class
) )
abstract class DeviceDao { abstract class DeviceDao {
@Query("SELECT * FROM device WHERE id = :deviceId") @Query("SELECT * FROM device WHERE id = :deviceId")
@ -45,6 +47,9 @@ abstract class DeviceDao {
@Query("SELECT * FROM device") @Query("SELECT * FROM device")
abstract fun getAllDevicesSync(): List<Device> abstract fun getAllDevicesSync(): List<Device>
@Query("SELECT device.*, user.name AS current_user_name, user.type AS current_user_type FROM device LEFT JOIN user ON (user.id = device.current_user_id)")
abstract fun getAllDevicesWithUserInfoFlow(): Flow<List<DeviceWithUserInfo>>
@Insert @Insert
abstract fun addDeviceSync(device: Device) abstract fun addDeviceSync(device: Device)
@ -116,4 +121,13 @@ data class DeviceDetailDataBase(
val appBaseVersion: String?, val appBaseVersion: String?,
@ColumnInfo(name = "app_diff_version") @ColumnInfo(name = "app_diff_version")
val appDiffVersion: String? val appDiffVersion: String?
)
data class DeviceWithUserInfo (
@Embedded
val device: Device,
@ColumnInfo(name = "current_user_name")
val currentUserName: String?,
@ColumnInfo(name = "current_user_type")
val currentUserType: UserType?
) )

View file

@ -21,6 +21,7 @@ import androidx.room.Insert
import androidx.room.Query import androidx.room.Query
import androidx.room.Update import androidx.room.Update
import io.timelimit.android.data.model.User import io.timelimit.android.data.model.User
import kotlinx.coroutines.flow.Flow
@Dao @Dao
abstract class UserDao { abstract class UserDao {
@ -45,6 +46,9 @@ abstract class UserDao {
@Query("SELECT * FROM user ORDER by type DESC, name ASC") @Query("SELECT * FROM user ORDER by type DESC, name ASC")
abstract fun getAllUsersLive(): LiveData<List<User>> abstract fun getAllUsersLive(): LiveData<List<User>>
@Query("SELECT * FROM user ORDER by type DESC, name ASC")
abstract fun getAllUsersFlow(): Flow<List<User>>
@Query("SELECT * FROM user") @Query("SELECT * FROM user")
abstract fun getAllUsersSync(): List<User> abstract fun getAllUsersSync(): List<User>

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -25,6 +25,8 @@ data class FullChildTask(
val childTask: ChildTask, val childTask: ChildTask,
@ColumnInfo(name = "category_title") @ColumnInfo(name = "category_title")
val categoryTitle: String, val categoryTitle: String,
@ColumnInfo(name = "child_id")
val childId: String,
@ColumnInfo(name = "child_name") @ColumnInfo(name = "child_name")
val childName: String, val childName: String,
@ColumnInfo(name = "child_timezone") @ColumnInfo(name = "child_timezone")

View file

@ -0,0 +1,38 @@
/*
* 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.extensions
import io.timelimit.android.util.Option
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
fun <T> Flow<T?>.takeWhileNotNull(): Flow<T> = this.transformWhile { value ->
if (value != null) {
emit(value)
true
} else false
}
@OptIn(ExperimentalCoroutinesApi::class)
fun <T> Flow<Boolean>.whileTrue(producer: suspend () -> Flow<T>): Flow<T> =
distinctUntilChanged()
.transformLatest {
if (it) emitAll(producer().map { Option.Some(it) })
else emit(null)
}
.takeWhileNotNull()
.map { it.value }

View file

@ -0,0 +1,11 @@
package io.timelimit.android.extensions
import androidx.fragment.app.FragmentContainerView
import androidx.fragment.app.FragmentManager
private val onContainerAvailable = FragmentManager::class.java.getDeclaredMethod(
"onContainerAvailable",
FragmentContainerView::class.java
).also { it.isAccessible = true }
fun FragmentManager.onContainerAvailable(view: FragmentContainerView) = onContainerAvailable.invoke(this, view)

View file

@ -0,0 +1,89 @@
/*
* 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
import android.util.Log
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import androidx.fragment.app.FragmentContainerView
import androidx.fragment.app.FragmentManager
import io.timelimit.android.BuildConfig
import io.timelimit.android.extensions.onContainerAvailable
import io.timelimit.android.ui.model.Screen
private const val LOG_TAG = "FragmentScreen"
@Composable
fun FragmentScreen(
screen: Screen.FragmentScreen,
fragmentManager: FragmentManager,
fragmentIds: MutableSet<Int>,
modifier: Modifier = Modifier
) {
AndroidView(
modifier = modifier.fillMaxSize(),
factory = { context ->
FragmentContainerView(context).also {
val containerId = screen.fragment.containerId
val fragment = fragmentManager.findFragmentById(containerId)
it.id = containerId
if (fragment == null) {
if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "connect $screen")
}
fragmentManager.beginTransaction()
.replace(
containerId,
screen.fragment.fragmentClass,
screen.fragment.arguments
)
.commitAllowingStateLoss()
} else {
if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "already bound $screen")
}
fragmentManager.beginTransaction()
.attach(fragment)
.commitAllowingStateLoss()
}
fragmentManager.onContainerAvailable(it)
fragmentIds.add(containerId)
}
}
)
DisposableEffect(true) {
onDispose {
if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "detach $screen")
}
fragmentManager.findFragmentById(screen.fragment.containerId)?.also { fragment ->
fragmentManager.beginTransaction()
.detach(fragment)
.commitAllowingStateLoss()
}
}
}
}

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -20,48 +20,61 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.SystemClock import android.os.SystemClock
import android.view.MenuItem import android.util.Log
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.DialogFragment import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.*
import androidx.lifecycle.Observer
import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModelProviders
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.fragment.NavHostFragment
import io.timelimit.android.Application import io.timelimit.android.Application
import io.timelimit.android.BuildConfig
import io.timelimit.android.R import io.timelimit.android.R
import io.timelimit.android.data.IdGenerator import io.timelimit.android.data.IdGenerator
import io.timelimit.android.data.model.UserType
import io.timelimit.android.extensions.showSafe import io.timelimit.android.extensions.showSafe
import io.timelimit.android.integration.platform.android.NotificationChannels import io.timelimit.android.integration.platform.android.NotificationChannels
import io.timelimit.android.livedata.ignoreUnchanged import io.timelimit.android.livedata.ignoreUnchanged
import io.timelimit.android.livedata.liveDataFromNullableValue import io.timelimit.android.livedata.liveDataFromNullableValue
import io.timelimit.android.livedata.map import io.timelimit.android.livedata.map
import io.timelimit.android.livedata.switchMap
import io.timelimit.android.logic.DefaultAppLogic import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.u2f.U2fManager import io.timelimit.android.u2f.U2fManager
import io.timelimit.android.u2f.protocol.U2FDevice import io.timelimit.android.u2f.protocol.U2FDevice
import io.timelimit.android.ui.animation.Transition
import io.timelimit.android.ui.login.AuthTokenLoginProcessor import io.timelimit.android.ui.login.AuthTokenLoginProcessor
import io.timelimit.android.ui.login.NewLoginFragment import io.timelimit.android.ui.login.NewLoginFragment
import io.timelimit.android.ui.main.ActivityViewModel import io.timelimit.android.ui.main.ActivityViewModel
import io.timelimit.android.ui.main.ActivityViewModelHolder import io.timelimit.android.ui.main.ActivityViewModelHolder
import io.timelimit.android.ui.main.AuthenticatedUser import io.timelimit.android.ui.main.AuthenticatedUser
import io.timelimit.android.ui.main.FragmentWithCustomTitle import io.timelimit.android.ui.main.FragmentWithCustomTitle
import io.timelimit.android.ui.manage.parent.ManageParentFragmentArgs import io.timelimit.android.ui.model.*
import io.timelimit.android.ui.payment.ActivityPurchaseModel import io.timelimit.android.ui.payment.ActivityPurchaseModel
import io.timelimit.android.ui.util.SyncStatusModel import io.timelimit.android.ui.util.SyncStatusModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import java.security.SecureRandom import java.security.SecureRandom
class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.DeviceFoundListener { class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.DeviceFoundListener, MainModelActivity {
companion object { companion object {
private const val LOG_TAG = "MainActivity"
private const val AUTH_DIALOG_TAG = "adt" private const val AUTH_DIALOG_TAG = "adt"
const val ACTION_USER_OPTIONS = "OPEN_USER_OPTIONS" const val ACTION_USER_OPTIONS = "OPEN_USER_OPTIONS"
const val EXTRA_USER_ID = "userId" const val EXTRA_USER_ID = "userId"
private const val EXTRA_AUTH_HANDOVER = "authHandover" private const val EXTRA_AUTH_HANDOVER = "authHandover"
private const val MAIN_MODEL_STATE = "mainModelState"
private const val FRAGMENT_IDS_STATE = "fragmentIds"
private var authHandover: Triple<Long, Long, AuthenticatedUser>? = null private var authHandover: Triple<Long, Long, AuthenticatedUser>? = null
@ -95,7 +108,8 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
} }
} }
private val currentNavigatorFragment = MutableLiveData<Fragment?>() private val mainModel by viewModels<MainModel>()
private var fragmentIds = mutableSetOf<Int>()
private val syncModel: SyncStatusModel by lazy { private val syncModel: SyncStatusModel by lazy {
ViewModelProviders.of(this).get(SyncStatusModel::class.java) ViewModelProviders.of(this).get(SyncStatusModel::class.java)
} }
@ -103,114 +117,162 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
override var ignoreStop: Boolean = false override var ignoreStop: Boolean = false
override val showPasswordRecovery: Boolean = true override val showPasswordRecovery: Boolean = true
@OptIn(ExperimentalAnimationApi::class)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
supportActionBar!!.hide()
U2fManager.setupActivity(this) U2fManager.setupActivity(this)
NotificationChannels.createNotificationChannels(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager, this) NotificationChannels.createNotificationChannels(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager, this)
if (savedInstanceState == null) { if (savedInstanceState != null) {
NavHostFragment.create(R.navigation.nav_graph).let { navhost -> mainModel.state.value = savedInstanceState.getSerializable(MAIN_MODEL_STATE) as State
supportFragmentManager.beginTransaction() fragmentIds.addAll(savedInstanceState.getIntegerArrayList(FRAGMENT_IDS_STATE) ?: emptyList())
.replace(R.id.nav_host, navhost)
.setPrimaryNavigationFragment(navhost)
.commitNow()
}
} }
// init the purchaseModel // init the purchaseModel
purchaseModel.getApplication<Application>() purchaseModel.getApplication<Application>()
// prepare livedata
val customTitle = currentNavigatorFragment.switchMap {
if (it != null && it is FragmentWithCustomTitle) {
it.getCustomTitle()
} else {
liveDataFromNullableValue(null as String?)
}
}.ignoreUnchanged()
val title = Transformations.map(customTitle) {
if (it == null) {
getString(R.string.app_name)
} else {
it
}
}
// up button
getNavController().addOnDestinationChangedListener(object: NavController.OnDestinationChangedListener {
override fun onDestinationChanged(controller: NavController, destination: NavDestination, arguments: Bundle?) {
supportActionBar!!.setDisplayHomeAsUpEnabled(controller.previousBackStackEntry != null)
}
})
// init if not yet done // init if not yet done
DefaultAppLogic.with(this) DefaultAppLogic.with(this)
val fragmentContainer = supportFragmentManager.findFragmentById(R.id.nav_host)!! val fragments = MutableStateFlow(emptyMap<Int, Fragment>())
val fragmentContainerManager = fragmentContainer.childFragmentManager
fragmentContainerManager.registerFragmentLifecycleCallbacks(object: FragmentManager.FragmentLifecycleCallbacks() { supportFragmentManager.registerFragmentLifecycleCallbacks(object: FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentStarted(fm: FragmentManager, f: Fragment) { override fun onFragmentStarted(fm: FragmentManager, f: Fragment) {
super.onFragmentStarted(fm, f) super.onFragmentStarted(fm, f)
if (!(f is DialogFragment)) { fragments.update {
currentNavigatorFragment.value = f it + Pair(f.id, f)
} }
} }
override fun onFragmentStopped(fm: FragmentManager, f: Fragment) { override fun onFragmentStopped(fm: FragmentManager, f: Fragment) {
super.onFragmentStopped(fm, f) super.onFragmentStopped(fm, f)
if (currentNavigatorFragment.value === f) { fragments.update {
currentNavigatorFragment.value = null it - f.id
} }
cleanupFragments()
} }
}, false) }, false)
title.observe(this, Observer { setTitle(it) })
syncModel.statusText.observe(this, Observer { supportActionBar!!.subtitle = it })
handleParameters(intent) handleParameters(intent)
val hasDeviceId = getActivityViewModel().logic.deviceId.map { it != null }.ignoreUnchanged() val hasDeviceId = getActivityViewModel().logic.deviceId.map { it != null }.ignoreUnchanged()
val hasParentKey = getActivityViewModel().logic.database.config().getParentModeKeyLive().map { it != null }.ignoreUnchanged() val hasParentKey = getActivityViewModel().logic.database.config().getParentModeKeyLive().map { it != null }.ignoreUnchanged()
hasDeviceId.observe(this) { hasDeviceId.observe(this) {
val rootDestination = getNavController().backQueue.getOrNull(1)?.destination?.id val rootDestination = mainModel.state.value.first()
if (!it) getActivityViewModel().logOut() if (!it) getActivityViewModel().logOut()
if ( if (
it && rootDestination != R.id.overviewFragment || it && rootDestination !is State.Overview ||
!it && rootDestination == R.id.overviewFragment !it && rootDestination is State.Overview
) { ) {
restartContent() restartContent()
} }
} }
hasParentKey.observe(this) { hasParentKey.observe(this) {
val rootDestination = getNavController().backQueue.getOrNull(1)?.destination?.id val rootDestination = mainModel.state.value.first()
if ( if (
it && rootDestination != R.id.parentModeFragment || it && rootDestination !is State.ParentMode ||
!it && rootDestination == R.id.parentModeFragment !it && rootDestination is State.ParentMode
) { ) {
restartContent() restartContent()
} }
} }
setContent {
Theme {
val screenLive by mainModel.screen.collectAsState(initial = null)
val subtitleLive by syncModel.statusText.asFlow().collectAsState(initial = null)
val isAuthenticated by getActivityViewModel().authenticatedUser
.map { it?.second?.type == UserType.Parent }
.asFlow().collectAsState(initial = false)
BackHandler(enabled = screenLive?.state?.previous != null) {
execute(UpdateStateCommand.BackToPreviousScreen)
}
updateTransition(
targetState = screenLive,
label = "AnimatedContent"
).AnimatedContent(
modifier = Modifier.background(Color.Black),
contentKey = { screen ->
when (screen) {
is Screen.FragmentScreen -> screen.fragment.containerId
is Screen.OverviewScreen -> "overview"
null -> null
}
},
transitionSpec = {
val from = initialState
val to = targetState
if (from == null || to == null) Transition.none
else {
val isOpening = to.state.hasPrevious(from.state)
val isClosing = from.state.hasPrevious(to.state)
if (isOpening) Transition.openScreen
else if (isClosing) Transition.closeScreen
else Transition.none
}
}
) { screen ->
val showAuthenticationDialog =
if (screen is ScreenWithAuthenticationFab && !isAuthenticated) ::showAuthenticationScreen
else null
val fragmentsLive by fragments.collectAsState()
val customTitle by when (screen) {
is Screen.FragmentScreen -> {
when (val fragment = fragmentsLive[screen.fragment.containerId]) {
is FragmentWithCustomTitle -> fragment.getCustomTitle()
else -> liveDataFromNullableValue(null)
}
}
else -> liveDataFromNullableValue(null)
}.asFlow().collectAsState(initial = null)
ScreenScaffold(
screen = screen,
title = customTitle ?: stringResource(R.string.app_name),
subtitle = subtitleLive,
executeCommand = ::execute,
content = { paddingValues ->
ScreenMultiplexer(
screen = screen,
executeCommand = ::execute,
fragmentManager = supportFragmentManager,
fragmentIds = fragmentIds,
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
)
},
showAuthenticationDialog = showAuthenticationDialog
)
}
}
}
} }
override fun onOptionsItemSelected(item: MenuItem) = when { override fun onSaveInstanceState(outState: Bundle) {
item.itemId == android.R.id.home -> { super.onSaveInstanceState(outState)
onBackPressed()
true outState.putSerializable(MAIN_MODEL_STATE, mainModel.state.value)
} outState.putIntegerArrayList(FRAGMENT_IDS_STATE, ArrayList(fragmentIds))
else -> super.onOptionsItemSelected(item)
} }
override fun onStart() { override fun onStart() {
@ -252,17 +314,7 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
val valid = userId != null && try { IdGenerator.assertIdValid(userId); true } catch (ex: IllegalArgumentException) {false} val valid = userId != null && try { IdGenerator.assertIdValid(userId); true } catch (ex: IllegalArgumentException) {false}
if (userId != null && valid) { if (userId != null && valid) {
getNavController().popBackStack(R.id.overviewFragment, true) execute(UpdateStateCommand.RecoverPassword(userId))
getNavController().handleDeepLink(
getNavController().createDeepLink()
.setDestination(R.id.manageParentFragment)
.setArguments(ManageParentFragmentArgs(userId).toBundle())
.createTaskStackBuilder()
.intents
.first()
)
return true
} }
} }
@ -277,47 +329,53 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
} }
if (handleParameters(intent)) return if (handleParameters(intent)) return
// at these screens, some users restart the App
// if they want to continue after opening the mail
// because they don't understand how to use the list of running Apps ...
// Due to that, on the relevant screens, the App does not
// go back to the start when opening it again
val isImportantScreen = when (getNavController().currentDestination?.id) {
R.id.setupParentModeFragment -> true
R.id.restoreParentPasswordFragment -> true
R.id.linkParentMailFragment -> true
else -> false
}
if (!isImportantScreen) restartContent()
} }
private fun restartContent() { private fun restartContent() {
while (getNavController().popBackStack()) {/* do nothing */} mainModel.execute(UpdateStateCommand.Reset)
getNavController().clearBackStack(R.id.launchFragment)
getNavController().navigate(R.id.launchFragment)
} }
override fun getActivityViewModel(): ActivityViewModel { override fun getActivityViewModel(): ActivityViewModel {
return ViewModelProviders.of(this).get(ActivityViewModel::class.java) return ViewModelProviders.of(this).get(ActivityViewModel::class.java)
} }
private fun getNavHostFragment(): NavHostFragment {
return supportFragmentManager.findFragmentById(R.id.nav_host) as NavHostFragment
}
private fun getNavController(): NavController {
return getNavHostFragment().navController
}
override fun showAuthenticationScreen() { override fun showAuthenticationScreen() {
if (supportFragmentManager.findFragmentByTag(AUTH_DIALOG_TAG) == null) { if (supportFragmentManager.findFragmentByTag(AUTH_DIALOG_TAG) == null) {
NewLoginFragment().showSafe(supportFragmentManager, AUTH_DIALOG_TAG) NewLoginFragment().showSafe(supportFragmentManager, AUTH_DIALOG_TAG)
} }
} }
private fun cleanupFragments() {
fragmentIds
.filter { fragmentId ->
var v = mainModel.state.value as State?
while (v != null) {
if (v is FragmentState && v.containerId == fragmentId) return@filter false
v = v.previous
}
true
}
.map { supportFragmentManager.findFragmentById(it) }
.filterNotNull()
.filter { it.isDetached }
.forEach {
if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "remove fragment $it")
}
val id = it.id
supportFragmentManager.beginTransaction()
.remove(it)
.commitAllowingStateLoss()
fragmentIds.remove(id)
}
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
@ -331,4 +389,6 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
} }
override fun onDeviceFound(device: U2FDevice) = AuthTokenLoginProcessor.process(device, getActivityViewModel()) override fun onDeviceFound(device: U2FDevice) = AuthTokenLoginProcessor.process(device, getActivityViewModel())
override fun execute(command: UpdateStateCommand) = mainModel.execute(command)
} }

View file

@ -0,0 +1,38 @@
/*
* 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
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.fragment.app.FragmentManager
import io.timelimit.android.ui.model.Screen
import io.timelimit.android.ui.model.UpdateStateCommand
import io.timelimit.android.ui.overview.overview.OverviewScreen
@Composable
fun ScreenMultiplexer(
screen: Screen?,
executeCommand: (UpdateStateCommand) -> Unit,
fragmentManager: FragmentManager,
fragmentIds: MutableSet<Int>,
modifier: Modifier = Modifier
) {
when (screen) {
null -> {/* nothing to do */ }
is Screen.OverviewScreen -> OverviewScreen(screen.content, executeCommand, modifier = modifier)
is Screen.FragmentScreen -> FragmentScreen(screen, fragmentManager, fragmentIds, modifier = modifier)
}
}

View file

@ -0,0 +1,111 @@
/*
* 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
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.LockOpen
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.runtime.*
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import io.timelimit.android.R
import io.timelimit.android.ui.model.Screen
import io.timelimit.android.ui.model.UpdateStateCommand
@Composable
fun ScreenScaffold(
screen: Screen?,
title: String,
subtitle: String?,
content: @Composable (PaddingValues) -> Unit,
executeCommand: (UpdateStateCommand) -> Unit,
showAuthenticationDialog: (() -> Unit)?
) {
var expandDropdown by remember { mutableStateOf(false) }
Scaffold(
topBar = {
TopAppBar(
title = {
Column {
Text(
title,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
if (subtitle != null) {
Text(
subtitle,
style = MaterialTheme.typography.subtitle1,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
},
navigationIcon = if (screen?.state?.previous != null) ({
IconButton(onClick = { executeCommand(UpdateStateCommand.BackToPreviousScreen) }) {
Icon(Icons.Default.ArrowBack, stringResource(R.string.generic_back))
}
}) else null,
actions = {
for (icon in screen?.toolbarIcons ?: emptyList()) {
IconButton(
onClick = {
executeCommand(icon.action)
}
) {
Icon(icon.icon, stringResource(icon.labelResource))
}
}
if (screen?.toolbarOptions?.isEmpty() == false) {
IconButton(onClick = { expandDropdown = true }) {
Icon(Icons.Default.MoreVert, stringResource(R.string.generic_menu))
}
DropdownMenu(
expanded = expandDropdown,
onDismissRequest = { expandDropdown = false }
) {
for (option in screen.toolbarOptions) {
DropdownMenuItem(onClick = {
executeCommand(option.action)
expandDropdown = false
}) {
Text(stringResource(option.labelResource))
}
}
}
}
}
)
},
floatingActionButton = {
if (showAuthenticationDialog != null) {
FloatingActionButton(onClick = showAuthenticationDialog) {
Icon(Icons.Default.LockOpen, stringResource(R.string.authentication_action))
}
}
},
content = content
)
}

View file

@ -0,0 +1,39 @@
/*
* 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
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import io.timelimit.android.R
@Composable
fun Theme(
content: @Composable () -> Unit
) {
val resources = LocalContext.current.resources
MaterialTheme(
content = content,
colors = MaterialTheme.colors.copy(
primary = Color(resources.getColor(R.color.colorPrimary)),
primaryVariant = Color(resources.getColor(R.color.colorPrimaryDark)),
secondary = Color(resources.getColor(R.color.colorAccent)),
onSecondary = Color.White
)
)
}

View file

@ -0,0 +1,22 @@
package io.timelimit.android.ui.animation
import androidx.compose.animation.*
@OptIn(ExperimentalAnimationApi::class)
object Transition {
val openScreen = ContentTransform(
targetContentEnter = slideInHorizontally { it },
initialContentExit = slideOutHorizontally() + fadeOut(targetAlpha = .5f)
)
val closeScreen = ContentTransform(
targetContentEnter = slideInHorizontally { -it / 2 } + fadeIn(initialAlpha = .5f),
initialContentExit = slideOutHorizontally { it },
targetContentZIndex = -1f
)
val none = ContentTransform(
targetContentEnter = EnterTransition.None,
initialContentExit = ExitTransition.None
)
}

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -22,72 +22,50 @@ import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.navigation.Navigation
import io.timelimit.android.R import io.timelimit.android.R
import io.timelimit.android.databinding.FragmentDiagnoseMainBinding import io.timelimit.android.databinding.FragmentDiagnoseMainBinding
import io.timelimit.android.extensions.safeNavigate
import io.timelimit.android.livedata.liveDataFromNonNullValue import io.timelimit.android.livedata.liveDataFromNonNullValue
import io.timelimit.android.livedata.liveDataFromNullableValue import io.timelimit.android.livedata.liveDataFromNullableValue
import io.timelimit.android.logic.DefaultAppLogic import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.ui.main.ActivityViewModelHolder import io.timelimit.android.ui.main.ActivityViewModelHolder
import io.timelimit.android.ui.main.AuthenticationFab import io.timelimit.android.ui.main.AuthenticationFab
import io.timelimit.android.ui.main.FragmentWithCustomTitle import io.timelimit.android.ui.main.FragmentWithCustomTitle
import io.timelimit.android.ui.model.UpdateStateCommand
import io.timelimit.android.ui.model.execute
class DiagnoseMainFragment : Fragment(), FragmentWithCustomTitle { class DiagnoseMainFragment : Fragment(), FragmentWithCustomTitle {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val binding = FragmentDiagnoseMainBinding.inflate(inflater, container, false) val binding = FragmentDiagnoseMainBinding.inflate(inflater, container, false)
val navigation = Navigation.findNavController(container!!)
val logic = DefaultAppLogic.with(requireContext()) val logic = DefaultAppLogic.with(requireContext())
val activity: ActivityViewModelHolder = activity as ActivityViewModelHolder val activity: ActivityViewModelHolder = activity as ActivityViewModelHolder
val auth = activity.getActivityViewModel() val auth = activity.getActivityViewModel()
binding.diagnoseClockButton.setOnClickListener { binding.diagnoseClockButton.setOnClickListener {
navigation.safeNavigate( requireActivity().execute(UpdateStateCommand.Diagnose.Clock)
DiagnoseMainFragmentDirections.actionDiagnoseMainFragmentToDiagnoseClockFragment(),
R.id.diagnoseMainFragment
)
} }
binding.diagnoseConnectionButton.setOnClickListener { binding.diagnoseConnectionButton.setOnClickListener {
navigation.safeNavigate( requireActivity().execute(UpdateStateCommand.Diagnose.Connection)
DiagnoseMainFragmentDirections.actionDiagnoseMainFragmentToDiagnoseConnectionFragment(),
R.id.diagnoseMainFragment
)
} }
binding.diagnoseSyncButton.setOnClickListener { binding.diagnoseSyncButton.setOnClickListener {
navigation.safeNavigate( requireActivity().execute(UpdateStateCommand.Diagnose.Sync)
DiagnoseMainFragmentDirections.actionDiagnoseMainFragmentToDiagnoseSyncFragment(),
R.id.diagnoseMainFragment
)
} }
binding.diagnoseCryButton.setOnClickListener { binding.diagnoseCryButton.setOnClickListener {
navigation.safeNavigate( requireActivity().execute(UpdateStateCommand.Diagnose.Crypto)
DiagnoseMainFragmentDirections.actionDiagnoseMainFragmentToDiagnoseCryptoFragment(),
R.id.diagnoseMainFragment
)
} }
binding.diagnoseBatteryButton.setOnClickListener { binding.diagnoseBatteryButton.setOnClickListener {
navigation.safeNavigate( requireActivity().execute(UpdateStateCommand.Diagnose.Battery)
DiagnoseMainFragmentDirections.actionDiagnoseMainFragmentToDiagnoseBatteryFragment(),
R.id.diagnoseMainFragment
)
} }
binding.diagnoseFgaButton.setOnClickListener { binding.diagnoseFgaButton.setOnClickListener {
navigation.safeNavigate( requireActivity().execute(UpdateStateCommand.Diagnose.ForegroundApp)
DiagnoseMainFragmentDirections.actionDiagnoseMainFragmentToDiagnoseForegroundAppFragment(),
R.id.diagnoseMainFragment
)
} }
binding.diagnoseExfButton.setOnClickListener { binding.diagnoseExfButton.setOnClickListener {
navigation.safeNavigate( requireActivity().execute(UpdateStateCommand.Diagnose.ExperimentalFlags)
DiagnoseMainFragmentDirections.actionDiagnoseMainFragmentToDiagnoseExperimentalFlagFragment(),
R.id.diagnoseMainFragment
)
} }
logic.backgroundTaskLogic.lastLoopException.observe(this, Observer { ex -> logic.backgroundTaskLogic.lastLoopException.observe(this, Observer { ex ->
@ -108,10 +86,7 @@ class DiagnoseMainFragment : Fragment(), FragmentWithCustomTitle {
} }
binding.diagnoseExitReasonsButton.setOnClickListener { binding.diagnoseExitReasonsButton.setOnClickListener {
navigation.safeNavigate( requireActivity().execute(UpdateStateCommand.Diagnose.ExitReasons)
DiagnoseMainFragmentDirections.actionDiagnoseMainFragmentToDiagnoseExitReasonFragment(),
R.id.diagnoseMainFragment
)
} }
AuthenticationFab.manageAuthenticationFab( AuthenticationFab.manageAuthenticationFab(

View file

@ -27,6 +27,8 @@ import io.timelimit.android.livedata.switchMap
import io.timelimit.android.ui.main.FragmentWithCustomTitle import io.timelimit.android.ui.main.FragmentWithCustomTitle
import io.timelimit.android.ui.manage.category.blocked_times.BlockedTimeAreasFragment import io.timelimit.android.ui.manage.category.blocked_times.BlockedTimeAreasFragment
import io.timelimit.android.ui.manage.category.settings.CategorySettingsFragment import io.timelimit.android.ui.manage.category.settings.CategorySettingsFragment
import io.timelimit.android.ui.model.UpdateStateCommand
import io.timelimit.android.ui.model.execute
abstract class CategoryFragmentWrapper: SingleFragmentWrapper(), FragmentWithCustomTitle { abstract class CategoryFragmentWrapper: SingleFragmentWrapper(), FragmentWithCustomTitle {
abstract val childId: String abstract val childId: String
@ -44,7 +46,7 @@ abstract class CategoryFragmentWrapper: SingleFragmentWrapper(), FragmentWithCus
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
category.observe(viewLifecycleOwner) { category.observe(viewLifecycleOwner) {
if (it == null) navigation.popBackStack() if (it == null) requireActivity().execute(UpdateStateCommand.ManageChild.LeaveCategory)
} }
} }

View file

@ -26,6 +26,8 @@ import io.timelimit.android.ui.manage.category.usagehistory.UsageHistoryFragment
import io.timelimit.android.ui.manage.child.advanced.ManageChildAdvancedFragment import io.timelimit.android.ui.manage.child.advanced.ManageChildAdvancedFragment
import io.timelimit.android.ui.manage.child.apps.ChildAppsFragment import io.timelimit.android.ui.manage.child.apps.ChildAppsFragment
import io.timelimit.android.ui.manage.child.tasks.ManageChildTasksFragment import io.timelimit.android.ui.manage.child.tasks.ManageChildTasksFragment
import io.timelimit.android.ui.model.UpdateStateCommand
import io.timelimit.android.ui.model.execute
abstract class ChildFragmentWrapper: SingleFragmentWrapper() { abstract class ChildFragmentWrapper: SingleFragmentWrapper() {
abstract val childId: String abstract val childId: String
@ -37,7 +39,7 @@ abstract class ChildFragmentWrapper: SingleFragmentWrapper() {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
child.observe(viewLifecycleOwner) { child.observe(viewLifecycleOwner) {
if (it == null) navigation.popBackStack() if (it == null) requireActivity().execute(UpdateStateCommand.ManageChild.LeaveChild)
} }
} }
} }

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -20,8 +20,6 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.navigation.NavController
import androidx.navigation.Navigation
import io.timelimit.android.R import io.timelimit.android.R
import io.timelimit.android.databinding.SingleFragmentWrapperBinding import io.timelimit.android.databinding.SingleFragmentWrapperBinding
import io.timelimit.android.livedata.liveDataFromNonNullValue import io.timelimit.android.livedata.liveDataFromNonNullValue
@ -30,14 +28,9 @@ import io.timelimit.android.ui.main.AuthenticationFab
abstract class SingleFragmentWrapper: Fragment() { abstract class SingleFragmentWrapper: Fragment() {
val activity: ActivityViewModelHolder by lazy { getActivity() as ActivityViewModelHolder } val activity: ActivityViewModelHolder by lazy { getActivity() as ActivityViewModelHolder }
private lateinit var navController: NavController
protected lateinit var binding: SingleFragmentWrapperBinding protected lateinit var binding: SingleFragmentWrapperBinding
protected val navigation get() = navController
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
navController = Navigation.findNavController(container!!)
binding = SingleFragmentWrapperBinding.inflate(inflater, container, false) binding = SingleFragmentWrapperBinding.inflate(inflater, container, false)
AuthenticationFab.manageAuthenticationFab( AuthenticationFab.manageAuthenticationFab(

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -17,8 +17,8 @@
package io.timelimit.android.ui.fragment package io.timelimit.android.ui.fragment
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import io.timelimit.android.R import io.timelimit.android.ui.model.UpdateStateCommand
import io.timelimit.android.extensions.safeNavigate import io.timelimit.android.ui.model.execute
import io.timelimit.android.ui.overview.about.AboutFragment import io.timelimit.android.ui.overview.about.AboutFragment
import io.timelimit.android.ui.overview.about.AboutFragmentParentHandlers import io.timelimit.android.ui.overview.about.AboutFragmentParentHandlers
@ -27,23 +27,14 @@ class AboutFragmentWrapped: SingleFragmentWrapper(), AboutFragmentParentHandlers
override fun createChildFragment(): Fragment = AboutFragment() override fun createChildFragment(): Fragment = AboutFragment()
override fun onShowDiagnoseScreen() { override fun onShowDiagnoseScreen() {
navigation.safeNavigate( requireActivity().execute(UpdateStateCommand.About.Diagnose)
AboutFragmentWrappedDirections.actionAboutFragmentWrappedToDiagnoseMainFragment(),
R.id.aboutFragmentWrapped
)
} }
override fun onShowPurchaseScreen() { override fun onShowPurchaseScreen() {
navigation.safeNavigate( requireActivity().execute(UpdateStateCommand.About.Purchase)
AboutFragmentWrappedDirections.actionAboutFragmentWrappedToPurchaseFragment(),
R.id.aboutFragmentWrapped
)
} }
override fun onShowStayAwesomeScreen() { override fun onShowStayAwesomeScreen() {
navigation.safeNavigate( requireActivity().execute(UpdateStateCommand.About.StayAwesome)
AboutFragmentWrappedDirections.actionAboutFragmentWrappedToStayAwesomeFragment(),
R.id.aboutFragmentWrapped
)
} }
} }

View file

@ -1,64 +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/>.
*/
package io.timelimit.android.ui.launch
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.Navigation
import io.timelimit.android.R
import io.timelimit.android.extensions.safeNavigate
import io.timelimit.android.ui.obsolete.ObsoleteDialogFragment
import io.timelimit.android.ui.overview.main.MainFragmentDirections
class LaunchFragment: Fragment() {
private val model by viewModels<LaunchModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ObsoleteDialogFragment.show(requireActivity(), false)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.circular_progress_indicator, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val navigation = Navigation.findNavController(view)
model.action.observe(viewLifecycleOwner) {
when (it) {
LaunchModel.Action.Setup -> navigation.safeNavigate(LaunchFragmentDirections.actionLaunchFragmentToSetupTermsFragment(), R.id.launchFragment)
LaunchModel.Action.Overview -> navigation.safeNavigate(LaunchFragmentDirections.actionLaunchFragmentToOverviewFragment(), R.id.launchFragment)
is LaunchModel.Action.Child -> {
navigation.safeNavigate(LaunchFragmentDirections.actionLaunchFragmentToOverviewFragment(), R.id.launchFragment)
navigation.safeNavigate(MainFragmentDirections.actionOverviewFragmentToManageChildFragment(it.id, fromRedirect = true), R.id.overviewFragment)
}
LaunchModel.Action.DeviceSetup -> {
navigation.safeNavigate(LaunchFragmentDirections.actionLaunchFragmentToOverviewFragment(), R.id.launchFragment)
navigation.safeNavigate(MainFragmentDirections.actionOverviewFragmentToSetupDeviceFragment(), R.id.overviewFragment)
}
LaunchModel.Action.ParentMode -> navigation.safeNavigate(LaunchFragmentDirections.actionLaunchFragmentToParentModeFragment(), R.id.launchFragment)
}
}
}
}

View file

@ -1,68 +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/>.
*/
package io.timelimit.android.ui.launch
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import io.timelimit.android.async.Threads
import io.timelimit.android.coroutines.executeAndWait
import io.timelimit.android.data.model.UserType
import io.timelimit.android.livedata.castDown
import io.timelimit.android.livedata.waitUntilValueMatches
import io.timelimit.android.logic.DefaultAppLogic
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class LaunchModel(application: Application): AndroidViewModel(application) {
private val actionInternal = MutableLiveData<Action>()
private val logic = DefaultAppLogic.with(application)
val action = actionInternal.castDown()
init {
viewModelScope.launch {
withContext(Dispatchers.Main) {
logic.isInitialized.waitUntilValueMatches { it == true }
actionInternal.value = Threads.database.executeAndWait {
val hasDeviceId = logic.database.config().getOwnDeviceIdSync() != null
val hasParentKey = logic.database.config().getParentModeKeySync() != null
if (hasDeviceId) {
val config = logic.database.derivedDataDao().getUserAndDeviceRelatedDataSync()
if (config?.userRelatedData?.user?.type == UserType.Child) Action.Child(config.userRelatedData.user.id)
else if (config?.userRelatedData == null) Action.DeviceSetup
else Action.Overview
}
else if (hasParentKey) Action.ParentMode
else Action.Setup
}
}
}
}
sealed class Action {
object Setup: Action()
object Overview: Action()
data class Child(val id: String): Action()
object DeviceSetup: Action()
object ParentMode: Action()
}
}

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -15,13 +15,7 @@
*/ */
package io.timelimit.android.ui.manage.category package io.timelimit.android.ui.manage.category
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import io.timelimit.android.R
import io.timelimit.android.extensions.safeNavigate
import io.timelimit.android.ui.fragment.CategoryFragmentWrapper import io.timelimit.android.ui.fragment.CategoryFragmentWrapper
import io.timelimit.android.ui.main.FragmentWithCustomTitle import io.timelimit.android.ui.main.FragmentWithCustomTitle
import io.timelimit.android.ui.manage.category.appsandrules.CombinedAppsAndRulesFragment import io.timelimit.android.ui.manage.category.appsandrules.CombinedAppsAndRulesFragment
@ -35,42 +29,4 @@ class ManageCategoryFragment : CategoryFragmentWrapper(), FragmentWithCustomTitl
childId = childId, childId = childId,
categoryId = categoryId categoryId = categoryId
) )
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.fragment_manage_category_menu, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
R.id.menu_manage_category_blocked_time_areas -> {
navigation.safeNavigate(
ManageCategoryFragmentDirections.actionManageCategoryFragmentToBlockedTimeAreasFragmentWrapper(
childId = params.childId,
categoryId = params.categoryId
),
R.id.manageCategoryFragment
)
true
}
R.id.menu_manage_category_settings -> {
navigation.safeNavigate(
ManageCategoryFragmentDirections.actionManageCategoryFragmentToCategoryAdvancedFragmentWrapper(
childId = params.childId,
categoryId = params.categoryId
),
R.id.manageCategoryFragment
)
true
}
else -> super.onOptionsItemSelected(item)
}
} }

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -16,14 +16,10 @@
package io.timelimit.android.ui.manage.child package io.timelimit.android.ui.manage.child
import android.os.Bundle import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View import android.view.View
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import io.timelimit.android.R import io.timelimit.android.R
import io.timelimit.android.extensions.safeNavigate
import io.timelimit.android.livedata.map import io.timelimit.android.livedata.map
import io.timelimit.android.ui.fragment.ChildFragmentWrapper import io.timelimit.android.ui.fragment.ChildFragmentWrapper
import io.timelimit.android.ui.main.FragmentWithCustomTitle import io.timelimit.android.ui.main.FragmentWithCustomTitle
@ -49,55 +45,5 @@ class ManageChildFragment : ChildFragmentWrapper(), FragmentWithCustomTitle {
} }
} }
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.fragment_manage_child_menu, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
R.id.menu_manage_child_apps -> {
navigation.safeNavigate(
ManageChildFragmentDirections.actionManageChildFragmentToChildAppsFragmentWrapper(childId = childId),
R.id.manageChildFragment
)
true
}
R.id.menu_manage_child_advanced -> {
navigation.safeNavigate(
ManageChildFragmentDirections.actionManageChildFragmentToChildAdvancedFragmentWrapper(childId = childId),
R.id.manageChildFragment
)
true
}
R.id.menu_manage_child_phone -> {
navigation.safeNavigate(
ManageChildFragmentDirections.actionManageChildFragmentToContactsFragment(),
R.id.manageChildFragment
)
true
}
R.id.menu_manage_child_usage_history -> {
navigation.safeNavigate(
ManageChildFragmentDirections.actionManageChildFragmentToChildUsageHistoryFragmentWrapper(childId = childId),
R.id.manageChildFragment
)
true
}
R.id.menu_manage_child_tasks -> {
navigation.safeNavigate(
ManageChildFragmentDirections.actionManageChildFragmentToManageChildTasksFragment(childId = childId),
R.id.manageChildFragment
)
true
}
else -> super.onOptionsItemSelected(item)
}
override fun getCustomTitle() = child.map { "${it?.name} < ${getString(R.string.main_tab_overview)}" as String? } override fun getCustomTitle() = child.map { "${it?.name} < ${getString(R.string.main_tab_overview)}" as String? }
} }

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -22,16 +22,13 @@ import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.navigation.Navigation
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.timelimit.android.R
import io.timelimit.android.async.Threads import io.timelimit.android.async.Threads
import io.timelimit.android.data.model.Category import io.timelimit.android.data.model.Category
import io.timelimit.android.data.model.HintsToShow import io.timelimit.android.data.model.HintsToShow
import io.timelimit.android.databinding.RecyclerFragmentBinding import io.timelimit.android.databinding.RecyclerFragmentBinding
import io.timelimit.android.extensions.safeNavigate
import io.timelimit.android.logic.AppLogic import io.timelimit.android.logic.AppLogic
import io.timelimit.android.logic.DefaultAppLogic import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.sync.actions.UpdateCategoryDisableLimitsAction import io.timelimit.android.sync.actions.UpdateCategoryDisableLimitsAction
@ -41,10 +38,11 @@ import io.timelimit.android.ui.consent.SyncAppListConsentDialogFragment
import io.timelimit.android.ui.main.ActivityViewModel import io.timelimit.android.ui.main.ActivityViewModel
import io.timelimit.android.ui.main.getActivityViewModel import io.timelimit.android.ui.main.getActivityViewModel
import io.timelimit.android.ui.manage.child.ManageChildFragmentArgs import io.timelimit.android.ui.manage.child.ManageChildFragmentArgs
import io.timelimit.android.ui.manage.child.ManageChildFragmentDirections
import io.timelimit.android.ui.manage.child.category.create.CreateCategoryDialogFragment import io.timelimit.android.ui.manage.child.category.create.CreateCategoryDialogFragment
import io.timelimit.android.ui.manage.child.category.specialmode.SetCategorySpecialModeFragment import io.timelimit.android.ui.manage.child.category.specialmode.SetCategorySpecialModeFragment
import io.timelimit.android.ui.manage.child.category.specialmode.SpecialModeDialogMode import io.timelimit.android.ui.manage.child.category.specialmode.SpecialModeDialogMode
import io.timelimit.android.ui.model.UpdateStateCommand
import io.timelimit.android.ui.model.execute
class ManageChildCategoriesFragment : Fragment() { class ManageChildCategoriesFragment : Fragment() {
companion object { companion object {
@ -69,17 +67,13 @@ class ManageChildCategoriesFragment : Fragment() {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val adapter = Adapter() val adapter = Adapter()
val navigation = Navigation.findNavController(view)
adapter.handlers = object: Handlers { adapter.handlers = object: Handlers {
override fun onCategoryClicked(category: Category) { override fun onCategoryClicked(category: Category) {
navigation.safeNavigate( requireActivity().execute(UpdateStateCommand.ManageChild.Category(
ManageChildFragmentDirections.actionManageChildFragmentToManageCategoryFragment( childId = params.childId,
params.childId, categoryId = category.id
category.id ))
),
R.id.manageChildFragment
)
} }
override fun onCreateCategoryClicked() { override fun onCreateCategoryClicked() {

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -25,13 +25,11 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import androidx.lifecycle.switchMap import androidx.lifecycle.switchMap
import androidx.navigation.Navigation
import io.timelimit.android.R import io.timelimit.android.R
import io.timelimit.android.crypto.Curve25519 import io.timelimit.android.crypto.Curve25519
import io.timelimit.android.crypto.HexString import io.timelimit.android.crypto.HexString
import io.timelimit.android.data.model.Device import io.timelimit.android.data.model.Device
import io.timelimit.android.databinding.FragmentManageDeviceBinding import io.timelimit.android.databinding.FragmentManageDeviceBinding
import io.timelimit.android.extensions.safeNavigate
import io.timelimit.android.livedata.* import io.timelimit.android.livedata.*
import io.timelimit.android.logic.AppLogic import io.timelimit.android.logic.AppLogic
import io.timelimit.android.logic.DefaultAppLogic import io.timelimit.android.logic.DefaultAppLogic
@ -42,6 +40,8 @@ import io.timelimit.android.ui.main.AuthenticationFab
import io.timelimit.android.ui.main.FragmentWithCustomTitle import io.timelimit.android.ui.main.FragmentWithCustomTitle
import io.timelimit.android.ui.manage.device.manage.feature.ManageDeviceFeaturesFragment import io.timelimit.android.ui.manage.device.manage.feature.ManageDeviceFeaturesFragment
import io.timelimit.android.ui.manage.device.manage.permission.ManageDevicePermissionsFragment import io.timelimit.android.ui.manage.device.manage.permission.ManageDevicePermissionsFragment
import io.timelimit.android.ui.model.UpdateStateCommand
import io.timelimit.android.ui.model.execute
class ManageDeviceFragment : Fragment(), FragmentWithCustomTitle { class ManageDeviceFragment : Fragment(), FragmentWithCustomTitle {
private val activity: ActivityViewModelHolder by lazy { getActivity() as ActivityViewModelHolder } private val activity: ActivityViewModelHolder by lazy { getActivity() as ActivityViewModelHolder }
@ -52,8 +52,7 @@ class ManageDeviceFragment : Fragment(), FragmentWithCustomTitle {
logic.database.device().getDeviceById(args.deviceId) logic.database.device().getDeviceById(args.deviceId)
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val navigation = Navigation.findNavController(container!!)
val binding = FragmentManageDeviceBinding.inflate(inflater, container, false) val binding = FragmentManageDeviceBinding.inflate(inflater, container, false)
val userEntries = logic.database.user().getAllUsersLive() val userEntries = logic.database.user().getAllUsersLive()
@ -82,39 +81,19 @@ class ManageDeviceFragment : Fragment(), FragmentWithCustomTitle {
binding.handlers = object: ManageDeviceFragmentHandlers { binding.handlers = object: ManageDeviceFragmentHandlers {
override fun showUserScreen() { override fun showUserScreen() {
navigation.safeNavigate( requireActivity().execute(UpdateStateCommand.ManageDevice.User(args.deviceId))
ManageDeviceFragmentDirections.actionManageDeviceFragmentToManageDeviceUserFragment(
deviceId = args.deviceId
),
R.id.manageDeviceFragment
)
} }
override fun showPermissionsScreen() { override fun showPermissionsScreen() {
navigation.safeNavigate( requireActivity().execute(UpdateStateCommand.ManageDevice.Permissions(args.deviceId))
ManageDeviceFragmentDirections.actionManageDeviceFragmentToManageDevicePermissionsFragment(
deviceId = args.deviceId
),
R.id.manageDeviceFragment
)
} }
override fun showFeaturesScreen() { override fun showFeaturesScreen() {
navigation.safeNavigate( requireActivity().execute(UpdateStateCommand.ManageDevice.Features(args.deviceId))
ManageDeviceFragmentDirections.actionManageDeviceFragmentToManageDeviceFeaturesFragment(
deviceId = args.deviceId
),
R.id.manageDeviceFragment
)
} }
override fun showManageScreen() { override fun showManageScreen() {
navigation.safeNavigate( requireActivity().execute(UpdateStateCommand.ManageDevice.Advanced(args.deviceId))
ManageDeviceFragmentDirections.actionManageDeviceFragmentToManageDeviceAdvancedFragment(
deviceId = args.deviceId
),
R.id.manageDeviceFragment
)
} }
override fun showAuthenticationScreen() { override fun showAuthenticationScreen() {
@ -126,7 +105,7 @@ class ManageDeviceFragment : Fragment(), FragmentWithCustomTitle {
device -> device ->
if (device == null) { if (device == null) {
navigation.popBackStack() requireActivity().execute(UpdateStateCommand.ManageDevice.Leave)
} else { } else {
val now = RealTime.newInstance() val now = RealTime.newInstance()
logic.realTimeLogic.getRealTime(now) logic.realTimeLogic.getRealTime(now)

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -23,7 +23,6 @@ import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.navigation.Navigation
import io.timelimit.android.R import io.timelimit.android.R
import io.timelimit.android.data.model.Device import io.timelimit.android.data.model.Device
import io.timelimit.android.data.model.User import io.timelimit.android.data.model.User
@ -35,6 +34,8 @@ import io.timelimit.android.ui.main.ActivityViewModel
import io.timelimit.android.ui.main.ActivityViewModelHolder import io.timelimit.android.ui.main.ActivityViewModelHolder
import io.timelimit.android.ui.main.AuthenticationFab import io.timelimit.android.ui.main.AuthenticationFab
import io.timelimit.android.ui.main.FragmentWithCustomTitle import io.timelimit.android.ui.main.FragmentWithCustomTitle
import io.timelimit.android.ui.model.UpdateStateCommand
import io.timelimit.android.ui.model.execute
class ManageDeviceAdvancedFragment : Fragment(), FragmentWithCustomTitle { class ManageDeviceAdvancedFragment : Fragment(), FragmentWithCustomTitle {
private val activity: ActivityViewModelHolder by lazy { getActivity() as ActivityViewModelHolder } private val activity: ActivityViewModelHolder by lazy { getActivity() as ActivityViewModelHolder }
@ -47,7 +48,6 @@ class ManageDeviceAdvancedFragment : Fragment(), FragmentWithCustomTitle {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val binding = ManageDeviceAdvancedFragmentBinding.inflate(inflater, container, false) val binding = ManageDeviceAdvancedFragmentBinding.inflate(inflater, container, false)
val navigation = Navigation.findNavController(container!!)
val isThisDevice = logic.deviceId.map { ownDeviceId -> ownDeviceId == args.deviceId }.ignoreUnchanged() val isThisDevice = logic.deviceId.map { ownDeviceId -> ownDeviceId == args.deviceId }.ignoreUnchanged()
val userEntry = deviceEntry.switchMap { device -> val userEntry = deviceEntry.switchMap { device ->
@ -102,7 +102,7 @@ class ManageDeviceAdvancedFragment : Fragment(), FragmentWithCustomTitle {
deviceEntry.observe(this, Observer { device -> deviceEntry.observe(this, Observer { device ->
if (device == null) { if (device == null) {
navigation.popBackStack(R.id.overviewFragment, false) requireActivity().execute(UpdateStateCommand.ManageDevice.Leave)
} }
}) })

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -24,7 +24,6 @@ import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.navigation.Navigation
import io.timelimit.android.R import io.timelimit.android.R
import io.timelimit.android.data.model.Device import io.timelimit.android.data.model.Device
import io.timelimit.android.data.model.NetworkTime import io.timelimit.android.data.model.NetworkTime
@ -40,6 +39,8 @@ import io.timelimit.android.ui.main.ActivityViewModel
import io.timelimit.android.ui.main.ActivityViewModelHolder import io.timelimit.android.ui.main.ActivityViewModelHolder
import io.timelimit.android.ui.main.AuthenticationFab import io.timelimit.android.ui.main.AuthenticationFab
import io.timelimit.android.ui.main.FragmentWithCustomTitle import io.timelimit.android.ui.main.FragmentWithCustomTitle
import io.timelimit.android.ui.model.UpdateStateCommand
import io.timelimit.android.ui.model.execute
class ManageDeviceFeaturesFragment : Fragment(), FragmentWithCustomTitle { class ManageDeviceFeaturesFragment : Fragment(), FragmentWithCustomTitle {
companion object { companion object {
@ -79,7 +80,6 @@ class ManageDeviceFeaturesFragment : Fragment(), FragmentWithCustomTitle {
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val navigation = Navigation.findNavController(container!!)
val binding = ManageDeviceFeaturesFragmentBinding.inflate(inflater, container, false) val binding = ManageDeviceFeaturesFragmentBinding.inflate(inflater, container, false)
// auth // auth
@ -129,7 +129,7 @@ class ManageDeviceFeaturesFragment : Fragment(), FragmentWithCustomTitle {
device -> device ->
if (device == null) { if (device == null) {
navigation.popBackStack(R.id.overviewFragment, false) requireActivity().execute(UpdateStateCommand.ManageDevice.Leave)
} else { } else {
val now = RealTime.newInstance() val now = RealTime.newInstance()
logic.realTimeLogic.getRealTime(now) logic.realTimeLogic.getRealTime(now)

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -23,7 +23,6 @@ import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.navigation.Navigation
import io.timelimit.android.R import io.timelimit.android.R
import io.timelimit.android.data.model.Device import io.timelimit.android.data.model.Device
import io.timelimit.android.data.model.UserType import io.timelimit.android.data.model.UserType
@ -41,6 +40,8 @@ import io.timelimit.android.ui.main.ActivityViewModel
import io.timelimit.android.ui.main.ActivityViewModelHolder import io.timelimit.android.ui.main.ActivityViewModelHolder
import io.timelimit.android.ui.main.AuthenticationFab import io.timelimit.android.ui.main.AuthenticationFab
import io.timelimit.android.ui.main.FragmentWithCustomTitle import io.timelimit.android.ui.main.FragmentWithCustomTitle
import io.timelimit.android.ui.model.UpdateStateCommand
import io.timelimit.android.ui.model.execute
class ManageDevicePermissionsFragment : Fragment(), FragmentWithCustomTitle { class ManageDevicePermissionsFragment : Fragment(), FragmentWithCustomTitle {
companion object { companion object {
@ -84,7 +85,6 @@ class ManageDevicePermissionsFragment : Fragment(), FragmentWithCustomTitle {
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val navigation = Navigation.findNavController(container!!)
val binding = ManageDevicePermissionsFragmentBinding.inflate(inflater, container, false) val binding = ManageDevicePermissionsFragmentBinding.inflate(inflater, container, false)
// auth // auth
@ -180,7 +180,7 @@ class ManageDevicePermissionsFragment : Fragment(), FragmentWithCustomTitle {
device -> device ->
if (device == null) { if (device == null) {
navigation.popBackStack(R.id.overviewFragment, false) requireActivity().execute(UpdateStateCommand.ManageDevice.Leave)
} else { } else {
binding.usageStatsAccess = device.currentUsageStatsPermission binding.usageStatsAccess = device.currentUsageStatsPermission
binding.notificationAccessPermission = device.currentNotificationAccessPermission binding.notificationAccessPermission = device.currentNotificationAccessPermission

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -24,7 +24,6 @@ import android.widget.RadioButton
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.navigation.Navigation
import io.timelimit.android.R import io.timelimit.android.R
import io.timelimit.android.data.model.Device import io.timelimit.android.data.model.Device
import io.timelimit.android.databinding.ManageDeviceUserFragmentBinding import io.timelimit.android.databinding.ManageDeviceUserFragmentBinding
@ -40,6 +39,8 @@ import io.timelimit.android.ui.main.ActivityViewModelHolder
import io.timelimit.android.ui.main.AuthenticationFab import io.timelimit.android.ui.main.AuthenticationFab
import io.timelimit.android.ui.main.FragmentWithCustomTitle import io.timelimit.android.ui.main.FragmentWithCustomTitle
import io.timelimit.android.ui.manage.device.manage.defaultuser.ManageDeviceDefaultUser import io.timelimit.android.ui.manage.device.manage.defaultuser.ManageDeviceDefaultUser
import io.timelimit.android.ui.model.UpdateStateCommand
import io.timelimit.android.ui.model.execute
class ManageDeviceUserFragment : Fragment(), FragmentWithCustomTitle { class ManageDeviceUserFragment : Fragment(), FragmentWithCustomTitle {
private val activity: ActivityViewModelHolder by lazy { getActivity() as ActivityViewModelHolder } private val activity: ActivityViewModelHolder by lazy { getActivity() as ActivityViewModelHolder }
@ -51,7 +52,6 @@ class ManageDeviceUserFragment : Fragment(), FragmentWithCustomTitle {
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val navigation = Navigation.findNavController(container!!)
val binding = ManageDeviceUserFragmentBinding.inflate(inflater, container, false) val binding = ManageDeviceUserFragmentBinding.inflate(inflater, container, false)
val userEntries = logic.database.user().getAllUsersLive() val userEntries = logic.database.user().getAllUsersLive()
@ -137,7 +137,7 @@ class ManageDeviceUserFragment : Fragment(), FragmentWithCustomTitle {
device -> device ->
if (device == null) { if (device == null) {
navigation.popBackStack(R.id.overviewFragment, false) requireActivity().execute(UpdateStateCommand.ManageDevice.Leave)
} }
}) })

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -24,11 +24,9 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import androidx.navigation.Navigation
import io.timelimit.android.R import io.timelimit.android.R
import io.timelimit.android.data.model.User import io.timelimit.android.data.model.User
import io.timelimit.android.databinding.FragmentManageParentBinding import io.timelimit.android.databinding.FragmentManageParentBinding
import io.timelimit.android.extensions.safeNavigate
import io.timelimit.android.livedata.liveDataFromNonNullValue import io.timelimit.android.livedata.liveDataFromNonNullValue
import io.timelimit.android.livedata.map import io.timelimit.android.livedata.map
import io.timelimit.android.logic.AppLogic import io.timelimit.android.logic.AppLogic
@ -40,6 +38,8 @@ import io.timelimit.android.ui.manage.child.advanced.timezone.UserTimezoneView
import io.timelimit.android.ui.manage.parent.delete.DeleteParentView import io.timelimit.android.ui.manage.parent.delete.DeleteParentView
import io.timelimit.android.ui.manage.parent.key.ManageUserKeyView import io.timelimit.android.ui.manage.parent.key.ManageUserKeyView
import io.timelimit.android.ui.manage.parent.limitlogin.ParentLimitLoginView import io.timelimit.android.ui.manage.parent.limitlogin.ParentLimitLoginView
import io.timelimit.android.ui.model.UpdateStateCommand
import io.timelimit.android.ui.model.execute
class ManageParentFragment : Fragment(), FragmentWithCustomTitle { class ManageParentFragment : Fragment(), FragmentWithCustomTitle {
private val activity: ActivityViewModelHolder by lazy { getActivity() as ActivityViewModelHolder } private val activity: ActivityViewModelHolder by lazy { getActivity() as ActivityViewModelHolder }
@ -50,7 +50,6 @@ class ManageParentFragment : Fragment(), FragmentWithCustomTitle {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val binding = FragmentManageParentBinding.inflate(inflater, container, false) val binding = FragmentManageParentBinding.inflate(inflater, container, false)
val navigation = Navigation.findNavController(container!!)
val model = ViewModelProviders.of(this).get(ManageParentModel::class.java) val model = ViewModelProviders.of(this).get(ManageParentModel::class.java)
AuthenticationFab.manageAuthenticationFab( AuthenticationFab.manageAuthenticationFab(
@ -83,9 +82,7 @@ class ManageParentFragment : Fragment(), FragmentWithCustomTitle {
parentUser.observe(this, Observer { parentUser.observe(this, Observer {
user -> user ->
if (user == null) { if (user == null) requireActivity().execute(UpdateStateCommand.ManageParent.Leave)
navigation.popBackStack()
}
}) })
} }
@ -140,45 +137,21 @@ class ManageParentFragment : Fragment(), FragmentWithCustomTitle {
binding.handlers = object: ManageParentFragmentHandlers { binding.handlers = object: ManageParentFragmentHandlers {
override fun onChangePasswordClicked() { override fun onChangePasswordClicked() {
navigation.safeNavigate( requireActivity().execute(UpdateStateCommand.ManageParent.ChangePassword)
ManageParentFragmentDirections.
actionManageParentFragmentToChangeParentPasswordFragment(
params.parentId
),
R.id.manageParentFragment
)
} }
override fun onRestorePasswordClicked() { override fun onRestorePasswordClicked() {
navigation.safeNavigate( requireActivity().execute(UpdateStateCommand.ManageParent.RestorePassword)
ManageParentFragmentDirections.
actionManageParentFragmentToRestoreParentPasswordFragment(
params.parentId
),
R.id.manageParentFragment
)
} }
override fun onLinkMailClicked() { override fun onLinkMailClicked() {
if (activity.getActivityViewModel().requestAuthenticationOrReturnTrue()) { if (activity.getActivityViewModel().requestAuthenticationOrReturnTrue()) {
navigation.safeNavigate( requireActivity().execute(UpdateStateCommand.ManageParent.LinkMail)
ManageParentFragmentDirections.
actionManageParentFragmentToLinkParentMailFragment(
params.parentId
),
R.id.manageParentFragment
)
} }
} }
override fun onManageU2FClicked() { override fun onManageU2FClicked() {
navigation.safeNavigate( requireActivity().execute(UpdateStateCommand.ManageParent.U2F)
ManageParentFragmentDirections.
actionManageParentFragmentToManageParentU2FKeyFragment(
params.parentId
),
R.id.manageParentFragment
)
} }
} }

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -23,7 +23,6 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import androidx.navigation.Navigation
import io.timelimit.android.R import io.timelimit.android.R
import io.timelimit.android.data.model.User import io.timelimit.android.data.model.User
import io.timelimit.android.databinding.LinkParentMailFragmentBinding import io.timelimit.android.databinding.LinkParentMailFragmentBinding
@ -32,6 +31,8 @@ import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.ui.authentication.AuthenticateByMailFragment import io.timelimit.android.ui.authentication.AuthenticateByMailFragment
import io.timelimit.android.ui.authentication.AuthenticateByMailFragmentListener import io.timelimit.android.ui.authentication.AuthenticateByMailFragmentListener
import io.timelimit.android.ui.main.FragmentWithCustomTitle import io.timelimit.android.ui.main.FragmentWithCustomTitle
import io.timelimit.android.ui.model.UpdateStateCommand
import io.timelimit.android.ui.model.execute
class LinkParentMailFragment : Fragment(), AuthenticateByMailFragmentListener, FragmentWithCustomTitle { class LinkParentMailFragment : Fragment(), AuthenticateByMailFragmentListener, FragmentWithCustomTitle {
companion object { companion object {
@ -57,7 +58,6 @@ class LinkParentMailFragment : Fragment(), AuthenticateByMailFragmentListener, F
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val binding = LinkParentMailFragmentBinding.inflate(inflater, container, false) val binding = LinkParentMailFragmentBinding.inflate(inflater, container, false)
val navigation = Navigation.findNavController(container!!)
model.status.observe(this, Observer { model.status.observe(this, Observer {
status -> status ->
@ -66,7 +66,7 @@ class LinkParentMailFragment : Fragment(), AuthenticateByMailFragmentListener, F
LinkParentMailViewModelStatus.WaitForAuthentication -> binding.flipper.displayedChild = PAGE_LOGIN LinkParentMailViewModelStatus.WaitForAuthentication -> binding.flipper.displayedChild = PAGE_LOGIN
LinkParentMailViewModelStatus.WaitForConfirmationWithPassword -> binding.flipper.displayedChild = PAGE_READY LinkParentMailViewModelStatus.WaitForConfirmationWithPassword -> binding.flipper.displayedChild = PAGE_READY
LinkParentMailViewModelStatus.ShouldLeaveScreen -> { LinkParentMailViewModelStatus.ShouldLeaveScreen -> {
navigation.popBackStack() requireActivity().execute(UpdateStateCommand.ManageParent.LeaveLinkMail)
null null
} }

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -24,7 +24,6 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import androidx.navigation.Navigation
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import io.timelimit.android.R import io.timelimit.android.R
import io.timelimit.android.data.model.User import io.timelimit.android.data.model.User
@ -33,6 +32,8 @@ import io.timelimit.android.livedata.map
import io.timelimit.android.logic.AppLogic import io.timelimit.android.logic.AppLogic
import io.timelimit.android.logic.DefaultAppLogic import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.ui.main.FragmentWithCustomTitle import io.timelimit.android.ui.main.FragmentWithCustomTitle
import io.timelimit.android.ui.model.UpdateStateCommand
import io.timelimit.android.ui.model.execute
class ChangeParentPasswordFragment : Fragment(), FragmentWithCustomTitle { class ChangeParentPasswordFragment : Fragment(), FragmentWithCustomTitle {
val logic: AppLogic by lazy { DefaultAppLogic.with(context!!) } val logic: AppLogic by lazy { DefaultAppLogic.with(context!!) }
@ -46,14 +47,13 @@ class ChangeParentPasswordFragment : Fragment(), FragmentWithCustomTitle {
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val navigation = Navigation.findNavController(container!!)
val binding = ChangeParentPasswordFragmentBinding.inflate(inflater, container, false) val binding = ChangeParentPasswordFragmentBinding.inflate(inflater, container, false)
parentUser.observe(this, Observer { parentUser.observe(this, Observer {
parentUser -> parentUser ->
if (parentUser == null) { if (parentUser == null) {
navigation.popBackStack(R.id.overviewFragment, false) requireActivity().execute(UpdateStateCommand.ManageParent.Leave)
} }
}) })
@ -104,7 +104,7 @@ class ChangeParentPasswordFragment : Fragment(), FragmentWithCustomTitle {
ChangeParentPasswordViewModelStatus.Done -> { ChangeParentPasswordViewModelStatus.Done -> {
Toast.makeText(context!!, R.string.manage_parent_change_password_toast_success, Toast.LENGTH_SHORT).show() Toast.makeText(context!!, R.string.manage_parent_change_password_toast_success, Toast.LENGTH_SHORT).show()
navigation.popBackStack() requireActivity().execute(UpdateStateCommand.ManageParent.LeaveChangePassword)
null null
} }

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -24,7 +24,6 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import androidx.navigation.Navigation
import io.timelimit.android.R import io.timelimit.android.R
import io.timelimit.android.data.model.User import io.timelimit.android.data.model.User
import io.timelimit.android.databinding.RestoreParentPasswordFragmentBinding import io.timelimit.android.databinding.RestoreParentPasswordFragmentBinding
@ -34,6 +33,8 @@ import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.ui.authentication.AuthenticateByMailFragment import io.timelimit.android.ui.authentication.AuthenticateByMailFragment
import io.timelimit.android.ui.authentication.AuthenticateByMailFragmentListener import io.timelimit.android.ui.authentication.AuthenticateByMailFragmentListener
import io.timelimit.android.ui.main.FragmentWithCustomTitle import io.timelimit.android.ui.main.FragmentWithCustomTitle
import io.timelimit.android.ui.model.UpdateStateCommand
import io.timelimit.android.ui.model.execute
class RestoreParentPasswordFragment : Fragment(), AuthenticateByMailFragmentListener, FragmentWithCustomTitle { class RestoreParentPasswordFragment : Fragment(), AuthenticateByMailFragmentListener, FragmentWithCustomTitle {
companion object { companion object {
@ -56,7 +57,6 @@ class RestoreParentPasswordFragment : Fragment(), AuthenticateByMailFragmentList
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val binding = RestoreParentPasswordFragmentBinding.inflate(inflater, container, false) val binding = RestoreParentPasswordFragmentBinding.inflate(inflater, container, false)
val navigation = Navigation.findNavController(container!!)
model.status.observe(this, Observer { model.status.observe(this, Observer {
status -> status ->
@ -74,14 +74,14 @@ class RestoreParentPasswordFragment : Fragment(), AuthenticateByMailFragmentList
RestoreParentPasswordStatus.NetworkError -> { RestoreParentPasswordStatus.NetworkError -> {
Toast.makeText(context!!, R.string.error_network, Toast.LENGTH_SHORT).show() Toast.makeText(context!!, R.string.error_network, Toast.LENGTH_SHORT).show()
navigation.popBackStack() requireActivity().execute(UpdateStateCommand.ManageParent.LeaveRestorePassword)
null null
} }
RestoreParentPasswordStatus.Done -> { RestoreParentPasswordStatus.Done -> {
Toast.makeText(context!!, R.string.manage_parent_change_password_toast_success, Toast.LENGTH_SHORT).show() Toast.makeText(context!!, R.string.manage_parent_change_password_toast_success, Toast.LENGTH_SHORT).show()
navigation.popBackStack() requireActivity().execute(UpdateStateCommand.ManageParent.LeaveRestorePassword)
null null
} }

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -22,7 +22,6 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.navigation.Navigation
import io.timelimit.android.R import io.timelimit.android.R
import io.timelimit.android.databinding.ManageParentU2fKeyFragmentBinding import io.timelimit.android.databinding.ManageParentU2fKeyFragmentBinding
import io.timelimit.android.livedata.liveDataFromNonNullValue import io.timelimit.android.livedata.liveDataFromNonNullValue
@ -31,12 +30,13 @@ import io.timelimit.android.ui.main.*
import io.timelimit.android.ui.manage.parent.u2fkey.add.AddU2FDialogFragment import io.timelimit.android.ui.manage.parent.u2fkey.add.AddU2FDialogFragment
import io.timelimit.android.ui.manage.parent.u2fkey.remove.RemoveU2FKeyDialogFragment import io.timelimit.android.ui.manage.parent.u2fkey.remove.RemoveU2FKeyDialogFragment
import io.timelimit.android.ui.manage.parent.u2fkey.remove.U2FRequiresPasswordForRemovalDialogFragment import io.timelimit.android.ui.manage.parent.u2fkey.remove.U2FRequiresPasswordForRemovalDialogFragment
import io.timelimit.android.ui.model.UpdateStateCommand
import io.timelimit.android.ui.model.execute
class ManageParentU2FKeyFragment : Fragment(), FragmentWithCustomTitle { class ManageParentU2FKeyFragment : Fragment(), FragmentWithCustomTitle {
val model: ManageParentU2FKeyModel by viewModels() val model: ManageParentU2FKeyModel by viewModels()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val navigation = Navigation.findNavController(container!!)
val params = ManageParentU2FKeyFragmentArgs.fromBundle(requireArguments()) val params = ManageParentU2FKeyFragmentArgs.fromBundle(requireArguments())
val binding = ManageParentU2fKeyFragmentBinding.inflate(inflater, container, false) val binding = ManageParentU2fKeyFragmentBinding.inflate(inflater, container, false)
val activityModel = getActivityViewModel(requireActivity()) val activityModel = getActivityViewModel(requireActivity())
@ -93,7 +93,9 @@ class ManageParentU2FKeyFragment : Fragment(), FragmentWithCustomTitle {
} }
} }
model.user.observe(viewLifecycleOwner) { if (it == null) navigation.popBackStack(R.id.overviewFragment, false) } model.user.observe(viewLifecycleOwner) {
if (it == null) requireActivity().execute(UpdateStateCommand.ManageParent.Leave)
}
model.listItems.observe(viewLifecycleOwner) { adapter.items = it } model.listItems.observe(viewLifecycleOwner) { adapter.items = it }

View file

@ -0,0 +1,27 @@
/*
* 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
import android.os.Bundle
import androidx.fragment.app.Fragment
interface FragmentState {
val containerId: Int
val fragmentClass: Class<out Fragment>
val arguments: Bundle get() = Bundle()
val toolbarIcons: List<Menu.Icon> get() = emptyList()
val toolbarOptions: List<Menu.Dropdown> get() = emptyList()
}

View file

@ -0,0 +1,12 @@
package io.timelimit.android.ui.model
import android.view.View
import androidx.fragment.app.Fragment
abstract class FragmentStateLegacy(
previous: State?,
@Transient override val fragmentClass: Class<out Fragment>,
override val containerId: Int = View.generateViewId()
): State(previous), FragmentState, java.io.Serializable {
override fun toString(): String = fragmentClass.name
}

View file

@ -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.model
import android.app.Application
import android.util.Log
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import io.timelimit.android.BuildConfig
import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.ui.model.launch.LaunchHandling
import io.timelimit.android.ui.model.main.OverviewHandling
import kotlinx.coroutines.flow.*
class MainModel(application: Application): AndroidViewModel(application) {
companion object {
private const val LOG_TAG = "MainModel"
}
private val logic = DefaultAppLogic.with(application)
val state = MutableStateFlow(State.LaunchState as State)
val screen: Flow<Screen> = flow {
while (true) {
when (state.value) {
is State.LaunchState -> LaunchHandling.processLaunchState(state, logic)
is State.Overview -> emitAll(OverviewHandling.processState(logic, viewModelScope, state))
is FragmentState -> emitAll(state.transformWhile {
if (it is FragmentState && it !is State.Overview) {
emit(Screen.FragmentScreen(it, it.toolbarIcons, it.toolbarOptions, it))
true
} else false
})
else -> throw IllegalStateException()
}
}
}
fun execute(command: UpdateStateCommand) {
state.update { oldState ->
command.transform(oldState) ?: oldState.also {
if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "execute($command) did not transform state")
}
}
}
}
}

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -13,13 +13,14 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package io.timelimit.android.extensions package io.timelimit.android.ui.model
import androidx.navigation.NavController import android.app.Activity
import androidx.navigation.NavDirections
fun NavController.safeNavigate(directions: NavDirections, currentScreen: Int) { interface MainModelActivity {
if (this.currentDestination?.id == currentScreen) { fun execute(command: UpdateStateCommand)
navigate(directions)
}
} }
fun Activity.execute(command: UpdateStateCommand) {
(this as MainModelActivity).execute(command)
}

View file

@ -0,0 +1,8 @@
package io.timelimit.android.ui.model
import androidx.compose.ui.graphics.vector.ImageVector
object Menu {
data class Icon(val icon: ImageVector, val labelResource: Int, val action: UpdateStateCommand)
data class Dropdown(val labelResource: Int, val action: UpdateStateCommand)
}

View file

@ -0,0 +1,52 @@
/*
* 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
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Info
import io.timelimit.android.R
import io.timelimit.android.ui.model.main.OverviewHandling
sealed class Screen(
val state: State,
val toolbarIcons: List<Menu.Icon> = emptyList(),
val toolbarOptions: List<Menu.Dropdown> = emptyList()
) {
open class FragmentScreen(
state: State,
toolbarIcons: List<Menu.Icon>,
toolbarOptions: List<Menu.Dropdown>,
val fragment: FragmentState
): Screen(state, toolbarIcons, toolbarOptions)
class OverviewScreen(
state: State,
val content: OverviewHandling.OverviewScreen
): Screen(
state,
listOf(Menu.Icon(
Icons.Outlined.Info,
R.string.main_tab_about,
UpdateStateCommand.Overview.LaunchAbout
)),
listOf(Menu.Dropdown(
R.string.main_tab_uninstall,
UpdateStateCommand.Overview.Uninstall
))
), ScreenWithAuthenticationFab
}
interface ScreenWithAuthenticationFab

View file

@ -0,0 +1,254 @@
/*
* 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
import android.os.Bundle
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DirectionsBike
import androidx.compose.material.icons.filled.Phone
import androidx.fragment.app.Fragment
import io.timelimit.android.R
import io.timelimit.android.ui.contacts.ContactsFragment
import io.timelimit.android.ui.diagnose.*
import io.timelimit.android.ui.diagnose.exitreason.DiagnoseExitReasonFragment
import io.timelimit.android.ui.fragment.*
import io.timelimit.android.ui.manage.category.ManageCategoryFragment
import io.timelimit.android.ui.manage.category.ManageCategoryFragmentArgs
import io.timelimit.android.ui.manage.child.ManageChildFragment
import io.timelimit.android.ui.manage.child.ManageChildFragmentArgs
import io.timelimit.android.ui.manage.device.manage.ManageDeviceFragment
import io.timelimit.android.ui.manage.device.manage.ManageDeviceFragmentArgs
import io.timelimit.android.ui.manage.device.manage.advanced.ManageDeviceAdvancedFragment
import io.timelimit.android.ui.manage.device.manage.advanced.ManageDeviceAdvancedFragmentArgs
import io.timelimit.android.ui.manage.device.manage.feature.ManageDeviceFeaturesFragment
import io.timelimit.android.ui.manage.device.manage.feature.ManageDeviceFeaturesFragmentArgs
import io.timelimit.android.ui.manage.device.manage.permission.ManageDevicePermissionsFragment
import io.timelimit.android.ui.manage.device.manage.permission.ManageDevicePermissionsFragmentArgs
import io.timelimit.android.ui.manage.device.manage.user.ManageDeviceUserFragment
import io.timelimit.android.ui.manage.device.manage.user.ManageDeviceUserFragmentArgs
import io.timelimit.android.ui.manage.parent.ManageParentFragment
import io.timelimit.android.ui.manage.parent.ManageParentFragmentArgs
import io.timelimit.android.ui.manage.parent.link.LinkParentMailFragment
import io.timelimit.android.ui.manage.parent.link.LinkParentMailFragmentArgs
import io.timelimit.android.ui.manage.parent.password.change.ChangeParentPasswordFragment
import io.timelimit.android.ui.manage.parent.password.change.ChangeParentPasswordFragmentArgs
import io.timelimit.android.ui.manage.parent.password.restore.RestoreParentPasswordFragment
import io.timelimit.android.ui.manage.parent.password.restore.RestoreParentPasswordFragmentArgs
import io.timelimit.android.ui.manage.parent.u2fkey.ManageParentU2FKeyFragment
import io.timelimit.android.ui.manage.parent.u2fkey.ManageParentU2FKeyFragmentArgs
import io.timelimit.android.ui.model.main.OverviewHandling
import io.timelimit.android.ui.overview.uninstall.UninstallFragment
import io.timelimit.android.ui.parentmode.ParentModeFragment
import io.timelimit.android.ui.payment.PurchaseFragment
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 java.io.Serializable
sealed class State (val previous: State?): Serializable {
fun hasPrevious(other: State): Boolean = this.previous == other || this.previous?.hasPrevious(other) ?: false
fun find(predicate: (State) -> Boolean): State? =
if (predicate(this)) this
else previous?.find(predicate)
fun first(): State = previous?.first() ?: this
object LaunchState: State(previous = null)
data class Overview(
val state: OverviewHandling.OverviewState = OverviewHandling.OverviewState.empty
): State(previous = null)
class About(previous: Overview): FragmentStateLegacy(previous = previous, fragmentClass = AboutFragmentWrapped::class.java)
class AddUser(previous: Overview): FragmentStateLegacy(previous = previous, fragmentClass = AddUserFragment::class.java)
sealed class ManageChild(previous: State, fragmentClass: Class<out Fragment>): FragmentStateLegacy(previous, fragmentClass) {
class Main(
previous: Overview,
val childId: String,
fromRedirect: Boolean
): ManageChild(previous = previous, ManageChildFragment::class.java) {
@Transient
override val arguments = ManageChildFragmentArgs(childId = childId, fromRedirect = fromRedirect).toBundle()
override val toolbarIcons: List<Menu.Icon> = listOf(
Menu.Icon(
Icons.Default.DirectionsBike,
R.string.manage_child_tasks,
UpdateStateCommand.ManageChild.Tasks
),
Menu.Icon(
Icons.Default.Phone,
R.string.contacts_title_long,
UpdateStateCommand.ManageChild.Contacts
)
)
override val toolbarOptions: List<Menu.Dropdown> = listOf(
Menu.Dropdown(R.string.child_apps_title, UpdateStateCommand.ManageChild.Apps),
Menu.Dropdown(R.string.usage_history_title, UpdateStateCommand.ManageChild.UsageHistory),
Menu.Dropdown(R.string.manage_child_tab_other, UpdateStateCommand.ManageChild.Advanced)
)
}
class Apps(val previousChild: Main): ManageChild(previousChild, ChildAppsFragmentWrapper::class.java) {
@Transient
override val arguments: Bundle = ChildAppsFragmentWrapperArgs(previousChild.childId).toBundle()
}
class Advanced(val previousChild: Main): ManageChild(previousChild, ChildAdvancedFragmentWrapper::class.java) {
@Transient
override val arguments: Bundle = ChildAdvancedFragmentWrapperArgs(previousChild.childId).toBundle()
}
class Contacts(val previousChild: Main): ManageChild(previousChild, ContactsFragment::class.java)
class UsageHistory(val previousChild: Main): ManageChild(previousChild, ChildUsageHistoryFragmentWrapper::class.java) {
@Transient
override val arguments: Bundle = ChildUsageHistoryFragmentWrapperArgs(previousChild.childId).toBundle()
}
class Tasks(val previousChild: Main): ManageChild(previousChild, ChildTasksFragmentWrapper::class.java) {
@Transient
override val arguments: Bundle = ChildTasksFragmentWrapperArgs(previousChild.childId).toBundle()
}
sealed class ManageCategory(previous: State, fragmentClass: Class<out Fragment>): ManageChild(previous, fragmentClass) {
class Main(
val previousChild: ManageChild.Main,
val categoryId: String
): ManageCategory(previous = previousChild, fragmentClass = ManageCategoryFragment::class.java) {
@Transient
override val arguments: Bundle = ManageCategoryFragmentArgs(
childId = previousChild.childId,
categoryId = categoryId
).toBundle()
override val toolbarOptions: List<Menu.Dropdown> = listOf(
Menu.Dropdown(R.string.blocked_time_areas, UpdateStateCommand.ManageChild.BlockedTimes),
Menu.Dropdown(R.string.category_settings, UpdateStateCommand.ManageChild.CategoryAdvanced)
)
}
class BlockedTimes(
val previousCategory: Main
): ManageCategory(previous = previousCategory, fragmentClass = BlockedTimeAreasFragmentWrapper::class.java) {
@Transient
override val arguments: Bundle = BlockedTimeAreasFragmentWrapperArgs(
childId = previousCategory.previousChild.childId,
categoryId = previousCategory.categoryId
).toBundle()
}
class Advanced(
val previousCategory: Main
): ManageCategory(previous = previousCategory, fragmentClass = CategoryAdvancedFragmentWrapper::class.java) {
@Transient
override val arguments: Bundle = CategoryAdvancedFragmentWrapperArgs(
childId = previousCategory.previousChild.childId,
categoryId = previousCategory.categoryId
).toBundle()
}
}
}
sealed class ManageParent(previous: State, fragmentClass: Class<out Fragment>): FragmentStateLegacy(previous = previous, fragmentClass = fragmentClass) {
class Main(
previous: Overview,
val parentId: String
): ManageParent(previous = previous, fragmentClass = ManageParentFragment::class.java) {
@Transient
override val arguments = ManageParentFragmentArgs(parentId).toBundle()
}
class ChangePassword(val previousParent: Main): ManageParent(previousParent, ChangeParentPasswordFragment::class.java) {
@Transient
override val arguments: Bundle = ChangeParentPasswordFragmentArgs(previousParent.parentId).toBundle()
}
class RestorePassword(val previousParent: Main): ManageParent(previousParent, RestoreParentPasswordFragment::class.java) {
@Transient
override val arguments: Bundle = RestoreParentPasswordFragmentArgs(previousParent.parentId).toBundle()
}
class LinkMail(val previousParent: Main): ManageParent(previousParent, LinkParentMailFragment::class.java) {
@Transient
override val arguments: Bundle = LinkParentMailFragmentArgs(previousParent.parentId).toBundle()
}
class U2F(val previousParent: Main): ManageParent(previousParent, ManageParentU2FKeyFragment::class.java) {
@Transient
override val arguments: Bundle = ManageParentU2FKeyFragmentArgs(previousParent.parentId).toBundle()
}
}
sealed class ManageDevice(
previous: State,
fragmentClass: Class<out Fragment>
): FragmentStateLegacy(previous, fragmentClass) {
class Main(
val previousOverview: Overview,
deviceId: String
): ManageDevice(previousOverview, ManageDeviceFragment::class.java) {
@Transient
override val arguments: Bundle = ManageDeviceFragmentArgs(deviceId).toBundle()
}
class User(
val previousMain: Main,
deviceId: String
): ManageDevice(previousMain, ManageDeviceUserFragment::class.java) {
@Transient
override val arguments: Bundle = ManageDeviceUserFragmentArgs(deviceId).toBundle()
}
class Permissions(
val previousMain: Main,
deviceId: String
): ManageDevice(previousMain, ManageDevicePermissionsFragment::class.java) {
@Transient
override val arguments: Bundle = ManageDevicePermissionsFragmentArgs(deviceId).toBundle()
}
class Features(
val previousMain: Main,
deviceId: String
): ManageDevice(previousMain, ManageDeviceFeaturesFragment::class.java) {
@Transient
override val arguments: Bundle = ManageDeviceFeaturesFragmentArgs(deviceId).toBundle()
}
class Advanced(
val previousMain: Main,
deviceId: String
): ManageDevice(previousMain, ManageDeviceAdvancedFragment::class.java) {
@Transient
override val arguments: Bundle = ManageDeviceAdvancedFragmentArgs(deviceId).toBundle()
}
}
class SetupDevice(val previousOverview: Overview): FragmentStateLegacy(previous = previousOverview, fragmentClass = SetupDeviceFragment::class.java)
class Uninstall(previous: Overview): FragmentStateLegacy(previous = previous, fragmentClass = UninstallFragment::class.java)
object DiagnoseScreen {
class Main(previous: About): FragmentStateLegacy(previous, DiagnoseMainFragment::class.java)
class Battery(previous: Main): FragmentStateLegacy(previous, DiagnoseBatteryFragment::class.java)
class Clock(previous: Main): FragmentStateLegacy(previous, DiagnoseClockFragment::class.java)
class Connection(previous: Main): FragmentStateLegacy(previous, DiagnoseConnectionFragment::class.java)
class ExperimentalFlags(previous: Main): FragmentStateLegacy(previous, DiagnoseExperimentalFlagFragment::class.java)
class ExitReasons(previous: Main): FragmentStateLegacy(previous, DiagnoseExitReasonFragment::class.java)
class Crypto(previous: Main): FragmentStateLegacy(previous, DiagnoseCryptoFragment::class.java)
class ForegroundApp(previous: Main): FragmentStateLegacy(previous, DiagnoseForegroundAppFragment::class.java)
class Sync(previous: Main): FragmentStateLegacy(previous, DiagnoseSyncFragment::class.java)
}
object Setup {
class SetupTerms: FragmentStateLegacy(previous = null, fragmentClass = SetupTermsFragment::class.java)
class SetupHelpInfo(previous: SetupTerms): FragmentStateLegacy(previous = previous, fragmentClass = SetupHelpInfoFragment::class.java)
class SelectMode(previous: SetupHelpInfo): FragmentStateLegacy(previous = previous, fragmentClass = SetupSelectModeFragment::class.java)
class DevicePermissions(previous: SelectMode): FragmentStateLegacy(previous = previous, fragmentClass = SetupDevicePermissionsFragment::class.java)
class LocalMode(previous: DevicePermissions): FragmentStateLegacy(previous = previous, fragmentClass = SetupLocalModeFragment::class.java)
class RemoteChild(previous: SelectMode): FragmentStateLegacy(previous = previous, fragmentClass = SetupRemoteChildFragment::class.java)
class ParentMode(previous: SelectMode): FragmentStateLegacy(previous = previous, fragmentClass = SetupParentModeFragment::class.java)
}
class ParentMode: FragmentStateLegacy(previous = null, fragmentClass = ParentModeFragment::class.java)
object Purchase {
class Purchase(previous: About): FragmentStateLegacy(previous, PurchaseFragment::class.java)
class StayAwesome(previous: About): FragmentStateLegacy(previous, StayAwesomeFragment::class.java)
}
}

View file

@ -0,0 +1,395 @@
/*
* 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
import io.timelimit.android.ui.model.main.OverviewHandling
sealed class UpdateStateCommand {
abstract fun transform(state: State): State?
object Reset: UpdateStateCommand() {
override fun transform(state: State) = State.LaunchState
}
object BackToPreviousScreen: UpdateStateCommand() {
override fun transform(state: State): State? = state.previous
}
object Launch {
object LaunchOverview: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.LaunchState) State.Overview()
else null
}
class LaunchChild(val childId: String): UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.LaunchState) State.ManageChild.Main(
childId = childId,
fromRedirect = true,
previous = State.Overview()
)
else null
}
object LaunchDeviceSetup: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.LaunchState) State.SetupDevice(State.Overview())
else null
}
object LaunchTerms: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.LaunchState) State.Setup.SetupTerms()
else null
}
object LaunchParentMode: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.LaunchState) State.ParentMode()
else null
}
}
object Overview {
object LaunchAbout: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.Overview) State.About(state)
else null
}
object AddUser: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.Overview) State.AddUser(state)
else null
}
data class ManageChild(val childId: String): UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.Overview) State.ManageChild.Main(state, childId, fromRedirect = false)
else null
}
data class ManageParent(val childId: String): UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.Overview) State.ManageParent.Main(state, childId)
else null
}
data class ManageDevice(val childId: String): UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.Overview) State.ManageDevice.Main(state, childId)
else null
}
object SetupDevice: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.Overview) State.SetupDevice(state)
else null
}
object Uninstall: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.Overview) State.Uninstall(state)
else null
}
object ShowAllUsers: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.Overview) state.copy(
state = state.state.copy(showAllUsers = true)
)
else null
}
class ShowMoreDevices(val deviceList: OverviewHandling.OverviewState.DeviceList): UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.Overview) state.copy(
state = state.state.copy(visibleDevices = deviceList)
)
else null
}
}
object About {
object Diagnose: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.About) State.DiagnoseScreen.Main(state)
else null
}
object Purchase: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.About) State.Purchase.Purchase(state)
else null
}
object StayAwesome: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.About) State.Purchase.StayAwesome(state)
else null
}
}
object AddUser {
object Leave: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.AddUser) state.previous
else null
}
}
object ManageDevice {
data class User(val childId: String): UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.ManageDevice.Main) State.ManageDevice.User(state, childId)
else null
}
data class Permissions(val childId: String): UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.ManageDevice.Main) State.ManageDevice.Permissions(state, childId)
else null
}
data class Features(val childId: String): UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.ManageDevice.Main) State.ManageDevice.Features(state, childId)
else null
}
data class Advanced(val childId: String): UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.ManageDevice.Main) State.ManageDevice.Advanced(state, childId)
else null
}
object Leave: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.ManageDevice) when (state) {
is State.ManageDevice.Main -> state.previousOverview
is State.ManageDevice.User -> state.previousMain.previousOverview
is State.ManageDevice.Permissions -> state.previousMain.previousOverview
is State.ManageDevice.Features -> state.previousMain.previousOverview
is State.ManageDevice.Advanced -> state.previousMain.previousOverview
}
else null
}
class EnterFromDeviceSetup(val deviceId: String): UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.SetupDevice) State.ManageDevice.Main(
deviceId = deviceId,
previousOverview = state.previousOverview
)
else null
}
}
object ManageChild {
object Apps: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.ManageChild.Main) State.ManageChild.Apps(state)
else null
}
object Advanced: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.ManageChild.Main) State.ManageChild.Advanced(state)
else null
}
object Contacts: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.ManageChild.Main) State.ManageChild.Contacts(state)
else null
}
object UsageHistory: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.ManageChild.Main) State.ManageChild.UsageHistory(state)
else null
}
object Tasks: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.ManageChild.Main) State.ManageChild.Tasks(state)
else null
}
class Category(val childId: String, val categoryId: String): UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.ManageChild.Main && state.childId == childId) State.ManageChild.ManageCategory.Main(previousChild = state, categoryId = categoryId)
else null
}
object BlockedTimes: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.ManageChild.ManageCategory.Main) State.ManageChild.ManageCategory.BlockedTimes(state)
else null
}
object CategoryAdvanced: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.ManageChild.ManageCategory.Main) State.ManageChild.ManageCategory.Advanced(state)
else null
}
object LeaveCategory: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.ManageChild.ManageCategory) when (state) {
is State.ManageChild.ManageCategory.Main -> state.previous
is State.ManageChild.ManageCategory.Advanced -> state.previousCategory.previous
is State.ManageChild.ManageCategory.BlockedTimes -> state.previousCategory.previous
}
else null
}
object LeaveChild: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.ManageChild) when (state) {
is State.ManageChild.Main -> state.previous
is State.ManageChild.Apps -> state.previousChild.previous
is State.ManageChild.Advanced -> state.previousChild.previous
is State.ManageChild.Contacts -> state.previousChild.previous
is State.ManageChild.Tasks -> state.previousChild.previous
is State.ManageChild.UsageHistory -> state.previousChild.previous
is State.ManageChild.ManageCategory.Main -> state.previousChild.previous
is State.ManageChild.ManageCategory.Advanced -> state.previousCategory.previousChild.previous
is State.ManageChild.ManageCategory.BlockedTimes -> state.previousCategory.previousChild.previous
}
else null
}
}
object ManageParent {
object ChangePassword: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.ManageParent.Main) State.ManageParent.ChangePassword(state)
else null
}
object RestorePassword: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.ManageParent.Main) State.ManageParent.RestorePassword(state)
else null
}
object LinkMail: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.ManageParent.Main) State.ManageParent.LinkMail(state)
else null
}
object U2F: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.ManageParent.Main) State.ManageParent.U2F(state)
else null
}
object Leave: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.ManageParent) when (state) {
is State.ManageParent.Main -> state.previous
is State.ManageParent.ChangePassword -> state.previousParent.previous
is State.ManageParent.U2F -> state.previousParent.previous
is State.ManageParent.LinkMail -> state.previousParent.previous
is State.ManageParent.RestorePassword -> state.previousParent.previous
}
else null
}
object LeaveRestorePassword: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.ManageParent.RestorePassword) state.previous
else null
}
object LeaveChangePassword: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.ManageParent.ChangePassword) state.previous
else null
}
object LeaveLinkMail: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.ManageParent.LinkMail) state.previous
else null
}
}
object Diagnose {
object Battery: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.DiagnoseScreen.Main) State.DiagnoseScreen.Battery(state)
else null
}
object Clock: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.DiagnoseScreen.Main) State.DiagnoseScreen.Clock(state)
else null
}
object Connection: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.DiagnoseScreen.Main) State.DiagnoseScreen.Connection(state)
else null
}
object Crypto: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.DiagnoseScreen.Main) State.DiagnoseScreen.Crypto(state)
else null
}
object ExperimentalFlags: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.DiagnoseScreen.Main) State.DiagnoseScreen.ExperimentalFlags(state)
else null
}
object ExitReasons: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.DiagnoseScreen.Main) State.DiagnoseScreen.ExitReasons(state)
else null
}
object ForegroundApp: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.DiagnoseScreen.Main) State.DiagnoseScreen.ForegroundApp(state)
else null
}
object Sync: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.DiagnoseScreen.Main) State.DiagnoseScreen.Sync(state)
else null
}
}
object Setup {
object Help: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.Setup.SetupTerms) State.Setup.SetupHelpInfo(state)
else null
}
object SelectMode: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.Setup.SetupHelpInfo) State.Setup.SelectMode(state)
else null
}
object DevicePermissions: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.Setup.SelectMode) State.Setup.DevicePermissions(state)
else null
}
object LocalMode: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.Setup.DevicePermissions) State.Setup.LocalMode(state)
else null
}
object RemoteChild: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.Setup.SelectMode) State.Setup.RemoteChild(state)
else null
}
object ParentMode: UpdateStateCommand() {
override fun transform(state: State): State? =
if (state is State.Setup.SelectMode) State.Setup.ParentMode(state)
else null
}
}
class RecoverPassword(val parentId: String): UpdateStateCommand() {
override fun transform(state: State): State? =
state.find { it is State.Overview }?.let { overview ->
State.ManageParent.Main(
parentId = parentId,
previous = overview as State.Overview
)
}
}
}

View file

@ -0,0 +1,46 @@
package io.timelimit.android.ui.model.launch
import io.timelimit.android.async.Threads
import io.timelimit.android.coroutines.executeAndWait
import io.timelimit.android.data.model.UserType
import io.timelimit.android.livedata.waitUntilValueMatches
import io.timelimit.android.logic.AppLogic
import io.timelimit.android.ui.model.State
import kotlinx.coroutines.flow.MutableStateFlow
object LaunchHandling {
suspend fun processLaunchState(
state: MutableStateFlow<State>,
logic: AppLogic
) {
val oldValue = state.value
if (oldValue is State.LaunchState) {
state.compareAndSet(oldValue, getInitialState(logic))
}
}
private suspend fun getInitialState(logic: AppLogic): State {
logic.isInitialized.waitUntilValueMatches { it == true }
// TODO: readd the obsolete dialog fragment
return Threads.database.executeAndWait {
val hasDeviceId = logic.database.config().getOwnDeviceIdSync() != null
val hasParentKey = logic.database.config().getParentModeKeySync() != null
if (hasDeviceId) {
val config = logic.database.derivedDataDao().getUserAndDeviceRelatedDataSync()
val overview = State.Overview()
if (config?.userRelatedData?.user?.type == UserType.Child) State.ManageChild.Main(
previous = overview,
childId = config.userRelatedData.user.id,
fromRedirect = true
)
else if (config?.userRelatedData == null) State.SetupDevice(overview)
else overview
} else if (hasParentKey) State.ParentMode()
else State.Setup.SetupTerms()
}
}
}

View file

@ -0,0 +1,299 @@
/*
* 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.main
import androidx.lifecycle.asFlow
import io.timelimit.android.BuildConfig
import io.timelimit.android.async.Threads
import io.timelimit.android.coroutines.executeAndWait
import io.timelimit.android.data.model.Device
import io.timelimit.android.data.model.HintsToShow
import io.timelimit.android.data.model.UserType
import io.timelimit.android.data.model.derived.FullChildTask
import io.timelimit.android.extensions.whileTrue
import io.timelimit.android.integration.platform.RuntimePermissionStatus
import io.timelimit.android.livedata.map
import io.timelimit.android.logic.AppLogic
import io.timelimit.android.logic.ServerApiLevelInfo
import io.timelimit.android.ui.model.Screen
import io.timelimit.android.ui.model.State
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.distinctUntilChanged
import java.util.*
object OverviewHandling {
fun processState(logic: AppLogic, scope: CoroutineScope, stateLive: MutableStateFlow<State>): Flow<Screen> {
val actions: Actions = getActions(logic, scope, stateLive)
val overviewStateLive: Flow<OverviewState> = stateLive.transform { if (it is State.Overview) emit(it.state) }
val overviewState2Live: Flow<State.Overview> = stateLive.transform { if (it is State.Overview) emit(it) }
val overviewScreenLive: Flow<OverviewScreen> = getScreen(logic, actions, overviewStateLive)
val hasMatchingStateLive = stateLive.map { it is State.Overview }.distinctUntilChanged()
return hasMatchingStateLive.whileTrue {
overviewState2Live.combine(overviewScreenLive) { state, overviewScreen ->
Screen.OverviewScreen(state, overviewScreen)
}
}
}
private fun getActions(logic: AppLogic, scope: CoroutineScope, stateLive: MutableStateFlow<State>): Actions = Actions(
hideIntro = {
scope.launch {
Threads.database.executeAndWait {
logic.database.config().setHintsShownSync(HintsToShow.OVERVIEW_INTRODUCTION)
}
}
},
skipTaskReview = { task ->
stateLive.update { oldState ->
if (oldState is State.Overview) oldState.copy(
state = oldState.state.copy(
hiddenTaskIds = oldState.state.hiddenTaskIds + task.task.childTask.taskId
)
)
else oldState
}
}
)
private fun getScreen(logic: AppLogic, actions: Actions, state: Flow<OverviewState>): Flow<OverviewScreen> {
val introLive = getIntroFlags(logic)
val taskToReviewLive = getTaskToReview(logic, state.map { it.hiddenTaskIds })
val usersLive = getUserList(logic, state.map { it.showAllUsers })
val devicesLive = getDeviceList(logic, state.map { it.visibleDevices })
return combine(
introLive, taskToReviewLive, usersLive, devicesLive
) { intro, taskToReview, users, devices ->
OverviewScreen(
intro = intro,
taskToReview = taskToReview,
users = users,
devices = devices,
actions = actions
)
}
}
private fun getUsers(logic: AppLogic): Flow<List<UserItem>> {
val userFlow = logic.database.user().getAllUsersFlow()
val timeFlow = flow {
while (true) {
emit(logic.realTimeLogic.getCurrentTimeInMillis())
delay(5000)
}
}
return userFlow.combine(timeFlow) { users, time ->
users.map { user ->
UserItem(
id = user.id,
name = user.name,
type = user.type,
areLimitsTemporarilyDisabled = user.disableLimitsUntil >= time,
viewingNeedsAuthentication = user.restrictViewingToParents
)
}
}.distinctUntilChanged()
}
private fun getUserList(logic: AppLogic, showAllUsersLive: Flow<Boolean>): Flow<UserList> {
val usersLive = getUsers(logic)
return usersLive.combine(showAllUsersLive) { users, showAllUsers ->
if (showAllUsers || users.none { it.type != UserType.Parent }) UserList(
list = users,
canAdd = true,
canShowMore = false
)
else UserList(
list = users.filter { it.type == UserType.Child },
canAdd = false,
canShowMore = true
)
}
}
private fun getDevices(logic: AppLogic): Flow<List<DeviceItem>> {
val ownDeviceIdLive = logic.database.config().getOwnDeviceIdFlow()
val devicesWithUserNameLive = logic.database.device().getAllDevicesWithUserInfoFlow()
val connectedDevicesLive = logic.websocket.connectedDevices.asFlow()
return combine (
ownDeviceIdLive, devicesWithUserNameLive, connectedDevicesLive
) { ownDeviceId, devicesWithUserName, connectedDevices ->
devicesWithUserName.map { deviceWithUserName ->
DeviceItem(
device = deviceWithUserName.device,
userName = deviceWithUserName.currentUserName,
userType = deviceWithUserName.currentUserType,
isCurrentDevice = deviceWithUserName.device.id == ownDeviceId,
isConnected = connectedDevices.contains(deviceWithUserName.device.id)
)
}
}
}
private fun getDeviceList(logic: AppLogic, deviceListLive: Flow<OverviewState.DeviceList>): Flow<DeviceList> {
val devicesLive = getDevices(logic)
return devicesLive.combine(deviceListLive) { allDevices, deviceList ->
val bareMinimum = allDevices.filter { it.isCurrentDevice || it.device.isImportant }
val allChildDevices = allDevices.filter { it.isCurrentDevice || it.device.isImportant || it.userType == UserType.Child }
val list = when (deviceList) {
OverviewState.DeviceList.BareMinimum -> bareMinimum
OverviewState.DeviceList.AllChildDevices -> allChildDevices
OverviewState.DeviceList.AllDevices -> allDevices
}
val canShowMore = when (deviceList) {
OverviewState.DeviceList.BareMinimum ->
if (bareMinimum.size != allChildDevices.size) OverviewState.DeviceList.AllChildDevices
else if (bareMinimum.size != allDevices.size) OverviewState.DeviceList.AllDevices
else null
OverviewState.DeviceList.AllChildDevices ->
if (allChildDevices.size != allDevices.size) OverviewState.DeviceList.AllDevices
else null
OverviewState.DeviceList.AllDevices -> null
}
val canAdd = list.size == allDevices.size
DeviceList(
list = list,
canAdd = canAdd,
canShowMore = canShowMore
)
}
}
private fun getIntroFlags(logic: AppLogic): Flow<IntroFlags> {
val showSetupOptionLive = logic.deviceUserEntry.map { it == null }.asFlow()
val serverVersionLive = logic.serverApiLevelLogic.infoLive.asFlow()
val showOutdatedServerLive = serverVersionLive.map { serverVersion ->
!serverVersion.hasLevelOrIsOffline(BuildConfig.minimumRecommendServerVersion)
}
val showServerMessageLive = logic.database.config().getServerMessageFlow()
val hasShownIntroductionLive = logic.database.config().wereHintsShown(HintsToShow.OVERVIEW_INTRODUCTION).asFlow()
val showIntroLive = hasShownIntroductionLive.map { !it }
return combine(
showSetupOptionLive, showOutdatedServerLive, showServerMessageLive, showIntroLive
) { showSetupOption, showOutdatedServer, showServerMessage, showIntro ->
IntroFlags(
showSetupOption = showSetupOption,
showOutdatedServer = showOutdatedServer,
showServerMessage = showServerMessage,
showIntro = showIntro
)
}
}
@OptIn(ExperimentalCoroutinesApi::class)
private fun getTaskToReview(logic: AppLogic, hiddenTaskIdsLive: Flow<Set<String>>): Flow<TaskToReview?> {
val pendingTasksLive = logic.database.childTasks().getPendingTasksFlow()
val serverApiLevelLive = logic.serverApiLevelLogic.infoLive.asFlow()
val hasPremiumLive = logic.fullVersion.shouldProvideFullVersionFunctions.asFlow()
val taskWithChildLive = pendingTasksLive.combine(hiddenTaskIdsLive) { pendingTasks, hiddenTaskIds ->
pendingTasks
.filterNot { hiddenTaskIds.contains(it.childTask.taskId) }
.firstOrNull()
}.transformLatest {
if (it != null) {
emitAll(logic.database.user().getChildUserByIdLive(it.childId).asFlow().map { childInfo ->
if (childInfo == null) null
else Pair(it, childInfo.timeZone)
})
} else emit(null)
}
return combine(
serverApiLevelLive, hasPremiumLive, taskWithChildLive
) { serverApiLevel, hasPremium, taskWithChild ->
if (taskWithChild == null) null
else TaskToReview(
task = taskWithChild.first,
childTimezone = TimeZone.getTimeZone(taskWithChild.second),
serverApiLevel = serverApiLevel,
hasPremium = hasPremium
)
}
}
data class OverviewState(
val hiddenTaskIds: Set<String>,
val visibleDevices: DeviceList,
val showAllUsers: Boolean
): java.io.Serializable {
companion object {
val empty = OverviewState(
hiddenTaskIds = emptySet(),
visibleDevices = DeviceList.BareMinimum,
showAllUsers = false
)
}
enum class DeviceList {
BareMinimum, // current device + devices with warnings
AllChildDevices,
AllDevices
}
}
data class OverviewScreen(
val intro: IntroFlags,
val taskToReview: TaskToReview?,
val users: UserList,
val devices: DeviceList,
val actions: Actions
)
data class Actions(
val hideIntro: () -> Unit,
val skipTaskReview: (TaskToReview) -> Unit
)
data class IntroFlags(
val showSetupOption: Boolean,
val showOutdatedServer: Boolean,
val showServerMessage: String?,
val showIntro: Boolean
)
data class TaskToReview(
val task: FullChildTask,
val hasPremium: Boolean,
val childTimezone: TimeZone,
val serverApiLevel: ServerApiLevelInfo
)
data class UserItem(
val id: String,
val name: String,
val type: UserType,
val areLimitsTemporarilyDisabled: Boolean,
val viewingNeedsAuthentication: Boolean
)
data class UserList(val list: List<UserItem>, val canAdd: Boolean, val canShowMore: Boolean)
data class DeviceItem(val device: Device, val userName: String?, val userType: UserType?, val isCurrentDevice: Boolean, val isConnected: Boolean) {
val isMissingRequiredPermission = userType == UserType.Child && (
device.currentUsageStatsPermission == RuntimePermissionStatus.NotGranted || device.missingPermissionAtQOrLater)
}
data class DeviceList(val list: List<DeviceItem>, val canAdd: Boolean, val canShowMore: OverviewState.DeviceList?)
}

View file

@ -1,132 +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/>.
*/
package io.timelimit.android.ui.overview.main
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData
import io.timelimit.android.R
import io.timelimit.android.extensions.safeNavigate
import io.timelimit.android.livedata.*
import io.timelimit.android.ui.fragment.SingleFragmentWrapper
import io.timelimit.android.ui.main.FragmentWithCustomTitle
import io.timelimit.android.ui.manage.device.add.AddDeviceFragment
import io.timelimit.android.ui.overview.about.AboutFragmentParentHandlers
import io.timelimit.android.ui.overview.overview.OverviewFragment
import io.timelimit.android.ui.overview.overview.OverviewFragmentParentHandlers
class MainFragment : SingleFragmentWrapper(), OverviewFragmentParentHandlers, AboutFragmentParentHandlers, FragmentWithCustomTitle {
override val showAuthButton: Boolean = true
override fun createChildFragment(): Fragment = OverviewFragment()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun openAddDeviceScreen() {
AddDeviceFragment().show(parentFragmentManager)
}
override fun openAddUserScreen() {
navigation.safeNavigate(
MainFragmentDirections.actionOverviewFragmentToAddUserFragment(),
R.id.overviewFragment
)
}
override fun openManageChildScreen(childId: String) {
navigation.safeNavigate(
MainFragmentDirections.actionOverviewFragmentToManageChildFragment(childId = childId, fromRedirect = false),
R.id.overviewFragment
)
}
override fun openManageDeviceScreen(deviceId: String) {
navigation.safeNavigate(
MainFragmentDirections.actionOverviewFragmentToManageDeviceFragment(deviceId),
R.id.overviewFragment
)
}
override fun onShowPurchaseScreen() {
navigation.safeNavigate(
MainFragmentDirections.actionOverviewFragmentToPurchaseFragment(),
R.id.overviewFragment
)
}
override fun onShowStayAwesomeScreen() {
navigation.safeNavigate(
MainFragmentDirections.actionOverviewFragmentToStayAwesomeFragment(),
R.id.overviewFragment
)
}
override fun openManageParentScreen(parentId: String) {
navigation.safeNavigate(
MainFragmentDirections.actionOverviewFragmentToManageParentFragment(parentId),
R.id.overviewFragment
)
}
override fun openSetupDeviceScreen() {
navigation.safeNavigate(
MainFragmentDirections.actionOverviewFragmentToSetupDeviceFragment(),
R.id.overviewFragment
)
}
override fun onShowDiagnoseScreen() {
navigation.safeNavigate(
MainFragmentDirections.actionOverviewFragmentToDiagnoseMainFragment(),
R.id.overviewFragment
)
}
override fun getCustomTitle(): LiveData<String?> = liveDataFromNullableValue("${getString(R.string.main_tab_overview)} (${getString(R.string.app_name)})")
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.fragment_main_menu, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
R.id.menu_main_about -> {
navigation.safeNavigate(
MainFragmentDirections.actionOverviewFragmentToAboutFragmentWrapped(),
R.id.overviewFragment
)
true
}
R.id.menu_main_uninstall -> {
navigation.safeNavigate(
MainFragmentDirections.actionOverviewFragmentToUninstallFragment(),
R.id.overviewFragment
)
true
}
else -> super.onOptionsItemSelected(item)
}
}

View file

@ -0,0 +1,133 @@
/*
* 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.overview.overview
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.material.MaterialTheme
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.runtime.Composable
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.model.UpdateStateCommand
import io.timelimit.android.ui.model.main.OverviewHandling
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LazyItemScope.DeviceItem(
item: OverviewHandling.DeviceItem,
executeCommand: (UpdateStateCommand) -> Unit
) {
ListCardCommon.Card(
Modifier
.animateItemPlacement()
.padding(horizontal = 8.dp)
.clickable(
onClick = {
executeCommand(UpdateStateCommand.Overview.ManageDevice(item.device.id))
}
)
) {
ListCardCommon.TextWithIcon(
icon = Icons.Default.Smartphone,
label = stringResource(R.string.overview_device_item_name),
value = item.device.name,
style = MaterialTheme.typography.h6
)
if (item.userName != null) {
ListCardCommon.TextWithIcon(
icon = Icons.Default.AccountCircle,
label = stringResource(R.string.overview_device_item_user_name),
value = item.userName
)
}
if (item.device.isUserKeptSignedIn) {
ListCardCommon.TextWithIcon(
icon = Icons.Default.LockOpen,
label = stringResource(R.string.overview_device_item_password_disabled),
value = stringResource(R.string.overview_device_item_password_disabled),
multiline = true
)
}
if (item.isConnected) {
ListCardCommon.TextWithIcon(
icon = Icons.Default.Wifi,
label = stringResource(R.string.overview_device_item_connected),
value = stringResource(R.string.overview_device_item_connected),
multiline = true
)
}
if (item.device.currentAppVersion < BuildConfig.VERSION_CODE) {
ListCardCommon.TextWithIcon(
icon = Icons.Default.Update,
label = stringResource(R.string.overview_device_item_older_version),
value = stringResource(R.string.overview_device_item_older_version),
tint = MaterialTheme.colors.primary,
multiline = true
)
}
if (item.device.hasAnyManipulation) {
ListCardCommon.TextWithIcon(
icon = Icons.Default.Warning,
label = stringResource(R.string.overview_device_item_manipulation),
value = stringResource(R.string.overview_device_item_manipulation),
tint = MaterialTheme.colors.error,
multiline = true
)
}
if (item.isMissingRequiredPermission) {
ListCardCommon.TextWithIcon(
icon = Icons.Default.Warning,
label = stringResource(R.string.overview_device_item_missing_permission),
value = stringResource(R.string.overview_device_item_missing_permission),
tint = MaterialTheme.colors.error,
multiline = true
)
}
if (item.device.didReportUninstall) {
ListCardCommon.TextWithIcon(
icon = Icons.Default.Warning,
label = stringResource(R.string.overview_device_item_uninstall),
value = stringResource(R.string.overview_device_item_uninstall),
tint = MaterialTheme.colors.error,
multiline = true
)
}
if (item.isCurrentDevice) {
ListCardCommon.TextWithIcon(
icon = null,
label = stringResource(R.string.manage_device_is_this_device),
value = stringResource(R.string.manage_device_is_this_device),
style = MaterialTheme.typography.subtitle1,
multiline = true
)
}
}
}

View file

@ -0,0 +1,89 @@
/*
* 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.overview.overview
import androidx.compose.foundation.layout.*
import androidx.compose.material.Button
import androidx.compose.material.Icon
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.Text
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.graphics.vector.ImageVector
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
object ListCardCommon {
@Composable
fun Card(
modifier: Modifier = Modifier,
content: @Composable ColumnScope.() -> Unit
) {
androidx.compose.material.Card(
modifier = modifier
) {
Column(modifier = Modifier.padding(8.dp)) {
content()
}
}
}
@Composable
fun TextWithIcon(
icon: ImageVector?,
label: String,
value: String,
style: TextStyle = LocalTextStyle.current,
tint: Color = Color.Unspecified,
multiline: Boolean = false
) {
Row {
if (icon != null) Icon(icon, label, tint = tint)
else Spacer(Modifier.size(24.dp))
Spacer(Modifier.width(8.dp))
Text(
value,
modifier = Modifier.weight(1.0f),
maxLines = if (multiline) Int.MAX_VALUE else 1,
overflow = TextOverflow.Ellipsis,
style = style,
color = tint
)
}
}
@Composable
fun ActionButton(
label: String,
action: () -> Unit
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.End
) {
Button(
onClick = action
) {
Text(label)
}
}
}
}

View file

@ -0,0 +1,84 @@
/*
* 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.overview.overview
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import io.timelimit.android.R
object ListCommon {
@Composable
fun SectionHeader(text: String, modifier: Modifier = Modifier) {
Text(
text,
textAlign = TextAlign.Center,
modifier = modifier.fillMaxWidth(),
style = MaterialTheme.typography.h5
)
}
@Composable
fun ActionListItem(
icon: ImageVector,
label: String,
action: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier
.fillMaxWidth()
.clickable(
onClickLabel = label,
onClick = action
)
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(icon, label)
Spacer(modifier = Modifier.width(8.dp))
Text(
label,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
@Composable
fun ShowMoreItem(modifier: Modifier = Modifier, action: () -> Unit) {
ActionListItem(
icon = Icons.Default.ExpandMore,
label = stringResource(R.string.generic_show_more),
action = action,
modifier = modifier
)
}
}

View file

@ -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/>.
*/
package io.timelimit.android.ui.overview.overview
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import io.timelimit.android.async.Threads
import io.timelimit.android.coroutines.CoroutineFragment
import io.timelimit.android.data.model.*
import io.timelimit.android.databinding.FragmentOverviewBinding
import io.timelimit.android.date.DateInTimezone
import io.timelimit.android.livedata.waitForNonNullValue
import io.timelimit.android.logic.AppLogic
import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.logic.ServerApiLevelInfo
import io.timelimit.android.sync.actions.ReviewChildTaskAction
import io.timelimit.android.ui.main.ActivityViewModel
import io.timelimit.android.ui.main.getActivityViewModel
import io.timelimit.android.ui.payment.RequiresPurchaseDialogFragment
import kotlinx.coroutines.launch
import java.util.*
class OverviewFragment : CoroutineFragment() {
private val handlers: OverviewFragmentParentHandlers by lazy { parentFragment as OverviewFragmentParentHandlers }
private val logic: AppLogic by lazy { DefaultAppLogic.with(requireContext()) }
private val auth: ActivityViewModel by lazy { getActivityViewModel(requireActivity()) }
private val model: OverviewFragmentModel by viewModels()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val binding = FragmentOverviewBinding.inflate(inflater, container, false)
val adapter = OverviewFragmentAdapter()
binding.recycler.adapter = adapter
binding.recycler.layoutManager = LinearLayoutManager(requireContext())
adapter.handlers = object: OverviewFragmentHandlers {
override fun onAddUserClicked() {
handlers.openAddUserScreen()
}
override fun onDeviceClicked(device: Device) {
handlers.openManageDeviceScreen(deviceId = device.id)
}
override fun onUserClicked(user: User) {
if (
user.restrictViewingToParents &&
logic.deviceUserId.value != user.id &&
!auth.requestAuthenticationOrReturnTrue()
) {
// do "nothing"/ request authentication
} else {
when (user.type) {
UserType.Child -> handlers.openManageChildScreen(childId = user.id)
UserType.Parent -> handlers.openManageParentScreen(parentId = user.id)
}.let { }
}
}
override fun onAddDeviceClicked() {
launch {
if (logic.database.config().getDeviceAuthTokenAsync().waitForNonNullValue().isEmpty()) {
CanNotAddDevicesInLocalModeDialogFragment()
.apply { setTargetFragment(this@OverviewFragment, 0) }
.show(fragmentManager!!)
} else if (auth.requestAuthenticationOrReturnTrue()) {
handlers.openAddDeviceScreen()
}
}
}
override fun onFinishSetupClicked() {
handlers.openSetupDeviceScreen()
}
override fun onShowAllUsersClicked() {
model.showAllUsers()
}
override fun onSetDeviceListVisibility(level: DeviceListItemVisibility) {
model.showMoreDevices(level)
}
override fun onTaskConfirmed(task: ChildTask, hasPremium: Boolean, timezone: TimeZone, serverApiLevel: ServerApiLevelInfo) {
if (hasPremium) {
val time = logic.timeApi.getCurrentTimeInMillis()
val day = DateInTimezone.newInstance(time, timezone).dayOfEpoch
auth.tryDispatchParentAction(
ReviewChildTaskAction(
taskId = task.taskId,
ok = true,
time = time,
day = if (serverApiLevel.hasLevelOrIsOffline(2)) day else null
)
)
} else RequiresPurchaseDialogFragment().show(parentFragmentManager)
}
override fun onTaskRejected(task: ChildTask) {
auth.tryDispatchParentAction(
ReviewChildTaskAction(
taskId = task.taskId,
ok = false,
time = logic.timeApi.getCurrentTimeInMillis(),
day = null
)
)
}
override fun onSkipTaskReviewClicked(task: ChildTask) {
if (auth.requestAuthenticationOrReturnTrue()) model.hideTask(task.taskId)
}
}
model.listEntries.observe(viewLifecycleOwner) { adapter.data = it }
ItemTouchHelper(
object: ItemTouchHelper.Callback() {
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
val index = viewHolder.adapterPosition
val item = if (index == RecyclerView.NO_POSITION) null else adapter.data!![index]
if (item == OverviewFragmentHeaderIntro) {
return makeFlag(ItemTouchHelper.ACTION_STATE_SWIPE, ItemTouchHelper.START or ItemTouchHelper.END) or
makeFlag(ItemTouchHelper.ACTION_STATE_IDLE, ItemTouchHelper.END or ItemTouchHelper.END)
} else {
return 0
}
}
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder) = throw IllegalStateException()
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
// remove the introduction header
Threads.database.execute {
logic.database.config().setHintsShownSync(HintsToShow.OVERVIEW_INTRODUCTION)
}
}
}
).attachToRecyclerView(binding.recycler)
return binding.root
}
}
interface OverviewFragmentParentHandlers {
fun openAddUserScreen()
fun openAddDeviceScreen()
fun openManageDeviceScreen(deviceId: String)
fun openManageChildScreen(childId: String)
fun openManageParentScreen(parentId: String)
fun openSetupDeviceScreen()
}

View file

@ -1,330 +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/>.
*/
package io.timelimit.android.ui.overview.overview
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import io.timelimit.android.BuildConfig
import io.timelimit.android.R
import io.timelimit.android.data.model.ChildTask
import io.timelimit.android.data.model.Device
import io.timelimit.android.data.model.User
import io.timelimit.android.data.model.UserType
import io.timelimit.android.databinding.*
import io.timelimit.android.logic.ServerApiLevelInfo
import io.timelimit.android.ui.util.DateUtil
import io.timelimit.android.util.TimeTextUtil
import java.util.*
import kotlin.properties.Delegates
class OverviewFragmentAdapter : RecyclerView.Adapter<OverviewFragmentViewHolder>() {
init {
setHasStableIds(true)
}
var data: List<OverviewFragmentItem>? by Delegates.observable(null as List<OverviewFragmentItem>?) { _, _, _ -> notifyDataSetChanged() }
var handlers: OverviewFragmentHandlers? = null
fun getItem(index: Int): OverviewFragmentItem {
return data!![index]
}
override fun getItemId(position: Int): Long {
val item = getItem(position)
return when (item) {
is OverviewFragmentItemDevice -> "device ${item.device.id}".hashCode().toLong()
is OverviewFragmentItemUser -> "user ${item.user.id}".hashCode().toLong()
is TaskReviewOverviewItem -> "task ${item.task.taskId}".hashCode().toLong()
else -> item.hashCode().toLong()
}
}
override fun getItemCount(): Int {
val data = this.data
if (data == null) {
return 0
} else {
return data.size
}
}
private fun getItemType(item: OverviewFragmentItem): OverviewFragmentViewType = when(item) {
is OverviewFragmentHeaderUsers -> OverviewFragmentViewType.Header
is OverviewFragmentHeaderDevices -> OverviewFragmentViewType.Header
is OverviewFragmentItemUser -> OverviewFragmentViewType.UserItem
is OverviewFragmentItemDevice -> OverviewFragmentViewType.DeviceItem
is OverviewFragmentActionAddUser -> OverviewFragmentViewType.AddUserItem
is OverviewFragmentActionAddDevice -> OverviewFragmentViewType.AddDeviceItem
is OverviewFragmentHeaderIntro -> OverviewFragmentViewType.Introduction
is OverviewFragmentHeaderFinishSetup -> OverviewFragmentViewType.FinishSetup
is OverviewFragmentItemMessage -> OverviewFragmentViewType.ServerMessage
is ShowMoreOverviewFragmentItem -> OverviewFragmentViewType.ShowMoreButton
is TaskReviewOverviewItem -> OverviewFragmentViewType.TaskReview
is OverviewFragmentItemOutdatedServer -> OverviewFragmentViewType.ServerMessage
}
override fun getItemViewType(position: Int) = getItemType(getItem(position)).ordinal
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when(viewType) {
OverviewFragmentViewType.Header.ordinal ->
HeaderViewHolder(
GenericListHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
OverviewFragmentViewType.UserItem.ordinal ->
UserViewHolder(
FragmentOverviewUserItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
OverviewFragmentViewType.DeviceItem.ordinal ->
DeviceViewHolder(
FragmentOverviewDeviceItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
OverviewFragmentViewType.AddUserItem.ordinal ->
AddUserViewHolder(
AddItemViewBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
).apply {
label = parent.context.getString(R.string.add_user_title)
root.setOnClickListener {
handlers?.onAddUserClicked()
}
}.root
)
OverviewFragmentViewType.AddDeviceItem.ordinal -> AddDeviceViewHolder(
AddItemViewBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
).apply {
label = parent.context.getString(R.string.overview_add_device)
root.setOnClickListener {
handlers?.onAddDeviceClicked()
}
}.root
)
OverviewFragmentViewType.Introduction.ordinal -> IntroViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.fragment_overview_intro, parent, false)
)
OverviewFragmentViewType.FinishSetup.ordinal -> FinishSetupViewHolder(
FragmentOverviewFinishSetupBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
).apply {
btnGo.setOnClickListener { handlers?.onFinishSetupClicked() }
}.root
)
OverviewFragmentViewType.ServerMessage.ordinal -> ServerMessageViewHolder(
FragmentOverviewServerMessageBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
OverviewFragmentViewType.ShowMoreButton.ordinal -> ShowMoreViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.show_more_list_item, parent, false)
)
OverviewFragmentViewType.TaskReview.ordinal -> TaskReviewHolder(
FragmentOverviewTaskReviewBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
else -> throw IllegalStateException()
}
override fun onBindViewHolder(holder: OverviewFragmentViewHolder, position: Int) {
val context = holder.itemView.context
val item = getItem(position)
when (item) {
is OverviewFragmentHeaderUsers -> {
if (holder !is HeaderViewHolder) {
throw IllegalStateException()
}
holder.header.text = holder.itemView.context.getString(R.string.overview_header_users)
holder.header.executePendingBindings()
}
is OverviewFragmentHeaderDevices -> {
if (holder !is HeaderViewHolder) {
throw IllegalStateException()
}
holder.header.text = holder.itemView.context.getString(R.string.overview_header_devices)
holder.header.executePendingBindings()
}
is OverviewFragmentItemUser -> {
if (holder !is UserViewHolder) {
throw IllegalStateException()
}
val binding = holder.binding
binding.username = item.user.name
binding.areTimeLimitsDisabled = item.limitsTemporarilyDisabled
binding.isTemporarilyBlocked = item.temporarilyBlocked
binding.isParent = item.user.type == UserType.Parent
binding.isChild = item.user.type == UserType.Child
binding.card.setOnClickListener {
this.handlers?.onUserClicked(item.user)
}
binding.executePendingBindings()
}
is OverviewFragmentItemDevice -> {
if (holder !is DeviceViewHolder) {
throw IllegalStateException()
}
val binding = holder.binding
binding.deviceTitle = item.device.name
binding.currentDeviceUserTitle = item.deviceUser?.name
binding.hasManipulation = item.device.hasAnyManipulation
binding.isCurrentDevice = item.isCurrentDevice
binding.isMissingRequiredPermission = item.isMissingRequiredPermission
binding.didUninstall = item.device.didReportUninstall
binding.isPasswordDisabled = item.device.isUserKeptSignedIn
binding.isConnected = item.isConnected
binding.isUsingOlderVersion = item.device.currentAppVersion < BuildConfig.VERSION_CODE
binding.executePendingBindings()
binding.card.setOnClickListener {
this.handlers?.onDeviceClicked(item.device)
}
}
is OverviewFragmentActionAddUser -> { /* nothing to do */ }
is OverviewFragmentActionAddDevice-> { /* nothing to do */ }
is OverviewFragmentHeaderIntro -> { /* nothing to do */ }
is OverviewFragmentHeaderFinishSetup -> { /* nothing to do */ }
is OverviewFragmentItemMessage -> {
holder as ServerMessageViewHolder
holder.binding.title = context.getString(R.string.overview_server_message)
holder.binding.text = item.message
holder.binding.executePendingBindings()
}
is ShowMoreOverviewFragmentItem -> {
holder as ShowMoreViewHolder
when (item) {
is ShowMoreOverviewFragmentItem.ShowAllUsers -> {
holder.itemView.setOnClickListener { handlers?.onShowAllUsersClicked() }
}
is ShowMoreOverviewFragmentItem.ShowMoreDevices -> {
holder.itemView.setOnClickListener { handlers?.onSetDeviceListVisibility(item.level) }
}
}.let { }
}
is TaskReviewOverviewItem -> {
holder as TaskReviewHolder
holder.binding.let {
it.categoryTitle = item.categoryTitle
it.childName = item.childTitle
it.duration = TimeTextUtil.time(item.task.extraTimeDuration, it.root.context)
it.lastGrant = if (item.task.lastGrantTimestamp == 0L) null else DateUtil.formatAbsoluteDate(it.root.context, item.task.lastGrantTimestamp)
it.taskTitle = item.task.taskTitle
it.yesButton.setOnClickListener {
handlers?.onTaskConfirmed(
task = item.task,
hasPremium = item.hasPremium,
timezone = item.childTimezone,
serverApiLevel = item.serverApiLevel
)
}
it.noButton.setOnClickListener { handlers?.onTaskRejected(item.task) }
it.skipButton.setOnClickListener { handlers?.onSkipTaskReviewClicked(item.task) }
}
holder.binding.executePendingBindings()
}
is OverviewFragmentItemOutdatedServer -> {
holder as ServerMessageViewHolder
holder.binding.title = context.getString(R.string.overview_server_outdated_title)
holder.binding.text = context.getString(R.string.overview_server_outdated_text)
holder.binding.executePendingBindings()
}
}.let { }
}
}
enum class OverviewFragmentViewType {
Header,
UserItem,
DeviceItem,
AddUserItem,
AddDeviceItem,
Introduction,
FinishSetup,
ServerMessage,
ShowMoreButton,
TaskReview
}
sealed class OverviewFragmentViewHolder(view: View): RecyclerView.ViewHolder(view)
class HeaderViewHolder(val header: GenericListHeaderBinding): OverviewFragmentViewHolder(header.root)
class AddUserViewHolder(view: View): OverviewFragmentViewHolder(view)
class UserViewHolder(val binding: FragmentOverviewUserItemBinding): OverviewFragmentViewHolder(binding.root)
class DeviceViewHolder(val binding: FragmentOverviewDeviceItemBinding): OverviewFragmentViewHolder(binding.root)
class AddDeviceViewHolder(view: View): OverviewFragmentViewHolder(view)
class IntroViewHolder(view: View): OverviewFragmentViewHolder(view)
class FinishSetupViewHolder(view: View): OverviewFragmentViewHolder(view)
class ServerMessageViewHolder(val binding: FragmentOverviewServerMessageBinding): OverviewFragmentViewHolder(binding.root)
class ShowMoreViewHolder(view: View): OverviewFragmentViewHolder(view)
class TaskReviewHolder(val binding: FragmentOverviewTaskReviewBinding): OverviewFragmentViewHolder(binding.root)
interface OverviewFragmentHandlers {
fun onAddUserClicked()
fun onAddDeviceClicked()
fun onUserClicked(user: User)
fun onDeviceClicked(device: Device)
fun onFinishSetupClicked()
fun onShowAllUsersClicked()
fun onSetDeviceListVisibility(level: DeviceListItemVisibility)
fun onSkipTaskReviewClicked(task: ChildTask)
fun onTaskConfirmed(task: ChildTask, hasPremium: Boolean, timezone: TimeZone, serverApiLevel: ServerApiLevelInfo)
fun onTaskRejected(task: ChildTask)
}

View file

@ -1,54 +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/>.
*/
package io.timelimit.android.ui.overview.overview
import io.timelimit.android.data.model.ChildTask
import io.timelimit.android.data.model.Device
import io.timelimit.android.data.model.User
import io.timelimit.android.data.model.UserType
import io.timelimit.android.integration.platform.RuntimePermissionStatus
import io.timelimit.android.logic.ServerApiLevelInfo
import java.util.*
sealed class OverviewFragmentItem
object OverviewFragmentHeaderUsers: OverviewFragmentItem()
object OverviewFragmentHeaderDevices: OverviewFragmentItem()
data class OverviewFragmentItemDevice(val device: Device, val deviceUser: User?, val isCurrentDevice: Boolean, val isConnected: Boolean): OverviewFragmentItem() {
val isMissingRequiredPermission = deviceUser?.type == UserType.Child && (
device.currentUsageStatsPermission == RuntimePermissionStatus.NotGranted || device.missingPermissionAtQOrLater
)
val isImportant get() = device.isImportant
}
data class OverviewFragmentItemUser(val user: User, val temporarilyBlocked: Boolean, val limitsTemporarilyDisabled: Boolean): OverviewFragmentItem()
object OverviewFragmentActionAddUser: OverviewFragmentItem()
object OverviewFragmentActionAddDevice: OverviewFragmentItem()
object OverviewFragmentHeaderIntro: OverviewFragmentItem()
object OverviewFragmentHeaderFinishSetup: OverviewFragmentItem()
data class OverviewFragmentItemMessage(val message: String): OverviewFragmentItem()
object OverviewFragmentItemOutdatedServer: OverviewFragmentItem()
sealed class ShowMoreOverviewFragmentItem: OverviewFragmentItem() {
object ShowAllUsers: ShowMoreOverviewFragmentItem()
data class ShowMoreDevices(val level: DeviceListItemVisibility): ShowMoreOverviewFragmentItem()
}
data class TaskReviewOverviewItem(
val task: ChildTask,
val childTitle: String,
val categoryTitle: String,
val hasPremium: Boolean,
val childTimezone: TimeZone,
val serverApiLevel: ServerApiLevelInfo
): OverviewFragmentItem()

View file

@ -1,205 +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/>.
*/
package io.timelimit.android.ui.overview.overview
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import io.timelimit.android.BuildConfig
import io.timelimit.android.data.model.HintsToShow
import io.timelimit.android.data.model.UserType
import io.timelimit.android.livedata.*
import io.timelimit.android.logic.DefaultAppLogic
import java.util.*
class OverviewFragmentModel(application: Application): AndroidViewModel(application) {
private val logic = DefaultAppLogic.with(application)
private val itemVisibility = MutableLiveData<OverviewItemVisibility>().apply { value = OverviewItemVisibility.default }
private val categoryEntries = logic.database.category().getAllCategoriesShortInfo()
private val usersWithTemporarilyDisabledLimits = logic.database.user().getAllUsersLive().switchMap {
users ->
liveDataFromFunction { logic.realTimeLogic.getCurrentTimeInMillis() }.map {
currentTime ->
users.map {
user ->
user to (user.disableLimitsUntil >= currentTime)
}
}
}.ignoreUnchanged()
private val userEntries = usersWithTemporarilyDisabledLimits.switchMap { users ->
categoryEntries.switchMap { categories ->
liveDataFromFunction (5000) { logic.realTimeLogic.getCurrentTimeInMillis() }.map { now ->
users.map { user ->
OverviewFragmentItemUser(
user = user.first,
limitsTemporarilyDisabled = user.second,
temporarilyBlocked = categories.find { category ->
category.childId == user.first.id &&
category.temporarilyBlocked && (
category.temporarilyBlockedEndTime == 0L ||
category.temporarilyBlockedEndTime > now
)
} != null
)
}
}
}.ignoreUnchanged()
}
private val ownDeviceId = logic.deviceId
private val devices = logic.database.device().getAllDevicesLive()
private val devicesWithUsers = devices.switchMap { devices ->
usersWithTemporarilyDisabledLimits.map { users ->
devices.map { device ->
device to users.find { it.first.id == device.currentUserId }
}
}
}
private val deviceEntries = ownDeviceId.switchMap { thisDeviceId ->
devicesWithUsers.switchMap { devices ->
logic.websocket.connectedDevices.map { connectedDevices ->
devices.map { (device, user) ->
OverviewFragmentItemDevice(
device = device,
deviceUser = user?.first,
isCurrentDevice = device.id == thisDeviceId,
isConnected = connectedDevices.contains(device.id)
)
}
}
}
}
private val isNoUserAssignedLive = logic.deviceUserEntry.map { it == null }.ignoreUnchanged()
private val hasShownIntroduction = logic.database.config().wereHintsShown(HintsToShow.OVERVIEW_INTRODUCTION)
private val messageLive = logic.database.config().getServerMessage()
private val serverVersion = logic.serverApiLevelLogic.infoLive
private val introEntries = mergeLiveDataWaitForValues(isNoUserAssignedLive, hasShownIntroduction, messageLive, serverVersion)
.map { (noUserAssigned, hasShownIntro, message, serverVersion) ->
val result = mutableListOf<OverviewFragmentItem>()
if (noUserAssigned) {
result.add(OverviewFragmentHeaderFinishSetup)
}
if (!serverVersion.hasLevelOrIsOffline(BuildConfig.minimumRecommendServerVersion)) {
result.add(OverviewFragmentItemOutdatedServer)
}
if (message != null) {
result.add(OverviewFragmentItemMessage(message))
}
if (!hasShownIntro) {
result.add(OverviewFragmentHeaderIntro)
}
result
}
private val hiddenTaskIdsLive = MutableLiveData<Set<String>>().apply { value = emptySet() }
private val tasksWithPendingReviewLive = logic.database.childTasks().getPendingTasks()
private val pendingTasksToShowLive = hiddenTaskIdsLive.switchMap { hiddenTaskIds ->
tasksWithPendingReviewLive.map { tasksWithPendingReview ->
tasksWithPendingReview.filterNot { hiddenTaskIds.contains(it.childTask.taskId) }
}
}
private val hasPremiumLive = logic.fullVersion.shouldProvideFullVersionFunctions
private val pendingTaskItemLive = hasPremiumLive.switchMap { hasPremium ->
logic.serverApiLevelLogic.infoLive.switchMap { serverApiLevel ->
pendingTasksToShowLive.map { tasks ->
tasks.firstOrNull()?.let {
TaskReviewOverviewItem(
task = it.childTask,
childTitle = it.childName,
categoryTitle = it.categoryTitle,
hasPremium = hasPremium,
childTimezone = TimeZone.getTimeZone(it.childTimezone),
serverApiLevel = serverApiLevel
)
}
}
}
}
fun hideTask(taskId: String) {
hiddenTaskIdsLive.value = (hiddenTaskIdsLive.value ?: emptySet()) + setOf(taskId)
}
val listEntries = introEntries.switchMap { introEntries ->
deviceEntries.switchMap { deviceEntries ->
userEntries.switchMap { userEntries ->
pendingTaskItemLive.switchMap { pendingTaskItem ->
itemVisibility.map { itemVisibility ->
mutableListOf<OverviewFragmentItem>().apply {
addAll(introEntries)
if (pendingTaskItem != null) add(pendingTaskItem)
add(OverviewFragmentHeaderDevices)
val shownDevices = when (itemVisibility.devices) {
DeviceListItemVisibility.BareMinimum -> deviceEntries.filter { it.isCurrentDevice || it.isImportant }
DeviceListItemVisibility.AllChildDevices -> deviceEntries.filter { it.isCurrentDevice || it.isImportant || it.deviceUser?.type == UserType.Child }
DeviceListItemVisibility.AllDevices -> deviceEntries
}
addAll(shownDevices)
if (shownDevices.size == deviceEntries.size) {
add(OverviewFragmentActionAddDevice)
} else {
add(ShowMoreOverviewFragmentItem.ShowMoreDevices(when (itemVisibility.devices) {
DeviceListItemVisibility.BareMinimum -> run {
if (
deviceEntries.any {
!it.isCurrentDevice &&
!it.isImportant &&
it.deviceUser?.type == UserType.Child
}
) DeviceListItemVisibility.AllChildDevices else DeviceListItemVisibility.AllDevices
}
DeviceListItemVisibility.AllChildDevices -> DeviceListItemVisibility.AllDevices
DeviceListItemVisibility.AllDevices -> DeviceListItemVisibility.AllDevices
}))
}
add(OverviewFragmentHeaderUsers)
userEntries.forEach { if (it.user.type != UserType.Parent) add(it) }
if (itemVisibility.showParentUsers || userEntries.all { it.user.type == UserType.Parent }) {
userEntries.forEach { if (it.user.type == UserType.Parent) add(it) }
add(OverviewFragmentActionAddUser)
} else {
add(ShowMoreOverviewFragmentItem.ShowAllUsers)
}
}.toList()
}
} as LiveData<List<OverviewFragmentItem>>
}
}
}
fun showAllUsers() {
itemVisibility.value = itemVisibility.value!!.copy(showParentUsers = true)
}
fun showMoreDevices(level: DeviceListItemVisibility) {
itemVisibility.value = itemVisibility.value!!.copy(devices = level)
}
}

View file

@ -1,38 +0,0 @@
/*
* TimeLimit Copyright <C> 2019 - 2021 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.overview.overview
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class OverviewItemVisibility(
val showParentUsers: Boolean,
val devices: DeviceListItemVisibility
) : Parcelable {
companion object {
val default = OverviewItemVisibility(
showParentUsers = false,
devices = DeviceListItemVisibility.BareMinimum
)
}
}
enum class DeviceListItemVisibility {
BareMinimum, // current device + devices with warnings
AllChildDevices,
AllDevices
}

View file

@ -0,0 +1,287 @@
/*
* 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.overview.overview
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.lifecycleScope
import io.timelimit.android.R
import io.timelimit.android.date.DateInTimezone
import io.timelimit.android.livedata.waitForNonNullValue
import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.sync.actions.ReviewChildTaskAction
import io.timelimit.android.ui.MainActivity
import io.timelimit.android.ui.manage.device.add.AddDeviceFragment
import io.timelimit.android.ui.model.UpdateStateCommand
import io.timelimit.android.ui.model.main.OverviewHandling
import io.timelimit.android.ui.payment.RequiresPurchaseDialogFragment
import io.timelimit.android.util.TimeTextUtil
import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class)
@Composable
fun OverviewScreen(
screen: OverviewHandling.OverviewScreen,
executeCommand: (UpdateStateCommand) -> Unit,
modifier: Modifier = Modifier
) {
val activity = LocalContext.current as MainActivity
LazyColumn (
contentPadding = PaddingValues(0.dp, 8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = modifier
) {
if (screen.intro.showSetupOption) {
item (key = Pair("intro", "finish setup")) {
ListCardCommon.Card(
modifier = Modifier
.animateItemPlacement()
.padding(horizontal = 8.dp)
) {
Text(
stringResource(R.string.overview_finish_setup_title),
style = MaterialTheme.typography.h6
)
Text(stringResource(R.string.overview_finish_setup_text))
ListCardCommon.ActionButton(
label = stringResource(R.string.generic_go),
action = {
executeCommand(UpdateStateCommand.Overview.SetupDevice)
}
)
}
}
}
if (screen.intro.showOutdatedServer) {
item (key = Pair("intro", "outdated server")) {
ListCardCommon.Card(
modifier = Modifier
.animateItemPlacement()
.padding(horizontal = 8.dp)
) {
Text(
stringResource(R.string.overview_server_outdated_title),
style = MaterialTheme.typography.h6
)
Text(stringResource(R.string.overview_server_outdated_text))
}
}
}
if (screen.intro.showServerMessage != null) {
item (key = Pair("intro", "servermessage")) {
ListCardCommon.Card(
modifier = Modifier
.animateItemPlacement()
.padding(horizontal = 8.dp)
) {
Text(
stringResource(R.string.overview_server_message),
style = MaterialTheme.typography.h6
)
Text(screen.intro.showServerMessage)
}
}
}
if (screen.intro.showIntro) {
item (key = Pair("intro", "intro")) {
val state = remember {
DismissState(
initialValue = DismissValue.Default,
confirmStateChange = {
screen.actions.hideIntro()
true
}
)
}
SwipeToDismiss(
state = state,
background = {},
modifier = Modifier.animateItemPlacement()
) {
ListCardCommon.Card(
modifier = Modifier.padding(horizontal = 8.dp)
) {
Text(
stringResource(R.string.overview_intro_title),
style = MaterialTheme.typography.h6
)
Text(stringResource(R.string.overview_intro_text))
Text(
stringResource(R.string.generic_swipe_to_dismiss),
style = MaterialTheme.typography.subtitle1
)
}
}
}
}
if (screen.taskToReview != null) {
item (key = Pair("task", "review")) {
ListCardCommon.Card(
modifier = Modifier
.animateItemPlacement()
.padding(horizontal = 8.dp)
) {
Text(
stringResource(R.string.task_review_title),
style = MaterialTheme.typography.h6
)
Text(
stringResource(R.string.task_review_text, screen.taskToReview.task.childName, screen.taskToReview.task.childTask.taskTitle)
)
Text(
stringResource(
R.string.task_review_category,
TimeTextUtil.time(screen.taskToReview.task.childTask.extraTimeDuration, LocalContext.current),
screen.taskToReview.task.categoryTitle
),
style = MaterialTheme.typography.subtitle1
)
Row {
val auth = activity.getActivityViewModel()
val logic = auth.logic
TextButton(onClick = {
if (activity.getActivityViewModel().isParentAuthenticated()) {
screen.actions.skipTaskReview(screen.taskToReview)
} else activity.showAuthenticationScreen()
}) {
Text(stringResource(R.string.generic_skip))
}
Spacer(Modifier.weight(1.0f))
OutlinedButton(onClick = {
if (activity.getActivityViewModel().isParentAuthenticated()) {
auth.tryDispatchParentAction(
ReviewChildTaskAction(
taskId = screen.taskToReview.task.childTask.taskId,
ok = false,
time = logic.timeApi.getCurrentTimeInMillis(),
day = null
)
)
} else activity.showAuthenticationScreen()
}) {
Text(stringResource(R.string.generic_no))
}
Spacer(Modifier.width(8.dp))
OutlinedButton(onClick = {
if (activity.getActivityViewModel().isParentAuthenticated()) {
if (screen.taskToReview.hasPremium) {
val time = logic.timeApi.getCurrentTimeInMillis()
val day = DateInTimezone.newInstance(time, screen.taskToReview.childTimezone).dayOfEpoch
auth.tryDispatchParentAction(
ReviewChildTaskAction(
taskId = screen.taskToReview.task.childTask.taskId,
ok = true,
time = time,
day = if (screen.taskToReview.serverApiLevel.hasLevelOrIsOffline(2)) day else null
)
)
} else RequiresPurchaseDialogFragment().show(activity.supportFragmentManager)
} else activity.showAuthenticationScreen()
}) {
Text(stringResource(R.string.generic_yes))
}
}
Text(
stringResource(R.string.purchase_required_info_local_mode_free),
style = MaterialTheme.typography.subtitle1
)
}
}
}
item (key = Pair("devices", "header")) { ListCommon.SectionHeader(stringResource(R.string.overview_header_devices), Modifier.animateItemPlacement()) }
items(screen.devices.list, key = { Pair("device", it.device.id) }) {
DeviceItem(it, executeCommand)
}
if (screen.devices.canAdd) {
// TODO: implement this without dependency on MainActivity
item (key = Pair("devices", "add")) {
ListCommon.ActionListItem(
icon = Icons.Default.Add,
label = stringResource(R.string.add_device),
action = {
activity.lifecycleScope.launch {
val logic = DefaultAppLogic.with(activity)
if (logic.database.config().getDeviceAuthTokenAsync()
.waitForNonNullValue().isEmpty()
) {
CanNotAddDevicesInLocalModeDialogFragment()
.show(activity.supportFragmentManager)
} else if (activity.getActivityViewModel().requestAuthenticationOrReturnTrue()) {
AddDeviceFragment().show(activity.supportFragmentManager)
}
}
},
modifier = Modifier.animateItemPlacement()
)
}
}
if (screen.devices.canShowMore != null) {
item (key = Pair("devices", "show more")) { ListCommon.ShowMoreItem(modifier = Modifier.animateItemPlacement()) {
executeCommand(UpdateStateCommand.Overview.ShowMoreDevices(screen.devices.canShowMore))
}}
}
item (key = Pair("header", "users")) { ListCommon.SectionHeader(stringResource(R.string.overview_header_users), Modifier.animateItemPlacement()) }
items(screen.users.list, key = { Pair("user", it.id) }) { UserItem(it, executeCommand) }
if (screen.users.canAdd) item (key = Pair("header", "user.create")) {
ListCommon.ActionListItem(
icon = Icons.Default.Add,
label = stringResource(R.string.add_user_title),
action = { executeCommand(UpdateStateCommand.Overview.AddUser) },
modifier = Modifier.animateItemPlacement()
)
}
if (screen.users.canShowMore) item (key = Pair("header", "user.more")) {
ListCommon.ShowMoreItem (modifier = Modifier.animateItemPlacement()) { executeCommand(UpdateStateCommand.Overview.ShowAllUsers) }
}
}
}

View file

@ -0,0 +1,94 @@
/*
* 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.overview.overview
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.material.MaterialTheme
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.filled.AlarmOff
import androidx.compose.material.icons.filled.Security
import androidx.compose.material.icons.filled.Settings
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.timelimit.android.R
import io.timelimit.android.data.model.UserType
import io.timelimit.android.ui.MainActivity
import io.timelimit.android.ui.model.UpdateStateCommand
import io.timelimit.android.ui.model.main.OverviewHandling
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LazyItemScope.UserItem(
user: OverviewHandling.UserItem,
executeCommand: (UpdateStateCommand) -> Unit
) {
// TODO: implement this without dependency on MainActivity
val activity = LocalContext.current as MainActivity
ListCardCommon.Card(
Modifier
.animateItemPlacement()
.padding(horizontal = 8.dp)
.clickable(
onClick = {
when (user.type) {
UserType.Child -> {
if (!user.viewingNeedsAuthentication || activity.getActivityViewModel().isParentOrChildAuthenticated(user.id)) {
executeCommand(UpdateStateCommand.Overview.ManageChild(user.id))
} else {
activity.showAuthenticationScreen()
}
}
UserType.Parent -> executeCommand(UpdateStateCommand.Overview.ManageParent(user.id))
}
}
)
) {
ListCardCommon.TextWithIcon(
icon = Icons.Default.AccountCircle,
label = stringResource(R.string.overview_user_item_name),
value = user.name,
style = MaterialTheme.typography.h6
)
ListCardCommon.TextWithIcon(
icon = when (user.type) {
UserType.Child -> Icons.Default.Security
UserType.Parent -> Icons.Default.Settings
},
label = stringResource(R.string.overview_user_item_role),
value = when (user.type) {
UserType.Child -> stringResource(R.string.overview_user_item_role_child)
UserType.Parent -> stringResource(R.string.overview_user_item_role_parent)
}
)
if (user.areLimitsTemporarilyDisabled) {
ListCardCommon.TextWithIcon(
icon = Icons.Default.AlarmOff,
label = stringResource(R.string.overview_user_item_temporarily_disabled),
value = stringResource(R.string.overview_user_item_temporarily_disabled)
)
}
}
}

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -20,15 +20,14 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.navigation.Navigation
import io.timelimit.android.R
import io.timelimit.android.async.Threads import io.timelimit.android.async.Threads
import io.timelimit.android.databinding.FragmentSetupDevicePermissionsBinding import io.timelimit.android.databinding.FragmentSetupDevicePermissionsBinding
import io.timelimit.android.extensions.safeNavigate
import io.timelimit.android.integration.platform.SystemPermission import io.timelimit.android.integration.platform.SystemPermission
import io.timelimit.android.logic.AppLogic import io.timelimit.android.logic.AppLogic
import io.timelimit.android.logic.DefaultAppLogic import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.ui.manage.device.manage.permission.PermissionInfoHelpDialog import io.timelimit.android.ui.manage.device.manage.permission.PermissionInfoHelpDialog
import io.timelimit.android.ui.model.UpdateStateCommand
import io.timelimit.android.ui.model.execute
class SetupDevicePermissionsFragment : Fragment() { class SetupDevicePermissionsFragment : Fragment() {
private val logic: AppLogic by lazy { DefaultAppLogic.with(context!!) } private val logic: AppLogic by lazy { DefaultAppLogic.with(context!!) }
@ -45,8 +44,6 @@ class SetupDevicePermissionsFragment : Fragment() {
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val navigation = Navigation.findNavController(container!!)
binding = FragmentSetupDevicePermissionsBinding.inflate(inflater, container, false) binding = FragmentSetupDevicePermissionsBinding.inflate(inflater, container, false)
binding.handlers = object: SetupDevicePermissionsHandlers { binding.handlers = object: SetupDevicePermissionsHandlers {
@ -71,11 +68,7 @@ class SetupDevicePermissionsFragment : Fragment() {
} }
override fun gotoNextStep() { override fun gotoNextStep() {
navigation.safeNavigate( requireActivity().execute(UpdateStateCommand.Setup.LocalMode)
SetupDevicePermissionsFragmentDirections
.actionSetupDevicePermissionsFragmentToSetupLocalModeFragment(),
R.id.setupDevicePermissionsFragment
)
} }
override fun helpUsageStatsAccess() { override fun helpUsageStatsAccess() {

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -21,11 +21,11 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.navigation.Navigation
import io.timelimit.android.R import io.timelimit.android.R
import io.timelimit.android.databinding.SetupHelpInfoFragmentBinding import io.timelimit.android.databinding.SetupHelpInfoFragmentBinding
import io.timelimit.android.extensions.safeNavigate
import io.timelimit.android.ui.help.HelpDialogFragment import io.timelimit.android.ui.help.HelpDialogFragment
import io.timelimit.android.ui.model.UpdateStateCommand
import io.timelimit.android.ui.model.execute
class SetupHelpInfoFragment: Fragment() { class SetupHelpInfoFragment: Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@ -41,10 +41,7 @@ class SetupHelpInfoFragment: Fragment() {
} }
binding.nextButton.setOnClickListener { binding.nextButton.setOnClickListener {
Navigation.findNavController(view!!).safeNavigate( requireActivity().execute(UpdateStateCommand.Setup.SelectMode)
SetupHelpInfoFragmentDirections.actionSetupHelpInfoFragmentToSetupSelectModeFragment(),
R.id.setupHelpInfoFragment
)
} }
return binding.root return binding.root

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -28,7 +28,6 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.* import androidx.lifecycle.*
import androidx.navigation.Navigation
import io.timelimit.android.BuildConfig import io.timelimit.android.BuildConfig
import io.timelimit.android.R import io.timelimit.android.R
import io.timelimit.android.coroutines.runAsync import io.timelimit.android.coroutines.runAsync

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -28,16 +28,15 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.navigation.NavController
import androidx.navigation.Navigation
import io.timelimit.android.BuildConfig import io.timelimit.android.BuildConfig
import io.timelimit.android.R import io.timelimit.android.R
import io.timelimit.android.async.Threads import io.timelimit.android.async.Threads
import io.timelimit.android.coroutines.executeAndWait import io.timelimit.android.coroutines.executeAndWait
import io.timelimit.android.coroutines.runAsync import io.timelimit.android.coroutines.runAsync
import io.timelimit.android.databinding.FragmentSetupSelectModeBinding import io.timelimit.android.databinding.FragmentSetupSelectModeBinding
import io.timelimit.android.extensions.safeNavigate
import io.timelimit.android.logic.DefaultAppLogic import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.ui.model.UpdateStateCommand
import io.timelimit.android.ui.model.execute
import io.timelimit.android.ui.setup.parentmode.SetupParentmodeDialogFragment import io.timelimit.android.ui.setup.parentmode.SetupParentmodeDialogFragment
import io.timelimit.android.ui.setup.privacy.PrivacyInfoDialogFragment import io.timelimit.android.ui.setup.privacy.PrivacyInfoDialogFragment
@ -49,7 +48,6 @@ class SetupSelectModeFragment : Fragment() {
private const val REQUEST_SETUP_PARENT_MODE = 3 private const val REQUEST_SETUP_PARENT_MODE = 3
} }
private lateinit var navigation: NavController
private lateinit var binding: FragmentSetupSelectModeBinding private lateinit var binding: FragmentSetupSelectModeBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@ -61,13 +59,8 @@ class SetupSelectModeFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
navigation = Navigation.findNavController(view)
binding.btnLocalMode.setOnClickListener { binding.btnLocalMode.setOnClickListener {
navigation.safeNavigate( requireActivity().execute(UpdateStateCommand.Setup.DevicePermissions)
SetupSelectModeFragmentDirections.actionSetupSelectModeFragmentToSetupDevicePermissionsFragment(),
R.id.setupSelectModeFragment
)
} }
binding.btnParentMode.setOnClickListener { binding.btnParentMode.setOnClickListener {
@ -131,15 +124,9 @@ class SetupSelectModeFragment : Fragment() {
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
if (requestCode == REQ_SETUP_CONNECTED_CHILD) { if (requestCode == REQ_SETUP_CONNECTED_CHILD) {
navigation.safeNavigate( requireActivity().execute(UpdateStateCommand.Setup.RemoteChild)
SetupSelectModeFragmentDirections.actionSetupSelectModeFragmentToSetupRemoteChildFragment(),
R.id.setupSelectModeFragment
)
} else if (requestCode == REQ_SETUP_CONNECTED_PARENT) { } else if (requestCode == REQ_SETUP_CONNECTED_PARENT) {
navigation.safeNavigate( requireActivity().execute(UpdateStateCommand.Setup.ParentMode)
SetupSelectModeFragmentDirections.actionSetupSelectModeFragmentToSetupParentModeFragment(),
R.id.setupSelectModeFragment
)
} }
} }
} }

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -23,11 +23,11 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.navigation.Navigation
import io.timelimit.android.R import io.timelimit.android.R
import io.timelimit.android.databinding.FragmentSetupTermsBinding import io.timelimit.android.databinding.FragmentSetupTermsBinding
import io.timelimit.android.extensions.safeNavigate
import io.timelimit.android.logic.DefaultAppLogic import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.ui.model.UpdateStateCommand
import io.timelimit.android.ui.model.execute
import io.timelimit.android.ui.obsolete.ObsoleteDialogFragment import io.timelimit.android.ui.obsolete.ObsoleteDialogFragment
import io.timelimit.android.ui.setup.customserver.SelectCustomServerDialogFragment import io.timelimit.android.ui.setup.customserver.SelectCustomServerDialogFragment
@ -66,9 +66,6 @@ class SetupTermsFragment : Fragment() {
} }
private fun acceptTerms() { private fun acceptTerms() {
Navigation.findNavController(view!!).safeNavigate( requireActivity().execute(UpdateStateCommand.Setup.Help)
SetupTermsFragmentDirections.actionSetupTermsFragmentToSetupHelpInfoFragment(),
R.id.setupTermsFragment
)
} }
} }

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -33,21 +33,20 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.observe import androidx.lifecycle.observe
import androidx.navigation.Navigation
import io.timelimit.android.R import io.timelimit.android.R
import io.timelimit.android.coroutines.runAsync import io.timelimit.android.coroutines.runAsync
import io.timelimit.android.data.IdGenerator import io.timelimit.android.data.IdGenerator
import io.timelimit.android.data.model.AppRecommendation import io.timelimit.android.data.model.AppRecommendation
import io.timelimit.android.data.model.UserType import io.timelimit.android.data.model.UserType
import io.timelimit.android.databinding.FragmentSetupDeviceBinding import io.timelimit.android.databinding.FragmentSetupDeviceBinding
import io.timelimit.android.extensions.safeNavigate
import io.timelimit.android.livedata.* import io.timelimit.android.livedata.*
import io.timelimit.android.logic.DefaultAppLogic import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.ui.main.ActivityViewModelHolder import io.timelimit.android.ui.main.ActivityViewModelHolder
import io.timelimit.android.ui.main.FragmentWithCustomTitle import io.timelimit.android.ui.main.FragmentWithCustomTitle
import io.timelimit.android.ui.manage.device.manage.advanced.ManageDeviceBackgroundSync import io.timelimit.android.ui.manage.device.manage.advanced.ManageDeviceBackgroundSync
import io.timelimit.android.ui.model.UpdateStateCommand
import io.timelimit.android.ui.model.execute
import io.timelimit.android.ui.mustread.MustReadFragment import io.timelimit.android.ui.mustread.MustReadFragment
import io.timelimit.android.ui.overview.main.MainFragmentDirections
import io.timelimit.android.ui.setup.SetupNetworkTimeVerification import io.timelimit.android.ui.setup.SetupNetworkTimeVerification
import io.timelimit.android.ui.update.UpdateConsentCard import io.timelimit.android.ui.update.UpdateConsentCard
import io.timelimit.android.ui.view.NotifyPermissionCard import io.timelimit.android.ui.view.NotifyPermissionCard
@ -121,7 +120,6 @@ class SetupDeviceFragment : Fragment(), FragmentWithCustomTitle {
val binding = FragmentSetupDeviceBinding.inflate(inflater, container, false) val binding = FragmentSetupDeviceBinding.inflate(inflater, container, false)
val logic = DefaultAppLogic.with(requireContext()) val logic = DefaultAppLogic.with(requireContext())
val activity = activity as ActivityViewModelHolder val activity = activity as ActivityViewModelHolder
val navigation = Navigation.findNavController(container!!)
binding.needsParent.authBtn.setOnClickListener { binding.needsParent.authBtn.setOnClickListener {
activity.showAuthenticationScreen() activity.showAuthenticationScreen()
@ -147,13 +145,7 @@ class SetupDeviceFragment : Fragment(), FragmentWithCustomTitle {
val ownDeviceId = logic.deviceId.waitForNullableValue()!! val ownDeviceId = logic.deviceId.waitForNullableValue()!!
navigation.popBackStack() requireActivity().execute(UpdateStateCommand.ManageDevice.EnterFromDeviceSetup(ownDeviceId))
navigation.safeNavigate(
MainFragmentDirections.actionOverviewFragmentToManageDeviceFragment(
ownDeviceId
),
R.id.overviewFragment
)
} }
} }
SetupDeviceModelStatus.Working -> { /* nothing to do */ } SetupDeviceModelStatus.Working -> { /* nothing to do */ }

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -24,7 +24,6 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import androidx.navigation.Navigation
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import io.timelimit.android.R import io.timelimit.android.R
import io.timelimit.android.data.model.UserType import io.timelimit.android.data.model.UserType
@ -33,6 +32,8 @@ import io.timelimit.android.livedata.*
import io.timelimit.android.ui.main.ActivityViewModel import io.timelimit.android.ui.main.ActivityViewModel
import io.timelimit.android.ui.main.ActivityViewModelHolder import io.timelimit.android.ui.main.ActivityViewModelHolder
import io.timelimit.android.ui.main.FragmentWithCustomTitle import io.timelimit.android.ui.main.FragmentWithCustomTitle
import io.timelimit.android.ui.model.UpdateStateCommand
import io.timelimit.android.ui.model.execute
class AddUserFragment : Fragment(), FragmentWithCustomTitle { class AddUserFragment : Fragment(), FragmentWithCustomTitle {
companion object { companion object {
@ -47,7 +48,6 @@ class AddUserFragment : Fragment(), FragmentWithCustomTitle {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val binding = FragmentAddUserBinding.inflate(inflater, container, false) val binding = FragmentAddUserBinding.inflate(inflater, container, false)
val navigation = Navigation.findNavController(container!!)
// user type // user type
@ -121,7 +121,8 @@ class AddUserFragment : Fragment(), FragmentWithCustomTitle {
} }
AddUserModelStatus.Done -> { AddUserModelStatus.Done -> {
Snackbar.make(binding.root, R.string.add_user_confirmation_done, Snackbar.LENGTH_SHORT).show() Snackbar.make(binding.root, R.string.add_user_confirmation_done, Snackbar.LENGTH_SHORT).show()
navigation.popBackStack()
requireActivity().execute(UpdateStateCommand.AddUser.Leave)
binding.flipper.displayedChild = PAGE_WAIT binding.flipper.displayedChild = PAGE_WAIT
} }

View file

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/nav_host" />

View file

@ -1,29 +0,0 @@
<?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/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".ui.overview.overview.OverviewFragment">
<androidx.recyclerview.widget.RecyclerView
android:layout_weight="1"
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="0dp" />
</LinearLayout>

View file

@ -1,182 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="deviceTitle"
type="String" />
<variable
name="currentDeviceUserTitle"
type="String" />
<variable
name="hasManipulation"
type="Boolean" />
<variable
name="isCurrentDevice"
type="Boolean" />
<variable
name="isMissingRequiredPermission"
type="Boolean" />
<variable
name="didUninstall"
type="Boolean" />
<variable
name="isPasswordDisabled"
type="boolean" />
<variable
name="isConnected"
type="boolean" />
<variable
name="isUsingOlderVersion"
type="boolean" />
<import type="android.view.View" />
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.cardview.widget.CardView
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:id="@+id/card"
android:foreground="?selectableItemBackground"
app:cardUseCompatPadding="true"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:padding="8dp"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
tools:ignore="UnusedAttribute"
android:drawableTint="?colorOnSurface"
android:drawablePadding="8dp"
android:drawableStart="@drawable/ic_smartphone_black_24dp"
tools:text="Galaxy S8"
android:textAppearance="?android:textAppearanceLarge"
android:text="@{deviceTitle}"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
tools:ignore="UnusedAttribute"
android:drawableTint="?colorOnSurface"
android:drawablePadding="8dp"
android:drawableStart="@drawable/ic_account_circle_black_24dp"
android:visibility="@{currentDeviceUserTitle == null ? View.GONE : View.VISIBLE}"
android:textAppearance="?android:textAppearanceMedium"
tools:text="Max Mustermann"
android:text="@{currentDeviceUserTitle}"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
tools:ignore="UnusedAttribute"
android:drawableTint="?colorOnSurface"
android:visibility="@{isPasswordDisabled ? View.VISIBLE : View.GONE}"
android:drawableStart="@drawable/ic_lock_open_black_24dp"
android:drawablePadding="8dp"
android:textAppearance="?android:textAppearanceMedium"
android:text="@string/overview_device_item_password_disabled"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
tools:ignore="UnusedAttribute"
android:drawableTint="?colorOnSurface"
android:drawableStart="@drawable/ic_wifi_black_24dp"
android:drawablePadding="8dp"
android:visibility="@{isConnected ? View.VISIBLE : View.GONE}"
android:textAppearance="?android:textAppearanceMedium"
android:text="@string/overview_device_item_connected"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
tools:ignore="UnusedAttribute"
android:drawableTint="?colorPrimary"
android:textColor="?colorPrimary"
android:drawableStart="@drawable/ic_update_black_24dp"
android:drawablePadding="8dp"
android:visibility="@{isUsingOlderVersion ? View.VISIBLE : View.GONE}"
android:textAppearance="?android:textAppearanceMedium"
android:text="@string/overview_device_item_older_version"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
tools:ignore="UnusedAttribute"
android:drawableTint="@color/orange_text"
android:textColor="@color/orange_text"
android:drawablePadding="8dp"
android:textAppearance="?android:textAppearanceMedium"
android:visibility="@{safeUnbox(hasManipulation) ? View.VISIBLE : View.GONE}"
android:drawableStart="@drawable/ic_warning_black_24dp"
android:text="@string/overview_device_item_manipulation"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
tools:ignore="UnusedAttribute"
android:drawableTint="@color/orange_text"
android:textColor="@color/orange_text"
android:drawablePadding="8dp"
android:textAppearance="?android:textAppearanceMedium"
android:visibility="@{safeUnbox(isMissingRequiredPermission) ? View.VISIBLE : View.GONE}"
android:drawableStart="@drawable/ic_warning_black_24dp"
android:text="@string/overview_device_item_missing_permission"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
tools:ignore="UnusedAttribute"
android:drawableTint="@color/orange_text"
android:textColor="@color/orange_text"
android:drawablePadding="8dp"
android:textAppearance="?android:textAppearanceMedium"
android:visibility="@{safeUnbox(didUninstall) ? View.VISIBLE : View.GONE}"
android:drawableStart="@drawable/ic_warning_black_24dp"
android:text="@string/overview_device_item_uninstall"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:visibility="@{safeUnbox(isCurrentDevice) ? View.VISIBLE : View.GONE}"
android:paddingStart="32dp"
android:paddingEnd="0dp"
android:textAppearance="?android:textAppearanceSmall"
android:text="@string/manage_device_is_this_device"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</FrameLayout>
</layout>

View file

@ -1,55 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.cardview.widget.CardView
android:layout_margin="8dp"
app:cardUseCompatPadding="true"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:padding="8dp"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:textAppearance="?android:textAppearanceLarge"
android:text="@string/overview_finish_setup_title"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:textAppearance="?android:textAppearanceMedium"
android:text="@string/overview_finish_setup_text"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:layout_marginEnd="4dp"
android:id="@+id/btn_go"
android:layout_gravity="end"
android:text="@string/generic_go"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</FrameLayout>
</layout>

View file

@ -1,51 +0,0 @@
<?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/>.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.cardview.widget.CardView
android:layout_margin="8dp"
app:cardUseCompatPadding="true"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:padding="8dp"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:textAppearance="?android:textAppearanceLarge"
android:text="@string/overview_intro_title"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:textAppearance="?android:textAppearanceMedium"
android:text="@string/overview_intro_text"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:textAppearance="?android:textAppearanceSmall"
android:text="@string/generic_swipe_to_dismiss"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</FrameLayout>

View file

@ -1,58 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="title"
type="String" />
<variable
name="text"
type="String" />
</data>
<androidx.cardview.widget.CardView
android:layout_margin="8dp"
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
tools:text="@string/overview_server_message"
android:text="@{title}"
android:textAppearance="?android:textAppearanceLarge"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:textAppearance="?android:textAppearanceMedium"
tools:text="That's a message"
android:text="@{text}"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</layout>

View file

@ -1,134 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="childName"
type="String" />
<variable
name="taskTitle"
type="String" />
<variable
name="categoryTitle"
type="String" />
<variable
name="duration"
type="String" />
<variable
name="lastGrant"
type="String" />
<import type="android.text.TextUtils" />
<import type="android.view.View" />
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.cardview.widget.CardView
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:foreground="?selectableItemBackground"
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:text="@string/task_review_title"
android:textAppearance="?android:textAppearanceLarge"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:textAppearance="?android:textAppearanceMedium"
tools:text="@string/task_review_text"
android:text="@{@string/task_review_text(childName, taskTitle)}"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:textAppearance="?android:textAppearanceSmall"
tools:text="@string/task_review_category"
android:text="@{@string/task_review_category(duration, categoryTitle)}"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:visibility="@{TextUtils.isEmpty(lastGrant) ? View.GONE : View.VISIBLE}"
android:textAppearance="?android:textAppearanceSmall"
tools:text="@string/task_review_last_grant"
android:text="@{@string/task_review_last_grant(lastGrant)}"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<LinearLayout
android:layout_marginEnd="4dp"
android:layout_marginTop="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:id="@+id/skip_button"
style="?borderlessButtonStyle"
android:text="@string/generic_skip"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<View
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="match_parent" />
<Button
android:id="@+id/no_button"
android:layout_marginEnd="8dp"
style="?materialButtonOutlinedStyle"
android:text="@string/generic_no"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Button
android:id="@+id/yes_button"
style="?materialButtonOutlinedStyle"
android:text="@string/generic_yes"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<TextView
android:text="@string/purchase_required_info_local_mode_free"
android:textAppearance="?android:textAppearanceSmall"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</FrameLayout>
</layout>

View file

@ -1,114 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="username"
type="String" />
<variable
name="isTemporarilyBlocked"
type="Boolean" />
<variable
name="areTimeLimitsDisabled"
type="Boolean" />
<variable
name="isParent"
type="Boolean" />
<variable
name="isChild"
type="Boolean" />
<import type="android.view.View" />
</data>
<androidx.cardview.widget.CardView
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:id="@+id/card"
android:foreground="?selectableItemBackground"
app:cardUseCompatPadding="true"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:padding="8dp"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
tools:ignore="UnusedAttribute"
android:drawableTint="?colorOnSurface"
android:drawablePadding="8dp"
android:drawableStart="@drawable/ic_account_circle_black_24dp"
android:textAppearance="?android:textAppearanceLarge"
tools:text="Anton"
android:text="@{username}"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
tools:ignore="UnusedAttribute"
android:drawableTint="?colorOnSurface"
android:drawablePadding="8dp"
android:drawableStart="@drawable/ic_settings_black_24dp"
android:visibility="@{safeUnbox(isParent) ? View.VISIBLE : View.GONE}"
android:textAppearance="?android:textAppearanceMedium"
android:text="@string/overview_user_item_role_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
tools:ignore="UnusedAttribute"
android:drawableTint="?colorOnSurface"
android:drawablePadding="8dp"
android:drawableStart="@drawable/ic_security_black_24dp"
android:visibility="@{safeUnbox(isChild) ? View.VISIBLE : View.GONE}"
android:textAppearance="?android:textAppearanceMedium"
android:text="@string/overview_user_item_role_child"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
tools:ignore="UnusedAttribute"
android:drawableTint="?colorOnSurface"
android:drawablePadding="8dp"
android:drawableStart="@drawable/ic_lock_outline_black_24dp"
android:visibility="@{safeUnbox(isTemporarilyBlocked) ? View.VISIBLE : View.GONE}"
android:textAppearance="?android:textAppearanceMedium"
android:text="@string/overview_user_item_temporarily_blocked"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
tools:ignore="UnusedAttribute"
android:drawableTint="?colorOnSurface"
android:drawablePadding="8dp"
android:drawableStart="@drawable/ic_alarm_off_black_24dp"
android:visibility="@{safeUnbox(areTimeLimitsDisabled) ? View.VISIBLE : View.GONE}"
android:textAppearance="?android:textAppearanceMedium"
android:text="@string/overview_user_item_temporarily_disabled"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</layout>

View file

@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
app:showAsAction="never"
app:iconTint="?colorOnPrimarySurface"
android:icon="@drawable/ic_delete_black_24dp"
android:title="@string/main_tab_uninstall"
android:id="@+id/menu_main_uninstall" />
<item
app:showAsAction="always"
app:iconTint="?colorOnPrimarySurface"
android:icon="@drawable/ic_info_outline_black_24dp"
android:title="@string/main_tab_about"
android:id="@+id/menu_main_about" />
</menu>

View file

@ -1,30 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:icon="@drawable/ic_time_white_24dp"
android:id="@+id/menu_manage_category_blocked_time_areas"
android:title="@string/blocked_time_areas"
app:iconTint="?colorOnPrimarySurface"
app:showAsAction="never" />
<item
android:icon="@drawable/ic_settings_white_24dp"
android:id="@+id/menu_manage_category_settings"
android:title="@string/category_settings"
app:iconTint="?colorOnPrimarySurface"
app:showAsAction="never" />
</menu>

View file

@ -1,54 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
TimeLimit Copyright <C> 2019 - 2021 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/>.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
app:showAsAction="ifRoom"
app:iconTint="?colorOnPrimarySurface"
android:icon="@drawable/ic_baseline_directions_bike_24"
android:title="@string/manage_child_tasks"
android:id="@+id/menu_manage_child_tasks" />
<item
app:showAsAction="ifRoom"
app:iconTint="?colorOnPrimarySurface"
android:icon="@drawable/ic_phone_black_24dp"
android:title="@string/contacts_title_long"
android:id="@+id/menu_manage_child_phone" />
<item
app:showAsAction="never"
app:iconTint="?colorOnPrimarySurface"
android:icon="@drawable/ic_apps_white_24dp"
android:title="@string/child_apps_title"
android:id="@+id/menu_manage_child_apps" />
<item
app:showAsAction="never"
app:iconTint="?colorOnPrimarySurface"
android:icon="@drawable/ic_history_black_24dp"
android:title="@string/usage_history_title"
android:id="@+id/menu_manage_child_usage_history" />
<item
app:showAsAction="never"
app:iconTint="?colorOnPrimarySurface"
android:icon="@drawable/ic_settings_white_24dp"
android:title="@string/manage_child_tab_other"
android:id="@+id/menu_manage_child_advanced" />
</menu>

View file

@ -32,6 +32,8 @@
<string name="generic_show_details">Details anzeigen</string> <string name="generic_show_details">Details anzeigen</string>
<string name="generic_accept">Akzeptieren</string> <string name="generic_accept">Akzeptieren</string>
<string name="generic_reject">Ablehnen</string> <string name="generic_reject">Ablehnen</string>
<string name="generic_menu">Menü</string>
<string name="generic_back">Zurück</string>
<string name="generic_swipe_to_dismiss">Sie können diesen Hinweis entfernen, indem Sie ihn zur Seite wischen</string> <string name="generic_swipe_to_dismiss">Sie können diesen Hinweis entfernen, indem Sie ihn zur Seite wischen</string>
<string name="generic_runtime_permission_rejected">Berechtigung abgelehnt; Sie können die Berechtigungen in den Systemeinstellungen verwalten</string> <string name="generic_runtime_permission_rejected">Berechtigung abgelehnt; Sie können die Berechtigungen in den Systemeinstellungen verwalten</string>
@ -126,6 +128,7 @@
</string> </string>
<string name="annoy_unlock_dialog_action">Entsperren</string> <string name="annoy_unlock_dialog_action">Entsperren</string>
<string name="authentication_action">Anmelden</string>
<string name="authentication_required_overlay_title">Melden Sie sich an, um Einstellungen zu ändern</string> <string name="authentication_required_overlay_title">Melden Sie sich an, um Einstellungen zu ändern</string>
<string name="authentication_required_overlay_text">Die Einstellungen werden wieder gesperrt, wenn Sie TimeLimit verlassen</string> <string name="authentication_required_overlay_text">Die Einstellungen werden wieder gesperrt, wenn Sie TimeLimit verlassen</string>
@ -1195,6 +1198,8 @@
\n\nBei den Benutzern können Sie die erlaubten Apps und die Zeitbeschränkungen wählen. \n\nBei den Benutzern können Sie die erlaubten Apps und die Zeitbeschränkungen wählen.
</string> </string>
<string name="overview_device_item_name">Name</string>
<string name="overview_device_item_user_name">aktueller Benutzer</string>
<string name="overview_device_item_manipulation">An diesem Gerät wurden Manipulationen durchgeführt</string> <string name="overview_device_item_manipulation">An diesem Gerät wurden Manipulationen durchgeführt</string>
<string name="overview_device_item_missing_permission">An diesem Gerät wurde eine erforderliche Berechtigung nicht gewährt</string> <string name="overview_device_item_missing_permission">An diesem Gerät wurde eine erforderliche Berechtigung nicht gewährt</string>
<string name="overview_device_item_uninstall">TimeLimit wurde auf diesem Gerät deinstalliert</string> <string name="overview_device_item_uninstall">TimeLimit wurde auf diesem Gerät deinstalliert</string>
@ -1202,10 +1207,12 @@
<string name="overview_device_item_connected">Verbunden</string> <string name="overview_device_item_connected">Verbunden</string>
<string name="overview_device_item_older_version">verwendet eine ältere TimeLimit-Version</string> <string name="overview_device_item_older_version">verwendet eine ältere TimeLimit-Version</string>
<string name="overview_user_item_name">Name</string>
<string name="overview_user_item_temporarily_blocked">vorübergehend gesperrt</string> <string name="overview_user_item_temporarily_blocked">vorübergehend gesperrt</string>
<string name="overview_user_item_temporarily_blocked_until">gesperrt bis %s</string> <string name="overview_user_item_temporarily_blocked_until">gesperrt bis %s</string>
<string name="overview_user_item_temporarily_disabled">Zeitbegrenzungen vorübergehend deaktiviert</string> <string name="overview_user_item_temporarily_disabled">Zeitbegrenzungen vorübergehend deaktiviert</string>
<string name="overview_user_item_temporarily_disabled_until">Zeitbegrenzungen deaktiviert bis %s</string> <string name="overview_user_item_temporarily_disabled_until">Zeitbegrenzungen deaktiviert bis %s</string>
<string name="overview_user_item_role">Rolle</string>
<string name="overview_user_item_role_child">wird eingeschränkt</string> <string name="overview_user_item_role_child">wird eingeschränkt</string>
<string name="overview_user_item_role_parent">kann Einstellungen ändern</string> <string name="overview_user_item_role_parent">kann Einstellungen ändern</string>

View file

@ -35,6 +35,8 @@
<string name="generic_show_details">Show details</string> <string name="generic_show_details">Show details</string>
<string name="generic_accept">Accept</string> <string name="generic_accept">Accept</string>
<string name="generic_reject">Reject</string> <string name="generic_reject">Reject</string>
<string name="generic_menu">Menu</string>
<string name="generic_back">Back</string>
<string name="generic_swipe_to_dismiss">Swipe to the side to remove this message</string> <string name="generic_swipe_to_dismiss">Swipe to the side to remove this message</string>
<string name="generic_runtime_permission_rejected">Permission rejected; You can manage permissions in the system settings</string> <string name="generic_runtime_permission_rejected">Permission rejected; You can manage permissions in the system settings</string>
@ -170,6 +172,7 @@
</string> </string>
<string name="annoy_unlock_dialog_action">Unlock</string> <string name="annoy_unlock_dialog_action">Unlock</string>
<string name="authentication_action">Authenticate</string>
<string name="authentication_required_overlay_title">Sign in to change settings</string> <string name="authentication_required_overlay_title">Sign in to change settings</string>
<string name="authentication_required_overlay_text">The settings will be locked again when you leave TimeLimit</string> <string name="authentication_required_overlay_text">The settings will be locked again when you leave TimeLimit</string>
@ -1239,6 +1242,8 @@
\n\nUnder users, you can configure the allowed Apps and time limits. \n\nUnder users, you can configure the allowed Apps and time limits.
</string> </string>
<string name="overview_device_item_name">Name</string>
<string name="overview_device_item_user_name">current User</string>
<string name="overview_device_item_manipulation">There was a manipulation on this device</string> <string name="overview_device_item_manipulation">There was a manipulation on this device</string>
<string name="overview_device_item_missing_permission">A required permission was not granted on this device</string> <string name="overview_device_item_missing_permission">A required permission was not granted on this device</string>
<string name="overview_device_item_uninstall">TimeLimit was uninstalled at this device</string> <string name="overview_device_item_uninstall">TimeLimit was uninstalled at this device</string>
@ -1246,10 +1251,12 @@
<string name="overview_device_item_connected">Connected</string> <string name="overview_device_item_connected">Connected</string>
<string name="overview_device_item_older_version">uses an older TimeLimit version</string> <string name="overview_device_item_older_version">uses an older TimeLimit version</string>
<string name="overview_user_item_name">Name</string>
<string name="overview_user_item_temporarily_blocked">temporarily blocked</string> <string name="overview_user_item_temporarily_blocked">temporarily blocked</string>
<string name="overview_user_item_temporarily_blocked_until">temporarily blocked until %s</string> <string name="overview_user_item_temporarily_blocked_until">temporarily blocked until %s</string>
<string name="overview_user_item_temporarily_disabled">Time limits temporarily disabled</string> <string name="overview_user_item_temporarily_disabled">Time limits temporarily disabled</string>
<string name="overview_user_item_temporarily_disabled_until">Time limits temporarily disabled until %s</string> <string name="overview_user_item_temporarily_disabled_until">Time limits temporarily disabled until %s</string>
<string name="overview_user_item_role">Role</string>
<string name="overview_user_item_role_child">is restricted</string> <string name="overview_user_item_role_child">is restricted</string>
<string name="overview_user_item_role_parent">can change settings</string> <string name="overview_user_item_role_parent">can change settings</string>