Compare commits

..

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

34 changed files with 238 additions and 382 deletions

1
.gitignore vendored
View file

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

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
@ -21,18 +21,17 @@ 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 36
compileSdk 35
defaultConfig {
applicationId "io.timelimit.android"
minSdkVersion 26
targetSdkVersion 36
versionCode 224
versionName "7.3.0"
targetSdkVersion 35
versionCode 220
versionName "7.2.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
kapt {
arguments {
@ -167,24 +166,24 @@ wire {
dependencies {
def nav_version = "2.5.3"
def room_version = "2.7.2"
def work_version = '2.10.2'
def paging_version = "3.3.6"
def room_version = "2.6.1"
def work_version = '2.9.1'
def paging_version = "3.3.2"
implementation fileTree(dir: 'libs', include: ['*.jar'])
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 "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.21"
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'androidx.core:core:1.15.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.gridlayout:gridlayout:1.1.0'
implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation "com.google.android.material:material:1.12.0"
implementation 'androidx.compose.material:material:1.8.3'
implementation 'androidx.activity:activity-compose:1.10.1'
implementation 'androidx.compose.material:material:1.7.5'
implementation 'androidx.activity:activity-compose:1.9.3'
implementation "com.google.accompanist:accompanist-flowlayout:0.30.0"
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.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.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui:$nav_version"
@ -201,8 +200,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 +217,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 +229,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

@ -134,19 +134,9 @@ class AndroidDeviceOwnerApi(
if (VERSION.SDK_INT < VERSION_CODES.LOLLIPOP) return false
if (!devicePolicyManager.isDeviceOwnerApp(componentName.packageName)) return false
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
}
return devicePolicyManager.setPermissionGrantState(
componentName, componentName.packageName, Manifest.permission.ACCESS_FINE_LOCATION,
DevicePolicyManager.PERMISSION_GRANT_STATE_GRANTED
)
}
}

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) {
@ -518,24 +519,16 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio
context.packageName,
Manifest.permission.ACCESS_FINE_LOCATION,
).let {
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
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
)
}
}
@ -556,6 +549,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 - 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
@ -64,10 +64,10 @@ object U2FResponse {
val flags = rawResponse.payload[0]
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 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 signature = rawResponse.payload.sliceArray(5 until rawResponse.payload.size)

View file

@ -144,6 +144,8 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
)
)
supportActionBar!!.hide()
U2fManager.setupActivity(this)
NotificationChannels.createNotificationChannels(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager, this)

View file

