Compare commits

...

62 commits

Author SHA1 Message Date
Jonas Lochmann
6c02c4e4c7
Release 7.3.0 2025-07-21 02:00:00 +02:00
Jonas Lochmann
c746770e89
Increase target sdk to 36 2025-07-21 02:00:00 +02:00
Jonas Lochmann
0505fa0f8b
Extend .gitignore 2025-07-21 02:00:00 +02:00
Jonas Lochmann
4498aa191f
Update Google Play Billing Library 2025-07-21 02:00:00 +02:00
Jonas Lochmann
3d49bd8229
Update buildtools and dependencies 2025-07-21 02:00:00 +02:00
Jonas Lochmann
abe6d47a96
Add new restrictions that can be set by the device owner 2025-07-21 02:00:00 +02:00
Jonas Lochmann
6ba9614557
Release 7.2.4 2025-06-23 02:00:00 +02:00
Jonas Lochmann
53732e47dd
catch SecurityException caused by adjusting sensor related permissions 2025-06-23 18:29:22 +02:00
Jonas Lochmann
acdd83c39a
Update build tools 2025-06-23 18:23:40 +02:00
Jonas Lochmann
f4c35dec55
Update dependencies 2025-06-16 02:00:00 +02:00
Jonas Lochmann
5f69014850
Release 7.2.3 2025-05-05 02:00:00 +02:00
Jonas Lochmann
d02631d5c1
Update dependencies 2025-05-05 02:00:00 +02:00
Jonas Lochmann
801bf760dc
update buildtools 2025-05-05 02:00:00 +02:00
Jonas Lochmann
daff66b26b
remove generated file 2025-05-05 02:00:00 +02:00
Jonas Lochmann
a36ffa861a
remove obsolete tests 2025-04-28 02:00:00 +02:00
Jonas Lochmann
de0b6d1c8f
Fix parsing u2f counter bytes >= 0x80 2025-04-28 02:00:00 +02:00
Jonas Lochmann
2e68798e2c
Update buildtools and dependencies 2025-04-28 02:00:00 +02:00
Jonas Lochmann
73a82f3cf8
Update buildtools and dependencies 2025-01-13 01:00:00 +01:00
Jonas Lochmann
bd88da38d2
Update copyright year 2025-01-13 01:00:00 +01:00
Jonas Lochmann
e54ff92cbb
Release 7.2.2 2024-10-28 01:00:00 +01:00
Jonas Lochmann
8e02eb3fb3
Move lockscreen tabs into the toolbar 2024-10-28 01:00:00 +01:00
Jonas Lochmann
11a47c5f30
Disable ActionBar in the theme 2024-11-04 20:01:46 +01:00
Jonas Lochmann
15ae018589
Add insets to the add app dialog 2024-11-04 20:01:43 +01:00
Jonas Lochmann
2ed75c7f0f
Release 7.2.1 2024-10-28 01:00:00 +01:00
Jonas Lochmann
13fe10b543
Disable room warnings regarding internally used column 2024-10-28 01:00:00 +01:00
Jonas Lochmann
bc0e83b916
Improve edge to edge support 2024-10-28 01:00:00 +01:00
Jonas Lochmann
679276e3cf
Enable edge to edge support 2024-10-28 01:00:00 +01:00
Jonas Lochmann
3206d925e3
Update dependencies 2024-10-28 01:00:00 +01:00
Jonas Lochmann
b0e5a338e6
Update buildtools 2024-10-28 01:00:00 +01:00
Jonas Lochmann
376b7ca6de
Release 7.2.0 2024-10-28 01:00:00 +01:00
Jonas Lochmann
1cdaed60e1
Fix incorrect used time at rules that apply per day at days where they do not apply 2024-10-28 01:00:00 +01:00
Jonas Lochmann
d5bc1f9881
Enable predictive back gesture 2024-10-14 02:00:00 +02:00
Jonas Lochmann
6e9641638f
Update target sdk 2024-10-14 02:00:00 +02:00
Jonas Lochmann
6a4b4505bb
Update dependencies 2024-10-14 02:00:00 +02:00
Jonas Lochmann
6b09de4c59
Update buildtools 2024-10-14 02:00:00 +02:00
Jonas Lochmann
e01cf09f51
Release 7.1.0 2024-07-29 02:00:00 +02:00
Jonas Lochmann
6e5bee8966
Update billing library 2024-07-29 02:00:00 +02:00
Jonas Lochmann
5544883cc3
Migrate the buildfeatures.buildconfig flag 2024-07-29 02:00:00 +02:00
Jonas Lochmann
1d9a1f1071
Improve background loop exception presentation 2024-07-29 02:00:00 +02:00
Jonas Lochmann
ac889c142e
Update dependencies 2024-07-29 02:00:00 +02:00
Jonas Lochmann
eb680a4f42
Update buildtools 2024-07-29 02:00:00 +02:00
Jonas Lochmann
faea667d98
Release 7.0.1 2024-07-08 02:00:00 +02:00
Jonas Lochmann
a2e553c6f5
Send app package and version in sendMailLoginCode 2024-07-08 02:00:00 +02:00
Jonas Lochmann
314f9af6ef
Catch missing attestation support 2024-07-08 02:00:00 +02:00
Jonas Lochmann
85aac49e06
Update dependencies 2024-07-08 02:00:00 +02:00
Jonas Lochmann
64f74d94fc
Update buildtools 2024-07-08 02:00:00 +02:00
Jonas Lochmann
00d87ac730
Update buildtools 2024-05-13 02:00:00 +02:00
Jonas Lochmann
b5a65530a9
Release 7.0.0 2024-04-22 02:00:00 +02:00
Jonas Lochmann
df009a2154
Take a look at the room compiler warnings 2024-04-15 02:00:00 +02:00
Jonas Lochmann
df172b99df
Fix warnings in RoomDatabase.kt 2024-04-15 02:00:00 +02:00
Jonas Lochmann
f425f5ce2f
Fix warning in RunAsync.kt 2024-04-15 02:00:00 +02:00
Jonas Lochmann
35839ced5c
Fix warnings in Migration.kt 2024-04-15 02:00:00 +02:00
Jonas Lochmann
d579f025e1
Update dependencies 2024-04-18 18:25:18 +02:00
Jonas Lochmann
c0ea7962cd
Disable AndroidDeviceOwnerApi for store builds 2024-04-18 18:25:15 +02:00
Jonas Lochmann
141c967b74
Self grant runtime permissions 2024-04-18 18:25:12 +02:00
Jonas Lochmann
82e8f65f0d
Add notification when extra time starts 2024-04-15 02:00:00 +02:00
Jonas Lochmann
de9be70480
Update build tools 2024-04-16 16:27:25 +02:00
Jonas Lochmann
984a0ed147
Update dependencies 2024-04-16 16:27:22 +02:00
Jonas Lochmann
73992bb6af
Replace threetenabp by Java API 2024-04-16 16:27:19 +02:00
Jonas Lochmann
a9f7bcb0b8
Increase minSdkVersion to 26 2024-04-16 16:27:16 +02:00
Jonas Lochmann
8a2f7ceb79
Release 6.20.1 2024-04-15 02:00:00 +02:00
Jonas Lochmann
ee0644e85c
Fix off by one for rule expire months 2024-04-15 02:00:00 +02:00
81 changed files with 2967 additions and 611 deletions

