Compare commits

...

12 commits

Author SHA1 Message Date
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
20 changed files with 399 additions and 236 deletions

View file

@ -25,13 +25,13 @@ plugins {
android { android {
namespace 'io.timelimit.android' namespace 'io.timelimit.android'
compileSdkVersion 34 compileSdk 35
defaultConfig { defaultConfig {
applicationId "io.timelimit.android" applicationId "io.timelimit.android"
minSdkVersion 26 minSdkVersion 26
targetSdkVersion 34 targetSdkVersion 35
versionCode 218 versionCode 220
versionName "7.1.0" versionName "7.2.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
kapt { kapt {
arguments { arguments {
@ -146,8 +146,8 @@ android {
} }
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_17 sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_21
} }
kotlinOptions { kotlinOptions {
@ -167,22 +167,23 @@ wire {
dependencies { dependencies {
def nav_version = "2.5.3" def nav_version = "2.5.3"
def room_version = "2.6.1" def room_version = "2.6.1"
def work_version = '2.9.0' def work_version = '2.9.1'
def paging_version = "3.3.1" def paging_version = "3.3.2"
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.21" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.21"
implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'androidx.core:core:1.13.1' implementation 'androidx.core:core:1.15.0'
implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.gridlayout:gridlayout:1.0.0' implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation "com.google.android.material:material:1.12.0" implementation "com.google.android.material:material:1.12.0"
implementation 'androidx.compose.material:material:1.6.8' implementation 'androidx.compose.material:material:1.7.5'
implementation 'androidx.activity:activity-compose:1.9.1' implementation 'androidx.activity:activity-compose:1.9.3'
implementation "com.google.accompanist:accompanist-flowlayout:0.30.0" implementation "com.google.accompanist:accompanist-flowlayout:0.30.0"
implementation 'androidx.compose.material:material-icons-extended:1.6.8' implementation 'androidx.compose.material:material-icons-extended:1.7.5'
debugImplementation "androidx.compose.ui:ui-tooling:1.6.8" debugImplementation "androidx.compose.ui:ui-tooling:1.7.5"
implementation 'androidx.fragment:fragment-ktx:1.8.2' implementation 'androidx.fragment:fragment-ktx:1.8.5'
implementation 'androidx.fragment:fragment-compose:1.8.5'
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui:$nav_version" implementation "androidx.navigation:navigation-ui:$nav_version"
@ -203,7 +204,7 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test:runner:1.6.1' androidTestImplementation 'androidx.test:runner:1.6.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
implementation 'org.mindrot:jbcrypt:0.4' implementation 'org.mindrot:jbcrypt:0.4'
@ -216,7 +217,7 @@ dependencies {
implementation 'com.squareup.okhttp3:okhttp-tls:4.9.3' implementation 'com.squareup.okhttp3:okhttp-tls:4.9.3'
implementation 'com.squareup.okhttp3:logging-interceptor:4.9.3' implementation 'com.squareup.okhttp3:logging-interceptor:4.9.3'
googleApiImplementation "com.android.billingclient:billing-ktx:7.0.0" googleApiImplementation "com.android.billingclient:billing-ktx:7.1.1"
implementation('io.socket:socket.io-client:2.0.0') { implementation('io.socket:socket.io-client:2.0.0') {
exclude group: 'org.json', module: 'json' exclude group: 'org.json', module: 'json'

View file

@ -55,7 +55,9 @@
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name" android:label="@string/app_name"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme"> android:theme="@style/AppTheme"
android:enableOnBackInvokedCallback="true"
tools:targetApi="tiramisu">
<!-- UI --> <!-- UI -->

View file

@ -19,6 +19,7 @@ import androidx.lifecycle.LiveData
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.Query import androidx.room.Query
import androidx.room.RoomWarnings
import io.timelimit.android.data.model.UsedTimeItem import io.timelimit.android.data.model.UsedTimeItem
import io.timelimit.android.data.model.UsedTimeListItem import io.timelimit.android.data.model.UsedTimeListItem
import io.timelimit.android.livedata.ignoreUnchanged import io.timelimit.android.livedata.ignoreUnchanged
@ -68,11 +69,13 @@ abstract class UsedTimeDao {
// breaking it into multiple lines causes issues during compilation ... // breaking it into multiple lines causes issues during compilation ...
// this warns about an unused column, but this column is used in the ORDER BY // this warns about an unused column, but this column is used in the ORDER BY
@SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
@Query("SELECT 2 AS type, start_time_of_day AS startMinuteOfDay, end_time_of_day AS endMinuteOfDay, used_time AS duration, day_of_epoch AS day, NULL AS lastUsage, NULL AS maxSessionDuration, NULL AS pauseDuration, category.id AS categoryId, category.title AS categoryTitle FROM used_time JOIN category ON (used_time.category_id = category.id) WHERE category.id = :categoryId UNION ALL SELECT 1 AS type, start_minute_of_day AS startMinuteOfDay, end_minute_of_day AS endMinuteOfDay, last_session_duration AS duration, NULL AS day, last_usage AS lastUsage, max_session_duration AS maxSessionDuration, session_pause_duration AS pauseDuration, category.id AS categoryId, category.title AS categoryTitle FROM session_duration JOIN category ON (session_duration.category_id = category.id) WHERE category.id = :categoryId ORDER BY type, day DESC, lastUsage DESC, startMinuteOfDay, endMinuteOfDay, categoryId") @Query("SELECT 2 AS type, start_time_of_day AS startMinuteOfDay, end_time_of_day AS endMinuteOfDay, used_time AS duration, day_of_epoch AS day, NULL AS lastUsage, NULL AS maxSessionDuration, NULL AS pauseDuration, category.id AS categoryId, category.title AS categoryTitle FROM used_time JOIN category ON (used_time.category_id = category.id) WHERE category.id = :categoryId UNION ALL SELECT 1 AS type, start_minute_of_day AS startMinuteOfDay, end_minute_of_day AS endMinuteOfDay, last_session_duration AS duration, NULL AS day, last_usage AS lastUsage, max_session_duration AS maxSessionDuration, session_pause_duration AS pauseDuration, category.id AS categoryId, category.title AS categoryTitle FROM session_duration JOIN category ON (session_duration.category_id = category.id) WHERE category.id = :categoryId ORDER BY type, day DESC, lastUsage DESC, startMinuteOfDay, endMinuteOfDay, categoryId")
abstract fun getUsedTimeListItemsByCategoryId(categoryId: String): Flow<List<UsedTimeListItem>> abstract fun getUsedTimeListItemsByCategoryId(categoryId: String): Flow<List<UsedTimeListItem>>
// breaking it into multiple lines causes issues during compilation ... // breaking it into multiple lines causes issues during compilation ...
// this warns about an unused column, but this column is used in the ORDER BY // this warns about an unused column, but this column is used in the ORDER BY
@SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
@Query("SELECT 2 AS type, start_time_of_day AS startMinuteOfDay, end_time_of_day AS endMinuteOfDay, used_time AS duration, day_of_epoch AS day, NULL AS lastUsage, NULL AS maxSessionDuration, NULL AS pauseDuration, category.id AS categoryId, category.title AS categoryTitle FROM used_time JOIN category ON (used_time.category_id = category.id) WHERE category.child_id = :userId UNION ALL SELECT 1 AS type, start_minute_of_day AS startMinuteOfDay, end_minute_of_day AS endMinuteOfDay, last_session_duration AS duration, NULL AS day, last_usage AS lastUsage, max_session_duration AS maxSessionDuration, session_pause_duration AS pauseDuration, category.id AS categoryId, category.title AS categoryTitle FROM session_duration JOIN category ON (session_duration.category_id = category.id) WHERE category.child_id = :userId ORDER BY type, day DESC, lastUsage DESC, startMinuteOfDay, endMinuteOfDay, categoryId") @Query("SELECT 2 AS type, start_time_of_day AS startMinuteOfDay, end_time_of_day AS endMinuteOfDay, used_time AS duration, day_of_epoch AS day, NULL AS lastUsage, NULL AS maxSessionDuration, NULL AS pauseDuration, category.id AS categoryId, category.title AS categoryTitle FROM used_time JOIN category ON (used_time.category_id = category.id) WHERE category.child_id = :userId UNION ALL SELECT 1 AS type, start_minute_of_day AS startMinuteOfDay, end_minute_of_day AS endMinuteOfDay, last_session_duration AS duration, NULL AS day, last_usage AS lastUsage, max_session_duration AS maxSessionDuration, session_pause_duration AS pauseDuration, category.id AS categoryId, category.title AS categoryTitle FROM session_duration JOIN category ON (session_duration.category_id = category.id) WHERE category.child_id = :userId ORDER BY type, day DESC, lastUsage DESC, startMinuteOfDay, endMinuteOfDay, categoryId")
abstract fun getUsedTimeListItemsByUserId(userId: String): Flow<List<UsedTimeListItem>> abstract fun getUsedTimeListItemsByUserId(userId: String): Flow<List<UsedTimeListItem>>
} }

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -17,8 +17,13 @@ package io.timelimit.android.ui
import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.navigationBarsIgnoringVisibility
import androidx.compose.foundation.layout.statusBarsIgnoringVisibility
import androidx.compose.foundation.layout.systemBarsIgnoringVisibility
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -37,6 +42,7 @@ import io.timelimit.android.ui.model.Screen
import io.timelimit.android.ui.model.Title import io.timelimit.android.ui.model.Title
import io.timelimit.android.ui.model.UpdateStateCommand import io.timelimit.android.ui.model.UpdateStateCommand
@OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
fun ScreenScaffold( fun ScreenScaffold(
screen: Screen?, screen: Screen?,
@ -111,7 +117,9 @@ fun ScreenScaffold(
} }
} }
} }
} },
modifier = Modifier,
windowInsets = WindowInsets.statusBarsIgnoringVisibility
) )
}, },
bottomBar = { bottomBar = {
@ -159,7 +167,8 @@ fun ScreenScaffold(
Text(title) Text(title)
} }
} }
} },
windowInsets = WindowInsets.navigationBarsIgnoringVisibility
) )
}, },
floatingActionButton = { floatingActionButton = {
@ -170,6 +179,7 @@ fun ScreenScaffold(
} }
}, },
snackbarHost = { SnackbarHost(snackbarHostState ?: it) }, snackbarHost = { SnackbarHost(snackbarHostState ?: it) },
content = content content = content,
contentWindowInsets = WindowInsets.systemBarsIgnoringVisibility
) )
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,2 +1,3 @@
- verbesserter Umgang mit Fehlern im Hintergrund - Anpassungen für Android 15
- falsch angezeigte Nutzungsdauer bei Regeln die je Tag gelten an Tagen, an denen diese nicht gelten, behoben
- enthaltene Komponenten aktualisiert - enthaltene Komponenten aktualisiert

View file

@ -1,2 +1,3 @@
- improved handling of errors in the background - adjustments for Android 15
- fix incorrect used time at rules that apply per day at days where they do not apply
- update contained software - 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

@ -15,8 +15,8 @@
*/ */
plugins { plugins {
id 'com.android.application' version '8.5.1' apply false id 'com.android.application' version '8.7.2' apply false
id 'com.android.library' version '8.5.1' apply false id 'com.android.library' version '8.7.2' apply false
id 'org.jetbrains.kotlin.android' version "1.9.21" apply false id 'org.jetbrains.kotlin.android' version "1.9.21" apply false
id 'com.google.devtools.ksp' version '1.9.21-1.0.16' apply false id 'com.google.devtools.ksp' version '1.9.21-1.0.16' apply false
id 'androidx.navigation.safeargs' version '2.6.0' apply false id 'androidx.navigation.safeargs' version '2.6.0' apply false

View file

@ -2,5 +2,5 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip
distributionSha256Sum=194717442575a6f96e1c1befa2c30e9a4fc90f701d7aee33eb879b79e7ff05c0 distributionSha256Sum=258e722ec21e955201e31447b0aed14201765a3bfbae296a46cf60b70e66db70