diff --git a/HEADER b/HEADER index 127f54a23770d9adf38b20e23c01d7059b406261..84b17628118a35aab8450306fe22ac23f73c3227 100644 --- a/HEADER +++ b/HEADER @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 MURENA SAS + * 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 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b8864cd8bce737c41e397d87eeb7c0926e1db3dc..d316cc46058b8a6993a1961440c712162e15317f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -7,6 +7,12 @@ + + + + + @@ -15,6 +21,12 @@ + + + + + + + + + + + + + + + + + + . + * + */ +package foundation.e.parentalcontrol + +import android.Manifest +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import androidx.annotation.RequiresPermission +import foundation.e.parentalcontrol.data.ScreenTimes +import foundation.e.parentalcontrol.utils.Constants +import foundation.e.parentalcontrol.utils.Constants.BLOCK_ALL_SYSTEM_APP +import foundation.e.parentalcontrol.utils.Constants.POPUP_HAS_ALREADY_BEEN_ACTIVATED +import foundation.e.parentalcontrol.utils.PrefsUtils +import java.time.LocalTime + +class AlarmReceiver : BroadcastReceiver() { + + companion object { + const val WHEN_ACTIVATE_POPUP_IN_MS = 10 * 60 * 1000 + const val ONE_DAY_IN_MS = 24 * 60 * 60 * 1000 + } + + @RequiresPermission(Manifest.permission.SCHEDULE_EXACT_ALARM) + override fun onReceive(context: Context, intent: Intent?) { + var allowedScreenTime = PrefsUtils.getAllowedScreenTime() + if (allowedScreenTime == ScreenTimes.DISABLE.value) { + allowedScreenTime = ONE_DAY_IN_MS + } else { + allowedScreenTime = allowedScreenTime * 3600 * 1000 + } + + val popupHasAlreadyBeenActive = + intent?.getBooleanExtra(POPUP_HAS_ALREADY_BEEN_ACTIVATED, false) ?: false + val stats = UsageStatsManager() + val res = stats.showTime(context) + if (allowedScreenTime <= res) { + blockApp(true, context) + stats.activeAlarm( + context, + (ONE_DAY_IN_MS - LocalTime.now().toSecondOfDay() * 1000).toLong(), + popupHasAlreadyBeenActive + ) + } else if (res >= allowedScreenTime - WHEN_ACTIVATE_POPUP_IN_MS) { + if (!popupHasAlreadyBeenActive) { + sendPopup(context) + popupHasAlreadyBeenActive + } + stats.activeAlarm(context, (allowedScreenTime - res), popupHasAlreadyBeenActive) + } else if (res < allowedScreenTime) { + blockApp(false, context) + stats.activeAlarm( + context, + (allowedScreenTime - (res + WHEN_ACTIVATE_POPUP_IN_MS)), + false + ) + } + } + + fun blockApp(blockAllSystemApp: Boolean, context: Context) { + val broadcastIntent = Intent() + broadcastIntent.action = Constants.BLOCKED_SYSTEM_APP + broadcastIntent.setClass(context, DeviceAdmin::class.java) + broadcastIntent.putExtra(BLOCK_ALL_SYSTEM_APP, blockAllSystemApp) + context.sendBroadcast(broadcastIntent) + } + + private fun sendPopup(context: Context) { + val intent = Intent(context, OverlayService::class.java) + context.startService(intent) + } +} diff --git a/app/src/main/java/foundation/e/parentalcontrol/DeviceAdmin.kt b/app/src/main/java/foundation/e/parentalcontrol/DeviceAdmin.kt index ff7ce0f74a790f0bb10155035cdd3df9fd2cf468..328a816a4409b8ff507ba407673e90a1e3105b80 100644 --- a/app/src/main/java/foundation/e/parentalcontrol/DeviceAdmin.kt +++ b/app/src/main/java/foundation/e/parentalcontrol/DeviceAdmin.kt @@ -22,15 +22,21 @@ import android.app.admin.DevicePolicyManager import android.content.ComponentName import android.content.Context import android.content.Intent +import android.os.Handler +import android.os.Looper import android.util.Log import android.widget.Toast import foundation.e.parentalcontrol.data.DnsManager import foundation.e.parentalcontrol.ui.view.MainUI import foundation.e.parentalcontrol.utils.Constants +import foundation.e.parentalcontrol.utils.Constants.BLOCK_ALL_SYSTEM_APP +import foundation.e.parentalcontrol.utils.Constants.DELAY_WAIT_OWNER_ACCESS +import foundation.e.parentalcontrol.utils.PrefsUtils import java.util.Objects class DeviceAdmin : DeviceAdminReceiver() { private val TAG = "DeviceAdmin" + private var blockAllSystemApp = false /** @return Returns an instance of a DevicePolicyManager object */ fun getDevicePolicyManager(context: Context): DevicePolicyManager { @@ -55,7 +61,17 @@ class DeviceAdmin : DeviceAdminReceiver() { } override fun onReceive(context: Context, intent: Intent) { + when (Objects.requireNonNull(intent.action)) { + Intent.ACTION_BOOT_COMPLETED, -> { + if (isAdminActive(context)) { + // Reschedule Alarm on reboot and restore Allowed apps. + PrefsUtils.init(context) + val stats = UsageStatsManager() + stats.activeAlarm(context, 0, false) + PrefsUtils.restoreAllowedAppList(context) + } + } DevicePolicyManager.ACTION_PROFILE_OWNER_CHANGED, DevicePolicyManager.ACTION_DEVICE_OWNER_CHANGED -> { Log.d(TAG, intent.action!!) @@ -68,6 +84,11 @@ class DeviceAdmin : DeviceAdminReceiver() { } setSettings(context) } + Constants.BLOCKED_SYSTEM_APP -> { + blockAllSystemApp = intent.getBooleanExtra(BLOCK_ALL_SYSTEM_APP, false) + onSetScreenTimeIsOver(blockAllSystemApp) + waitOwnerAccess(context) + } Constants.RESET_PRIVATE_DNS -> { if (isAdminActive(context)) { val mainUI = MainUI(context) @@ -97,4 +118,23 @@ class DeviceAdmin : DeviceAdminReceiver() { val isDeviceOwner = isDeviceOwnerApp(context) return isProfileOwner || isDeviceOwner } + + private fun waitOwnerAccess(context: Context) { + // Important: needs to be delayed. + // see Handler(Looper.getMainLooper()).postDelayed({ onStartUp() }, DELAY_CONFIRM_PASSWORD) + Handler(Looper.getMainLooper()) + .postDelayed( + { + if (isAdminActive(context)) { + val mainUI = MainUI(context) + mainUI.blockApps(blockAllSystemApp) + } + }, + DELAY_WAIT_OWNER_ACCESS + ) + } + + private fun onSetScreenTimeIsOver(screenTimeIsOver: Boolean) { + PrefsUtils.setScreenTimeIsOver(screenTimeIsOver) + } } diff --git a/app/src/main/java/foundation/e/parentalcontrol/MainActivity.kt b/app/src/main/java/foundation/e/parentalcontrol/MainActivity.kt index 65b36cb570d02f5dc963707f3036c847c228ecc5..8b8bad5ab198b143bde67a013be84e6a24a5e968 100644 --- a/app/src/main/java/foundation/e/parentalcontrol/MainActivity.kt +++ b/app/src/main/java/foundation/e/parentalcontrol/MainActivity.kt @@ -17,13 +17,13 @@ */ package foundation.e.parentalcontrol +import android.Manifest +import android.annotation.SuppressLint import android.app.admin.DevicePolicyManager import android.app.admin.DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE import android.content.ComponentName 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.Handler @@ -34,16 +34,24 @@ import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.BackHandler import androidx.activity.compose.setContent +import androidx.annotation.RequiresPermission import androidx.browser.customtabs.CustomTabsIntent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions @@ -51,6 +59,7 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.Lock import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button @@ -61,6 +70,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -68,16 +78,18 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalClipboardManager -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.TextStyle @@ -89,8 +101,13 @@ import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +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 import foundation.e.parentalcontrol.ui.buttons.ToggleWithText @@ -103,12 +120,31 @@ import foundation.e.parentalcontrol.ui.view.AskPassword import foundation.e.parentalcontrol.ui.view.AuthenticationTypeSelectionView import foundation.e.parentalcontrol.ui.view.MainUI import foundation.e.parentalcontrol.ui.view.SelectAge +import foundation.e.parentalcontrol.ui.view.SelectAllowedApps +import foundation.e.parentalcontrol.ui.view.SelectAllowedScreenTime +import foundation.e.parentalcontrol.ui.view.SelectManageTypeApp import foundation.e.parentalcontrol.ui.view.selectedAge +import foundation.e.parentalcontrol.ui.view.selectedAllowedScreenTime +import foundation.e.parentalcontrol.ui.view.selectedTypeAppManagement import foundation.e.parentalcontrol.utils.Constants +import foundation.e.parentalcontrol.utils.Constants.DELAY_CONFIRM_PASSWORD +import foundation.e.parentalcontrol.utils.Constants.DELAY_DISABLE_FMD +import foundation.e.parentalcontrol.utils.Constants.KEY_FIND_MY_DEVICE +import foundation.e.parentalcontrol.utils.Constants.KEY_FIND_MY_DEVICE_STATUS +import foundation.e.parentalcontrol.utils.Constants.KEY_FMD_STATUS_DONE +import foundation.e.parentalcontrol.utils.Constants.KEY_FMD_STATUS_ENROLLING +import foundation.e.parentalcontrol.utils.Constants.KEY_FMD_STATUS_NONE +import foundation.e.parentalcontrol.utils.Constants.KEY_PARENTAL_CONTROL_AUTHENTICATE import foundation.e.parentalcontrol.utils.CryptUtils import foundation.e.parentalcontrol.utils.Dimens 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 import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -123,11 +159,32 @@ class MainActivity : ComponentActivity() { super.onCreate(savedInstanceState) PrefsUtils.init(mActivity) - onStartUp() } private fun onStartUp() { + + if (BuildConfig.DEBUG) { + lifecycleScope.launch { + val stats = UsageStatsManager() + val totalUsageMillis = stats.showTime(mActivity) + val totalUsageMinutes = totalUsageMillis.milliseconds.toDouble(DurationUnit.MINUTES) + Toast.makeText( + mActivity, + "Usage: ${totalUsageMinutes.roundToInt()} min", + Toast.LENGTH_SHORT + ) + .show() + } + } + + if (intent?.action == KEY_PARENTAL_CONTROL_AUTHENTICATE) { + if (isAdminActive()) { + mainScreen(page = Pages.AskPasswordForAppInstallation) + } + return // do NOT startUp + } + if (SystemUtils.getUserId() != 0) { mainScreen(page = Pages.NotMainUser) } else if (isAdminSet()) { @@ -146,6 +203,19 @@ class MainActivity : ComponentActivity() { !PrefsUtils.getPassword().isNullOrEmpty()) } + private fun disableFMDAccess() { + // Important: needs to be delayed. + // see Handler(Looper.getMainLooper()).postDelayed({ onStartUp() }, DELAY_CONFIRM_PASSWORD) + Handler(Looper.getMainLooper()) + .postDelayed( + { + val mainUI = MainUI(mActivity) + mainUI.manageFmD(true) + }, + DELAY_DISABLE_FMD + ) + } + @Composable fun SetRestrictionsScreen() { BackHandler(onBack = { onStartUp() }) @@ -166,10 +236,16 @@ class MainActivity : ComponentActivity() { setResult(RESULT_OK) } finishAfterTransition() + + if (isAdminSet()) disableFMDAccess() else setFMDStatus(KEY_FMD_STATUS_NONE) } override fun onResume() { super.onResume() + + if (getFMDStatus() == KEY_FMD_STATUS_ENROLLING) { + setFMDStatus(KEY_FMD_STATUS_DONE) + } if (isAdminActive()) { onStartUp() } @@ -309,6 +385,8 @@ class MainActivity : ComponentActivity() { verticalArrangement = Arrangement.spacedBy(Dimens.SCREEN_PADDING), horizontalAlignment = Alignment.CenterHorizontally ) { + @SuppressLint("ScheduleExactAlarm") + @RequiresPermission(Manifest.permission.SCHEDULE_EXACT_ALARM) fun confirmPassword() { val originalPass = passText val confirmedPass = confirmPassText @@ -332,16 +410,23 @@ class MainActivity : ComponentActivity() { confirmPassText = "" onSetAge() + onSetAllowedScreenTime() + onSetTypeAppManagement() + onSetAllowedApps() + val stats = UsageStatsManager() + stats.activeAlarm(mActivity, 0, false) if (!isAdminActive()) { setAdmin(true) } if (SystemUtils.isSetupFinished(mActivity)) { - Handler(Looper.getMainLooper()).postDelayed({ onStartUp() }, 500L) + Handler(Looper.getMainLooper()) + .postDelayed({ onStartUp() }, DELAY_CONFIRM_PASSWORD) } else { onExitApp(true) } + performSelectedAppManagement() } else { isError = true if ( @@ -564,7 +649,7 @@ class MainActivity : ComponentActivity() { ) } - ActivateAdminSummary() + ActivateAdminSummary(false) var checkedState by remember { mutableStateOf(false) } var moveToNext by remember { mutableStateOf(false) } @@ -576,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) } @@ -595,53 +680,116 @@ class MainActivity : ComponentActivity() { } @Composable - fun ActivateAdminSummary() { - val clipboardManager = LocalClipboardManager.current + fun ActivateAdminSummary(collapsed: Boolean = false) { + var isExpanded by remember { mutableStateOf(!collapsed) } + + Column(modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp)) { + if (collapsed) { + val animationDelay = 250 + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier.fillMaxWidth() + .background( + colorResource(foundation.e.elib.R.color.e_background_overlay), + shape = RoundedCornerShape(Dimens.SCREEN_PADDING / 2) + ) + .clickable { isExpanded = !isExpanded } + .padding( + horizontal = Dimens.SCREEN_PADDING / 2, + vertical = Dimens.SCREEN_PADDING / 4 + ) + ) { + Text( + text = stringResource(R.string.activate_admin_summary_title), + color = colorResource(foundation.e.elib.R.color.e_primary_text_color), + modifier = Modifier.weight(1f), + fontWeight = FontWeight.Medium, + ) - Text( - text = stringResource(R.string.activate_admin_summary), - color = colorResource(foundation.e.elib.R.color.e_primary_text_color), - fontSize = 15.sp, - maxLines = Int.MAX_VALUE, - overflow = TextOverflow.Clip, - modifier = Modifier.padding(bottom = Dimens.SCREEN_PADDING / 2) - ) + val rotation by + animateFloatAsState( + targetValue = if (isExpanded) 180f else 0f, + animationSpec = tween(durationMillis = animationDelay), + label = "ArrowRotation" + ) - val docUrl = stringResource(R.string.e_foundation_docs_link) - Text( - text = docUrl, - fontSize = 15.sp, - color = colorResource(foundation.e.elib.R.color.e_accent), - maxLines = Int.MAX_VALUE, - overflow = TextOverflow.Clip, - style = TextStyle(textDecoration = TextDecoration.Underline), - modifier = - Modifier.padding(bottom = 8.dp).pointerInput(Unit) { - detectTapGestures( - onTap = { - try { - val customTabsIntent = - CustomTabsIntent.Builder().setShowTitle(true).build() - customTabsIntent.launchUrl(mActivity, Uri.parse(docUrl)) - } catch (e: Exception) { - // Fallback to default browser - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(docUrl)) - startActivity(intent) - } - }, - onLongPress = { - // Copy to clipboard - clipboardManager.setText(AnnotatedString(docUrl)) - Toast.makeText( - mActivity, - getString(R.string.link_copied_to_clipboard), - Toast.LENGTH_SHORT - ) - .show() - } + Icon( + imageVector = Icons.Default.KeyboardArrowDown, + contentDescription = null, + modifier = Modifier.graphicsLayer(rotationZ = rotation), + tint = colorResource(foundation.e.elib.R.color.e_primary_text_color) ) } - ) + + AnimatedVisibility( + visible = isExpanded, + enter = + androidx.compose.animation.expandVertically( + animationSpec = tween(durationMillis = animationDelay) + ), + exit = + androidx.compose.animation.shrinkVertically( + animationSpec = tween(durationMillis = animationDelay) + ) + ) { + ContentAdminSummary() + } + } else { + ContentAdminSummary() + } + } + } + + @Composable + private fun ContentAdminSummary() { + val docUrl = stringResource(R.string.e_foundation_docs_link) + val clipboardManager = LocalClipboardManager.current + Column { + Text( + text = stringResource(R.string.activate_admin_summary), + color = colorResource(foundation.e.elib.R.color.e_primary_text_color), + fontSize = 15.sp, + maxLines = Int.MAX_VALUE, + overflow = TextOverflow.Clip, + modifier = Modifier.padding(bottom = Dimens.SCREEN_PADDING / 2), + ) + + Text( + text = docUrl, + fontSize = 15.sp, + color = colorResource(foundation.e.elib.R.color.e_accent), + maxLines = Int.MAX_VALUE, + overflow = TextOverflow.Clip, + style = TextStyle(textDecoration = TextDecoration.Underline), + modifier = + Modifier.padding(bottom = 8.dp).pointerInput(Unit) { + detectTapGestures( + onTap = { + try { + val customTabsIntent = + CustomTabsIntent.Builder().setShowTitle(true).build() + customTabsIntent.launchUrl(mActivity, docUrl.toUri()) + } catch (e: Exception) { + // Fallback to default browser + val intent = Intent(Intent.ACTION_VIEW, docUrl.toUri()) + startActivity(intent) + } + }, + onLongPress = { + // Copy to clipboard + clipboardManager.setText(AnnotatedString(docUrl)) + Toast.makeText( + mActivity, + getString(R.string.link_copied_to_clipboard), + Toast.LENGTH_SHORT + ) + .show() + } + ) + } + ) + } } @Composable @@ -661,28 +809,272 @@ class MainActivity : ComponentActivity() { Column( modifier = Modifier.fillMaxSize() - .padding(start = Dimens.SCREEN_PADDING, end = Dimens.SCREEN_PADDING), + .padding(start = Dimens.SCREEN_PADDING * 2, end = Dimens.SCREEN_PADDING * 2), horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.Top ) { + FindMyDevice() + if (showLoginDialog && PrefsUtils.getBlockedApps() == null) { + val mainUI = MainUI(mActivity) + + if (getFMDStatus() == KEY_FMD_STATUS_DONE) { + AlertDialog( + onDismissRequest = { + showLoginDialog = false + mainUI.blockAllUserApps(true) + }, + title = { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.Default.Info, + contentDescription = "Info icon", + tint = Color.Gray + ) + Text( + text = stringResource(R.string.login_required), + fontSize = 20.sp, + modifier = Modifier.padding(start = Dimens.SCREEN_PADDING / 2) + ) + } + }, + text = { + Text( + text = stringResource(R.string.confirm_dialog_summar), + color = + colorResource( + id = foundation.e.elib.R.color.e_secondary_text_color + ), + maxLines = Int.MAX_VALUE, + overflow = TextOverflow.Clip, + ) + }, + confirmButton = { + Text( + modifier = + Modifier.clickable { + showLoginDialog = false + val launchIntent: Intent? = + packageManager.getLaunchIntentForPackage( + Constants.APP_LOUNGE_PKG + ) + if (launchIntent != null) { + launchIntent.putExtra( + Constants.REQUEST_GPLAY_LOGIN, + true + ) + launchIntent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP + startActivity(launchIntent) + } + } + .padding(start = Dimens.SCREEN_PADDING / 2), + color = colorResource(id = foundation.e.elib.R.color.e_accent), + text = stringResource(R.string.launch_applounge).uppercase(), + fontSize = 14.sp + ) + }, + dismissButton = { + Text( + modifier = + Modifier.clickable { + showLoginDialog = false + mainUI.blockAllUserApps(true) + }, + color = colorResource(id = foundation.e.elib.R.color.e_accent), + text = stringResource(R.string.discard).uppercase(), + fontSize = 14.sp + ) + }, + shape = RoundedCornerShape(4.dp) + ) + } + } + + ActivateAdminSummary(true) + + ToggleWithText( + text = stringResource(R.string.activate_parental_control), + isChecked = isAdminActive(), + fontWeight = FontWeight.Medium, + onCheckedChange = { + if (it) { + setAdmin(true) + disableFMDAccess() + } else { + mainScreen(page = Pages.AskPasswordForAdminRemoval) + } + }, + ) + + SelectAge( + onRadioClick = { mainScreen(page = Pages.AskPasswordForAgeReset) }, + onNextClick = {} + ) + + Box(modifier = Modifier.fillMaxSize()) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Row( + modifier = + Modifier.fillMaxWidth() + .padding(vertical = Dimens.SCREEN_PADDING / 2) + .clip(RoundedCornerShape(16.dp)) + .background( + colorResource(foundation.e.elib.R.color.e_background_overlay) + ) + .padding(14.dp), + verticalAlignment = Alignment.CenterVertically + ) { + val allowedScreenTime = PrefsUtils.getAllowedScreenTime() + val allowedScreenTimeButton: String + val line1: String = stringResource(R.string.allowed_screen_time_allowed) + val line2: String + + if (allowedScreenTime == ScreenTimes.DISABLE.value) { + allowedScreenTimeButton = + stringResource(R.string.allowed_screen_time_set).uppercase() + line2 = stringResource(R.string.allowed_screen_time_not_set) + } else { + allowedScreenTimeButton = + stringResource(R.string.allowed_screen_time_change).uppercase() + line2 = + when (allowedScreenTime) { + ScreenTimes.NINE_HOURS.value -> + stringResource(R.string.slider_9) + ScreenTimes.ONE_HOUR.value, + ScreenTimes.ZERO_HOUR.value -> + "$allowedScreenTime ${stringResource(R.string.allowed_screen_time_unit)}" + else -> + "$allowedScreenTime ${stringResource(R.string.allowed_screen_time_unit_s)}" + } + } + + Column(modifier = Modifier.weight(1f)) { + Text( + text = line1, + style = MaterialTheme.typography.bodyMedium, + color = + colorResource(foundation.e.elib.R.color.e_primary_text_color), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = line2, + style = + MaterialTheme.typography.bodyMedium.copy( + fontWeight = FontWeight.Bold + ), + color = + colorResource(foundation.e.elib.R.color.e_primary_text_color), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + + Spacer(modifier = Modifier.width(Dimens.SCREEN_PADDING / 2)) + + Text( + text = allowedScreenTimeButton, + color = colorResource(foundation.e.elib.R.color.e_accent), + fontSize = 14.sp, + modifier = + Modifier.clickable( + onClick = { + mainScreen(page = Pages.AskPasswordForScreenTimeReset) + } + ) + .wrapContentWidth(Alignment.End), + maxLines = 2 + ) + } + + Column(modifier = Modifier.fillMaxWidth()) { + Text( + text = stringResource(R.string.manage_app_use).uppercase(), + color = colorResource(foundation.e.elib.R.color.e_accent), + modifier = + Modifier.padding(top = Dimens.SCREEN_PADDING) + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .background( + colorResource( + foundation.e.elib.R.color.e_background_overlay + ) + ) + .padding(14.dp) + .clickable { mainScreen(page = Pages.AskPasswordForAppsReset) } + .wrapContentWidth(Alignment.CenterHorizontally), + fontSize = 14.sp, + ) + + Text( + text = stringResource(R.string.change_my_security_code).uppercase(), + color = colorResource(foundation.e.elib.R.color.e_accent), + modifier = + Modifier.padding(top = Dimens.SCREEN_PADDING) + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .background( + colorResource( + foundation.e.elib.R.color.e_background_overlay + ) + ) + .padding(14.dp) + .clickable { + mainScreen(page = Pages.AskPasswordForPasswordReset) + } + .wrapContentWidth(Alignment.CenterHorizontally), + fontSize = 14.sp, + ) + } + } + OnDebugBuild() + } + } + } + + @Composable + private fun FindMyDevice() { + + var statusFMD = false + val uri = KEY_FIND_MY_DEVICE.toUri() + val cursor = this.contentResolver.query(uri, null, null, null, null) + cursor?.use { + if (it.moveToFirst()) { + val statusValue = it.getInt(it.getColumnIndexOrThrow(KEY_FIND_MY_DEVICE_STATUS)) + statusFMD = statusValue == 1 + } + } + + if ( + !statusFMD && + (getFMDStatus() == KEY_FMD_STATUS_NONE || + getFMDStatus() == KEY_FMD_STATUS_ENROLLING) + ) { + + var dialogDisplayed by remember { mutableStateOf(false) } + val onDismissAction = { + setFMDStatus(KEY_FMD_STATUS_DONE) + dialogDisplayed = false + } + if (dialogDisplayed) { AlertDialog( - onDismissRequest = { - showLoginDialog = false - mainUI.blockAllUserApps(true) - }, + onDismissRequest = { onDismissAction() }, title = { Row( verticalAlignment = Alignment.CenterVertically, ) { Icon( - imageVector = Icons.Default.Info, - contentDescription = "Info icon", - tint = Color.Gray + painter = painterResource(id = R.drawable.find_my_device), + contentDescription = "Phone icon", + tint = colorResource(id = foundation.e.elib.R.color.e_accent) ) Text( - text = stringResource(R.string.login_required), + text = stringResource(R.string.fmd_title), fontSize = 20.sp, modifier = Modifier.padding(start = Dimens.SCREEN_PADDING / 2) ) @@ -690,7 +1082,7 @@ class MainActivity : ComponentActivity() { }, text = { Text( - text = stringResource(R.string.confirm_dialog_summar), + text = stringResource(R.string.fmd_description), color = colorResource( id = foundation.e.elib.R.color.e_secondary_text_color @@ -703,75 +1095,36 @@ class MainActivity : ComponentActivity() { Text( modifier = Modifier.clickable { - showLoginDialog = false - val launchIntent: Intent? = - packageManager.getLaunchIntentForPackage( - Constants.APP_LOUNGE_PKG - ) - if (launchIntent != null) { - launchIntent.putExtra( - Constants.REQUEST_GPLAY_LOGIN, - true - ) - launchIntent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP - startActivity(launchIntent) - } + onDismissAction() + val intent = + Intent().apply { + component = + ComponentName( + Constants.APP_FIND_MY_DEVICE_PKG, + Constants.APP_FIND_MY_DEVICE_ACT + ) + } + this@MainActivity.startActivity(intent) } .padding(start = Dimens.SCREEN_PADDING / 2), color = colorResource(id = foundation.e.elib.R.color.e_accent), - text = stringResource(R.string.launch_applounge).uppercase(), + text = stringResource(R.string.fmd_button_ok).uppercase(), fontSize = 14.sp ) }, dismissButton = { Text( - modifier = - Modifier.clickable { - showLoginDialog = false - mainUI.blockAllUserApps(true) - }, + modifier = Modifier.clickable { onDismissAction() }, color = colorResource(id = foundation.e.elib.R.color.e_accent), - text = stringResource(R.string.discard).uppercase(), + text = stringResource(R.string.fmd_button_cancel).uppercase(), fontSize = 14.sp ) }, shape = RoundedCornerShape(4.dp) ) + LaunchedEffect(Unit) { setFMDStatus(KEY_FMD_STATUS_ENROLLING) } } - - ActivateAdminSummary() - - ToggleWithText( - text = stringResource(R.string.activate_parental_control), - isChecked = isAdminActive(), - fontWeight = FontWeight.Medium, - onCheckedChange = { - if (it) { - setAdmin(true) - } else { - mainScreen(page = Pages.AskPasswordForAdminRemoval) - } - }, - ) - - SelectAge( - onRadioClick = { mainScreen(page = Pages.AskPasswordForAgeReset) }, - onNextClick = {} - ) - - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text( - text = stringResource(R.string.change_my_security_code).uppercase(), - color = colorResource(foundation.e.elib.R.color.e_accent), - modifier = - Modifier.clickable( - onClick = { mainScreen(page = Pages.AskPasswordForPasswordReset) } - ), - fontWeight = FontWeight.Medium, - ) - - OnDebugBuild() - } + SideEffect { dialogDisplayed = true } } } @@ -832,11 +1185,125 @@ class MainActivity : ComponentActivity() { SelectAge( onRadioClick = {}, - onNextClick = { mainScreen(page = Pages.AuthenticationTypeSelectionView) } + onNextClick = { mainScreen(page = Pages.SelectAllowedScreenTime) } + ) + } + } + ///////////////////////////////////////////////////////////////////////////// + @Composable + fun SelectAllowedAppsPage() { + val context = LocalContext.current + fun onBackPress() { + if (SystemUtils.isSetupFinished(mActivity)) { + onStartUp() + } else { + onExitApp() + } + } + + BackHandler(onBack = { onBackPress() }) + + CustomTopAppBar( + title = stringResource(R.string.allowed_apps_caption), + onClick = { onBackPress() } + ) + + Column( + modifier = + Modifier.fillMaxSize() + .padding(start = Dimens.SCREEN_PADDING, end = Dimens.SCREEN_PADDING), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top + ) { + SelectAllowedApps( + onNextClick = { + if (DeviceAdmin().isAdminActive(context)) { + MainUI(mActivity).blockBlackListedApps() + mainScreen(page = Pages.ActivateAdmin) + } else { + mainScreen(page = Pages.AuthenticationTypeSelectionView) + } + } ) } } + private fun performSelectedAppManagement() {} + + private fun onSetAllowedApps() {} + + @Composable + fun SelectTypeAppManagementPage() { + fun onBackPress() { + selectedTypeAppManagement = null + if (SystemUtils.isSetupFinished(mActivity)) { + onStartUp() + } else { + onExitApp() + } + } + + BackHandler(onBack = { onBackPress() }) + + CustomTopAppBar( + title = stringResource(R.string.manage_app_use), + onClick = { onBackPress() } + ) + + Column( + modifier = + Modifier.fillMaxSize() + .padding(start = Dimens.SCREEN_PADDING, end = Dimens.SCREEN_PADDING), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top + ) { + SelectManageTypeApp( + onRadioClick = { PrefsUtils.setTypeAppManagement(selectedTypeAppManagement) }, + onNextClick = { mainScreen(page = Pages.SelectAllowedApps) } + ) + } + } + + private fun onSetTypeAppManagement() { + if (selectedTypeAppManagement == null) return + with(PrefsUtils.getEdit()) { + putInt(Constants.PREF_TYPE_APP_MANAGEMENT, selectedTypeAppManagement!!.ordinal) + apply() + } + } + + @Composable + fun SelectAllowedScreenTimePage() { + fun onBackPress() { + if (isAdminActive()) mainScreen(page = Pages.ActivateAdmin) + else mainScreen(page = Pages.SelectAge) + } + + BackHandler(onBack = { onBackPress() }) + + CustomTopAppBar( + title = stringResource(R.string.allowed_screen_time), + onClick = { onBackPress() } + ) + + Column( + modifier = + Modifier.fillMaxSize() + .padding(start = Dimens.SCREEN_PADDING, end = Dimens.SCREEN_PADDING), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top + ) { + SelectAllowedScreenTime({ mainScreen(page = Pages.SelectTypeAppManagement) }) + } + } + + private fun onSetAllowedScreenTime() { + with(PrefsUtils.getEdit()) { + putInt(Constants.PREF_ALLOWED_SCREEN_TIME, selectedAllowedScreenTime) + apply() + } + } + @Composable fun NotMainUser() { BackHandler(onBack = { onExitApp() }) @@ -860,22 +1327,37 @@ class MainActivity : ComponentActivity() { @Suppress("DEPRECATION") private fun setAdmin(activate: Boolean) { + val component = dA.getAdminName(mActivity) if (activate) { - SystemUtils.safeSetProp("persist.sys.mdm_active", "1") + SystemUtils.safeSetProp(Constants.PROP_MDM_SET_PACKAGE, component.flattenToString()) + SystemUtils.safeSetProp(Constants.PROP_MDM_ACTIVATE, "1") } else { - SystemUtils.safeSetProp("persist.sys.mdm_active", "0") + SystemUtils.safeSetProp(Constants.PROP_MDM_SET_PACKAGE, "") + SystemUtils.safeSetProp(Constants.PROP_MDM_ACTIVATE, "0") val mainUI = MainUI(mActivity) mainUI.blockAllUserApps(false) + mainUI.blockApps(false) mainUI.removePrivateDns() - val devicePolicyManager: DevicePolicyManager = dA.getDevicePolicyManager(mActivity) + mainUI.manageFmD(false) + mainUI.removeDateTimeRestriction() + val devicePolicyManager = dA.getDevicePolicyManager(mActivity) if (dA.isDeviceOwnerApp(mActivity)) { devicePolicyManager.clearDeviceOwnerApp(mActivity.packageName) } else if (dA.isProfileOwner(mActivity)) { - devicePolicyManager.clearProfileOwner( - ComponentName(mActivity, DeviceAdmin::class.java) - ) + devicePolicyManager.clearProfileOwner(component) } } + ParentalControlStatusList.setParentalControlStatus( + ParentalControlStatus(Date(System.currentTimeMillis()), activate) + ) + PrefsUtils.saveParentalControlStatusList( + ParentalControlStatusList.getParentalControlStatus() + ) + } + + // Allows to navigate to another screen from external component + fun navigateScreen(page: Pages) { + mainScreen(page) } private fun mainScreen(page: Pages) { @@ -884,14 +1366,16 @@ class MainActivity : ComponentActivity() { window.statusBarColor = MaterialTheme.colorScheme.background.toArgb() window.navigationBarColor = MaterialTheme.colorScheme.background.toArgb() Surface(color = MaterialTheme.colorScheme.background) { - val configuration = LocalConfiguration.current - val isLandscape = - configuration.orientation == Configuration.ORIENTATION_LANDSCAPE - Column( modifier = Modifier.fillMaxSize().let { - if (isLandscape) it.verticalScroll(rememberScrollState()) else it + if (page == Pages.SelectAllowedApps) { + // this page manage its own scrolling + it + } else { + // All other pages whatever their orientation can scroll + it.verticalScroll(rememberScrollState()) + } }, horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.Top @@ -903,7 +1387,7 @@ class MainActivity : ComponentActivity() { if (isAdminActive()) { mainScreen(page = Pages.ActivateAdmin) } else { - mainScreen(page = Pages.SelectAge) + mainScreen(page = Pages.SelectAllowedScreenTime) } }, onSelection = { mainScreen(page = Pages.SetupPinPassword) } @@ -927,8 +1411,12 @@ class MainActivity : ComponentActivity() { Pages.AskPasswordForAdminRemoval -> { AskPassword( onPasswordMatch = { - PrefsUtils.clearAll() + PrefsUtils.clearAllExcept( + Constants.PARENTAL_CONTROL_STATUS_LIST + ) selectedAge = null + selectedAllowedScreenTime = 0 + selectedTypeAppManagement = null setAdmin(false) onStartUp() }, @@ -943,6 +1431,18 @@ class MainActivity : ComponentActivity() { onBackPressed = { onStartUp() } ) } + Pages.AskPasswordForScreenTimeReset -> { + AskPassword( + onPasswordMatch = { + mainScreen(page = Pages.SelectAllowedScreenTime) + }, + onPasswordMisMatch = { + selectedAllowedScreenTime = + PrefsUtils.getAllowedScreenTime() + }, + onBackPressed = { onStartUp() } + ) + } Pages.AskPasswordForAgeReset -> { AskPassword( onPasswordMatch = { @@ -950,7 +1450,15 @@ class MainActivity : ComponentActivity() { MainUI(mActivity).blockBlackListedApps() onStartUp() }, - onPasswordMisMatch = { selectedAge = PrefsUtils.getAge() }, + onBackPressed = { onStartUp() } + ) + } + Pages.AskPasswordForAppsReset -> { + AskPassword( + onPasswordMatch = { + mainScreen(page = Pages.SelectTypeAppManagement) + }, + onPasswordMisMatch = {}, onBackPressed = { onStartUp() } ) } @@ -962,12 +1470,49 @@ class MainActivity : ComponentActivity() { onBackPressed = { onStartUp() } ) } + Pages.AskPasswordForAppInstallation -> { + AskPassword( + onPasswordMatch = { + val resultIntent = + Intent().apply { + putExtra("authentication_result", true) + } + setResult(RESULT_OK, resultIntent) + onExitApp(false) + }, + onPasswordMisMatch = { + val resultIntent = + Intent().apply { + putExtra("authentication_result", false) + } + setResult(RESULT_CANCELED, resultIntent) + onExitApp(false) + }, + onBackPressed = { + val resultIntent = + Intent().apply { + putExtra("authentication_result", false) + } + setResult(RESULT_CANCELED, resultIntent) + onExitApp(false) + } + ) + } Pages.SelectAge -> { SelectAgePage() } Pages.NotMainUser -> { NotMainUser() } + Pages.SelectAllowedApps -> { + SelectAllowedAppsPage() + } + Pages.SelectTypeAppManagement -> { + SelectTypeAppManagementPage() + } + Pages.SelectAllowedScreenTime -> { + SelectAllowedScreenTimePage() + } } } } diff --git a/app/src/main/java/foundation/e/parentalcontrol/OverlayService.kt b/app/src/main/java/foundation/e/parentalcontrol/OverlayService.kt new file mode 100644 index 0000000000000000000000000000000000000000..c52ba977140dbeee4e807a1567d2ad21fbc9e128 --- /dev/null +++ b/app/src/main/java/foundation/e/parentalcontrol/OverlayService.kt @@ -0,0 +1,72 @@ +/* + * 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 + +import android.app.Service +import android.content.Intent +import android.graphics.PixelFormat +import android.os.IBinder +import android.util.Log +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.WindowManager +import android.widget.Button + +class OverlayService : Service() { + + private lateinit var windowManager: WindowManager + private lateinit var overlayView: View + + override fun onCreate() { + super.onCreate() + Log.d("popup", "in create") + windowManager = getSystemService(WINDOW_SERVICE) as WindowManager + + val inflater = getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater + overlayView = inflater.inflate(R.layout.overlay_popup, null) + + val params = + WindowManager.LayoutParams( + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or + WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN, + PixelFormat.TRANSLUCENT + ) + + params.gravity = Gravity.CENTER + + windowManager.addView(overlayView, params) + + val closeButton = overlayView.findViewById