@ -51,7 +51,6 @@ fun ScreenScaffold(
backStack: List<BackStackItem>,
snackbarHostState: SnackbarHostState?,
content: @Composable (PaddingValues) -> Unit,
extraBars: (@Composable () -> Unit)? = null,
executeCommand: (UpdateStateCommand) -> Unit,
showAuthenticationDialog: (() -> Unit)?
) {
@ -59,73 +58,69 @@ fun ScreenScaffold(
Scaffold(
topBar = {
Column {
TopAppBar(
title = {
Column {
TopAppBar(
title = {
Column {
Text(
title,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
if (subtitle != null) {
Text(
title,
subtitle,
style = MaterialTheme.typography.subtitle1,
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 = {
if (icon.action != null) executeCommand(icon.action)
}
},
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()
}
) {
Icon(icon.icon, stringResource(icon.labelResource))
icon.handler()
}
) {
Icon(icon.icon, stringResource(icon.labelResource))
}
}
if (screen?.toolbarOptions?.isEmpty() == false) {
IconButton(onClick = { expandDropdown = true }) {
Icon(Icons.Default.MoreVert, stringResource(R.string.generic_menu))
}
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 = {
if (option.action != null) executeCommand(option.action)
DropdownMenu(
expanded = expandDropdown,
onDismissRequest = { expandDropdown = false }
) {
for (option in screen.toolbarOptions) {
DropdownMenuItem(onClick = {
if (option.action != null) executeCommand(option.action)
option.handler()
option.handler()
expandDropdown = false
}) {
Text(stringResource(option.labelResource))
}
expandDropdown = false
}) {
Text(stringResource(option.labelResource))
}
}
}
},
modifier = Modifier,
windowInsets = WindowInsets.statusBarsIgnoringVisibility
)
extraBars?.invoke()
}
}
},
modifier = Modifier,
windowInsets = WindowInsets.statusBarsIgnoringVisibility
)
},
bottomBar = {
val backStackColors = ButtonDefaults.textButtonColors(

View file

@ -120,6 +120,8 @@ class LockActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
)
)
supportActionBar!!.hide()
U2fManager.setupActivity(this)
val subtitleLive = syncModel.statusText.asFlow()
@ -147,61 +149,62 @@ class LockActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
subtitle = subtitle,
backStack = emptyList(),
snackbarHostState = null,
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) }
) {
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)
)
}
}
},
content = { padding ->
HorizontalPager(
pager,
Modifier.fillMaxSize().padding(padding),
pageContent = { index ->
when (index) {
0 -> AndroidFragment<LockReasonFragment>(Modifier.fillMaxSize())
1 -> AndroidFragment<LockActionFragment>(Modifier.fillMaxSize())
2 -> AndroidFragment<LockTaskFragment>(Modifier.fillMaxSize())
Column (Modifier.fillMaxSize().padding(padding)) {
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) }
) {
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)
)
}
}
)
HorizontalPager(
pager,
Modifier.weight(1.0F, fill = true),
pageContent = { index ->
when (index) {
0 -> AndroidFragment<LockReasonFragment>(Modifier.fillMaxSize())
1 -> AndroidFragment<LockActionFragment>(Modifier.fillMaxSize())
2 -> AndroidFragment<LockTaskFragment>(Modifier.fillMaxSize())
}
}
)
}
},
executeCommand = {},
showAuthenticationDialog =
@ -211,6 +214,8 @@ class LockActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
}
}
syncModel.statusText.observe(this) { supportActionBar?.subtitle = it }
currentInstances.add(this)
model.init(blockedPackageName, blockedActivityName)

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 - 2024 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
@ -16,21 +16,14 @@
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
@ -227,29 +220,9 @@ class AddCategoryAppsFragment : DialogFragment() {
}
}
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, windowInsets ->
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
v.updateLayoutParams<MarginLayoutParams> {
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()
.also { dialog ->
if (VERSION.SDK_INT >= VERSION_CODES.VANILLA_ICE_CREAM) dialog.setOnShowListener {
WindowInsetsControllerCompat(dialog.window!!, binding.root).run {
isAppearanceLightStatusBars = true
}
}
}
.setView(binding.root)
.create()
}
fun show(manager: FragmentManager) {

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 - 2024 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
@ -16,16 +16,9 @@
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
@ -121,30 +114,9 @@ class AddAppActivitiesDialogFragment: DialogFragment() {
dismissAllowingStateLoss()
}
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, windowInsets ->
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
v.updateLayoutParams<MarginLayoutParams> {
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()
.also { dialog ->
if (VERSION.SDK_INT >= VERSION_CODES.VANILLA_ICE_CREAM) dialog.setOnShowListener {
WindowInsetsControllerCompat(dialog.window!!, binding.root).run {
isAppearanceLightStatusBars = true
}
}
}
.setView(binding.root)
.create()
}
fun show(fragmentManager: FragmentManager) = showSafe(fragmentManager, DIALOG_TAG)

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
@ -49,7 +49,7 @@ fun ManageDeviceUserScreen(
Card(
onClick = { actions.select(item) },
modifier = Modifier
.animateItem()
.animateItemPlacement()
.fillMaxWidth(),
backgroundColor = when (item.selected) {
true -> MaterialTheme.colors.secondary

View file

@ -89,6 +89,8 @@ class AnnoyActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.D
)
)
supportActionBar!!.hide()
setContent {
Theme {
ScreenScaffold(

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
@ -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.animateItem())
ListCommon.SectionHeader(stringResource(R.string.overview_header_devices), Modifier.animateItemPlacement())
}
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.animateItem()
modifier = Modifier.animateItemPlacement()
)
}
}
@ -56,7 +56,7 @@ fun LazyListScope.deviceItems(screen: OverviewHandling.OverviewScreen) {
if (screen.devices.canShowMore != null) {
item (key = Pair("devices", "more")) {
ListCommon.ShowMoreItem(
modifier = Modifier.animateItem(),
modifier = Modifier.animateItemPlacement(),
action = { screen.actions.showMoreDevices(screen.devices.canShowMore) }
)
}
@ -71,7 +71,7 @@ fun LazyItemScope.DeviceItem(
) {
ListCardCommon.Card(
Modifier
.animateItem()
.animateItemPlacement()
.padding(horizontal = 8.dp)
.clickable(onClick = { openAction(item) })
) {

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
@ -40,7 +40,7 @@ fun LazyListScope.introItems(
item (key = Pair("intro", "finish setup")) {
ListCardCommon.Card(
modifier = Modifier
.animateItem()
.animateItemPlacement()
.padding(horizontal = 8.dp)
) {
Text(
@ -62,7 +62,7 @@ fun LazyListScope.introItems(
item (key = Pair("intro", "outdated server")) {
ListCardCommon.Card(
modifier = Modifier
.animateItem()
.animateItemPlacement()
.padding(horizontal = 8.dp)
) {
Text(
@ -79,7 +79,7 @@ fun LazyListScope.introItems(
item (key = Pair("intro", "server message")) {
ListCardCommon.Card(
modifier = Modifier
.animateItem()
.animateItemPlacement()
.padding(horizontal = 8.dp)
) {
Text(
@ -108,7 +108,7 @@ fun LazyListScope.introItems(
SwipeToDismiss(
state = state,
background = {},
modifier = Modifier.animateItem()
modifier = Modifier.animateItemPlacement()
) {
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
.animateItem()
.animateItemPlacement()
.padding(horizontal = 8.dp)
) {
Text(

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
@ -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.animateItem())
ListCommon.SectionHeader(stringResource(R.string.overview_header_users), Modifier.animateItemPlacement())
}
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.animateItem()
modifier = Modifier.animateItemPlacement()
)
}
if (screen.users.canShowMore) item (key = Pair("users", "more")) {
ListCommon.ShowMoreItem (
modifier = Modifier.animateItem(),
modifier = Modifier.animateItemPlacement(),
action = screen.actions.showMoreUsers
)
}
@ -65,7 +65,7 @@ fun LazyItemScope.UserItem(
) {
ListCardCommon.Card(
Modifier
.animateItem()
.animateItemPlacement()
.padding(horizontal = 8.dp)
.clickable(onClick = { actions.openUser(user) })
) {

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

@ -47,6 +47,8 @@ class UpdateActivity: AppCompatActivity() {
)
)
supportActionBar!!.hide()
setContent {
Theme {
ScreenScaffold(

View file

@ -1,2 +1,3 @@
- Funktionsumfang bei Verwendung der Geräte-Besitzer-Berechtigung erweitert
- Anpassungen für Android 15
- falsch angezeigte Nutzungsdauer bei Regeln die je Tag gelten an Tagen, an denen diese nicht gelten, behoben
- enthaltene Komponenten aktualisiert

View file

@ -1,2 +1,3 @@
- add more features for users of the device owner permission
- adjustments for Android 15
- fix incorrect used time at rules that apply per day at days where they do not apply
- 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

@ -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.
@ -1649,7 +1649,7 @@
<a href="https://legal.timelimit.io/en/privacy/">https://legal.timelimit.io/en/privacy/</a>
</string>
<string name="terms_gpl" translatable="false">
TimeLimit Copyright &#169; 2019 - 2025 Jonas Lochmann
TimeLimit Copyright &#169; 2019 - 2024 Jonas Lochmann
\nThis 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.
@ -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 - 2024 Jonas Lochmann
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.
@ -15,7 +15,7 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorSecondary">@color/colorAccent</item>

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

@ -0,0 +1,23 @@
package io.timelimit.android.ui.manage.category.blocked_times
import org.junit.Assert.assertEquals
import org.junit.Test
class MinutesOfWeekItemsTest {
@Test
fun canGetAllItems() {
for (i in 0 until MinuteOfWeekItems.itemsPerWeek) {
MinuteOfWeekItems.getItemAtPosition(i)
}
}
@Test
fun reverseLookupReturnsSameItem() {
for (i in 0 until MinuteOfWeekItems.itemsPerWeek) {
val item = MinuteOfWeekItems.getItemAtPosition(i)
val index = MinuteOfWeekItems.getPositionOfItem(item)
assertEquals(item.toString(), i, index)
}
}
}

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
@ -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 'org.jetbrains.kotlin.android' version "2.0.21" apply false
id 'com.android.application' version '8.7.2' apply false
id 'com.android.library' version '8.7.2' apply false
id 'org.jetbrains.kotlin.android' version "1.9.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.9-all.zip
distributionSha256Sum=258e722ec21e955201e31447b0aed14201765a3bfbae296a46cf60b70e66db70