diff --git a/app/build.gradle b/app/build.gradle index 566f68b..ac9ed68 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -25,13 +25,13 @@ plugins { android { namespace 'io.timelimit.android' - compileSdkVersion 34 + compileSdk 35 defaultConfig { applicationId "io.timelimit.android" minSdkVersion 26 - targetSdkVersion 34 - versionCode 218 - versionName "7.1.0" + targetSdkVersion 35 + versionCode 220 + versionName "7.2.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" kapt { arguments { @@ -146,8 +146,8 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 + sourceCompatibility JavaVersion.VERSION_21 + targetCompatibility JavaVersion.VERSION_21 } kotlinOptions { @@ -167,22 +167,23 @@ wire { dependencies { def nav_version = "2.5.3" def room_version = "2.6.1" - def work_version = '2.9.0' - def paging_version = "3.3.1" + def work_version = '2.9.1' + def paging_version = "3.3.2" implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.21" 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.gridlayout:gridlayout:1.0.0' implementation "com.google.android.material:material:1.12.0" - implementation 'androidx.compose.material:material:1.6.8' - implementation 'androidx.activity:activity-compose:1.9.1' + implementation 'androidx.compose.material:material:1.7.5' + implementation 'androidx.activity:activity-compose:1.9.3' implementation "com.google.accompanist:accompanist-flowlayout:0.30.0" - implementation 'androidx.compose.material:material-icons-extended:1.6.8' - debugImplementation "androidx.compose.ui:ui-tooling:1.6.8" - implementation 'androidx.fragment:fragment-ktx:1.8.2' + implementation 'androidx.compose.material:material-icons-extended:1.7.5' + debugImplementation "androidx.compose.ui:ui-tooling:1.7.5" + implementation 'androidx.fragment:fragment-ktx:1.8.5' + implementation 'androidx.fragment:fragment-compose:1.8.5' implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" implementation "androidx.navigation:navigation-ui:$nav_version" @@ -203,7 +204,7 @@ dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' 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' implementation 'org.mindrot:jbcrypt:0.4' @@ -216,7 +217,7 @@ dependencies { implementation 'com.squareup.okhttp3:okhttp-tls:4.9.3' implementation 'com.squareup.okhttp3:logging-interceptor:4.9.3' - googleApiImplementation "com.android.billingclient:billing-ktx:7.0.0" + googleApiImplementation "com.android.billingclient:billing-ktx:7.1.1" implementation('io.socket:socket.io-client:2.0.0') { exclude group: 'org.json', module: 'json' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7bb37a4..74a652d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -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"> diff --git a/app/src/main/java/io/timelimit/android/data/dao/UsedTimeDao.kt b/app/src/main/java/io/timelimit/android/data/dao/UsedTimeDao.kt index e1d20d7..8b707f8 100644 --- a/app/src/main/java/io/timelimit/android/data/dao/UsedTimeDao.kt +++ b/app/src/main/java/io/timelimit/android/data/dao/UsedTimeDao.kt @@ -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 @@ -68,11 +69,13 @@ abstract class UsedTimeDao { // breaking it into multiple lines causes issues during compilation ... // this warns about an unused column, but this column is used in the ORDER BY + @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH) @Query("SELECT 2 AS type, start_time_of_day AS startMinuteOfDay, end_time_of_day AS endMinuteOfDay, used_time AS duration, day_of_epoch AS day, NULL AS lastUsage, NULL AS maxSessionDuration, NULL AS pauseDuration, category.id AS categoryId, category.title AS categoryTitle FROM used_time JOIN category ON (used_time.category_id = category.id) WHERE category.id = :categoryId UNION ALL SELECT 1 AS type, start_minute_of_day AS startMinuteOfDay, end_minute_of_day AS endMinuteOfDay, last_session_duration AS duration, NULL AS day, last_usage AS lastUsage, max_session_duration AS maxSessionDuration, session_pause_duration AS pauseDuration, category.id AS categoryId, category.title AS categoryTitle FROM session_duration JOIN category ON (session_duration.category_id = category.id) WHERE category.id = :categoryId ORDER BY type, day DESC, lastUsage DESC, startMinuteOfDay, endMinuteOfDay, categoryId") abstract fun getUsedTimeListItemsByCategoryId(categoryId: String): Flow> // 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.CURSOR_MISMATCH) @Query("SELECT 2 AS type, start_time_of_day AS startMinuteOfDay, end_time_of_day AS endMinuteOfDay, used_time AS duration, day_of_epoch AS day, NULL AS lastUsage, NULL AS maxSessionDuration, NULL AS pauseDuration, category.id AS categoryId, category.title AS categoryTitle FROM used_time JOIN category ON (used_time.category_id = category.id) WHERE category.child_id = :userId UNION ALL SELECT 1 AS type, start_minute_of_day AS startMinuteOfDay, end_minute_of_day AS endMinuteOfDay, last_session_duration AS duration, NULL AS day, last_usage AS lastUsage, max_session_duration AS maxSessionDuration, session_pause_duration AS pauseDuration, category.id AS categoryId, category.title AS categoryTitle FROM session_duration JOIN category ON (session_duration.category_id = category.id) WHERE category.child_id = :userId ORDER BY type, day DESC, lastUsage DESC, startMinuteOfDay, endMinuteOfDay, categoryId") abstract fun getUsedTimeListItemsByUserId(userId: String): Flow> } diff --git a/app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/LollipopForegroundAppHelper.kt b/app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/LollipopForegroundAppHelper.kt index 417e86e..ddabc3f 100644 --- a/app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/LollipopForegroundAppHelper.kt +++ b/app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/LollipopForegroundAppHelper.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 - 2022 Jonas Lochmann + * TimeLimit Copyright 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) { diff --git a/app/src/main/java/io/timelimit/android/ui/MainActivity.kt b/app/src/main/java/io/timelimit/android/ui/MainActivity.kt index f5e1408..3bd2739 100644 --- a/app/src/main/java/io/timelimit/android/ui/MainActivity.kt +++ b/app/src/main/java/io/timelimit/android/ui/MainActivity.kt @@ -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,10 +130,20 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De if (granted) mainModel.reportPermissionsChanged() } - @OptIn(ExperimentalAnimationApi::class) 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) + ) + ) + supportActionBar!!.hide() U2fManager.setupActivity(this) @@ -314,9 +325,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, diff --git a/app/src/main/java/io/timelimit/android/ui/ScreenMultiplexer.kt b/app/src/main/java/io/timelimit/android/ui/ScreenMultiplexer.kt index 301338e..5dad22f 100644 --- a/app/src/main/java/io/timelimit/android/ui/ScreenMultiplexer.kt +++ b/app/src/main/java/io/timelimit/android/ui/ScreenMultiplexer.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 - 2023 Jonas Lochmann + * TimeLimit Copyright 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, - 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)) } } \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/ScreenScaffold.kt b/app/src/main/java/io/timelimit/android/ui/ScreenScaffold.kt index 8429b74..583136f 100644 --- a/app/src/main/java/io/timelimit/android/ui/ScreenScaffold.kt +++ b/app/src/main/java/io/timelimit/android/ui/ScreenScaffold.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 - 2023 Jonas Lochmann + * TimeLimit Copyright 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?, @@ -111,7 +117,9 @@ fun ScreenScaffold( } } } - } + }, + modifier = Modifier, + windowInsets = WindowInsets.statusBarsIgnoringVisibility ) }, bottomBar = { @@ -159,7 +167,8 @@ fun ScreenScaffold( Text(title) } } - } + }, + windowInsets = WindowInsets.navigationBarsIgnoringVisibility ) }, floatingActionButton = { @@ -170,6 +179,7 @@ fun ScreenScaffold( } }, snackbarHost = { SnackbarHost(snackbarHostState ?: it) }, - content = content + content = content, + contentWindowInsets = WindowInsets.systemBarsIgnoringVisibility ) } \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/lock/LockActivity.kt b/app/src/main/java/io/timelimit/android/ui/lock/LockActivity.kt index 3e7d114..62e6c5d 100644 --- a/app/src/main/java/io/timelimit/android/ui/lock/LockActivity.kt +++ b/app/src/main/java/io/timelimit/android/ui/lock/LockActivity.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 - 2022 Jonas Lochmann + * TimeLimit Copyright 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,17 +106,113 @@ class LockActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De null } - private val showAuth = MutableLiveData().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) + ) + ) + + supportActionBar!!.hide() + 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() + + 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(Modifier.fillMaxSize()) + 1 -> AndroidFragment(Modifier.fillMaxSize()) + 2 -> AndroidFragment(Modifier.fillMaxSize()) + } + } + ) + } + }, + executeCommand = {}, + showAuthenticationDialog = + if (pager.currentPage == 1 && !isAuthenticated) ({ showAuthenticationScreen() }) + else null + ) + } + } syncModel.statusText.observe(this) { supportActionBar?.subtitle = it } @@ -103,8 +220,6 @@ class LockActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De 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 +230,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) { diff --git a/app/src/main/java/io/timelimit/android/ui/lock/LockActivityAdapter.kt b/app/src/main/java/io/timelimit/android/ui/lock/LockActivityAdapter.kt deleted file mode 100644 index 04080e3..0000000 --- a/app/src/main/java/io/timelimit/android/ui/lock/LockActivityAdapter.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * TimeLimit Copyright 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 . - */ - -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() - }) -} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/manage/category/appsandrules/AppAndRuleAdapter.kt b/app/src/main/java/io/timelimit/android/ui/manage/category/appsandrules/AppAndRuleAdapter.kt index 2312e86..be2a04a 100644 --- a/app/src/main/java/io/timelimit/android/ui/manage/category/appsandrules/AppAndRuleAdapter.kt +++ b/app/src/main/java/io/timelimit/android/ui/manage/category/appsandrules/AppAndRuleAdapter.kt @@ -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() { 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? = + 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 -> diff --git a/app/src/main/java/io/timelimit/android/ui/manipulation/AnnoyActivity.kt b/app/src/main/java/io/timelimit/android/ui/manipulation/AnnoyActivity.kt index 612ceb1..6bf0fa7 100644 --- a/app/src/main/java/io/timelimit/android/ui/manipulation/AnnoyActivity.kt +++ b/app/src/main/java/io/timelimit/android/ui/manipulation/AnnoyActivity.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 - 2023 Jonas Lochmann + * TimeLimit Copyright 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,75 @@ 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) + ) + ) + + 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) { val systemImageApps = packageManager.getInstalledApplications(0) @@ -92,35 +166,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() diff --git a/app/src/main/java/io/timelimit/android/ui/overview/overview/OverviewScreen.kt b/app/src/main/java/io/timelimit/android/ui/overview/overview/OverviewScreen.kt index 087d884..a1635f4 100644 --- a/app/src/main/java/io/timelimit/android/ui/overview/overview/OverviewScreen.kt +++ b/app/src/main/java/io/timelimit/android/ui/overview/overview/OverviewScreen.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 - 2023 Jonas Lochmann + * TimeLimit Copyright 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 ) { diff --git a/app/src/main/java/io/timelimit/android/ui/update/UpdateActivity.kt b/app/src/main/java/io/timelimit/android/ui/update/UpdateActivity.kt index bed632a..e213776 100644 --- a/app/src/main/java/io/timelimit/android/ui/update/UpdateActivity.kt +++ b/app/src/main/java/io/timelimit/android/ui/update/UpdateActivity.kt @@ -1,23 +1,83 @@ +/* + * TimeLimit Copyright 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 . + */ 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(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) + ) ) + + 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 + ) + } + } } } \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/widget/config/WidgetConfigActivity.kt b/app/src/main/java/io/timelimit/android/ui/widget/config/WidgetConfigActivity.kt index cfa4da5..2373c02 100644 --- a/app/src/main/java/io/timelimit/android/ui/widget/config/WidgetConfigActivity.kt +++ b/app/src/main/java/io/timelimit/android/ui/widget/config/WidgetConfigActivity.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 - 2022 Jonas Lochmann + * TimeLimit Copyright 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) diff --git a/app/src/main/java/io/timelimit/android/update/UpdateIntegration.kt b/app/src/main/java/io/timelimit/android/update/UpdateIntegration.kt index 33a3684..f1bc4f8 100644 --- a/app/src/main/java/io/timelimit/android/update/UpdateIntegration.kt +++ b/app/src/main/java/io/timelimit/android/update/UpdateIntegration.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 - 2020 Jonas Lochmann + * TimeLimit Copyright 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())) } diff --git a/app/src/main/play/de-DE/whatsnew b/app/src/main/play/de-DE/whatsnew index afeb2da..7996b5a 100644 --- a/app/src/main/play/de-DE/whatsnew +++ b/app/src/main/play/de-DE/whatsnew @@ -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 diff --git a/app/src/main/play/en-US/whatsnew b/app/src/main/play/en-US/whatsnew index 4ff2027..09107ef 100644 --- a/app/src/main/play/en-US/whatsnew +++ b/app/src/main/play/en-US/whatsnew @@ -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 diff --git a/app/src/main/res/layout/lock_activity.xml b/app/src/main/res/layout/lock_activity.xml deleted file mode 100644 index 1ec69cf..0000000 --- a/app/src/main/res/layout/lock_activity.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/build.gradle b/build.gradle index 703c8bc..406bfde 100644 --- a/build.gradle +++ b/build.gradle @@ -15,8 +15,8 @@ */ plugins { - id 'com.android.application' version '8.5.1' apply false - id 'com.android.library' version '8.5.1' apply false + id 'com.android.application' version '8.7.2' apply false + id 'com.android.library' version '8.7.2' apply false id 'org.jetbrains.kotlin.android' version "1.9.21" apply false id 'com.google.devtools.ksp' version '1.9.21-1.0.16' apply false id 'androidx.navigation.safeargs' version '2.6.0' apply false diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 9118fc8..bfe49c6 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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.7-all.zip -distributionSha256Sum=194717442575a6f96e1c1befa2c30e9a4fc90f701d7aee33eb879b79e7ff05c0 \ No newline at end of file +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip +distributionSha256Sum=258e722ec21e955201e31447b0aed14201765a3bfbae296a46cf60b70e66db70 \ No newline at end of file