Compare commits

...

60 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
81 changed files with 2965 additions and 607 deletions

1
.gitignore vendored
View file

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

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
<?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
it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License.
@ -55,7 +55,9 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
android:theme="@style/AppTheme"
android:enableOnBackInvokedCallback="true"
tools:targetApi="tiramisu">
<!-- UI -->
@ -124,6 +126,14 @@
</intent-filter>
</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 -->
<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
* 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.view.View
import com.jakewharton.threetenabp.AndroidThreeTen
class Application : Application() {
// 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
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
* 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 kotlinx.coroutines.*
@OptIn(DelicateCoroutinesApi::class)
fun <T> runAsync(block: suspend CoroutineScope.() -> T) {
GlobalScope.launch (Dispatchers.Main) {
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
* it under the terms of the GNU General Public License as published by
@ -50,7 +50,7 @@ interface Database {
fun widgetCategory(): WidgetCategoryDao
fun widgetConfig(): WidgetConfigDao
fun <T> runInTransaction(block: Callable<T>): T
fun <T> runInTransaction(body: Callable<T>): T
fun <T> runInUnobservedTransaction(block: () -> T): T
fun registerWeakObserver(tables: Array<Table>, observer: WeakReference<Observer>)
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
* 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 {
private val MIGRATE_TO_V2 = object: Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE device ADD COLUMN did_report_uninstall INTEGER NOT NULL DEFAULT 0")
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE device ADD COLUMN did_report_uninstall INTEGER NOT NULL DEFAULT 0")
}
}
private val MIGRATE_TO_V3 = object: Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE device ADD COLUMN is_user_kept_signed_in INTEGER NOT NULL DEFAULT 0")
override fun migrate(db: SupportSQLiteDatabase) {
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) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `user` ADD COLUMN `category_for_not_assigned_apps` TEXT NOT NULL DEFAULT \"\"")
override fun migrate(db: SupportSQLiteDatabase) {
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) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `category` ADD COLUMN `parent_category_id` TEXT NOT NULL DEFAULT \"\"")
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE `category` ADD COLUMN `parent_category_id` TEXT NOT NULL DEFAULT \"\"")
}
}
private val MIGRATE_TO_V6 = object: Migration(5, 6) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `device` ADD COLUMN `show_device_connected` INTEGER NOT NULL DEFAULT 0")
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE `device` ADD COLUMN `show_device_connected` INTEGER NOT NULL DEFAULT 0")
}
}
private val MIGRATE_TO_V7 = object: Migration(6, 7) {
override fun migrate(database: SupportSQLiteDatabase) {
database.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")
database.execSQL("ALTER TABLE `user` ADD COLUMN `relax_primary_device` INTEGER NOT NULL DEFAULT 0")
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE `device` ADD COLUMN `default_user` TEXT NOT NULL DEFAULT \"\"")
db.execSQL("ALTER TABLE `device` ADD COLUMN `default_user_timeout` 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) {
override fun migrate(database: SupportSQLiteDatabase) {
override fun migrate(db: SupportSQLiteDatabase) {
// this is empty
//
// 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) {
override fun migrate(database: SupportSQLiteDatabase) {
database.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")
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE `device` ADD COLUMN `did_reboot` 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) {
override fun migrate(database: SupportSQLiteDatabase) {
override fun migrate(db: SupportSQLiteDatabase) {
// this is empty
//
// 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) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `user` ADD COLUMN `mail_notification_flags` INTEGER NOT NULL DEFAULT 0")
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE `user` ADD COLUMN `mail_notification_flags` INTEGER NOT NULL DEFAULT 0")
}
}
private val MIGRATE_TO_V12 = object: Migration(11, 12) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `category` ADD COLUMN `block_all_notifications` INTEGER NOT NULL DEFAULT 0")
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE `category` ADD COLUMN `block_all_notifications` INTEGER NOT NULL DEFAULT 0")
}
}
private val MIGRATE_TO_V13 = object: Migration(12, 13) {
override fun migrate(database: SupportSQLiteDatabase) {
database.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\"")
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE `device` ADD COLUMN `current_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) {
override fun migrate(database: SupportSQLiteDatabase) {
database.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")
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE `device` ADD COLUMN `current_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) {
override fun migrate(database: 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`))")
database.execSQL("ALTER TABLE `device` ADD COLUMN `enable_activity_level_blocking` INTEGER NOT NULL DEFAULT 0")
override fun migrate(db: SupportSQLiteDatabase) {
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`))")
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) {
override fun migrate(database: SupportSQLiteDatabase) {
override fun migrate(db: SupportSQLiteDatabase) {
// this is empty
//
// 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) {
override fun migrate(database: 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`))")
override fun migrate(db: SupportSQLiteDatabase) {
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) {
override fun migrate(database: SupportSQLiteDatabase) {
database.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")
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE `device` ADD COLUMN `q_or_later` 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) {
override fun migrate(database: SupportSQLiteDatabase) {
override fun migrate(db: SupportSQLiteDatabase) {
// this is empty
//
// 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) {
override fun migrate(database: 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)")
override fun migrate(db: SupportSQLiteDatabase) {
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) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `device` ADD COLUMN `had_manipulation_flags` INTEGER NOT NULL DEFAULT 0")
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE `device` ADD COLUMN `had_manipulation_flags` INTEGER NOT NULL DEFAULT 0")
}
}
private val MIGRATE_TO_V22 = object: Migration(21, 22) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `user` ADD COLUMN `blocked_times` TEXT NOT NULL DEFAULT \"\"")
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE `user` ADD COLUMN `blocked_times` TEXT NOT NULL DEFAULT \"\"")
}
}
private val MIGRATE_TO_V23 = object: Migration(22, 23) {
override fun migrate(database: SupportSQLiteDatabase) {
database.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")
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE `category` ADD COLUMN `min_battery_charging` 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) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `category` ADD COLUMN `temporarily_blocked_end_time` INTEGER NOT NULL DEFAULT 0")
override fun migrate(db: SupportSQLiteDatabase) {
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) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `category` ADD COLUMN `sort` INTEGER NOT NULL DEFAULT 0")
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE `category` ADD COLUMN `sort` INTEGER NOT NULL DEFAULT 0")
}
}
private val MIGRATE_TO_V26 = object: Migration(25, 26) {
override fun migrate(database: SupportSQLiteDatabase) {
override fun migrate(db: SupportSQLiteDatabase) {
// this is empty
//
// 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) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `category` ADD COLUMN `extra_time_day` INTEGER NOT NULL DEFAULT -1")
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE `category` ADD COLUMN `extra_time_day` INTEGER NOT NULL DEFAULT -1")
}
}
private val MIGRATE_TO_V28 = object: Migration(27, 28) {
override fun migrate(database: 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 )")
database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_user_key_key` ON `user_key` (`key`)")
override fun migrate(db: SupportSQLiteDatabase) {
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 )")
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) {
override fun migrate(database: SupportSQLiteDatabase) {
database.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}")
database.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")
override fun migrate(db: SupportSQLiteDatabase) {
db.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 `end_minute_of_day` INTEGER NOT NULL DEFAULT ${TimeLimitRule.MAX_END_MINUTE}")
db.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_pause_milliseconds` INTEGER NOT NULL DEFAULT 0")
database.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`))")
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`")
database.execSQL("DROP TABLE `used_time_old`")
db.execSQL("ALTER TABLE `used_time` RENAME TO `used_time_old`")
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`))")
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`")
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 )")
database.execSQL("CREATE INDEX IF NOT EXISTS `session_duration_index_category_id` ON `session_duration` (`category_id`)")
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 )")
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) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `user` ADD COLUMN `flags` INTEGER NOT NULL DEFAULT 0")
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE `user` ADD COLUMN `flags` INTEGER NOT NULL DEFAULT 0")
}
}
private val MIGRATE_TO_V31 = object: Migration(30, 31) {
override fun migrate(database: 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 )")
database.execSQL("CREATE INDEX IF NOT EXISTS `user_limit_login_category_index_category_id` ON `user_limit_login_category` (`category_id`)")
override fun migrate(db: SupportSQLiteDatabase) {
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 )")
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) {
override fun migrate(database: 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 )")
override fun migrate(db: SupportSQLiteDatabase) {
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) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `category` ADD COLUMN `disable_limits_until` INTEGER NOT NULL DEFAULT 0")
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE `category` ADD COLUMN `disable_limits_until` INTEGER NOT NULL DEFAULT 0")
}
}
private val MIGRATE_TO_V34 = object: Migration(33, 34) {
override fun migrate(database: 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 )")
database.execSQL("ALTER TABLE `category` ADD COLUMN `tasks_version` TEXT NOT NULL DEFAULT ''")
override fun migrate(db: SupportSQLiteDatabase) {
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 )")
db.execSQL("ALTER TABLE `category` ADD COLUMN `tasks_version` TEXT NOT NULL DEFAULT ''")
}
}
private val MIGRATE_TO_V35 = object: Migration(34, 35) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `time_limit_rule` ADD COLUMN `per_day` INTEGER NOT NULL DEFAULT 0")
override fun migrate(db: SupportSQLiteDatabase) {
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) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `user_limit_login_category` ADD COLUMN pre_block_duration INTEGER NOT NULL DEFAULT 0")
override fun migrate(db: SupportSQLiteDatabase) {
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) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `category` ADD COLUMN `flags` INTEGER NOT NULL DEFAULT 0")
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE `category` ADD COLUMN `flags` INTEGER NOT NULL DEFAULT 0")
}
}
private val MIGRATE_TO_V38 = object: Migration(37, 38) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE category ADD COLUMN block_notification_delay INTEGER NOT NULL DEFAULT 0")
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE category ADD COLUMN block_notification_delay INTEGER NOT NULL DEFAULT 0")
}
}
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
}
}
private val MIGRATE_TO_V40 = object: Migration(39, 40) {
override fun migrate(database: 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 )")
override fun migrate(db: SupportSQLiteDatabase) {
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) {
override fun migrate(database: SupportSQLiteDatabase) {
override fun migrate(db: SupportSQLiteDatabase) {
// nothing to do, there was just a new config item type added
}
}
private val MIGRATE_TO_V42 = object: Migration(41, 42) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE device ADD COLUMN manipulation_flags INTEGER NOT NULL DEFAULT 0")
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE device ADD COLUMN manipulation_flags INTEGER NOT NULL DEFAULT 0")
}
}
private val MIGRATE_TP_V43 = object: Migration(42, 43) {
override fun migrate(database: 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 )")
database.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`)")
override fun migrate(db: SupportSQLiteDatabase) {
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 )")
db.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_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 )")
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 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 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 )")
database.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 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 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_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) {
override fun migrate(database: 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 )")
database.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`)")
override fun migrate(db: SupportSQLiteDatabase) {
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 )")
db.execSQL("CREATE INDEX IF NOT EXISTS `index_user_u2f_key_user_id` ON `user_u2f_key` (`user_id`)")
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) {
override fun migrate(database: 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 )")
database.execSQL("CREATE INDEX IF NOT EXISTS `index_widget_category_category_id` ON `widget_category` (`category_id`)")
override fun migrate(db: SupportSQLiteDatabase) {
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 )")
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) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `widget_config` (`widget_id` INTEGER NOT NULL, `translucent` INTEGER NOT NULL, PRIMARY KEY(`widget_id`))")
override fun migrate(db: SupportSQLiteDatabase) {
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,
WidgetCategory::class,
WidgetConfig::class
], version = 48, autoMigrations = [
], version = 49, autoMigrations = [
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 {
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")
override fun 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
* 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.Insert
import androidx.room.Query
import androidx.room.RoomWarnings
import io.timelimit.android.data.model.UsedTimeItem
import io.timelimit.android.data.model.UsedTimeListItem
import io.timelimit.android.livedata.ignoreUnchanged
@ -67,10 +68,14 @@ abstract class UsedTimeDao {
abstract fun getAllUsedTimeItemsSync(): List<UsedTimeItem>
// 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")
abstract fun getUsedTimeListItemsByCategoryId(categoryId: String): Flow<List<UsedTimeListItem>>
// breaking it into multiple lines causes issues during compilation ...
// this warns about an unused column, but this column is used in the ORDER BY
@SuppressWarnings(RoomWarnings.QUERY_MISMATCH)
@Query("SELECT 2 AS type, start_time_of_day AS startMinuteOfDay, end_time_of_day AS endMinuteOfDay, used_time AS duration, day_of_epoch AS day, NULL AS lastUsage, NULL AS maxSessionDuration, NULL AS pauseDuration, category.id AS categoryId, category.title AS categoryTitle FROM used_time JOIN category ON (used_time.category_id = category.id) WHERE category.child_id = :userId UNION ALL SELECT 1 AS type, start_minute_of_day AS startMinuteOfDay, end_minute_of_day AS endMinuteOfDay, last_session_duration AS duration, NULL AS day, last_usage AS lastUsage, max_session_duration AS maxSessionDuration, session_pause_duration AS pauseDuration, category.id AS categoryId, category.title AS categoryTitle FROM session_duration JOIN category ON (session_duration.category_id = category.id) WHERE category.child_id = :userId ORDER BY type, day DESC, lastUsage DESC, startMinuteOfDay, endMinuteOfDay, categoryId")
abstract fun getUsedTimeListItemsByUserId(userId: String): Flow<List<UsedTimeListItem>>
}

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
* TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -41,7 +41,7 @@ data class ChildTask(
@PrimaryKey
@ColumnInfo(name = "task_id")
val taskId: String,
@ColumnInfo(name = "category_id")
@ColumnInfo(name = "category_id", index = true)
val categoryId: String,
@ColumnInfo(name = "task_title")
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
* it under the terms of the GNU General Public License as published by
@ -295,4 +295,7 @@ object ExperimentalFlags {
object ConsentFlags {
const val APP_LIST_SYNC = 1L
// this is used internally
const val BLOCK_USER_SWITCH_BY_DEFAULT = 2L
}

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
* it under the terms of the GNU General Public License as published by
@ -73,4 +73,5 @@ data class DeviceRelatedData (
}
fun isExperimentalFlagSetSync(flags: Long) = (experimentalFlags and flags) == flags
fun isConsentFlagSet(flags: Long) = (consentFlags and flags) == flags
}

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
* it under the terms of the GNU General Public License as published by
@ -15,8 +15,8 @@
*/
package io.timelimit.android.date
import org.threeten.bp.DayOfWeek
import org.threeten.bp.LocalDate
import java.time.DayOfWeek
import java.time.LocalDate
import java.util.*
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
* it under the terms of the GNU General Public License as published by
@ -15,7 +15,7 @@
*/
package io.timelimit.android.extensions
import org.threeten.bp.LocalDateTime
import org.threeten.bp.ZoneId
import java.time.LocalDateTime
import java.time.ZoneId
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
* it under the terms of the GNU General Public License as published by
@ -25,4 +25,7 @@ interface DeviceOwnerApi {
fun setOrganizationName(name: String)
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
* 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 showRemoteResetNotification()
abstract fun showTimeWarningNotification(title: String, text: String)
abstract fun showExtraTimeStartedNotification(categoryId: String, categoryTitle: String)
// returns package names for which it was set
abstract fun setSuspendedApps(packageNames: List<String>, suspend: Boolean): List<String>
abstract fun stopSuspendingForAllApps()
@ -218,7 +219,8 @@ data class AppStatusMessage(
val title: String,
val text: String,
val subtext: String? = null,
val showSwitchToDefaultUserOption: Boolean = false
val showSwitchToDefaultUserOption: Boolean = false,
val showErrorMessage: Boolean = false
): Parcelable
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
* it under the terms of the GNU General Public License as published by
@ -15,6 +15,7 @@
*/
package io.timelimit.android.integration.platform.android
import android.Manifest
import android.app.admin.DeviceAdminReceiver
import android.app.admin.DevicePolicyManager
import android.content.ComponentName
@ -70,6 +71,7 @@ class AndroidDeviceOwnerApi(
override val delegations: List<DeviceOwnerApi.DelegationScope> = delegationList.map { it.second }
override fun setDelegations(packageName: String, scopes: List<DeviceOwnerApi.DelegationScope>) {
if (BuildConfig.storeCompilant) throw IllegalStateException()
if (VERSION.SDK_INT <= VERSION_CODES.O) throw IllegalStateException()
val resolvedScopes = scopes.map { scope ->
@ -88,6 +90,7 @@ class AndroidDeviceOwnerApi(
}
override fun getDelegations(): Map<String, List<DeviceOwnerApi.DelegationScope>> {
if (BuildConfig.storeCompilant) return emptyMap()
if (VERSION.SDK_INT <= VERSION_CODES.O) throw IllegalStateException()
return delegationList.map { (scope, delegation) ->
@ -108,6 +111,7 @@ class AndroidDeviceOwnerApi(
}
override fun transferOwnership(packageName: String, dryRun: Boolean) {
if (BuildConfig.storeCompilant) throw IllegalStateException()
if (VERSION.SDK_INT < VERSION_CODES.P) throw IllegalStateException()
if (!devicePolicyManager.isDeviceOwnerApp(componentName.packageName)) throw SecurityException()
@ -124,4 +128,25 @@ class AndroidDeviceOwnerApi(
devicePolicyManager.setDelegatedScopes(componentName, packageName, emptyList())
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
* it under the terms of the GNU General Public License as published by
@ -27,6 +27,10 @@ import io.timelimit.android.integration.platform.PlatformFeature
object AndroidFeatures {
private const val FEATURE_ADB = "adb"
private const val FEATURE_CONFIG_PRIVATE_DNS = "dns"
const val FEATURE_ADD_USER = "add_user"
const val FEATURE_USER_SWITCH = "user_switch"
private const val FEATURE_VPN = "vpn"
private const val FEATURE_UNKNOWN_SOURCES = "unknown_sources"
fun applyBlockedFeatures(features: Set<String>, policyManager: DevicePolicyManager, admin: ComponentName): Boolean {
fun apply(feature: String, restriction: String) {
@ -40,6 +44,18 @@ object AndroidFeatures {
apply(FEATURE_CONFIG_PRIVATE_DNS, UserManager.DISALLOW_CONFIG_PRIVATE_DNS)
}
apply(FEATURE_ADD_USER, UserManager.DISALLOW_ADD_USER)
if (VERSION.SDK_INT >= VERSION_CODES.P) {
apply(FEATURE_USER_SWITCH, UserManager.DISALLOW_USER_SWITCH)
}
apply(FEATURE_VPN, UserManager.DISALLOW_CONFIG_VPN)
if (VERSION.SDK_INT >= VERSION_CODES.Q) {
apply(FEATURE_UNKNOWN_SOURCES, UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY)
}
return true
}
@ -60,6 +76,30 @@ object AndroidFeatures {
)
}
result.add(PlatformFeature(
id = FEATURE_ADD_USER,
title = context.getString(R.string.dummy_app_feature_add_user)
))
if (VERSION.SDK_INT >= VERSION_CODES.P) {
result.add(PlatformFeature(
id = FEATURE_USER_SWITCH,
title = context.getString(R.string.dummy_app_feature_switch_user)
))
}
result.add(PlatformFeature(
id = FEATURE_VPN,
title = context.getString(R.string.dummy_app_feature_vpn)
))
if (VERSION.SDK_INT >= VERSION_CODES.Q) {
result.add(PlatformFeature(
id = FEATURE_UNKNOWN_SOURCES,
title = context.getString(R.string.dummy_app_feature_unknown_sources)
))
}
return result
}
}

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
* it under the terms of the GNU General Public License as published by
@ -15,6 +15,7 @@
*/
package io.timelimit.android.integration.platform.android
import android.Manifest
import android.annotation.TargetApi
import android.app.ActivityManager
import android.app.Application
@ -40,6 +41,7 @@ import android.widget.Toast
import androidx.collection.LruCache
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.LiveData
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() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (policyManager.isDeviceOwnerApp(context.packageName)) {
@ -485,21 +507,84 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio
if (enableLockdown) {
// disable problematic features
policyManager.addUserRestriction(deviceAdmin, UserManager.DISALLOW_ADD_USER)
policyManager.addUserRestriction(deviceAdmin, UserManager.DISALLOW_FACTORY_RESET)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
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 */ {
// enable problematic features
policyManager.clearUserRestriction(deviceAdmin, UserManager.DISALLOW_ADD_USER)
policyManager.clearUserRestriction(deviceAdmin, UserManager.DISALLOW_FACTORY_RESET)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
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()
stopSuspendingForAllApps()
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
* 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.apply.ApplyActionUtil
import io.timelimit.android.ui.MainActivity
import io.timelimit.android.ui.diagnose.exception.DiagnoseExceptionActivity
import io.timelimit.android.ui.notification.NotificationAreaSync
class BackgroundActionService: Service() {
@ -44,7 +45,7 @@ class BackgroundActionService: Service() {
fun prepareRevokeTemporarilyAllowed(context: Context) = Intent(context, BackgroundActionService::class.java)
.putExtra(ACTION, ACTION_REVOKE_TEMPORARILY_ALLOWED_APPS)
fun getSwitchToDefaultUserIntent(context: Context) = PendingIntent.getService(
fun getSwitchToDefaultUserIntent(context: Context): PendingIntent = PendingIntent.getService(
context,
PendingIntentIds.SWITCH_TO_DEFAULT_USER,
Intent(context, BackgroundActionService::class.java)
@ -57,14 +58,24 @@ class BackgroundActionService: Service() {
.putExtra(EXTRA_NOTIFICATION_TYPE, type)
.putExtra(EXTRA_NOTIFICATION_ID, id)
fun getOpenAppIntent(context: Context) = PendingIntent.getActivity(
fun getOpenAppIntent(context: Context): PendingIntent = PendingIntent.getActivity(
context,
PendingIntentIds.OPEN_MAIN_APP,
Intent(context, MainActivity::class.java),
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,
PendingIntentIds.SYNC_NOTIFICATIONS,
Intent(context, BackgroundActionService::class.java)

View file

@ -86,7 +86,12 @@ class BackgroundService: Service() {
.setContentTitle(appStatusMessage.title)
.setContentText(appStatusMessage.text)
.setSubText(appStatusMessage.subtext)
.setContentIntent(BackgroundActionService.getOpenAppIntent(context))
.setContentIntent(
if (appStatusMessage.showErrorMessage)
BackgroundActionService.getOpenAppWithErrorIntent(context)
else
BackgroundActionService.getOpenAppIntent(context)
)
.setWhen(0)
.setShowWhen(false)
.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
* 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_SYNC_BACKGROUND = 9
const val NEW_DEVICE = 10
const val EXTRA_TIME_STARTED = 11
}
object NotificationChannels {
@ -47,6 +48,7 @@ object NotificationChannels {
const val TEMP_ALLOWED_APP = "temporarily allowed App"
const val APP_RESET = "app reset"
const val NEW_DEVICE = "new device"
const val EXTRA_TIME_STARTED = "extra time started"
private fun createAppStatusChannel(notificationManager: NotificationManager, context: Context) {
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) {
createAppStatusChannel(notificationManager, context)
createBlockedNotificationChannel(notificationManager, context)
@ -225,6 +241,7 @@ object NotificationChannels {
createTempAllowedAppChannel(notificationManager, context)
createAppResetChannel(notificationManager, context)
createNewDeviceChannel(notificationManager, context)
createExtraTimeStartedNotificationChannel(notificationManager, context)
}
}
@ -237,6 +254,7 @@ object PendingIntentIds {
const val OPEN_UPDATER = 6
const val U2F_NFC_DISCOVERY = 7
const val U2F_USB_RESPONSE = 8
const val OPEN_MAIN_APP_WITH_ERROR = 9
val DYNAMIC_NOTIFICATION_RANGE = 100..10000
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
* 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 {
packageManager.getPackageInfo(app.packageName, PackageManager.GET_ACTIVITIES).activities.find {
packageManager.getPackageInfo(app.packageName, PackageManager.GET_ACTIVITIES).activities?.find {
it.enabled && it.targetActivity == app.activityName
} != null
} 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
* it under the terms of the GNU General Public License as published by
@ -153,6 +153,10 @@ class DummyIntegration(
// nothing to do
}
override fun showExtraTimeStartedNotification(categoryId: String, categoryTitle: String) {
// nothing to do
}
override fun disableDeviceAdmin() {
// nothing to do
}
@ -203,5 +207,7 @@ class DummyIntegration(
override fun setOrganizationName(name: String) = throw SecurityException()
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
* it under the terms of the GNU General Public License as published by
@ -65,6 +65,11 @@ class AppSetupLogic(private val appLogic: AppLogic) {
appLogic.database.deleteAllData()
appLogic.database.config().setCustomServerUrlSync(customServerUrl)
appLogic.database.config().setConsentFlagSync(
ConsentFlags.BLOCK_USER_SWITCH_BY_DEFAULT,
true
)
}
run {

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
* 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 newRemainingTime = oldRemainingTime - timeToSubtract
val oldRemainingNonExtraTime = nowRemaining.default - timeToSubtractForCategory
val newRemainingNonExtraTime = oldRemainingNonExtraTime - timeToSubtract
val commitedSessionDuration = handling.remainingSessionDuration
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) {
val newSessionDuration = oldSessionDuration - timeToSubtract
@ -897,7 +904,8 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
appLogic.context.getString(R.string.background_logic_error),
appLogic.context.getString(R.string.background_logic_error_internal),
showSwitchToDefaultUserOption = deviceRelatedData.canSwitchToDefaultUser
showSwitchToDefaultUserOption = deviceRelatedData.canSwitchToDefaultUser,
showErrorMessage = true
))
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
* it under the terms of the GNU General Public License as published by
@ -19,10 +19,12 @@ import io.timelimit.android.async.Threads
import io.timelimit.android.data.invalidation.Observer
import io.timelimit.android.data.invalidation.Table
import io.timelimit.android.data.model.CategoryApp
import io.timelimit.android.data.model.ConsentFlags
import io.timelimit.android.data.model.ExperimentalFlags
import io.timelimit.android.data.model.UserType
import io.timelimit.android.data.model.derived.UserRelatedData
import io.timelimit.android.integration.platform.ProtectionLevel
import io.timelimit.android.integration.platform.android.AndroidFeatures
import io.timelimit.android.integration.platform.android.AndroidIntegrationApps
import io.timelimit.android.logic.blockingreason.CategoryHandlingCache
import java.lang.ref.WeakReference
@ -107,12 +109,23 @@ class SuspendAppsLogic(private val appLogic: AppLogic): Observer {
val hasManagedFeatures = featureCategoryApps.isNotEmpty()
val enableBlocking = isRestrictedUser && (enableBlockingAtSystemLevel || hasManagedFeatures)
val blockUserSwitchByDefault =
userAndDeviceRelatedData?.deviceRelatedData?.isConsentFlagSet(ConsentFlags.BLOCK_USER_SWITCH_BY_DEFAULT) == true
&& userAndDeviceRelatedData.userRelatedData?.user?.type == UserType.Child
val featureToAllowDefaults = mapOf(
AndroidFeatures.FEATURE_ADD_USER to false,
AndroidFeatures.FEATURE_USER_SWITCH to !blockUserSwitchByDefault
)
if (!enableBlocking) {
lastDefaultCategory = null
lastAllowedCategoryList = emptySet()
lastCategoryApps = emptyList()
applySuspendedApps(emptyList())
applyBlockedFeatures(emptySet())
applyBlockedFeatures(
featureToAllowDefaults.filter { !it.value }.map { it.key }.toSet()
)
return
}
@ -191,9 +204,15 @@ class SuspendAppsLogic(private val appLogic: AppLogic): Observer {
val deviceSpecificFeatureIdentifiers = deviceSpecificFeatures.map { it.appSpecifierString }.toSet()
val globalFeatures = featureCategoryApps.filter { !deviceSpecificFeatureIdentifiers.contains(it.appSpecifierString) }
val effectiveFeatures = deviceSpecificFeatures + globalFeatures
val featuresToBlock = effectiveFeatures.filter { !categoryIdsToAllow.contains(it.categoryId) }
.map { it.appSpecifierString.substring(DummyApps.FEATURE_APP_PREFIX.length) }
.toSet()
val featuresToAllow = featureToAllowDefaults + effectiveFeatures.associate {
Pair(
it.appSpecifierString.substring(DummyApps.FEATURE_APP_PREFIX.length),
categoryIdsToAllow.contains(it.categoryId)
)
}
val featuresToBlock = featuresToAllow.filter { !it.value }.map { it.key }.toSet()
applySuspendedApps(appsToBlock)
applyBlockedFeatures(featuresToBlock)

View file

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

View file

@ -43,6 +43,7 @@ import java.io.OutputStreamWriter
import java.security.KeyPairGenerator
import java.security.KeyStore
import java.security.KeyStore.PrivateKeyEntry
import java.security.ProviderException
import java.security.cert.X509Certificate
import java.security.spec.ECGenParameterSpec
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 ->
postJsonRequest(
"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.beginObject()
writer.name(MAIL).value(mail)
@ -658,10 +663,17 @@ class HttpServerApi(private val endpointWithoutSlashAtEnd: String): ServerApi {
private suspend fun postJsonRequest(
path: String,
client: OkHttpClient = httpClient,
requestBody: (writer: JsonWriter) -> Unit
transformRequest: (Request.Builder) -> Request.Builder = { it },
requestBody: (writer: JsonWriter) -> Unit,
): Response {
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
@ -670,14 +682,21 @@ class HttpServerApi(private val endpointWithoutSlashAtEnd: String): ServerApi {
sendContentLength = true
}
return postJsonRequest(path, requestBody, transmitContentLength = true, client = client)
return postJsonRequest(
path,
requestBody,
transmitContentLength = true,
client = client,
transformRequest = transformRequest
)
}
private suspend fun postJsonRequest(
path: String,
requestBody: (writer: JsonWriter) -> Unit,
transmitContentLength: Boolean,
client: OkHttpClient = httpClient
client: OkHttpClient = httpClient,
transformRequest: (Request.Builder) -> Request.Builder = { it }
): Response {
val body = createJsonRequestBody(requestBody, transmitContentLength)
@ -686,6 +705,7 @@ class HttpServerApi(private val endpointWithoutSlashAtEnd: String): ServerApi {
.url("$endpointWithoutSlashAtEnd/$path")
.post(body)
.header("Content-Encoding", "gzip")
.let { transformRequest(it) }
.build()
).waitForResponse()
}
@ -697,28 +717,34 @@ class HttpServerApi(private val endpointWithoutSlashAtEnd: String): ServerApi {
val keyId = "temp-" + UUID.randomUUID().toString()
val now = getTimeInMillis()
KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, keyStoreName)
.also {
it.initialize(
KeyGenParameterSpec.Builder(
keyId,
KeyProperties.PURPOSE_SIGN
try {
KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, keyStoreName)
.also {
it.initialize(
KeyGenParameterSpec.Builder(
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(
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()
)
}.genKeyPair()
}.genKeyPair()
} catch (ex: ProviderException) {
// java.security.ProviderException: Failed to generate attestation certificate chain
return block(httpClient)
}
try {
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
* it under the terms of the GNU General Public License as published by
@ -64,10 +64,10 @@ object U2FResponse {
val flags = rawResponse.payload[0]
val counter = rawResponse.payload[4].toUInt() or
rawResponse.payload[3].toUInt().shl(8) or
rawResponse.payload[2].toUInt().shl(16) or
rawResponse.payload[1].toUInt().shl(24)
val counter = rawResponse.payload[4].toUByte().toUInt() or
rawResponse.payload[3].toUByte().toUInt().shl(8) or
rawResponse.payload[2].toUByte().toUInt().shl(16) or
rawResponse.payload[1].toUByte().toUInt().shl(24)
val signature = rawResponse.payload.sliceArray(5 until rawResponse.payload.size)

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
* 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.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.SystemClock
import android.provider.Settings
import android.util.Log
import androidx.activity.SystemBarStyle
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
@ -129,11 +130,19 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
if (granted) mainModel.reportPermissionsChanged()
}
@OptIn(ExperimentalAnimationApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
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)
@ -314,9 +323,8 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
screen = screen,
fragmentManager = supportFragmentManager,
fragmentIds = mainModel.fragmentIds,
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
modifier = Modifier.fillMaxSize(),
paddingValues = paddingValues
)
},
showAuthenticationDialog = showAuthenticationDialog,
@ -383,10 +391,10 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
return false
}
override fun onNewIntent(intent: Intent?) {
override fun onNewIntent(intent: 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
}

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
* it under the terms of the GNU General Public License as published by
@ -15,6 +15,8 @@
*/
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.ui.Modifier
import androidx.fragment.app.FragmentManager
@ -42,27 +44,28 @@ fun ScreenMultiplexer(
screen: Screen?,
fragmentManager: FragmentManager,
fragmentIds: MutableSet<Int>,
modifier: Modifier = Modifier
modifier: Modifier = Modifier,
paddingValues: PaddingValues
) {
when (screen) {
null -> {/* nothing to do */ }
is Screen.FragmentScreen -> FragmentScreen(screen, fragmentManager, fragmentIds, modifier = modifier)
is Screen.OverviewScreen -> OverviewScreen(screen.content, modifier = modifier)
is Screen.ManageDeviceUserScreen -> ManageDeviceUserScreen(screen.items, screen.actions, screen.overlay, modifier)
is Screen.DeviceOwnerScreen -> DeviceOwnerScreen(screen.content, modifier = modifier)
is Screen.SetupDevicePermissionsScreen -> SetupDevicePermissionsScreen(screen, modifier)
is Screen.ManageDevicePermissions -> ManageDevicePermissionScreen(screen.content, modifier)
is Screen.SetupConnectModePrivacyScreen -> SetupConnectedModePrivacyScreen(screen.customServerDomain, screen.accept, modifier)
is Screen.SetupSelectConnectedModeScreen -> SelectConnectedModeScreen(mailLogin = screen.mailLogin, codeLogin = screen.codeLogin, modifier = modifier)
is Screen.SetupSelectModeScreen -> SelectModeScreen(selectLocal = screen.selectLocal, selectConnected = screen.selectConnected, selectUninstall = screen.selectUninstall, modifier = modifier)
is Screen.DeleteRegistration -> DeleteRegistrationScreen(screen.content, modifier)
is Screen.ManageBlockedTimes -> BlockedTimesScreen(screen.content, screen.intro, modifier)
is Screen.ChildUsageHistory -> UsageHistoryScreen(screen.content, modifier)
is Screen.SetupParentMailAuthentication -> AuthenticateByMailScreen(screen.content, modifier)
is Screen.SignupBlocked -> SignupBlockedScreen(modifier)
is Screen.SignInWrongMailAddress -> SignInWrongMailAddress(modifier)
is Screen.ConfirmNewParentAccount -> ConfirmNewParentAccount(confirm = screen.confirm, reject = screen.reject, modifier = modifier)
is Screen.ParentBaseConfiguration -> ParentBaseConfiguration(content = screen.content, modifier = modifier)
is Screen.ParentSetupConsent -> ParentSetupConsent(content = screen.content, errorDialog = screen.errorDialog, modifier = modifier)
is Screen.FragmentScreen -> FragmentScreen(screen, fragmentManager, fragmentIds, modifier = modifier.padding(paddingValues))
is Screen.OverviewScreen -> OverviewScreen(screen.content, modifier = modifier, paddingValues = paddingValues)
is Screen.ManageDeviceUserScreen -> ManageDeviceUserScreen(screen.items, screen.actions, screen.overlay, modifier.padding(paddingValues))
is Screen.DeviceOwnerScreen -> DeviceOwnerScreen(screen.content, modifier = modifier.padding(paddingValues))
is Screen.SetupDevicePermissionsScreen -> SetupDevicePermissionsScreen(screen, modifier.padding(paddingValues))
is Screen.ManageDevicePermissions -> ManageDevicePermissionScreen(screen.content, modifier.padding(paddingValues))
is Screen.SetupConnectModePrivacyScreen -> SetupConnectedModePrivacyScreen(screen.customServerDomain, screen.accept, modifier.padding(paddingValues))
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.padding(paddingValues))
is Screen.DeleteRegistration -> DeleteRegistrationScreen(screen.content, modifier.padding(paddingValues))
is Screen.ManageBlockedTimes -> BlockedTimesScreen(screen.content, screen.intro, modifier.padding(paddingValues))
is Screen.ChildUsageHistory -> UsageHistoryScreen(screen.content, modifier.padding(paddingValues))
is Screen.SetupParentMailAuthentication -> AuthenticateByMailScreen(screen.content, modifier.padding(paddingValues))
is Screen.SignupBlocked -> SignupBlockedScreen(modifier.padding(paddingValues))
is Screen.SignInWrongMailAddress -> SignInWrongMailAddress(modifier.padding(paddingValues))
is Screen.ConfirmNewParentAccount -> ConfirmNewParentAccount(confirm = screen.confirm, reject = screen.reject, modifier = modifier.padding(paddingValues))
is Screen.ParentBaseConfiguration -> ParentBaseConfiguration(content = screen.content, modifier = modifier.padding(paddingValues))
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
* 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.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.PaddingValues
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.material.*
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.UpdateStateCommand
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun ScreenScaffold(
screen: Screen?,
@ -45,6 +51,7 @@ fun ScreenScaffold(
backStack: List<BackStackItem>,
snackbarHostState: SnackbarHostState?,
content: @Composable (PaddingValues) -> Unit,
extraBars: (@Composable () -> Unit)? = null,
executeCommand: (UpdateStateCommand) -> Unit,
showAuthenticationDialog: (() -> Unit)?
) {
@ -52,67 +59,73 @@ fun ScreenScaffold(
Scaffold(
topBar = {
TopAppBar(
title = {
Column {
Text(
title,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
if (subtitle != null) {
Column {
TopAppBar(
title = {
Column {
Text(
subtitle,
style = MaterialTheme.typography.subtitle1,
title,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
},
navigationIcon = if (screen?.state?.previous != null) ({
IconButton(onClick = { executeCommand(UpdateStateCommand.BackToPreviousScreen) }) {
Icon(Icons.Default.ArrowBack, stringResource(R.string.generic_back))
}
}) else null,
actions = {
for (icon in screen?.toolbarIcons ?: emptyList()) {
IconButton(
onClick = {
if (icon.action != null) executeCommand(icon.action)
icon.handler()
if (subtitle != null) {
Text(
subtitle,
style = MaterialTheme.typography.subtitle1,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
) {
Icon(icon.icon, stringResource(icon.labelResource))
}
}
},
navigationIcon = if (screen?.state?.previous != null) ({
IconButton(onClick = { executeCommand(UpdateStateCommand.BackToPreviousScreen) }) {
Icon(Icons.Default.ArrowBack, stringResource(R.string.generic_back))
}
}) else null,
actions = {
for (icon in screen?.toolbarIcons ?: emptyList()) {
IconButton(
onClick = {
if (icon.action != null) executeCommand(icon.action)
if (screen?.toolbarOptions?.isEmpty() == false) {
IconButton(onClick = { expandDropdown = true }) {
Icon(Icons.Default.MoreVert, stringResource(R.string.generic_menu))
icon.handler()
}
) {
Icon(icon.icon, stringResource(icon.labelResource))
}
}
DropdownMenu(
expanded = expandDropdown,
onDismissRequest = { expandDropdown = false }
) {
for (option in screen.toolbarOptions) {
DropdownMenuItem(onClick = {
if (option.action != null) executeCommand(option.action)
if (screen?.toolbarOptions?.isEmpty() == false) {
IconButton(onClick = { expandDropdown = true }) {
Icon(Icons.Default.MoreVert, stringResource(R.string.generic_menu))
}
option.handler()
DropdownMenu(
expanded = expandDropdown,
onDismissRequest = { expandDropdown = false }
) {
for (option in screen.toolbarOptions) {
DropdownMenuItem(onClick = {
if (option.action != null) executeCommand(option.action)
expandDropdown = false
}) {
Text(stringResource(option.labelResource))
option.handler()
expandDropdown = false
}) {
Text(stringResource(option.labelResource))
}
}
}
}
}
}
)
},
modifier = Modifier,
windowInsets = WindowInsets.statusBarsIgnoringVisibility
)
extraBars?.invoke()
}
},
bottomBar = {
val backStackColors = ButtonDefaults.textButtonColors(
@ -159,7 +172,8 @@ fun ScreenScaffold(
Text(title)
}
}
}
},
windowInsets = WindowInsets.navigationBarsIgnoringVisibility
)
},
floatingActionButton = {
@ -170,6 +184,7 @@ fun ScreenScaffold(
}
},
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
* 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 {
val message = ExceptionUtil.format(requireArguments().getSerializable(EXCEPTION) as Exception)
val message = ExceptionUtil.formatInterpreted(requireContext(), requireArguments().getSerializable(EXCEPTION) as Exception)
return AlertDialog.Builder(requireContext(), theme)
.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
* it under the terms of the GNU General Public License as published by
@ -15,11 +15,26 @@
*/
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.StringWriter
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 ->
tr.printStackTrace(pw)
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
* 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)
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
* 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 requestLocationPermission() {
RequestWifiPermission.doRequest(this@LockActionFragment, LOCATION_REQUEST_CODE)
RequestWifiPermission.doRequest(this@LockActionFragment, LOCATION_REQUEST_CODE, auth.logic.platformIntegration)
}
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
* 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.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import androidx.activity.OnBackPressedCallback
import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.MutableLiveData
import androidx.viewpager.widget.ViewPager
import androidx.compose.foundation.layout.Column
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.databinding.LockActivityBinding
import io.timelimit.android.data.model.UserType
import io.timelimit.android.extensions.showSafe
import io.timelimit.android.logic.BlockingReason
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.protocol.U2FDevice
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.NewLoginFragment
import io.timelimit.android.ui.main.ActivityViewModel
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.util.SyncStatusModel
@ -85,26 +106,115 @@ class LockActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
null
}
private val showAuth = MutableLiveData<Boolean>().apply { value = false }
override fun onCreate(savedInstanceState: Bundle?) {
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)
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)
setContentView(binding.root)
isTimeOver
}.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)
model.init(blockedPackageName, blockedActivityName)
binding.pager.adapter = adapter
model.content.observe(this) {
if (isResumed && it is LockscreenContent.Blocked.BlockedCategory && it.reason == BlockingReason.RequiresCurrentDevice && !model.didOpenSetCurrentDeviceScreen) {
model.didOpenSetCurrentDeviceScreen = true
@ -115,30 +225,12 @@ class LockActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
}
}
AuthenticationFab.manageAuthenticationFab(
fab = binding.fab,
shouldHighlight = activityModel.shouldHighlightAuthenticationButton,
authenticatedUser = activityModel.authenticatedUser,
activity = this,
doesSupportAuth = showAuth
)
activityModel.shouldHighlightAuthenticationButton.observe(this) {
if (it) {
activityModel.shouldHighlightAuthenticationButton.postValue(false)
binding.fab.setOnClickListener { showAuthenticationScreen() }
binding.pager.addOnPageChangeListener(object: ViewPager.SimpleOnPageChangeListener() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
showAuth.value = position == 1
showAuthenticationScreen()
}
})
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) {

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
* it under the terms of the GNU General Public License as published by
@ -16,14 +16,21 @@
package io.timelimit.android.ui.manage.category.apps.add
import android.app.Dialog
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.MarginLayoutParams
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
@ -220,9 +227,29 @@ class AddCategoryAppsFragment : DialogFragment() {
}
}
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, windowInsets ->
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
v.updateLayoutParams<MarginLayoutParams> {
topMargin = insets.top
bottomMargin = insets.bottom
leftMargin = insets.left
rightMargin = insets.right
}
WindowInsetsCompat.CONSUMED
}
return AlertDialog.Builder(requireContext(), R.style.AppTheme)
.setView(binding.root)
.create()
.setView(binding.root)
.create()
.also { dialog ->
if (VERSION.SDK_INT >= VERSION_CODES.VANILLA_ICE_CREAM) dialog.setOnShowListener {
WindowInsetsControllerCompat(dialog.window!!, binding.root).run {
isAppearanceLightStatusBars = true
}
}
}
}
fun show(manager: FragmentManager) {

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
* it under the terms of the GNU General Public License as published by
@ -16,9 +16,16 @@
package io.timelimit.android.ui.manage.category.apps.addactivity
import android.app.Dialog
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup.MarginLayoutParams
import androidx.appcompat.app.AlertDialog
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
@ -114,9 +121,30 @@ class AddAppActivitiesDialogFragment: DialogFragment() {
dismissAllowingStateLoss()
}
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, windowInsets ->
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
v.updateLayoutParams<MarginLayoutParams> {
topMargin = insets.top
bottomMargin = insets.bottom
leftMargin = insets.left
rightMargin = insets.right
}
WindowInsetsCompat.CONSUMED
}
return AlertDialog.Builder(requireContext(), R.style.AppTheme)
.setView(binding.root)
.create()
.setView(binding.root)
.create()
.also { dialog ->
if (VERSION.SDK_INT >= VERSION_CODES.VANILLA_ICE_CREAM) dialog.setOnShowListener {
WindowInsetsControllerCompat(dialog.window!!, binding.root).run {
isAppearanceLightStatusBars = true
}
}
}
}
fun show(fragmentManager: FragmentManager) = showSafe(fragmentManager, DIALOG_TAG)

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.util.DateUtil
import io.timelimit.android.util.DayNameUtil
import io.timelimit.android.util.Option
import io.timelimit.android.util.TimeTextUtil
import kotlin.properties.Delegates
@ -159,12 +160,24 @@ class AppAndRuleAdapter: RecyclerView.Adapter<AppAndRuleAdapter.Holder>() {
val binding = holder.itemView.tag as FragmentCategoryTimeLimitRuleItemBinding
val context = binding.root.context
val usedTime = date?.let { date ->
RemainingTime.getUsedTime(
usedTimes = usedTimes,
rule = rule,
firstDayOfWeekAsEpochDay = date.firstDayOfWeekAsEpochDay,
dayOfWeekForDailyRule = if (rule.perDay) date.dayOfWeek else null
).toInt()
val dayOfWeekForDailyRule: Option<Int?>? =
if (rule.perDay) {
(0 until 7)
.map { (7 + date.dayOfWeek - it) % 7 } // make the current day the last one
.firstOrNull { rule.dayMask.toInt() and (1 shl it) != 0 }
?.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
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
* it under the terms of the GNU General Public License as published by
@ -117,7 +117,7 @@ object ManageCategoryNetworksView {
}
view.grantPermissionButton.setOnClickListener {
RequestWifiPermission.doRequest(fragment, permissionRequestCode)
RequestWifiPermission.doRequest(fragment, permissionRequestCode, auth.logic.platformIntegration)
}
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
* 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.fragment.app.Fragment
import io.timelimit.android.R
import io.timelimit.android.integration.platform.PlatformIntegration
object RequestWifiPermission {
private fun isLocationEnabled(context: Context): Boolean = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P)
@ -36,8 +37,10 @@ object RequestWifiPermission {
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 (platformIntegration.deviceOwner.grantLocationAccess()) return
fragment.requestPermissions(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), permissionRequestCode)
} else if (!isLocationEnabled(fragment.requireContext())) {
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.setFragmentResult
import io.timelimit.android.extensions.showSafe
import org.threeten.bp.LocalDate
import java.time.LocalDate
class DatePickerDialogFragment: DialogFragment() {
companion object {

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.SelectTimeSpanViewListener
import io.timelimit.android.util.TimeTextUtil
import org.threeten.bp.Instant
import org.threeten.bp.LocalDate
import org.threeten.bp.ZoneId
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
import java.util.*
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
* 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.ui.main.ActivityViewModel
import io.timelimit.android.ui.main.getActivityViewModel
import org.threeten.bp.LocalDate
import org.threeten.bp.ZoneId
import java.time.LocalDate
import java.time.ZoneId
import java.util.*
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
* 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.ui.main.ActivityViewModel
import io.timelimit.android.ui.main.getActivityViewModel
import org.threeten.bp.Instant
import org.threeten.bp.LocalDateTime
import org.threeten.bp.ZoneId
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
class DisableTimelimitsUntilTimeDialogFragment: DialogFragment() {
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
* 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.payment.RequiresPurchaseDialogFragment
import io.timelimit.android.ui.view.ManageDisableTimelimitsViewHandlers
import org.threeten.bp.LocalDate
import org.threeten.bp.ZoneId
import java.time.LocalDate
import java.time.ZoneId
import java.util.*
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
* 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.getActivityViewModel
import io.timelimit.android.ui.payment.RequiresPurchaseDialogFragment
import org.threeten.bp.Instant
import org.threeten.bp.LocalDate
import org.threeten.bp.LocalDateTime
import org.threeten.bp.LocalTime
import org.threeten.bp.ZoneId
import java.time.Instant
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.ZoneId
class SetCategorySpecialModeFragment: DialogFragment() {
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
* 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.UpdateCategoryTemporarilyBlockedAction
import io.timelimit.android.ui.main.ActivityViewModel
import org.threeten.bp.LocalDate
import java.time.LocalDate
class SetCategorySpecialModeModel(application: Application): AndroidViewModel(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
* 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 io.timelimit.android.R
import io.timelimit.android.date.DateInTimezone
import org.threeten.bp.LocalDate
import org.threeten.bp.ZoneId
import java.time.LocalDate
import java.time.ZoneId
import java.util.*
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
* 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.ui.model.managechild.ManageChildUsageHistory
import io.timelimit.android.util.TimeTextUtil
import org.threeten.bp.LocalDate
import org.threeten.bp.ZoneOffset
import java.time.LocalDate
import java.time.ZoneOffset
import java.util.Date
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
* it under the terms of the GNU General Public License as published by
@ -49,7 +49,7 @@ fun ManageDeviceUserScreen(
Card(
onClick = { actions.select(item) },
modifier = Modifier
.animateItemPlacement()
.animateItem()
.fillMaxWidth(),
backgroundColor = when (item.selected) {
true -> MaterialTheme.colors.secondary

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
* 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.Intent
import android.content.pm.ApplicationInfo
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import androidx.activity.OnBackPressedCallback
import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
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 io.timelimit.android.BuildConfig
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.u2f.U2fManager
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.login.AuthTokenLoginProcessor
import io.timelimit.android.ui.login.NewLoginFragment
@ -65,12 +76,73 @@ class AnnoyActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.D
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
U2fManager.setupActivity(this)
val logic = DefaultAppLogic.with(this)
val binding = AnnoyActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
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)
)
)
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) {
val systemImageApps = packageManager.getInstalledApplications(0)
@ -92,35 +164,6 @@ class AnnoyActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.D
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 ->
if (user?.second?.type == UserType.Parent) {
logic.annoyLogic.doParentTempUnlock()

View file

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

View file

@ -160,7 +160,7 @@ object MailAuthentication {
)
if (result == SnackbarResult.ActionPerformed) {
val message = ExceptionUtil.format(ex)
val message = ExceptionUtil.formatInterpreted(logic.context, ex)
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
* it under the terms of the GNU General Public License as published by
@ -181,7 +181,7 @@ object DeviceOwnerHandling {
)
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
* it under the terms of the GNU General Public License as published by
@ -23,6 +23,7 @@ import io.timelimit.android.async.Threads
import io.timelimit.android.coroutines.executeAndWait
import io.timelimit.android.data.backup.DatabaseBackup
import io.timelimit.android.data.devicename.DeviceName
import io.timelimit.android.data.model.ConsentFlags
import io.timelimit.android.logic.AppLogic
import io.timelimit.android.sync.ApplyServerDataStatus
import io.timelimit.android.sync.network.NewDeviceInfo
@ -333,6 +334,11 @@ object SetupParentHandling {
database.config().setDeviceAuthTokenSync(result.deviceAuthToken)
database.config().setEnableBackgroundSync(state.backgroundSync)
database.config().setConsentFlagSync(
ConsentFlags.BLOCK_USER_SWITCH_BY_DEFAULT,
true
)
ApplyServerDataStatus.applyServerDataStatusSync(result.serverDataStatus, logic.database, logic.platformIntegration)
}
}
@ -363,7 +369,7 @@ object SetupParentHandling {
)
if (result == SnackbarResult.ActionPerformed) updateState {
it.copy(error = ExceptionUtil.format(ex))
it.copy(error = ExceptionUtil.formatInterpreted(logic.context, ex))
}
}
} 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
* it under the terms of the GNU General Public License as published by
@ -35,7 +35,7 @@ import io.timelimit.android.ui.model.main.OverviewHandling
@OptIn(ExperimentalFoundationApi::class)
fun LazyListScope.deviceItems(screen: OverviewHandling.OverviewScreen) {
item (key = Pair("devices", "header")) {
ListCommon.SectionHeader(stringResource(R.string.overview_header_devices), Modifier.animateItemPlacement())
ListCommon.SectionHeader(stringResource(R.string.overview_header_devices), Modifier.animateItem())
}
items(screen.devices.list, key = { Pair("device", it.device.id) }) {
@ -48,7 +48,7 @@ fun LazyListScope.deviceItems(screen: OverviewHandling.OverviewScreen) {
icon = Icons.Default.Add,
label = stringResource(R.string.add_device),
action = screen.actions.addDevice,
modifier = Modifier.animateItemPlacement()
modifier = Modifier.animateItem()
)
}
}
@ -56,7 +56,7 @@ fun LazyListScope.deviceItems(screen: OverviewHandling.OverviewScreen) {
if (screen.devices.canShowMore != null) {
item (key = Pair("devices", "more")) {
ListCommon.ShowMoreItem(
modifier = Modifier.animateItemPlacement(),
modifier = Modifier.animateItem(),
action = { screen.actions.showMoreDevices(screen.devices.canShowMore) }
)
}
@ -71,7 +71,7 @@ fun LazyItemScope.DeviceItem(
) {
ListCardCommon.Card(
Modifier
.animateItemPlacement()
.animateItem()
.padding(horizontal = 8.dp)
.clickable(onClick = { openAction(item) })
) {

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
* it under the terms of the GNU General Public License as published by
@ -40,7 +40,7 @@ fun LazyListScope.introItems(
item (key = Pair("intro", "finish setup")) {
ListCardCommon.Card(
modifier = Modifier
.animateItemPlacement()
.animateItem()
.padding(horizontal = 8.dp)
) {
Text(
@ -62,7 +62,7 @@ fun LazyListScope.introItems(
item (key = Pair("intro", "outdated server")) {
ListCardCommon.Card(
modifier = Modifier
.animateItemPlacement()
.animateItem()
.padding(horizontal = 8.dp)
) {
Text(
@ -79,7 +79,7 @@ fun LazyListScope.introItems(
item (key = Pair("intro", "server message")) {
ListCardCommon.Card(
modifier = Modifier
.animateItemPlacement()
.animateItem()
.padding(horizontal = 8.dp)
) {
Text(
@ -108,7 +108,7 @@ fun LazyListScope.introItems(
SwipeToDismiss(
state = state,
background = {},
modifier = Modifier.animateItemPlacement()
modifier = Modifier.animateItem()
) {
ListCardCommon.Card(
modifier = Modifier.padding(horizontal = 8.dp)
@ -133,7 +133,7 @@ fun LazyListScope.introItems(
item (key = Pair("intro", "task review")) {
ListCardCommon.Card(
modifier = Modifier
.animateItemPlacement()
.animateItem()
.padding(horizontal = 8.dp)
) {
Text(

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
* 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.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import io.timelimit.android.ui.model.main.OverviewHandling
@Composable
fun OverviewScreen(
screen: OverviewHandling.OverviewScreen,
paddingValues: PaddingValues,
modifier: Modifier = Modifier
) {
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),
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
* it under the terms of the GNU General Public License as published by
@ -35,7 +35,7 @@ import io.timelimit.android.ui.model.main.OverviewHandling
@OptIn(ExperimentalFoundationApi::class)
fun LazyListScope.userItems(screen: OverviewHandling.OverviewScreen) {
item (key = Pair("users", "header")) {
ListCommon.SectionHeader(stringResource(R.string.overview_header_users), Modifier.animateItemPlacement())
ListCommon.SectionHeader(stringResource(R.string.overview_header_users), Modifier.animateItem())
}
items(screen.users.list, key = { Pair("user", it.id) }) { UserItem(it, screen.actions) }
@ -45,13 +45,13 @@ fun LazyListScope.userItems(screen: OverviewHandling.OverviewScreen) {
icon = Icons.Default.Add,
label = stringResource(R.string.add_user_title),
action = screen.actions.addUser,
modifier = Modifier.animateItemPlacement()
modifier = Modifier.animateItem()
)
}
if (screen.users.canShowMore) item (key = Pair("users", "more")) {
ListCommon.ShowMoreItem (
modifier = Modifier.animateItemPlacement(),
modifier = Modifier.animateItem(),
action = screen.actions.showMoreUsers
)
}
@ -65,7 +65,7 @@ fun LazyItemScope.UserItem(
) {
ListCardCommon.Card(
Modifier
.animateItemPlacement()
.animateItem()
.padding(horizontal = 8.dp)
.clickable(onClick = { actions.openUser(user) })
) {

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
* it under the terms of the GNU General Public License as published by
@ -74,9 +74,14 @@ class ActivityPurchaseModel(application: Application): AndroidViewModel(applicat
clientMutex.withLock {
if (_billingClient == null) {
_billingClient = BillingClient.newBuilder(getApplication())
.enablePendingPurchases()
.setListener(purchaseUpdatedListener)
.build()
.enablePendingPurchases(
PendingPurchasesParams
.newBuilder()
.enableOneTimeProducts()
.build()
)
.setListener(purchaseUpdatedListener)
.build()
}
val initBillingClient = _billingClient!!

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
* it under the terms of the GNU General Public License as published by
@ -24,6 +24,7 @@ import io.timelimit.android.coroutines.executeAndWait
import io.timelimit.android.coroutines.runAsync
import io.timelimit.android.data.backup.DatabaseBackup
import io.timelimit.android.data.devicename.DeviceName
import io.timelimit.android.data.model.ConsentFlags
import io.timelimit.android.livedata.castDown
import io.timelimit.android.logic.AppLogic
import io.timelimit.android.logic.DefaultAppLogic
@ -70,6 +71,11 @@ class SetupRemoteChildViewModel(application: Application): AndroidViewModel(appl
logic.database.config().setOwnDeviceIdSync(registerResponse.ownDeviceId)
logic.database.config().setDeviceAuthTokenSync(registerResponse.deviceAuthToken)
logic.database.config().setConsentFlagSync(
ConsentFlags.BLOCK_USER_SWITCH_BY_DEFAULT,
true
)
ApplyServerDataStatus.applyServerDataStatusSync(clientStatusResponse, logic.database, logic.platformIntegration)
}
}

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
import android.content.res.Configuration
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.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.databinding.UpdateActivityBinding
import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.ui.ScreenScaffold
import io.timelimit.android.ui.Theme
class UpdateActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
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(
view = binding.update,
lifecycleOwner = this,
fragmentManager = supportFragmentManager,
appLogic = DefaultAppLogic.with(this)
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 = 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
* 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.os.Bundle
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.fragment.app.FragmentActivity
import io.timelimit.android.R
@ -30,6 +31,8 @@ class WidgetConfigActivity: FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
if (model.state.value == WidgetConfigModel.State.WaitingForInit) {
model.init(
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
* 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) {
// 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 {
// old signature
// 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())) }

View file

@ -1 +1,2 @@
- Fehler beim Bearbeiten von Zeitbegrenzungsregeln behoben
- Funktionsumfang bei Verwendung der Geräte-Besitzer-Berechtigung erweitert
- enthaltene Komponenten aktualisiert

View file

@ -1 +1,2 @@
- fix issue in the time limit rule editor
- add more features for users of the device owner permission
- 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"?>
<!--
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
it under the terms of the GNU General Public License as published by
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_permission_sanction_title">Berechtigung fehlt</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_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_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_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_extra_time_started">Extrazeit beginnt</string>
<string name="obsolete_message">Sie verwenden TimeLimit auf einer älteren Android-Version.
Das kann funktionieren, aber es wird nicht empfohlen.
</string>
@ -1741,6 +1752,10 @@
<string name="dummy_app_unassigned_system_image_app">nicht zugeordnete Apps von der Systempartition</string>
<string name="dummy_app_feature_adb">Entwickleroptionen</string>
<string name="dummy_app_feature_dns">DNS-Einstellungen</string>
<string name="dummy_app_feature_add_user">Systembenutzer erstellen</string>
<string name="dummy_app_feature_switch_user">Systembenutzer wechseln</string>
<string name="dummy_app_feature_vpn">VPN konfigurieren</string>
<string name="dummy_app_feature_unknown_sources">Apps aus unbekannten Quellen installieren</string>
<string name="dummy_app_activity_audio">Hintergrundmusikwiedergabe</string>
<string name="notify_permission_title">Benachrichtigungen</string>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
TimeLimit Copyright <C> 2019 - 2025 Jonas Lochmann
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
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_permission_sanction_title">Missing permission</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_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_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_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_extra_time_started">Extra time starts</string>
<string name="obsolete_message">You are using TimeLimit at a obsolete Android version.
Although this can work, it is not recommend.
</string>
@ -1638,7 +1649,7 @@
<a href="https://legal.timelimit.io/en/privacy/">https://legal.timelimit.io/en/privacy/</a>
</string>
<string name="terms_gpl" translatable="false">
TimeLimit Copyright &#169; 2019 - 2024 Jonas Lochmann
TimeLimit Copyright &#169; 2019 - 2025 Jonas Lochmann
\nThis program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License.
@ -1793,6 +1804,10 @@
<string name="dummy_app_unassigned_system_image_app">not assigned Apps from the system image</string>
<string name="dummy_app_feature_adb">Developer Options</string>
<string name="dummy_app_feature_dns">DNS Settings</string>
<string name="dummy_app_feature_add_user">Create System User</string>
<string name="dummy_app_feature_switch_user">Switch System User</string>
<string name="dummy_app_feature_vpn">Configure VPN</string>
<string name="dummy_app_feature_unknown_sources">Install Apps from unknown sources</string>
<string name="dummy_app_activity_audio">Background Audio Playback</string>
<string name="notify_permission_title">Notifications</string>

View file

@ -1,5 +1,5 @@
<!--
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License.
@ -15,7 +15,7 @@
<resources>
<!-- 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="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorSecondary">@color/colorAccent</item>
@ -32,6 +32,10 @@
<item name="colorAccent">@color/white</item>
</style>
<style name="AppTheme.Translucent" parent="AppTheme">
<item name="android:windowIsFloating">true</item>
</style>
<!-- from https://stackoverflow.com/a/46286184 -->
<style name="BottomSheetDialog" parent="Theme.MaterialComponents.DayNight.BottomSheetDialog">
<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
* it under the terms of the GNU General Public License as published by
@ -42,7 +42,7 @@ object BillingClient {
enum class ProductType { INAPP }
object Builder {
fun enablePendingPurchases() = this
fun enablePendingPurchases(params: PendingPurchasesParams) = this
fun setListener(listener: PurchasesUpdatedListener) = this
fun build() = BillingClient
}
@ -143,4 +143,13 @@ object QueryPurchasesParams {
fun newBuilder() = this
fun setProductType(type: BillingClient.ProductType) = this
fun build() = this
}
object PendingPurchasesParams {
object Builder {
fun enableOneTimeProducts() = this
fun build() = PendingPurchasesParams
}
fun newBuilder() = Builder
}

View file

@ -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
* it under the terms of the GNU General Public License as published by
@ -15,10 +15,10 @@
*/
plugins {
id 'com.android.application' version '8.3.1' apply false
id 'com.android.library' version '8.3.1' apply false
id 'org.jetbrains.kotlin.android' version "1.9.20" apply false
id 'com.google.devtools.ksp' version '1.8.21-1.0.11' apply false
id 'com.android.application' version '8.11.1' apply false
id 'com.android.library' version '8.11.1' apply false
id 'org.jetbrains.kotlin.android' version "2.0.21" apply false
id 'com.google.devtools.ksp' version '1.9.21-1.0.16' apply false
id 'androidx.navigation.safeargs' version '2.6.0' apply false
id 'com.squareup.wire' version '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.
# The setting is particularly useful for tweaking memory settings.
android.useAndroidX=true
android.defaults.buildfeatures.buildconfig=true
org.gradle.jvmargs=-Xmx4096m
# When configured, Gradle will run in incubating parallel mode.
# 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
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip
distributionSha256Sum=f2b9ed0faf8472cbe469255ae6c86eddb77076c75191741b4a462f33128dd419
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
distributionSha256Sum=20f1b1176237254a6fc204d8434196fa11a4cfb387567519c61556e8710aed78