Compare commits

..

No commits in common. "master" and "release-7.2.4" have entirely different histories.

19 changed files with 40 additions and 141 deletions

1
.gitignore vendored
View file

@ -10,4 +10,3 @@
/captures
.externalNativeBuild
.idea
.kotlin

View file

@ -21,18 +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"
id("org.jetbrains.kotlin.plugin.compose") version "2.0.21"
}
android {
namespace 'io.timelimit.android'
compileSdk 36
compileSdk 35
defaultConfig {
applicationId "io.timelimit.android"
minSdkVersion 26
targetSdkVersion 36
versionCode 224
versionName "7.3.0"
targetSdkVersion 35
versionCode 223
versionName "7.2.4"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
kapt {
arguments {
@ -172,7 +172,7 @@ dependencies {
def paging_version = "3.3.6"
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.1.10"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.0.21"
implementation 'androidx.appcompat:appcompat:1.7.1'
implementation 'androidx.core:core:1.16.0'
implementation 'androidx.cardview:cardview:1.0.0'
@ -201,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.9.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test:runner:1.6.2'
@ -218,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:8.0.0"
googleApiImplementation "com.android.billingclient:billing-ktx:7.1.1"
implementation('io.socket:socket.io-client:2.0.0') {
exclude group: 'org.json', module: 'json'
@ -230,5 +230,5 @@ dependencies {
implementation 'com.google.zxing:core:3.3.3'
api "com.squareup.wire:wire-runtime:5.3.5"
api "com.squareup.wire:wire-runtime:4.4.3"
}

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 - 2025 Jonas Lochmann
* TimeLimit Copyright <C> 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
@ -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.QUERY_MISMATCH)
@SuppressWarnings(RoomWarnings.CURSOR_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<List<UsedTimeListItem>>
// 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.QUERY_MISMATCH)
@SuppressWarnings(RoomWarnings.CURSOR_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<List<UsedTimeListItem>>
}

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 - 2025 Jonas Lochmann
* 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
@ -295,7 +295,4 @@ object ExperimentalFlags {
object ConsentFlags {
const val APP_LIST_SYNC = 1L
// this is used internally
const val BLOCK_USER_SWITCH_BY_DEFAULT = 2L
}

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 - 2025 Jonas Lochmann
* 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
@ -73,5 +73,4 @@ data class DeviceRelatedData (
}
fun isExperimentalFlagSetSync(flags: Long) = (experimentalFlags and flags) == flags
fun isConsentFlagSet(flags: Long) = (consentFlags and flags) == flags
}

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 - 2025 Jonas Lochmann
* 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
@ -27,10 +27,6 @@ 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<String>, policyManager: DevicePolicyManager, admin: ComponentName): Boolean {
fun apply(feature: String, restriction: String) {
@ -44,18 +40,6 @@ 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
}
@ -76,30 +60,6 @@ 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
}
}

View file

@ -507,6 +507,7 @@ 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) {
@ -556,6 +557,7 @@ 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) {

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 - 2025 Jonas Lochmann
* 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
@ -65,11 +65,6 @@ 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 {

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 - 2025 Jonas Lochmann
* 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
@ -19,12 +19,10 @@ 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
@ -109,23 +107,12 @@ 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(
featureToAllowDefaults.filter { !it.value }.map { it.key }.toSet()
)
applyBlockedFeatures(emptySet())
return
}
@ -204,15 +191,9 @@ 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 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()
val featuresToBlock = effectiveFeatures.filter { !categoryIdsToAllow.contains(it.categoryId) }
.map { it.appSpecifierString.substring(DummyApps.FEATURE_APP_PREFIX.length) }
.toSet()
applySuspendedApps(appsToBlock)
applyBlockedFeatures(featuresToBlock)

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 - 2025 Jonas Lochmann
* TimeLimit Copyright <C> 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
@ -23,7 +23,6 @@ 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
@ -334,11 +333,6 @@ 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)
}
}

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 - 2025 Jonas Lochmann
* 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
@ -74,14 +74,9 @@ class ActivityPurchaseModel(application: Application): AndroidViewModel(applicat
clientMutex.withLock {
if (_billingClient == null) {
_billingClient = BillingClient.newBuilder(getApplication())
.enablePendingPurchases(
PendingPurchasesParams
.newBuilder()
.enableOneTimeProducts()
.build()
)
.setListener(purchaseUpdatedListener)
.build()
.enablePendingPurchases()
.setListener(purchaseUpdatedListener)
.build()
}
val initBillingClient = _billingClient!!

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 - 2025 Jonas Lochmann
* 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
@ -24,7 +24,6 @@ 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
@ -71,11 +70,6 @@ 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)
}
}