1
.gitignore vendored
View file

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

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2025 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -21,17 +21,18 @@ plugins {
id "androidx.navigation.safeargs.kotlin" id "androidx.navigation.safeargs.kotlin"
id 'kotlin-kapt' id 'kotlin-kapt'
id 'com.squareup.wire' id 'com.squareup.wire'
id("org.jetbrains.kotlin.plugin.compose") version "2.1.10"
} }
android { android {
namespace 'io.timelimit.android' namespace 'io.timelimit.android'
compileSdkVersion 34 compileSdk 36
defaultConfig { defaultConfig {
applicationId "io.timelimit.android" applicationId "io.timelimit.android"
minSdkVersion 21 minSdkVersion 26
targetSdkVersion 34 targetSdkVersion 36
versionCode 214 versionCode 224
versionName "6.20.0" versionName "7.3.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
kapt { kapt {
arguments { arguments {
@ -50,6 +51,7 @@ android {
buildFeatures { buildFeatures {
compose true compose true
viewBinding true viewBinding true
buildConfig true
} }
flavorDimensions 'api', 'channel', 'server' flavorDimensions 'api', 'channel', 'server'
@ -145,8 +147,8 @@ android {
} }
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_17 sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_21
} }
kotlinOptions { kotlinOptions {
@ -155,7 +157,7 @@ android {
} }
composeOptions { composeOptions {
kotlinCompilerExtensionVersion = "1.5.5" kotlinCompilerExtensionVersion = "1.5.7"
} }
} }
@ -165,23 +167,24 @@ wire {
dependencies { dependencies {
def nav_version = "2.5.3" def nav_version = "2.5.3"
def room_version = "2.6.1" def room_version = "2.7.2"
def work_version = '2.9.0' def work_version = '2.10.2'
def paging_version = "3.2.1" def paging_version = "3.3.6"
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.20" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.1.10"
implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.appcompat:appcompat:1.7.1'
implementation 'androidx.core:core:1.12.0' implementation 'androidx.core:core:1.16.0'
implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.gridlayout:gridlayout:1.0.0' implementation 'androidx.gridlayout:gridlayout:1.1.0'
implementation "com.google.android.material:material:1.11.0" implementation "com.google.android.material:material:1.12.0"
implementation 'androidx.compose.material:material:1.6.3' implementation 'androidx.compose.material:material:1.8.3'
implementation 'androidx.activity:activity-compose:1.8.2' implementation 'androidx.activity:activity-compose:1.10.1'
implementation "com.google.accompanist:accompanist-flowlayout:0.30.0" implementation "com.google.accompanist:accompanist-flowlayout:0.30.0"
implementation 'androidx.compose.material:material-icons-extended:1.6.3' implementation 'androidx.compose.material:material-icons-extended:1.7.8'
debugImplementation "androidx.compose.ui:ui-tooling:1.6.3" debugImplementation "androidx.compose.ui:ui-tooling:1.8.3"
implementation 'androidx.fragment:fragment-ktx:1.6.2' 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-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui:$nav_version" implementation "androidx.navigation:navigation-ui:$nav_version"
@ -198,14 +201,12 @@ dependencies {
implementation "androidx.work:work-runtime-ktx:$work_version" implementation "androidx.work:work-runtime-ktx:$work_version"
// androidTestImplementation "android.arch.work:work-testing:$work_version" // androidTestImplementation "android.arch.work:work-testing:$work_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0'
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test:runner:1.5.2' androidTestImplementation 'androidx.test:runner:1.6.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
implementation 'com.jakewharton.threetenabp:threetenabp:1.1.0'
implementation 'org.mindrot:jbcrypt:0.4' implementation 'org.mindrot:jbcrypt:0.4'
@ -213,11 +214,11 @@ dependencies {
implementation 'com.google.android.flexbox:flexbox:3.0.0' implementation 'com.google.android.flexbox:flexbox:3.0.0'
implementation 'com.squareup.okhttp3:okhttp:4.10.0' implementation 'com.squareup.okhttp3:okhttp:4.12.0'
implementation 'com.squareup.okhttp3:okhttp-tls:4.9.3' implementation 'com.squareup.okhttp3:okhttp-tls:4.9.3'
implementation 'com.squareup.okhttp3:logging-interceptor:4.9.3' implementation 'com.squareup.okhttp3:logging-interceptor:4.9.3'
googleApiImplementation "com.android.billingclient:billing-ktx:6.2.0" googleApiImplementation "com.android.billingclient:billing-ktx:8.0.0"
implementation('io.socket:socket.io-client:2.0.0') { implementation('io.socket:socket.io-client:2.0.0') {
exclude group: 'org.json', module: 'json' exclude group: 'org.json', module: 'json'
@ -229,5 +230,5 @@ dependencies {
implementation 'com.google.zxing:core:3.3.3' 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"
} }

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- <!--
TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License. the Free Software Foundation version 3 of the License.
@ -55,7 +55,9 @@
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name" android:label="@string/app_name"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme"> android:theme="@style/AppTheme"
android:enableOnBackInvokedCallback="true"
tools:targetApi="tiramisu">
<!-- UI --> <!-- UI -->
@ -124,6 +126,14 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".ui.diagnose.exception.DiagnoseExceptionActivity"
android:theme="@style/AppTheme.Translucent"
android:exported="false"
android:excludeFromRecents="true"
android:taskAffinity=":exception"
android:launchMode="singleTop" />
<!-- system integration --> <!-- system integration -->
<receiver android:name=".integration.platform.android.receiver.BootReceiver" android:exported="false"> <receiver android:name=".integration.platform.android.receiver.BootReceiver" android:exported="false">

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -17,16 +17,9 @@ package io.timelimit.android
import android.app.Application import android.app.Application
import android.view.View import android.view.View
import com.jakewharton.threetenabp.AndroidThreeTen
class Application : Application() { class Application : Application() {
// two legacy screens use small id numbers as they want; by running generateViewId() often enough, // two legacy screens use small id numbers as they want; by running generateViewId() often enough,
// all ids that are harcoded this way are not returned from generateViewId // all ids that are harcoded this way are not returned from generateViewId
init { (0..1024).forEach { _ -> View.generateViewId() } } init { (0..1024).forEach { _ -> View.generateViewId() } }
override fun onCreate() {
super.onCreate()
AndroidThreeTen.init(this)
}
} }

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -18,6 +18,7 @@ package io.timelimit.android.coroutines
import io.timelimit.android.async.Threads import io.timelimit.android.async.Threads
import kotlinx.coroutines.* import kotlinx.coroutines.*
@OptIn(DelicateCoroutinesApi::class)
fun <T> runAsync(block: suspend CoroutineScope.() -> T) { fun <T> runAsync(block: suspend CoroutineScope.() -> T) {
GlobalScope.launch (Dispatchers.Main) { GlobalScope.launch (Dispatchers.Main) {
block() block()

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -50,7 +50,7 @@ interface Database {
fun widgetCategory(): WidgetCategoryDao fun widgetCategory(): WidgetCategoryDao
fun widgetConfig(): WidgetConfigDao fun widgetConfig(): WidgetConfigDao
fun <T> runInTransaction(block: Callable<T>): T fun <T> runInTransaction(body: Callable<T>): T
fun <T> runInUnobservedTransaction(block: () -> T): T fun <T> runInUnobservedTransaction(block: () -> T): T
fun registerWeakObserver(tables: Array<Table>, observer: WeakReference<Observer>) fun registerWeakObserver(tables: Array<Table>, observer: WeakReference<Observer>)
fun registerTransactionCommitListener(listener: () -> Unit) fun registerTransactionCommitListener(listener: () -> Unit)

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -22,45 +22,45 @@ import io.timelimit.android.extensions.MinuteOfDay
object DatabaseMigrations { object DatabaseMigrations {
private val MIGRATE_TO_V2 = object: Migration(1, 2) { private val MIGRATE_TO_V2 = object: Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE device ADD COLUMN did_report_uninstall INTEGER NOT NULL DEFAULT 0") db.execSQL("ALTER TABLE device ADD COLUMN did_report_uninstall INTEGER NOT NULL DEFAULT 0")
} }
} }
private val MIGRATE_TO_V3 = object: Migration(2, 3) { private val MIGRATE_TO_V3 = object: Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE device ADD COLUMN is_user_kept_signed_in INTEGER NOT NULL DEFAULT 0") db.execSQL("ALTER TABLE device ADD COLUMN is_user_kept_signed_in INTEGER NOT NULL DEFAULT 0")
} }
} }
private val MIGRATE_TO_V4 = object: Migration(3, 4) { private val MIGRATE_TO_V4 = object: Migration(3, 4) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `user` ADD COLUMN `category_for_not_assigned_apps` TEXT NOT NULL DEFAULT \"\"") db.execSQL("ALTER TABLE `user` ADD COLUMN `category_for_not_assigned_apps` TEXT NOT NULL DEFAULT \"\"")
} }
} }
private val MIGRATE_TO_V5 = object: Migration(4, 5) { private val MIGRATE_TO_V5 = object: Migration(4, 5) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `category` ADD COLUMN `parent_category_id` TEXT NOT NULL DEFAULT \"\"") db.execSQL("ALTER TABLE `category` ADD COLUMN `parent_category_id` TEXT NOT NULL DEFAULT \"\"")
} }
} }
private val MIGRATE_TO_V6 = object: Migration(5, 6) { private val MIGRATE_TO_V6 = object: Migration(5, 6) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `device` ADD COLUMN `show_device_connected` INTEGER NOT NULL DEFAULT 0") db.execSQL("ALTER TABLE `device` ADD COLUMN `show_device_connected` INTEGER NOT NULL DEFAULT 0")
} }
} }
private val MIGRATE_TO_V7 = object: Migration(6, 7) { private val MIGRATE_TO_V7 = object: Migration(6, 7) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `device` ADD COLUMN `default_user` TEXT NOT NULL DEFAULT \"\"") db.execSQL("ALTER TABLE `device` ADD COLUMN `default_user` TEXT NOT NULL DEFAULT \"\"")
database.execSQL("ALTER TABLE `device` ADD COLUMN `default_user_timeout` INTEGER NOT NULL DEFAULT 0") db.execSQL("ALTER TABLE `device` ADD COLUMN `default_user_timeout` INTEGER NOT NULL DEFAULT 0")
database.execSQL("ALTER TABLE `user` ADD COLUMN `relax_primary_device` INTEGER NOT NULL DEFAULT 0") db.execSQL("ALTER TABLE `user` ADD COLUMN `relax_primary_device` INTEGER NOT NULL DEFAULT 0")
} }
} }
private val MIGRATE_TO_V8 = object: Migration(7, 8) { private val MIGRATE_TO_V8 = object: Migration(7, 8) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
// this is empty // this is empty
// //
// a new possible enum value was added, the version upgrade enables the downgrade mechanism // a new possible enum value was added, the version upgrade enables the downgrade mechanism
@ -68,14 +68,14 @@ object DatabaseMigrations {
} }
private val MIGRATE_TO_V9 = object: Migration(8, 9) { private val MIGRATE_TO_V9 = object: Migration(8, 9) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `device` ADD COLUMN `did_reboot` INTEGER NOT NULL DEFAULT 0") db.execSQL("ALTER TABLE `device` ADD COLUMN `did_reboot` INTEGER NOT NULL DEFAULT 0")
database.execSQL("ALTER TABLE `device` ADD COLUMN `consider_reboot_manipulation` INTEGER NOT NULL DEFAULT 0") db.execSQL("ALTER TABLE `device` ADD COLUMN `consider_reboot_manipulation` INTEGER NOT NULL DEFAULT 0")
} }
} }
private val MIGRATE_TO_V10 = object: Migration(9, 10) { private val MIGRATE_TO_V10 = object: Migration(9, 10) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
// this is empty // this is empty
// //
// a new possible enum value was added, the version upgrade enables the downgrade mechanism // a new possible enum value was added, the version upgrade enables the downgrade mechanism
@ -83,40 +83,40 @@ object DatabaseMigrations {
} }
private val MIGRATE_TO_V11 = object: Migration(10, 11) { private val MIGRATE_TO_V11 = object: Migration(10, 11) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `user` ADD COLUMN `mail_notification_flags` INTEGER NOT NULL DEFAULT 0") db.execSQL("ALTER TABLE `user` ADD COLUMN `mail_notification_flags` INTEGER NOT NULL DEFAULT 0")
} }
} }
private val MIGRATE_TO_V12 = object: Migration(11, 12) { private val MIGRATE_TO_V12 = object: Migration(11, 12) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `category` ADD COLUMN `block_all_notifications` INTEGER NOT NULL DEFAULT 0") db.execSQL("ALTER TABLE `category` ADD COLUMN `block_all_notifications` INTEGER NOT NULL DEFAULT 0")
} }
} }
private val MIGRATE_TO_V13 = object: Migration(12, 13) { private val MIGRATE_TO_V13 = object: Migration(12, 13) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `device` ADD COLUMN `current_overlay_permission` TEXT NOT NULL DEFAULT \"not granted\"") db.execSQL("ALTER TABLE `device` ADD COLUMN `current_overlay_permission` TEXT NOT NULL DEFAULT \"not granted\"")
database.execSQL("ALTER TABLE `device` ADD COLUMN `highest_overlay_permission` TEXT NOT NULL DEFAULT \"not granted\"") db.execSQL("ALTER TABLE `device` ADD COLUMN `highest_overlay_permission` TEXT NOT NULL DEFAULT \"not granted\"")
} }
} }
private val MIGRATE_TO_V14 = object: Migration(13, 14) { private val MIGRATE_TO_V14 = object: Migration(13, 14) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `device` ADD COLUMN `current_accessibility_service_permission` INTEGER NOT NULL DEFAULT 0") db.execSQL("ALTER TABLE `device` ADD COLUMN `current_accessibility_service_permission` INTEGER NOT NULL DEFAULT 0")
database.execSQL("ALTER TABLE `device` ADD COLUMN `was_accessibility_service_permission` INTEGER NOT NULL DEFAULT 0") db.execSQL("ALTER TABLE `device` ADD COLUMN `was_accessibility_service_permission` INTEGER NOT NULL DEFAULT 0")
} }
} }
private val MIGRATE_TO_V15 = object: Migration(14, 15) { private val MIGRATE_TO_V15 = object: Migration(14, 15) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE `app_activity` (`device_id` TEXT NOT NULL, `app_package_name` TEXT NOT NULL, `activity_class_name` TEXT NOT NULL, `activity_title` TEXT NOT NULL, PRIMARY KEY(`device_id`, `app_package_name`, `activity_class_name`))") db.execSQL("CREATE TABLE `app_activity` (`device_id` TEXT NOT NULL, `app_package_name` TEXT NOT NULL, `activity_class_name` TEXT NOT NULL, `activity_title` TEXT NOT NULL, PRIMARY KEY(`device_id`, `app_package_name`, `activity_class_name`))")
database.execSQL("ALTER TABLE `device` ADD COLUMN `enable_activity_level_blocking` INTEGER NOT NULL DEFAULT 0") db.execSQL("ALTER TABLE `device` ADD COLUMN `enable_activity_level_blocking` INTEGER NOT NULL DEFAULT 0")
} }
} }
private val MIGRATE_TO_V16 = object: Migration(15, 16) { private val MIGRATE_TO_V16 = object: Migration(15, 16) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
// this is empty // this is empty
// //
// a new possible enum value was added, the version upgrade enables the downgrade mechanism // a new possible enum value was added, the version upgrade enables the downgrade mechanism
@ -124,20 +124,20 @@ object DatabaseMigrations {
} }
private val MIGRATE_TO_V17 = object: Migration(16, 17) { private val MIGRATE_TO_V17 = object: Migration(16, 17) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `notification` (`type` INTEGER NOT NULL, `id` TEXT NOT NULL, `first_notify_time` INTEGER NOT NULL, `dismissed` INTEGER NOT NULL, PRIMARY KEY(`type`, `id`))") db.execSQL("CREATE TABLE IF NOT EXISTS `notification` (`type` INTEGER NOT NULL, `id` TEXT NOT NULL, `first_notify_time` INTEGER NOT NULL, `dismissed` INTEGER NOT NULL, PRIMARY KEY(`type`, `id`))")
} }
} }
private val MIGRATE_TO_V18 = object: Migration(17, 18) { private val MIGRATE_TO_V18 = object: Migration(17, 18) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `device` ADD COLUMN `q_or_later` INTEGER NOT NULL DEFAULT 0") db.execSQL("ALTER TABLE `device` ADD COLUMN `q_or_later` INTEGER NOT NULL DEFAULT 0")
database.execSQL("ALTER TABLE `category` ADD COLUMN `time_warnings` INTEGER NOT NULL DEFAULT 0") db.execSQL("ALTER TABLE `category` ADD COLUMN `time_warnings` INTEGER NOT NULL DEFAULT 0")
} }
} }
private val MIGRATE_TO_V19 = object: Migration(18, 19) { private val MIGRATE_TO_V19 = object: Migration(18, 19) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
// this is empty // this is empty
// //
// a new possible enum value was added, the version upgrade enables the downgrade mechanism // a new possible enum value was added, the version upgrade enables the downgrade mechanism
@ -145,44 +145,44 @@ object DatabaseMigrations {
} }
private val MIGRATE_TO_V20 = object: Migration(19, 20) { private val MIGRATE_TO_V20 = object: Migration(19, 20) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `allowed_contact` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `phone` TEXT NOT NULL)") db.execSQL("CREATE TABLE IF NOT EXISTS `allowed_contact` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `phone` TEXT NOT NULL)")
} }
} }
private val MIGRATE_TO_V21 = object: Migration(20, 21) { private val MIGRATE_TO_V21 = object: Migration(20, 21) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `device` ADD COLUMN `had_manipulation_flags` INTEGER NOT NULL DEFAULT 0") db.execSQL("ALTER TABLE `device` ADD COLUMN `had_manipulation_flags` INTEGER NOT NULL DEFAULT 0")
} }
} }
private val MIGRATE_TO_V22 = object: Migration(21, 22) { private val MIGRATE_TO_V22 = object: Migration(21, 22) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `user` ADD COLUMN `blocked_times` TEXT NOT NULL DEFAULT \"\"") db.execSQL("ALTER TABLE `user` ADD COLUMN `blocked_times` TEXT NOT NULL DEFAULT \"\"")
} }
} }
private val MIGRATE_TO_V23 = object: Migration(22, 23) { private val MIGRATE_TO_V23 = object: Migration(22, 23) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `category` ADD COLUMN `min_battery_charging` INTEGER NOT NULL DEFAULT 0") db.execSQL("ALTER TABLE `category` ADD COLUMN `min_battery_charging` INTEGER NOT NULL DEFAULT 0")
database.execSQL("ALTER TABLE `category` ADD COLUMN `min_battery_mobile` INTEGER NOT NULL DEFAULT 0") db.execSQL("ALTER TABLE `category` ADD COLUMN `min_battery_mobile` INTEGER NOT NULL DEFAULT 0")
} }
} }
private val MIGRATE_TO_V24 = object: Migration(23, 24) { private val MIGRATE_TO_V24 = object: Migration(23, 24) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `category` ADD COLUMN `temporarily_blocked_end_time` INTEGER NOT NULL DEFAULT 0") db.execSQL("ALTER TABLE `category` ADD COLUMN `temporarily_blocked_end_time` INTEGER NOT NULL DEFAULT 0")
} }
} }
private val MIGRATE_TO_V25 = object: Migration(24, 25) { private val MIGRATE_TO_V25 = object: Migration(24, 25) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `category` ADD COLUMN `sort` INTEGER NOT NULL DEFAULT 0") db.execSQL("ALTER TABLE `category` ADD COLUMN `sort` INTEGER NOT NULL DEFAULT 0")
} }
} }
private val MIGRATE_TO_V26 = object: Migration(25, 26) { private val MIGRATE_TO_V26 = object: Migration(25, 26) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
// this is empty // this is empty
// //
// a new possible enum value was added, the version upgrade enables the downgrade mechanism // a new possible enum value was added, the version upgrade enables the downgrade mechanism
@ -190,152 +190,152 @@ object DatabaseMigrations {
} }
private val MIGRATE_TO_V27 = object: Migration(26, 27) { private val MIGRATE_TO_V27 = object: Migration(26, 27) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `category` ADD COLUMN `extra_time_day` INTEGER NOT NULL DEFAULT -1") db.execSQL("ALTER TABLE `category` ADD COLUMN `extra_time_day` INTEGER NOT NULL DEFAULT -1")
} }
} }
private val MIGRATE_TO_V28 = object: Migration(27, 28) { private val MIGRATE_TO_V28 = object: Migration(27, 28) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `user_key` (`user_id` TEXT NOT NULL, `key` BLOB NOT NULL, `last_use` INTEGER NOT NULL, PRIMARY KEY(`user_id`), FOREIGN KEY(`user_id`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )") db.execSQL("CREATE TABLE IF NOT EXISTS `user_key` (`user_id` TEXT NOT NULL, `key` BLOB NOT NULL, `last_use` INTEGER NOT NULL, PRIMARY KEY(`user_id`), FOREIGN KEY(`user_id`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )")
database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_user_key_key` ON `user_key` (`key`)") db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_user_key_key` ON `user_key` (`key`)")
} }
} }
private val MIGRATE_TO_V29 = object: Migration(28, 29) { private val MIGRATE_TO_V29 = object: Migration(28, 29) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `time_limit_rule` ADD COLUMN `start_minute_of_day` INTEGER NOT NULL DEFAULT ${TimeLimitRule.MIN_START_MINUTE}") db.execSQL("ALTER TABLE `time_limit_rule` ADD COLUMN `start_minute_of_day` INTEGER NOT NULL DEFAULT ${TimeLimitRule.MIN_START_MINUTE}")
database.execSQL("ALTER TABLE `time_limit_rule` ADD COLUMN `end_minute_of_day` INTEGER NOT NULL DEFAULT ${TimeLimitRule.MAX_END_MINUTE}") db.execSQL("ALTER TABLE `time_limit_rule` ADD COLUMN `end_minute_of_day` INTEGER NOT NULL DEFAULT ${TimeLimitRule.MAX_END_MINUTE}")
database.execSQL("ALTER TABLE `time_limit_rule` ADD COLUMN `session_duration_milliseconds` INTEGER NOT NULL DEFAULT 0") db.execSQL("ALTER TABLE `time_limit_rule` ADD COLUMN `session_duration_milliseconds` INTEGER NOT NULL DEFAULT 0")
database.execSQL("ALTER TABLE `time_limit_rule` ADD COLUMN `session_pause_milliseconds` INTEGER NOT NULL DEFAULT 0") db.execSQL("ALTER TABLE `time_limit_rule` ADD COLUMN `session_pause_milliseconds` INTEGER NOT NULL DEFAULT 0")
database.execSQL("ALTER TABLE `used_time` RENAME TO `used_time_old`") db.execSQL("ALTER TABLE `used_time` RENAME TO `used_time_old`")
database.execSQL("CREATE TABLE IF NOT EXISTS `used_time` (`day_of_epoch` INTEGER NOT NULL, `used_time` INTEGER NOT NULL, `category_id` TEXT NOT NULL, `start_time_of_day` INTEGER NOT NULL, `end_time_of_day` INTEGER NOT NULL, PRIMARY KEY(`category_id`, `day_of_epoch`, `start_time_of_day`, `end_time_of_day`))") db.execSQL("CREATE TABLE IF NOT EXISTS `used_time` (`day_of_epoch` INTEGER NOT NULL, `used_time` INTEGER NOT NULL, `category_id` TEXT NOT NULL, `start_time_of_day` INTEGER NOT NULL, `end_time_of_day` INTEGER NOT NULL, PRIMARY KEY(`category_id`, `day_of_epoch`, `start_time_of_day`, `end_time_of_day`))")
database.execSQL("INSERT INTO `used_time` SELECT `day_of_epoch`, `used_time`, `category_id`, ${MinuteOfDay.MIN} AS `start_time_of_day`, ${MinuteOfDay.MAX} AS `end_time_of_day` FROM `used_time_old`") db.execSQL("INSERT INTO `used_time` SELECT `day_of_epoch`, `used_time`, `category_id`, ${MinuteOfDay.MIN} AS `start_time_of_day`, ${MinuteOfDay.MAX} AS `end_time_of_day` FROM `used_time_old`")
database.execSQL("DROP TABLE `used_time_old`") db.execSQL("DROP TABLE `used_time_old`")
database.execSQL("CREATE TABLE IF NOT EXISTS `session_duration` (`category_id` TEXT NOT NULL, `max_session_duration` INTEGER NOT NULL, `session_pause_duration` INTEGER NOT NULL, `start_minute_of_day` INTEGER NOT NULL, `end_minute_of_day` INTEGER NOT NULL, `last_usage` INTEGER NOT NULL, `last_session_duration` INTEGER NOT NULL, PRIMARY KEY(`category_id`, `max_session_duration`, `session_pause_duration`, `start_minute_of_day`, `end_minute_of_day`), FOREIGN KEY(`category_id`) REFERENCES `category`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )") db.execSQL("CREATE TABLE IF NOT EXISTS `session_duration` (`category_id` TEXT NOT NULL, `max_session_duration` INTEGER NOT NULL, `session_pause_duration` INTEGER NOT NULL, `start_minute_of_day` INTEGER NOT NULL, `end_minute_of_day` INTEGER NOT NULL, `last_usage` INTEGER NOT NULL, `last_session_duration` INTEGER NOT NULL, PRIMARY KEY(`category_id`, `max_session_duration`, `session_pause_duration`, `start_minute_of_day`, `end_minute_of_day`), FOREIGN KEY(`category_id`) REFERENCES `category`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )")
database.execSQL("CREATE INDEX IF NOT EXISTS `session_duration_index_category_id` ON `session_duration` (`category_id`)") db.execSQL("CREATE INDEX IF NOT EXISTS `session_duration_index_category_id` ON `session_duration` (`category_id`)")
} }
} }
private val MIGRATE_TO_V30 = object: Migration(29, 30) { private val MIGRATE_TO_V30 = object: Migration(29, 30) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `user` ADD COLUMN `flags` INTEGER NOT NULL DEFAULT 0") db.execSQL("ALTER TABLE `user` ADD COLUMN `flags` INTEGER NOT NULL DEFAULT 0")
} }
} }
private val MIGRATE_TO_V31 = object: Migration(30, 31) { private val MIGRATE_TO_V31 = object: Migration(30, 31) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `user_limit_login_category` (`user_id` TEXT NOT NULL, `category_id` TEXT NOT NULL, PRIMARY KEY(`user_id`), FOREIGN KEY(`user_id`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`category_id`) REFERENCES `category`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )") db.execSQL("CREATE TABLE IF NOT EXISTS `user_limit_login_category` (`user_id` TEXT NOT NULL, `category_id` TEXT NOT NULL, PRIMARY KEY(`user_id`), FOREIGN KEY(`user_id`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`category_id`) REFERENCES `category`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )")
database.execSQL("CREATE INDEX IF NOT EXISTS `user_limit_login_category_index_category_id` ON `user_limit_login_category` (`category_id`)") db.execSQL("CREATE INDEX IF NOT EXISTS `user_limit_login_category_index_category_id` ON `user_limit_login_category` (`category_id`)")
} }
} }
private val MIGRATE_TO_V32 = object: Migration(31, 32) { private val MIGRATE_TO_V32 = object: Migration(31, 32) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `category_network_id` (`category_id` TEXT NOT NULL, `network_item_id` TEXT NOT NULL, `hashed_network_id` TEXT NOT NULL, PRIMARY KEY(`category_id`, `network_item_id`), FOREIGN KEY(`category_id`) REFERENCES `category`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )") db.execSQL("CREATE TABLE IF NOT EXISTS `category_network_id` (`category_id` TEXT NOT NULL, `network_item_id` TEXT NOT NULL, `hashed_network_id` TEXT NOT NULL, PRIMARY KEY(`category_id`, `network_item_id`), FOREIGN KEY(`category_id`) REFERENCES `category`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )")
} }
} }
private val MIGRATE_TO_V33 = object: Migration(32, 33) { private val MIGRATE_TO_V33 = object: Migration(32, 33) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `category` ADD COLUMN `disable_limits_until` INTEGER NOT NULL DEFAULT 0") db.execSQL("ALTER TABLE `category` ADD COLUMN `disable_limits_until` INTEGER NOT NULL DEFAULT 0")
} }
} }
private val MIGRATE_TO_V34 = object: Migration(33, 34) { private val MIGRATE_TO_V34 = object: Migration(33, 34) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `child_task` (`task_id` TEXT NOT NULL, `category_id` TEXT NOT NULL, `task_title` TEXT NOT NULL, `extra_time_duration` INTEGER NOT NULL, `pending_request` INTEGER NOT NULL, `last_grant_timestamp` INTEGER NOT NULL, PRIMARY KEY(`task_id`), FOREIGN KEY(`category_id`) REFERENCES `category`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )") db.execSQL("CREATE TABLE IF NOT EXISTS `child_task` (`task_id` TEXT NOT NULL, `category_id` TEXT NOT NULL, `task_title` TEXT NOT NULL, `extra_time_duration` INTEGER NOT NULL, `pending_request` INTEGER NOT NULL, `last_grant_timestamp` INTEGER NOT NULL, PRIMARY KEY(`task_id`), FOREIGN KEY(`category_id`) REFERENCES `category`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )")
database.execSQL("ALTER TABLE `category` ADD COLUMN `tasks_version` TEXT NOT NULL DEFAULT ''") db.execSQL("ALTER TABLE `category` ADD COLUMN `tasks_version` TEXT NOT NULL DEFAULT ''")
} }
} }
private val MIGRATE_TO_V35 = object: Migration(34, 35) { private val MIGRATE_TO_V35 = object: Migration(34, 35) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `time_limit_rule` ADD COLUMN `per_day` INTEGER NOT NULL DEFAULT 0") db.execSQL("ALTER TABLE `time_limit_rule` ADD COLUMN `per_day` INTEGER NOT NULL DEFAULT 0")
} }
} }
private val MIGRATE_TO_V36 = object: Migration(35, 36) { private val MIGRATE_TO_V36 = object: Migration(35, 36) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `user_limit_login_category` ADD COLUMN pre_block_duration INTEGER NOT NULL DEFAULT 0") db.execSQL("ALTER TABLE `user_limit_login_category` ADD COLUMN pre_block_duration INTEGER NOT NULL DEFAULT 0")
} }
} }
private val MIGRATE_TO_V37 = object: Migration(36, 37) { private val MIGRATE_TO_V37 = object: Migration(36, 37) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `category` ADD COLUMN `flags` INTEGER NOT NULL DEFAULT 0") db.execSQL("ALTER TABLE `category` ADD COLUMN `flags` INTEGER NOT NULL DEFAULT 0")
} }
} }
private val MIGRATE_TO_V38 = object: Migration(37, 38) { private val MIGRATE_TO_V38 = object: Migration(37, 38) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE category ADD COLUMN block_notification_delay INTEGER NOT NULL DEFAULT 0") db.execSQL("ALTER TABLE category ADD COLUMN block_notification_delay INTEGER NOT NULL DEFAULT 0")
} }
} }
private val MIGRATE_TO_V39 = object: Migration(38, 39) { private val MIGRATE_TO_V39 = object: Migration(38, 39) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
// nothing to do, there was just a new config item type added // nothing to do, there was just a new config item type added
} }
} }
private val MIGRATE_TO_V40 = object: Migration(39, 40) { private val MIGRATE_TO_V40 = object: Migration(39, 40) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `category_time_warning` (`category_id` TEXT NOT NULL, `minutes` INTEGER NOT NULL, PRIMARY KEY(`category_id`, `minutes`), FOREIGN KEY(`category_id`) REFERENCES `category`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )") db.execSQL("CREATE TABLE IF NOT EXISTS `category_time_warning` (`category_id` TEXT NOT NULL, `minutes` INTEGER NOT NULL, PRIMARY KEY(`category_id`, `minutes`), FOREIGN KEY(`category_id`) REFERENCES `category`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )")
} }
} }
private val MIGRATE_TO_V41 = object: Migration(40, 41) { private val MIGRATE_TO_V41 = object: Migration(40, 41) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
// nothing to do, there was just a new config item type added // nothing to do, there was just a new config item type added
} }
} }
private val MIGRATE_TO_V42 = object: Migration(41, 42) { private val MIGRATE_TO_V42 = object: Migration(41, 42) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE device ADD COLUMN manipulation_flags INTEGER NOT NULL DEFAULT 0") db.execSQL("ALTER TABLE device ADD COLUMN manipulation_flags INTEGER NOT NULL DEFAULT 0")
} }
} }
private val MIGRATE_TP_V43 = object: Migration(42, 43) { private val MIGRATE_TP_V43 = object: Migration(42, 43) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `crypt_container_metadata` (`crypt_container_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `device_id` TEXT, `category_id` TEXT, `type` INTEGER NOT NULL, `server_version` TEXT NOT NULL, `current_generation` INTEGER NOT NULL, `current_generation_first_timestamp` INTEGER NOT NULL, `next_counter` INTEGER NOT NULL, `current_generation_key` BLOB, `status` INTEGER NOT NULL, FOREIGN KEY(`device_id`) REFERENCES `device`(`id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`category_id`) REFERENCES `category`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )") db.execSQL("CREATE TABLE IF NOT EXISTS `crypt_container_metadata` (`crypt_container_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `device_id` TEXT, `category_id` TEXT, `type` INTEGER NOT NULL, `server_version` TEXT NOT NULL, `current_generation` INTEGER NOT NULL, `current_generation_first_timestamp` INTEGER NOT NULL, `next_counter` INTEGER NOT NULL, `current_generation_key` BLOB, `status` INTEGER NOT NULL, FOREIGN KEY(`device_id`) REFERENCES `device`(`id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`category_id`) REFERENCES `category`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )")
database.execSQL("CREATE INDEX IF NOT EXISTS `index_crypt_container_metadata_device_id` ON `crypt_container_metadata` (`device_id`)") db.execSQL("CREATE INDEX IF NOT EXISTS `index_crypt_container_metadata_device_id` ON `crypt_container_metadata` (`device_id`)")
database.execSQL("CREATE INDEX IF NOT EXISTS `index_crypt_container_metadata_category_id` ON `crypt_container_metadata` (`category_id`)") db.execSQL("CREATE INDEX IF NOT EXISTS `index_crypt_container_metadata_category_id` ON `crypt_container_metadata` (`category_id`)")
database.execSQL("CREATE TABLE IF NOT EXISTS `crypt_container_data` (`crypt_container_id` INTEGER NOT NULL, `encrypted_data` BLOB NOT NULL, PRIMARY KEY(`crypt_container_id`), FOREIGN KEY(`crypt_container_id`) REFERENCES `crypt_container_metadata`(`crypt_container_id`) ON UPDATE CASCADE ON DELETE CASCADE )") db.execSQL("CREATE TABLE IF NOT EXISTS `crypt_container_data` (`crypt_container_id` INTEGER NOT NULL, `encrypted_data` BLOB NOT NULL, PRIMARY KEY(`crypt_container_id`), FOREIGN KEY(`crypt_container_id`) REFERENCES `crypt_container_metadata`(`crypt_container_id`) ON UPDATE CASCADE ON DELETE CASCADE )")
database.execSQL("CREATE TABLE IF NOT EXISTS `crypt_container_pending_key_request` (`crypt_container_id` INTEGER NOT NULL, `request_time_crypt_container_generation` INTEGER NOT NULL, `request_sequence_id` INTEGER NOT NULL, `request_key` BLOB NOT NULL, PRIMARY KEY(`crypt_container_id`), FOREIGN KEY(`crypt_container_id`) REFERENCES `crypt_container_metadata`(`crypt_container_id`) ON UPDATE CASCADE ON DELETE CASCADE )") db.execSQL("CREATE TABLE IF NOT EXISTS `crypt_container_pending_key_request` (`crypt_container_id` INTEGER NOT NULL, `request_time_crypt_container_generation` INTEGER NOT NULL, `request_sequence_id` INTEGER NOT NULL, `request_key` BLOB NOT NULL, PRIMARY KEY(`crypt_container_id`), FOREIGN KEY(`crypt_container_id`) REFERENCES `crypt_container_metadata`(`crypt_container_id`) ON UPDATE CASCADE ON DELETE CASCADE )")
database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_crypt_container_pending_key_request_request_sequence_id` ON `crypt_container_pending_key_request` (`request_sequence_id`)") db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_crypt_container_pending_key_request_request_sequence_id` ON `crypt_container_pending_key_request` (`request_sequence_id`)")
database.execSQL("CREATE TABLE IF NOT EXISTS `crypt_container_key_result` (`request_sequence_id` INTEGER NOT NULL, `device_id` TEXT NOT NULL, `status` INTEGER NOT NULL, PRIMARY KEY(`request_sequence_id`, `device_id`), FOREIGN KEY(`request_sequence_id`) REFERENCES `crypt_container_pending_key_request`(`request_sequence_id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`device_id`) REFERENCES `device`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )") db.execSQL("CREATE TABLE IF NOT EXISTS `crypt_container_key_result` (`request_sequence_id` INTEGER NOT NULL, `device_id` TEXT NOT NULL, `status` INTEGER NOT NULL, PRIMARY KEY(`request_sequence_id`, `device_id`), FOREIGN KEY(`request_sequence_id`) REFERENCES `crypt_container_pending_key_request`(`request_sequence_id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`device_id`) REFERENCES `device`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )")
database.execSQL("CREATE INDEX IF NOT EXISTS `index_crypt_container_key_result_request_sequence_id` ON `crypt_container_key_result` (`request_sequence_id`)") db.execSQL("CREATE INDEX IF NOT EXISTS `index_crypt_container_key_result_request_sequence_id` ON `crypt_container_key_result` (`request_sequence_id`)")
database.execSQL("CREATE INDEX IF NOT EXISTS `index_crypt_container_key_result_device_id` ON `crypt_container_key_result` (`device_id`)") db.execSQL("CREATE INDEX IF NOT EXISTS `index_crypt_container_key_result_device_id` ON `crypt_container_key_result` (`device_id`)")
database.execSQL("CREATE TABLE IF NOT EXISTS `device_public_key` (`device_id` TEXT NOT NULL, `public_key` BLOB NOT NULL, `next_sequence_number` INTEGER NOT NULL, PRIMARY KEY(`device_id`), FOREIGN KEY(`device_id`) REFERENCES `device`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )") db.execSQL("CREATE TABLE IF NOT EXISTS `device_public_key` (`device_id` TEXT NOT NULL, `public_key` BLOB NOT NULL, `next_sequence_number` INTEGER NOT NULL, PRIMARY KEY(`device_id`), FOREIGN KEY(`device_id`) REFERENCES `device`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )")
} }
} }
private val MIGRATE_TO_V44 = object: Migration(43, 44) { private val MIGRATE_TO_V44 = object: Migration(43, 44) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `user_u2f_key` (`key_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `user_id` TEXT NOT NULL, `added_at` INTEGER NOT NULL, `key_handle` BLOB NOT NULL, `public_key` BLOB NOT NULL, `next_counter` INTEGER NOT NULL, FOREIGN KEY(`user_id`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )") db.execSQL("CREATE TABLE IF NOT EXISTS `user_u2f_key` (`key_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `user_id` TEXT NOT NULL, `added_at` INTEGER NOT NULL, `key_handle` BLOB NOT NULL, `public_key` BLOB NOT NULL, `next_counter` INTEGER NOT NULL, FOREIGN KEY(`user_id`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )")
database.execSQL("CREATE INDEX IF NOT EXISTS `index_user_u2f_key_user_id` ON `user_u2f_key` (`user_id`)") db.execSQL("CREATE INDEX IF NOT EXISTS `index_user_u2f_key_user_id` ON `user_u2f_key` (`user_id`)")
database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_user_u2f_key_key_handle_public_key` ON `user_u2f_key` (`key_handle`, `public_key`)") db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_user_u2f_key_key_handle_public_key` ON `user_u2f_key` (`key_handle`, `public_key`)")
} }
} }
val MIGRATE_TO_V45 = object: Migration(44, 45) { val MIGRATE_TO_V45 = object: Migration(44, 45) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `widget_category` (`widget_id` INTEGER NOT NULL, `category_id` TEXT NOT NULL, PRIMARY KEY(`widget_id`, `category_id`), FOREIGN KEY(`category_id`) REFERENCES `category`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )") db.execSQL("CREATE TABLE IF NOT EXISTS `widget_category` (`widget_id` INTEGER NOT NULL, `category_id` TEXT NOT NULL, PRIMARY KEY(`widget_id`, `category_id`), FOREIGN KEY(`category_id`) REFERENCES `category`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )")
database.execSQL("CREATE INDEX IF NOT EXISTS `index_widget_category_category_id` ON `widget_category` (`category_id`)") db.execSQL("CREATE INDEX IF NOT EXISTS `index_widget_category_category_id` ON `widget_category` (`category_id`)")
} }
} }
val MIGRATE_TO_V46 = object: Migration(45, 46) { val MIGRATE_TO_V46 = object: Migration(45, 46) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `widget_config` (`widget_id` INTEGER NOT NULL, `translucent` INTEGER NOT NULL, PRIMARY KEY(`widget_id`))") db.execSQL("CREATE TABLE IF NOT EXISTS `widget_config` (`widget_id` INTEGER NOT NULL, `translucent` INTEGER NOT NULL, PRIMARY KEY(`widget_id`))")
} }
} }

