diff --git a/.gitignore b/.gitignore index 3e14967..472c576 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ /captures .externalNativeBuild .idea +.kotlin diff --git a/app/build.gradle b/app/build.gradle index ac9ed68..2dcb3a4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 - 2024 Jonas Lochmann + * TimeLimit Copyright 2019 - 2025 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 @@ -21,17 +21,18 @@ plugins { id "androidx.navigation.safeargs.kotlin" id 'kotlin-kapt' id 'com.squareup.wire' + id("org.jetbrains.kotlin.plugin.compose") version "2.1.10" } android { namespace 'io.timelimit.android' - compileSdk 35 + compileSdk 36 defaultConfig { applicationId "io.timelimit.android" minSdkVersion 26 - targetSdkVersion 35 - versionCode 220 - versionName "7.2.1" + targetSdkVersion 36 + versionCode 224 + versionName "7.3.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" kapt { arguments { @@ -166,24 +167,24 @@ wire { dependencies { def nav_version = "2.5.3" - def room_version = "2.6.1" - def work_version = '2.9.1' - def paging_version = "3.3.2" + def room_version = "2.7.2" + def work_version = '2.10.2' + def paging_version = "3.3.6" implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.21" - implementation 'androidx.appcompat:appcompat:1.7.0' - implementation 'androidx.core:core:1.15.0' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.1.10" + implementation 'androidx.appcompat:appcompat:1.7.1' + implementation 'androidx.core:core:1.16.0' implementation 'androidx.cardview:cardview:1.0.0' - implementation 'androidx.gridlayout:gridlayout:1.0.0' + implementation 'androidx.gridlayout:gridlayout:1.1.0' implementation "com.google.android.material:material:1.12.0" - implementation 'androidx.compose.material:material:1.7.5' - implementation 'androidx.activity:activity-compose:1.9.3' + implementation 'androidx.compose.material:material:1.8.3' + implementation 'androidx.activity:activity-compose:1.10.1' implementation "com.google.accompanist:accompanist-flowlayout:0.30.0" - implementation 'androidx.compose.material:material-icons-extended:1.7.5' - debugImplementation "androidx.compose.ui:ui-tooling:1.7.5" - implementation 'androidx.fragment:fragment-ktx:1.8.5' - implementation 'androidx.fragment:fragment-compose:1.8.5' + implementation 'androidx.compose.material:material-icons-extended:1.7.8' + debugImplementation "androidx.compose.ui:ui-tooling:1.8.3" + implementation 'androidx.fragment:fragment-ktx:1.8.8' + implementation 'androidx.fragment:fragment-compose:1.8.8' implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" implementation "androidx.navigation:navigation-ui:$nav_version" @@ -200,8 +201,8 @@ dependencies { implementation "androidx.work:work-runtime-ktx:$work_version" // androidTestImplementation "android.arch.work:work-testing:$work_version" - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3' - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test:runner:1.6.2' @@ -217,7 +218,7 @@ dependencies { implementation 'com.squareup.okhttp3:okhttp-tls:4.9.3' implementation 'com.squareup.okhttp3:logging-interceptor:4.9.3' - googleApiImplementation "com.android.billingclient:billing-ktx:7.1.1" + googleApiImplementation "com.android.billingclient:billing-ktx:8.0.0" implementation('io.socket:socket.io-client:2.0.0') { exclude group: 'org.json', module: 'json' @@ -229,5 +230,5 @@ dependencies { implementation 'com.google.zxing:core:3.3.3' - api "com.squareup.wire:wire-runtime:4.4.3" + api "com.squareup.wire:wire-runtime:5.3.5" } diff --git a/app/src/main/java/io/timelimit/android/data/dao/UsedTimeDao.kt b/app/src/main/java/io/timelimit/android/data/dao/UsedTimeDao.kt index 8b707f8..3c34d23 100644 --- a/app/src/main/java/io/timelimit/android/data/dao/UsedTimeDao.kt +++ b/app/src/main/java/io/timelimit/android/data/dao/UsedTimeDao.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 - 2024 Jonas Lochmann + * TimeLimit Copyright 2019 - 2025 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 @@ -69,13 +69,13 @@ abstract class UsedTimeDao { // breaking it into multiple lines causes issues during compilation ... // this warns about an unused column, but this column is used in the ORDER BY - @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH) + @SuppressWarnings(RoomWarnings.QUERY_MISMATCH) @Query("SELECT 2 AS type, start_time_of_day AS startMinuteOfDay, end_time_of_day AS endMinuteOfDay, used_time AS duration, day_of_epoch AS day, NULL AS lastUsage, NULL AS maxSessionDuration, NULL AS pauseDuration, category.id AS categoryId, category.title AS categoryTitle FROM used_time JOIN category ON (used_time.category_id = category.id) WHERE category.id = :categoryId UNION ALL SELECT 1 AS type, start_minute_of_day AS startMinuteOfDay, end_minute_of_day AS endMinuteOfDay, last_session_duration AS duration, NULL AS day, last_usage AS lastUsage, max_session_duration AS maxSessionDuration, session_pause_duration AS pauseDuration, category.id AS categoryId, category.title AS categoryTitle FROM session_duration JOIN category ON (session_duration.category_id = category.id) WHERE category.id = :categoryId ORDER BY type, day DESC, lastUsage DESC, startMinuteOfDay, endMinuteOfDay, categoryId") abstract fun getUsedTimeListItemsByCategoryId(categoryId: String): Flow> // breaking it into multiple lines causes issues during compilation ... // this warns about an unused column, but this column is used in the ORDER BY - @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH) + @SuppressWarnings(RoomWarnings.QUERY_MISMATCH) @Query("SELECT 2 AS type, start_time_of_day AS startMinuteOfDay, end_time_of_day AS endMinuteOfDay, used_time AS duration, day_of_epoch AS day, NULL AS lastUsage, NULL AS maxSessionDuration, NULL AS pauseDuration, category.id AS categoryId, category.title AS categoryTitle FROM used_time JOIN category ON (used_time.category_id = category.id) WHERE category.child_id = :userId UNION ALL SELECT 1 AS type, start_minute_of_day AS startMinuteOfDay, end_minute_of_day AS endMinuteOfDay, last_session_duration AS duration, NULL AS day, last_usage AS lastUsage, max_session_duration AS maxSessionDuration, session_pause_duration AS pauseDuration, category.id AS categoryId, category.title AS categoryTitle FROM session_duration JOIN category ON (session_duration.category_id = category.id) WHERE category.child_id = :userId ORDER BY type, day DESC, lastUsage DESC, startMinuteOfDay, endMinuteOfDay, categoryId") abstract fun getUsedTimeListItemsByUserId(userId: String): Flow> } diff --git a/app/src/main/java/io/timelimit/android/data/model/ConfigurationItem.kt b/app/src/main/java/io/timelimit/android/data/model/ConfigurationItem.kt index 9f9e743..b5db91e 100644 --- a/app/src/main/java/io/timelimit/android/data/model/ConfigurationItem.kt +++ b/app/src/main/java/io/timelimit/android/data/model/ConfigurationItem.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 - 2022 Jonas Lochmann + * TimeLimit Copyright 2019 - 2025 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 @@ -295,4 +295,7 @@ object ExperimentalFlags { object ConsentFlags { const val APP_LIST_SYNC = 1L + + // this is used internally + const val BLOCK_USER_SWITCH_BY_DEFAULT = 2L } \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/data/model/derived/DeviceRelatedData.kt b/app/src/main/java/io/timelimit/android/data/model/derived/DeviceRelatedData.kt index e168684..858d667 100644 --- a/app/src/main/java/io/timelimit/android/data/model/derived/DeviceRelatedData.kt +++ b/app/src/main/java/io/timelimit/android/data/model/derived/DeviceRelatedData.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 - 2022 Jonas Lochmann + * TimeLimit Copyright 2019 - 2025 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 @@ -73,4 +73,5 @@ data class DeviceRelatedData ( } fun isExperimentalFlagSetSync(flags: Long) = (experimentalFlags and flags) == flags + fun isConsentFlagSet(flags: Long) = (consentFlags and flags) == flags } \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/integration/platform/android/AndroidDeviceOwnerApi.kt b/app/src/main/java/io/timelimit/android/integration/platform/android/AndroidDeviceOwnerApi.kt index 34a5814..44bbc15 100644 --- a/app/src/main/java/io/timelimit/android/integration/platform/android/AndroidDeviceOwnerApi.kt +++ b/app/src/main/java/io/timelimit/android/integration/platform/android/AndroidDeviceOwnerApi.kt @@ -134,9 +134,19 @@ class AndroidDeviceOwnerApi( if (VERSION.SDK_INT < VERSION_CODES.LOLLIPOP) return false if (!devicePolicyManager.isDeviceOwnerApp(componentName.packageName)) return false - return devicePolicyManager.setPermissionGrantState( - componentName, componentName.packageName, Manifest.permission.ACCESS_FINE_LOCATION, - DevicePolicyManager.PERMISSION_GRANT_STATE_GRANTED - ) + try { + return devicePolicyManager.setPermissionGrantState( + componentName, componentName.packageName, Manifest.permission.ACCESS_FINE_LOCATION, + DevicePolicyManager.PERMISSION_GRANT_STATE_GRANTED + ) + } catch (ex: SecurityException) { + // set to default so that granting this manually is possible + devicePolicyManager.setPermissionGrantState( + componentName, componentName.packageName, Manifest.permission.ACCESS_FINE_LOCATION, + DevicePolicyManager.PERMISSION_GRANT_STATE_DEFAULT + ) + + return false + } } } \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/integration/platform/android/AndroidFeatures.kt b/app/src/main/java/io/timelimit/android/integration/platform/android/AndroidFeatures.kt index d5fcef9..97cc81a 100644 --- a/app/src/main/java/io/timelimit/android/integration/platform/android/AndroidFeatures.kt +++ b/app/src/main/java/io/timelimit/android/integration/platform/android/AndroidFeatures.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 - 2022 Jonas Lochmann + * TimeLimit Copyright 2019 - 2025 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 @@ -27,6 +27,10 @@ import io.timelimit.android.integration.platform.PlatformFeature object AndroidFeatures { private const val FEATURE_ADB = "adb" private const val FEATURE_CONFIG_PRIVATE_DNS = "dns" + const val FEATURE_ADD_USER = "add_user" + const val FEATURE_USER_SWITCH = "user_switch" + private const val FEATURE_VPN = "vpn" + private const val FEATURE_UNKNOWN_SOURCES = "unknown_sources" fun applyBlockedFeatures(features: Set, policyManager: DevicePolicyManager, admin: ComponentName): Boolean { fun apply(feature: String, restriction: String) { @@ -40,6 +44,18 @@ object AndroidFeatures { apply(FEATURE_CONFIG_PRIVATE_DNS, UserManager.DISALLOW_CONFIG_PRIVATE_DNS) } + apply(FEATURE_ADD_USER, UserManager.DISALLOW_ADD_USER) + + if (VERSION.SDK_INT >= VERSION_CODES.P) { + apply(FEATURE_USER_SWITCH, UserManager.DISALLOW_USER_SWITCH) + } + + apply(FEATURE_VPN, UserManager.DISALLOW_CONFIG_VPN) + + if (VERSION.SDK_INT >= VERSION_CODES.Q) { + apply(FEATURE_UNKNOWN_SOURCES, UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY) + } + return true } @@ -60,6 +76,30 @@ object AndroidFeatures { ) } + result.add(PlatformFeature( + id = FEATURE_ADD_USER, + title = context.getString(R.string.dummy_app_feature_add_user) + )) + + if (VERSION.SDK_INT >= VERSION_CODES.P) { + result.add(PlatformFeature( + id = FEATURE_USER_SWITCH, + title = context.getString(R.string.dummy_app_feature_switch_user) + )) + } + + result.add(PlatformFeature( + id = FEATURE_VPN, + title = context.getString(R.string.dummy_app_feature_vpn) + )) + + if (VERSION.SDK_INT >= VERSION_CODES.Q) { + result.add(PlatformFeature( + id = FEATURE_UNKNOWN_SOURCES, + title = context.getString(R.string.dummy_app_feature_unknown_sources) + )) + } + return result } } \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/integration/platform/android/AndroidIntegration.kt b/app/src/main/java/io/timelimit/android/integration/platform/android/AndroidIntegration.kt index bb43054..cce566b 100644 --- a/app/src/main/java/io/timelimit/android/integration/platform/android/AndroidIntegration.kt +++ b/app/src/main/java/io/timelimit/android/integration/platform/android/AndroidIntegration.kt @@ -507,7 +507,6 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio if (enableLockdown) { // disable problematic features - policyManager.addUserRestriction(deviceAdmin, UserManager.DISALLOW_ADD_USER) policyManager.addUserRestriction(deviceAdmin, UserManager.DISALLOW_FACTORY_RESET) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { @@ -519,16 +518,24 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio context.packageName, Manifest.permission.ACCESS_FINE_LOCATION, ).let { - if (it == DevicePolicyManager.PERMISSION_GRANT_STATE_DEFAULT) { - policyManager.setPermissionGrantState( - deviceAdmin, - context.packageName, - Manifest.permission.ACCESS_FINE_LOCATION, - if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) - DevicePolicyManager.PERMISSION_GRANT_STATE_GRANTED - else - DevicePolicyManager.PERMISSION_GRANT_STATE_DENIED - ) + try { + if (it == DevicePolicyManager.PERMISSION_GRANT_STATE_DEFAULT) { + policyManager.setPermissionGrantState( + deviceAdmin, + context.packageName, + Manifest.permission.ACCESS_FINE_LOCATION, + if (ContextCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_FINE_LOCATION + ) == PackageManager.PERMISSION_GRANTED + ) + DevicePolicyManager.PERMISSION_GRANT_STATE_GRANTED + else + DevicePolicyManager.PERMISSION_GRANT_STATE_DENIED + ) + } + } catch (ex: SecurityException) { + // ignore } } @@ -549,7 +556,6 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio } } else /* disable lockdown */ { // enable problematic features - policyManager.clearUserRestriction(deviceAdmin, UserManager.DISALLOW_ADD_USER) policyManager.clearUserRestriction(deviceAdmin, UserManager.DISALLOW_FACTORY_RESET) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { diff --git a/app/src/main/java/io/timelimit/android/logic/AppSetupLogic.kt b/app/src/main/java/io/timelimit/android/logic/AppSetupLogic.kt index 89e02b2..eaca44d 100644 --- a/app/src/main/java/io/timelimit/android/logic/AppSetupLogic.kt +++ b/app/src/main/java/io/timelimit/android/logic/AppSetupLogic.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 - 2022 Jonas Lochmann + * TimeLimit Copyright 2019 - 2025 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 @@ -65,6 +65,11 @@ class AppSetupLogic(private val appLogic: AppLogic) { appLogic.database.deleteAllData() appLogic.database.config().setCustomServerUrlSync(customServerUrl) + + appLogic.database.config().setConsentFlagSync( + ConsentFlags.BLOCK_USER_SWITCH_BY_DEFAULT, + true + ) } run { diff --git a/app/src/main/java/io/timelimit/android/logic/SuspendAppsLogic.kt b/app/src/main/java/io/timelimit/android/logic/SuspendAppsLogic.kt index 9807629..b7385a2 100644 --- a/app/src/main/java/io/timelimit/android/logic/SuspendAppsLogic.kt +++ b/app/src/main/java/io/timelimit/android/logic/SuspendAppsLogic.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 - 2022 Jonas Lochmann + * TimeLimit Copyright 2019 - 2025 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 @@ -19,10 +19,12 @@ import io.timelimit.android.async.Threads import io.timelimit.android.data.invalidation.Observer import io.timelimit.android.data.invalidation.Table import io.timelimit.android.data.model.CategoryApp +import io.timelimit.android.data.model.ConsentFlags import io.timelimit.android.data.model.ExperimentalFlags import io.timelimit.android.data.model.UserType import io.timelimit.android.data.model.derived.UserRelatedData import io.timelimit.android.integration.platform.ProtectionLevel +import io.timelimit.android.integration.platform.android.AndroidFeatures import io.timelimit.android.integration.platform.android.AndroidIntegrationApps import io.timelimit.android.logic.blockingreason.CategoryHandlingCache import java.lang.ref.WeakReference @@ -107,12 +109,23 @@ class SuspendAppsLogic(private val appLogic: AppLogic): Observer { val hasManagedFeatures = featureCategoryApps.isNotEmpty() val enableBlocking = isRestrictedUser && (enableBlockingAtSystemLevel || hasManagedFeatures) + val blockUserSwitchByDefault = + userAndDeviceRelatedData?.deviceRelatedData?.isConsentFlagSet(ConsentFlags.BLOCK_USER_SWITCH_BY_DEFAULT) == true + && userAndDeviceRelatedData.userRelatedData?.user?.type == UserType.Child + + val featureToAllowDefaults = mapOf( + AndroidFeatures.FEATURE_ADD_USER to false, + AndroidFeatures.FEATURE_USER_SWITCH to !blockUserSwitchByDefault + ) + if (!enableBlocking) { lastDefaultCategory = null lastAllowedCategoryList = emptySet() lastCategoryApps = emptyList() applySuspendedApps(emptyList()) - applyBlockedFeatures(emptySet()) + applyBlockedFeatures( + featureToAllowDefaults.filter { !it.value }.map { it.key }.toSet() + ) return } @@ -191,9 +204,15 @@ class SuspendAppsLogic(private val appLogic: AppLogic): Observer { val deviceSpecificFeatureIdentifiers = deviceSpecificFeatures.map { it.appSpecifierString }.toSet() val globalFeatures = featureCategoryApps.filter { !deviceSpecificFeatureIdentifiers.contains(it.appSpecifierString) } val effectiveFeatures = deviceSpecificFeatures + globalFeatures - val featuresToBlock = effectiveFeatures.filter { !categoryIdsToAllow.contains(it.categoryId) } - .map { it.appSpecifierString.substring(DummyApps.FEATURE_APP_PREFIX.length) } - .toSet() + + val featuresToAllow = featureToAllowDefaults + effectiveFeatures.associate { + Pair( + it.appSpecifierString.substring(DummyApps.FEATURE_APP_PREFIX.length), + categoryIdsToAllow.contains(it.categoryId) + ) + } + + val featuresToBlock = featuresToAllow.filter { !it.value }.map { it.key }.toSet() applySuspendedApps(appsToBlock) applyBlockedFeatures(featuresToBlock) diff --git a/app/src/main/java/io/timelimit/android/u2f/protocol/U2FResponse.kt b/app/src/main/java/io/timelimit/android/u2f/protocol/U2FResponse.kt index ee43d8c..84afa7b 100644 --- a/app/src/main/java/io/timelimit/android/u2f/protocol/U2FResponse.kt +++ b/app/src/main/java/io/timelimit/android/u2f/protocol/U2FResponse.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 - 2022 Jonas Lochmann + * TimeLimit Copyright 2019 - 2025 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 @@ -64,10 +64,10 @@ object U2FResponse { val flags = rawResponse.payload[0] - val counter = rawResponse.payload[4].toUInt() or - rawResponse.payload[3].toUInt().shl(8) or - rawResponse.payload[2].toUInt().shl(16) or - rawResponse.payload[1].toUInt().shl(24) + val counter = rawResponse.payload[4].toUByte().toUInt() or + rawResponse.payload[3].toUByte().toUInt().shl(8) or + rawResponse.payload[2].toUByte().toUInt().shl(16) or + rawResponse.payload[1].toUByte().toUInt().shl(24) val signature = rawResponse.payload.sliceArray(5 until rawResponse.payload.size) diff --git a/app/src/main/java/io/timelimit/android/ui/MainActivity.kt b/app/src/main/java/io/timelimit/android/ui/MainActivity.kt index 3bd2739..3872c24 100644 --- a/app/src/main/java/io/timelimit/android/ui/MainActivity.kt +++ b/app/src/main/java/io/timelimit/android/ui/MainActivity.kt @@ -144,8 +144,6 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De ) ) - supportActionBar!!.hide() - U2fManager.setupActivity(this) NotificationChannels.createNotificationChannels(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager, this) diff --git a/app/src/main/java/io/timelimit/android/ui/ScreenScaffold.kt b/app/src/main/java/io/timelimit/android/ui/ScreenScaffold.kt index 583136f..28139c0 100644 --- a/app/src/main/java/io/timelimit/android/ui/ScreenScaffold.kt +++ b/app/src/main/java/io/timelimit/android/ui/ScreenScaffold.kt @@ -51,6 +51,7 @@ fun ScreenScaffold( backStack: List, snackbarHostState: SnackbarHostState?, content: @Composable (PaddingValues) -> Unit, + extraBars: (@Composable () -> Unit)? = null, executeCommand: (UpdateStateCommand) -> Unit, showAuthenticationDialog: (() -> Unit)? ) { @@ -58,69 +59,73 @@ fun ScreenScaffold( Scaffold( topBar = { - TopAppBar( - title = { - Column { - Text( - title, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - - if (subtitle != null) { + Column { + TopAppBar( + title = { + Column { Text( - subtitle, - style = MaterialTheme.typography.subtitle1, + title, 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 = { - if (icon.action != null) executeCommand(icon.action) - icon.handler() + if (subtitle != null) { + Text( + subtitle, + style = MaterialTheme.typography.subtitle1, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) } - ) { - Icon(icon.icon, stringResource(icon.labelResource)) } - } + }, + 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 = { + if (icon.action != null) executeCommand(icon.action) - if (screen?.toolbarOptions?.isEmpty() == false) { - IconButton(onClick = { expandDropdown = true }) { - Icon(Icons.Default.MoreVert, stringResource(R.string.generic_menu)) + icon.handler() + } + ) { + Icon(icon.icon, stringResource(icon.labelResource)) + } } - DropdownMenu( - expanded = expandDropdown, - onDismissRequest = { expandDropdown = false } - ) { - for (option in screen.toolbarOptions) { - DropdownMenuItem(onClick = { - if (option.action != null) executeCommand(option.action) + if (screen?.toolbarOptions?.isEmpty() == false) { + IconButton(onClick = { expandDropdown = true }) { + Icon(Icons.Default.MoreVert, stringResource(R.string.generic_menu)) + } - option.handler() + DropdownMenu( + expanded = expandDropdown, + onDismissRequest = { expandDropdown = false } + ) { + for (option in screen.toolbarOptions) { + DropdownMenuItem(onClick = { + if (option.action != null) executeCommand(option.action) - expandDropdown = false - }) { - Text(stringResource(option.labelResource)) + option.handler() + + expandDropdown = false + }) { + Text(stringResource(option.labelResource)) + } } } } - } - }, - modifier = Modifier, - windowInsets = WindowInsets.statusBarsIgnoringVisibility - ) + }, + modifier = Modifier, + windowInsets = WindowInsets.statusBarsIgnoringVisibility + ) + + extraBars?.invoke() + } }, bottomBar = { val backStackColors = ButtonDefaults.textButtonColors( diff --git a/app/src/main/java/io/timelimit/android/ui/lock/LockActivity.kt b/app/src/main/java/io/timelimit/android/ui/lock/LockActivity.kt index 62e6c5d..07861ff 100644 --- a/app/src/main/java/io/timelimit/android/ui/lock/LockActivity.kt +++ b/app/src/main/java/io/timelimit/android/ui/lock/LockActivity.kt @@ -120,8 +120,6 @@ class LockActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De ) ) - supportActionBar!!.hide() - U2fManager.setupActivity(this) val subtitleLive = syncModel.statusText.asFlow() @@ -149,63 +147,62 @@ class LockActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De subtitle = subtitle, backStack = emptyList(), snackbarHostState = null, - content = { padding -> - Column (Modifier.fillMaxSize().padding(padding)) { - TabRow( - pager.currentPage, - indicator = { tabPositions -> - // workaround for bug - TabRowDefaults.Indicator( - Modifier.tabIndicatorOffset(tabPositions[ - pager.currentPage.coerceAtMost(tabPositions.size - 1) - ]) - ) - } + extraBars = { + TabRow( + pager.currentPage, + indicator = { tabPositions -> + // workaround for bug + TabRowDefaults.Indicator( + Modifier.tabIndicatorOffset(tabPositions[ + pager.currentPage.coerceAtMost(tabPositions.size - 1) + ]) + ) + } + ) { + Tab( + selected = pager.currentPage == 0, + onClick = { pager.requestScrollToPage(0) } ) { - Tab( - selected = pager.currentPage == 0, - onClick = { pager.requestScrollToPage(0) } - ) { - Text( - stringResource(R.string.lock_tab_reason), - Modifier.padding(16.dp) - ) - } - - Tab( - selected = pager.currentPage == 1, - onClick = { pager.requestScrollToPage(1) } - ) { - Text( - stringResource(R.string.lock_tab_action), - Modifier.padding(16.dp) - ) - } - - if (showTasks) Tab( - selected = pager.currentPage == 2, - onClick = { pager.requestScrollToPage(2) } - ) { - Text( - stringResource(R.string.lock_tab_task), - Modifier.padding(16.dp) - ) - } + Text( + stringResource(R.string.lock_tab_reason), + Modifier.padding(16.dp) + ) } - HorizontalPager( - pager, - Modifier.weight(1.0F, fill = true), - pageContent = { index -> - when (index) { - 0 -> AndroidFragment(Modifier.fillMaxSize()) - 1 -> AndroidFragment(Modifier.fillMaxSize()) - 2 -> AndroidFragment(Modifier.fillMaxSize()) - } - } - ) + Tab( + selected = pager.currentPage == 1, + onClick = { pager.requestScrollToPage(1) } + ) { + Text( + stringResource(R.string.lock_tab_action), + Modifier.padding(16.dp) + ) + } + + if (showTasks) Tab( + selected = pager.currentPage == 2, + onClick = { pager.requestScrollToPage(2) } + ) { + Text( + stringResource(R.string.lock_tab_task), + Modifier.padding(16.dp) + ) + } } }, + content = { padding -> + HorizontalPager( + pager, + Modifier.fillMaxSize().padding(padding), + pageContent = { index -> + when (index) { + 0 -> AndroidFragment(Modifier.fillMaxSize()) + 1 -> AndroidFragment(Modifier.fillMaxSize()) + 2 -> AndroidFragment(Modifier.fillMaxSize()) + } + } + ) + }, executeCommand = {}, showAuthenticationDialog = if (pager.currentPage == 1 && !isAuthenticated) ({ showAuthenticationScreen() }) @@ -214,8 +211,6 @@ class LockActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De } } - syncModel.statusText.observe(this) { supportActionBar?.subtitle = it } - currentInstances.add(this) model.init(blockedPackageName, blockedActivityName) diff --git a/app/src/main/java/io/timelimit/android/ui/manage/category/apps/add/AddCategoryAppsFragment.kt b/app/src/main/java/io/timelimit/android/ui/manage/category/apps/add/AddCategoryAppsFragment.kt index ed748f7..2980329 100644 --- a/app/src/main/java/io/timelimit/android/ui/manage/category/apps/add/AddCategoryAppsFragment.kt +++ b/app/src/main/java/io/timelimit/android/ui/manage/category/apps/add/AddCategoryAppsFragment.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 - 2022 Jonas Lochmann + * TimeLimit Copyright 2019 - 2024 Jonas Lochmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,14 +16,21 @@ package io.timelimit.android.ui.manage.category.apps.add import android.app.Dialog +import android.os.Build.VERSION +import android.os.Build.VERSION_CODES import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.ViewGroup.MarginLayoutParams import android.widget.TextView import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat +import androidx.core.view.updateLayoutParams import androidx.fragment.app.DialogFragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.viewModels @@ -220,9 +227,29 @@ class AddCategoryAppsFragment : DialogFragment() { } } + ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, windowInsets -> + val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + + v.updateLayoutParams { + topMargin = insets.top + bottomMargin = insets.bottom + leftMargin = insets.left + rightMargin = insets.right + } + + WindowInsetsCompat.CONSUMED + } + return AlertDialog.Builder(requireContext(), R.style.AppTheme) - .setView(binding.root) - .create() + .setView(binding.root) + .create() + .also { dialog -> + if (VERSION.SDK_INT >= VERSION_CODES.VANILLA_ICE_CREAM) dialog.setOnShowListener { + WindowInsetsControllerCompat(dialog.window!!, binding.root).run { + isAppearanceLightStatusBars = true + } + } + } } fun show(manager: FragmentManager) { diff --git a/app/src/main/java/io/timelimit/android/ui/manage/category/apps/addactivity/AddAppActivitiesDialogFragment.kt b/app/src/main/java/io/timelimit/android/ui/manage/category/apps/addactivity/AddAppActivitiesDialogFragment.kt index ee84029..4dceae8 100644 --- a/app/src/main/java/io/timelimit/android/ui/manage/category/apps/addactivity/AddAppActivitiesDialogFragment.kt +++ b/app/src/main/java/io/timelimit/android/ui/manage/category/apps/addactivity/AddAppActivitiesDialogFragment.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 - 2022 Jonas Lochmann + * TimeLimit Copyright 2019 - 2024 Jonas Lochmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,9 +16,16 @@ package io.timelimit.android.ui.manage.category.apps.addactivity import android.app.Dialog +import android.os.Build.VERSION +import android.os.Build.VERSION_CODES import android.os.Bundle import android.view.LayoutInflater +import android.view.ViewGroup.MarginLayoutParams import androidx.appcompat.app.AlertDialog +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat +import androidx.core.view.updateLayoutParams import androidx.fragment.app.DialogFragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.viewModels @@ -114,9 +121,30 @@ class AddAppActivitiesDialogFragment: DialogFragment() { dismissAllowingStateLoss() } + + ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, windowInsets -> + val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + + v.updateLayoutParams { + topMargin = insets.top + bottomMargin = insets.bottom + leftMargin = insets.left + rightMargin = insets.right + } + + WindowInsetsCompat.CONSUMED + } + return AlertDialog.Builder(requireContext(), R.style.AppTheme) - .setView(binding.root) - .create() + .setView(binding.root) + .create() + .also { dialog -> + if (VERSION.SDK_INT >= VERSION_CODES.VANILLA_ICE_CREAM) dialog.setOnShowListener { + WindowInsetsControllerCompat(dialog.window!!, binding.root).run { + isAppearanceLightStatusBars = true + } + } + } } fun show(fragmentManager: FragmentManager) = showSafe(fragmentManager, DIALOG_TAG) diff --git a/app/src/main/java/io/timelimit/android/ui/manage/device/manage/user/ManageDeviceUserScreen.kt b/app/src/main/java/io/timelimit/android/ui/manage/device/manage/user/ManageDeviceUserScreen.kt index 7a9aeee..55cef2a 100644 --- a/app/src/main/java/io/timelimit/android/ui/manage/device/manage/user/ManageDeviceUserScreen.kt +++ b/app/src/main/java/io/timelimit/android/ui/manage/device/manage/user/ManageDeviceUserScreen.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 - 2023 Jonas Lochmann + * TimeLimit Copyright 2019 - 2025 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 @@ -49,7 +49,7 @@ fun ManageDeviceUserScreen( Card( onClick = { actions.select(item) }, modifier = Modifier - .animateItemPlacement() + .animateItem() .fillMaxWidth(), backgroundColor = when (item.selected) { true -> MaterialTheme.colors.secondary diff --git a/app/src/main/java/io/timelimit/android/ui/manipulation/AnnoyActivity.kt b/app/src/main/java/io/timelimit/android/ui/manipulation/AnnoyActivity.kt index 6bf0fa7..721485a 100644 --- a/app/src/main/java/io/timelimit/android/ui/manipulation/AnnoyActivity.kt +++ b/app/src/main/java/io/timelimit/android/ui/manipulation/AnnoyActivity.kt @@ -89,8 +89,6 @@ class AnnoyActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.D ) ) - supportActionBar!!.hide() - setContent { Theme { ScreenScaffold( diff --git a/app/src/main/java/io/timelimit/android/ui/model/setup/SetupParentHandling.kt b/app/src/main/java/io/timelimit/android/ui/model/setup/SetupParentHandling.kt index b29c7cc..38ccee1 100644 --- a/app/src/main/java/io/timelimit/android/ui/model/setup/SetupParentHandling.kt +++ b/app/src/main/java/io/timelimit/android/ui/model/setup/SetupParentHandling.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 - 2024 Jonas Lochmann + * TimeLimit Copyright 2019 - 2025 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 @@ -23,6 +23,7 @@ import io.timelimit.android.async.Threads import io.timelimit.android.coroutines.executeAndWait import io.timelimit.android.data.backup.DatabaseBackup import io.timelimit.android.data.devicename.DeviceName +import io.timelimit.android.data.model.ConsentFlags import io.timelimit.android.logic.AppLogic import io.timelimit.android.sync.ApplyServerDataStatus import io.timelimit.android.sync.network.NewDeviceInfo @@ -333,6 +334,11 @@ object SetupParentHandling { database.config().setDeviceAuthTokenSync(result.deviceAuthToken) database.config().setEnableBackgroundSync(state.backgroundSync) + database.config().setConsentFlagSync( + ConsentFlags.BLOCK_USER_SWITCH_BY_DEFAULT, + true + ) + ApplyServerDataStatus.applyServerDataStatusSync(result.serverDataStatus, logic.database, logic.platformIntegration) } } diff --git a/app/src/main/java/io/timelimit/android/ui/overview/overview/Device.kt b/app/src/main/java/io/timelimit/android/ui/overview/overview/Device.kt index 5580924..2640d9e 100644 --- a/app/src/main/java/io/timelimit/android/ui/overview/overview/Device.kt +++ b/app/src/main/java/io/timelimit/android/ui/overview/overview/Device.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 - 2023 Jonas Lochmann + * TimeLimit Copyright 2019 - 2025 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 @@ -35,7 +35,7 @@ import io.timelimit.android.ui.model.main.OverviewHandling @OptIn(ExperimentalFoundationApi::class) fun LazyListScope.deviceItems(screen: OverviewHandling.OverviewScreen) { item (key = Pair("devices", "header")) { - ListCommon.SectionHeader(stringResource(R.string.overview_header_devices), Modifier.animateItemPlacement()) + ListCommon.SectionHeader(stringResource(R.string.overview_header_devices), Modifier.animateItem()) } items(screen.devices.list, key = { Pair("device", it.device.id) }) { @@ -48,7 +48,7 @@ fun LazyListScope.deviceItems(screen: OverviewHandling.OverviewScreen) { icon = Icons.Default.Add, label = stringResource(R.string.add_device), action = screen.actions.addDevice, - modifier = Modifier.animateItemPlacement() + modifier = Modifier.animateItem() ) } } @@ -56,7 +56,7 @@ fun LazyListScope.deviceItems(screen: OverviewHandling.OverviewScreen) { if (screen.devices.canShowMore != null) { item (key = Pair("devices", "more")) { ListCommon.ShowMoreItem( - modifier = Modifier.animateItemPlacement(), + modifier = Modifier.animateItem(), action = { screen.actions.showMoreDevices(screen.devices.canShowMore) } ) } @@ -71,7 +71,7 @@ fun LazyItemScope.DeviceItem( ) { ListCardCommon.Card( Modifier - .animateItemPlacement() + .animateItem() .padding(horizontal = 8.dp) .clickable(onClick = { openAction(item) }) ) { diff --git a/app/src/main/java/io/timelimit/android/ui/overview/overview/Intro.kt b/app/src/main/java/io/timelimit/android/ui/overview/overview/Intro.kt index 8099c13..b05a8d4 100644 --- a/app/src/main/java/io/timelimit/android/ui/overview/overview/Intro.kt +++ b/app/src/main/java/io/timelimit/android/ui/overview/overview/Intro.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 - 2023 Jonas Lochmann + * TimeLimit Copyright 2019 - 2025 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 @@ -40,7 +40,7 @@ fun LazyListScope.introItems( item (key = Pair("intro", "finish setup")) { ListCardCommon.Card( modifier = Modifier - .animateItemPlacement() + .animateItem() .padding(horizontal = 8.dp) ) { Text( @@ -62,7 +62,7 @@ fun LazyListScope.introItems( item (key = Pair("intro", "outdated server")) { ListCardCommon.Card( modifier = Modifier - .animateItemPlacement() + .animateItem() .padding(horizontal = 8.dp) ) { Text( @@ -79,7 +79,7 @@ fun LazyListScope.introItems( item (key = Pair("intro", "server message")) { ListCardCommon.Card( modifier = Modifier - .animateItemPlacement() + .animateItem() .padding(horizontal = 8.dp) ) { Text( @@ -108,7 +108,7 @@ fun LazyListScope.introItems( SwipeToDismiss( state = state, background = {}, - modifier = Modifier.animateItemPlacement() + modifier = Modifier.animateItem() ) { ListCardCommon.Card( modifier = Modifier.padding(horizontal = 8.dp) @@ -133,7 +133,7 @@ fun LazyListScope.introItems( item (key = Pair("intro", "task review")) { ListCardCommon.Card( modifier = Modifier - .animateItemPlacement() + .animateItem() .padding(horizontal = 8.dp) ) { Text( diff --git a/app/src/main/java/io/timelimit/android/ui/overview/overview/User.kt b/app/src/main/java/io/timelimit/android/ui/overview/overview/User.kt index 7cd85d5..7805783 100644 --- a/app/src/main/java/io/timelimit/android/ui/overview/overview/User.kt +++ b/app/src/main/java/io/timelimit/android/ui/overview/overview/User.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 - 2023 Jonas Lochmann + * TimeLimit Copyright 2019 - 2025 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 @@ -35,7 +35,7 @@ import io.timelimit.android.ui.model.main.OverviewHandling @OptIn(ExperimentalFoundationApi::class) fun LazyListScope.userItems(screen: OverviewHandling.OverviewScreen) { item (key = Pair("users", "header")) { - ListCommon.SectionHeader(stringResource(R.string.overview_header_users), Modifier.animateItemPlacement()) + ListCommon.SectionHeader(stringResource(R.string.overview_header_users), Modifier.animateItem()) } items(screen.users.list, key = { Pair("user", it.id) }) { UserItem(it, screen.actions) } @@ -45,13 +45,13 @@ fun LazyListScope.userItems(screen: OverviewHandling.OverviewScreen) { icon = Icons.Default.Add, label = stringResource(R.string.add_user_title), action = screen.actions.addUser, - modifier = Modifier.animateItemPlacement() + modifier = Modifier.animateItem() ) } if (screen.users.canShowMore) item (key = Pair("users", "more")) { ListCommon.ShowMoreItem ( - modifier = Modifier.animateItemPlacement(), + modifier = Modifier.animateItem(), action = screen.actions.showMoreUsers ) } @@ -65,7 +65,7 @@ fun LazyItemScope.UserItem( ) { ListCardCommon.Card( Modifier - .animateItemPlacement() + .animateItem() .padding(horizontal = 8.dp) .clickable(onClick = { actions.openUser(user) }) ) { diff --git a/app/src/main/java/io/timelimit/android/ui/payment/ActivityPurchaseModel.kt b/app/src/main/java/io/timelimit/android/ui/payment/ActivityPurchaseModel.kt index 001cf94..ec3bc27 100644 --- a/app/src/main/java/io/timelimit/android/ui/payment/ActivityPurchaseModel.kt +++ b/app/src/main/java/io/timelimit/android/ui/payment/ActivityPurchaseModel.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 - 2023 Jonas Lochmann + * TimeLimit Copyright 2019 - 2025 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 @@ -74,9 +74,14 @@ class ActivityPurchaseModel(application: Application): AndroidViewModel(applicat clientMutex.withLock { if (_billingClient == null) { _billingClient = BillingClient.newBuilder(getApplication()) - .enablePendingPurchases() - .setListener(purchaseUpdatedListener) - .build() + .enablePendingPurchases( + PendingPurchasesParams + .newBuilder() + .enableOneTimeProducts() + .build() + ) + .setListener(purchaseUpdatedListener) + .build() } val initBillingClient = _billingClient!! diff --git a/app/src/main/java/io/timelimit/android/ui/setup/child/SetupRemoteChildViewModel.kt b/app/src/main/java/io/timelimit/android/ui/setup/child/SetupRemoteChildViewModel.kt index 6fa11ca..392a0b5 100644 --- a/app/src/main/java/io/timelimit/android/ui/setup/child/SetupRemoteChildViewModel.kt +++ b/app/src/main/java/io/timelimit/android/ui/setup/child/SetupRemoteChildViewModel.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 - 2023 Jonas Lochmann + * TimeLimit Copyright 2019 - 2025 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 @@ -24,6 +24,7 @@ import io.timelimit.android.coroutines.executeAndWait import io.timelimit.android.coroutines.runAsync import io.timelimit.android.data.backup.DatabaseBackup import io.timelimit.android.data.devicename.DeviceName +import io.timelimit.android.data.model.ConsentFlags import io.timelimit.android.livedata.castDown import io.timelimit.android.logic.AppLogic import io.timelimit.android.logic.DefaultAppLogic @@ -70,6 +71,11 @@ class SetupRemoteChildViewModel(application: Application): AndroidViewModel(appl logic.database.config().setOwnDeviceIdSync(registerResponse.ownDeviceId) logic.database.config().setDeviceAuthTokenSync(registerResponse.deviceAuthToken) + logic.database.config().setConsentFlagSync( + ConsentFlags.BLOCK_USER_SWITCH_BY_DEFAULT, + true + ) + ApplyServerDataStatus.applyServerDataStatusSync(clientStatusResponse, logic.database, logic.platformIntegration) } } diff --git a/app/src/main/java/io/timelimit/android/ui/update/UpdateActivity.kt b/app/src/main/java/io/timelimit/android/ui/update/UpdateActivity.kt index e213776..8698abd 100644 --- a/app/src/main/java/io/timelimit/android/ui/update/UpdateActivity.kt +++ b/app/src/main/java/io/timelimit/android/ui/update/UpdateActivity.kt @@ -47,8 +47,6 @@ class UpdateActivity: AppCompatActivity() { ) ) - supportActionBar!!.hide() - setContent { Theme { ScreenScaffold( diff --git a/app/src/main/play/de-DE/whatsnew b/app/src/main/play/de-DE/whatsnew index 7996b5a..eb689fd 100644 --- a/app/src/main/play/de-DE/whatsnew +++ b/app/src/main/play/de-DE/whatsnew @@ -1,3 +1,2 @@ -- Anpassungen für Android 15 -- falsch angezeigte Nutzungsdauer bei Regeln die je Tag gelten an Tagen, an denen diese nicht gelten, behoben +- Funktionsumfang bei Verwendung der Geräte-Besitzer-Berechtigung erweitert - enthaltene Komponenten aktualisiert diff --git a/app/src/main/play/en-US/whatsnew b/app/src/main/play/en-US/whatsnew index 09107ef..8e96ea5 100644 --- a/app/src/main/play/en-US/whatsnew +++ b/app/src/main/play/en-US/whatsnew @@ -1,3 +1,2 @@ -- adjustments for Android 15 -- fix incorrect used time at rules that apply per day at days where they do not apply +- add more features for users of the device owner permission - update contained software diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index cf95126..17198eb 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -1,6 +1,6 @@ -