From 161e834955ca9cdac68d0d99196a7a192dda0fa0 Mon Sep 17 00:00:00 2001 From: frankpreel Date: Tue, 20 May 2025 17:09:24 +0200 Subject: [PATCH 1/4] Lint --- .../e/parentalcontrol/MainActivity.kt | 15 ++- .../e/parentalcontrol/UsageStatsManager.kt | 91 ++++++++++++++++--- .../data/ParentalControlStatusList.kt | 41 +++++++++ .../e/parentalcontrol/utils/Constants.kt | 2 + .../e/parentalcontrol/utils/PrefsUtils.kt | 25 ++++- 5 files changed, 156 insertions(+), 18 deletions(-) create mode 100644 app/src/main/java/foundation/e/parentalcontrol/data/ParentalControlStatusList.kt diff --git a/app/src/main/java/foundation/e/parentalcontrol/MainActivity.kt b/app/src/main/java/foundation/e/parentalcontrol/MainActivity.kt index 71bdfad..8b8bad5 100644 --- a/app/src/main/java/foundation/e/parentalcontrol/MainActivity.kt +++ b/app/src/main/java/foundation/e/parentalcontrol/MainActivity.kt @@ -105,6 +105,8 @@ import androidx.core.net.toUri import androidx.lifecycle.lifecycleScope import foundation.e.parentalcontrol.data.AuthenticationType import foundation.e.parentalcontrol.data.Pages +import foundation.e.parentalcontrol.data.ParentalControlStatus +import foundation.e.parentalcontrol.data.ParentalControlStatusList import foundation.e.parentalcontrol.data.ScreenTimes import foundation.e.parentalcontrol.data.isNotLoggedIn import foundation.e.parentalcontrol.providers.AppLoungeData @@ -139,6 +141,7 @@ import foundation.e.parentalcontrol.utils.PrefsUtils import foundation.e.parentalcontrol.utils.PrefsUtils.getFMDStatus import foundation.e.parentalcontrol.utils.PrefsUtils.setFMDStatus import foundation.e.parentalcontrol.utils.SystemUtils +import java.util.Date import kotlin.math.roundToInt import kotlin.time.Duration.Companion.milliseconds import kotlin.time.DurationUnit @@ -658,7 +661,7 @@ class MainActivity : ComponentActivity() { fontWeight = FontWeight.Medium, onCheckedChange = { checkedState = it - PrefsUtils.clearAll() + PrefsUtils.clearAllExcept(Constants.PARENTAL_CONTROL_STATUS_LIST) if (isAdminActive()) { setAdmin(false) } @@ -1344,6 +1347,12 @@ class MainActivity : ComponentActivity() { devicePolicyManager.clearProfileOwner(component) } } + ParentalControlStatusList.setParentalControlStatus( + ParentalControlStatus(Date(System.currentTimeMillis()), activate) + ) + PrefsUtils.saveParentalControlStatusList( + ParentalControlStatusList.getParentalControlStatus() + ) } // Allows to navigate to another screen from external component @@ -1402,7 +1411,9 @@ class MainActivity : ComponentActivity() { Pages.AskPasswordForAdminRemoval -> { AskPassword( onPasswordMatch = { - PrefsUtils.clearAll() + PrefsUtils.clearAllExcept( + Constants.PARENTAL_CONTROL_STATUS_LIST + ) selectedAge = null selectedAllowedScreenTime = 0 selectedTypeAppManagement = null diff --git a/app/src/main/java/foundation/e/parentalcontrol/UsageStatsManager.kt b/app/src/main/java/foundation/e/parentalcontrol/UsageStatsManager.kt index 883e3ac..74c6f15 100644 --- a/app/src/main/java/foundation/e/parentalcontrol/UsageStatsManager.kt +++ b/app/src/main/java/foundation/e/parentalcontrol/UsageStatsManager.kt @@ -30,8 +30,11 @@ import android.os.SystemClock import android.provider.Settings import android.util.Log import androidx.annotation.RequiresPermission +import foundation.e.parentalcontrol.data.ParentalControlStatus +import foundation.e.parentalcontrol.data.ParentalControlStatusList import foundation.e.parentalcontrol.utils.Constants.PACKAGES_TO_EXCLUDE import foundation.e.parentalcontrol.utils.Constants.POPUP_HAS_ALREADY_BEEN_ACTIVATED +import java.util.Date import java.util.concurrent.TimeUnit class UsageStatsManager { @@ -63,20 +66,27 @@ class UsageStatsManager { val intervals = mutableListOf>() for (event in allEvents) { - val pkg = event.packageName - when (event.eventType) { - UsageEvents.Event.MOVE_TO_FOREGROUND -> { - activePackages[pkg] = event.timeStamp - } - UsageEvents.Event.MOVE_TO_BACKGROUND, - UsageEvents.Event.ACTIVITY_STOPPED -> { - val startTime = activePackages.remove(pkg) - if ( - startTime != null && - event.timeStamp > startTime && - pkg !in PACKAGES_TO_EXCLUDE - ) { - intervals.add(startTime to event.timeStamp) + if ( + isParentalControlActivated( + ParentalControlStatusList.getParentalControlStatus(), + event.timeStamp + ) + ) { + val pkg = event.packageName + when (event.eventType) { + UsageEvents.Event.MOVE_TO_FOREGROUND -> { + activePackages[pkg] = event.timeStamp + } + UsageEvents.Event.MOVE_TO_BACKGROUND, + UsageEvents.Event.ACTIVITY_STOPPED -> { + val startTime = activePackages.remove(pkg) + if ( + startTime != null && + event.timeStamp > startTime && + pkg !in PACKAGES_TO_EXCLUDE + ) { + intervals.add(startTime to event.timeStamp) + } } } } @@ -117,6 +127,59 @@ class UsageStatsManager { return result } + fun isParentalControlActivated( + statusList: List, + timeStamp: Long + ): Boolean { + val now = Date() + val day = Calendar.getInstance().apply { time = now } + + // Elements for currents day. + var filtered = + statusList + .filter { + val cal = Calendar.getInstance().apply { time = it.date } + cal.get(Calendar.YEAR) == day.get(Calendar.YEAR) && + cal.get(Calendar.DAY_OF_YEAR) == day.get(Calendar.DAY_OF_YEAR) + } + .sortedBy { it.date } + + if (filtered.isNotEmpty() && !filtered.first().activated) { + // Dummy element in case the first element is not activated. + val startOfDay = + Calendar.getInstance() + .apply { + time = now + set(Calendar.HOUR_OF_DAY, 0) + set(Calendar.MINUTE, 0) + set(Calendar.SECOND, 0) + set(Calendar.MILLISECOND, 0) + } + .time + val startStatus = ParentalControlStatus(startOfDay, true) + filtered = listOf(startStatus) + filtered + } + + var lastActiveStart: Date? = null + + for (status in filtered) { + if (status.activated) { + lastActiveStart = status.date + } else if (lastActiveStart != null) { + if (timeStamp in lastActiveStart.time..status.date.time) { + return true + } + lastActiveStart = null + } + } + + if (lastActiveStart != null && timeStamp >= lastActiveStart.time && timeStamp <= now.time) { + return true + } + + return false + } + fun getStartOfDayInMillis(): Long { val calendar = Calendar.getInstance() calendar.set(Calendar.HOUR_OF_DAY, 0) diff --git a/app/src/main/java/foundation/e/parentalcontrol/data/ParentalControlStatusList.kt b/app/src/main/java/foundation/e/parentalcontrol/data/ParentalControlStatusList.kt new file mode 100644 index 0000000..81daefb --- /dev/null +++ b/app/src/main/java/foundation/e/parentalcontrol/data/ParentalControlStatusList.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2025 MURENA SAS + * + * 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, either version 3 of the License, or + * (at your option) any later version. + * + * 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 foundation.e.parentalcontrol.data + +import java.util.Date + +data class ParentalControlStatus(val date: Date, val activated: Boolean = true) + +object ParentalControlStatusList { + private val parentalControlStatusList: MutableList = mutableListOf() + + fun setParentalControlStatus(parentalControlStatus: ParentalControlStatus) { + if ( + parentalControlStatusList.isNotEmpty() && + parentalControlStatusList.last().activated == parentalControlStatus.activated + ) { + parentalControlStatusList[parentalControlStatusList.lastIndex] = parentalControlStatus + } else { + parentalControlStatusList.add(parentalControlStatus) + } + } + + fun getParentalControlStatus(): List { + return parentalControlStatusList.toList() + } +} diff --git a/app/src/main/java/foundation/e/parentalcontrol/utils/Constants.kt b/app/src/main/java/foundation/e/parentalcontrol/utils/Constants.kt index bb9bf13..82a5856 100644 --- a/app/src/main/java/foundation/e/parentalcontrol/utils/Constants.kt +++ b/app/src/main/java/foundation/e/parentalcontrol/utils/Constants.kt @@ -84,4 +84,6 @@ object Constants { const val BLOCK_ALL_SYSTEM_APP = "blockAllSystemApp" const val POPUP_HAS_ALREADY_BEEN_ACTIVATED = "popupHasAlreadyBeenActivated" + + const val PARENTAL_CONTROL_STATUS_LIST = "parentalControlStatusList" } diff --git a/app/src/main/java/foundation/e/parentalcontrol/utils/PrefsUtils.kt b/app/src/main/java/foundation/e/parentalcontrol/utils/PrefsUtils.kt index 1cb7e0f..35ffebf 100644 --- a/app/src/main/java/foundation/e/parentalcontrol/utils/PrefsUtils.kt +++ b/app/src/main/java/foundation/e/parentalcontrol/utils/PrefsUtils.kt @@ -28,6 +28,7 @@ import foundation.e.parentalcontrol.data.AllowedApp import foundation.e.parentalcontrol.data.AllowedAppList import foundation.e.parentalcontrol.data.AuthenticationType import foundation.e.parentalcontrol.data.LightApplicationInfo +import foundation.e.parentalcontrol.data.ParentalControlStatus import foundation.e.parentalcontrol.data.ScreenTimes import foundation.e.parentalcontrol.data.TypeAppManagement import foundation.e.parentalcontrol.utils.Constants.KEY_FMD_STATUS_NONE @@ -104,6 +105,17 @@ object PrefsUtils { sharedPreferences.edit { putString(Constants.PREF_TYPE_ALLOWED_APPS, json) } } + fun saveParentalControlStatusList(parentalControlStatusList: List) { + val gson = Gson() + val parentalControlStatusList = + parentalControlStatusList.map { + ParentalControlStatus(date = it.date, activated = it.activated) + } + + val json = gson.toJson(parentalControlStatusList) + sharedPreferences.edit { putString(Constants.PARENTAL_CONTROL_STATUS_LIST, json) } + } + fun restoreAllowedAppList(context: Context) { val gson = Gson() val json = sharedPreferences.getString(Constants.PREF_TYPE_ALLOWED_APPS, null) @@ -128,8 +140,17 @@ object PrefsUtils { } } - fun clearAll() { - sharedPreferences.edit() { clear() } + fun clearAllExcept(vararg keysToKeep: String) { + val allPrefs = sharedPreferences.all + val editor = sharedPreferences.edit() + + for ((key, _) in allPrefs) { + if (key !in keysToKeep) { + editor.remove(key) + } + } + + editor.apply() } fun getSharePref(): SharedPreferences { -- GitLab From c34353f1b2067ca1d6c84e482bbd90cb3a08301a Mon Sep 17 00:00:00 2001 From: frankpreel Date: Wed, 21 May 2025 09:32:20 +0200 Subject: [PATCH 2/4] Better compute PaCo activate status by unsing intervals rather than single timestamp --- .../e/parentalcontrol/UsageStatsManager.kt | 77 +++++++++++++------ 1 file changed, 52 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/foundation/e/parentalcontrol/UsageStatsManager.kt b/app/src/main/java/foundation/e/parentalcontrol/UsageStatsManager.kt index 74c6f15..dabc533 100644 --- a/app/src/main/java/foundation/e/parentalcontrol/UsageStatsManager.kt +++ b/app/src/main/java/foundation/e/parentalcontrol/UsageStatsManager.kt @@ -66,27 +66,20 @@ class UsageStatsManager { val intervals = mutableListOf>() for (event in allEvents) { - if ( - isParentalControlActivated( - ParentalControlStatusList.getParentalControlStatus(), - event.timeStamp - ) - ) { - val pkg = event.packageName - when (event.eventType) { - UsageEvents.Event.MOVE_TO_FOREGROUND -> { - activePackages[pkg] = event.timeStamp - } - UsageEvents.Event.MOVE_TO_BACKGROUND, - UsageEvents.Event.ACTIVITY_STOPPED -> { - val startTime = activePackages.remove(pkg) - if ( - startTime != null && - event.timeStamp > startTime && - pkg !in PACKAGES_TO_EXCLUDE - ) { - intervals.add(startTime to event.timeStamp) - } + val pkg = event.packageName + when (event.eventType) { + UsageEvents.Event.MOVE_TO_FOREGROUND -> { + activePackages[pkg] = event.timeStamp + } + UsageEvents.Event.MOVE_TO_BACKGROUND, + UsageEvents.Event.ACTIVITY_STOPPED -> { + val startTime = activePackages.remove(pkg) + if ( + startTime != null && + event.timeStamp > startTime && + pkg !in PACKAGES_TO_EXCLUDE + ) { + intervals.add(startTime to event.timeStamp) } } } @@ -99,7 +92,20 @@ class UsageStatsManager { } } - val merged = mergeIntervals(intervals) + val intervalsFiltered = mutableListOf>() + + for (interval in intervals) { + if ( + isParentalControlActivated( + ParentalControlStatusList.getParentalControlStatus(), + interval.first, + interval.second + ) + ) { + intervalsFiltered.add(interval) + } + } + val merged = mergeIntervals(intervalsFiltered) val totalUsage = merged.sumOf { it.second - it.first } @@ -129,7 +135,8 @@ class UsageStatsManager { fun isParentalControlActivated( statusList: List, - timeStamp: Long + startTimeStamp: Long, + endTimeStamp: Long ): Boolean { val now = Date() val day = Calendar.getInstance().apply { time = now } @@ -160,20 +167,40 @@ class UsageStatsManager { filtered = listOf(startStatus) + filtered } + // case when PACO has been actived before today + if (filtered.isEmpty()) { + if (statusList.isEmpty()) { + return false + } + val lastStatus = statusList.get(statusList.lastIndex) + if (!lastStatus.activated) { + return false + } + return true + } + var lastActiveStart: Date? = null for (status in filtered) { if (status.activated) { lastActiveStart = status.date } else if (lastActiveStart != null) { - if (timeStamp in lastActiveStart.time..status.date.time) { + if ( + (startTimeStamp in lastActiveStart.time..status.date.time) && + (endTimeStamp in lastActiveStart.time..status.date.time) + ) { return true } lastActiveStart = null } } - if (lastActiveStart != null && timeStamp >= lastActiveStart.time && timeStamp <= now.time) { + if ( + (lastActiveStart != null && + startTimeStamp >= lastActiveStart.time && + startTimeStamp <= now.time) && + (endTimeStamp >= lastActiveStart.time && endTimeStamp <= now.time) + ) { return true } -- GitLab From 3c2a579bdd0b4c5a34663ef1a8eb4b4b8f92e5a9 Mon Sep 17 00:00:00 2001 From: Frank Preel Date: Wed, 21 May 2025 07:32:49 +0000 Subject: [PATCH 3/4] Apply 1 suggestion(s) to 1 file(s) Co-authored-by: Jonathan Klee --- .../main/java/foundation/e/parentalcontrol/UsageStatsManager.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/foundation/e/parentalcontrol/UsageStatsManager.kt b/app/src/main/java/foundation/e/parentalcontrol/UsageStatsManager.kt index 74c6f15..c442423 100644 --- a/app/src/main/java/foundation/e/parentalcontrol/UsageStatsManager.kt +++ b/app/src/main/java/foundation/e/parentalcontrol/UsageStatsManager.kt @@ -135,7 +135,7 @@ class UsageStatsManager { val day = Calendar.getInstance().apply { time = now } // Elements for currents day. - var filtered = + var elementsForToday = statusList .filter { val cal = Calendar.getInstance().apply { time = it.date } -- GitLab From da722d646478c596cc2c12a0b125981def739a17 Mon Sep 17 00:00:00 2001 From: frankpreel Date: Wed, 21 May 2025 09:35:53 +0200 Subject: [PATCH 4/4] Refactor for applyed suggestion --- .../foundation/e/parentalcontrol/UsageStatsManager.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/foundation/e/parentalcontrol/UsageStatsManager.kt b/app/src/main/java/foundation/e/parentalcontrol/UsageStatsManager.kt index 5ce5b29..fb8e628 100644 --- a/app/src/main/java/foundation/e/parentalcontrol/UsageStatsManager.kt +++ b/app/src/main/java/foundation/e/parentalcontrol/UsageStatsManager.kt @@ -151,7 +151,7 @@ class UsageStatsManager { } .sortedBy { it.date } - if (filtered.isNotEmpty() && !filtered.first().activated) { + if (elementsForToday.isNotEmpty() && !elementsForToday.first().activated) { // Dummy element in case the first element is not activated. val startOfDay = Calendar.getInstance() @@ -164,11 +164,11 @@ class UsageStatsManager { } .time val startStatus = ParentalControlStatus(startOfDay, true) - filtered = listOf(startStatus) + filtered + elementsForToday = listOf(startStatus) + elementsForToday } - // case when PACO has been actived before today - if (filtered.isEmpty()) { + // case when PACO has been activated before today + if (elementsForToday.isEmpty()) { if (statusList.isEmpty()) { return false } @@ -181,7 +181,7 @@ class UsageStatsManager { var lastActiveStart: Date? = null - for (status in filtered) { + for (status in elementsForToday) { if (status.activated) { lastActiveStart = status.date } else if (lastActiveStart != null) { -- GitLab