├── app ├── .gitignore ├── src │ └── main │ │ ├── res │ │ ├── font │ │ │ ├── lora_bold.ttf │ │ │ ├── bitter_bold.ttf │ │ │ ├── bitter_italic.ttf │ │ │ ├── caudex_bold.ttf │ │ │ ├── caudex_italic.ttf │ │ │ ├── lora_italic.ttf │ │ │ ├── lora_regular.ttf │ │ │ ├── opensans_bold.ttf │ │ │ ├── poppins_bold.ttf │ │ │ ├── roboto_bold.ttf │ │ │ ├── roboto_italic.ttf │ │ │ ├── bitter_regular.ttf │ │ │ ├── caudex_regular.ttf │ │ │ ├── lora_bolditalic.ttf │ │ │ ├── opensans_italic.ttf │ │ │ ├── poppins_italic.ttf │ │ │ ├── poppins_regular.ttf │ │ │ ├── roboto_regular.ttf │ │ │ ├── bitter_bolditalic.ttf │ │ │ ├── caudex_bolditalic.ttf │ │ │ ├── opensans_regular.ttf │ │ │ ├── poppins_bolditalic.ttf │ │ │ ├── roboto_bolditalic.ttf │ │ │ └── opensans_bolditalic.ttf │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ ├── values │ │ │ ├── ic_launcher_background.xml │ │ │ ├── themes.xml │ │ │ └── splash.xml │ │ ├── xml │ │ │ ├── backup_rules.xml │ │ │ └── data_extraction_rules.xml │ │ ├── values-v30 │ │ │ └── themes.xml │ │ ├── mipmap-anydpi │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ ├── values-v33 │ │ │ └── splash.xml │ │ └── drawable │ │ │ ├── ic_close.xml │ │ │ ├── ic_launch.xml │ │ │ ├── ic_keepon_monochrome.xml │ │ │ ├── ic_keepon.xml │ │ │ └── ic_launcher_foreground.xml │ │ ├── ic_launcher-playstore.png │ │ ├── java │ │ └── fr │ │ │ └── twentynine │ │ │ └── keepon │ │ │ ├── data │ │ │ ├── mapper │ │ │ │ ├── Mapper.kt │ │ │ │ ├── ScreenTimeoutUIToScreenTimeoutMapper.kt │ │ │ │ └── ScreenTimeoutToScreenTimeoutUIMapper.kt │ │ │ ├── enums │ │ │ │ ├── TimeoutIconSize.kt │ │ │ │ ├── DataStoreSourceType.kt │ │ │ │ ├── CreditInfoType.kt │ │ │ │ ├── SpecialScreenTimeoutType.kt │ │ │ │ └── ItemPosition.kt │ │ │ ├── model │ │ │ │ ├── QSTimeoutData.kt │ │ │ │ ├── AppInfo.kt │ │ │ │ ├── DismissedTips.kt │ │ │ │ ├── NeededPermission.kt │ │ │ │ ├── TimeoutIconData.kt │ │ │ │ ├── TipsConstraintState.kt │ │ │ │ ├── ScreenTimeoutUI.kt │ │ │ │ ├── TaskerUIEvent.kt │ │ │ │ ├── TimeoutIconStyle.kt │ │ │ │ ├── TaskerEditUIState.kt │ │ │ │ ├── MainViewUIState.kt │ │ │ │ ├── MainUIEvent.kt │ │ │ │ ├── OldTimeoutIconStyle.kt │ │ │ │ └── ScreenTimeout.kt │ │ │ ├── repo │ │ │ │ ├── TipsInfoRepository.kt │ │ │ │ ├── AppInfoRepository.kt │ │ │ │ ├── IconFontFamilyRepository.kt │ │ │ │ ├── CreditInfoRepository.kt │ │ │ │ └── ScreenTimeoutRepository.kt │ │ │ └── local │ │ │ │ ├── PreferenceDataStoreConstants.kt │ │ │ │ ├── CreditInfo.kt │ │ │ │ ├── IconFontFamily.kt │ │ │ │ └── TipsInfo.kt │ │ │ ├── util │ │ │ ├── extensions │ │ │ │ ├── StringToUuid.kt │ │ │ │ ├── DpToPxConverter.kt │ │ │ │ ├── MutableListRemoveUntil.kt │ │ │ │ └── BroadcastReceiverGoAsync.kt │ │ │ ├── coil │ │ │ │ ├── TimeoutIconDataKeyer.kt │ │ │ │ ├── MemoryCacheManager.kt │ │ │ │ └── TimeoutIconDataFetcher.kt │ │ │ ├── RequiredPermissionsManager.kt │ │ │ ├── DynamicShortcutManager.kt │ │ │ ├── LockableJob.kt │ │ │ ├── StringResourceProvider.kt │ │ │ ├── QSTileUpdater.kt │ │ │ ├── BundleScrubber.kt │ │ │ ├── SystemScreenTimeoutController.kt │ │ │ ├── AddTileServiceManager.kt │ │ │ ├── DevicePolicyManagerHelper.kt │ │ │ ├── AppVersionManager.kt │ │ │ ├── BatteryOptimizationManager.kt │ │ │ ├── AppRateHelper.kt │ │ │ ├── DesiredScreenTimeoutController.kt │ │ │ ├── SystemSettingPermissionManager.kt │ │ │ ├── PostNotificationPermissionManager.kt │ │ │ └── DataMigrationHelper.kt │ │ │ ├── ui │ │ │ ├── navigation │ │ │ │ ├── NavigationDestinationWithBadge.kt │ │ │ │ ├── NavigationActions.kt │ │ │ │ └── NavigationDestination.kt │ │ │ ├── util │ │ │ │ ├── WindowStateUtils.kt │ │ │ │ ├── PulsatingIcon.kt │ │ │ │ ├── CardUtils.kt │ │ │ │ ├── GlowingText.kt │ │ │ │ └── NestedScrollConnectionExtensions.kt │ │ │ ├── view │ │ │ │ ├── ItemCardView.kt │ │ │ │ ├── SwipeableScreenTimeoutUICardView.kt │ │ │ │ ├── DismissActionRowView.kt │ │ │ │ ├── ErrorView.kt │ │ │ │ ├── ScreenTimeoutSetDefaultDismissActionRowView.kt │ │ │ │ └── CardHeaderView.kt │ │ │ └── theme │ │ │ │ ├── icons │ │ │ │ ├── HomeFilled.kt │ │ │ │ ├── HomeOutlined.kt │ │ │ │ └── StyleFilled.kt │ │ │ │ ├── Type.kt │ │ │ │ ├── Color.kt │ │ │ │ └── Theme.kt │ │ │ ├── di │ │ │ ├── AppRateHelperModule.kt │ │ │ ├── QSTileUpdaterModule.kt │ │ │ ├── MemoryCacheManagerModule.kt │ │ │ ├── AddTileServiceManagerModule.kt │ │ │ ├── StringResourceProviderModule.kt │ │ │ ├── DevicePolicyManagerHelperModule.kt │ │ │ ├── PreferenceDataStoreHelperModule.kt │ │ │ ├── SystemScreenTimeoutControllerModule.kt │ │ │ ├── ScreenOffReceiverServiceManagerModule.kt │ │ │ ├── BatteryOptimizationManagerModule.kt │ │ │ ├── SystemSettingPermissionManagerModule.kt │ │ │ ├── PostNotificationPermissionManagerModule.kt │ │ │ ├── AppVersionManagerModule.kt │ │ │ └── UserPreferencesRepositoryModule.kt │ │ │ ├── tasker │ │ │ ├── PluginBundleManager.kt │ │ │ └── FireReceiver.kt │ │ │ ├── receiver │ │ │ ├── ScreenOffReceiver.kt │ │ │ └── RebootAppReceiver.kt │ │ │ ├── worker │ │ │ ├── MonitorSystemScreenTimeoutWorkScheduler.kt │ │ │ ├── SetNewScreenTimeoutWorkScheduler.kt │ │ │ ├── SetNewScreenTimeoutWork.kt │ │ │ └── MonitorSystemScreenTimeoutWork.kt │ │ │ ├── KeepOnApplication.kt │ │ │ └── services │ │ │ └── ScreenOffReceiverServiceManager.kt │ │ └── AndroidManifest.xml ├── proguard-rules.pro └── build.gradle.kts ├── fastlane └── metadata │ └── android │ ├── en-US │ ├── changelogs │ │ ├── 21.txt │ │ ├── 20.txt │ │ ├── 24.txt │ │ ├── 23.txt │ │ ├── 25.txt │ │ ├── 26.txt │ │ └── 22.txt │ ├── title.txt │ ├── short_description.txt │ ├── images │ │ ├── icon.png │ │ ├── featureGraphic.png │ │ └── phoneScreenshots │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ ├── 3.png │ │ │ └── 4.png │ └── full_description.txt │ └── fr-FR │ ├── changelogs │ ├── 21.txt │ ├── 20.txt │ ├── 24.txt │ ├── 23.txt │ ├── 25.txt │ ├── 26.txt │ └── 22.txt │ ├── title.txt │ ├── short_description.txt │ └── full_description.txt ├── .github ├── banner.jpg ├── screenshot1.jpg ├── screenshot2.jpg ├── screenshot3.jpg ├── screenshot4.jpg └── GetItOnGooglePlay_Badge.png ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── .gitignore ├── settings.gradle.kts ├── gradle.properties ├── README.md └── gradlew.bat /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/21.txt: -------------------------------------------------------------------------------- 1 | - Updated Gradle -------------------------------------------------------------------------------- /fastlane/metadata/android/fr-FR/changelogs/21.txt: -------------------------------------------------------------------------------- 1 | - Mise à jour de Gradle -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/title.txt: -------------------------------------------------------------------------------- 1 | KeepOn - Keep your screen on smartly! -------------------------------------------------------------------------------- /fastlane/metadata/android/fr-FR/title.txt: -------------------------------------------------------------------------------- 1 | KeepOn - Garde ton écran allumé intelligemment! -------------------------------------------------------------------------------- /.github/banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/.github/banner.jpg -------------------------------------------------------------------------------- /.github/screenshot1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/.github/screenshot1.jpg -------------------------------------------------------------------------------- /.github/screenshot2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/.github/screenshot2.jpg -------------------------------------------------------------------------------- /.github/screenshot3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/.github/screenshot3.jpg -------------------------------------------------------------------------------- /.github/screenshot4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/.github/screenshot4.jpg -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | Keep your screen on smartly and easily with Quick Settings. -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.github/GetItOnGooglePlay_Badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/.github/GetItOnGooglePlay_Badge.png -------------------------------------------------------------------------------- /app/src/main/res/font/lora_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/app/src/main/res/font/lora_bold.ttf -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/20.txt: -------------------------------------------------------------------------------- 1 | - Application completely rewritten for better support and improved performance -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/res/font/bitter_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/app/src/main/res/font/bitter_bold.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/bitter_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/app/src/main/res/font/bitter_italic.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/caudex_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/app/src/main/res/font/caudex_bold.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/caudex_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/app/src/main/res/font/caudex_italic.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/lora_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/app/src/main/res/font/lora_italic.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/lora_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/app/src/main/res/font/lora_regular.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/opensans_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/app/src/main/res/font/opensans_bold.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/poppins_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/app/src/main/res/font/poppins_bold.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/roboto_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/app/src/main/res/font/roboto_bold.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/roboto_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/app/src/main/res/font/roboto_italic.ttf -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/24.txt: -------------------------------------------------------------------------------- 1 | - Bug fixes 2 | - Enhanced user interface 3 | - Improved performance and stability -------------------------------------------------------------------------------- /fastlane/metadata/android/fr-FR/short_description.txt: -------------------------------------------------------------------------------- 1 | Garde ton écran allumé intelligemment et facilement grâce aux réglages rapide. -------------------------------------------------------------------------------- /app/src/main/res/font/bitter_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/app/src/main/res/font/bitter_regular.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/caudex_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/app/src/main/res/font/caudex_regular.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/lora_bolditalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/app/src/main/res/font/lora_bolditalic.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/opensans_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/app/src/main/res/font/opensans_italic.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/poppins_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/app/src/main/res/font/poppins_italic.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/poppins_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/app/src/main/res/font/poppins_regular.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/roboto_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/app/src/main/res/font/roboto_regular.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/bitter_bolditalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/app/src/main/res/font/bitter_bolditalic.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/caudex_bolditalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/app/src/main/res/font/caudex_bolditalic.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/opensans_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/app/src/main/res/font/opensans_regular.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/poppins_bolditalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/app/src/main/res/font/poppins_bolditalic.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/roboto_bolditalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/app/src/main/res/font/roboto_bolditalic.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/opensans_bolditalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/app/src/main/res/font/opensans_bolditalic.ttf -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/fastlane/metadata/android/en-US/images/icon.png -------------------------------------------------------------------------------- /fastlane/metadata/android/fr-FR/changelogs/20.txt: -------------------------------------------------------------------------------- 1 | - Application entièrement réécrite pour une meilleure prise en charge et des performances améliorées -------------------------------------------------------------------------------- /fastlane/metadata/android/fr-FR/changelogs/24.txt: -------------------------------------------------------------------------------- 1 | - Corrections de bugs 2 | - Interface utilisateur améliorée 3 | - Performances et stabilité améliorées -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/23.txt: -------------------------------------------------------------------------------- 1 | - Bug fixes 2 | - Enhanced user interface 3 | - Improved performance and stability 4 | - Dependency updates -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/25.txt: -------------------------------------------------------------------------------- 1 | - Bug fixes 2 | - Enhanced user interface 3 | - Improved performance and stability 4 | - Dependency updates -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/26.txt: -------------------------------------------------------------------------------- 1 | - Bug fixes 2 | - Enhanced user interface 3 | - Improved performance and stability 4 | - Dependency updates -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/featureGraphic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/fastlane/metadata/android/en-US/images/featureGraphic.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/22.txt: -------------------------------------------------------------------------------- 1 | - Behavior change to set the default screen timeout value by swiping 2 | - Bug fixes and optimizations 3 | - Dependency updates -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twentynine78/KeepOn/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #83D0F8 4 | -------------------------------------------------------------------------------- /fastlane/metadata/android/fr-FR/changelogs/23.txt: -------------------------------------------------------------------------------- 1 | - Corrections de bugs 2 | - Interface utilisateur améliorée 3 | - Performances et stabilité améliorées 4 | - Mises à jour des dépendances -------------------------------------------------------------------------------- /fastlane/metadata/android/fr-FR/changelogs/25.txt: -------------------------------------------------------------------------------- 1 | - Corrections de bugs 2 | - Interface utilisateur améliorée 3 | - Performances et stabilité améliorées 4 | - Mises à jour des dépendances -------------------------------------------------------------------------------- /fastlane/metadata/android/fr-FR/changelogs/26.txt: -------------------------------------------------------------------------------- 1 | - Corrections de bugs 2 | - Interface utilisateur améliorée 3 | - Performances et stabilité améliorées 4 | - Mises à jour des dépendances -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/data/mapper/Mapper.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.data.mapper 2 | 3 | fun interface Mapper { 4 | fun map(from: From): To 5 | } 6 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/data/enums/TimeoutIconSize.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.data.enums 2 | 3 | enum class TimeoutIconSize(val size: Int) { 4 | LARGE(40), 5 | MEDIUM(24) 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/data/enums/DataStoreSourceType.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.data.enums 2 | 3 | enum class DataStoreSourceType { 4 | DATA_SOURCE_BACKED_UP, 5 | DATA_SOURCE 6 | } 7 | -------------------------------------------------------------------------------- /fastlane/metadata/android/fr-FR/changelogs/22.txt: -------------------------------------------------------------------------------- 1 | - Modification du comportement pour définir la valeur par défaut du délai d'expiration de l'écran en balayant l'écran 2 | - Corrections de bogues et optimisations 3 | - Mises à jour des dépendances -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/util/extensions/StringToUuid.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.util.extensions 2 | 3 | import java.util.UUID 4 | 5 | fun String.uuid(): UUID { 6 | return UUID.nameUUIDFromBytes(this.toByteArray()) 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/data/model/QSTimeoutData.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.data.model 2 | 3 | import androidx.compose.runtime.Stable 4 | 5 | @Stable 6 | data class QSTimeoutData( 7 | val keepOnState: Boolean, 8 | val iconData: TimeoutIconData 9 | ) 10 | -------------------------------------------------------------------------------- /app/src/main/res/values-v30/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Feb 22 19:11:03 CET 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-all.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/data/model/AppInfo.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.data.model 2 | 3 | import androidx.compose.runtime.Stable 4 | 5 | @Stable 6 | data class AppInfo( 7 | val version: String, 8 | val author: String, 9 | val sourceCodeUrl: String, 10 | ) 11 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/data/model/DismissedTips.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.data.model 2 | 3 | import androidx.compose.runtime.Stable 4 | import kotlinx.serialization.Serializable 5 | 6 | @Stable 7 | @Serializable 8 | data class DismissedTips( 9 | val id: Int 10 | ) 11 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/data/enums/CreditInfoType.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.data.enums 2 | 3 | import fr.twentynine.keepon.R 4 | 5 | enum class CreditInfoType(val typeNameId: Int) { 6 | Library(R.string.credit_info_type_library), 7 | Font(R.string.credit_info_type_font), 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | .idea 4 | .kotlin 5 | /local.properties 6 | /.idea/caches 7 | /.idea/libraries 8 | /.idea/modules.xml 9 | /.idea/workspace.xml 10 | /.idea/navEditor.xml 11 | /.idea/assetWizardSettings.xml 12 | .DS_Store 13 | /build 14 | /captures 15 | .externalNativeBuild 16 | .cxx 17 | local.properties 18 | */debug 19 | */release 20 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/data/model/NeededPermission.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.data.model 2 | 3 | import androidx.compose.runtime.Immutable 4 | 5 | @Immutable 6 | data class NeededPermission( 7 | val title: String, 8 | val description: String, 9 | val requestNeeded: Boolean, 10 | val requestAction: () -> Unit, 11 | ) 12 | -------------------------------------------------------------------------------- /app/src/main/res/values/splash.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/data/repo/TipsInfoRepository.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.data.repo 2 | 3 | import fr.twentynine.keepon.data.local.TipsInfo 4 | 5 | object TipsInfoRepository { 6 | 7 | val tipsInfoList = listOf( 8 | TipsInfo.PostNotification, 9 | TipsInfo.AddQSTile, 10 | TipsInfo.RateApp, 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/data/model/TimeoutIconData.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.data.model 2 | 3 | import androidx.compose.runtime.Stable 4 | import fr.twentynine.keepon.data.enums.TimeoutIconSize 5 | 6 | @Stable 7 | data class TimeoutIconData( 8 | val iconTimeout: ScreenTimeout, 9 | val iconSize: TimeoutIconSize, 10 | val iconStyle: TimeoutIconStyle 11 | ) 12 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/util/extensions/DpToPxConverter.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.util.extensions 2 | 3 | import android.content.res.Resources 4 | 5 | // Extension function to convert dp to px 6 | val Int.px: Int 7 | get() = (this * Resources.getSystem().displayMetrics.density).toInt() 8 | val Float.px: Float 9 | get() = (this * Resources.getSystem().displayMetrics.density) 10 | -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/util/coil/TimeoutIconDataKeyer.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.util.coil 2 | 3 | import coil3.key.Keyer 4 | import coil3.request.Options 5 | import fr.twentynine.keepon.data.model.TimeoutIconData 6 | 7 | class TimeoutIconDataKeyer : Keyer { 8 | override fun key(data: TimeoutIconData, options: Options): String { 9 | return data.toString() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/data/enums/SpecialScreenTimeoutType.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.data.enums 2 | 3 | private const val DEFAULT_SCREEN_TIMEOUT_VALUE = -42 4 | private const val PREVIOUS_SCREEN_TIMEOUT_VALUE = -43 5 | 6 | enum class SpecialScreenTimeoutType(val value: Int) { 7 | DEFAULT_SCREEN_TIMEOUT_TYPE(DEFAULT_SCREEN_TIMEOUT_VALUE), 8 | PREVIOUS_SCREEN_TIMEOUT_TYPE(PREVIOUS_SCREEN_TIMEOUT_VALUE) 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/res/values-v33/splash.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | gradlePluginPortal() 6 | } 7 | } 8 | @Suppress("UnstableApiUsage") 9 | dependencyResolutionManagement { 10 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 11 | repositories { 12 | google() 13 | mavenCentral() 14 | } 15 | } 16 | 17 | rootProject.name = "KeepOn" 18 | include(":app") 19 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/data/mapper/ScreenTimeoutUIToScreenTimeoutMapper.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.data.mapper 2 | 3 | import fr.twentynine.keepon.data.model.ScreenTimeout 4 | import fr.twentynine.keepon.data.model.ScreenTimeoutUI 5 | 6 | object ScreenTimeoutUIToScreenTimeoutMapper : Mapper { 7 | override fun map(from: ScreenTimeoutUI): ScreenTimeout { 8 | return ScreenTimeout(from.value) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/util/extensions/MutableListRemoveUntil.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.util.extensions 2 | 3 | fun MutableList.removeUntil(target: T) { 4 | synchronized(this) { 5 | val lastIndex = this.lastIndexOf(target) 6 | 7 | if (lastIndex != -1) { 8 | // Remove elements from the beginning up to and including the target. 9 | this.subList(0, lastIndex + 1).clear() 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/util/RequiredPermissionsManager.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.util 2 | 3 | import android.content.Context 4 | 5 | class RequiredPermissionsManager { 6 | companion object { 7 | fun isPermissionsGranted(context: Context): Boolean { 8 | return BatteryOptimizationManager.isBatteryNotOptimized(context) && 9 | SystemSettingPermissionManager.canWriteSystemSettings(context) 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/data/model/TipsConstraintState.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.data.model 2 | 3 | import androidx.compose.runtime.Immutable 4 | 5 | @Immutable 6 | data class TipsConstraintState( 7 | val canPostNotification: Boolean = false, 8 | val servicesNotificationChannelIsDisabled: Boolean = true, 9 | val batteryIsNotOptimized: Boolean = false, 10 | val tileServiceIsAdded: Boolean = false, 11 | val showRateApp: Boolean = false, 12 | ) 13 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/data/model/ScreenTimeoutUI.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.data.model 2 | 3 | import android.os.Parcelable 4 | import androidx.compose.runtime.Stable 5 | import kotlinx.parcelize.Parcelize 6 | 7 | @Stable 8 | @Parcelize 9 | data class ScreenTimeoutUI( 10 | val value: Int, 11 | val displayName: String, 12 | val isSelected: Boolean, 13 | val isDefault: Boolean, 14 | val isCurrent: Boolean, 15 | val isLocked: Boolean 16 | ) : Parcelable 17 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/ui/navigation/NavigationDestinationWithBadge.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.ui.navigation 2 | 3 | import androidx.compose.runtime.Stable 4 | 5 | data class NavigationDestinationWithBadge( 6 | val destination: NavigationDestination, 7 | val badgeAmount: Int? = null 8 | ) 9 | 10 | @Stable 11 | fun NavigationDestination.withBadge(badgeAmount: Int?): NavigationDestinationWithBadge { 12 | return NavigationDestinationWithBadge(this, badgeAmount) 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/data/model/TaskerUIEvent.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.data.model 2 | 3 | sealed interface TaskerUIEvent { 4 | data object RequestWriteSystemSettingPermission : TaskerUIEvent 5 | data object RequestDisableBatteryOptimization : TaskerUIEvent 6 | data object RequestPostNotification : TaskerUIEvent 7 | data object UpdateIsFirstLaunch : TaskerUIEvent 8 | data object CheckNeededPermissions : TaskerUIEvent 9 | data class SetSelectedScreenTimeout(val screenTimeoutUI: ScreenTimeoutUI) : TaskerUIEvent 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/util/coil/MemoryCacheManager.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.util.coil 2 | 3 | import android.content.Context 4 | import coil3.imageLoader 5 | import dagger.hilt.android.qualifiers.ApplicationContext 6 | import javax.inject.Inject 7 | 8 | interface MemoryCacheManager { 9 | fun clear() 10 | } 11 | 12 | class MemoryCacheManagerImpl @Inject constructor(@param:ApplicationContext private val context: Context) : MemoryCacheManager { 13 | override fun clear() { 14 | context.imageLoader.memoryCache?.clear() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/ui/util/WindowStateUtils.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.ui.util 2 | 3 | /** 4 | * Different type of navigation supported by app depending on device size and state. 5 | */ 6 | enum class KeepOnNavigationType { 7 | BOTTOM_NAVIGATION, NAVIGATION_RAIL 8 | } 9 | 10 | /** 11 | * Different position of navigation content inside Navigation Rail, Navigation Drawer depending on device size and state. 12 | */ 13 | enum class KeepOnNavigationContentPosition { 14 | TOP, CENTER 15 | } 16 | 17 | const val MAX_SCREEN_CONTENT_WIDTH_IN_DP = 650 18 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/data/repo/AppInfoRepository.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.data.repo 2 | 3 | import android.content.Context 4 | import fr.twentynine.keepon.data.model.AppInfo 5 | 6 | class AppInfoRepository { 7 | 8 | fun getKeepOnAppInfo(context: Context): AppInfo { 9 | val appVersion = context.packageManager.getPackageInfo(context.packageName, 0).versionName ?: "0.0.0" 10 | val appAuthor = "TwentyNine78" 11 | val codeSourceUrl = "https://github.com/twentynine78/KeepOn" 12 | 13 | return AppInfo(appVersion, appAuthor, codeSourceUrl) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/util/DynamicShortcutManager.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.util 2 | 3 | import android.content.Context 4 | 5 | object DynamicShortcutManager { 6 | fun removeAllDynamicShortcut(context: Context) { 7 | val shortcutManager = context.getSystemService(Context.SHORTCUT_SERVICE) as android.content.pm.ShortcutManager 8 | val dynamicShortcutsId = shortcutManager.dynamicShortcuts.map { shortcut -> shortcut.id } 9 | 10 | if (dynamicShortcutsId.isNotEmpty()) { 11 | shortcutManager.removeDynamicShortcuts(dynamicShortcutsId) 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_close.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/data/repo/IconFontFamilyRepository.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.data.repo 2 | 3 | import fr.twentynine.keepon.data.local.IconFontFamily 4 | import kotlinx.collections.immutable.toImmutableMap 5 | 6 | object IconFontFamilyRepository { 7 | val iconFontFamilies = listOf( 8 | IconFontFamily.Roboto, 9 | IconFontFamily.Bitter, 10 | IconFontFamily.OpenSans, 11 | IconFontFamily.Caudex, 12 | IconFontFamily.Poppins, 13 | IconFontFamily.Lora 14 | ) 15 | .associateBy { it.name } 16 | .withDefault { IconFontFamily.OpenSans } 17 | .toImmutableMap() 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/data/repo/CreditInfoRepository.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.data.repo 2 | 3 | import fr.twentynine.keepon.data.local.CreditInfo 4 | import kotlinx.collections.immutable.toImmutableMap 5 | 6 | object CreditInfoRepository { 7 | val creditInfoMap = listOf( 8 | CreditInfo.Coil, 9 | CreditInfo.Roboto, 10 | CreditInfo.Bitter, 11 | CreditInfo.OpenSans, 12 | CreditInfo.Caudex, 13 | CreditInfo.Poppins, 14 | CreditInfo.Lora 15 | ) 16 | .groupBy( 17 | keySelector = { it.type }, 18 | valueTransform = { it }, 19 | ) 20 | .toImmutableMap() 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/data/enums/ItemPosition.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.data.enums 2 | 3 | enum class ItemPosition { 4 | FIRST, 5 | MIDDLE, 6 | LAST, 7 | FIRST_AND_LAST; 8 | 9 | companion object { 10 | fun getItemPosition(itemIndex: Int, listSize: Int): ItemPosition { 11 | return when { 12 | listSize == 1 -> FIRST_AND_LAST 13 | itemIndex == 0 -> FIRST 14 | itemIndex == listSize - 1 -> LAST 15 | itemIndex in 1 until listSize - 1 -> MIDDLE 16 | else -> throw IllegalArgumentException("Invalid item index: $itemIndex") 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/di/AppRateHelperModule.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.di 2 | 3 | import android.content.Context 4 | import dagger.Module 5 | import dagger.Provides 6 | import dagger.hilt.InstallIn 7 | import dagger.hilt.android.qualifiers.ApplicationContext 8 | import dagger.hilt.components.SingletonComponent 9 | import fr.twentynine.keepon.util.AppRateHelper 10 | import fr.twentynine.keepon.util.AppRateHelperImpl 11 | 12 | @Module 13 | @InstallIn(SingletonComponent::class) 14 | object AppRateHelperModule { 15 | 16 | @Provides 17 | fun provideAppRateHelper(@ApplicationContext context: Context): AppRateHelper { 18 | return AppRateHelperImpl(context) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/di/QSTileUpdaterModule.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.di 2 | 3 | import android.content.Context 4 | import dagger.Module 5 | import dagger.Provides 6 | import dagger.hilt.InstallIn 7 | import dagger.hilt.android.qualifiers.ApplicationContext 8 | import dagger.hilt.components.SingletonComponent 9 | import fr.twentynine.keepon.util.QSTileUpdater 10 | import fr.twentynine.keepon.util.QSTileUpdaterImpl 11 | 12 | @Module 13 | @InstallIn(SingletonComponent::class) 14 | object QSTileUpdaterModule { 15 | 16 | @Provides 17 | fun provideQSTileUpdater(@ApplicationContext context: Context): QSTileUpdater { 18 | return QSTileUpdaterImpl(context) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launch.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/data/model/TimeoutIconStyle.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.data.model 2 | 3 | import androidx.compose.runtime.Stable 4 | import fr.twentynine.keepon.data.local.IconFontFamily 5 | import kotlinx.serialization.Serializable 6 | 7 | @Stable 8 | @Serializable 9 | data class TimeoutIconStyle( 10 | val iconStyleFontSize: Int = 0, 11 | val iconStyleFontHorizontalSpacing: Int = 0, 12 | val iconStyleFontVerticalSpacing: Int = 0, 13 | val iconFontFamilyName: String = IconFontFamily.Roboto.name, 14 | val iconStyleFontBold: Boolean = false, 15 | val iconStyleFontItalic: Boolean = false, 16 | val iconStyleFontUnderline: Boolean = false, 17 | val iconStyleTextOutlined: Boolean = false 18 | ) 19 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/tasker/PluginBundleManager.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.tasker 2 | 3 | import android.os.Bundle 4 | 5 | class PluginBundleManager private constructor() { 6 | companion object { 7 | internal const val BUNDLE_EXTRA_TIMEOUT_VALUE = "fr.twentynine.keepon.tasker.TIMEOUT_VALUE" 8 | 9 | fun isBundleValid(bundle: Bundle?): Boolean { 10 | if (null == bundle) { 11 | return false 12 | } 13 | // Make sure the expected extras exist 14 | return bundle.containsKey(BUNDLE_EXTRA_TIMEOUT_VALUE) 15 | } 16 | } 17 | 18 | init { 19 | throw UnsupportedOperationException("This class is non-instantiable") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/di/MemoryCacheManagerModule.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.di 2 | 3 | import android.content.Context 4 | import dagger.Module 5 | import dagger.Provides 6 | import dagger.hilt.InstallIn 7 | import dagger.hilt.android.qualifiers.ApplicationContext 8 | import dagger.hilt.components.SingletonComponent 9 | import fr.twentynine.keepon.util.coil.MemoryCacheManager 10 | import fr.twentynine.keepon.util.coil.MemoryCacheManagerImpl 11 | 12 | @Module 13 | @InstallIn(SingletonComponent::class) 14 | object MemoryCacheManagerModule { 15 | 16 | @Provides 17 | fun provideMemoryCacheManager(@ApplicationContext context: Context): MemoryCacheManager { 18 | return MemoryCacheManagerImpl(context) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/di/AddTileServiceManagerModule.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.di 2 | 3 | import android.content.Context 4 | import dagger.Module 5 | import dagger.Provides 6 | import dagger.hilt.InstallIn 7 | import dagger.hilt.android.qualifiers.ApplicationContext 8 | import dagger.hilt.components.SingletonComponent 9 | import fr.twentynine.keepon.util.AddTileServiceManager 10 | import fr.twentynine.keepon.util.AddTileServiceManagerImpl 11 | 12 | @Module 13 | @InstallIn(SingletonComponent::class) 14 | object AddTileServiceManagerModule { 15 | 16 | @Provides 17 | fun provideAddTileServiceManager(@ApplicationContext context: Context): AddTileServiceManager { 18 | return AddTileServiceManagerImpl(context) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/di/StringResourceProviderModule.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.di 2 | 3 | import android.content.Context 4 | import dagger.Module 5 | import dagger.Provides 6 | import dagger.hilt.InstallIn 7 | import dagger.hilt.android.qualifiers.ApplicationContext 8 | import dagger.hilt.components.SingletonComponent 9 | import fr.twentynine.keepon.util.StringResourceProvider 10 | import fr.twentynine.keepon.util.StringResourceProviderImpl 11 | 12 | @Module 13 | @InstallIn(SingletonComponent::class) 14 | object StringResourceProviderModule { 15 | 16 | @Provides 17 | fun provideStringResourceProvider(@ApplicationContext context: Context): StringResourceProvider { 18 | return StringResourceProviderImpl(context) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/di/DevicePolicyManagerHelperModule.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.di 2 | 3 | import android.content.Context 4 | import dagger.Module 5 | import dagger.Provides 6 | import dagger.hilt.InstallIn 7 | import dagger.hilt.android.qualifiers.ApplicationContext 8 | import dagger.hilt.components.SingletonComponent 9 | import fr.twentynine.keepon.util.DevicePolicyManagerHelper 10 | import fr.twentynine.keepon.util.DevicePolicyManagerHelperImpl 11 | 12 | @Module 13 | @InstallIn(SingletonComponent::class) 14 | object DevicePolicyManagerHelperModule { 15 | 16 | @Provides 17 | fun provideDevicePolicyManagerHelper(@ApplicationContext context: Context): DevicePolicyManagerHelper { 18 | return DevicePolicyManagerHelperImpl(context) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/di/PreferenceDataStoreHelperModule.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.di 2 | 3 | import android.content.Context 4 | import dagger.Module 5 | import dagger.Provides 6 | import dagger.hilt.InstallIn 7 | import dagger.hilt.android.qualifiers.ApplicationContext 8 | import dagger.hilt.components.SingletonComponent 9 | import fr.twentynine.keepon.data.local.PreferenceDataStoreHelper 10 | import fr.twentynine.keepon.data.local.PreferenceDataStoreHelperImpl 11 | 12 | @Module 13 | @InstallIn(SingletonComponent::class) 14 | object PreferenceDataStoreHelperModule { 15 | 16 | @Provides 17 | fun providePreferenceDataStoreHelper(@ApplicationContext context: Context): PreferenceDataStoreHelper { 18 | return PreferenceDataStoreHelperImpl(context) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/ui/view/ItemCardView.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.ui.view 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | import fr.twentynine.keepon.data.enums.ItemPosition 6 | 7 | @Composable 8 | fun ItemCardView( 9 | itemPosition: ItemPosition, 10 | modifier: Modifier = Modifier, 11 | onClick: (() -> Unit)? = null, 12 | content: @Composable () -> Unit 13 | ) { 14 | SwipeableItemCardView( 15 | item = null, 16 | itemPosition = itemPosition, 17 | modifier = modifier, 18 | swipeEnabled = false, 19 | onClickAction = { if (onClick != null) onClick() }, 20 | onSwipeAction = null, 21 | backgroundContent = null, 22 | content = { content() } 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/util/LockableJob.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.util 2 | 3 | import kotlinx.coroutines.Job 4 | import kotlinx.coroutines.cancelAndJoin 5 | 6 | class LockableJob { 7 | var job: Job? = null 8 | 9 | enum class LockableJobState { 10 | LOCKED, UNLOCKED 11 | } 12 | 13 | private var state: LockableJobState = LockableJobState.UNLOCKED 14 | 15 | fun lock() { 16 | state = LockableJobState.LOCKED 17 | } 18 | 19 | fun unlock() { 20 | state = LockableJobState.UNLOCKED 21 | } 22 | 23 | suspend fun cancelOrJoin() { 24 | when (state) { 25 | LockableJobState.LOCKED -> job?.join() 26 | LockableJobState.UNLOCKED -> job?.cancelAndJoin() 27 | } 28 | unlock() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/di/SystemScreenTimeoutControllerModule.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.di 2 | 3 | import android.content.Context 4 | import dagger.Module 5 | import dagger.Provides 6 | import dagger.hilt.InstallIn 7 | import dagger.hilt.android.qualifiers.ApplicationContext 8 | import dagger.hilt.components.SingletonComponent 9 | import fr.twentynine.keepon.util.SystemScreenTimeoutController 10 | import fr.twentynine.keepon.util.SystemScreenTimeoutControllerImpl 11 | 12 | @Module 13 | @InstallIn(SingletonComponent::class) 14 | object SystemScreenTimeoutControllerModule { 15 | 16 | @Provides 17 | fun provideSystemScreenTimeoutController(@ApplicationContext context: Context): SystemScreenTimeoutController { 18 | return SystemScreenTimeoutControllerImpl(context) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/util/StringResourceProvider.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.util 2 | 3 | import android.content.Context 4 | import dagger.hilt.android.qualifiers.ApplicationContext 5 | import javax.inject.Inject 6 | 7 | interface StringResourceProvider { 8 | fun getString(resourceId: Int): String 9 | fun getPlural(resourceId: Int, count: Int): String 10 | } 11 | 12 | class StringResourceProviderImpl @Inject constructor(@param:ApplicationContext private val context: Context) : StringResourceProvider { 13 | private val resources by lazy { context.resources } 14 | 15 | override fun getString(resourceId: Int): String = resources.getString(resourceId) 16 | 17 | override fun getPlural(resourceId: Int, count: Int): String = resources.getQuantityString(resourceId, count, count) 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/util/QSTileUpdater.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.util 2 | 3 | import android.content.ComponentName 4 | import android.content.Context 5 | import android.service.quicksettings.TileService 6 | import dagger.hilt.android.qualifiers.ApplicationContext 7 | import fr.twentynine.keepon.services.KeepOnTileService 8 | import javax.inject.Inject 9 | 10 | interface QSTileUpdater { 11 | fun requestUpdate() 12 | } 13 | 14 | class QSTileUpdaterImpl @Inject constructor(@param:ApplicationContext private val context: Context) : QSTileUpdater { 15 | 16 | override fun requestUpdate() { 17 | TileService.requestListeningState( 18 | context.applicationContext, 19 | ComponentName(context.applicationContext, KeepOnTileService::class.java) 20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/di/ScreenOffReceiverServiceManagerModule.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.di 2 | 3 | import android.content.Context 4 | import dagger.Module 5 | import dagger.Provides 6 | import dagger.hilt.InstallIn 7 | import dagger.hilt.android.qualifiers.ApplicationContext 8 | import dagger.hilt.components.SingletonComponent 9 | import fr.twentynine.keepon.services.ScreenOffReceiverServiceManager 10 | import fr.twentynine.keepon.services.ScreenOffReceiverServiceManagerImpl 11 | 12 | @Module 13 | @InstallIn(SingletonComponent::class) 14 | object ScreenOffReceiverServiceManagerModule { 15 | 16 | @Provides 17 | fun provideScreenOffReceiverServiceManager(@ApplicationContext context: Context): ScreenOffReceiverServiceManager { 18 | return ScreenOffReceiverServiceManagerImpl(context) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/util/extensions/BroadcastReceiverGoAsync.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.util.extensions 2 | 3 | import android.content.BroadcastReceiver 4 | import kotlinx.coroutines.CoroutineScope 5 | import kotlinx.coroutines.DelicateCoroutinesApi 6 | import kotlinx.coroutines.GlobalScope 7 | import kotlinx.coroutines.launch 8 | import kotlinx.coroutines.withTimeout 9 | import kotlin.coroutines.CoroutineContext 10 | import kotlin.coroutines.EmptyCoroutineContext 11 | 12 | fun BroadcastReceiver.goAsync( 13 | context: CoroutineContext = EmptyCoroutineContext, 14 | block: suspend CoroutineScope.() -> Unit 15 | ) { 16 | val pendingResult = goAsync() 17 | @OptIn(DelicateCoroutinesApi::class) 18 | GlobalScope.launch(context) { withTimeout(5000) { block() } } 19 | .invokeOnCompletion { pendingResult.finish() } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/di/BatteryOptimizationManagerModule.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.di 2 | 3 | import android.content.Context 4 | import dagger.Module 5 | import dagger.Provides 6 | import dagger.hilt.InstallIn 7 | import dagger.hilt.android.components.ActivityComponent 8 | import dagger.hilt.android.qualifiers.ActivityContext 9 | import dagger.hilt.android.scopes.ActivityScoped 10 | import fr.twentynine.keepon.util.BatteryOptimizationManager 11 | import fr.twentynine.keepon.util.BatteryOptimizationManagerImpl 12 | 13 | @Module 14 | @InstallIn(ActivityComponent::class) 15 | object BatteryOptimizationManagerModule { 16 | @Provides 17 | @ActivityScoped 18 | fun provideBatteryOptimizationManager(@ActivityContext context: Context): BatteryOptimizationManager { 19 | return BatteryOptimizationManagerImpl(context) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/data/model/TaskerEditUIState.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.data.model 2 | 3 | import androidx.compose.runtime.Immutable 4 | 5 | sealed interface TaskerEditUIState { 6 | data object Loading : TaskerEditUIState 7 | data class Error(val error: String) : TaskerEditUIState 8 | 9 | @Immutable 10 | data class Success( 11 | val canWriteSystemSettings: Boolean, 12 | val canPostNotification: Boolean, 13 | val batteryIsNotOptimized: Boolean, 14 | val defaultScreenTimeout: ScreenTimeout, 15 | val previousScreenTimeout: ScreenTimeout, 16 | val timeoutIconStyle: TimeoutIconStyle, 17 | val specialScreenTimeouts: List, 18 | val screenTimeouts: List, 19 | val selectedScreenTimeout: ScreenTimeoutUI?, 20 | ) : TaskerEditUIState 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/di/SystemSettingPermissionManagerModule.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.di 2 | 3 | import android.content.Context 4 | import dagger.Module 5 | import dagger.Provides 6 | import dagger.hilt.InstallIn 7 | import dagger.hilt.android.components.ActivityComponent 8 | import dagger.hilt.android.qualifiers.ActivityContext 9 | import dagger.hilt.android.scopes.ActivityScoped 10 | import fr.twentynine.keepon.util.SystemSettingPermissionManager 11 | import fr.twentynine.keepon.util.SystemSettingPermissionManagerImpl 12 | 13 | @Module 14 | @InstallIn(ActivityComponent::class) 15 | object SystemSettingPermissionManagerModule { 16 | @Provides 17 | @ActivityScoped 18 | fun bindSystemSettingPermissionManager(@ActivityContext context: Context): SystemSettingPermissionManager { 19 | return SystemSettingPermissionManagerImpl(context) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/di/PostNotificationPermissionManagerModule.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.di 2 | 3 | import android.content.Context 4 | import dagger.Module 5 | import dagger.Provides 6 | import dagger.hilt.InstallIn 7 | import dagger.hilt.android.components.ActivityComponent 8 | import dagger.hilt.android.qualifiers.ActivityContext 9 | import dagger.hilt.android.scopes.ActivityScoped 10 | import fr.twentynine.keepon.util.PostNotificationPermissionManager 11 | import fr.twentynine.keepon.util.PostNotificationPermissionManagerImpl 12 | 13 | @Module 14 | @InstallIn(ActivityComponent::class) 15 | object PostNotificationPermissionManagerModule { 16 | @Provides 17 | @ActivityScoped 18 | fun bindPostNotificationPermissionManager(@ActivityContext context: Context): PostNotificationPermissionManager { 19 | return PostNotificationPermissionManagerImpl(context) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/util/BundleScrubber.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.util 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | 6 | object BundleScrubber { 7 | fun scrub(intent: Intent?): Boolean { 8 | return if (null == intent) { 9 | false 10 | } else { 11 | scrub(intent.extras) 12 | } 13 | } 14 | 15 | private fun scrub(bundle: Bundle?): Boolean { 16 | if (null == bundle) { 17 | return false 18 | } 19 | 20 | // This is a hack to work around a private serializable classloader attack 21 | try { 22 | // if a private serializable exists, this will throw an exception 23 | bundle.containsKey(null) 24 | } catch (_: Exception) { 25 | bundle.clear() 26 | return true 27 | } 28 | return false 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/di/AppVersionManagerModule.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.di 2 | 3 | import android.content.Context 4 | import dagger.Module 5 | import dagger.Provides 6 | import dagger.hilt.InstallIn 7 | import dagger.hilt.android.qualifiers.ApplicationContext 8 | import dagger.hilt.components.SingletonComponent 9 | import fr.twentynine.keepon.data.repo.UserPreferencesRepository 10 | import fr.twentynine.keepon.util.AppVersionManager 11 | import fr.twentynine.keepon.util.AppVersionManagerImpl 12 | 13 | @Module 14 | @InstallIn(SingletonComponent::class) 15 | object AppVersionManagerModule { 16 | 17 | @Provides 18 | fun provideAppVersionManager( 19 | userPreferencesRepository: UserPreferencesRepository, 20 | @ApplicationContext context: Context, 21 | ): AppVersionManager { 22 | return AppVersionManagerImpl(userPreferencesRepository, context) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/data/model/MainViewUIState.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.data.model 2 | 3 | import androidx.compose.runtime.Immutable 4 | import fr.twentynine.keepon.data.local.TipsInfo 5 | 6 | sealed interface MainViewUIState { 7 | data object Loading : MainViewUIState 8 | data class Error(val error: String) : MainViewUIState 9 | 10 | @Immutable 11 | data class Success( 12 | val canWriteSystemSettings: Boolean, 13 | val canPostNotification: Boolean, 14 | val batteryIsNotOptimized: Boolean, 15 | val screenTimeouts: List, 16 | val tipsList: List, 17 | val resetTimeoutWhenScreenOff: Boolean, 18 | val currentScreenTimeout: ScreenTimeout, 19 | val keepOnIsActive: Boolean, 20 | val timeoutIconStyle: TimeoutIconStyle, 21 | val isFirstLaunch: Boolean, 22 | ) : MainViewUIState 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/ui/navigation/NavigationActions.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.ui.navigation 2 | 3 | import androidx.navigation.NavGraph.Companion.findStartDestination 4 | import androidx.navigation.NavHostController 5 | 6 | class NavigationActions(private val navController: NavHostController) { 7 | 8 | fun navigateTo(destination: NavigationDestination) { 9 | navController.navigate(destination.route) { 10 | // Pop up to the start destination of the graph to 11 | // avoid building up a large stack of destinations 12 | // on the back stack as users select items 13 | popUpTo(navController.graph.findStartDestination().id) { 14 | saveState = true 15 | } 16 | // Avoid multiple copies of the same destination when 17 | // reselecting the same item 18 | launchSingleTop = true 19 | // Restore state when reselecting a previously selected item 20 | restoreState = true 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/receiver/ScreenOffReceiver.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.receiver 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import dagger.hilt.android.AndroidEntryPoint 7 | import fr.twentynine.keepon.data.repo.UserPreferencesRepository 8 | import fr.twentynine.keepon.util.QSTileUpdater 9 | import fr.twentynine.keepon.util.extensions.goAsync 10 | import kotlinx.coroutines.Dispatchers 11 | import javax.inject.Inject 12 | 13 | @AndroidEntryPoint 14 | class ScreenOffReceiver : BroadcastReceiver() { 15 | 16 | @Inject 17 | lateinit var userPreferencesRepository: UserPreferencesRepository 18 | 19 | @Inject 20 | lateinit var qsTileUpdater: QSTileUpdater 21 | 22 | override fun onReceive(context: Context, intent: Intent) { 23 | if (intent.action == Intent.ACTION_SCREEN_OFF) { 24 | goAsync(Dispatchers.Default) { 25 | userPreferencesRepository.resetSystemScreenTimeoutToDefault { qsTileUpdater.requestUpdate() } 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/data/model/MainUIEvent.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.data.model 2 | 3 | sealed interface MainUIEvent { 4 | data object RequestWriteSystemSettingPermission : MainUIEvent 5 | data object RequestDisableBatteryOptimization : MainUIEvent 6 | data object SetNextSelectedSystemScreenTimeout : MainUIEvent 7 | data object UpdateIsFirstLaunch : MainUIEvent 8 | data object RequestPostNotification : MainUIEvent 9 | data object RequestAddTileService : MainUIEvent 10 | data object RequestAppRate : MainUIEvent 11 | data object CheckNeededPermissions : MainUIEvent 12 | data object IncrementAppLaunchCount : MainUIEvent 13 | data class SetResetTimeoutWhenScreenOff(val resetTimeoutWhenScreenOff: Boolean) : MainUIEvent 14 | data class ToggleScreenTimeoutSelection(val screenTimeoutUI: ScreenTimeoutUI) : MainUIEvent 15 | data class SetDefaultScreenTimeout(val timeout: ScreenTimeout) : MainUIEvent 16 | data class UpdateTimeoutIconStyle(val timeoutIconStyle: TimeoutIconStyle) : MainUIEvent 17 | data class DismissTips(val tipsId: Int) : MainUIEvent 18 | } 19 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | 23 | # Coil rules from https://github.com/coil-kt/coil/blob/main/docs/faq.md#how-to-i-use-proguard-with-coil 24 | -keep class coil3.util.DecoderServiceLoaderTarget { *; } 25 | -keep class coil3.util.FetcherServiceLoaderTarget { *; } 26 | -keep class coil3.util.ServiceLoaderComponentRegistry { *; } 27 | -keep class * implements coil3.util.DecoderServiceLoaderTarget { *; } 28 | -keep class * implements coil3.util.FetcherServiceLoaderTarget { *; } -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/data/repo/ScreenTimeoutRepository.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.data.repo 2 | 3 | import fr.twentynine.keepon.data.enums.SpecialScreenTimeoutType 4 | import fr.twentynine.keepon.data.model.ScreenTimeout 5 | import kotlinx.collections.immutable.toPersistentList 6 | 7 | object ScreenTimeoutRepository { 8 | val screenTimeouts = listOf( 9 | ScreenTimeout( 10 | value = 15000 11 | ), 12 | ScreenTimeout( 13 | value = 30000 14 | ), 15 | ScreenTimeout( 16 | value = 60000 17 | ), 18 | ScreenTimeout( 19 | value = 120000 20 | ), 21 | ScreenTimeout( 22 | value = 300000 23 | ), 24 | ScreenTimeout( 25 | value = 600000 26 | ), 27 | ScreenTimeout( 28 | value = 1800000 29 | ), 30 | ScreenTimeout( 31 | value = 3600000 32 | ), 33 | ScreenTimeout( 34 | value = Int.MAX_VALUE 35 | ) 36 | ).toPersistentList() 37 | 38 | val specialScreenTimeouts = listOf( 39 | ScreenTimeout( 40 | value = SpecialScreenTimeoutType.DEFAULT_SCREEN_TIMEOUT_TYPE.value 41 | ), 42 | ScreenTimeout( 43 | value = SpecialScreenTimeoutType.PREVIOUS_SCREEN_TIMEOUT_TYPE.value 44 | ) 45 | ).toPersistentList() 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/util/SystemScreenTimeoutController.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.util 2 | 3 | import android.content.Context 4 | import android.provider.Settings 5 | import dagger.hilt.android.qualifiers.ApplicationContext 6 | import fr.twentynine.keepon.data.model.ScreenTimeout 7 | import javax.inject.Inject 8 | 9 | interface SystemScreenTimeoutController { 10 | fun getSystemScreenTimeout(): ScreenTimeout 11 | fun setSystemScreenTimeout(timeout: ScreenTimeout) 12 | } 13 | 14 | class SystemScreenTimeoutControllerImpl @Inject constructor(@param:ApplicationContext private val context: Context) : SystemScreenTimeoutController { 15 | 16 | private val contentResolver by lazy { context.contentResolver } 17 | 18 | override fun getSystemScreenTimeout(): ScreenTimeout { 19 | return ScreenTimeout( 20 | Settings.System.getInt( 21 | contentResolver, 22 | Settings.System.SCREEN_OFF_TIMEOUT, 23 | DEFAULT_SCREEN_TIMEOUT 24 | ) 25 | ) 26 | } 27 | 28 | override fun setSystemScreenTimeout(timeout: ScreenTimeout) { 29 | Settings.System.putInt( 30 | contentResolver, 31 | Settings.System.SCREEN_OFF_TIMEOUT, 32 | timeout.value 33 | ) 34 | } 35 | 36 | companion object { 37 | private const val DEFAULT_SCREEN_TIMEOUT = 60000 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/di/UserPreferencesRepositoryModule.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.di 2 | 3 | import dagger.Module 4 | import dagger.Provides 5 | import dagger.hilt.InstallIn 6 | import dagger.hilt.components.SingletonComponent 7 | import fr.twentynine.keepon.data.local.PreferenceDataStoreHelper 8 | import fr.twentynine.keepon.data.repo.UserPreferencesRepository 9 | import fr.twentynine.keepon.data.repo.UserPreferencesRepositoryImpl 10 | import fr.twentynine.keepon.services.ScreenOffReceiverServiceManager 11 | import fr.twentynine.keepon.util.DevicePolicyManagerHelper 12 | import fr.twentynine.keepon.util.SystemScreenTimeoutController 13 | 14 | @Module 15 | @InstallIn(SingletonComponent::class) 16 | object UserPreferencesRepositoryModule { 17 | 18 | @Provides 19 | fun provideUserPreferencesRepository( 20 | preferenceDataStoreHelper: PreferenceDataStoreHelper, 21 | systemScreenTimeoutController: dagger.Lazy, 22 | devicePolicyManagerHelper: dagger.Lazy, 23 | screenOffReceiverServiceManager: dagger.Lazy, 24 | ): UserPreferencesRepository { 25 | return UserPreferencesRepositoryImpl( 26 | preferenceDataStoreHelper, 27 | systemScreenTimeoutController, 28 | devicePolicyManagerHelper, 29 | screenOffReceiverServiceManager 30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/ui/navigation/NavigationDestination.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.ui.navigation 2 | 3 | import androidx.annotation.StringRes 4 | import androidx.compose.material.icons.Icons 5 | import androidx.compose.material.icons.filled.Info 6 | import androidx.compose.material.icons.outlined.Info 7 | import androidx.compose.ui.graphics.vector.ImageVector 8 | import fr.twentynine.keepon.R 9 | import fr.twentynine.keepon.ui.theme.icons.HomeFilled 10 | import fr.twentynine.keepon.ui.theme.icons.HomeOutlined 11 | import fr.twentynine.keepon.ui.theme.icons.IconStyleFilled 12 | import fr.twentynine.keepon.ui.theme.icons.IconStyleOutlined 13 | 14 | sealed class NavigationDestination( 15 | val route: String, 16 | @param:StringRes val iconTextId: Int, 17 | val selectedIcon: ImageVector, 18 | val unSelectedIcon: ImageVector, 19 | ) { 20 | data object Home : NavigationDestination( 21 | "home", 22 | R.string.screen_home, 23 | HomeFilled, 24 | HomeOutlined, 25 | ) 26 | data object Style : NavigationDestination( 27 | "style", 28 | R.string.screen_style, 29 | IconStyleFilled, 30 | IconStyleOutlined, 31 | ) 32 | data object About : NavigationDestination( 33 | "about", 34 | R.string.screen_about, 35 | Icons.Filled.Info, 36 | Icons.Outlined.Info, 37 | ) 38 | } 39 | 40 | val TOP_LEVEL_DESTINATIONS = listOf( 41 | NavigationDestination.Home, 42 | NavigationDestination.Style, 43 | NavigationDestination.About 44 | ) 45 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 | Features: 2 | 3 | KeepOn is free and open-source (FOSS), without any ads/tracks and no Internet use. 4 | KeepOn allows you to keep your device's screen on for the desired duration and also allows you to return to the default settings automaticaly when the screen turns off. 5 | KeepOn adapts to your device's configuration to easy use! 6 | A Tasker/Locale plugin is integrated and allows you to use KeepOn functions from another compatible application! 7 | 8 | Permissions: 9 | 10 | - android.permission.WRITE_SETTINGS: Needed by Android to modify screen timeout settings 11 | - android.permission.FOREGROUND_SERVICE: Needed to detect screen off events and other screen timeout modifications 12 | - android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS: Needed to prevent Android from closing KeepOn while it is running 13 | - android.permission.POST_NOTIFICATIONS: Needed to allow KeepOn to display notification while it is running 14 | 15 | 16 | Credits: 17 | 18 | Libraries: 19 | - Coil: https://github.com/coil-kt/coil 20 | 21 | Fonts: 22 | - Roboto: https://fonts.google.com/specimen/Roboto?query=roboto 23 | - Bitter: https://fonts.google.com/specimen/Bitter?query=bitter 24 | - Open Sans: https://fonts.google.com/specimen/Open+Sans?query=open+sans 25 | - Caudex: https://fonts.google.com/specimen/Caudex?query=Caudex 26 | - Poppins: https://fonts.google.com/specimen/Poppins?query=Poppins 27 | - Lora: https://fonts.google.com/specimen/Lora?query=Lora 28 | 29 | Source code: https://github.com/twentynine78/KeepOn -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/util/AddTileServiceManager.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.util 2 | 3 | import android.app.StatusBarManager 4 | import android.content.ComponentName 5 | import android.content.Context 6 | import android.graphics.drawable.Icon 7 | import android.os.Build 8 | import dagger.hilt.android.qualifiers.ApplicationContext 9 | import fr.twentynine.keepon.R 10 | import fr.twentynine.keepon.services.KeepOnTileService 11 | import java.util.concurrent.Executor 12 | import javax.inject.Inject 13 | 14 | interface AddTileServiceManager { 15 | fun requestAddTileService(successCallback: () -> Unit, errorCallback: (Int) -> Unit = {}) 16 | } 17 | 18 | class AddTileServiceManagerImpl @Inject constructor(@param:ApplicationContext private val context: Context) : AddTileServiceManager { 19 | 20 | override fun requestAddTileService(successCallback: () -> Unit, errorCallback: (Int) -> Unit) { 21 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 22 | val statusBarManager = 23 | context.getSystemService(StatusBarManager::class.java) 24 | 25 | val resultSuccessExecutor = Executor { 26 | successCallback() 27 | } 28 | 29 | statusBarManager.requestAddTileService( 30 | ComponentName(context, KeepOnTileService::class.java), 31 | context.getString(R.string.qs_service_name), 32 | Icon.createWithResource(context, R.drawable.ic_keepon), 33 | resultSuccessExecutor, 34 | errorCallback 35 | ) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/util/DevicePolicyManagerHelper.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.util 2 | 3 | import android.app.admin.DevicePolicyManager 4 | import android.content.Context 5 | import dagger.hilt.android.qualifiers.ApplicationContext 6 | import fr.twentynine.keepon.data.model.ScreenTimeout 7 | import javax.inject.Inject 8 | 9 | interface DevicePolicyManagerHelper { 10 | fun getMaxAllowedScreenTimeout(): Long 11 | fun isValidTimeout(timeout: ScreenTimeout): Boolean 12 | fun removeNotAllowedScreenTimeout(timeouts: List): List 13 | } 14 | 15 | class DevicePolicyManagerHelperImpl @Inject constructor(@param:ApplicationContext private val context: Context) : DevicePolicyManagerHelper { 16 | 17 | private val devicePolicyManager by lazy { 18 | context.getSystemService(DevicePolicyManager::class.java) 19 | } 20 | 21 | override fun getMaxAllowedScreenTimeout(): Long { 22 | var maxAllowedTimeout = devicePolicyManager.getMaximumTimeToLock(null) 23 | if (maxAllowedTimeout == 0L) maxAllowedTimeout = Long.MAX_VALUE 24 | 25 | return maxAllowedTimeout 26 | } 27 | 28 | override fun isValidTimeout(timeout: ScreenTimeout): Boolean { 29 | return timeout.value <= getMaxAllowedScreenTimeout() && timeout.value != -1 30 | } 31 | 32 | override fun removeNotAllowedScreenTimeout(timeouts: List): List { 33 | val maxAllowedScreenTimeout = getMaxAllowedScreenTimeout() 34 | return timeouts.filter { timeout -> 35 | timeout.value <= maxAllowedScreenTimeout 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/ui/util/PulsatingIcon.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.ui.util 2 | 3 | import androidx.compose.animation.core.InfiniteTransition 4 | import androidx.compose.animation.core.RepeatMode 5 | import androidx.compose.animation.core.animateFloat 6 | import androidx.compose.animation.core.infiniteRepeatable 7 | import androidx.compose.animation.core.tween 8 | import androidx.compose.foundation.layout.Box 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.layout.size 11 | import androidx.compose.material3.Icon 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.getValue 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.graphics.vector.ImageVector 17 | import androidx.compose.ui.unit.dp 18 | 19 | @Composable 20 | fun PulsatingIcon( 21 | infiniteTransition: InfiniteTransition, 22 | initialSize: Float, 23 | imageVector: ImageVector, 24 | contentDescription: String?, 25 | modifier: Modifier = Modifier, 26 | ) { 27 | val pulsate by infiniteTransition.animateFloat( 28 | initialValue = initialSize * 0.9f, 29 | targetValue = initialSize * 1.49f, 30 | animationSpec = infiniteRepeatable(tween(1200), RepeatMode.Reverse) 31 | ) 32 | Box( 33 | modifier = Modifier 34 | .size((initialSize * 1.5f).dp) 35 | .padding(bottom = 2.dp), 36 | contentAlignment = Alignment.Center, 37 | ) { 38 | Icon( 39 | imageVector = imageVector, 40 | contentDescription = contentDescription, 41 | modifier = modifier 42 | .size(pulsate.dp) 43 | ) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /fastlane/metadata/android/fr-FR/full_description.txt: -------------------------------------------------------------------------------- 1 | Caractéristique : 2 | 3 | KeepOn est gratuit et open-source (FOSS), sans publicités/trackers et sans utilisation d'Internet. 4 | KeepOn te permet de garder l'écran de ton appareil allumé pendant la durée souhaitée et te permet également de revenir automatiquement aux paramètres par défaut lorsque l'écran s'éteint. 5 | KeepOn s'adapte à la configuration de ton appareil pour une utilisation facile ! 6 | Une extension Tasker/Locale est intégrée et permet d'utiliser les fonctions de KeepOn depuis une autre application compatible ! 7 | 8 | Permissions : 9 | 10 | - android.permission.WRITE_SETTINGS : Nécessaire à Android pour modifier les paramètres de délai d'expiration d'écran 11 | - android.permission.FOREGROUND_SERVICE : Nécessaire pour détecter les événements de désactivation de l'écran et d'autres modifications de délai d'expiration de l'écran 12 | - android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS: Nécessaire pour empêcher Android de fermer KeepOn pendant son exécution 13 | - android.permission.POST_NOTIFICATIONS: Nécessaire pour permettre à KeepOn d'afficher des notifications pendant son exécution 14 | 15 | 16 | Crédits : 17 | 18 | Bibliothèques: 19 | - Coil: https://github.com/coil-kt/coil 20 | 21 | Police de caractères: 22 | - Roboto: https://fonts.google.com/specimen/Roboto?query=roboto 23 | - Bitter: https://fonts.google.com/specimen/Bitter?query=bitter 24 | - Open Sans: https://fonts.google.com/specimen/Open+Sans?query=open+sans 25 | - Caudex: https://fonts.google.com/specimen/Caudex?query=Caudex 26 | - Poppins: https://fonts.google.com/specimen/Poppins?query=Poppins 27 | - Lora: https://fonts.google.com/specimen/Lora?query=Lora 28 | 29 | 30 | Code source : https://github.com/twentynine78/KeepOn -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/data/local/PreferenceDataStoreConstants.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.data.local 2 | 3 | import androidx.compose.runtime.Immutable 4 | import androidx.datastore.preferences.core.booleanPreferencesKey 5 | import androidx.datastore.preferences.core.intPreferencesKey 6 | import androidx.datastore.preferences.core.longPreferencesKey 7 | import androidx.datastore.preferences.core.stringPreferencesKey 8 | 9 | // Use old key for compatibility 10 | @Immutable 11 | object PreferenceDataStoreConstants { 12 | // Backed up data 13 | val SELECTED_SCREEN_TIMEOUT = stringPreferencesKey("newSelectedTimeout") 14 | val OLD_SELECTED_SCREEN_TIMEOUT = stringPreferencesKey("selectedTimeout") 15 | val RESET_TIMEOUT_WHEN_SCREEN_OFF = booleanPreferencesKey("resetTimeoutWhenScreenOff") 16 | val OLD_RESET_TIMEOUT_WHEN_SCREEN_OFF = booleanPreferencesKey("resetTimeoutOnScreenOff") 17 | val OLD_TIMEOUT_ICON_STYLE = stringPreferencesKey("timeoutIconStyle") 18 | val TIMEOUT_ICON_STYLE = stringPreferencesKey("newTimeoutIconStyle") 19 | val DISMISSED_TIPS = stringPreferencesKey("dismissedTips") 20 | val APP_LAUNCH_COUNT = longPreferencesKey("appLaunchCount") 21 | val OLD_APP_REVIEW_ASKED = booleanPreferencesKey("appReviewAsked") 22 | val OLD_SKIP_INTRO = booleanPreferencesKey("skipIntro") 23 | val IS_FIRST_LAUNCH = booleanPreferencesKey("isFirstLaunch") 24 | val LAST_RUN_VERSION_CODE = longPreferencesKey("lastRunVersionCode") 25 | 26 | // No backed up data 27 | val DEFAULT_SCREEN_TIMEOUT = intPreferencesKey("originalTimeout") 28 | val CURRENT_SCREEN_TIMEOUT = intPreferencesKey("newValue") 29 | val PREVIOUS_SCREEN_TIMEOUT = intPreferencesKey("previousValue") 30 | val QSTILE_ADDED = booleanPreferencesKey("tileAdded") 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/util/AppVersionManager.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.util 2 | 3 | import android.content.Context 4 | import android.content.pm.PackageManager 5 | import dagger.hilt.android.qualifiers.ApplicationContext 6 | import fr.twentynine.keepon.data.repo.UserPreferencesRepository 7 | import javax.inject.Inject 8 | 9 | interface AppVersionManager { 10 | suspend fun runAppMigrationIfNeeded() 11 | } 12 | 13 | class AppVersionManagerImpl @Inject constructor( 14 | private val userPreferencesRepository: UserPreferencesRepository, 15 | @param:ApplicationContext private val context: Context, 16 | ) : AppVersionManager { 17 | 18 | override suspend fun runAppMigrationIfNeeded() { 19 | val lastRunVersionCode = userPreferencesRepository.getLastRunVersionCode() 20 | val currentVersionCode = getCurrentVersionCode() 21 | 22 | if (lastRunVersionCode < currentVersionCode) { 23 | runMigrationTasks(lastRunVersionCode) 24 | userPreferencesRepository.setLastRunVersionCode(currentVersionCode) 25 | } 26 | } 27 | 28 | private fun getCurrentVersionCode(): Long { 29 | return try { 30 | val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0) 31 | 32 | packageInfo.longVersionCode 33 | } catch (_: PackageManager.NameNotFoundException) { 34 | -1 35 | } 36 | } 37 | 38 | private fun runMigrationTasks(lastRunVersionCode: Long) { 39 | if (lastRunVersionCode < 20) { 40 | // Remove old resources used by old KeepOn versions 41 | DynamicShortcutManager.removeAllDynamicShortcut(context) 42 | PostNotificationPermissionManager.removeOldNotificationChannelKeepOnService(context) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.parallel=true 10 | org.gradle.caching=true 11 | org.gradle.jvmargs=-Xmx6g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:+UseParallelGC -XX:MaxMetaspaceSize=1g 12 | org.gradle.configuration-cache=true 13 | # Use this flag carefully, in case some of the plugins are not fully compatible. 14 | org.gradle.configuration-cache.problems=warn 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | # AndroidX package structure to make it clearer which packages are bundled with the 20 | # Android operating system, and which are packaged with your app's APK 21 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 22 | android.useAndroidX=true 23 | # Kotlin code style for this project: "official" or "obsolete": 24 | kotlin.code.style=official 25 | # Enables namespacing of each library's R class so that its R class includes only the 26 | # resources declared in the library itself and none from the library's dependencies, 27 | # thereby reducing the size of the R class for that library 28 | android.nonTransitiveRClass=true 29 | 30 | # System properties 31 | systemProp.pts.enabled=true 32 | systemProp.log4j2.disableJmx=true 33 | systemProp.file.encoding = UTF-8 -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/util/coil/TimeoutIconDataFetcher.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.util.coil 2 | 3 | import coil3.ImageLoader 4 | import coil3.asImage 5 | import coil3.decode.DataSource 6 | import coil3.fetch.FetchResult 7 | import coil3.fetch.Fetcher 8 | import coil3.fetch.ImageFetchResult 9 | import coil3.request.Options 10 | import dagger.hilt.EntryPoint 11 | import dagger.hilt.InstallIn 12 | import dagger.hilt.android.EntryPointAccessors 13 | import dagger.hilt.components.SingletonComponent 14 | import fr.twentynine.keepon.data.model.TimeoutIconData 15 | import fr.twentynine.keepon.util.StringResourceProvider 16 | 17 | class TimeoutIconDataFetcher( 18 | private val data: TimeoutIconData, 19 | private val options: Options 20 | ) : Fetcher { 21 | 22 | @EntryPoint 23 | @InstallIn(SingletonComponent::class) 24 | interface TimeoutIconDataFetcherEntryPoint { 25 | fun stringResourceProvider(): StringResourceProvider 26 | } 27 | 28 | override suspend fun fetch(): FetchResult { 29 | val hiltEntryPoint = EntryPointAccessors.fromApplication( 30 | options.context, 31 | TimeoutIconDataFetcherEntryPoint::class.java 32 | ) 33 | val stringResourceProvider = hiltEntryPoint.stringResourceProvider() 34 | 35 | return ImageFetchResult( 36 | image = TimeoutIconGenerator().getBitmapFromText( 37 | options.context, 38 | data, 39 | stringResourceProvider 40 | ).asImage(shareable = true), 41 | isSampled = false, 42 | dataSource = DataSource.MEMORY 43 | ) 44 | } 45 | 46 | class Factory : Fetcher.Factory { 47 | 48 | override fun create(data: TimeoutIconData, options: Options, imageLoader: ImageLoader): Fetcher { 49 | return TimeoutIconDataFetcher(data, options) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/worker/MonitorSystemScreenTimeoutWorkScheduler.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.worker 2 | 3 | import android.provider.Settings 4 | import androidx.work.BackoffPolicy 5 | import androidx.work.Constraints 6 | import androidx.work.ExistingWorkPolicy 7 | import androidx.work.OneTimeWorkRequestBuilder 8 | import androidx.work.WorkManager 9 | import androidx.work.WorkRequest 10 | import fr.twentynine.keepon.util.extensions.uuid 11 | import java.time.Duration 12 | 13 | object MonitorSystemScreenTimeoutWorkScheduler { 14 | 15 | private const val SYSTEM_SCREEN_TIMEOUT_WORKER = "system_screen_timeout_worker" 16 | private val SYSTEM_SCREEN_TIMEOUT_WORKER_ID = SYSTEM_SCREEN_TIMEOUT_WORKER.uuid() 17 | 18 | private val screenTimeoutSettingContentUri = Settings.System.getUriFor(Settings.System.SCREEN_OFF_TIMEOUT) 19 | 20 | private val workRequestConstraints = Constraints.Builder() 21 | .addContentUriTrigger(screenTimeoutSettingContentUri, true) 22 | .setTriggerContentUpdateDelay(Duration.ZERO) 23 | .build() 24 | 25 | private val monitorSystemScreenTimeoutWorkRequest = OneTimeWorkRequestBuilder() 26 | .setId(SYSTEM_SCREEN_TIMEOUT_WORKER_ID) 27 | .setConstraints(workRequestConstraints) 28 | .setInitialDelay(Duration.ZERO) 29 | .setBackoffCriteria(BackoffPolicy.LINEAR, Duration.ofMillis(WorkRequest.MIN_BACKOFF_MILLIS)) 30 | .build() 31 | 32 | fun scheduleWork(workManager: WorkManager, requeueIfRunning: Boolean = false) { 33 | workManager.pruneWork() 34 | val workInfo = workManager.getWorkInfoById(SYSTEM_SCREEN_TIMEOUT_WORKER_ID).get() 35 | if (workInfo == null || requeueIfRunning) { 36 | workManager.enqueueUniqueWork( 37 | SYSTEM_SCREEN_TIMEOUT_WORKER, 38 | ExistingWorkPolicy.REPLACE, 39 | monitorSystemScreenTimeoutWorkRequest 40 | ) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/data/model/OldTimeoutIconStyle.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.data.model 2 | 3 | import androidx.compose.runtime.Stable 4 | import fr.twentynine.keepon.data.local.IconFontFamily 5 | import kotlinx.serialization.Serializable 6 | 7 | @Stable 8 | @Serializable 9 | data class OldTimeoutIconStyle( 10 | val iconStyleFontSize: Int = 0, 11 | val iconStyleFontSkew: Int = 0, 12 | val iconStyleFontSpacing: Int = 0, 13 | val iconStyleTypefaceSansSerif: Boolean = true, 14 | val iconStyleTypefaceSerif: Boolean = false, 15 | val iconStyleTypefaceMonospace: Boolean = false, 16 | val iconStyleFontBold: Boolean = true, 17 | val iconStyleFontUnderline: Boolean = false, 18 | val iconStyleFontSMCP: Boolean = false, 19 | val iconStyleTextFill: Boolean = true, 20 | val iconStyleTextFillStroke: Boolean = false, 21 | val iconStyleTextStroke: Boolean = false 22 | ) { 23 | val toTimeoutIconStyle: TimeoutIconStyle 24 | get() { 25 | val newFontFamilyName = when { 26 | this.iconStyleTypefaceSansSerif -> IconFontFamily.Roboto.name 27 | this.iconStyleTypefaceSerif -> IconFontFamily.Bitter.name 28 | this.iconStyleTypefaceMonospace -> IconFontFamily.Roboto.name 29 | else -> IconFontFamily.Roboto.name 30 | } 31 | 32 | return TimeoutIconStyle( 33 | iconStyleFontSize = if (this.iconStyleFontSize != 0) this.iconStyleFontSize - 1 else 0, 34 | iconStyleFontHorizontalSpacing = this.iconStyleFontSpacing, 35 | iconStyleFontVerticalSpacing = 0, 36 | iconFontFamilyName = newFontFamilyName, 37 | iconStyleFontBold = this.iconStyleFontBold || this.iconStyleTextFillStroke, 38 | iconStyleFontItalic = this.iconStyleFontSkew == 1, 39 | iconStyleFontUnderline = this.iconStyleFontUnderline, 40 | iconStyleTextOutlined = this.iconStyleTextStroke, 41 | ) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/worker/SetNewScreenTimeoutWorkScheduler.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.worker 2 | 3 | import android.content.Context 4 | import androidx.work.BackoffPolicy 5 | import androidx.work.Constraints 6 | import androidx.work.Data 7 | import androidx.work.ExistingWorkPolicy 8 | import androidx.work.OneTimeWorkRequestBuilder 9 | import androidx.work.OutOfQuotaPolicy 10 | import androidx.work.WorkManager 11 | import androidx.work.WorkRequest 12 | import java.time.Duration 13 | 14 | class SetNewScreenTimeoutWorkScheduler { 15 | 16 | private val workRequestConstraints = Constraints.Builder().build() 17 | 18 | fun scheduleWork(newScreenTimeout: Int, context: Context, updatePreviousTimeout: Boolean = false) { 19 | val dataBuilder = Data.Builder() 20 | dataBuilder.putInt(NEW_SCREEN_TIMEOUT_DATA_KEY, newScreenTimeout) 21 | dataBuilder.putBoolean(UPDATE_PREVIOUS_TIMEOUT_DATA_KEY, updatePreviousTimeout) 22 | 23 | val setNewScreenTimeoutWorkRequest = OneTimeWorkRequestBuilder() 24 | .setConstraints(workRequestConstraints) 25 | .setInitialDelay(Duration.ZERO) 26 | .setBackoffCriteria(BackoffPolicy.LINEAR, Duration.ofMillis(WorkRequest.MIN_BACKOFF_MILLIS)) 27 | .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) 28 | .setInputData(dataBuilder.build()) 29 | .build() 30 | 31 | val workManager: WorkManager = WorkManager.getInstance(context.applicationContext) 32 | 33 | workManager.enqueueUniqueWork( 34 | NEW_SCREEN_TIMEOUT_WORKER, 35 | ExistingWorkPolicy.APPEND_OR_REPLACE, 36 | setNewScreenTimeoutWorkRequest 37 | ) 38 | } 39 | 40 | companion object { 41 | private const val NEW_SCREEN_TIMEOUT_WORKER = "new_screen_timeout_worker" 42 | const val NEW_SCREEN_TIMEOUT_DATA_KEY = "new_screen_timeout_data" 43 | const val UPDATE_PREVIOUS_TIMEOUT_DATA_KEY = "update_previous_timeout_data" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/data/local/CreditInfo.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.data.local 2 | 3 | import androidx.compose.runtime.Immutable 4 | import fr.twentynine.keepon.R 5 | import fr.twentynine.keepon.data.enums.CreditInfoType 6 | 7 | @Immutable 8 | sealed class CreditInfo( 9 | val name: String, 10 | val author: String, 11 | val url: String, 12 | val versionResId: Int? = null, 13 | val type: CreditInfoType 14 | ) { 15 | data object Coil : CreditInfo( 16 | name = "Coil", 17 | author = "Instacart team", 18 | url = "https://github.com/coil-kt/coil", 19 | versionResId = R.string.coil_version, 20 | type = CreditInfoType.Library 21 | ) 22 | data object Roboto : CreditInfo( 23 | name = "Roboto", 24 | author = "Christian Robertson", 25 | url = "https://fonts.google.com/specimen/Roboto?query=roboto", 26 | type = CreditInfoType.Font 27 | ) 28 | data object Bitter : CreditInfo( 29 | name = "Bitter", 30 | author = "Sol Matas", 31 | url = "https://fonts.google.com/specimen/Bitter?query=bitter", 32 | type = CreditInfoType.Font 33 | ) 34 | data object OpenSans : CreditInfo( 35 | name = "Open Sans", 36 | author = "Steve Matteson", 37 | url = "https://fonts.google.com/specimen/Open+Sans?query=open+sans", 38 | type = CreditInfoType.Font 39 | ) 40 | data object Caudex : CreditInfo( 41 | name = "Caudex", 42 | author = "Nidud", 43 | url = "https://fonts.google.com/specimen/Caudex?query=Caudex", 44 | type = CreditInfoType.Font 45 | ) 46 | data object Poppins : CreditInfo( 47 | name = "Poppins", 48 | author = "Indian Type Foundry, Jonny Pinhorn ", 49 | url = "https://fonts.google.com/specimen/Poppins?query=Poppins", 50 | type = CreditInfoType.Font 51 | ) 52 | data object Lora : CreditInfo( 53 | name = "Lora", 54 | author = "Cyreal", 55 | url = "https://fonts.google.com/specimen/Lora?query=Lora", 56 | type = CreditInfoType.Font 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/ui/view/SwipeableScreenTimeoutUICardView.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.ui.view 2 | 3 | import androidx.compose.material3.SwipeToDismissBoxValue 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.remember 6 | import androidx.compose.ui.Modifier 7 | import fr.twentynine.keepon.data.enums.ItemPosition 8 | import fr.twentynine.keepon.data.model.ScreenTimeoutUI 9 | 10 | private const val SCREEN_TIMEOUT_CARD_SWIPE_THRESHOLD = 0.30f 11 | 12 | @Composable 13 | fun SwipeableScreenTimeoutUICardView( 14 | item: ScreenTimeoutUI, 15 | itemPosition: ItemPosition, 16 | modifier: Modifier = Modifier, 17 | swipeEnabled: Boolean = true, 18 | isFirstLaunch: Boolean, 19 | onClickAction: ((ScreenTimeoutUI) -> Unit)?, 20 | onSwipeAction: ((SwipeToDismissBoxValue, ScreenTimeoutUI) -> Unit)?, 21 | content: @Composable (ScreenTimeoutUI?) -> Unit 22 | ) { 23 | val isDefault = remember(item.isDefault) { item.isDefault } 24 | val isFirst = remember(itemPosition) { itemPosition == ItemPosition.FIRST } 25 | 26 | SwipeableItemCardView( 27 | item = item, 28 | itemPosition = itemPosition, 29 | modifier = modifier, 30 | swipeEnabled = swipeEnabled, 31 | animateSwipeCondition = isDefault, 32 | animateFirstDisplayCondition = isFirst && isFirstLaunch, 33 | onClickAction = onClickAction, 34 | onSwipeAction = onSwipeAction, 35 | swipeThresholdFraction = SCREEN_TIMEOUT_CARD_SWIPE_THRESHOLD, 36 | backgroundContent = { dismissDirection, dismissProgress, currentItem -> 37 | if (currentItem != null) { 38 | ScreenTimeoutSetDefaultDismissActionRowView( 39 | dismissDirection = dismissDirection, 40 | dismissProgress = dismissProgress, 41 | screenTimeoutUI = currentItem, 42 | swipeEnabledState = swipeEnabled, 43 | swipeThresholdFraction = SCREEN_TIMEOUT_CARD_SWIPE_THRESHOLD, 44 | ) 45 | } 46 | }, 47 | content = content 48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/data/mapper/ScreenTimeoutToScreenTimeoutUIMapper.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.data.mapper 2 | 3 | import fr.twentynine.keepon.data.model.ScreenTimeout 4 | import fr.twentynine.keepon.data.model.ScreenTimeoutUI 5 | import fr.twentynine.keepon.util.StringResourceProvider 6 | 7 | object ScreenTimeoutToScreenTimeoutUIMapper : Mapper { 8 | private var stringResourceProvider: StringResourceProvider? = null 9 | 10 | private var isSelected: Boolean = false 11 | private var isDefault: Boolean = false 12 | private var isCurrent: Boolean = false 13 | private var isLocked: Boolean = false 14 | 15 | fun setStringResourceProvider(stringResourceProvider: StringResourceProvider): ScreenTimeoutToScreenTimeoutUIMapper { 16 | this.stringResourceProvider = stringResourceProvider 17 | 18 | return this 19 | } 20 | 21 | fun setIsSelected(isSelected: Boolean): ScreenTimeoutToScreenTimeoutUIMapper { 22 | this.isSelected = isSelected 23 | 24 | return this 25 | } 26 | 27 | fun setIsDefault(isDefault: Boolean): ScreenTimeoutToScreenTimeoutUIMapper { 28 | this.isDefault = isDefault 29 | 30 | return this 31 | } 32 | 33 | fun setIsCurrent(isCurrent: Boolean): ScreenTimeoutToScreenTimeoutUIMapper { 34 | this.isCurrent = isCurrent 35 | 36 | return this 37 | } 38 | 39 | fun setIsLocked(isLocked: Boolean): ScreenTimeoutToScreenTimeoutUIMapper { 40 | this.isLocked = isLocked 41 | 42 | return this 43 | } 44 | 45 | override fun map(from: ScreenTimeout): ScreenTimeoutUI { 46 | if (this.stringResourceProvider == null) { 47 | throw InstantiationException("stringResourceProvider not set!") 48 | } 49 | 50 | return ScreenTimeoutUI( 51 | value = from.value, 52 | displayName = this.stringResourceProvider?.let { from.getFullDisplayTimeout(it) }.toString(), 53 | isSelected = this.isSelected, 54 | isDefault = this.isDefault, 55 | isCurrent = this.isCurrent, 56 | isLocked = this.isLocked 57 | ) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/ui/theme/icons/HomeFilled.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.ui.theme.icons 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.graphics.Color 6 | import androidx.compose.ui.graphics.SolidColor 7 | import androidx.compose.ui.graphics.vector.ImageVector 8 | import androidx.compose.ui.graphics.vector.path 9 | import androidx.compose.ui.tooling.preview.Preview 10 | import androidx.compose.ui.unit.dp 11 | 12 | @Preview 13 | @Composable 14 | private fun VectorPreview() { 15 | Image(HomeFilled, null) 16 | } 17 | 18 | val HomeFilled: ImageVector 19 | get() { 20 | if (_HomeFilled != null) { 21 | return _HomeFilled!! 22 | } 23 | _HomeFilled = ImageVector.Builder( 24 | name = "HomeFilled", 25 | defaultWidth = 24.dp, 26 | defaultHeight = 24.dp, 27 | viewportWidth = 24f, 28 | viewportHeight = 24f 29 | ).apply { 30 | path(fill = SolidColor(Color(0xFF000000))) { 31 | moveTo(10f, 19f) 32 | verticalLineToRelative(-5f) 33 | horizontalLineToRelative(4f) 34 | verticalLineToRelative(5f) 35 | curveToRelative(0f, 0.55f, 0.45f, 1f, 1f, 1f) 36 | horizontalLineToRelative(3f) 37 | curveToRelative(0.55f, 0f, 1f, -0.45f, 1f, -1f) 38 | verticalLineToRelative(-7f) 39 | horizontalLineToRelative(1.7f) 40 | curveToRelative(0.46f, 0f, 0.68f, -0.57f, 0.33f, -0.87f) 41 | lineTo(12.67f, 3.6f) 42 | curveToRelative(-0.38f, -0.34f, -0.96f, -0.34f, -1.34f, 0f) 43 | lineToRelative(-8.36f, 7.53f) 44 | curveToRelative(-0.34f, 0.3f, -0.13f, 0.87f, 0.33f, 0.87f) 45 | horizontalLineTo(5f) 46 | verticalLineToRelative(7f) 47 | curveToRelative(0f, 0.55f, 0.45f, 1f, 1f, 1f) 48 | horizontalLineToRelative(3f) 49 | curveToRelative(0.55f, 0f, 1f, -0.45f, 1f, -1f) 50 | close() 51 | } 52 | }.build() 53 | 54 | return _HomeFilled!! 55 | } 56 | 57 | @Suppress("ObjectPropertyName") 58 | private var _HomeFilled: ImageVector? = null 59 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/ui/util/CardUtils.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.ui.util 2 | 3 | import androidx.compose.foundation.shape.RoundedCornerShape 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.remember 6 | import androidx.compose.ui.unit.Dp 7 | import androidx.compose.ui.unit.dp 8 | import fr.twentynine.keepon.data.enums.ItemPosition 9 | 10 | val defaultRoundedCornerSize = 24.dp 11 | val defaultBorderWidth = 1.dp 12 | val defaultCardHorizontalPadding = 16.dp 13 | val itemPaddingTopFirst = 0.dp 14 | val itemPaddingBottomLast = 12.dp 15 | val itemPaddingDefault = 0.dp 16 | 17 | @Composable 18 | fun rememberTopPadding(itemPosition: ItemPosition): Dp { 19 | return remember(itemPosition) { 20 | when (itemPosition) { 21 | ItemPosition.FIRST, ItemPosition.FIRST_AND_LAST -> itemPaddingTopFirst 22 | else -> itemPaddingDefault 23 | } 24 | } 25 | } 26 | 27 | @Composable 28 | fun rememberBottomPadding(itemPosition: ItemPosition): Dp { 29 | return remember(itemPosition) { 30 | when (itemPosition) { 31 | ItemPosition.LAST, ItemPosition.FIRST_AND_LAST -> itemPaddingBottomLast 32 | else -> itemPaddingDefault 33 | } 34 | } 35 | } 36 | 37 | @Composable 38 | fun rememberItemBottomBorderPadding(itemPosition: ItemPosition): Dp { 39 | return remember(itemPosition) { 40 | when (itemPosition) { 41 | ItemPosition.LAST, ItemPosition.FIRST_AND_LAST -> itemPaddingDefault 42 | else -> defaultBorderWidth 43 | } 44 | } 45 | } 46 | 47 | @Composable 48 | fun rememberCardShape(itemPosition: ItemPosition): RoundedCornerShape { 49 | return remember(itemPosition) { 50 | when (itemPosition) { 51 | ItemPosition.FIRST -> RoundedCornerShape( 52 | topStart = defaultRoundedCornerSize, 53 | topEnd = defaultRoundedCornerSize, 54 | ) 55 | ItemPosition.LAST -> RoundedCornerShape( 56 | bottomStart = defaultRoundedCornerSize, 57 | bottomEnd = defaultRoundedCornerSize, 58 | ) 59 | ItemPosition.FIRST_AND_LAST -> RoundedCornerShape( 60 | size = defaultRoundedCornerSize 61 | ) 62 | else -> RoundedCornerShape(0.dp) 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | val Typography = Typography( 10 | titleLarge = TextStyle( 11 | fontFamily = FontFamily.Default, 12 | fontWeight = FontWeight.SemiBold, 13 | fontSize = 22.sp, 14 | lineHeight = 28.sp, 15 | letterSpacing = 0.sp 16 | ), 17 | titleMedium = TextStyle( 18 | fontFamily = FontFamily.Default, 19 | fontWeight = FontWeight.SemiBold, 20 | fontSize = 20.sp, 21 | lineHeight = 24.sp, 22 | letterSpacing = 0.sp 23 | ), 24 | titleSmall = TextStyle( 25 | fontFamily = FontFamily.Default, 26 | fontWeight = FontWeight.SemiBold, 27 | fontSize = 18.sp, 28 | lineHeight = 24.sp, 29 | letterSpacing = 0.sp 30 | ), 31 | bodyLarge = TextStyle( 32 | fontFamily = FontFamily.Default, 33 | fontWeight = FontWeight.Normal, 34 | fontSize = 16.sp, 35 | lineHeight = 24.sp, 36 | letterSpacing = 0.15.sp 37 | ), 38 | bodyMedium = TextStyle( 39 | fontFamily = FontFamily.Default, 40 | fontWeight = FontWeight.Medium, 41 | fontSize = 14.sp, 42 | lineHeight = 20.sp, 43 | letterSpacing = 0.25.sp 44 | ), 45 | bodySmall = TextStyle( 46 | fontFamily = FontFamily.Default, 47 | fontWeight = FontWeight.Bold, 48 | fontSize = 12.sp, 49 | lineHeight = 16.sp, 50 | letterSpacing = 0.4.sp 51 | ), 52 | labelMedium = TextStyle( 53 | fontFamily = FontFamily.Default, 54 | fontWeight = FontWeight.SemiBold, 55 | fontSize = 12.sp, 56 | lineHeight = 16.sp, 57 | letterSpacing = 0.5.sp 58 | ), 59 | labelLarge = TextStyle( 60 | fontFamily = FontFamily.Default, 61 | fontWeight = FontWeight.Bold, 62 | fontSize = 16.sp, 63 | lineHeight = 16.sp, 64 | letterSpacing = 0.5.sp 65 | ), 66 | headlineLarge = TextStyle( 67 | fontFamily = FontFamily.Default, 68 | fontWeight = FontWeight.ExtraBold, 69 | fontSize = 32.sp, 70 | lineHeight = 40.sp, 71 | letterSpacing = 0.sp 72 | ) 73 | ) 74 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/util/BatteryOptimizationManager.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.util 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.os.PowerManager 7 | import android.provider.Settings 8 | import androidx.core.net.toUri 9 | import dagger.hilt.android.qualifiers.ActivityContext 10 | import kotlinx.coroutines.flow.Flow 11 | import kotlinx.coroutines.flow.MutableStateFlow 12 | import kotlinx.coroutines.flow.distinctUntilChanged 13 | import kotlinx.coroutines.flow.update 14 | import javax.inject.Inject 15 | 16 | interface BatteryOptimizationManager { 17 | val batteryIsNotOptimized: Flow 18 | fun checkBatteryOptimizationState() 19 | fun requestDisableBatteryOptimization() 20 | fun isBatteryNotOptimized(): Boolean 21 | 22 | companion object { 23 | fun isBatteryNotOptimized(context: Context): Boolean { 24 | return BatteryOptimizationManagerImpl(context).isBatteryNotOptimized() 25 | } 26 | } 27 | } 28 | 29 | class BatteryOptimizationManagerImpl @Inject constructor( 30 | @param:ActivityContext private val context: Context 31 | ) : BatteryOptimizationManager { 32 | 33 | private val packageName by lazy { context.packageName } 34 | 35 | private val packageManager by lazy { context.getSystemService(PowerManager::class.java) } 36 | 37 | private val _batteryIsNotOptimized = MutableStateFlow(false) 38 | override val batteryIsNotOptimized = _batteryIsNotOptimized.distinctUntilChanged { old, new -> old == new } 39 | 40 | override fun checkBatteryOptimizationState() { 41 | _batteryIsNotOptimized.update { isBatteryNotOptimized() } 42 | } 43 | 44 | override fun isBatteryNotOptimized(): Boolean { 45 | return packageManager.isIgnoringBatteryOptimizations(packageName) 46 | } 47 | 48 | @SuppressLint("BatteryLife") 49 | override fun requestDisableBatteryOptimization() { 50 | val batteryOptimizationIntent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) 51 | .setData(("package:" + context.packageName).toUri()) 52 | .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) 53 | .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 54 | .addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) 55 | .addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) 56 | 57 | context.startActivity(batteryOptimizationIntent) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/data/local/IconFontFamily.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.data.local 2 | 3 | import androidx.compose.runtime.Immutable 4 | import fr.twentynine.keepon.R 5 | 6 | @Immutable 7 | sealed class IconFontFamily( 8 | val name: String, 9 | val displayName: String, 10 | val regularTypefaceId: Int, 11 | val boldTypefaceId: Int, 12 | val italicTypefaceId: Int, 13 | val boldItalicTypefaceId: Int 14 | ) { 15 | data object Roboto : IconFontFamily( 16 | name = "roboto", 17 | displayName = "Roboto", 18 | regularTypefaceId = R.font.roboto_regular, 19 | boldTypefaceId = R.font.roboto_bold, 20 | italicTypefaceId = R.font.roboto_italic, 21 | boldItalicTypefaceId = R.font.roboto_bolditalic, 22 | ) 23 | data object Caudex : IconFontFamily( 24 | name = "caudex", 25 | displayName = "Caudex", 26 | regularTypefaceId = R.font.caudex_regular, 27 | boldTypefaceId = R.font.caudex_bold, 28 | italicTypefaceId = R.font.caudex_italic, 29 | boldItalicTypefaceId = R.font.caudex_bolditalic, 30 | ) 31 | data object Bitter : IconFontFamily( 32 | name = "bitter", 33 | displayName = "Bitter", 34 | regularTypefaceId = R.font.bitter_regular, 35 | boldTypefaceId = R.font.bitter_bold, 36 | italicTypefaceId = R.font.bitter_italic, 37 | boldItalicTypefaceId = R.font.bitter_bolditalic, 38 | ) 39 | data object Poppins : IconFontFamily( 40 | name = "poppins", 41 | displayName = "Poppins", 42 | regularTypefaceId = R.font.poppins_regular, 43 | boldTypefaceId = R.font.poppins_bold, 44 | italicTypefaceId = R.font.poppins_italic, 45 | boldItalicTypefaceId = R.font.poppins_bolditalic, 46 | ) 47 | data object Lora : IconFontFamily( 48 | name = "lora", 49 | displayName = "Lora", 50 | regularTypefaceId = R.font.lora_regular, 51 | boldTypefaceId = R.font.lora_bold, 52 | italicTypefaceId = R.font.lora_italic, 53 | boldItalicTypefaceId = R.font.lora_bolditalic, 54 | ) 55 | data object OpenSans : IconFontFamily( 56 | name = "opensans", 57 | displayName = "Open Sans", 58 | regularTypefaceId = R.font.opensans_regular, 59 | boldTypefaceId = R.font.opensans_bold, 60 | italicTypefaceId = R.font.opensans_italic, 61 | boldItalicTypefaceId = R.font.opensans_bolditalic, 62 | ) 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/data/local/TipsInfo.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.data.local 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.rounded.AddCircleOutline 5 | import androidx.compose.material.icons.rounded.NotificationsActive 6 | import androidx.compose.material.icons.rounded.StarOutline 7 | import androidx.compose.runtime.Immutable 8 | import androidx.compose.ui.graphics.vector.ImageVector 9 | import fr.twentynine.keepon.R 10 | import fr.twentynine.keepon.data.model.MainUIEvent 11 | import fr.twentynine.keepon.data.model.TipsConstraintState 12 | 13 | @Immutable 14 | sealed class TipsInfo( 15 | val id: Int, 16 | val titleId: Int, 17 | val textId: Int, 18 | val buttonAction: MainUIEvent, 19 | val buttonTextId: Int, 20 | val buttonDismissTextId: Int, 21 | val iconImageVector: ImageVector, 22 | val constraint: (tipsConstraintState: TipsConstraintState) -> Boolean, 23 | ) { 24 | data object PostNotification : TipsInfo( 25 | id = 100, 26 | titleId = R.string.tip_general_notification_title, 27 | textId = R.string.tip_general_notification_text, 28 | buttonTextId = R.string.tip_general_notification_action_button_text, 29 | buttonDismissTextId = R.string.tip_general_notification_dismiss_button_text, 30 | iconImageVector = Icons.Rounded.NotificationsActive, 31 | buttonAction = MainUIEvent.RequestPostNotification, 32 | constraint = { uiState -> !uiState.canPostNotification } 33 | ) 34 | data object AddQSTile : TipsInfo( 35 | id = 200, 36 | titleId = R.string.tip_qstile_title, 37 | textId = R.string.tip_qstile_text, 38 | buttonTextId = R.string.tip_qstile_action_button_text, 39 | buttonDismissTextId = R.string.tip_qstile_dismiss_button_text, 40 | iconImageVector = Icons.Rounded.AddCircleOutline, 41 | buttonAction = MainUIEvent.RequestAddTileService, 42 | constraint = { uiState -> !uiState.tileServiceIsAdded } 43 | ) 44 | data object RateApp : TipsInfo( 45 | id = 300, 46 | titleId = R.string.tip_rateapp_title, 47 | textId = R.string.tip_rateapp_text, 48 | buttonTextId = R.string.tip_rateapp_action_button_text, 49 | buttonDismissTextId = R.string.tip_rateapp_dismiss_button_text, 50 | iconImageVector = Icons.Rounded.StarOutline, 51 | buttonAction = MainUIEvent.RequestAppRate, 52 | constraint = { uiState -> uiState.showRateApp } 53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/worker/SetNewScreenTimeoutWork.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.worker 2 | 3 | import android.content.Context 4 | import android.widget.Toast 5 | import androidx.hilt.work.HiltWorker 6 | import androidx.work.CoroutineWorker 7 | import androidx.work.WorkerParameters 8 | import dagger.assisted.Assisted 9 | import dagger.assisted.AssistedInject 10 | import fr.twentynine.keepon.R 11 | import fr.twentynine.keepon.data.model.ScreenTimeout 12 | import fr.twentynine.keepon.data.repo.UserPreferencesRepository 13 | import fr.twentynine.keepon.util.QSTileUpdater 14 | import fr.twentynine.keepon.util.RequiredPermissionsManager 15 | import fr.twentynine.keepon.worker.SetNewScreenTimeoutWorkScheduler.Companion.NEW_SCREEN_TIMEOUT_DATA_KEY 16 | import fr.twentynine.keepon.worker.SetNewScreenTimeoutWorkScheduler.Companion.UPDATE_PREVIOUS_TIMEOUT_DATA_KEY 17 | import kotlinx.coroutines.Dispatchers 18 | import kotlinx.coroutines.withContext 19 | 20 | @HiltWorker 21 | class SetNewScreenTimeoutWork @AssistedInject constructor( 22 | @Assisted private val appContext: Context, 23 | @Assisted workerParams: WorkerParameters, 24 | private val userPreferencesRepository: UserPreferencesRepository, 25 | private val qsTileUpdater: QSTileUpdater, 26 | ) : CoroutineWorker(appContext, workerParams) { 27 | 28 | override suspend fun doWork(): Result { 29 | return try { 30 | if (RequiredPermissionsManager.isPermissionsGranted(appContext)) { 31 | val newScreenTimeout = inputData.getInt(NEW_SCREEN_TIMEOUT_DATA_KEY, -1) 32 | val updatePreviousTimeout = inputData.getBoolean(UPDATE_PREVIOUS_TIMEOUT_DATA_KEY, false) 33 | 34 | if (newScreenTimeout != -1) { 35 | userPreferencesRepository.setNewSystemScreenTimeout( 36 | ScreenTimeout(newScreenTimeout), 37 | updatePreviousTimeout 38 | ) { 39 | qsTileUpdater.requestUpdate() 40 | } 41 | } 42 | } else { 43 | withContext(Dispatchers.Main) { 44 | Toast.makeText( 45 | appContext, 46 | appContext.getString(R.string.toast_missing_permission), 47 | Toast.LENGTH_SHORT 48 | ).show() 49 | } 50 | } 51 | 52 | Result.success() 53 | } catch (_: Exception) { 54 | Result.retry() 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/data/model/ScreenTimeout.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.data.model 2 | 3 | import androidx.compose.runtime.Stable 4 | import fr.twentynine.keepon.R 5 | import fr.twentynine.keepon.data.enums.SpecialScreenTimeoutType 6 | import fr.twentynine.keepon.util.StringResourceProvider 7 | import kotlinx.serialization.Serializable 8 | 9 | @Stable 10 | @Serializable 11 | data class ScreenTimeout( 12 | val value: Int 13 | ) { 14 | fun getFullDisplayTimeout(stringResourceProvider: StringResourceProvider): String { 15 | return when { 16 | value == Int.MAX_VALUE -> 17 | stringResourceProvider.getString(R.string.qs_long_infinite) 18 | value >= 3600000 && (value % 3600000) == 0 -> { 19 | val nbHour = value / 3600000 20 | stringResourceProvider.getPlural(R.plurals.qs_long_hour, nbHour) 21 | } 22 | value >= 60000 && (value % 60000) == 0 -> { 23 | val nbMinute = value / 60000 24 | stringResourceProvider.getPlural(R.plurals.qs_long_minute, nbMinute) 25 | } 26 | value == SpecialScreenTimeoutType.DEFAULT_SCREEN_TIMEOUT_TYPE.value -> 27 | stringResourceProvider.getString(R.string.timeout_restore_long) 28 | value == SpecialScreenTimeoutType.PREVIOUS_SCREEN_TIMEOUT_TYPE.value -> 29 | stringResourceProvider.getString(R.string.timeout_previous_long) 30 | else -> { 31 | val nbSecond = value / 1000 32 | stringResourceProvider.getPlural(R.plurals.qs_long_second, nbSecond) 33 | } 34 | } 35 | } 36 | 37 | fun getShortDisplayTimeout(stringResourceProvider: StringResourceProvider): String { 38 | return when { 39 | value == Int.MAX_VALUE -> 40 | stringResourceProvider.getString(R.string.qs_short_infinite) 41 | value >= 3600000 && (value % 3600000) == 0 -> 42 | buildString { 43 | append(value / 3600000) 44 | append(stringResourceProvider.getString(R.string.qs_short_hour)) 45 | } 46 | value >= 60000 && (value % 60000) == 0 -> 47 | buildString { 48 | append(value / 60000) 49 | append(stringResourceProvider.getString(R.string.qs_short_minute)) 50 | } 51 | else -> 52 | buildString { 53 | append(value / 1000) 54 | append(stringResourceProvider.getString(R.string.qs_short_second)) 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/ui/view/DismissActionRowView.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.ui.view 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.material3.Icon 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.graphics.Color 14 | import androidx.compose.ui.graphics.vector.ImageVector 15 | import androidx.compose.ui.unit.dp 16 | 17 | @Composable 18 | fun DismissActionRowView( 19 | modifier: Modifier = Modifier, 20 | icon: ImageVector?, 21 | text: String?, 22 | contentColor: Color, 23 | backgroundColor: Color, 24 | horizontalArrangement: Arrangement.Horizontal, 25 | contentVisible: Boolean 26 | ) { 27 | Row( 28 | modifier = modifier 29 | .fillMaxSize() 30 | .background(backgroundColor), 31 | verticalAlignment = Alignment.CenterVertically, 32 | horizontalArrangement = horizontalArrangement, 33 | ) { 34 | if (contentVisible) { 35 | val actualIcon = icon 36 | val actualText = text 37 | 38 | if (horizontalArrangement == Arrangement.End) { 39 | if (actualText != null) { 40 | Text( 41 | text = actualText, 42 | color = contentColor, 43 | ) 44 | } 45 | if (actualIcon != null) { 46 | Icon( 47 | modifier = Modifier.padding(horizontal = 18.dp), 48 | imageVector = actualIcon, 49 | tint = contentColor, 50 | contentDescription = null, 51 | ) 52 | } 53 | } else { 54 | if (actualIcon != null) { 55 | Icon( 56 | modifier = Modifier.padding(horizontal = 18.dp), 57 | imageVector = actualIcon, 58 | tint = contentColor, 59 | contentDescription = null, 60 | ) 61 | } 62 | if (actualText != null) { 63 | Text( 64 | text = actualText, 65 | color = contentColor, 66 | ) 67 | } 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/ui/view/ErrorView.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.ui.view 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.material.icons.Icons 8 | import androidx.compose.material.icons.rounded.Warning 9 | import androidx.compose.material3.Icon 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.remember 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.res.stringResource 17 | import androidx.compose.ui.text.style.TextAlign 18 | import androidx.compose.ui.unit.dp 19 | import fr.twentynine.keepon.R 20 | 21 | @Composable 22 | fun ErrorView( 23 | modifier: Modifier = Modifier, 24 | errorMessage: String, 25 | ) { 26 | val errorMessagePrefix = stringResource(R.string.error_view_message_prefix) 27 | val errorMessageWithPrefix = remember(errorMessage) { 28 | buildString { 29 | append(errorMessagePrefix) 30 | append(" ") 31 | append(errorMessage) 32 | } 33 | } 34 | 35 | Column( 36 | modifier = modifier.fillMaxSize(), 37 | verticalArrangement = Arrangement.Center, 38 | horizontalAlignment = Alignment.CenterHorizontally 39 | ) { 40 | Icon( 41 | modifier = Modifier.padding(8.dp), 42 | imageVector = Icons.Rounded.Warning, 43 | contentDescription = stringResource(R.string.error_view_title), 44 | tint = MaterialTheme.colorScheme.primary, 45 | ) 46 | Text( 47 | modifier = Modifier.padding(8.dp), 48 | text = stringResource(R.string.error_view_title), 49 | style = MaterialTheme.typography.titleLarge, 50 | textAlign = TextAlign.Center, 51 | color = MaterialTheme.colorScheme.primary 52 | ) 53 | Text( 54 | modifier = Modifier.padding(8.dp), 55 | text = stringResource(R.string.error_view_text), 56 | style = MaterialTheme.typography.bodyMedium, 57 | textAlign = TextAlign.Center, 58 | color = MaterialTheme.colorScheme.outline 59 | ) 60 | 61 | Text( 62 | modifier = Modifier.padding(8.dp), 63 | text = errorMessageWithPrefix, 64 | style = MaterialTheme.typography.bodyMedium, 65 | textAlign = TextAlign.Center, 66 | color = MaterialTheme.colorScheme.outline 67 | ) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/util/AppRateHelper.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.util 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.content.Intent 6 | import androidx.core.net.toUri 7 | import dagger.hilt.android.qualifiers.ApplicationContext 8 | import java.util.Calendar 9 | import java.util.TimeZone 10 | import javax.inject.Inject 11 | 12 | interface AppRateHelper { 13 | fun openPlayStore() 14 | fun canRateApp(): Boolean 15 | fun getFirstInstallTime(): Long 16 | fun needShowRateTip( 17 | currentCount: Long, 18 | firstInstallTime: Long, 19 | canRateApp: Boolean, 20 | ): Boolean 21 | } 22 | 23 | class AppRateHelperImpl @Inject constructor(@param:ApplicationContext private val context: Context) : AppRateHelper { 24 | 25 | private val packageName by lazy { context.packageName } 26 | private val packageManager by lazy { context.packageManager } 27 | 28 | private fun getPlayStoreIntent(): Intent { 29 | val uri = "market://details?id=$packageName".toUri() 30 | return Intent(Intent.ACTION_VIEW, uri) 31 | } 32 | 33 | override fun openPlayStore() { 34 | val intent = getPlayStoreIntent() 35 | intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK 36 | context.applicationContext.startActivity(intent) 37 | } 38 | 39 | override fun canRateApp(): Boolean { 40 | return canOpenIntent(getPlayStoreIntent()) 41 | } 42 | 43 | override fun getFirstInstallTime(): Long { 44 | return packageManager.getPackageInfo(packageName, 0).firstInstallTime 45 | } 46 | 47 | private fun getRemainingCount(currentCount: Long): Long { 48 | return if (currentCount < DEFAULT_COUNT) { 49 | DEFAULT_COUNT - currentCount 50 | } else { 51 | 0 52 | } 53 | } 54 | 55 | override fun needShowRateTip( 56 | currentCount: Long, 57 | firstInstallTime: Long, 58 | canRateApp: Boolean, 59 | ): Boolean { 60 | val shouldShowRequest = ( 61 | getRemainingCount(currentCount) == 0L && 62 | Calendar.getInstance( 63 | TimeZone.getTimeZone("utc") 64 | ).timeInMillis > (firstInstallTime + DEFAULT_INSTALL_TIME) 65 | ) 66 | 67 | return shouldShowRequest && canRateApp 68 | } 69 | 70 | @SuppressLint("QueryPermissionsNeeded") 71 | private fun canOpenIntent(intent: Intent): Boolean { 72 | return packageManager 73 | .queryIntentActivities(intent, 0).isNotEmpty() 74 | } 75 | 76 | companion object { 77 | private const val DEFAULT_COUNT = 10 78 | private const val DEFAULT_INSTALL_TIME = (3 * (1000 * 60 * 60 * 24)).toLong() // 3 days 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/tasker/FireReceiver.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.tasker 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.ComponentName 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.widget.Toast 8 | import dagger.hilt.android.AndroidEntryPoint 9 | import fr.twentynine.keepon.R 10 | import fr.twentynine.keepon.data.model.ScreenTimeout 11 | import fr.twentynine.keepon.data.repo.UserPreferencesRepository 12 | import fr.twentynine.keepon.util.BundleScrubber 13 | import fr.twentynine.keepon.worker.SetNewScreenTimeoutWorkScheduler 14 | import javax.inject.Inject 15 | 16 | @AndroidEntryPoint 17 | class FireReceiver : BroadcastReceiver() { 18 | 19 | @Inject 20 | lateinit var userPreferencesRepository: UserPreferencesRepository 21 | 22 | override fun onReceive(context: Context, intent: Intent) { 23 | // A hack to prevent a private serializable classloader attack 24 | if (BundleScrubber.scrub(intent)) { 25 | return 26 | } 27 | 28 | // Check that the Intent action will be ACTION_FIRE_SETTING 29 | if (TaskerIntent.ACTION_FIRE_SETTING != intent.action) { 30 | return 31 | } 32 | 33 | // Ignore implicit intents, because they are not valid. 34 | if (context.packageName != intent.getPackage() && 35 | ComponentName(context, this.javaClass.name) != intent.component 36 | ) { 37 | return 38 | } 39 | 40 | val bundle = intent.getBundleExtra(TaskerIntent.EXTRA_BUNDLE) 41 | 42 | if (BundleScrubber.scrub(intent) || 43 | null == bundle || 44 | !PluginBundleManager.isBundleValid(bundle) 45 | ) { 46 | return 47 | } 48 | 49 | // Get screen timeout value from Intent bundle 50 | val timeoutValue = bundle.getInt(PluginBundleManager.BUNDLE_EXTRA_TIMEOUT_VALUE, -1) 51 | 52 | if (timeoutValue != -1) { 53 | // Check if the received timeout value is valid 54 | val screenTimeoutValue = ScreenTimeout(timeoutValue) 55 | val isValidScreenTimeout = (userPreferencesRepository.screenTimeouts + userPreferencesRepository.specialScreenTimeouts) 56 | .contains(screenTimeoutValue) 57 | 58 | if (isValidScreenTimeout) { 59 | SetNewScreenTimeoutWorkScheduler().scheduleWork( 60 | timeoutValue, 61 | context.applicationContext, 62 | true 63 | ) 64 | return 65 | } 66 | } 67 | 68 | Toast.makeText( 69 | context.applicationContext, 70 | R.string.toast_invalid_screen_timeout, 71 | Toast.LENGTH_SHORT 72 | ).show() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/ui/util/GlowingText.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.ui.util 2 | 3 | import androidx.compose.animation.core.animateFloatAsState 4 | import androidx.compose.animation.core.tween 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.material3.LocalTextStyle 8 | import androidx.compose.material3.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.getValue 11 | import androidx.compose.runtime.remember 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.geometry.Offset 15 | import androidx.compose.ui.graphics.Color 16 | import androidx.compose.ui.graphics.Shadow 17 | import androidx.compose.ui.text.TextStyle 18 | import androidx.compose.ui.text.font.FontWeight 19 | import androidx.compose.ui.unit.Dp 20 | import androidx.compose.ui.unit.TextUnit 21 | 22 | private const val GLOW_ALPHA_ANIMATION_DURATION_MS = 600 23 | private const val MAX_GLOW_ALPHA_VALUE = 0.6f 24 | 25 | @Composable 26 | fun GlowingText( 27 | text: String, 28 | modifier: Modifier = Modifier, 29 | style: TextStyle = LocalTextStyle.current, 30 | fontSize: TextUnit = TextUnit.Unspecified, 31 | fontWeight: FontWeight? = null, 32 | showGlow: Boolean = true, 33 | glowColor: Color, 34 | glowRadius: Dp, 35 | glowSpread: Dp, 36 | ) { 37 | Box(contentAlignment = Alignment.Center, modifier = modifier) { 38 | val animatedGlowAlpha by animateFloatAsState( 39 | targetValue = if (showGlow) MAX_GLOW_ALPHA_VALUE else 0f, 40 | animationSpec = tween(GLOW_ALPHA_ANIMATION_DURATION_MS), 41 | label = "TextGlowAnimation" 42 | ) 43 | val glowShadow = remember(animatedGlowAlpha, glowColor, glowRadius) { 44 | Shadow( 45 | color = glowColor.copy(alpha = animatedGlowAlpha), 46 | offset = Offset.Zero, 47 | blurRadius = glowRadius.value 48 | ) 49 | } 50 | val glowStyle = remember(style, glowShadow) { 51 | style.copy( 52 | color = Color.Transparent, 53 | shadow = glowShadow, 54 | ) 55 | } 56 | 57 | if (animatedGlowAlpha > 0f) { 58 | Text( 59 | text = text, 60 | style = glowStyle, 61 | fontSize = fontSize, 62 | fontWeight = fontWeight, 63 | modifier = Modifier.padding(glowSpread), 64 | ) 65 | } 66 | 67 | Text( 68 | text = text, 69 | style = style, 70 | fontSize = fontSize, 71 | fontWeight = fontWeight, 72 | modifier = Modifier.padding(glowSpread), 73 | ) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KeepOn 2 | 3 | KeepOn - Keep your screen on smartly and easily with Quick Settings 6 | 7 | ## Features 8 | 9 | KeepOn is free and open-source (FOSS), without any ads/tracks and no Internet use. 10 | 11 | KeepOn allows you to keep your device's screen on for the desired duration and also allows you to return to the default settings automaticaly when the screen turns off. 12 | 13 | KeepOn adapts to your device's configuration to easy use and allow you to customize the Quick Settings view! 14 | 15 | A Tasker/Locale plugin is integrated and allows you to use KeepOn functions from another compatible application! 16 | 17 | ## Download 18 | 19 | [Get it on PlayStore](https://play.google.com/store/apps/details?id=fr.twentynine.keepon) 22 | [Get it on F-Droid](https://f-droid.org/packages/fr.twentynine.keepon/) 25 | [Get it on GitHub](https://github.com/twentynine78/KeepOn/releases/latest) 28 | 29 | ## Permissions 30 | 31 | - android.permission.WRITE_SETTINGS: Needed by Android to modify screen timeout settings 32 | - android.permission.FOREGROUND_SERVICE: Needed to detect screen off events and other screen timeout modifications 33 | - android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS: Needed to prevent Android from closing KeepOn while it is running 34 | - android.permission.POST_NOTIFICATIONS: Needed to allow KeepOn to display notification while it is running 35 | 36 | You can read / audit code and compile your own apk if you want! 37 | 38 | ## Credit 39 | 40 | ###  Libraries: 41 | - Coil: *https://github.com/coil-kt/coil* 42 | 43 | ###  Fonts: 44 | - Roboto: *https://fonts.google.com/specimen/Roboto?query=roboto* 45 | - Bitter: *https://fonts.google.com/specimen/Bitter?query=bitter* 46 | - Open Sans: *https://fonts.google.com/specimen/Open+Sans?query=open+sans* 47 | - Caudex: *https://fonts.google.com/specimen/Caudex?query=Caudex* 48 | - Poppins: *https://fonts.google.com/specimen/Poppins?query=Poppins* 49 | - Lora: *https://fonts.google.com/specimen/Lora?query=Lora* 50 | 51 | ## Screenshots 52 | 53 | Easy to use, thanks to Quick Settings 56 | Dynamic theme, adapted to your preferences 59 | Customizable icon, choose the icon you like 62 | Automation integrations, user with Tasker, MacroDroid or Automate apps -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_keepon_monochrome.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 33 | 36 | 37 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/util/DesiredScreenTimeoutController.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.util 2 | 3 | import fr.twentynine.keepon.data.model.ScreenTimeout 4 | import fr.twentynine.keepon.util.extensions.removeUntil 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.async 7 | import kotlinx.coroutines.awaitAll 8 | import kotlinx.coroutines.coroutineScope 9 | import kotlinx.coroutines.delay 10 | import kotlinx.coroutines.sync.Mutex 11 | import kotlinx.coroutines.sync.withLock 12 | import kotlinx.coroutines.withContext 13 | import kotlinx.coroutines.withTimeout 14 | import java.util.concurrent.LinkedBlockingQueue 15 | import java.util.concurrent.TimeUnit 16 | 17 | object DesiredScreenTimeoutController { 18 | private val WAIT_TIME_FOR_TIMEOUT_APPLIED = TimeUnit.SECONDS.toMillis(2) 19 | 20 | private val defaultDispatchers = Dispatchers.Default 21 | private val screenTimeoutProcessingLock = Mutex() 22 | 23 | @Volatile 24 | private var pendingTimeouts = LinkedBlockingQueue() 25 | 26 | @Volatile 27 | private var desiredScreenTimeouts = mutableListOf() 28 | 29 | fun getDesiredScreenTimeout(currentTimeout: ScreenTimeout): ScreenTimeout? { 30 | synchronized(desiredScreenTimeouts) { 31 | return if (desiredScreenTimeouts.contains(currentTimeout)) { 32 | desiredScreenTimeouts.removeUntil(currentTimeout) 33 | currentTimeout 34 | } else { 35 | desiredScreenTimeouts.clear() 36 | null 37 | } 38 | } 39 | } 40 | 41 | suspend fun setDesiredScreenTimeout( 42 | timeout: ScreenTimeout, 43 | systemScreenTimeoutController: SystemScreenTimeoutController, 44 | ) { 45 | withContext(defaultDispatchers) { 46 | pendingTimeouts.add(timeout) 47 | 48 | screenTimeoutProcessingLock.withLock { 49 | while (pendingTimeouts.isNotEmpty()) { 50 | val requestedTimeout = pendingTimeouts.poll() 51 | 52 | if (requestedTimeout != null) { 53 | coroutineScope { 54 | val deferreds = listOf( 55 | async { 56 | synchronized(desiredScreenTimeouts) { desiredScreenTimeouts.add(requestedTimeout) } 57 | }, 58 | async { 59 | systemScreenTimeoutController.setSystemScreenTimeout(requestedTimeout) 60 | } 61 | ) 62 | deferreds.awaitAll() 63 | } 64 | 65 | withTimeout(WAIT_TIME_FOR_TIMEOUT_APPLIED) { 66 | while (requestedTimeout != systemScreenTimeoutController.getSystemScreenTimeout()) { 67 | delay(1) 68 | } 69 | } 70 | } 71 | } 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/receiver/RebootAppReceiver.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.receiver 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.ComponentName 5 | import android.content.Context 6 | import android.content.Intent 7 | import dagger.hilt.android.AndroidEntryPoint 8 | import fr.twentynine.keepon.data.enums.SpecialScreenTimeoutType 9 | import fr.twentynine.keepon.data.repo.UserPreferencesRepository 10 | import fr.twentynine.keepon.services.ScreenOffReceiverServiceManager 11 | import fr.twentynine.keepon.util.QSTileUpdater 12 | import fr.twentynine.keepon.util.extensions.goAsync 13 | import fr.twentynine.keepon.worker.SetNewScreenTimeoutWorkScheduler 14 | import kotlinx.coroutines.Dispatchers 15 | import javax.inject.Inject 16 | 17 | @AndroidEntryPoint 18 | class RebootAppReceiver : BroadcastReceiver() { 19 | 20 | @Inject 21 | lateinit var userPreferencesRepository: UserPreferencesRepository 22 | 23 | @Inject 24 | lateinit var qsTileUpdater: QSTileUpdater 25 | 26 | @Inject 27 | lateinit var screenOffReceiverServiceManager: ScreenOffReceiverServiceManager 28 | 29 | override fun onReceive(context: Context, intent: Intent) { 30 | // Ignore implicit intents, because they are not valid. 31 | if (context.packageName != intent.getPackage() && ComponentName(context, this.javaClass.name) != intent.component) { 32 | return 33 | } 34 | 35 | val action = intent.action 36 | if (action == null) { 37 | return 38 | } 39 | 40 | when (action) { 41 | Intent.ACTION_BOOT_COMPLETED -> { 42 | goAsync(Dispatchers.Default) { 43 | // Reset the timeout to default at boot 44 | if (userPreferencesRepository.getResetTimeoutWhenScreenOff()) { 45 | val currentScreenTimeout = userPreferencesRepository.getCurrentScreenTimeout() 46 | val defaultScreenTimeout = userPreferencesRepository.getDefaultScreenTimeout() 47 | 48 | if (currentScreenTimeout != defaultScreenTimeout) { 49 | SetNewScreenTimeoutWorkScheduler().scheduleWork( 50 | SpecialScreenTimeoutType.DEFAULT_SCREEN_TIMEOUT_TYPE.value, 51 | context.applicationContext 52 | ) 53 | } 54 | } 55 | qsTileUpdater.requestUpdate() 56 | } 57 | } 58 | Intent.ACTION_MY_PACKAGE_REPLACED -> { 59 | goAsync(Dispatchers.Default) { 60 | val keepOnIsActive = userPreferencesRepository.getKeepOnIsActive() 61 | val resetWhenScreenOff = userPreferencesRepository.getResetTimeoutWhenScreenOff() 62 | 63 | if (keepOnIsActive && resetWhenScreenOff) { 64 | screenOffReceiverServiceManager.startService() 65 | } 66 | qsTileUpdater.requestUpdate() 67 | } 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/ui/theme/icons/HomeOutlined.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.ui.theme.icons 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.graphics.Color 6 | import androidx.compose.ui.graphics.SolidColor 7 | import androidx.compose.ui.graphics.vector.ImageVector 8 | import androidx.compose.ui.graphics.vector.path 9 | import androidx.compose.ui.tooling.preview.Preview 10 | import androidx.compose.ui.unit.dp 11 | 12 | @Preview 13 | @Composable 14 | private fun VectorPreview() { 15 | Image(HomeOutlined, null) 16 | } 17 | 18 | val HomeOutlined: ImageVector 19 | get() { 20 | if (_HomeOutlined != null) { 21 | return _HomeOutlined!! 22 | } 23 | _HomeOutlined = ImageVector.Builder( 24 | name = "HomeOutlined", 25 | defaultWidth = 24.dp, 26 | defaultHeight = 24.dp, 27 | viewportWidth = 24f, 28 | viewportHeight = 24f 29 | ).apply { 30 | path(fill = SolidColor(Color(0xFF000000))) { 31 | moveTo(21f, 11.1f) 32 | lineToRelative(-8.4f, -7.5f) 33 | curveToRelative(-0.4f, -0.3f, -1f, -0.3f, -1.3f, 0f) 34 | lineTo(3f, 11.1f) 35 | curveTo(2.6f, 11.4f, 2.8f, 12f, 3.3f, 12f) 36 | horizontalLineTo(5f) 37 | verticalLineToRelative(7f) 38 | curveToRelative(0f, 0.5f, 0.5f, 1f, 1f, 1f) 39 | horizontalLineToRelative(3f) 40 | curveToRelative(0.5f, 0f, 1f, -0.5f, 1f, -1f) 41 | verticalLineToRelative(-5f) 42 | horizontalLineToRelative(4f) 43 | verticalLineToRelative(5f) 44 | curveToRelative(0f, 0.5f, 0.5f, 1f, 1f, 1f) 45 | horizontalLineToRelative(3f) 46 | curveToRelative(0.5f, 0f, 1f, -0.5f, 1f, -1f) 47 | verticalLineToRelative(-7f) 48 | horizontalLineToRelative(1.7f) 49 | curveTo(21.2f, 12f, 21.4f, 11.4f, 21f, 11.1f) 50 | close() 51 | moveTo(17f, 18f) 52 | horizontalLineToRelative(-1f) 53 | verticalLineToRelative(-5.5f) 54 | curveToRelative(0f, -0.3f, -0.2f, -0.6f, -0.6f, -0.6f) 55 | horizontalLineTo(8.5f) 56 | curveTo(8.2f, 12f, 8f, 12.2f, 8f, 12.6f) 57 | verticalLineTo(18f) 58 | horizontalLineTo(7f) 59 | verticalLineToRelative(-7.5f) 60 | curveToRelative(0f, -0.2f, 0.1f, -0.3f, 0.2f, -0.4f) 61 | lineToRelative(4.5f, -4f) 62 | curveToRelative(0.2f, -0.2f, 0.5f, -0.2f, 0.7f, 0f) 63 | lineToRelative(4.5f, 4f) 64 | curveToRelative(0.1f, 0.1f, 0.2f, 0.3f, 0.2f, 0.4f) 65 | verticalLineTo(18f) 66 | horizontalLineTo(17f) 67 | close() 68 | } 69 | }.build() 70 | 71 | return _HomeOutlined!! 72 | } 73 | 74 | @Suppress("ObjectPropertyName") 75 | private var _HomeOutlined: ImageVector? = null 76 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_keepon.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/KeepOnApplication.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon 2 | 3 | import android.app.Application 4 | import android.graphics.Bitmap 5 | import androidx.hilt.work.HiltWorkerFactory 6 | import androidx.work.Configuration 7 | import androidx.work.WorkManager 8 | import coil3.ImageLoader 9 | import coil3.PlatformContext 10 | import coil3.SingletonImageLoader 11 | import coil3.bitmapFactoryExifOrientationStrategy 12 | import coil3.decode.ExifOrientationStrategy 13 | import coil3.request.CachePolicy 14 | import coil3.request.allowConversionToBitmap 15 | import coil3.request.allowHardware 16 | import coil3.request.bitmapConfig 17 | import coil3.size.Precision 18 | import dagger.hilt.android.HiltAndroidApp 19 | import fr.twentynine.keepon.util.AppVersionManager 20 | import fr.twentynine.keepon.util.coil.TimeoutIconDataFetcher 21 | import fr.twentynine.keepon.util.coil.TimeoutIconDataKeyer 22 | import fr.twentynine.keepon.worker.MonitorSystemScreenTimeoutWorkScheduler 23 | import kotlinx.coroutines.CoroutineScope 24 | import kotlinx.coroutines.Dispatchers 25 | import kotlinx.coroutines.SupervisorJob 26 | import kotlinx.coroutines.cancel 27 | import kotlinx.coroutines.launch 28 | import javax.inject.Inject 29 | import kotlin.coroutines.CoroutineContext 30 | 31 | @HiltAndroidApp 32 | class KeepOnApplication : Application(), SingletonImageLoader.Factory, Configuration.Provider, CoroutineScope { 33 | 34 | @Inject 35 | lateinit var hiltWorkerFactory: HiltWorkerFactory 36 | 37 | @Inject 38 | lateinit var appVersionManager: AppVersionManager 39 | 40 | private val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) 41 | 42 | override val coroutineContext: CoroutineContext by lazy { applicationScope.coroutineContext } 43 | 44 | override fun onCreate() { 45 | super.onCreate() 46 | 47 | applicationScope.launch { 48 | val workManager = WorkManager.getInstance(this@KeepOnApplication) 49 | MonitorSystemScreenTimeoutWorkScheduler.scheduleWork(workManager) 50 | 51 | appVersionManager.runAppMigrationIfNeeded() 52 | } 53 | } 54 | 55 | override fun onTerminate() { 56 | super.onTerminate() 57 | 58 | applicationScope.cancel("Application is terminating") 59 | } 60 | 61 | override fun newImageLoader(context: PlatformContext): ImageLoader = ImageLoader.Builder(this.applicationContext) 62 | .components { 63 | add(TimeoutIconDataKeyer()) 64 | add(TimeoutIconDataFetcher.Factory()) 65 | } 66 | .allowHardware(true) 67 | .allowConversionToBitmap(true) 68 | .bitmapConfig(Bitmap.Config.HARDWARE) 69 | .bitmapFactoryExifOrientationStrategy(ExifOrientationStrategy.IGNORE) 70 | .precision(Precision.INEXACT) 71 | .memoryCachePolicy(CachePolicy.ENABLED) 72 | .diskCachePolicy(CachePolicy.DISABLED) 73 | .build() 74 | 75 | override val workManagerConfiguration: Configuration 76 | get() = Configuration.Builder() 77 | .setWorkerFactory(hiltWorkerFactory) 78 | .setWorkerCoroutineContext(Dispatchers.IO) 79 | .setDefaultProcessName(this.applicationInfo.processName) 80 | .build() 81 | } 82 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val md_theme_light_primary = Color(0xFF0060A9) 6 | val md_theme_light_onPrimary = Color(0xFFFFFFFF) 7 | val md_theme_light_primaryContainer = Color(0xFFD3E4FF) 8 | val md_theme_light_onPrimaryContainer = Color(0xFF001C38) 9 | val md_theme_light_secondary = Color(0xFF545F70) 10 | val md_theme_light_onSecondary = Color(0xFFFFFFFF) 11 | val md_theme_light_secondaryContainer = Color(0xFFD8E3F8) 12 | val md_theme_light_onSecondaryContainer = Color(0xFF111C2B) 13 | val md_theme_light_tertiary = Color(0xFF755B00) 14 | val md_theme_light_onTertiary = Color(0xFFFFFFFF) 15 | val md_theme_light_tertiaryContainer = Color(0xFFFFE08E) 16 | val md_theme_light_onTertiaryContainer = Color(0xFF241A00) 17 | val md_theme_light_error = Color(0xFFBA1A1A) 18 | val md_theme_light_errorContainer = Color(0xFFFFDAD6) 19 | val md_theme_light_onError = Color(0xFFFFFFFF) 20 | val md_theme_light_onErrorContainer = Color(0xFF410002) 21 | val md_theme_light_background = Color(0xFFFDFCFF) 22 | val md_theme_light_onBackground = Color(0xFF1A1C1E) 23 | val md_theme_light_surface = Color(0xFFFDFCFF) 24 | val md_theme_light_onSurface = Color(0xFF1A1C1E) 25 | val md_theme_light_surfaceVariant = Color(0xFFDFE2EB) 26 | val md_theme_light_onSurfaceVariant = Color(0xFF43474E) 27 | val md_theme_light_outline = Color(0xFF73777F) 28 | val md_theme_light_inverseOnSurface = Color(0xFFECECEE) 29 | val md_theme_light_inverseSurface = Color(0xFF2F3033) 30 | val md_theme_light_inversePrimary = Color(0xFFA2C9FF) 31 | val md_theme_light_surfaceTint = Color(0xFF0060A9) 32 | val md_theme_light_outlineVariant = Color(0xFFC3C6CF) 33 | val md_theme_light_scrim = Color(0xFF000000) 34 | 35 | val md_theme_dark_primary = Color(0xFFA2C9FF) 36 | val md_theme_dark_onPrimary = Color(0xFF00315B) 37 | val md_theme_dark_primaryContainer = Color(0xFF004881) 38 | val md_theme_dark_onPrimaryContainer = Color(0xFFD3E4FF) 39 | val md_theme_dark_secondary = Color(0xFFBCC7DB) 40 | val md_theme_dark_onSecondary = Color(0xFF263141) 41 | val md_theme_dark_secondaryContainer = Color(0xFF3C4758) 42 | val md_theme_dark_onSecondaryContainer = Color(0xFFD8E3F8) 43 | val md_theme_dark_tertiary = Color(0xFFEDC13E) 44 | val md_theme_dark_onTertiary = Color(0xFF3D2E00) 45 | val md_theme_dark_tertiaryContainer = Color(0xFF584400) 46 | val md_theme_dark_onTertiaryContainer = Color(0xFFFFE08E) 47 | val md_theme_dark_error = Color(0xFFFFB4AB) 48 | val md_theme_dark_errorContainer = Color(0xFF93000A) 49 | val md_theme_dark_onError = Color(0xFF690005) 50 | val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6) 51 | val md_theme_dark_background = Color(0xFF1A1C1E) 52 | val md_theme_dark_onBackground = Color(0xFFE3E2E6) 53 | val md_theme_dark_surface = Color(0xFF1A1C1E) 54 | val md_theme_dark_onSurface = Color(0xFFE3E2E6) 55 | val md_theme_dark_surfaceVariant = Color(0xFF43474E) 56 | val md_theme_dark_onSurfaceVariant = Color(0xFFC3C6CF) 57 | val md_theme_dark_outline = Color(0xFF8D9199) 58 | val md_theme_dark_inverseOnSurface = Color(0xFF282B2E) 59 | val md_theme_dark_inverseSurface = Color(0xFFE3E2E6) 60 | val md_theme_dark_inversePrimary = Color(0xFF0060A9) 61 | val md_theme_dark_surfaceTint = Color(0xFFA2C9FF) 62 | val md_theme_dark_outlineVariant = Color(0xFF43474E) 63 | val md_theme_dark_scrim = Color(0xFF000000) 64 | -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.application) 3 | alias(libs.plugins.org.jetbrains.kotlin.android) 4 | alias(libs.plugins.com.google.devtools.ksp) 5 | alias(libs.plugins.com.google.dagger.hilt.android) 6 | alias(libs.plugins.compose.compiler) 7 | alias(libs.plugins.kotlin.parcelize) 8 | alias(libs.plugins.kotlin.serialization) 9 | } 10 | 11 | android { 12 | namespace = "fr.twentynine.keepon" 13 | compileSdk = 36 14 | 15 | defaultConfig { 16 | applicationId = "fr.twentynine.keepon" 17 | minSdk = 28 18 | targetSdk = 36 19 | versionCode = 26 20 | versionName = "2.0.6" 21 | 22 | vectorDrawables { 23 | useSupportLibrary = true 24 | } 25 | 26 | val coilVersionFromToml: String = libs.versions.coilVersion.get() 27 | resValue("string", "coil_version", coilVersionFromToml) 28 | } 29 | 30 | buildTypes { 31 | debug { 32 | isMinifyEnabled = false 33 | isShrinkResources = false 34 | proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") 35 | isDebuggable = true 36 | isJniDebuggable = true 37 | isPseudoLocalesEnabled = true 38 | } 39 | release { 40 | isMinifyEnabled = true 41 | isShrinkResources = true 42 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 43 | isDebuggable = false 44 | isJniDebuggable = false 45 | isPseudoLocalesEnabled = false 46 | } 47 | } 48 | 49 | compileOptions { 50 | sourceCompatibility = JavaVersion.VERSION_21 51 | targetCompatibility = JavaVersion.VERSION_21 52 | } 53 | 54 | buildFeatures { 55 | compose = true 56 | } 57 | } 58 | kotlin { 59 | jvmToolchain(JavaVersion.VERSION_21.majorVersion.toInt()) 60 | } 61 | 62 | dependencies { 63 | implementation(libs.androidx.core.ktx) 64 | implementation(libs.androidx.lifecycle.runtime.compose) 65 | implementation(libs.androidx.lifecycle.runtime) 66 | implementation(libs.androidx.lifecycle.service) 67 | implementation(libs.androidx.navigation.compose) 68 | implementation(libs.androidx.work.ktx) 69 | implementation(libs.androidx.datastore.preferences) 70 | implementation(libs.androidx.splashscreen) 71 | implementation(libs.androidx.activity.ktx) 72 | implementation(libs.androidx.activity.compose) 73 | implementation(platform(libs.compose.bom)) 74 | implementation(libs.compose.ui) 75 | implementation(libs.compose.ui.graphics) 76 | implementation(libs.compose.ui.tooling.preview) 77 | implementation(libs.compose.material.icons.extended) 78 | implementation(libs.compose.material3) 79 | implementation(libs.compose.material3.window.size) 80 | implementation(libs.compose.material3.adaptive.navigation) 81 | ksp(libs.com.google.dagger.hilt.compiler) 82 | implementation(libs.com.google.android.material) 83 | ksp(libs.androidx.hilt.compiler) 84 | implementation(libs.com.google.dagger.hilt.android) 85 | implementation(libs.androidx.hilt.work) 86 | implementation(libs.coil.compose) 87 | implementation(libs.coil.core) 88 | implementation(libs.org.jetbrains.kotlinx.collections.immutable) 89 | implementation(libs.org.jetbrains.kotlinx.serialization.json) 90 | } 91 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/services/ScreenOffReceiverServiceManager.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.services 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.widget.Toast 6 | import androidx.core.content.ContextCompat 7 | import dagger.hilt.android.qualifiers.ApplicationContext 8 | import fr.twentynine.keepon.R 9 | import fr.twentynine.keepon.services.ScreenOffReceiverServiceManager.Companion.ACTION_STOP_FOREGROUND_SCREEN_OFF_SERVICE 10 | import fr.twentynine.keepon.services.ScreenOffReceiverServiceManager.Companion.getIsRunning 11 | import fr.twentynine.keepon.util.RequiredPermissionsManager 12 | import kotlinx.coroutines.Dispatchers 13 | import kotlinx.coroutines.time.delay 14 | import kotlinx.coroutines.withContext 15 | import java.time.Duration 16 | import javax.inject.Inject 17 | 18 | interface ScreenOffReceiverServiceManager { 19 | suspend fun startService() 20 | suspend fun stopService() 21 | suspend fun restartService() 22 | 23 | companion object { 24 | const val ACTION_STOP_FOREGROUND_SCREEN_OFF_SERVICE = "ACTION_STOP_FOREGROUND_SCREEN_OFF_SERVICE" 25 | 26 | @Volatile 27 | private var isRunning: Boolean = false 28 | 29 | fun getIsRunning(): Boolean { 30 | synchronized(this) { 31 | return isRunning 32 | } 33 | } 34 | fun setIsRunning(running: Boolean) { 35 | synchronized(this) { 36 | isRunning = running 37 | } 38 | } 39 | } 40 | } 41 | 42 | class ScreenOffReceiverServiceManagerImpl @Inject constructor(@param:ApplicationContext private val context: Context) : ScreenOffReceiverServiceManager { 43 | override suspend fun startService() { 44 | if (!getIsRunning()) { 45 | if (RequiredPermissionsManager.isPermissionsGranted(context)) { 46 | try { 47 | ContextCompat.startForegroundService( 48 | context, 49 | Intent(context.applicationContext, ScreenOffReceiverService::class.java) 50 | ) 51 | } catch (_: Exception) { 52 | showMissingPermissionToast() 53 | } 54 | } else { 55 | showMissingPermissionToast() 56 | } 57 | } 58 | } 59 | 60 | override suspend fun stopService() { 61 | if (getIsRunning()) { 62 | try { 63 | context.startService( 64 | Intent( 65 | context.applicationContext, 66 | ScreenOffReceiverService::class.java 67 | ).also { 68 | it.action = 69 | ACTION_STOP_FOREGROUND_SCREEN_OFF_SERVICE 70 | } 71 | ) 72 | } catch (_: Exception) { 73 | showMissingPermissionToast() 74 | } 75 | } 76 | } 77 | 78 | override suspend fun restartService() { 79 | if (!getIsRunning()) { 80 | return 81 | } 82 | stopService() 83 | delay(Duration.ofMillis(500)) 84 | startService() 85 | } 86 | 87 | private suspend fun showMissingPermissionToast() { 88 | withContext(Dispatchers.Main) { 89 | Toast.makeText( 90 | context.applicationContext, 91 | context.getString(R.string.toast_missing_permission), 92 | Toast.LENGTH_SHORT 93 | ).show() 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/util/SystemSettingPermissionManager.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.util 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.provider.Settings 6 | import androidx.activity.ComponentActivity 7 | import androidx.core.net.toUri 8 | import androidx.lifecycle.lifecycleScope 9 | import dagger.hilt.android.qualifiers.ActivityContext 10 | import kotlinx.coroutines.Dispatchers 11 | import kotlinx.coroutines.Job 12 | import kotlinx.coroutines.delay 13 | import kotlinx.coroutines.flow.Flow 14 | import kotlinx.coroutines.flow.MutableStateFlow 15 | import kotlinx.coroutines.flow.distinctUntilChanged 16 | import kotlinx.coroutines.flow.update 17 | import kotlinx.coroutines.launch 18 | import kotlinx.coroutines.withTimeout 19 | import javax.inject.Inject 20 | 21 | interface SystemSettingPermissionManager { 22 | val canWriteSystemSetting: Flow 23 | fun checkWriteSystemSettingsPermission() 24 | fun requestWriteSystemSettingsPermission() 25 | fun canWriteSystemSettings(): Boolean 26 | 27 | companion object { 28 | fun canWriteSystemSettings(context: Context): Boolean { 29 | return SystemSettingPermissionManagerImpl(context).canWriteSystemSettings() 30 | } 31 | } 32 | } 33 | 34 | class SystemSettingPermissionManagerImpl @Inject constructor(@param:ActivityContext private val context: Context) : SystemSettingPermissionManager { 35 | 36 | private val _canWriteSystemSetting = MutableStateFlow(false) 37 | override val canWriteSystemSetting = _canWriteSystemSetting.distinctUntilChanged { old, new -> old == new } 38 | 39 | private var checkPermissionJob: Job? = null 40 | 41 | private val waitTimeInMillis = 200L 42 | private val maxCheckRepeat = 300 43 | 44 | override fun checkWriteSystemSettingsPermission() { 45 | _canWriteSystemSetting.update { canWriteSystemSettings() } 46 | } 47 | 48 | override fun requestWriteSystemSettingsPermission() { 49 | val permissionIntent = Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS) 50 | .setData(("package:" + context.packageName).toUri()) 51 | .addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) 52 | .addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) 53 | 54 | context.startActivity(permissionIntent) 55 | checkPermission() 56 | } 57 | 58 | override fun canWriteSystemSettings(): Boolean { 59 | return Settings.System.canWrite(context.applicationContext) 60 | } 61 | 62 | private fun checkPermission() { 63 | checkPermissionJob?.cancel() 64 | 65 | if (context is ComponentActivity) { 66 | val restartActivityIntent = context.intent 67 | .addFlags(Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP) 68 | .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) 69 | 70 | checkPermissionJob = context.lifecycleScope.launch(Dispatchers.Default) { 71 | withTimeout(waitTimeInMillis * maxCheckRepeat) { 72 | repeat(maxCheckRepeat) { 73 | if (Settings.System.canWrite(context)) { 74 | try { 75 | context.startActivity(restartActivityIntent) 76 | } finally { 77 | checkPermissionJob?.cancel() 78 | } 79 | } else { 80 | delay(waitTimeInMillis) 81 | } 82 | } 83 | } 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/ui/util/NestedScrollConnectionExtensions.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.ui.util 2 | 3 | import androidx.compose.ui.geometry.Offset 4 | import androidx.compose.ui.input.nestedscroll.NestedScrollConnection 5 | import androidx.compose.ui.input.nestedscroll.NestedScrollSource 6 | import androidx.compose.ui.unit.Velocity 7 | 8 | operator fun NestedScrollConnection.plus(other: NestedScrollConnection): NestedScrollConnection { 9 | // Optimization: if one of the connections is "empty" (our internal instance), 10 | // there's no need to create a delegation chain. 11 | // Note: This identity check will only work if you ensure 12 | // that any "empty" connection uses `EmptyNestedScrollConnection`. 13 | // Compose's default behaviors might return their own internal instance 14 | // for an "empty" connection, so this optimization is limited. 15 | // The main reason to keep it is to handle cases where you explicitly pass 16 | // a connection you know is a no-op. 17 | 18 | // More general and robust case: return a new instance that delegates. 19 | val self = this // Left-hand connection (this) 20 | val argument = other // Right-hand connection (other) 21 | 22 | // If both are identical, no need to combine them further (though this is rare for behavior instances) 23 | if (self === argument) return self 24 | 25 | return object : NestedScrollConnection { 26 | override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { 27 | val selfConsumed = self.onPreScroll(available, source) 28 | val remainingForArgument = available - selfConsumed 29 | val argumentConsumed = argument.onPreScroll(remainingForArgument, source) 30 | return selfConsumed + argumentConsumed 31 | } 32 | 33 | override fun onPostScroll( 34 | consumed: Offset, 35 | available: Offset, 36 | source: NestedScrollSource 37 | ): Offset { 38 | val selfConsumedPost = self.onPostScroll(consumed, available, source) 39 | val remainingForArgument = available - selfConsumedPost 40 | // The argument sees what 'self' consumed in post-scroll added to what the child consumed 41 | val argumentConsumedPost = argument.onPostScroll( 42 | consumed = consumed + selfConsumedPost, // The argument sees what the child AND 'self' consumed 43 | available = remainingForArgument, 44 | source = source 45 | ) 46 | return selfConsumedPost + argumentConsumedPost 47 | } 48 | 49 | override suspend fun onPreFling(available: Velocity): Velocity { 50 | val selfConsumed = self.onPreFling(available) 51 | val remainingForArgument = available - selfConsumed 52 | val argumentConsumed = argument.onPreFling(remainingForArgument) 53 | return selfConsumed + argumentConsumed 54 | } 55 | 56 | override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { 57 | val selfConsumedPost = self.onPostFling(consumed, available) 58 | val remainingForArgument = available - selfConsumedPost 59 | val argumentConsumedPost = argument.onPostFling( 60 | // The argument sees what the child AND 'self' consumed in post-fling 61 | consumed = consumed + selfConsumedPost, 62 | available = remainingForArgument 63 | ) 64 | return selfConsumedPost + argumentConsumedPost 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/worker/MonitorSystemScreenTimeoutWork.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.worker 2 | 3 | import android.content.Context 4 | import androidx.hilt.work.HiltWorker 5 | import androidx.work.CoroutineWorker 6 | import androidx.work.WorkManager 7 | import androidx.work.WorkerParameters 8 | import dagger.assisted.Assisted 9 | import dagger.assisted.AssistedInject 10 | import fr.twentynine.keepon.data.model.ScreenTimeout 11 | import fr.twentynine.keepon.data.repo.UserPreferencesRepository 12 | import fr.twentynine.keepon.services.ScreenOffReceiverServiceManager 13 | import fr.twentynine.keepon.util.DesiredScreenTimeoutController 14 | import fr.twentynine.keepon.util.QSTileUpdater 15 | import fr.twentynine.keepon.util.SystemScreenTimeoutController 16 | import kotlinx.coroutines.Dispatchers 17 | import kotlinx.coroutines.withContext 18 | 19 | @HiltWorker 20 | class MonitorSystemScreenTimeoutWork @AssistedInject constructor( 21 | @Assisted private val appContext: Context, 22 | @Assisted private val workerParams: WorkerParameters, 23 | private val userPreferencesRepository: UserPreferencesRepository, 24 | private val systemScreenTimeoutController: SystemScreenTimeoutController, 25 | private val qsTileUpdater: QSTileUpdater, 26 | private val screenOffReceiverServiceManager: ScreenOffReceiverServiceManager, 27 | ) : CoroutineWorker(appContext, workerParams) { 28 | 29 | private val workManager = WorkManager.getInstance(appContext) 30 | 31 | override suspend fun doWork(): Result { 32 | return withContext(Dispatchers.IO) { 33 | return@withContext try { 34 | // Get desired screen timeout 35 | val currentScreenTimeout = systemScreenTimeoutController.getSystemScreenTimeout() 36 | val desiredScreenTimeout = DesiredScreenTimeoutController.getDesiredScreenTimeout(currentScreenTimeout) 37 | 38 | // Update current screen timeout with new data 39 | updateCurrentSystemScreenTimeout(currentScreenTimeout, desiredScreenTimeout) 40 | 41 | // Update the QS tile 42 | qsTileUpdater.requestUpdate() 43 | 44 | // Re-schedule the worker 45 | MonitorSystemScreenTimeoutWorkScheduler.scheduleWork( 46 | workManager = workManager, 47 | requeueIfRunning = true 48 | ) 49 | 50 | Result.success() 51 | } catch (_: Exception) { 52 | Result.failure() 53 | } 54 | } 55 | } 56 | 57 | private suspend fun updateCurrentSystemScreenTimeout( 58 | currentScreenTimeout: ScreenTimeout, 59 | desiredScreenTimeout: ScreenTimeout? 60 | ) { 61 | // Check if the new timeout is initiated by the app with the desiredScreenTimeout value 62 | if (desiredScreenTimeout == currentScreenTimeout) { 63 | val startScreenOffReceiverService = currentScreenTimeout != userPreferencesRepository.getDefaultScreenTimeout() && 64 | userPreferencesRepository.getResetTimeoutWhenScreenOff() 65 | 66 | if (startScreenOffReceiverService) { 67 | screenOffReceiverServiceManager.startService() 68 | } else { 69 | screenOffReceiverServiceManager.stopService() 70 | } 71 | } else { 72 | userPreferencesRepository.setDefaultScreenTimeout(currentScreenTimeout) 73 | 74 | screenOffReceiverServiceManager.stopService() 75 | } 76 | 77 | userPreferencesRepository.setCurrentScreenTimeout(currentScreenTimeout) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/ui/view/ScreenTimeoutSetDefaultDismissActionRowView.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.ui.view 2 | 3 | import androidx.compose.animation.animateColorAsState 4 | import androidx.compose.animation.core.tween 5 | import androidx.compose.material.icons.Icons 6 | import androidx.compose.material.icons.rounded.Build 7 | import androidx.compose.material.icons.rounded.Done 8 | import androidx.compose.material3.MaterialTheme 9 | import androidx.compose.material3.SwipeToDismissBoxValue 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.getValue 12 | import androidx.compose.runtime.remember 13 | import androidx.compose.ui.graphics.Color 14 | import androidx.compose.ui.res.stringResource 15 | import fr.twentynine.keepon.R 16 | import fr.twentynine.keepon.data.model.ScreenTimeoutUI 17 | 18 | private const val CONTENT_COLOR_ANIMATION_DURATION_MS = 200 19 | private const val BACKGROUND_COLOR_ANIMATION_DURATION_MS = 400 20 | 21 | @Composable 22 | fun ScreenTimeoutSetDefaultDismissActionRowView( 23 | dismissDirection: SwipeToDismissBoxValue, 24 | dismissProgress: Float, 25 | screenTimeoutUI: ScreenTimeoutUI, 26 | swipeEnabledState: Boolean, 27 | swipeThresholdFraction: Float, 28 | ) { 29 | val backgroundEnabledColor = MaterialTheme.colorScheme.secondaryContainer 30 | val backgroundDisabledColor = MaterialTheme.colorScheme.outlineVariant 31 | val contentEnabledTint = MaterialTheme.colorScheme.onSecondaryContainer 32 | val contentDisabledTint = MaterialTheme.colorScheme.outline 33 | 34 | val enabledText = stringResource(R.string.select_timeouts_swipe_set_default_text) 35 | val disabledText = stringResource(R.string.select_timeouts_swipe_already_default_text) 36 | 37 | val textSetDefault = remember(screenTimeoutUI.isDefault, enabledText, disabledText) { 38 | if (!screenTimeoutUI.isDefault) enabledText else disabledText 39 | } 40 | 41 | val iconSetDefault = remember(screenTimeoutUI.isDefault) { 42 | if (!screenTimeoutUI.isDefault) Icons.Rounded.Build else Icons.Rounded.Done 43 | } 44 | val currentContentTintForAnimation = remember(screenTimeoutUI.isDefault) { 45 | if (!screenTimeoutUI.isDefault) contentEnabledTint else contentDisabledTint 46 | } 47 | val currentBackgroundColorForAnimation = remember(screenTimeoutUI.isDefault, swipeEnabledState) { 48 | when { 49 | !swipeEnabledState -> Color.Transparent 50 | screenTimeoutUI.isDefault -> backgroundDisabledColor 51 | else -> backgroundEnabledColor 52 | } 53 | } 54 | 55 | val animatedBackgroundColor by animateColorAsState( 56 | targetValue = currentBackgroundColorForAnimation, 57 | animationSpec = tween(durationMillis = BACKGROUND_COLOR_ANIMATION_DURATION_MS), 58 | label = "backgroundColorAnimation" 59 | ) 60 | 61 | val animatedContentColor by animateColorAsState( 62 | targetValue = currentContentTintForAnimation.copy( 63 | alpha = if (screenTimeoutUI.isDefault || (dismissProgress < swipeThresholdFraction && dismissProgress != 0f)) 0.5f else 1f 64 | ), 65 | animationSpec = tween(durationMillis = CONTENT_COLOR_ANIMATION_DURATION_MS), 66 | label = "contentColorAnimation" 67 | ) 68 | 69 | val contentIsVisible = swipeEnabledState 70 | 71 | val arrangement = when (dismissDirection) { 72 | SwipeToDismissBoxValue.EndToStart -> androidx.compose.foundation.layout.Arrangement.End 73 | SwipeToDismissBoxValue.StartToEnd -> androidx.compose.foundation.layout.Arrangement.Start 74 | else -> androidx.compose.foundation.layout.Arrangement.Start 75 | } 76 | 77 | DismissActionRowView( 78 | icon = iconSetDefault, 79 | text = textSetDefault, 80 | contentColor = animatedContentColor, 81 | backgroundColor = animatedBackgroundColor, 82 | horizontalArrangement = arrangement, 83 | contentVisible = contentIsVisible 84 | ) 85 | } 86 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/util/PostNotificationPermissionManager.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.util 2 | 3 | import android.Manifest 4 | import android.app.NotificationChannel 5 | import android.app.NotificationManager 6 | import android.content.Context 7 | import android.content.pm.PackageManager 8 | import android.os.Build 9 | import androidx.activity.compose.ManagedActivityResultLauncher 10 | import androidx.core.app.NotificationCompat 11 | import androidx.core.content.ContextCompat 12 | import dagger.hilt.android.qualifiers.ActivityContext 13 | import fr.twentynine.keepon.R 14 | import fr.twentynine.keepon.util.PostNotificationPermissionManager.Companion.NOTIFICATION_CHANNEL_SCREEN_MONITOR_ID 15 | import kotlinx.coroutines.flow.MutableStateFlow 16 | import kotlinx.coroutines.flow.StateFlow 17 | import kotlinx.coroutines.flow.asStateFlow 18 | import kotlinx.coroutines.flow.update 19 | import javax.inject.Inject 20 | 21 | interface PostNotificationPermissionManager { 22 | val canPostNotification: StateFlow 23 | 24 | fun updatePostNotificationPermission(canPostNotification: Boolean) 25 | fun checkPostNotificationPermission() 26 | fun requestPostNotificationPermission( 27 | requestPostNotificationPermissionLauncher: ManagedActivityResultLauncher 28 | ) 29 | 30 | companion object { 31 | const val NOTIFICATION_CHANNEL_OLD_KEEPON_SERVICE = "keepon_services" 32 | const val NOTIFICATION_CHANNEL_SCREEN_MONITOR_ID = "keepon_screen_monitor" 33 | const val NOTIFICATION_GROUP_KEY = "keepon_notification" 34 | 35 | fun removeOldNotificationChannelKeepOnService(context: Context) { 36 | val notificationManager = context.getSystemService(NotificationManager::class.java) 37 | notificationManager.deleteNotificationChannel(NOTIFICATION_CHANNEL_OLD_KEEPON_SERVICE) 38 | } 39 | } 40 | } 41 | 42 | class PostNotificationPermissionManagerImpl @Inject constructor( 43 | @param:ActivityContext private val context: Context 44 | ) : PostNotificationPermissionManager { 45 | 46 | private val _canPostNotification = MutableStateFlow(Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) 47 | override val canPostNotification = _canPostNotification.asStateFlow() 48 | 49 | private val notificationManager by lazy { context.getSystemService(NotificationManager::class.java) } 50 | 51 | private val notificationChannelKeepOnScreenMonitor = NotificationChannel( 52 | NOTIFICATION_CHANNEL_SCREEN_MONITOR_ID, 53 | context.getString( 54 | R.string.notification_channel_screen_monitor_name 55 | ), 56 | NotificationManager.IMPORTANCE_MIN 57 | ).also { 58 | it.lockscreenVisibility = NotificationCompat.VISIBILITY_SECRET 59 | it.enableLights(false) 60 | it.setShowBadge(false) 61 | } 62 | init { 63 | // Create notification channel if needed 64 | createNotificationChannelKeepOnScreenMonitor() 65 | } 66 | 67 | private fun createNotificationChannelKeepOnScreenMonitor() { 68 | notificationManager.createNotificationChannel(notificationChannelKeepOnScreenMonitor) 69 | } 70 | 71 | override fun updatePostNotificationPermission(canPostNotification: Boolean) { 72 | _canPostNotification.update { canPostNotification } 73 | } 74 | 75 | override fun checkPostNotificationPermission() { 76 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 77 | val permission = Manifest.permission.POST_NOTIFICATIONS 78 | 79 | _canPostNotification.update { 80 | ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED 81 | } 82 | } 83 | } 84 | 85 | override fun requestPostNotificationPermission( 86 | requestPostNotificationPermissionLauncher: ManagedActivityResultLauncher, 87 | ) { 88 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 89 | requestPostNotificationPermissionLauncher.launch( 90 | Manifest.permission.POST_NOTIFICATIONS 91 | ) 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/ui/view/CardHeaderView.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.ui.view 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.animation.expandVertically 5 | import androidx.compose.animation.shrinkVertically 6 | import androidx.compose.foundation.clickable 7 | import androidx.compose.foundation.interaction.MutableInteractionSource 8 | import androidx.compose.foundation.layout.Arrangement 9 | import androidx.compose.foundation.layout.Column 10 | import androidx.compose.foundation.layout.Row 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.layout.size 13 | import androidx.compose.material.icons.Icons 14 | import androidx.compose.material.icons.outlined.Info 15 | import androidx.compose.material3.Icon 16 | import androidx.compose.material3.MaterialTheme 17 | import androidx.compose.material3.Text 18 | import androidx.compose.runtime.Composable 19 | import androidx.compose.runtime.mutableStateOf 20 | import androidx.compose.runtime.remember 21 | import androidx.compose.runtime.saveable.rememberSaveable 22 | import androidx.compose.ui.Alignment 23 | import androidx.compose.ui.Modifier 24 | import androidx.compose.ui.graphics.painter.Painter 25 | import androidx.compose.ui.graphics.vector.ImageVector 26 | import androidx.compose.ui.res.stringResource 27 | import androidx.compose.ui.text.font.FontWeight 28 | import androidx.compose.ui.unit.dp 29 | import fr.twentynine.keepon.R 30 | 31 | @Composable 32 | fun CardHeaderView( 33 | modifier: Modifier = Modifier, 34 | icon: Painter? = null, 35 | iconVector: ImageVector? = null, 36 | iconSize: Int = 16, 37 | title: String, 38 | descText: String? = null 39 | ) { 40 | val infoVisible = rememberSaveable { mutableStateOf(false) } 41 | val moreInfoContentDesc = stringResource(R.string.more_info_icon_desc) 42 | 43 | Column(modifier = modifier) { 44 | Row( 45 | modifier = Modifier 46 | .padding(bottom = 20.dp, start = 20.dp, end = 20.dp) 47 | .clickable( 48 | onClick = { 49 | if (descText != null) { 50 | infoVisible.value = !infoVisible.value 51 | } 52 | }, 53 | indication = null, 54 | interactionSource = remember { MutableInteractionSource() } 55 | ), 56 | horizontalArrangement = Arrangement.Start, 57 | verticalAlignment = Alignment.CenterVertically 58 | ) { 59 | when { 60 | icon != null -> Icon( 61 | painter = icon, 62 | contentDescription = title, 63 | tint = MaterialTheme.colorScheme.onSurface, 64 | modifier = Modifier.size(iconSize.dp), 65 | ) 66 | iconVector != null -> Icon( 67 | imageVector = iconVector, 68 | contentDescription = title, 69 | tint = MaterialTheme.colorScheme.onSurface, 70 | modifier = Modifier.size(iconSize.dp), 71 | ) 72 | } 73 | Text( 74 | text = title, 75 | style = MaterialTheme.typography.titleMedium, 76 | fontWeight = FontWeight.Bold, 77 | modifier = Modifier 78 | .padding(start = 6.dp) 79 | .weight(1f) 80 | ) 81 | if (descText != null) { 82 | Icon( 83 | imageVector = Icons.Outlined.Info, 84 | contentDescription = moreInfoContentDesc, 85 | tint = MaterialTheme.colorScheme.onSurface, 86 | modifier = Modifier 87 | .padding(start = 12.dp) 88 | .size(14.dp), 89 | ) 90 | } 91 | } 92 | AnimatedVisibility( 93 | visible = infoVisible.value && descText != null, 94 | enter = expandVertically(expandFrom = Alignment.Top), 95 | exit = shrinkVertically(shrinkTowards = Alignment.Top), 96 | ) { 97 | Text( 98 | text = descText!!, 99 | style = MaterialTheme.typography.bodyLarge, 100 | modifier = Modifier 101 | .padding(bottom = 16.dp, start = 24.dp, end = 24.dp), 102 | color = MaterialTheme.colorScheme.outline 103 | ) 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | # Plugins versions 3 | androidGradlePlugin = "8.13.0" 4 | kotlin = "2.2.20" 5 | ksp = "2.2.20-2.0.3" 6 | hilt = "2.57.2" 7 | # Libraries versions 8 | coreKtxVersion = "1.17.0" 9 | lifecycleVersion = "2.9.4" 10 | navigationComposeVersion = "2.9.5" 11 | workVersion = "2.10.5" 12 | datastorePrefVersion = "1.1.7" 13 | splashscreenVersion = "1.0.1" 14 | activityVersion = "1.11.0" 15 | composeBomVersion = "2025.09.01" 16 | androidMaterialVersion = "1.13.0" 17 | androidXHiltVersion = "1.3.0" 18 | kotlinXCollectionsImmutableVersion = "0.4.0" 19 | kotlinXSerializationJsonVersion = "1.9.0" 20 | coilVersion = "3.3.0" 21 | 22 | [plugins] 23 | android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } 24 | org-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 25 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 26 | com-google-dagger-hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } 27 | com-google-devtools-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } 28 | kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } 29 | kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } 30 | 31 | [libraries] 32 | # Jetpack Core and SplashScreen Libraries 33 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtxVersion" } 34 | androidx-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "splashscreenVersion" } 35 | # Jetpack Activity Libraries 36 | androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activityVersion" } 37 | androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityVersion" } 38 | # Jetpack Lifecycle Libraries 39 | androidx-lifecycle-runtime = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleVersion" } 40 | androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycleVersion" } 41 | androidx-lifecycle-service = { group = "androidx.lifecycle", name = "lifecycle-service", version.ref = "lifecycleVersion" } 42 | # Jetpack Compose Libraries (BOM) 43 | compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBomVersion" } 44 | compose-ui = { module = "androidx.compose.ui:ui" } 45 | compose-ui-graphics = { module = "androidx.compose.ui:ui-graphics" } 46 | compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } 47 | compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } 48 | compose-material3 = { module = "androidx.compose.material3:material3" } 49 | compose-material3-window-size = { module = "androidx.compose.material3:material3-window-size-class" } 50 | compose-material3-adaptive-navigation = { module = "androidx.compose.material3:material3-adaptive-navigation-suite" } 51 | # Jetpack Navigation 52 | androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationComposeVersion" } 53 | # Jetpack Data & Storage 54 | androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastorePrefVersion" } 55 | androidx-work-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "workVersion" } 56 | # Android Material 57 | com-google-android-material = { group = "com.google.android.material", name = "material", version.ref = "androidMaterialVersion" } 58 | # Dependency Injection (Hilt) 59 | com-google-dagger-hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } 60 | com-google-dagger-hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" } 61 | androidx-hilt-work = { group = "androidx.hilt", name = "hilt-work", version.ref = "androidXHiltVersion" } 62 | androidx-hilt-compiler = { group = "androidx.hilt", name = "hilt-compiler", version.ref = "androidXHiltVersion" } 63 | # Image Loading (Coil) 64 | coil-compose = { group = "io.coil-kt.coil3", name = "coil-compose-android", version.ref = "coilVersion" } 65 | coil-core = { group = "io.coil-kt.coil3", name = "coil-core", version.ref = "coilVersion" } 66 | # KotlinX Libraries 67 | org-jetbrains-kotlinx-collections-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "kotlinXCollectionsImmutableVersion" } 68 | org-jetbrains-kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinXSerializationJsonVersion" } 69 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 33 | 36 | 41 | 44 | 47 | 50 | 53 | 56 | 59 | 60 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/ui/theme/icons/StyleFilled.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.ui.theme.icons 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.graphics.Color 6 | import androidx.compose.ui.graphics.PathFillType 7 | import androidx.compose.ui.graphics.SolidColor 8 | import androidx.compose.ui.graphics.StrokeCap 9 | import androidx.compose.ui.graphics.StrokeJoin 10 | import androidx.compose.ui.graphics.vector.ImageVector 11 | import androidx.compose.ui.graphics.vector.path 12 | import androidx.compose.ui.tooling.preview.Preview 13 | import androidx.compose.ui.unit.dp 14 | 15 | @Preview 16 | @Composable 17 | private fun VectorPreview() { 18 | Image(IconStyleFilled, null) 19 | } 20 | 21 | private var iconStyleFilled: ImageVector? = null 22 | 23 | val IconStyleFilled: ImageVector 24 | get() { 25 | if (iconStyleFilled != null) { 26 | return iconStyleFilled!! 27 | } 28 | iconStyleFilled = ImageVector.Builder( 29 | name = "StyleFilled", 30 | defaultWidth = 24.dp, 31 | defaultHeight = 24.dp, 32 | viewportWidth = 24f, 33 | viewportHeight = 24f 34 | ).apply { 35 | path( 36 | fill = null, 37 | fillAlpha = 1.0f, 38 | stroke = SolidColor(Color(0xFFFFFFFF)), 39 | strokeAlpha = 1.0f, 40 | strokeLineWidth = 2.0f, 41 | strokeLineCap = StrokeCap.Butt, 42 | strokeLineJoin = StrokeJoin.Miter, 43 | strokeLineMiter = 1.0f, 44 | pathFillType = PathFillType.NonZero 45 | ) { 46 | moveTo(12f, 3f) 47 | curveToRelative(-4.97f, 0f, -9f, 4.03f, -9f, 9f) 48 | reflectiveCurveToRelative(4.03f, 9f, 9f, 9f) 49 | curveToRelative(0.83f, 0f, 1.5f, -0.67f, 1.5f, -1.5f) 50 | curveToRelative(0f, -0.39f, -0.15f, -0.74f, -0.39f, -1.01f) 51 | curveToRelative(-0.23f, -0.26f, -0.38f, -0.61f, -0.38f, -0.99f) 52 | curveToRelative(0f, -0.83f, 0.67f, -1.5f, 1.5f, -1.5f) 53 | lineTo(16f, 16f) 54 | curveToRelative(2.76f, 0f, 5f, -2.24f, 5f, -5f) 55 | curveToRelative(0f, -4.42f, -4.03f, -8f, -9f, -8f) 56 | close() 57 | } 58 | path( 59 | fill = SolidColor(Color(0xFFFFFFFF)), 60 | fillAlpha = 1.0f, 61 | stroke = null, 62 | strokeAlpha = 1.0f, 63 | strokeLineWidth = 1.0f, 64 | strokeLineCap = StrokeCap.Butt, 65 | strokeLineJoin = StrokeJoin.Miter, 66 | strokeLineMiter = 1.0f, 67 | pathFillType = PathFillType.NonZero 68 | ) { 69 | moveTo(12f, 3f) 70 | curveToRelative(-4.97f, 0f, -9f, 4.03f, -9f, 9f) 71 | reflectiveCurveToRelative(4.03f, 9f, 9f, 9f) 72 | curveToRelative(0.83f, 0f, 1.5f, -0.67f, 1.5f, -1.5f) 73 | curveToRelative(0f, -0.39f, -0.15f, -0.74f, -0.39f, -1.01f) 74 | curveToRelative(-0.23f, -0.26f, -0.38f, -0.61f, -0.38f, -0.99f) 75 | curveToRelative(0f, -0.83f, 0.67f, -1.5f, 1.5f, -1.5f) 76 | lineTo(16f, 16f) 77 | curveToRelative(2.76f, 0f, 5f, -2.24f, 5f, -5f) 78 | curveToRelative(0f, -4.42f, -4.03f, -8f, -9f, -8f) 79 | close() 80 | moveTo(6.5f, 12f) 81 | curveToRelative(-0.83f, 0f, -1.5f, -0.67f, -1.5f, -1.5f) 82 | reflectiveCurveTo(5.67f, 9f, 6.5f, 9f) 83 | reflectiveCurveTo(8f, 9.67f, 8f, 10.5f) 84 | reflectiveCurveTo(7.33f, 12f, 6.5f, 12f) 85 | close() 86 | moveTo(9.5f, 8f) 87 | curveTo(8.67f, 8f, 8f, 7.33f, 8f, 6.5f) 88 | reflectiveCurveTo(8.67f, 5f, 9.5f, 5f) 89 | reflectiveCurveToRelative(1.5f, 0.67f, 1.5f, 1.5f) 90 | reflectiveCurveTo(10.33f, 8f, 9.5f, 8f) 91 | close() 92 | moveTo(14.5f, 8f) 93 | curveToRelative(-0.83f, 0f, -1.5f, -0.67f, -1.5f, -1.5f) 94 | reflectiveCurveTo(13.67f, 5f, 14.5f, 5f) 95 | reflectiveCurveToRelative(1.5f, 0.67f, 1.5f, 1.5f) 96 | reflectiveCurveTo(15.33f, 8f, 14.5f, 8f) 97 | close() 98 | moveTo(17.5f, 12f) 99 | curveToRelative(-0.83f, 0f, -1.5f, -0.67f, -1.5f, -1.5f) 100 | reflectiveCurveTo(16.67f, 9f, 17.5f, 9f) 101 | reflectiveCurveToRelative(1.5f, 0.67f, 1.5f, 1.5f) 102 | reflectiveCurveToRelative(-0.67f, 1.5f, -1.5f, 1.5f) 103 | close() 104 | } 105 | }.build() 106 | return iconStyleFilled!! 107 | } 108 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/util/DataMigrationHelper.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.util 2 | 3 | import fr.twentynine.keepon.data.local.TipsInfo 4 | import fr.twentynine.keepon.data.model.DismissedTips 5 | import fr.twentynine.keepon.data.model.ScreenTimeout 6 | import fr.twentynine.keepon.data.model.TimeoutIconStyle 7 | import fr.twentynine.keepon.data.repo.UserPreferencesRepository 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.withContext 10 | 11 | object DataMigrationHelper { 12 | private val ioDispatcher = Dispatchers.IO 13 | 14 | suspend fun getDefaultSelectedScreenTimeoutOrMigrateFromOld( 15 | userPreferencesRepository: UserPreferencesRepository 16 | ): List = 17 | withContext(ioDispatcher) { 18 | val oldSelectedScreenTimeout = userPreferencesRepository.getOldSelectedScreenTimeouts() 19 | return@withContext if (oldSelectedScreenTimeout.isNotEmpty()) { 20 | // Migrate old config 21 | val newList = intListFromStr(oldSelectedScreenTimeout).map { timeoutValue -> 22 | ScreenTimeout(timeoutValue) 23 | } 24 | userPreferencesRepository.setSelectedScreenTimeouts(newList) 25 | userPreferencesRepository.removeOldSelectedScreenTimeouts() 26 | 27 | newList 28 | } else { 29 | val currentTimeout = userPreferencesRepository.getCurrentScreenTimeout() 30 | val allTimeout = userPreferencesRepository.screenTimeouts 31 | val defaultSelectedTimeout = allTimeout.filter { it.value >= currentTimeout.value } 32 | defaultSelectedTimeout 33 | } 34 | } 35 | 36 | suspend fun getDefaultTimeoutIconStyleOrMigrateFromOld( 37 | userPreferencesRepository: UserPreferencesRepository 38 | ): TimeoutIconStyle = 39 | withContext(ioDispatcher) { 40 | val oldTimeoutIconStyle = userPreferencesRepository.getOldTimeoutIconStyle() 41 | return@withContext if (oldTimeoutIconStyle != null) { 42 | // Migrate old config 43 | val newTimeoutIconStyle = oldTimeoutIconStyle.toTimeoutIconStyle 44 | userPreferencesRepository.setTimeoutIconStyle(newTimeoutIconStyle) 45 | userPreferencesRepository.removeOldTimeoutIconStyle() 46 | 47 | newTimeoutIconStyle 48 | } else { 49 | TimeoutIconStyle() 50 | } 51 | } 52 | 53 | suspend fun getDefaultDismissedTipsListOrMigrateFromOld( 54 | userPreferencesRepository: UserPreferencesRepository 55 | ): List = 56 | withContext(ioDispatcher) { 57 | val oldAppReviewAsked = userPreferencesRepository.getOldAppReviewAsked() 58 | return@withContext if (oldAppReviewAsked) { 59 | // Migrate old config 60 | val rateAppTip = DismissedTips(TipsInfo.RateApp.id) 61 | userPreferencesRepository.setDismissedTip(rateAppTip) 62 | userPreferencesRepository.removeOldAppReviewAsked() 63 | 64 | listOf(rateAppTip) 65 | } else { 66 | emptyList() 67 | } 68 | } 69 | 70 | suspend fun getDefaultIsFirstLaunchOrMigrateFromOld( 71 | userPreferencesRepository: UserPreferencesRepository 72 | ): Boolean = 73 | withContext(ioDispatcher) { 74 | // Migrate old config 75 | val oldSkipIntro = userPreferencesRepository.getOldSkipIntro() 76 | 77 | if (oldSkipIntro) { 78 | userPreferencesRepository.setIsFirstLaunch(false) 79 | userPreferencesRepository.removeOldSkipIntro() 80 | } 81 | 82 | return@withContext !oldSkipIntro 83 | } 84 | 85 | suspend fun getResetTimeoutWhenScreenOffOrMigrateFromOld( 86 | userPreferencesRepository: UserPreferencesRepository 87 | ): Boolean = 88 | withContext(ioDispatcher) { 89 | // Migrate old config 90 | val oldResetTimeoutWhenScreenOff = userPreferencesRepository.getOldResetTimeoutWhenScreenOff() 91 | 92 | return@withContext if (oldResetTimeoutWhenScreenOff != null) { 93 | userPreferencesRepository.removeOldResetTimeoutWhenScreenOff() 94 | userPreferencesRepository.setResetTimeoutWhenScreenOff(!oldResetTimeoutWhenScreenOff) 95 | 96 | !oldResetTimeoutWhenScreenOff 97 | } else { 98 | true 99 | } 100 | } 101 | 102 | private fun intListFromStr(stringIntList: String?): List { 103 | // Retrieve old data format 104 | val resultList: ArrayList = ArrayList() 105 | val tempList = stringIntList?.split("|") 106 | if (tempList != null) { 107 | for (string: String in tempList) { 108 | if (string.isNotEmpty()) { 109 | resultList.add(string.toInt()) 110 | } 111 | } 112 | } 113 | return resultList 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /app/src/main/java/fr/twentynine/keepon/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package fr.twentynine.keepon.ui.theme 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import android.view.WindowManager 6 | import androidx.compose.foundation.isSystemInDarkTheme 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.material3.darkColorScheme 9 | import androidx.compose.material3.dynamicDarkColorScheme 10 | import androidx.compose.material3.dynamicLightColorScheme 11 | import androidx.compose.material3.lightColorScheme 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.LaunchedEffect 14 | import androidx.compose.ui.platform.LocalContext 15 | import androidx.compose.ui.platform.LocalView 16 | import androidx.core.view.WindowCompat 17 | 18 | private val LightColorScheme = lightColorScheme( 19 | primary = md_theme_light_primary, 20 | onPrimary = md_theme_light_onPrimary, 21 | primaryContainer = md_theme_light_primaryContainer, 22 | onPrimaryContainer = md_theme_light_onPrimaryContainer, 23 | secondary = md_theme_light_secondary, 24 | onSecondary = md_theme_light_onSecondary, 25 | secondaryContainer = md_theme_light_secondaryContainer, 26 | onSecondaryContainer = md_theme_light_onSecondaryContainer, 27 | tertiary = md_theme_light_tertiary, 28 | onTertiary = md_theme_light_onTertiary, 29 | tertiaryContainer = md_theme_light_tertiaryContainer, 30 | onTertiaryContainer = md_theme_light_onTertiaryContainer, 31 | error = md_theme_light_error, 32 | errorContainer = md_theme_light_errorContainer, 33 | onError = md_theme_light_onError, 34 | onErrorContainer = md_theme_light_onErrorContainer, 35 | background = md_theme_light_background, 36 | onBackground = md_theme_light_onBackground, 37 | surface = md_theme_light_surface, 38 | onSurface = md_theme_light_onSurface, 39 | surfaceVariant = md_theme_light_surfaceVariant, 40 | onSurfaceVariant = md_theme_light_onSurfaceVariant, 41 | outline = md_theme_light_outline, 42 | inverseOnSurface = md_theme_light_inverseOnSurface, 43 | inverseSurface = md_theme_light_inverseSurface, 44 | inversePrimary = md_theme_light_inversePrimary, 45 | surfaceTint = md_theme_light_surfaceTint, 46 | outlineVariant = md_theme_light_outlineVariant, 47 | scrim = md_theme_light_scrim, 48 | ) 49 | 50 | private val DarkColorScheme = darkColorScheme( 51 | primary = md_theme_dark_primary, 52 | onPrimary = md_theme_dark_onPrimary, 53 | primaryContainer = md_theme_dark_primaryContainer, 54 | onPrimaryContainer = md_theme_dark_onPrimaryContainer, 55 | secondary = md_theme_dark_secondary, 56 | onSecondary = md_theme_dark_onSecondary, 57 | secondaryContainer = md_theme_dark_secondaryContainer, 58 | onSecondaryContainer = md_theme_dark_onSecondaryContainer, 59 | tertiary = md_theme_dark_tertiary, 60 | onTertiary = md_theme_dark_onTertiary, 61 | tertiaryContainer = md_theme_dark_tertiaryContainer, 62 | onTertiaryContainer = md_theme_dark_onTertiaryContainer, 63 | error = md_theme_dark_error, 64 | errorContainer = md_theme_dark_errorContainer, 65 | onError = md_theme_dark_onError, 66 | onErrorContainer = md_theme_dark_onErrorContainer, 67 | background = md_theme_dark_background, 68 | onBackground = md_theme_dark_onBackground, 69 | surface = md_theme_dark_surface, 70 | onSurface = md_theme_dark_onSurface, 71 | surfaceVariant = md_theme_dark_surfaceVariant, 72 | onSurfaceVariant = md_theme_dark_onSurfaceVariant, 73 | outline = md_theme_dark_outline, 74 | inverseOnSurface = md_theme_dark_inverseOnSurface, 75 | inverseSurface = md_theme_dark_inverseSurface, 76 | inversePrimary = md_theme_dark_inversePrimary, 77 | surfaceTint = md_theme_dark_surfaceTint, 78 | outlineVariant = md_theme_dark_outlineVariant, 79 | scrim = md_theme_dark_scrim, 80 | ) 81 | 82 | @Composable 83 | fun KeepOnTheme( 84 | darkTheme: Boolean = isSystemInDarkTheme(), 85 | dynamicColor: Boolean = true, 86 | content: @Composable () -> Unit 87 | ) { 88 | val context = LocalContext.current 89 | val view = LocalView.current 90 | 91 | val isDynamicColor = dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S 92 | val colorScheme = when { 93 | isDynamicColor && darkTheme -> dynamicDarkColorScheme(context) 94 | isDynamicColor && !darkTheme -> dynamicLightColorScheme(context) 95 | darkTheme -> DarkColorScheme 96 | else -> LightColorScheme 97 | } 98 | 99 | // Manage system bar color 100 | LaunchedEffect(darkTheme) { 101 | val window = (view.context as Activity).window 102 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { 103 | window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) 104 | } else { 105 | window.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS) 106 | } 107 | 108 | val insetsController = WindowCompat.getInsetsController(window, view) 109 | insetsController.isAppearanceLightStatusBars = !darkTheme 110 | insetsController.isAppearanceLightNavigationBars = !darkTheme 111 | } 112 | 113 | MaterialTheme( 114 | colorScheme = colorScheme, 115 | typography = Typography, 116 | content = content 117 | ) 118 | } 119 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 21 | 22 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 39 | 47 | 48 | 49 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 88 | 89 | 90 | 91 | 92 | 93 | 96 | 97 | 98 | 99 | 100 | 101 | 106 | 110 | 111 | 112 | --------------------------------------------------------------------------------