View file

@ -1,2 +1,2 @@
- Funktionsumfang bei Verwendung der Geräte-Besitzer-Berechtigung erweitert
- Abstürze in bestimmten Fällen behoben
- enthaltene Komponenten aktualisiert

View file

@ -1,2 +1,2 @@
- add more features for users of the device owner permission
- fix crashes in specific scenarios
- update contained software

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
TimeLimit Copyright <C> 2019 - 2025 Jonas Lochmann
TimeLimit Copyright <C> 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
the Free Software Foundation version 3 of the License.
@ -1752,10 +1752,6 @@
<string name="dummy_app_unassigned_system_image_app">nicht zugeordnete Apps von der Systempartition</string>
<string name="dummy_app_feature_adb">Entwickleroptionen</string>
<string name="dummy_app_feature_dns">DNS-Einstellungen</string>
<string name="dummy_app_feature_add_user">Systembenutzer erstellen</string>
<string name="dummy_app_feature_switch_user">Systembenutzer wechseln</string>
<string name="dummy_app_feature_vpn">VPN konfigurieren</string>
<string name="dummy_app_feature_unknown_sources">Apps aus unbekannten Quellen installieren</string>
<string name="dummy_app_activity_audio">Hintergrundmusikwiedergabe</string>
<string name="notify_permission_title">Benachrichtigungen</string>

View file

@ -1804,10 +1804,6 @@
<string name="dummy_app_unassigned_system_image_app">not assigned Apps from the system image</string>
<string name="dummy_app_feature_adb">Developer Options</string>
<string name="dummy_app_feature_dns">DNS Settings</string>
<string name="dummy_app_feature_add_user">Create System User</string>
<string name="dummy_app_feature_switch_user">Switch System User</string>
<string name="dummy_app_feature_vpn">Configure VPN</string>
<string name="dummy_app_feature_unknown_sources">Install Apps from unknown sources</string>
<string name="dummy_app_activity_audio">Background Audio Playback</string>
<string name="notify_permission_title">Notifications</string>

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 - 2025 Jonas Lochmann
* 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
@ -42,7 +42,7 @@ object BillingClient {
enum class ProductType { INAPP }
object Builder {
fun enablePendingPurchases(params: PendingPurchasesParams) = this
fun enablePendingPurchases() = this
fun setListener(listener: PurchasesUpdatedListener) = this
fun build() = BillingClient
}
@ -143,13 +143,4 @@ object QueryPurchasesParams {
fun newBuilder() = this
fun setProductType(type: BillingClient.ProductType) = this
fun build() = this
}
object PendingPurchasesParams {
object Builder {
fun enableOneTimeProducts() = this
fun build() = PendingPurchasesParams
}
fun newBuilder() = Builder
}

View file

@ -15,10 +15,10 @@
*/
plugins {
id 'com.android.application' version '8.11.1' apply false
id 'com.android.library' version '8.11.1' apply false
id 'com.android.application' version '8.10.1' apply false
id 'com.android.library' version '8.10.1' apply false
id 'org.jetbrains.kotlin.android' version "2.0.21" apply false
id 'com.google.devtools.ksp' version '1.9.21-1.0.16' apply false
id 'androidx.navigation.safeargs' version '2.6.0' apply false
id 'com.squareup.wire' version '5.3.5' apply false
id 'com.squareup.wire' version '4.4.3' apply false
}

View file

@ -2,5 +2,5 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
distributionSha256Sum=20f1b1176237254a6fc204d8434196fa11a4cfb387567519c61556e8710aed78
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
distributionSha256Sum=89d4e70e4e84e2d2dfbb63e4daa53e21b25017cc70c37e4eea31ee51fb15098a