From abe6d47a96b70f4d6ff0e5b85d1335cc9caba9dd Mon Sep 17 00:00:00 2001 From: Jonas Lochmann Date: Mon, 21 Jul 2025 02:00:00 +0200 Subject: [PATCH] Add new restrictions that can be set by the device owner --- .../android/data/model/ConfigurationItem.kt | 5 ++- .../data/model/derived/DeviceRelatedData.kt | 3 +- .../platform/android/AndroidFeatures.kt | 42 ++++++++++++++++++- .../platform/android/AndroidIntegration.kt | 2 - .../timelimit/android/logic/AppSetupLogic.kt | 7 +++- .../android/logic/SuspendAppsLogic.kt | 29 ++++++++++--- .../ui/model/setup/SetupParentHandling.kt | 8 +++- .../setup/child/SetupRemoteChildViewModel.kt | 8 +++- app/src/main/res/values-de/strings.xml | 6 ++- app/src/main/res/values/strings.xml | 4 ++ 10 files changed, 100 insertions(+), 14 deletions(-) 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/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 4170731..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) { @@ -557,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/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/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/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 @@