├── 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 |
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 | [
](https://play.google.com/store/apps/details?id=fr.twentynine.keepon)
22 | [
](https://f-droid.org/packages/fr.twentynine.keepon/)
25 | [
](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 |
56 |
59 |
62 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------