View file

@ -61,9 +61,10 @@ import java.util.concurrent.TimeUnit
UserU2FKey::class, UserU2FKey::class,
WidgetCategory::class, WidgetCategory::class,
WidgetConfig::class WidgetConfig::class
], version = 48, autoMigrations = [ ], version = 49, autoMigrations = [
AutoMigration(from = 46, to = 47), AutoMigration(from = 46, to = 47),
AutoMigration(from = 47, to = 48) AutoMigration(from = 47, to = 48),
AutoMigration(from = 48, to = 49)
]) ])
abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database { abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database {
companion object { companion object {
@ -139,6 +140,10 @@ abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database
} }
} }
@Deprecated(
"endTransaction() is deprecated",
replaceWith = ReplaceWith("runInTransaction(Runnable)")
)
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
override fun endTransaction() { override fun endTransaction() {
openHelper.writableDatabase.endTransaction() openHelper.writableDatabase.endTransaction()

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2025 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -19,6 +19,7 @@ import androidx.lifecycle.LiveData
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.Query import androidx.room.Query
import androidx.room.RoomWarnings
import io.timelimit.android.data.model.UsedTimeItem import io.timelimit.android.data.model.UsedTimeItem
import io.timelimit.android.data.model.UsedTimeListItem import io.timelimit.android.data.model.UsedTimeListItem
import io.timelimit.android.livedata.ignoreUnchanged import io.timelimit.android.livedata.ignoreUnchanged
@ -67,10 +68,14 @@ abstract class UsedTimeDao {
abstract fun getAllUsedTimeItemsSync(): List<UsedTimeItem> abstract fun getAllUsedTimeItemsSync(): List<UsedTimeItem>
// breaking it into multiple lines causes issues during compilation ... // 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)
@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") @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>> abstract fun getUsedTimeListItemsByCategoryId(categoryId: String): Flow<List<UsedTimeListItem>>
// breaking it into multiple lines causes issues during compilation ... // 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)
@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") @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>> abstract fun getUsedTimeListItemsByUserId(userId: String): Flow<List<UsedTimeListItem>>
} }

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -41,7 +41,7 @@ data class ChildTask(
@PrimaryKey @PrimaryKey
@ColumnInfo(name = "task_id") @ColumnInfo(name = "task_id")
val taskId: String, val taskId: String,
@ColumnInfo(name = "category_id") @ColumnInfo(name = "category_id", index = true)
val categoryId: String, val categoryId: String,
@ColumnInfo(name = "task_title") @ColumnInfo(name = "task_title")
val taskTitle: String, val taskTitle: String,

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2025 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -295,4 +295,7 @@ object ExperimentalFlags {
object ConsentFlags { object ConsentFlags {
const val APP_LIST_SYNC = 1L 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 - 2022 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2025 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -73,4 +73,5 @@ data class DeviceRelatedData (
} }
fun isExperimentalFlagSetSync(flags: Long) = (experimentalFlags and flags) == flags 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 - 2020 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -15,8 +15,8 @@
*/ */
package io.timelimit.android.date package io.timelimit.android.date
import org.threeten.bp.DayOfWeek import java.time.DayOfWeek
import org.threeten.bp.LocalDate import java.time.LocalDate
import java.util.* import java.util.*
data class DateInTimezone(val dayOfWeek: Int, val dayOfEpoch: Int, val localDate: LocalDate) { data class DateInTimezone(val dayOfWeek: Int, val dayOfEpoch: Int, val localDate: LocalDate) {

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -15,7 +15,7 @@
*/ */
package io.timelimit.android.extensions package io.timelimit.android.extensions
import org.threeten.bp.LocalDateTime import java.time.LocalDateTime
import org.threeten.bp.ZoneId import java.time.ZoneId
fun LocalDateTime.toInstant(zone: ZoneId) = toInstant(zone.rules.getOffset(this)) fun LocalDateTime.toInstant(zone: ZoneId) = toInstant(zone.rules.getOffset(this))

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -25,4 +25,7 @@ interface DeviceOwnerApi {
fun setOrganizationName(name: String) fun setOrganizationName(name: String)
fun transferOwnership(packageName: String, dryRun: Boolean = false) fun transferOwnership(packageName: String, dryRun: Boolean = false)
// returns true on success; never throws
fun grantLocationAccess(): Boolean
} }

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -56,6 +56,7 @@ abstract class PlatformIntegration(
abstract fun setShowNotificationToRevokeTemporarilyAllowedApps(show: Boolean) abstract fun setShowNotificationToRevokeTemporarilyAllowedApps(show: Boolean)
abstract fun showRemoteResetNotification() abstract fun showRemoteResetNotification()
abstract fun showTimeWarningNotification(title: String, text: String) abstract fun showTimeWarningNotification(title: String, text: String)
abstract fun showExtraTimeStartedNotification(categoryId: String, categoryTitle: String)
// returns package names for which it was set // returns package names for which it was set
abstract fun setSuspendedApps(packageNames: List<String>, suspend: Boolean): List<String> abstract fun setSuspendedApps(packageNames: List<String>, suspend: Boolean): List<String>
abstract fun stopSuspendingForAllApps() abstract fun stopSuspendingForAllApps()
@ -218,7 +219,8 @@ data class AppStatusMessage(
val title: String, val title: String,
val text: String, val text: String,
val subtext: String? = null, val subtext: String? = null,
val showSwitchToDefaultUserOption: Boolean = false val showSwitchToDefaultUserOption: Boolean = false,
val showErrorMessage: Boolean = false
): Parcelable ): Parcelable
data class BatteryStatus( data class BatteryStatus(

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -15,6 +15,7 @@
*/ */
package io.timelimit.android.integration.platform.android package io.timelimit.android.integration.platform.android
import android.Manifest
import android.app.admin.DeviceAdminReceiver import android.app.admin.DeviceAdminReceiver
import android.app.admin.DevicePolicyManager import android.app.admin.DevicePolicyManager
import android.content.ComponentName import android.content.ComponentName
@ -70,6 +71,7 @@ class AndroidDeviceOwnerApi(
override val delegations: List<DeviceOwnerApi.DelegationScope> = delegationList.map { it.second } override val delegations: List<DeviceOwnerApi.DelegationScope> = delegationList.map { it.second }
override fun setDelegations(packageName: String, scopes: List<DeviceOwnerApi.DelegationScope>) { override fun setDelegations(packageName: String, scopes: List<DeviceOwnerApi.DelegationScope>) {
if (BuildConfig.storeCompilant) throw IllegalStateException()
if (VERSION.SDK_INT <= VERSION_CODES.O) throw IllegalStateException() if (VERSION.SDK_INT <= VERSION_CODES.O) throw IllegalStateException()
val resolvedScopes = scopes.map { scope -> val resolvedScopes = scopes.map { scope ->
@ -88,6 +90,7 @@ class AndroidDeviceOwnerApi(
} }
override fun getDelegations(): Map<String, List<DeviceOwnerApi.DelegationScope>> { override fun getDelegations(): Map<String, List<DeviceOwnerApi.DelegationScope>> {
if (BuildConfig.storeCompilant) return emptyMap()
if (VERSION.SDK_INT <= VERSION_CODES.O) throw IllegalStateException() if (VERSION.SDK_INT <= VERSION_CODES.O) throw IllegalStateException()
return delegationList.map { (scope, delegation) -> return delegationList.map { (scope, delegation) ->
@ -108,6 +111,7 @@ class AndroidDeviceOwnerApi(
} }
override fun transferOwnership(packageName: String, dryRun: Boolean) { override fun transferOwnership(packageName: String, dryRun: Boolean) {
if (BuildConfig.storeCompilant) throw IllegalStateException()
if (VERSION.SDK_INT < VERSION_CODES.P) throw IllegalStateException() if (VERSION.SDK_INT < VERSION_CODES.P) throw IllegalStateException()
if (!devicePolicyManager.isDeviceOwnerApp(componentName.packageName)) throw SecurityException() if (!devicePolicyManager.isDeviceOwnerApp(componentName.packageName)) throw SecurityException()
@ -124,4 +128,25 @@ class AndroidDeviceOwnerApi(
devicePolicyManager.setDelegatedScopes(componentName, packageName, emptyList()) devicePolicyManager.setDelegatedScopes(componentName, packageName, emptyList())
devicePolicyManager.transferOwnership(componentName, targetComponentName, null) devicePolicyManager.transferOwnership(componentName, targetComponentName, null)
} }
override fun grantLocationAccess(): Boolean {
if (BuildConfig.storeCompilant) return false
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
}
}
} }

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2025 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -27,6 +27,10 @@ import io.timelimit.android.integration.platform.PlatformFeature
object AndroidFeatures { object AndroidFeatures {
private const val FEATURE_ADB = "adb" private const val FEATURE_ADB = "adb"
private const val FEATURE_CONFIG_PRIVATE_DNS = "dns" 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 applyBlockedFeatures(features: Set<String>, policyManager: DevicePolicyManager, admin: ComponentName): Boolean {
fun apply(feature: String, restriction: String) { fun apply(feature: String, restriction: String) {
@ -40,6 +44,18 @@ object AndroidFeatures {
apply(FEATURE_CONFIG_PRIVATE_DNS, UserManager.DISALLOW_CONFIG_PRIVATE_DNS) 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 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 return result
} }
} }

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -15,6 +15,7 @@
*/ */
package io.timelimit.android.integration.platform.android package io.timelimit.android.integration.platform.android
import android.Manifest
import android.annotation.TargetApi import android.annotation.TargetApi
import android.app.ActivityManager import android.app.ActivityManager
import android.app.Application import android.app.Application
@ -40,6 +41,7 @@ import android.widget.Toast
import androidx.collection.LruCache import androidx.collection.LruCache
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import io.timelimit.android.BuildConfig import io.timelimit.android.BuildConfig
@ -441,6 +443,26 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio
) )
} }
override fun showExtraTimeStartedNotification(categoryId: String, categoryTitle: String) {
NotificationChannels.createNotificationChannels(notificationManager, context)
notificationManager.notify(
categoryId,
NotificationIds.EXTRA_TIME_STARTED,
NotificationCompat.Builder(context, NotificationChannels.EXTRA_TIME_STARTED)
.setSmallIcon(R.drawable.ic_stat_timelapse)
.setContentTitle(context.getString(R.string.notification_extra_time_started))
.setContentText(categoryTitle)
.setWhen(System.currentTimeMillis())
.setShowWhen(true)
.setLocalOnly(true)
.setAutoCancel(false)
.setOngoing(false)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.build()
)
}
override fun disableDeviceAdmin() { override fun disableDeviceAdmin() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (policyManager.isDeviceOwnerApp(context.packageName)) { if (policyManager.isDeviceOwnerApp(context.packageName)) {
@ -485,21 +507,84 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio
if (enableLockdown) { if (enableLockdown) {
// disable problematic features // disable problematic features
policyManager.addUserRestriction(deviceAdmin, UserManager.DISALLOW_ADD_USER)
policyManager.addUserRestriction(deviceAdmin, UserManager.DISALLOW_FACTORY_RESET) policyManager.addUserRestriction(deviceAdmin, UserManager.DISALLOW_FACTORY_RESET)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
policyManager.addUserRestriction(deviceAdmin, UserManager.DISALLOW_SAFE_BOOT) policyManager.addUserRestriction(deviceAdmin, UserManager.DISALLOW_SAFE_BOOT)
} }
policyManager.getPermissionGrantState(
deviceAdmin,
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
}
}
policyManager.setPermissionGrantState(
deviceAdmin,
context.packageName,
Manifest.permission.CALL_PHONE,
DevicePolicyManager.PERMISSION_GRANT_STATE_GRANTED
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
policyManager.setPermissionGrantState(
deviceAdmin,
context.packageName,
Manifest.permission.POST_NOTIFICATIONS,
DevicePolicyManager.PERMISSION_GRANT_STATE_GRANTED
)
}
} else /* disable lockdown */ { } else /* disable lockdown */ {
// enable problematic features // enable problematic features
policyManager.clearUserRestriction(deviceAdmin, UserManager.DISALLOW_ADD_USER)
policyManager.clearUserRestriction(deviceAdmin, UserManager.DISALLOW_FACTORY_RESET) policyManager.clearUserRestriction(deviceAdmin, UserManager.DISALLOW_FACTORY_RESET)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
policyManager.clearUserRestriction(deviceAdmin, UserManager.DISALLOW_SAFE_BOOT) policyManager.clearUserRestriction(deviceAdmin, UserManager.DISALLOW_SAFE_BOOT)
} }
policyManager.setPermissionGrantState(
deviceAdmin,
context.packageName,
Manifest.permission.ACCESS_FINE_LOCATION,
DevicePolicyManager.PERMISSION_GRANT_STATE_DEFAULT
)
policyManager.setPermissionGrantState(
deviceAdmin,
context.packageName,
Manifest.permission.CALL_PHONE,
DevicePolicyManager.PERMISSION_GRANT_STATE_DEFAULT
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
policyManager.setPermissionGrantState(
deviceAdmin,
context.packageName,
Manifest.permission.POST_NOTIFICATIONS,
DevicePolicyManager.PERMISSION_GRANT_STATE_DEFAULT
)
}
enableSystemApps() enableSystemApps()
stopSuspendingForAllApps() stopSuspendingForAllApps()
setBlockedFeatures(emptySet()) setBlockedFeatures(emptySet())

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -29,6 +29,7 @@ import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.sync.actions.SignOutAtDeviceAction import io.timelimit.android.sync.actions.SignOutAtDeviceAction
import io.timelimit.android.sync.actions.apply.ApplyActionUtil import io.timelimit.android.sync.actions.apply.ApplyActionUtil
import io.timelimit.android.ui.MainActivity import io.timelimit.android.ui.MainActivity
import io.timelimit.android.ui.diagnose.exception.DiagnoseExceptionActivity
import io.timelimit.android.ui.notification.NotificationAreaSync import io.timelimit.android.ui.notification.NotificationAreaSync
class BackgroundActionService: Service() { class BackgroundActionService: Service() {
@ -44,7 +45,7 @@ class BackgroundActionService: Service() {
fun prepareRevokeTemporarilyAllowed(context: Context) = Intent(context, BackgroundActionService::class.java) fun prepareRevokeTemporarilyAllowed(context: Context) = Intent(context, BackgroundActionService::class.java)
.putExtra(ACTION, ACTION_REVOKE_TEMPORARILY_ALLOWED_APPS) .putExtra(ACTION, ACTION_REVOKE_TEMPORARILY_ALLOWED_APPS)
fun getSwitchToDefaultUserIntent(context: Context) = PendingIntent.getService( fun getSwitchToDefaultUserIntent(context: Context): PendingIntent = PendingIntent.getService(
context, context,
PendingIntentIds.SWITCH_TO_DEFAULT_USER, PendingIntentIds.SWITCH_TO_DEFAULT_USER,
Intent(context, BackgroundActionService::class.java) Intent(context, BackgroundActionService::class.java)
@ -57,14 +58,24 @@ class BackgroundActionService: Service() {
.putExtra(EXTRA_NOTIFICATION_TYPE, type) .putExtra(EXTRA_NOTIFICATION_TYPE, type)
.putExtra(EXTRA_NOTIFICATION_ID, id) .putExtra(EXTRA_NOTIFICATION_ID, id)
fun getOpenAppIntent(context: Context) = PendingIntent.getActivity( fun getOpenAppIntent(context: Context): PendingIntent = PendingIntent.getActivity(
context, context,
PendingIntentIds.OPEN_MAIN_APP, PendingIntentIds.OPEN_MAIN_APP,
Intent(context, MainActivity::class.java), Intent(context, MainActivity::class.java),
PendingIntentIds.PENDING_INTENT_FLAGS PendingIntentIds.PENDING_INTENT_FLAGS
) )
fun getSyncNotificationsPendingIntent(context: Context) = PendingIntent.getService( fun getOpenAppWithErrorIntent(context: Context): PendingIntent = PendingIntent.getActivities(
context,
PendingIntentIds.OPEN_MAIN_APP_WITH_ERROR,
arrayOf(
Intent(context, MainActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
Intent(context, DiagnoseExceptionActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
),
PendingIntentIds.PENDING_INTENT_FLAGS
)
fun getSyncNotificationsPendingIntent(context: Context): PendingIntent = PendingIntent.getService(
context, context,
PendingIntentIds.SYNC_NOTIFICATIONS, PendingIntentIds.SYNC_NOTIFICATIONS,
Intent(context, BackgroundActionService::class.java) Intent(context, BackgroundActionService::class.java)

View file

@ -86,7 +86,12 @@ class BackgroundService: Service() {
.setContentTitle(appStatusMessage.title) .setContentTitle(appStatusMessage.title)
.setContentText(appStatusMessage.text) .setContentText(appStatusMessage.text)
.setSubText(appStatusMessage.subtext) .setSubText(appStatusMessage.subtext)
.setContentIntent(BackgroundActionService.getOpenAppIntent(context)) .setContentIntent(
if (appStatusMessage.showErrorMessage)
BackgroundActionService.getOpenAppWithErrorIntent(context)
else
BackgroundActionService.getOpenAppIntent(context)
)
.setWhen(0) .setWhen(0)
.setShowWhen(false) .setShowWhen(false)
.setSound(null) .setSound(null)

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -34,6 +34,7 @@ object NotificationIds {
const val WORKER_REPORT_UNINSTALL = 8 const val WORKER_REPORT_UNINSTALL = 8
const val WORKER_SYNC_BACKGROUND = 9 const val WORKER_SYNC_BACKGROUND = 9
const val NEW_DEVICE = 10 const val NEW_DEVICE = 10
const val EXTRA_TIME_STARTED = 11
} }
object NotificationChannels { object NotificationChannels {
@ -47,6 +48,7 @@ object NotificationChannels {
const val TEMP_ALLOWED_APP = "temporarily allowed App" const val TEMP_ALLOWED_APP = "temporarily allowed App"
const val APP_RESET = "app reset" const val APP_RESET = "app reset"
const val NEW_DEVICE = "new device" const val NEW_DEVICE = "new device"
const val EXTRA_TIME_STARTED = "extra time started"
private fun createAppStatusChannel(notificationManager: NotificationManager, context: Context) { private fun createAppStatusChannel(notificationManager: NotificationManager, context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@ -214,6 +216,20 @@ object NotificationChannels {
} }
} }
private fun createExtraTimeStartedNotificationChannel(notificationManager: NotificationManager, context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
notificationManager.createNotificationChannel(
NotificationChannel(
EXTRA_TIME_STARTED,
context.getString(R.string.notification_channel_extra_time_started_title),
NotificationManager.IMPORTANCE_HIGH
).apply {
description = context.getString(R.string.notification_channel_extra_time_started_description)
}
)
}
}
fun createNotificationChannels(notificationManager: NotificationManager, context: Context) { fun createNotificationChannels(notificationManager: NotificationManager, context: Context) {
createAppStatusChannel(notificationManager, context) createAppStatusChannel(notificationManager, context)
createBlockedNotificationChannel(notificationManager, context) createBlockedNotificationChannel(notificationManager, context)
@ -225,6 +241,7 @@ object NotificationChannels {
createTempAllowedAppChannel(notificationManager, context) createTempAllowedAppChannel(notificationManager, context)
createAppResetChannel(notificationManager, context) createAppResetChannel(notificationManager, context)
createNewDeviceChannel(notificationManager, context) createNewDeviceChannel(notificationManager, context)
createExtraTimeStartedNotificationChannel(notificationManager, context)
} }
} }
@ -237,6 +254,7 @@ object PendingIntentIds {
const val OPEN_UPDATER = 6 const val OPEN_UPDATER = 6
const val U2F_NFC_DISCOVERY = 7 const val U2F_NFC_DISCOVERY = 7
const val U2F_USB_RESPONSE = 8 const val U2F_USB_RESPONSE = 8
const val OPEN_MAIN_APP_WITH_ERROR = 9
val DYNAMIC_NOTIFICATION_RANGE = 100..10000 val DYNAMIC_NOTIFICATION_RANGE = 100..10000
val PENDING_INTENT_FLAGS = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { val PENDING_INTENT_FLAGS = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -184,7 +184,7 @@ class LollipopForegroundAppHelper(context: Context) : UsageStatsForegroundAppHel
} }
private fun doesActivityExistAsAlias(app: ForegroundApp) = try { private fun doesActivityExistAsAlias(app: ForegroundApp) = try {
packageManager.getPackageInfo(app.packageName, PackageManager.GET_ACTIVITIES).activities.find { packageManager.getPackageInfo(app.packageName, PackageManager.GET_ACTIVITIES).activities?.find {
it.enabled && it.targetActivity == app.activityName it.enabled && it.targetActivity == app.activityName
} != null } != null
} catch (ex: PackageManager.NameNotFoundException) { } catch (ex: PackageManager.NameNotFoundException) {

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -153,6 +153,10 @@ class DummyIntegration(
// nothing to do // nothing to do
} }
override fun showExtraTimeStartedNotification(categoryId: String, categoryTitle: String) {
// nothing to do
}
override fun disableDeviceAdmin() { override fun disableDeviceAdmin() {
// nothing to do // nothing to do
} }
@ -203,5 +207,7 @@ class DummyIntegration(
override fun setOrganizationName(name: String) = throw SecurityException() override fun setOrganizationName(name: String) = throw SecurityException()
override fun transferOwnership(packageName: String, dryRun: Boolean) = throw IllegalStateException("unsupported operation") override fun transferOwnership(packageName: String, dryRun: Boolean) = throw IllegalStateException("unsupported operation")
override fun grantLocationAccess(): Boolean = false
} }
} }

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2025 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -65,6 +65,11 @@ class AppSetupLogic(private val appLogic: AppLogic) {
appLogic.database.deleteAllData() appLogic.database.deleteAllData()
appLogic.database.config().setCustomServerUrlSync(customServerUrl) appLogic.database.config().setCustomServerUrlSync(customServerUrl)
appLogic.database.config().setConsentFlagSync(
ConsentFlags.BLOCK_USER_SWITCH_BY_DEFAULT,
true
)
} }
run { run {

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -436,6 +436,9 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
val oldRemainingTime = nowRemaining.includingExtraTime - timeToSubtractForCategory val oldRemainingTime = nowRemaining.includingExtraTime - timeToSubtractForCategory
val newRemainingTime = oldRemainingTime - timeToSubtract val newRemainingTime = oldRemainingTime - timeToSubtract
val oldRemainingNonExtraTime = nowRemaining.default - timeToSubtractForCategory
val newRemainingNonExtraTime = oldRemainingNonExtraTime - timeToSubtract
val commitedSessionDuration = handling.remainingSessionDuration val commitedSessionDuration = handling.remainingSessionDuration
val oldSessionDuration = handling.remainingSessionDuration?.let { it - timeToSubtractForCategory } val oldSessionDuration = handling.remainingSessionDuration?.let { it - timeToSubtractForCategory }
@ -476,6 +479,10 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
) )
} }
if (oldRemainingNonExtraTime > 0 && newRemainingNonExtraTime <= 0) {
appLogic.platformIntegration.showExtraTimeStartedNotification(categoryId, category.title)
}
if (oldSessionDuration != null) { if (oldSessionDuration != null) {
val newSessionDuration = oldSessionDuration - timeToSubtract val newSessionDuration = oldSessionDuration - timeToSubtract
@ -897,7 +904,8 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage( appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
appLogic.context.getString(R.string.background_logic_error), appLogic.context.getString(R.string.background_logic_error),
appLogic.context.getString(R.string.background_logic_error_internal), appLogic.context.getString(R.string.background_logic_error_internal),
showSwitchToDefaultUserOption = deviceRelatedData.canSwitchToDefaultUser showSwitchToDefaultUserOption = deviceRelatedData.canSwitchToDefaultUser,
showErrorMessage = true
)) ))
appLogic.platformIntegration.setShowBlockingOverlay(false) appLogic.platformIntegration.setShowBlockingOverlay(false)
} }

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2025 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -19,10 +19,12 @@ import io.timelimit.android.async.Threads
import io.timelimit.android.data.invalidation.Observer import io.timelimit.android.data.invalidation.Observer
import io.timelimit.android.data.invalidation.Table import io.timelimit.android.data.invalidation.Table
import io.timelimit.android.data.model.CategoryApp 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.ExperimentalFlags
import io.timelimit.android.data.model.UserType import io.timelimit.android.data.model.UserType
import io.timelimit.android.data.model.derived.UserRelatedData import io.timelimit.android.data.model.derived.UserRelatedData
import io.timelimit.android.integration.platform.ProtectionLevel 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.integration.platform.android.AndroidIntegrationApps
import io.timelimit.android.logic.blockingreason.CategoryHandlingCache import io.timelimit.android.logic.blockingreason.CategoryHandlingCache
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
@ -107,12 +109,23 @@ class SuspendAppsLogic(private val appLogic: AppLogic): Observer {
val hasManagedFeatures = featureCategoryApps.isNotEmpty() val hasManagedFeatures = featureCategoryApps.isNotEmpty()
val enableBlocking = isRestrictedUser && (enableBlockingAtSystemLevel || hasManagedFeatures) 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) { if (!enableBlocking) {
lastDefaultCategory = null lastDefaultCategory = null
lastAllowedCategoryList = emptySet() lastAllowedCategoryList = emptySet()
lastCategoryApps = emptyList() lastCategoryApps = emptyList()
applySuspendedApps(emptyList()) applySuspendedApps(emptyList())
applyBlockedFeatures(emptySet()) applyBlockedFeatures(
featureToAllowDefaults.filter { !it.value }.map { it.key }.toSet()
)
return return
} }
@ -191,9 +204,15 @@ class SuspendAppsLogic(private val appLogic: AppLogic): Observer {
val deviceSpecificFeatureIdentifiers = deviceSpecificFeatures.map { it.appSpecifierString }.toSet() val deviceSpecificFeatureIdentifiers = deviceSpecificFeatures.map { it.appSpecifierString }.toSet()
val globalFeatures = featureCategoryApps.filter { !deviceSpecificFeatureIdentifiers.contains(it.appSpecifierString) } val globalFeatures = featureCategoryApps.filter { !deviceSpecificFeatureIdentifiers.contains(it.appSpecifierString) }
val effectiveFeatures = deviceSpecificFeatures + globalFeatures val effectiveFeatures = deviceSpecificFeatures + globalFeatures
val featuresToBlock = effectiveFeatures.filter { !categoryIdsToAllow.contains(it.categoryId) }
.map { it.appSpecifierString.substring(DummyApps.FEATURE_APP_PREFIX.length) } val featuresToAllow = featureToAllowDefaults + effectiveFeatures.associate {
.toSet() 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) applySuspendedApps(appsToBlock)
applyBlockedFeatures(featuresToBlock) applyBlockedFeatures(featuresToBlock)

View file

@ -29,7 +29,7 @@ import io.timelimit.android.logic.RemainingSessionDuration
import io.timelimit.android.logic.RemainingTime import io.timelimit.android.logic.RemainingTime
import io.timelimit.android.sync.actions.AddUsedTimeActionItemAdditionalCountingSlot import io.timelimit.android.sync.actions.AddUsedTimeActionItemAdditionalCountingSlot
import io.timelimit.android.sync.actions.AddUsedTimeActionItemSessionDurationLimitSlot import io.timelimit.android.sync.actions.AddUsedTimeActionItemSessionDurationLimitSlot
import org.threeten.bp.ZoneId import java.time.ZoneId
import java.util.* import java.util.*
data class CategoryItselfHandling ( data class CategoryItselfHandling (

View file

@ -43,6 +43,7 @@ import java.io.OutputStreamWriter
import java.security.KeyPairGenerator import java.security.KeyPairGenerator
import java.security.KeyStore import java.security.KeyStore
import java.security.KeyStore.PrivateKeyEntry import java.security.KeyStore.PrivateKeyEntry
import java.security.ProviderException
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import java.security.spec.ECGenParameterSpec import java.security.spec.ECGenParameterSpec
import java.util.Date import java.util.Date
@ -145,7 +146,11 @@ class HttpServerApi(private val endpointWithoutSlashAtEnd: String): ServerApi {
private suspend fun sendMailLoginCode(mail: String, locale: String, deviceAuthToken: String?, skipDeviceVerification: Boolean): String = withDeviceVerification (enable = !skipDeviceVerification) { client -> private suspend fun sendMailLoginCode(mail: String, locale: String, deviceAuthToken: String?, skipDeviceVerification: Boolean): String = withDeviceVerification (enable = !skipDeviceVerification) { client ->
postJsonRequest( postJsonRequest(
"auth/send-mail-login-code-v2", "auth/send-mail-login-code-v2",
client = client client = client,
transformRequest = { it
.header("X-Client-Package", BuildConfig.APPLICATION_ID)
.header("X-Client-Version", BuildConfig.VERSION_NAME)
}
) { writer -> ) { writer ->
writer.beginObject() writer.beginObject()
writer.name(MAIL).value(mail) writer.name(MAIL).value(mail)
@ -658,10 +663,17 @@ class HttpServerApi(private val endpointWithoutSlashAtEnd: String): ServerApi {
private suspend fun postJsonRequest( private suspend fun postJsonRequest(
path: String, path: String,
client: OkHttpClient = httpClient, client: OkHttpClient = httpClient,
requestBody: (writer: JsonWriter) -> Unit transformRequest: (Request.Builder) -> Request.Builder = { it },
requestBody: (writer: JsonWriter) -> Unit,
): Response { ): Response {
if (!sendContentLength) { if (!sendContentLength) {
val response = postJsonRequest(path, requestBody, transmitContentLength = false, client = client) val response = postJsonRequest(
path,
requestBody,
transmitContentLength = false,
client = client,
transformRequest = transformRequest
)
if (response.code != 411) return response if (response.code != 411) return response
@ -670,14 +682,21 @@ class HttpServerApi(private val endpointWithoutSlashAtEnd: String): ServerApi {
sendContentLength = true sendContentLength = true
} }
return postJsonRequest(path, requestBody, transmitContentLength = true, client = client) return postJsonRequest(
path,
requestBody,
transmitContentLength = true,
client = client,
transformRequest = transformRequest
)
} }
private suspend fun postJsonRequest( private suspend fun postJsonRequest(
path: String, path: String,
requestBody: (writer: JsonWriter) -> Unit, requestBody: (writer: JsonWriter) -> Unit,
transmitContentLength: Boolean, transmitContentLength: Boolean,
client: OkHttpClient = httpClient client: OkHttpClient = httpClient,
transformRequest: (Request.Builder) -> Request.Builder = { it }
): Response { ): Response {
val body = createJsonRequestBody(requestBody, transmitContentLength) val body = createJsonRequestBody(requestBody, transmitContentLength)
@ -686,6 +705,7 @@ class HttpServerApi(private val endpointWithoutSlashAtEnd: String): ServerApi {
.url("$endpointWithoutSlashAtEnd/$path") .url("$endpointWithoutSlashAtEnd/$path")
.post(body) .post(body)
.header("Content-Encoding", "gzip") .header("Content-Encoding", "gzip")
.let { transformRequest(it) }
.build() .build()
).waitForResponse() ).waitForResponse()
} }
@ -697,28 +717,34 @@ class HttpServerApi(private val endpointWithoutSlashAtEnd: String): ServerApi {
val keyId = "temp-" + UUID.randomUUID().toString() val keyId = "temp-" + UUID.randomUUID().toString()
val now = getTimeInMillis() val now = getTimeInMillis()
KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, keyStoreName) try {
.also { KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, keyStoreName)
it.initialize( .also {
KeyGenParameterSpec.Builder( it.initialize(
keyId, KeyGenParameterSpec.Builder(
KeyProperties.PURPOSE_SIGN keyId,
KeyProperties.PURPOSE_SIGN
)
.setAlgorithmParameterSpec(
ECGenParameterSpec("prime256v1")
)
.setDigests(
KeyProperties.DIGEST_NONE,
KeyProperties.DIGEST_SHA256,
KeyProperties.DIGEST_SHA384,
KeyProperties.DIGEST_SHA512
)
.setCertificateNotBefore(Date(now - 1000 * 60))
.setCertificateNotAfter(Date(now + 1000 * 60))
.setAttestationChallenge(byteArrayOf())
.build()
) )
.setAlgorithmParameterSpec( }.genKeyPair()
ECGenParameterSpec("prime256v1") } catch (ex: ProviderException) {
) // java.security.ProviderException: Failed to generate attestation certificate chain
.setDigests(
KeyProperties.DIGEST_NONE, return block(httpClient)
KeyProperties.DIGEST_SHA256, }
KeyProperties.DIGEST_SHA384,
KeyProperties.DIGEST_SHA512
)
.setCertificateNotBefore(Date(now - 1000 * 60))
.setCertificateNotAfter(Date(now + 1000 * 60))
.setAttestationChallenge(byteArrayOf())
.build()
)
}.genKeyPair()
try { try {
val key = keyStore.getEntry(keyId, null) as PrivateKeyEntry val key = keyStore.getEntry(keyId, null) as PrivateKeyEntry

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2025 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -64,10 +64,10 @@ object U2FResponse {
val flags = rawResponse.payload[0] val flags = rawResponse.payload[0]
val counter = rawResponse.payload[4].toUInt() or val counter = rawResponse.payload[4].toUByte().toUInt() or
rawResponse.payload[3].toUInt().shl(8) or rawResponse.payload[3].toUByte().toUInt().shl(8) or
rawResponse.payload[2].toUInt().shl(16) or rawResponse.payload[2].toUByte().toUInt().shl(16) or
rawResponse.payload[1].toUInt().shl(24) rawResponse.payload[1].toUByte().toUInt().shl(24)
val signature = rawResponse.payload.sliceArray(5 until rawResponse.payload.size) val signature = rawResponse.payload.sliceArray(5 until rawResponse.payload.size)

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -19,23 +19,24 @@ import android.Manifest
import android.app.NotificationManager import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.Configuration
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.SystemClock import android.os.SystemClock
import android.provider.Settings import android.provider.Settings
import android.util.Log import android.util.Log
import androidx.activity.SystemBarStyle
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.updateTransition import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -129,11 +130,19 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
if (granted) mainModel.reportPermissionsChanged() if (granted) mainModel.reportPermissionsChanged()
} }
@OptIn(ExperimentalAnimationApi::class)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
supportActionBar!!.hide() val isNightMode =
(resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) ==
Configuration.UI_MODE_NIGHT_YES
enableEdgeToEdge(
statusBarStyle = SystemBarStyle.dark(
if (isNightMode) android.graphics.Color.TRANSPARENT
else resources.getColor(R.color.colorPrimaryDark)
)
)
U2fManager.setupActivity(this) U2fManager.setupActivity(this)
@ -314,9 +323,8 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
screen = screen, screen = screen,
fragmentManager = supportFragmentManager, fragmentManager = supportFragmentManager,
fragmentIds = mainModel.fragmentIds, fragmentIds = mainModel.fragmentIds,
modifier = Modifier modifier = Modifier.fillMaxSize(),
.fillMaxSize() paddingValues = paddingValues
.padding(paddingValues)
) )
}, },
showAuthenticationDialog = showAuthenticationDialog, showAuthenticationDialog = showAuthenticationDialog,
@ -383,10 +391,10 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
return false return false
} }
override fun onNewIntent(intent: Intent?) { override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent) super.onNewIntent(intent)
if ((intent?.flags ?: 0) and Intent.FLAG_ACTIVITY_REORDER_TO_FRONT == Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) { if (intent.flags and Intent.FLAG_ACTIVITY_REORDER_TO_FRONT == Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) {
return return
} }

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -15,6 +15,8 @@
*/ */
package io.timelimit.android.ui package io.timelimit.android.ui
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
@ -42,27 +44,28 @@ fun ScreenMultiplexer(
screen: Screen?, screen: Screen?,
fragmentManager: FragmentManager, fragmentManager: FragmentManager,
fragmentIds: MutableSet<Int>, fragmentIds: MutableSet<Int>,
modifier: Modifier = Modifier modifier: Modifier = Modifier,
paddingValues: PaddingValues
) { ) {
when (screen) { when (screen) {
null -> {/* nothing to do */ } null -> {/* nothing to do */ }
is Screen.FragmentScreen -> FragmentScreen(screen, fragmentManager, fragmentIds, modifier = modifier) is Screen.FragmentScreen -> FragmentScreen(screen, fragmentManager, fragmentIds, modifier = modifier.padding(paddingValues))
is Screen.OverviewScreen -> OverviewScreen(screen.content, modifier = modifier) is Screen.OverviewScreen -> OverviewScreen(screen.content, modifier = modifier, paddingValues = paddingValues)
is Screen.ManageDeviceUserScreen -> ManageDeviceUserScreen(screen.items, screen.actions, screen.overlay, modifier) is Screen.ManageDeviceUserScreen -> ManageDeviceUserScreen(screen.items, screen.actions, screen.overlay, modifier.padding(paddingValues))
is Screen.DeviceOwnerScreen -> DeviceOwnerScreen(screen.content, modifier = modifier) is Screen.DeviceOwnerScreen -> DeviceOwnerScreen(screen.content, modifier = modifier.padding(paddingValues))
is Screen.SetupDevicePermissionsScreen -> SetupDevicePermissionsScreen(screen, modifier) is Screen.SetupDevicePermissionsScreen -> SetupDevicePermissionsScreen(screen, modifier.padding(paddingValues))
is Screen.ManageDevicePermissions -> ManageDevicePermissionScreen(screen.content, modifier) is Screen.ManageDevicePermissions -> ManageDevicePermissionScreen(screen.content, modifier.padding(paddingValues))
is Screen.SetupConnectModePrivacyScreen -> SetupConnectedModePrivacyScreen(screen.customServerDomain, screen.accept, modifier) is Screen.SetupConnectModePrivacyScreen -> SetupConnectedModePrivacyScreen(screen.customServerDomain, screen.accept, modifier.padding(paddingValues))
is Screen.SetupSelectConnectedModeScreen -> SelectConnectedModeScreen(mailLogin = screen.mailLogin, codeLogin = screen.codeLogin, modifier = modifier) is Screen.SetupSelectConnectedModeScreen -> SelectConnectedModeScreen(mailLogin = screen.mailLogin, codeLogin = screen.codeLogin, modifier = modifier.padding(paddingValues))
is Screen.SetupSelectModeScreen -> SelectModeScreen(selectLocal = screen.selectLocal, selectConnected = screen.selectConnected, selectUninstall = screen.selectUninstall, modifier = modifier) is Screen.SetupSelectModeScreen -> SelectModeScreen(selectLocal = screen.selectLocal, selectConnected = screen.selectConnected, selectUninstall = screen.selectUninstall, modifier = modifier.padding(paddingValues))
is Screen.DeleteRegistration -> DeleteRegistrationScreen(screen.content, modifier) is Screen.DeleteRegistration -> DeleteRegistrationScreen(screen.content, modifier.padding(paddingValues))
is Screen.ManageBlockedTimes -> BlockedTimesScreen(screen.content, screen.intro, modifier) is Screen.ManageBlockedTimes -> BlockedTimesScreen(screen.content, screen.intro, modifier.padding(paddingValues))
is Screen.ChildUsageHistory -> UsageHistoryScreen(screen.content, modifier) is Screen.ChildUsageHistory -> UsageHistoryScreen(screen.content, modifier.padding(paddingValues))
is Screen.SetupParentMailAuthentication -> AuthenticateByMailScreen(screen.content, modifier) is Screen.SetupParentMailAuthentication -> AuthenticateByMailScreen(screen.content, modifier.padding(paddingValues))
is Screen.SignupBlocked -> SignupBlockedScreen(modifier) is Screen.SignupBlocked -> SignupBlockedScreen(modifier.padding(paddingValues))
is Screen.SignInWrongMailAddress -> SignInWrongMailAddress(modifier) is Screen.SignInWrongMailAddress -> SignInWrongMailAddress(modifier.padding(paddingValues))
is Screen.ConfirmNewParentAccount -> ConfirmNewParentAccount(confirm = screen.confirm, reject = screen.reject, modifier = modifier) is Screen.ConfirmNewParentAccount -> ConfirmNewParentAccount(confirm = screen.confirm, reject = screen.reject, modifier = modifier.padding(paddingValues))
is Screen.ParentBaseConfiguration -> ParentBaseConfiguration(content = screen.content, modifier = modifier) is Screen.ParentBaseConfiguration -> ParentBaseConfiguration(content = screen.content, modifier = modifier.padding(paddingValues))
is Screen.ParentSetupConsent -> ParentSetupConsent(content = screen.content, errorDialog = screen.errorDialog, modifier = modifier) is Screen.ParentSetupConsent -> ParentSetupConsent(content = screen.content, errorDialog = screen.errorDialog, modifier = modifier.padding(paddingValues))
} }
} }

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -17,8 +17,13 @@ package io.timelimit.android.ui
import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.navigationBarsIgnoringVisibility
import androidx.compose.foundation.layout.statusBarsIgnoringVisibility
import androidx.compose.foundation.layout.systemBarsIgnoringVisibility
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -37,6 +42,7 @@ import io.timelimit.android.ui.model.Screen
import io.timelimit.android.ui.model.Title import io.timelimit.android.ui.model.Title
import io.timelimit.android.ui.model.UpdateStateCommand import io.timelimit.android.ui.model.UpdateStateCommand
@OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
fun ScreenScaffold( fun ScreenScaffold(
screen: Screen?, screen: Screen?,
@ -45,6 +51,7 @@ fun ScreenScaffold(
backStack: List<BackStackItem>, backStack: List<BackStackItem>,
snackbarHostState: SnackbarHostState?, snackbarHostState: SnackbarHostState?,
content: @Composable (PaddingValues) -> Unit, content: @Composable (PaddingValues) -> Unit,
extraBars: (@Composable () -> Unit)? = null,
executeCommand: (UpdateStateCommand) -> Unit, executeCommand: (UpdateStateCommand) -> Unit,
showAuthenticationDialog: (() -> Unit)? showAuthenticationDialog: (() -> Unit)?
) { ) {
@ -52,67 +59,73 @@ fun ScreenScaffold(
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( Column {
title = { TopAppBar(
Column { title = {
Text( Column {
title,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
if (subtitle != null) {
Text( Text(
subtitle, title,
style = MaterialTheme.typography.subtitle1,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis 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) { icon.handler()
IconButton(onClick = { expandDropdown = true }) { }
Icon(Icons.Default.MoreVert, stringResource(R.string.generic_menu)) ) {
Icon(icon.icon, stringResource(icon.labelResource))
}
} }
DropdownMenu( if (screen?.toolbarOptions?.isEmpty() == false) {
expanded = expandDropdown, IconButton(onClick = { expandDropdown = true }) {
onDismissRequest = { expandDropdown = false } Icon(Icons.Default.MoreVert, stringResource(R.string.generic_menu))
) { }
for (option in screen.toolbarOptions) {
DropdownMenuItem(onClick = {
if (option.action != null) executeCommand(option.action)
option.handler() DropdownMenu(
expanded = expandDropdown,
onDismissRequest = { expandDropdown = false }
) {
for (option in screen.toolbarOptions) {
DropdownMenuItem(onClick = {
if (option.action != null) executeCommand(option.action)
expandDropdown = false option.handler()
}) {
Text(stringResource(option.labelResource)) expandDropdown = false
}) {
Text(stringResource(option.labelResource))
}
} }
} }
} }
} },
} modifier = Modifier,
) windowInsets = WindowInsets.statusBarsIgnoringVisibility
)
extraBars?.invoke()
}
}, },
bottomBar = { bottomBar = {
val backStackColors = ButtonDefaults.textButtonColors( val backStackColors = ButtonDefaults.textButtonColors(
@ -159,7 +172,8 @@ fun ScreenScaffold(
Text(title) Text(title)
} }
} }
} },
windowInsets = WindowInsets.navigationBarsIgnoringVisibility
) )
}, },
floatingActionButton = { floatingActionButton = {
@ -170,6 +184,7 @@ fun ScreenScaffold(
} }
}, },
snackbarHost = { SnackbarHost(snackbarHostState ?: it) }, snackbarHost = { SnackbarHost(snackbarHostState ?: it) },
content = content content = content,
contentWindowInsets = WindowInsets.systemBarsIgnoringVisibility
) )
} }

View file

@ -0,0 +1,36 @@
/*
* 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.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.timelimit.android.ui.diagnose.exception
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.fragment.app.FragmentActivity
import io.timelimit.android.logic.DefaultAppLogic
class DiagnoseExceptionActivity: FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val ex = DefaultAppLogic.with(this).backgroundTaskLogic.lastLoopException.value
if (ex != null) setContent {
DiagnoseExceptionDialog(
message = ExceptionUtil.formatInterpreted(this, ex),
close = { finish() }
)
} else finish()
}
}

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -37,7 +37,7 @@ class DiagnoseExceptionDialogFragment: DialogFragment() {
} }
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val message = ExceptionUtil.format(requireArguments().getSerializable(EXCEPTION) as Exception) val message = ExceptionUtil.formatInterpreted(requireContext(), requireArguments().getSerializable(EXCEPTION) as Exception)
return AlertDialog.Builder(requireContext(), theme) return AlertDialog.Builder(requireContext(), theme)
.setMessage(message) .setMessage(message)

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -15,11 +15,26 @@
*/ */
package io.timelimit.android.ui.diagnose.exception package io.timelimit.android.ui.diagnose.exception
import android.content.Context
import io.timelimit.android.R
import io.timelimit.android.integration.platform.android.foregroundapp.InstanceIdForegroundAppHelper.InstanceIdException
import java.io.PrintWriter import java.io.PrintWriter
import java.io.StringWriter import java.io.StringWriter
object ExceptionUtil { object ExceptionUtil {
fun format(tr: Throwable): String = StringWriter().let { sw -> fun formatInterpreted(context: Context, tr: Throwable): String {
val explain = when (tr) {
is InstanceIdException.EventsNotSortedByTimestamp -> context.getString(R.string.background_logic_errpr_detailed_instanceid_sorting)
else -> null
}
val tr2 = formatSimple(tr)
return if (explain != null) "$explain\n\n$tr2"
else tr2
}
private fun formatSimple(tr: Throwable): String = StringWriter().let { sw ->
PrintWriter(sw).let { pw -> PrintWriter(sw).let { pw ->
tr.printStackTrace(pw) tr.printStackTrace(pw)
pw.flush() pw.flush()

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -75,7 +75,7 @@ class HomescreenActivity: AppCompatActivity() {
}) })
} }
override fun onNewIntent(intent: Intent?) { override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent) super.onNewIntent(intent)
model.handleLaunch(intent?.getBooleanExtra(FORCE_SELECTION, false) ?: false) model.handleLaunch(intent?.getBooleanExtra(FORCE_SELECTION, false) ?: false)

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -124,7 +124,7 @@ class LockActionFragment : Fragment() {
override fun setThisDeviceAsCurrentDevice() = this@LockActionFragment.setThisDeviceAsCurrentDevice() override fun setThisDeviceAsCurrentDevice() = this@LockActionFragment.setThisDeviceAsCurrentDevice()
override fun requestLocationPermission() { override fun requestLocationPermission() {
RequestWifiPermission.doRequest(this@LockActionFragment, LOCATION_REQUEST_CODE) RequestWifiPermission.doRequest(this@LockActionFragment, LOCATION_REQUEST_CODE, auth.logic.platformIntegration)
} }
override fun disableLimitsTemporarily() { override fun disableLimitsTemporarily() {

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -18,15 +18,35 @@ package io.timelimit.android.ui.lock
import android.app.ActivityManager import android.app.ActivityManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.Configuration
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.MutableLiveData import androidx.compose.foundation.layout.Column
import androidx.viewpager.widget.ViewPager import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.Tab
import androidx.compose.material.TabRow
import androidx.compose.material.TabRowDefaults
import androidx.compose.material.TabRowDefaults.tabIndicatorOffset
import androidx.compose.material.Text
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.fragment.compose.AndroidFragment
import androidx.lifecycle.asFlow
import androidx.lifecycle.map
import io.timelimit.android.R import io.timelimit.android.R
import io.timelimit.android.databinding.LockActivityBinding import io.timelimit.android.data.model.UserType
import io.timelimit.android.extensions.showSafe import io.timelimit.android.extensions.showSafe
import io.timelimit.android.logic.BlockingReason import io.timelimit.android.logic.BlockingReason
import io.timelimit.android.logic.DefaultAppLogic import io.timelimit.android.logic.DefaultAppLogic
@ -34,11 +54,12 @@ import io.timelimit.android.sync.network.UpdatePrimaryDeviceRequestType
import io.timelimit.android.u2f.U2fManager import io.timelimit.android.u2f.U2fManager
import io.timelimit.android.u2f.protocol.U2FDevice import io.timelimit.android.u2f.protocol.U2FDevice
import io.timelimit.android.ui.IsAppInForeground import io.timelimit.android.ui.IsAppInForeground
import io.timelimit.android.ui.ScreenScaffold
import io.timelimit.android.ui.Theme
import io.timelimit.android.ui.login.AuthTokenLoginProcessor import io.timelimit.android.ui.login.AuthTokenLoginProcessor
import io.timelimit.android.ui.login.NewLoginFragment import io.timelimit.android.ui.login.NewLoginFragment
import io.timelimit.android.ui.main.ActivityViewModel import io.timelimit.android.ui.main.ActivityViewModel
import io.timelimit.android.ui.main.ActivityViewModelHolder import io.timelimit.android.ui.main.ActivityViewModelHolder
import io.timelimit.android.ui.main.AuthenticationFab
import io.timelimit.android.ui.manage.child.primarydevice.UpdatePrimaryDeviceDialogFragment import io.timelimit.android.ui.manage.child.primarydevice.UpdatePrimaryDeviceDialogFragment
import io.timelimit.android.ui.util.SyncStatusModel import io.timelimit.android.ui.util.SyncStatusModel
@ -85,26 +106,115 @@ class LockActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
null null
} }
private val showAuth = MutableLiveData<Boolean>().apply { value = false }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val isNightMode =
(resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) ==
Configuration.UI_MODE_NIGHT_YES
enableEdgeToEdge(
statusBarStyle = SystemBarStyle.dark(
if (isNightMode) android.graphics.Color.TRANSPARENT
else resources.getColor(R.color.colorPrimaryDark)
)
)
U2fManager.setupActivity(this) U2fManager.setupActivity(this)
val adapter = LockActivityAdapter(supportFragmentManager, this) val subtitleLive = syncModel.statusText.asFlow()
val showTasksLive = model.content.map {
val isTimeOver = it is LockscreenContent.Blocked.BlockedCategory && it.blockingHandling.activityBlockingReason == BlockingReason.TimeOver
val binding = LockActivityBinding.inflate(layoutInflater) isTimeOver
setContentView(binding.root) }.asFlow()
syncModel.statusText.observe(this) { supportActionBar?.subtitle = it } setContent {
val subtitle by subtitleLive.collectAsState(null)
val showTasks by showTasksLive.collectAsState(false)
val pager = rememberPagerState(initialPage = 0, pageCount = {
if (showTasks) 3
else 2
})
val isAuthenticated by getActivityViewModel().authenticatedUser
.map { it?.second?.type == UserType.Parent }
.asFlow().collectAsState(initial = false)
Theme {
ScreenScaffold(
screen = null,
title = getString(R.string.app_name),
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())
}
}
)
},
executeCommand = {},
showAuthenticationDialog =
if (pager.currentPage == 1 && !isAuthenticated) ({ showAuthenticationScreen() })
else null
)
}
}
currentInstances.add(this) currentInstances.add(this)
model.init(blockedPackageName, blockedActivityName) model.init(blockedPackageName, blockedActivityName)
binding.pager.adapter = adapter
model.content.observe(this) { model.content.observe(this) {
if (isResumed && it is LockscreenContent.Blocked.BlockedCategory && it.reason == BlockingReason.RequiresCurrentDevice && !model.didOpenSetCurrentDeviceScreen) { if (isResumed && it is LockscreenContent.Blocked.BlockedCategory && it.reason == BlockingReason.RequiresCurrentDevice && !model.didOpenSetCurrentDeviceScreen) {
model.didOpenSetCurrentDeviceScreen = true model.didOpenSetCurrentDeviceScreen = true
@ -115,30 +225,12 @@ class LockActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
} }
} }
AuthenticationFab.manageAuthenticationFab( activityModel.shouldHighlightAuthenticationButton.observe(this) {
fab = binding.fab, if (it) {
shouldHighlight = activityModel.shouldHighlightAuthenticationButton, activityModel.shouldHighlightAuthenticationButton.postValue(false)
authenticatedUser = activityModel.authenticatedUser,
activity = this,
doesSupportAuth = showAuth
)
binding.fab.setOnClickListener { showAuthenticationScreen() } showAuthenticationScreen()
binding.pager.addOnPageChangeListener(object: ViewPager.SimpleOnPageChangeListener() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
showAuth.value = position == 1
} }
})
binding.tabs.setupWithViewPager(binding.pager)
model.content.observe(this) {
val isTimeOver = it is LockscreenContent.Blocked.BlockedCategory && it.blockingHandling.activityBlockingReason == BlockingReason.TimeOver
adapter.showTasksFragment = isTimeOver
} }
onBackPressedDispatcher.addCallback(object: OnBackPressedCallback(true) { onBackPressedDispatcher.addCallback(object: OnBackPressedCallback(true) {

View file

@ -1,44 +0,0 @@
/*
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.timelimit.android.ui.lock
import android.content.Context
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter
import io.timelimit.android.R
import kotlin.properties.Delegates
class LockActivityAdapter(fragmentManager: FragmentManager, private val context: Context): FragmentPagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
var showTasksFragment: Boolean by Delegates.observable(false) { _, _, _ -> notifyDataSetChanged() }
override fun getCount(): Int = if (showTasksFragment) 3 else 2
override fun getItem(position: Int): Fragment = when (position) {
0 -> LockReasonFragment()
1 -> LockActionFragment()
2 -> LockTaskFragment()
else -> throw IllegalArgumentException()
}
override fun getPageTitle(position: Int): CharSequence? = context.getString(when (position) {
0 -> R.string.lock_tab_reason
1 -> R.string.lock_tab_action
2 -> R.string.lock_tab_task
else -> throw IllegalArgumentException()
})
}

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -16,14 +16,21 @@
package io.timelimit.android.ui.manage.category.apps.add package io.timelimit.android.ui.manage.category.apps.add
import android.app.Dialog import android.app.Dialog
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewGroup.MarginLayoutParams
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.coordinatorlayout.widget.CoordinatorLayout 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.DialogFragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels 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<MarginLayoutParams> {
topMargin = insets.top
bottomMargin = insets.bottom
leftMargin = insets.left
rightMargin = insets.right
}
WindowInsetsCompat.CONSUMED
}
return AlertDialog.Builder(requireContext(), R.style.AppTheme) return AlertDialog.Builder(requireContext(), R.style.AppTheme)
.setView(binding.root) .setView(binding.root)
.create() .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) { fun show(manager: FragmentManager) {

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -16,9 +16,16 @@
package io.timelimit.android.ui.manage.category.apps.addactivity package io.timelimit.android.ui.manage.category.apps.addactivity
import android.app.Dialog import android.app.Dialog
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup.MarginLayoutParams
import androidx.appcompat.app.AlertDialog 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.DialogFragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
@ -114,9 +121,30 @@ class AddAppActivitiesDialogFragment: DialogFragment() {
dismissAllowingStateLoss() 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) return AlertDialog.Builder(requireContext(), R.style.AppTheme)
.setView(binding.root) .setView(binding.root)
.create() .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) fun show(fragmentManager: FragmentManager) = showSafe(fragmentManager, DIALOG_TAG)

View file

@ -31,6 +31,7 @@ import io.timelimit.android.logic.RemainingTime
import io.timelimit.android.ui.manage.category.timelimit_rules.TimeLimitRulesHandlers import io.timelimit.android.ui.manage.category.timelimit_rules.TimeLimitRulesHandlers
import io.timelimit.android.ui.util.DateUtil import io.timelimit.android.ui.util.DateUtil
import io.timelimit.android.util.DayNameUtil import io.timelimit.android.util.DayNameUtil
import io.timelimit.android.util.Option
import io.timelimit.android.util.TimeTextUtil import io.timelimit.android.util.TimeTextUtil
import kotlin.properties.Delegates import kotlin.properties.Delegates
@ -159,12 +160,24 @@ class AppAndRuleAdapter: RecyclerView.Adapter<AppAndRuleAdapter.Holder>() {
val binding = holder.itemView.tag as FragmentCategoryTimeLimitRuleItemBinding val binding = holder.itemView.tag as FragmentCategoryTimeLimitRuleItemBinding
val context = binding.root.context val context = binding.root.context
val usedTime = date?.let { date -> val usedTime = date?.let { date ->
RemainingTime.getUsedTime( val dayOfWeekForDailyRule: Option<Int?>? =
usedTimes = usedTimes, if (rule.perDay) {
rule = rule, (0 until 7)
firstDayOfWeekAsEpochDay = date.firstDayOfWeekAsEpochDay, .map { (7 + date.dayOfWeek - it) % 7 } // make the current day the last one
dayOfWeekForDailyRule = if (rule.perDay) date.dayOfWeek else null .firstOrNull { rule.dayMask.toInt() and (1 shl it) != 0 }
).toInt() ?.let { Option.Some(it) } // skip calculation if no day matches
} else Option.Some(null) // use the value null
dayOfWeekForDailyRule?.let {
RemainingTime.getUsedTime(
usedTimes = usedTimes,
rule = rule,
firstDayOfWeekAsEpochDay = date.firstDayOfWeekAsEpochDay,
dayOfWeekForDailyRule =
if (it is Option.Some) it.value
else null
).toInt()
}
} ?: 0 } ?: 0
binding.maxTimeString = rule.maximumTimeInMillis.let { time -> binding.maxTimeString = rule.maximumTimeInMillis.let { time ->

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -117,7 +117,7 @@ object ManageCategoryNetworksView {
} }
view.grantPermissionButton.setOnClickListener { view.grantPermissionButton.setOnClickListener {
RequestWifiPermission.doRequest(fragment, permissionRequestCode) RequestWifiPermission.doRequest(fragment, permissionRequestCode, auth.logic.platformIntegration)
} }
isFullVersionLive.observe(lifecycleOwner, Observer { isFullVersion -> isFullVersionLive.observe(lifecycleOwner, Observer { isFullVersion ->

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -26,6 +26,7 @@ import android.widget.Toast
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import io.timelimit.android.R import io.timelimit.android.R
import io.timelimit.android.integration.platform.PlatformIntegration
object RequestWifiPermission { object RequestWifiPermission {
private fun isLocationEnabled(context: Context): Boolean = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) private fun isLocationEnabled(context: Context): Boolean = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P)
@ -36,8 +37,10 @@ object RequestWifiPermission {
locationManager.isLocationEnabled locationManager.isLocationEnabled
} }
fun doRequest(fragment: Fragment, permissionRequestCode: Int) { fun doRequest(fragment: Fragment, permissionRequestCode: Int, platformIntegration: PlatformIntegration) {
if (ContextCompat.checkSelfPermission(fragment.requireContext(), Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { if (ContextCompat.checkSelfPermission(fragment.requireContext(), Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
if (platformIntegration.deviceOwner.grantLocationAccess()) return
fragment.requestPermissions(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), permissionRequestCode) fragment.requestPermissions(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), permissionRequestCode)
} else if (!isLocationEnabled(fragment.requireContext())) { } else if (!isLocationEnabled(fragment.requireContext())) {
Toast.makeText(fragment.requireContext(), R.string.category_networks_toast_enable_location_service, Toast.LENGTH_SHORT).show() Toast.makeText(fragment.requireContext(), R.string.category_networks_toast_enable_location_service, Toast.LENGTH_SHORT).show()

View file

@ -22,7 +22,7 @@ import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.fragment.app.setFragmentResult import androidx.fragment.app.setFragmentResult
import io.timelimit.android.extensions.showSafe import io.timelimit.android.extensions.showSafe
import org.threeten.bp.LocalDate import java.time.LocalDate
class DatePickerDialogFragment: DialogFragment() { class DatePickerDialogFragment: DialogFragment() {
companion object { companion object {
@ -81,8 +81,8 @@ class DatePickerDialogFragment: DialogFragment() {
val requestKey = requireArguments().getString(REQUEST_KEY)!! val requestKey = requireArguments().getString(REQUEST_KEY)!!
return DatePickerDialog(requireContext(), theme, { _, year, month, day -> return DatePickerDialog(requireContext(), theme, { _, year, month, day ->
setFragmentResult(requestKey, Result(year, month, day).bundle) setFragmentResult(requestKey, Result(year, month + 1, day).bundle)
}, startYear, startMonthOfYear, startDayOfMonth) }, startYear, startMonthOfYear - 1, startDayOfMonth)
} }
fun show(fragmentManager: FragmentManager) = showSafe(fragmentManager, DIALOG_TAG) fun show(fragmentManager: FragmentManager) = showSafe(fragmentManager, DIALOG_TAG)

View file

@ -47,9 +47,9 @@ import io.timelimit.android.ui.util.DateUtil
import io.timelimit.android.ui.view.SelectDayViewHandlers import io.timelimit.android.ui.view.SelectDayViewHandlers
import io.timelimit.android.ui.view.SelectTimeSpanViewListener import io.timelimit.android.ui.view.SelectTimeSpanViewListener
import io.timelimit.android.util.TimeTextUtil import io.timelimit.android.util.TimeTextUtil
import org.threeten.bp.Instant import java.time.Instant
import org.threeten.bp.LocalDate import java.time.LocalDate
import org.threeten.bp.ZoneId import java.time.ZoneId
import java.util.* import java.util.*
class EditTimeLimitRuleDialogFragment : BottomSheetDialogFragment() { class EditTimeLimitRuleDialogFragment : BottomSheetDialogFragment() {

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -36,8 +36,8 @@ import io.timelimit.android.logic.RealTime
import io.timelimit.android.sync.actions.SetUserDisableLimitsUntilAction import io.timelimit.android.sync.actions.SetUserDisableLimitsUntilAction
import io.timelimit.android.ui.main.ActivityViewModel import io.timelimit.android.ui.main.ActivityViewModel
import io.timelimit.android.ui.main.getActivityViewModel import io.timelimit.android.ui.main.getActivityViewModel
import org.threeten.bp.LocalDate import java.time.LocalDate
import org.threeten.bp.ZoneId import java.time.ZoneId
import java.util.* import java.util.*
class DisableTimelimitsUntilDateDialogFragment: DialogFragment() { class DisableTimelimitsUntilDateDialogFragment: DialogFragment() {

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -35,9 +35,9 @@ import io.timelimit.android.logic.RealTime
import io.timelimit.android.sync.actions.SetUserDisableLimitsUntilAction import io.timelimit.android.sync.actions.SetUserDisableLimitsUntilAction
import io.timelimit.android.ui.main.ActivityViewModel import io.timelimit.android.ui.main.ActivityViewModel
import io.timelimit.android.ui.main.getActivityViewModel import io.timelimit.android.ui.main.getActivityViewModel
import org.threeten.bp.Instant import java.time.Instant
import org.threeten.bp.LocalDateTime import java.time.LocalDateTime
import org.threeten.bp.ZoneId import java.time.ZoneId
class DisableTimelimitsUntilTimeDialogFragment: DialogFragment() { class DisableTimelimitsUntilTimeDialogFragment: DialogFragment() {
companion object { companion object {

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -29,8 +29,8 @@ import io.timelimit.android.ui.help.HelpDialogFragment
import io.timelimit.android.ui.main.getActivityViewModel import io.timelimit.android.ui.main.getActivityViewModel
import io.timelimit.android.ui.payment.RequiresPurchaseDialogFragment import io.timelimit.android.ui.payment.RequiresPurchaseDialogFragment
import io.timelimit.android.ui.view.ManageDisableTimelimitsViewHandlers import io.timelimit.android.ui.view.ManageDisableTimelimitsViewHandlers
import org.threeten.bp.LocalDate import java.time.LocalDate
import org.threeten.bp.ZoneId import java.time.ZoneId
import java.util.* import java.util.*
object ManageDisableTimelimitsViewHelper { object ManageDisableTimelimitsViewHelper {

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -34,11 +34,11 @@ import io.timelimit.android.extensions.toInstant
import io.timelimit.android.ui.main.ActivityViewModel import io.timelimit.android.ui.main.ActivityViewModel
import io.timelimit.android.ui.main.getActivityViewModel import io.timelimit.android.ui.main.getActivityViewModel
import io.timelimit.android.ui.payment.RequiresPurchaseDialogFragment import io.timelimit.android.ui.payment.RequiresPurchaseDialogFragment
import org.threeten.bp.Instant import java.time.Instant
import org.threeten.bp.LocalDate import java.time.LocalDate
import org.threeten.bp.LocalDateTime import java.time.LocalDateTime
import org.threeten.bp.LocalTime import java.time.LocalTime
import org.threeten.bp.ZoneId import java.time.ZoneId
class SetCategorySpecialModeFragment: DialogFragment() { class SetCategorySpecialModeFragment: DialogFragment() {
companion object { companion object {

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -26,7 +26,7 @@ import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.sync.actions.UpdateCategoryDisableLimitsAction import io.timelimit.android.sync.actions.UpdateCategoryDisableLimitsAction
import io.timelimit.android.sync.actions.UpdateCategoryTemporarilyBlockedAction import io.timelimit.android.sync.actions.UpdateCategoryTemporarilyBlockedAction
import io.timelimit.android.ui.main.ActivityViewModel import io.timelimit.android.ui.main.ActivityViewModel
import org.threeten.bp.LocalDate import java.time.LocalDate
class SetCategorySpecialModeModel(application: Application): AndroidViewModel(application) { class SetCategorySpecialModeModel(application: Application): AndroidViewModel(application) {
private val logic = DefaultAppLogic.with(application) private val logic = DefaultAppLogic.with(application)

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -19,8 +19,8 @@ import android.content.Context
import android.text.format.DateUtils import android.text.format.DateUtils
import io.timelimit.android.R import io.timelimit.android.R
import io.timelimit.android.date.DateInTimezone import io.timelimit.android.date.DateInTimezone
import org.threeten.bp.LocalDate import java.time.LocalDate
import org.threeten.bp.ZoneId import java.time.ZoneId
import java.util.* import java.util.*
sealed class SpecialModeOption { sealed class SpecialModeOption {

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -48,8 +48,8 @@ import io.timelimit.android.R
import io.timelimit.android.extensions.MinuteOfDay import io.timelimit.android.extensions.MinuteOfDay
import io.timelimit.android.ui.model.managechild.ManageChildUsageHistory import io.timelimit.android.ui.model.managechild.ManageChildUsageHistory
import io.timelimit.android.util.TimeTextUtil import io.timelimit.android.util.TimeTextUtil
import org.threeten.bp.LocalDate import java.time.LocalDate
import org.threeten.bp.ZoneOffset import java.time.ZoneOffset
import java.util.Date import java.util.Date
import java.util.TimeZone import java.util.TimeZone

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2025 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -49,7 +49,7 @@ fun ManageDeviceUserScreen(
Card( Card(
onClick = { actions.select(item) }, onClick = { actions.select(item) },
modifier = Modifier modifier = Modifier
.animateItemPlacement() .animateItem()
.fillMaxWidth(), .fillMaxWidth(),
backgroundColor = when (item.selected) { backgroundColor = when (item.selected) {
true -> MaterialTheme.colors.secondary true -> MaterialTheme.colors.secondary

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -18,12 +18,21 @@ package io.timelimit.android.ui.manipulation
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.res.Configuration
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.LayoutInflater
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.map import androidx.lifecycle.map
import io.timelimit.android.BuildConfig import io.timelimit.android.BuildConfig
import io.timelimit.android.R import io.timelimit.android.R
@ -34,6 +43,8 @@ import io.timelimit.android.integration.platform.android.AndroidIntegrationApps
import io.timelimit.android.logic.DefaultAppLogic import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.u2f.U2fManager import io.timelimit.android.u2f.U2fManager
import io.timelimit.android.u2f.protocol.U2FDevice import io.timelimit.android.u2f.protocol.U2FDevice
import io.timelimit.android.ui.ScreenScaffold
import io.timelimit.android.ui.Theme
import io.timelimit.android.ui.backdoor.BackdoorDialogFragment import io.timelimit.android.ui.backdoor.BackdoorDialogFragment
import io.timelimit.android.ui.login.AuthTokenLoginProcessor import io.timelimit.android.ui.login.AuthTokenLoginProcessor
import io.timelimit.android.ui.login.NewLoginFragment import io.timelimit.android.ui.login.NewLoginFragment
@ -65,12 +76,73 @@ class AnnoyActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.D
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
U2fManager.setupActivity(this)
val logic = DefaultAppLogic.with(this) val logic = DefaultAppLogic.with(this)
val binding = AnnoyActivityBinding.inflate(layoutInflater) val isNightMode =
setContentView(binding.root) (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) ==
Configuration.UI_MODE_NIGHT_YES
enableEdgeToEdge(
statusBarStyle = SystemBarStyle.dark(
if (isNightMode) android.graphics.Color.TRANSPARENT
else resources.getColor(R.color.colorPrimaryDark)
)
)
setContent {
Theme {
ScreenScaffold(
screen = null,
title = getString(R.string.app_name),
subtitle = null,
backStack = emptyList(),
snackbarHostState = null,
content = { padding ->
AndroidView(
factory = {
val binding = AnnoyActivityBinding.inflate(LayoutInflater.from(it))
logic.annoyLogic.nextManualUnblockCountdown.observe(this) { countdown ->
binding.canRequestUnlock = countdown == 0L
binding.countdownText = getString(R.string.annoy_timer, TimeTextUtil.seconds((countdown / 1000).toInt(), this@AnnoyActivity))
}
logic.deviceEntry.map {
val reasonItems = (it?.let { ManipulationWarnings.getFromDevice(it) } ?: ManipulationWarnings.empty)
.current
.map { getString(it.labelResourceId) }
if (reasonItems.isEmpty()) {
null
} else {
getString(R.string.annoy_reason, reasonItems.joinToString(separator = ", "))
}
}.observe(this) { binding.reasonText = it }
binding.unlockTemporarilyButton.setOnClickListener {
AnnoyUnlockDialogFragment.newInstance(AnnoyUnlockDialogFragment.UnlockDuration.Short)
.show(supportFragmentManager)
}
binding.parentUnlockButton.setOnClickListener {
AnnoyUnlockDialogFragment.newInstance(AnnoyUnlockDialogFragment.UnlockDuration.Long)
.show(supportFragmentManager)
}
binding.useBackdoorButton.setOnClickListener { BackdoorDialogFragment().show(supportFragmentManager) }
binding.root
},
modifier = Modifier.fillMaxSize().padding(padding)
)
},
executeCommand = {},
showAuthenticationDialog = null
)
}
}
U2fManager.setupActivity(this)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val systemImageApps = packageManager.getInstalledApplications(0) val systemImageApps = packageManager.getInstalledApplications(0)
@ -92,35 +164,6 @@ class AnnoyActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.D
if (!shouldRun) shutdown() if (!shouldRun) shutdown()
} }
logic.annoyLogic.nextManualUnblockCountdown.observe(this) { countdown ->
binding.canRequestUnlock = countdown == 0L
binding.countdownText = getString(R.string.annoy_timer, TimeTextUtil.seconds((countdown / 1000).toInt(), this@AnnoyActivity))
}
logic.deviceEntry.map {
val reasonItems = (it?.let { ManipulationWarnings.getFromDevice(it) } ?: ManipulationWarnings.empty)
.current
.map { getString(it.labelResourceId) }
if (reasonItems.isEmpty()) {
null
} else {
getString(R.string.annoy_reason, reasonItems.joinToString(separator = ", "))
}
}.observe(this) { binding.reasonText = it }
binding.unlockTemporarilyButton.setOnClickListener {
AnnoyUnlockDialogFragment.newInstance(AnnoyUnlockDialogFragment.UnlockDuration.Short)
.show(supportFragmentManager)
}
binding.parentUnlockButton.setOnClickListener {
AnnoyUnlockDialogFragment.newInstance(AnnoyUnlockDialogFragment.UnlockDuration.Long)
.show(supportFragmentManager)
}
binding.useBackdoorButton.setOnClickListener { BackdoorDialogFragment().show(supportFragmentManager) }
model.authenticatedUser.observe(this) { user -> model.authenticatedUser.observe(this) { user ->
if (user?.second?.type == UserType.Parent) { if (user?.second?.type == UserType.Parent) {
logic.annoyLogic.doParentTempUnlock() logic.annoyLogic.doParentTempUnlock()

View file

@ -181,7 +181,7 @@ object AccountDeletion {
) )
if (result == SnackbarResult.ActionPerformed) updateState { if (result == SnackbarResult.ActionPerformed) updateState {
it.copy(errorDialog = ExceptionUtil.format(ex)) it.copy(errorDialog = ExceptionUtil.formatInterpreted(logic.context, ex))
} }
} }
} finally { } finally {

View file

@ -160,7 +160,7 @@ object MailAuthentication {
) )
if (result == SnackbarResult.ActionPerformed) { if (result == SnackbarResult.ActionPerformed) {
val message = ExceptionUtil.format(ex) val message = ExceptionUtil.formatInterpreted(logic.context, ex)
updateState { it.withError(error = ErrorDialog.ExceptionDetails(message)) } updateState { it.withError(error = ErrorDialog.ExceptionDetails(message)) }
} }

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -181,7 +181,7 @@ object DeviceOwnerHandling {
) )
if (result == SnackbarResult.ActionPerformed) updateState { if (result == SnackbarResult.ActionPerformed) updateState {
it.copy(dialog = OwnerState.ErrorDialog(ExceptionUtil.format(ex))) it.copy(dialog = OwnerState.ErrorDialog(ExceptionUtil.formatInterpreted(logic.context, ex)))
} }
} }
} }

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2025 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -23,6 +23,7 @@ import io.timelimit.android.async.Threads
import io.timelimit.android.coroutines.executeAndWait import io.timelimit.android.coroutines.executeAndWait
import io.timelimit.android.data.backup.DatabaseBackup import io.timelimit.android.data.backup.DatabaseBackup
import io.timelimit.android.data.devicename.DeviceName import io.timelimit.android.data.devicename.DeviceName
import io.timelimit.android.data.model.ConsentFlags
import io.timelimit.android.logic.AppLogic import io.timelimit.android.logic.AppLogic
import io.timelimit.android.sync.ApplyServerDataStatus import io.timelimit.android.sync.ApplyServerDataStatus
import io.timelimit.android.sync.network.NewDeviceInfo import io.timelimit.android.sync.network.NewDeviceInfo
@ -333,6 +334,11 @@ object SetupParentHandling {
database.config().setDeviceAuthTokenSync(result.deviceAuthToken) database.config().setDeviceAuthTokenSync(result.deviceAuthToken)
database.config().setEnableBackgroundSync(state.backgroundSync) database.config().setEnableBackgroundSync(state.backgroundSync)
database.config().setConsentFlagSync(
ConsentFlags.BLOCK_USER_SWITCH_BY_DEFAULT,
true
)
ApplyServerDataStatus.applyServerDataStatusSync(result.serverDataStatus, logic.database, logic.platformIntegration) ApplyServerDataStatus.applyServerDataStatusSync(result.serverDataStatus, logic.database, logic.platformIntegration)
} }
} }
@ -363,7 +369,7 @@ object SetupParentHandling {
) )
if (result == SnackbarResult.ActionPerformed) updateState { if (result == SnackbarResult.ActionPerformed) updateState {
it.copy(error = ExceptionUtil.format(ex)) it.copy(error = ExceptionUtil.formatInterpreted(logic.context, ex))
} }
} }
} finally { } finally {

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2025 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -35,7 +35,7 @@ import io.timelimit.android.ui.model.main.OverviewHandling
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
fun LazyListScope.deviceItems(screen: OverviewHandling.OverviewScreen) { fun LazyListScope.deviceItems(screen: OverviewHandling.OverviewScreen) {
item (key = Pair("devices", "header")) { 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) }) { items(screen.devices.list, key = { Pair("device", it.device.id) }) {
@ -48,7 +48,7 @@ fun LazyListScope.deviceItems(screen: OverviewHandling.OverviewScreen) {
icon = Icons.Default.Add, icon = Icons.Default.Add,
label = stringResource(R.string.add_device), label = stringResource(R.string.add_device),
action = screen.actions.addDevice, 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) { if (screen.devices.canShowMore != null) {
item (key = Pair("devices", "more")) { item (key = Pair("devices", "more")) {
ListCommon.ShowMoreItem( ListCommon.ShowMoreItem(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItem(),
action = { screen.actions.showMoreDevices(screen.devices.canShowMore) } action = { screen.actions.showMoreDevices(screen.devices.canShowMore) }
) )
} }
@ -71,7 +71,7 @@ fun LazyItemScope.DeviceItem(
) { ) {
ListCardCommon.Card( ListCardCommon.Card(
Modifier Modifier
.animateItemPlacement() .animateItem()
.padding(horizontal = 8.dp) .padding(horizontal = 8.dp)
.clickable(onClick = { openAction(item) }) .clickable(onClick = { openAction(item) })
) { ) {

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2025 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -40,7 +40,7 @@ fun LazyListScope.introItems(
item (key = Pair("intro", "finish setup")) { item (key = Pair("intro", "finish setup")) {
ListCardCommon.Card( ListCardCommon.Card(
modifier = Modifier modifier = Modifier
.animateItemPlacement() .animateItem()
.padding(horizontal = 8.dp) .padding(horizontal = 8.dp)
) { ) {
Text( Text(
@ -62,7 +62,7 @@ fun LazyListScope.introItems(
item (key = Pair("intro", "outdated server")) { item (key = Pair("intro", "outdated server")) {
ListCardCommon.Card( ListCardCommon.Card(
modifier = Modifier modifier = Modifier
.animateItemPlacement() .animateItem()
.padding(horizontal = 8.dp) .padding(horizontal = 8.dp)
) { ) {
Text( Text(
@ -79,7 +79,7 @@ fun LazyListScope.introItems(
item (key = Pair("intro", "server message")) { item (key = Pair("intro", "server message")) {
ListCardCommon.Card( ListCardCommon.Card(
modifier = Modifier modifier = Modifier
.animateItemPlacement() .animateItem()
.padding(horizontal = 8.dp) .padding(horizontal = 8.dp)
) { ) {
Text( Text(
@ -108,7 +108,7 @@ fun LazyListScope.introItems(
SwipeToDismiss( SwipeToDismiss(
state = state, state = state,
background = {}, background = {},
modifier = Modifier.animateItemPlacement() modifier = Modifier.animateItem()
) { ) {
ListCardCommon.Card( ListCardCommon.Card(
modifier = Modifier.padding(horizontal = 8.dp) modifier = Modifier.padding(horizontal = 8.dp)
@ -133,7 +133,7 @@ fun LazyListScope.introItems(
item (key = Pair("intro", "task review")) { item (key = Pair("intro", "task review")) {
ListCardCommon.Card( ListCardCommon.Card(
modifier = Modifier modifier = Modifier
.animateItemPlacement() .animateItem()
.padding(horizontal = 8.dp) .padding(horizontal = 8.dp)
) { ) {
Text( Text(

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -19,16 +19,24 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.timelimit.android.ui.model.main.OverviewHandling import io.timelimit.android.ui.model.main.OverviewHandling
@Composable @Composable
fun OverviewScreen( fun OverviewScreen(
screen: OverviewHandling.OverviewScreen, screen: OverviewHandling.OverviewScreen,
paddingValues: PaddingValues,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
LazyColumn ( LazyColumn (
contentPadding = PaddingValues(0.dp, 8.dp), contentPadding = object: PaddingValues {
override fun calculateLeftPadding(layoutDirection: LayoutDirection): Dp = paddingValues.calculateLeftPadding(layoutDirection)
override fun calculateRightPadding(layoutDirection: LayoutDirection): Dp = paddingValues.calculateRightPadding(layoutDirection)
override fun calculateTopPadding(): Dp = paddingValues.calculateTopPadding() + 8.dp
override fun calculateBottomPadding(): Dp = paddingValues.calculateBottomPadding() + 8.dp
},
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = modifier modifier = modifier
) { ) {

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2025 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -35,7 +35,7 @@ import io.timelimit.android.ui.model.main.OverviewHandling
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
fun LazyListScope.userItems(screen: OverviewHandling.OverviewScreen) { fun LazyListScope.userItems(screen: OverviewHandling.OverviewScreen) {
item (key = Pair("users", "header")) { 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) } 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, icon = Icons.Default.Add,
label = stringResource(R.string.add_user_title), label = stringResource(R.string.add_user_title),
action = screen.actions.addUser, action = screen.actions.addUser,
modifier = Modifier.animateItemPlacement() modifier = Modifier.animateItem()
) )
} }
if (screen.users.canShowMore) item (key = Pair("users", "more")) { if (screen.users.canShowMore) item (key = Pair("users", "more")) {
ListCommon.ShowMoreItem ( ListCommon.ShowMoreItem (
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItem(),
action = screen.actions.showMoreUsers action = screen.actions.showMoreUsers
) )
} }
@ -65,7 +65,7 @@ fun LazyItemScope.UserItem(
) { ) {
ListCardCommon.Card( ListCardCommon.Card(
Modifier Modifier
.animateItemPlacement() .animateItem()
.padding(horizontal = 8.dp) .padding(horizontal = 8.dp)
.clickable(onClick = { actions.openUser(user) }) .clickable(onClick = { actions.openUser(user) })
) { ) {

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2025 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -74,9 +74,14 @@ class ActivityPurchaseModel(application: Application): AndroidViewModel(applicat
clientMutex.withLock { clientMutex.withLock {
if (_billingClient == null) { if (_billingClient == null) {
_billingClient = BillingClient.newBuilder(getApplication()) _billingClient = BillingClient.newBuilder(getApplication())
.enablePendingPurchases() .enablePendingPurchases(
.setListener(purchaseUpdatedListener) PendingPurchasesParams
.build() .newBuilder()
.enableOneTimeProducts()
.build()
)
.setListener(purchaseUpdatedListener)
.build()
} }
val initBillingClient = _billingClient!! val initBillingClient = _billingClient!!

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2025 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -24,6 +24,7 @@ import io.timelimit.android.coroutines.executeAndWait
import io.timelimit.android.coroutines.runAsync import io.timelimit.android.coroutines.runAsync
import io.timelimit.android.data.backup.DatabaseBackup import io.timelimit.android.data.backup.DatabaseBackup
import io.timelimit.android.data.devicename.DeviceName import io.timelimit.android.data.devicename.DeviceName
import io.timelimit.android.data.model.ConsentFlags
import io.timelimit.android.livedata.castDown import io.timelimit.android.livedata.castDown
import io.timelimit.android.logic.AppLogic import io.timelimit.android.logic.AppLogic
import io.timelimit.android.logic.DefaultAppLogic 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().setOwnDeviceIdSync(registerResponse.ownDeviceId)
logic.database.config().setDeviceAuthTokenSync(registerResponse.deviceAuthToken) logic.database.config().setDeviceAuthTokenSync(registerResponse.deviceAuthToken)
logic.database.config().setConsentFlagSync(
ConsentFlags.BLOCK_USER_SWITCH_BY_DEFAULT,
true
)
ApplyServerDataStatus.applyServerDataStatusSync(clientStatusResponse, logic.database, logic.platformIntegration) ApplyServerDataStatus.applyServerDataStatusSync(clientStatusResponse, logic.database, logic.platformIntegration)
} }
} }

View file

@ -1,23 +1,81 @@
/*
* 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.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.timelimit.android.ui.update package io.timelimit.android.ui.update
import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import io.timelimit.android.R import io.timelimit.android.R
import io.timelimit.android.databinding.UpdateActivityBinding import io.timelimit.android.databinding.UpdateActivityBinding
import io.timelimit.android.logic.DefaultAppLogic import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.ui.ScreenScaffold
import io.timelimit.android.ui.Theme
class UpdateActivity: AppCompatActivity() { class UpdateActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val binding = DataBindingUtil.setContentView<UpdateActivityBinding>(this, R.layout.update_activity) val isNightMode =
(resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) ==
Configuration.UI_MODE_NIGHT_YES
UpdateView.bind( enableEdgeToEdge(
view = binding.update, statusBarStyle = SystemBarStyle.dark(
lifecycleOwner = this, if (isNightMode) android.graphics.Color.TRANSPARENT
fragmentManager = supportFragmentManager, else resources.getColor(R.color.colorPrimaryDark)
appLogic = DefaultAppLogic.with(this) )
) )
setContent {
Theme {
ScreenScaffold(
screen = null,
title = getString(R.string.app_name),
subtitle = null,
backStack = emptyList(),
snackbarHostState = null,
content = { padding ->
AndroidView(
factory = {
val binding = UpdateActivityBinding.inflate(LayoutInflater.from(it))
UpdateView.bind(
view = binding.update,
lifecycleOwner = this,
fragmentManager = supportFragmentManager,
appLogic = DefaultAppLogic.with(this)
)
binding.root
},
modifier = Modifier.fillMaxSize().padding(padding)
)
},
executeCommand = {},
showAuthenticationDialog = null
)
}
}
} }
} }

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -19,6 +19,7 @@ import android.appwidget.AppWidgetManager
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.widget.Toast import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import io.timelimit.android.R import io.timelimit.android.R
@ -30,6 +31,8 @@ class WidgetConfigActivity: FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge()
if (model.state.value == WidgetConfigModel.State.WaitingForInit) { if (model.state.value == WidgetConfigModel.State.WaitingForInit) {
model.init( model.init(
intent?.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID) intent?.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID)

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -70,12 +70,12 @@ object UpdateIntegration {
val signatures = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { val signatures = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// new signature // new signature
context.packageManager.getPackageInfo(context.packageName, PackageManager.GET_SIGNING_CERTIFICATES).signingInfo.apkContentsSigners context.packageManager.getPackageInfo(context.packageName, PackageManager.GET_SIGNING_CERTIFICATES).signingInfo!!.apkContentsSigners
} else { } else {
// old signature // old signature
// this is "unsafe", but it is not used for security features // this is "unsafe", but it is not used for security features
context.packageManager.getPackageInfo(context.packageName, PackageManager.GET_SIGNATURES).signatures context.packageManager.getPackageInfo(context.packageName, PackageManager.GET_SIGNATURES).signatures!!
} }
return signatures.map { HexString.toHex(MessageDigest.getInstance("SHA-256").digest(it.toByteArray())) } return signatures.map { HexString.toHex(MessageDigest.getInstance("SHA-256").digest(it.toByteArray())) }

View file

@ -1,2 +1,2 @@
- Zeitbegrenzungsregeln mit Ablaufzeitpunkt - Funktionsumfang bei Verwendung der Geräte-Besitzer-Berechtigung erweitert
- Vorbereitung für technische Änderungen seitens Let's Encrypt - enthaltene Komponenten aktualisiert

View file

@ -1,2 +1,2 @@
- add support for time limit rules that expire - add more features for users of the device owner permission
- prepare for technical adjustments at Let's Encrypt - update contained software

View file

@ -1,50 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
TimeLimit Copyright <C> 2019 Jonas Lochmann
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".ui.lock.LockActivity" >
<com.google.android.material.tabs.TabLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/tabs"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" />
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.viewpager.widget.ViewPager
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/pager" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
app:fabSize="normal"
android:src="@drawable/ic_lock_open_white_24dp"
android:layout_margin="16dp"
android:layout_gravity="end|bottom"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</LinearLayout>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- <!--
TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann TimeLimit Copyright <C> 2019 - 2025 Jonas Lochmann
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License. the Free Software Foundation version 3 of the License.
@ -197,6 +197,12 @@
<string name="background_logic_opening_lockscreen">Sperrbildschirm wird geöffnet</string> <string name="background_logic_opening_lockscreen">Sperrbildschirm wird geöffnet</string>
<string name="background_logic_permission_sanction_title">Berechtigung fehlt</string> <string name="background_logic_permission_sanction_title">Berechtigung fehlt</string>
<string name="background_logic_permission_sanction_text">nehme an, dass alles verwendet wird</string> <string name="background_logic_permission_sanction_text">nehme an, dass alles verwendet wird</string>
<string name="background_logic_errpr_detailed_instanceid_sorting">Es gibt ein Problem mit der
Nutzungsstatisitkdatenbank Ihres Gerätes.
Deshalb kann TimeLimit nicht zuverlässig erkennen, welche App verwendet wird.
Dieses Problem kann nur behoben werden durch vier Tagen Geduld oder das Zurücksetzen des Gerätes.
Eine Neuinstallation von TimeLimit hilft NICHT.
</string>
<string name="background_logic_temporarily_allowed_title">Apps wurden vorübergehend erlaubt</string> <string name="background_logic_temporarily_allowed_title">Apps wurden vorübergehend erlaubt</string>
<string name="background_logic_temporarily_allowed_text">Zum Rückgängig machen hier tippen oder Bildschirm ausschalten</string> <string name="background_logic_temporarily_allowed_text">Zum Rückgängig machen hier tippen oder Bildschirm ausschalten</string>
@ -1207,6 +1213,9 @@
<string name="notification_channel_new_device_title">neue Geräte</string> <string name="notification_channel_new_device_title">neue Geräte</string>
<string name="notification_channel_new_device_description">Zeigt eine Benachrichtigung an, wenn TimeLimit mit Vernetzung verwendet wird und ein neues Gerät verknüpft wurde</string> <string name="notification_channel_new_device_description">Zeigt eine Benachrichtigung an, wenn TimeLimit mit Vernetzung verwendet wird und ein neues Gerät verknüpft wurde</string>
<string name="notification_channel_extra_time_started_title">Extrazeit beginnt</string>
<string name="notification_channel_extra_time_started_description">Informiert, wenn des reguläre Zeitkontingent verbraucht ist und nun Extrazeit verwendet wird</string>
<string name="notification_filter_not_blocked_title">TimeLimit hat eine Benachrichtigung blockiert</string> <string name="notification_filter_not_blocked_title">TimeLimit hat eine Benachrichtigung blockiert</string>
<string name="notification_filter_blocking_failed_title">TimeLimit konnte eine Benachrichtigung nicht blockieren</string> <string name="notification_filter_blocking_failed_title">TimeLimit konnte eine Benachrichtigung nicht blockieren</string>
@ -1222,6 +1231,8 @@
<string name="notification_new_device_title">Gerät hinzugefügt</string> <string name="notification_new_device_title">Gerät hinzugefügt</string>
<string name="notification_extra_time_started">Extrazeit beginnt</string>
<string name="obsolete_message">Sie verwenden TimeLimit auf einer älteren Android-Version. <string name="obsolete_message">Sie verwenden TimeLimit auf einer älteren Android-Version.
Das kann funktionieren, aber es wird nicht empfohlen. Das kann funktionieren, aber es wird nicht empfohlen.
</string> </string>
@ -1741,6 +1752,10 @@
<string name="dummy_app_unassigned_system_image_app">nicht zugeordnete Apps von der Systempartition</string> <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_adb">Entwickleroptionen</string>
<string name="dummy_app_feature_dns">DNS-Einstellungen</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="dummy_app_activity_audio">Hintergrundmusikwiedergabe</string>
<string name="notify_permission_title">Benachrichtigungen</string> <string name="notify_permission_title">Benachrichtigungen</string>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- <!--
TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann TimeLimit Copyright <C> 2019 - 2025 Jonas Lochmann
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License. the Free Software Foundation version 3 of the License.
@ -240,6 +240,12 @@
<string name="background_logic_opening_lockscreen">Opening lock screen</string> <string name="background_logic_opening_lockscreen">Opening lock screen</string>
<string name="background_logic_permission_sanction_title">Missing permission</string> <string name="background_logic_permission_sanction_title">Missing permission</string>
<string name="background_logic_permission_sanction_text">assuming that everything is used</string> <string name="background_logic_permission_sanction_text">assuming that everything is used</string>
<string name="background_logic_errpr_detailed_instanceid_sorting">There is an issue with
the data in the usage stats database of your device.
Due to that, TimeLimit can not reliable detect the running Apps.
You can do nothing to solve this except waiting for four days or doing a reset of the whole device.
Reinstalling TimeLimit does NOT help.
</string>
<string name="background_logic_temporarily_allowed_title">Apps are temporarily allowed</string> <string name="background_logic_temporarily_allowed_title">Apps are temporarily allowed</string>
<string name="background_logic_temporarily_allowed_text">Tap here or turn screen off to undo</string> <string name="background_logic_temporarily_allowed_text">Tap here or turn screen off to undo</string>
@ -1256,6 +1262,9 @@
<string name="notification_channel_new_device_title">New Device</string> <string name="notification_channel_new_device_title">New Device</string>
<string name="notification_channel_new_device_description">Shows a notification if the connected mode is used and a new device was linked</string> <string name="notification_channel_new_device_description">Shows a notification if the connected mode is used and a new device was linked</string>
<string name="notification_channel_extra_time_started_title">Extra time starts</string>
<string name="notification_channel_extra_time_started_description">Informs when the regular time contingent was consumed and the extra time starts</string>
<string name="notification_filter_not_blocked_title">TimeLimit has blocked a notification</string> <string name="notification_filter_not_blocked_title">TimeLimit has blocked a notification</string>
<string name="notification_filter_blocking_failed_title">TimeLimit could not block a notification</string> <string name="notification_filter_blocking_failed_title">TimeLimit could not block a notification</string>
@ -1271,6 +1280,8 @@
<string name="notification_new_device_title">Device added</string> <string name="notification_new_device_title">Device added</string>
<string name="notification_extra_time_started">Extra time starts</string>
<string name="obsolete_message">You are using TimeLimit at a obsolete Android version. <string name="obsolete_message">You are using TimeLimit at a obsolete Android version.
Although this can work, it is not recommend. Although this can work, it is not recommend.
</string> </string>
@ -1638,7 +1649,7 @@
<a href="https://legal.timelimit.io/en/privacy/">https://legal.timelimit.io/en/privacy/</a> <a href="https://legal.timelimit.io/en/privacy/">https://legal.timelimit.io/en/privacy/</a>
</string> </string>
<string name="terms_gpl" translatable="false"> <string name="terms_gpl" translatable="false">
TimeLimit Copyright &#169; 2019 - 2024 Jonas Lochmann TimeLimit Copyright &#169; 2019 - 2025 Jonas Lochmann
\nThis program is free software: you can redistribute it and/or modify \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 it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License. the Free Software Foundation version 3 of the License.
@ -1793,6 +1804,10 @@
<string name="dummy_app_unassigned_system_image_app">not assigned Apps from the system image</string> <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_adb">Developer Options</string>
<string name="dummy_app_feature_dns">DNS Settings</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="dummy_app_activity_audio">Background Audio Playback</string>
<string name="notify_permission_title">Notifications</string> <string name="notify_permission_title">Notifications</string>

View file

@ -1,5 +1,5 @@
<!-- <!--
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License. the Free Software Foundation version 3 of the License.
@ -15,7 +15,7 @@
<resources> <resources>
<!-- Base application theme. --> <!-- Base application theme. -->
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.DarkActionBar"> <style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorSecondary">@color/colorAccent</item> <item name="colorSecondary">@color/colorAccent</item>
@ -32,6 +32,10 @@
<item name="colorAccent">@color/white</item> <item name="colorAccent">@color/white</item>
</style> </style>
<style name="AppTheme.Translucent" parent="AppTheme">
<item name="android:windowIsFloating">true</item>
</style>
<!-- from https://stackoverflow.com/a/46286184 --> <!-- from https://stackoverflow.com/a/46286184 -->
<style name="BottomSheetDialog" parent="Theme.MaterialComponents.DayNight.BottomSheetDialog"> <style name="BottomSheetDialog" parent="Theme.MaterialComponents.DayNight.BottomSheetDialog">
<item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimary">@color/colorPrimary</item>

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2025 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -42,7 +42,7 @@ object BillingClient {
enum class ProductType { INAPP } enum class ProductType { INAPP }
object Builder { object Builder {
fun enablePendingPurchases() = this fun enablePendingPurchases(params: PendingPurchasesParams) = this
fun setListener(listener: PurchasesUpdatedListener) = this fun setListener(listener: PurchasesUpdatedListener) = this
fun build() = BillingClient fun build() = BillingClient
} }
@ -143,4 +143,13 @@ object QueryPurchasesParams {
fun newBuilder() = this fun newBuilder() = this
fun setProductType(type: BillingClient.ProductType) = this fun setProductType(type: BillingClient.ProductType) = this
fun build() = this fun build() = this
}
object PendingPurchasesParams {
object Builder {
fun enableOneTimeProducts() = this
fun build() = PendingPurchasesParams
}
fun newBuilder() = Builder
} }

View file

@ -1,23 +0,0 @@
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 - 2023 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2025 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -15,10 +15,10 @@
*/ */
plugins { plugins {
id 'com.android.application' version '8.3.1' apply false id 'com.android.application' version '8.11.1' apply false
id 'com.android.library' version '8.3.1' apply false id 'com.android.library' version '8.11.1' apply false
id 'org.jetbrains.kotlin.android' version "1.9.20" apply false id 'org.jetbrains.kotlin.android' version "2.0.21" apply false
id 'com.google.devtools.ksp' version '1.8.21-1.0.11' 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 'androidx.navigation.safeargs' version '2.6.0' apply false
id 'com.squareup.wire' version '4.4.3' apply false id 'com.squareup.wire' version '5.3.5' apply false
} }

View file

@ -7,7 +7,6 @@
# Specifies the JVM arguments used for the daemon process. # Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings. # The setting is particularly useful for tweaking memory settings.
android.useAndroidX=true android.useAndroidX=true
android.defaults.buildfeatures.buildconfig=true
org.gradle.jvmargs=-Xmx4096m org.gradle.jvmargs=-Xmx4096m
# When configured, Gradle will run in incubating parallel mode. # When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit # This option should only be used with decoupled projects. More details, visit

View file

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