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 {
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'

View file

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

View file

@ -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<List<UsedTimeListItem>>
// breaking it into multiple lines causes issues during compilation ...
// this warns about an unused column, but this column is used in the ORDER BY
@SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
@Query("SELECT 2 AS type, start_time_of_day AS startMinuteOfDay, end_time_of_day AS endMinuteOfDay, used_time AS duration, day_of_epoch AS day, NULL AS lastUsage, NULL AS maxSessionDuration, NULL AS pauseDuration, category.id AS categoryId, category.title AS categoryTitle FROM used_time JOIN category ON (used_time.category_id = category.id) WHERE category.child_id = :userId UNION ALL SELECT 1 AS type, start_minute_of_day AS startMinuteOfDay, end_minute_of_day AS endMinuteOfDay, last_session_duration AS duration, NULL AS day, last_usage AS lastUsage, max_session_duration AS maxSessionDuration, session_pause_duration AS pauseDuration, category.id AS categoryId, category.title AS categoryTitle FROM session_duration JOIN category ON (session_duration.category_id = category.id) WHERE category.child_id = :userId ORDER BY type, day DESC, lastUsage DESC, startMinuteOfDay, endMinuteOfDay, categoryId")
abstract fun getUsedTimeListItemsByUserId(userId: String): Flow<List<UsedTimeListItem>>
}

View file

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

View file

@ -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,

View file

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

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
* TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -17,8 +17,13 @@ package io.timelimit.android.ui
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.navigationBarsIgnoringVisibility
import androidx.compose.foundation.layout.statusBarsIgnoringVisibility
import androidx.compose.foundation.layout.systemBarsIgnoringVisibility
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
@ -37,6 +42,7 @@ import io.timelimit.android.ui.model.Screen
import io.timelimit.android.ui.model.Title
import io.timelimit.android.ui.model.UpdateStateCommand
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun ScreenScaffold(
screen: Screen?,
@ -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
)
}

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
* TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -18,15 +18,35 @@ package io.timelimit.android.ui.lock
import android.app.ActivityManager
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import androidx.activity.OnBackPressedCallback
import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.MutableLiveData
import androidx.viewpager.widget.ViewPager
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.Tab
import androidx.compose.material.TabRow
import androidx.compose.material.TabRowDefaults
import androidx.compose.material.TabRowDefaults.tabIndicatorOffset
import androidx.compose.material.Text
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.fragment.compose.AndroidFragment
import androidx.lifecycle.asFlow
import androidx.lifecycle.map
import io.timelimit.android.R
import io.timelimit.android.databinding.LockActivityBinding
import io.timelimit.android.data.model.UserType
import io.timelimit.android.extensions.showSafe
import io.timelimit.android.logic.BlockingReason
import io.timelimit.android.logic.DefaultAppLogic
@ -34,11 +54,12 @@ import io.timelimit.android.sync.network.UpdatePrimaryDeviceRequestType
import io.timelimit.android.u2f.U2fManager
import io.timelimit.android.u2f.protocol.U2FDevice
import io.timelimit.android.ui.IsAppInForeground
import io.timelimit.android.ui.ScreenScaffold
import io.timelimit.android.ui.Theme
import io.timelimit.android.ui.login.AuthTokenLoginProcessor
import io.timelimit.android.ui.login.NewLoginFragment
import io.timelimit.android.ui.main.ActivityViewModel
import io.timelimit.android.ui.main.ActivityViewModelHolder
import io.timelimit.android.ui.main.AuthenticationFab
import io.timelimit.android.ui.manage.child.primarydevice.UpdatePrimaryDeviceDialogFragment
import io.timelimit.android.ui.util.SyncStatusModel
@ -85,17 +106,113 @@ class LockActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
null
}
private val showAuth = MutableLiveData<Boolean>().apply { value = false }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val isNightMode =
(resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) ==
Configuration.UI_MODE_NIGHT_YES
enableEdgeToEdge(
statusBarStyle = SystemBarStyle.dark(
if (isNightMode) android.graphics.Color.TRANSPARENT
else resources.getColor(R.color.colorPrimaryDark)
)
)
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<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 }
@ -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) {

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

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
* TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -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()

View file

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

View file

@ -1,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
import android.content.res.Configuration
import android.os.Bundle
import android.view.LayoutInflater
import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import io.timelimit.android.R
import io.timelimit.android.databinding.UpdateActivityBinding
import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.ui.ScreenScaffold
import io.timelimit.android.ui.Theme
class UpdateActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = DataBindingUtil.setContentView<UpdateActivityBinding>(this, R.layout.update_activity)
val isNightMode =
(resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) ==
Configuration.UI_MODE_NIGHT_YES
UpdateView.bind(
view = binding.update,
lifecycleOwner = this,
fragmentManager = supportFragmentManager,
appLogic = DefaultAppLogic.with(this)
enableEdgeToEdge(
statusBarStyle = SystemBarStyle.dark(
if (isNightMode) android.graphics.Color.TRANSPARENT
else resources.getColor(R.color.colorPrimaryDark)
)
)
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
* it under the terms of the GNU General Public License as published by
@ -19,6 +19,7 @@ import android.appwidget.AppWidgetManager
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.fragment.app.FragmentActivity
import io.timelimit.android.R
@ -30,6 +31,8 @@ class WidgetConfigActivity: FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
if (model.state.value == WidgetConfigModel.State.WaitingForInit) {
model.init(
intent?.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID)

View file

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

View file

@ -1,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

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

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 {
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

View file

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