├── .gitignore
├── LICENSE.md
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── sweak
│ │ └── unlockmaster
│ │ └── presentation
│ │ └── common
│ │ └── util
│ │ └── GetCompactDurationStringTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── sweak
│ │ │ └── unlockmaster
│ │ │ ├── data
│ │ │ ├── local
│ │ │ │ └── database
│ │ │ │ │ ├── UnlockMasterDatabase.kt
│ │ │ │ │ ├── dao
│ │ │ │ │ ├── CounterPausedEventsDao.kt
│ │ │ │ │ ├── CounterUnpausedEventsDao.kt
│ │ │ │ │ ├── LockEventsDao.kt
│ │ │ │ │ ├── ScreenOnEventsDao.kt
│ │ │ │ │ ├── ScreenTimeLimitsDao.kt
│ │ │ │ │ ├── UnlockEventsDao.kt
│ │ │ │ │ └── UnlockLimitsDao.kt
│ │ │ │ │ └── entities
│ │ │ │ │ ├── CounterPausedEventEntity.kt
│ │ │ │ │ ├── CounterUnpausedEventEntity.kt
│ │ │ │ │ ├── LockEventEntity.kt
│ │ │ │ │ ├── ScreenOnEventEntity.kt
│ │ │ │ │ ├── ScreenTimeLimitEntity.kt
│ │ │ │ │ ├── UnlockEventEntity.kt
│ │ │ │ │ └── UnlockLimitEntity.kt
│ │ │ ├── management
│ │ │ │ ├── UnlockMasterAlarmManagerImpl.kt
│ │ │ │ └── UnlockMasterBackupManagerImpl.kt
│ │ │ └── repository
│ │ │ │ ├── CounterPausedEventsRepositoryImpl.kt
│ │ │ │ ├── CounterUnpausedEventsRepositoryImpl.kt
│ │ │ │ ├── LockEventsRepositoryImpl.kt
│ │ │ │ ├── ScreenOnEventsRepositoryImpl.kt
│ │ │ │ ├── ScreenTimeLimitsRepositoryImpl.kt
│ │ │ │ ├── TimeRepositoryImpl.kt
│ │ │ │ ├── UnlockEventsRepositoryImpl.kt
│ │ │ │ ├── UnlockLimitsRepositoryImpl.kt
│ │ │ │ └── UserSessionRepositoryImpl.kt
│ │ │ ├── di
│ │ │ ├── ApplicationModule.kt
│ │ │ └── ViewModelModule.kt
│ │ │ ├── domain
│ │ │ ├── Constants.kt
│ │ │ ├── DateTimeUtils.kt
│ │ │ ├── management
│ │ │ │ ├── UnlockMasterAlarmManager.kt
│ │ │ │ └── UnlockMasterBackupManager.kt
│ │ │ ├── model
│ │ │ │ ├── DailyWrapUpData.kt
│ │ │ │ ├── DailyWrapUpNotificationsTime.kt
│ │ │ │ ├── ScreenTimeLimit.kt
│ │ │ │ ├── ScreenTimeLimitWarningState.kt
│ │ │ │ ├── SessionEvent.kt
│ │ │ │ ├── UiThemeMode.kt
│ │ │ │ ├── UnlockLimit.kt
│ │ │ │ └── UnlockMasterEvent.kt
│ │ │ ├── repository
│ │ │ │ ├── CounterPausedEventsRepository.kt
│ │ │ │ ├── CounterUnpausedEventsRepository.kt
│ │ │ │ ├── LockEventsRepository.kt
│ │ │ │ ├── ScreenOnEventsRepository.kt
│ │ │ │ ├── ScreenTimeLimitsRepository.kt
│ │ │ │ ├── TimeRepository.kt
│ │ │ │ ├── UnlockEventsRepository.kt
│ │ │ │ ├── UnlockLimitsRepository.kt
│ │ │ │ └── UserSessionRepository.kt
│ │ │ └── use_case
│ │ │ │ ├── counter_pause
│ │ │ │ ├── AddCounterPausedEventUseCase.kt
│ │ │ │ └── AddCounterUnpausedEventUseCase.kt
│ │ │ │ ├── daily_wrap_up
│ │ │ │ ├── GetDailyWrapUpDataUseCase.kt
│ │ │ │ ├── GetDailyWrapUpNotificationsTimeUseCase.kt
│ │ │ │ ├── IsGivenDayEligibleForDailyWrapUpUseCase.kt
│ │ │ │ ├── ScheduleDailyWrapUpNotificationsUseCase.kt
│ │ │ │ └── SetDailyWrapUpNotificationsTimeUseCase.kt
│ │ │ │ ├── lock_events
│ │ │ │ ├── AddLockEventUseCase.kt
│ │ │ │ └── ShouldAddLockEventUseCase.kt
│ │ │ │ ├── screen_on_events
│ │ │ │ ├── AddScreenOnEventUseCase.kt
│ │ │ │ └── GetScreenOnEventsCountForGivenDayUseCase.kt
│ │ │ │ ├── screen_time
│ │ │ │ ├── GetHourlyUsageMinutesForGivenDayUseCase.kt
│ │ │ │ ├── GetScreenTimeDurationForGivenDayUseCase.kt
│ │ │ │ └── GetSessionEventsForGivenDayUseCase.kt
│ │ │ │ ├── screen_time_limits
│ │ │ │ ├── AddOrUpdateScreenTimeLimitForTodayUseCase.kt
│ │ │ │ ├── AddOrUpdateScreenTimeLimitForTomorrowUseCase.kt
│ │ │ │ ├── DeleteScreenTimeLimitForTomorrowUseCase.kt
│ │ │ │ ├── GetScreenTimeLimitMinutesForTodayUseCase.kt
│ │ │ │ └── GetScreenTimeLimitMinutesForTomorrowUseCase.kt
│ │ │ │ ├── unlock_events
│ │ │ │ ├── AddUnlockEventUseCase.kt
│ │ │ │ ├── GetAllTimeDaysToUnlockEventCountsUseCase.kt
│ │ │ │ ├── GetLastWeekUnlockEventCountsUseCase.kt
│ │ │ │ └── GetUnlockEventsCountForGivenDayUseCase.kt
│ │ │ │ └── unlock_limits
│ │ │ │ ├── AddOrUpdateUnlockLimitForTodayUseCase.kt
│ │ │ │ ├── AddOrUpdateUnlockLimitForTomorrowUseCase.kt
│ │ │ │ ├── DeleteUnlockLimitForTomorrowUseCase.kt
│ │ │ │ ├── GetUnlockLimitAmountForGivenDayUseCase.kt
│ │ │ │ ├── GetUnlockLimitAmountForTodayUseCase.kt
│ │ │ │ ├── GetUnlockLimitAmountForTomorrowUseCase.kt
│ │ │ │ └── GetUnlockLimitApplianceDayForGivenDayUseCase.kt
│ │ │ └── presentation
│ │ │ ├── MainActivity.kt
│ │ │ ├── UnlockMasterApplication.kt
│ │ │ ├── background_work
│ │ │ ├── Constants.kt
│ │ │ ├── UnlockMasterService.kt
│ │ │ ├── global_receivers
│ │ │ │ ├── ApplicationUpdatedReceiver.kt
│ │ │ │ ├── BootReceiver.kt
│ │ │ │ ├── ShutdownReceiver.kt
│ │ │ │ ├── TimePreferencesChangeReceiver.kt
│ │ │ │ └── screen_event_receivers
│ │ │ │ │ ├── ScreenLockReceiver.kt
│ │ │ │ │ ├── ScreenOnReceiver.kt
│ │ │ │ │ └── ScreenUnlockReceiver.kt
│ │ │ └── local_receivers
│ │ │ │ ├── DailyWrapUpAlarmReceiver.kt
│ │ │ │ ├── ScreenTimeLimitStateReceiver.kt
│ │ │ │ └── UnlockCounterPauseReceiver.kt
│ │ │ ├── common
│ │ │ ├── Screen.kt
│ │ │ ├── components
│ │ │ │ ├── Dialog.kt
│ │ │ │ ├── InformationCard.kt
│ │ │ │ ├── NavigationBar.kt
│ │ │ │ ├── ObserveAsEvents.kt
│ │ │ │ ├── OnResume.kt
│ │ │ │ └── ProceedButton.kt
│ │ │ ├── theme
│ │ │ │ ├── Color.kt
│ │ │ │ ├── Shape.kt
│ │ │ │ ├── Space.kt
│ │ │ │ ├── Theme.kt
│ │ │ │ └── Type.kt
│ │ │ └── util
│ │ │ │ ├── DateTimeFormattingUtils.kt
│ │ │ │ ├── RoundedBarChartRenderer.kt
│ │ │ │ └── ThrottledNavigation.kt
│ │ │ ├── daily_wrap_up
│ │ │ ├── DailyWrapUpScreen.kt
│ │ │ ├── DailyWrapUpScreenEvent.kt
│ │ │ ├── DailyWrapUpScreenState.kt
│ │ │ ├── DailyWrapUpViewModel.kt
│ │ │ └── components
│ │ │ │ ├── DailyWrapUpCriterionPreviewCard.kt
│ │ │ │ ├── DailyWrapUpScreenOnEventsDetailsCard.kt
│ │ │ │ ├── DailyWrapUpScreenTimeDetailsCard.kt
│ │ │ │ ├── DailyWrapUpScreenTimeLimitDetailsCard.kt
│ │ │ │ ├── DailyWrapUpScreenUnlocksDetailsCard.kt
│ │ │ │ └── DailyWrapUpUnlockLimitDetailsCard.kt
│ │ │ ├── introduction
│ │ │ ├── background_work
│ │ │ │ ├── WorkInBackgroundScreen.kt
│ │ │ │ ├── WorkInBackgroundScreenEvent.kt
│ │ │ │ ├── WorkInBackgroundScreenState.kt
│ │ │ │ └── WorkInBackgroundViewModel.kt
│ │ │ ├── components
│ │ │ │ ├── ScreenTimeLimitPickerSlider.kt
│ │ │ │ └── UnlockLimitPickerSlider.kt
│ │ │ ├── introduction
│ │ │ │ └── IntroductionScreen.kt
│ │ │ ├── limit_setup
│ │ │ │ ├── screen_time
│ │ │ │ │ ├── ScreenTimeLimitSetupScreen.kt
│ │ │ │ │ ├── ScreenTimeLimitSetupScreenEvent.kt
│ │ │ │ │ ├── ScreenTimeLimitSetupScreenState.kt
│ │ │ │ │ └── ScreenTimeLimitSetupViewModel.kt
│ │ │ │ └── unlock
│ │ │ │ │ ├── UnlockLimitSetupScreen.kt
│ │ │ │ │ ├── UnlockLimitSetupScreenEvent.kt
│ │ │ │ │ ├── UnlockLimitSetupScreenState.kt
│ │ │ │ │ └── UnlockLimitSetupViewModel.kt
│ │ │ ├── setup_complete
│ │ │ │ └── SetupCompleteScreen.kt
│ │ │ └── welcome
│ │ │ │ └── WelcomeScreen.kt
│ │ │ ├── main
│ │ │ ├── home
│ │ │ │ ├── HomeScreen.kt
│ │ │ │ ├── HomeScreenEvent.kt
│ │ │ │ ├── HomeScreenState.kt
│ │ │ │ ├── HomeViewModel.kt
│ │ │ │ └── components
│ │ │ │ │ ├── SemiTransparentBlueRectangleMarkerView.kt
│ │ │ │ │ └── WeeklyUnlocksChart.kt
│ │ │ ├── screen_time
│ │ │ │ ├── ScreenTimeScreen.kt
│ │ │ │ ├── ScreenTimeScreenState.kt
│ │ │ │ ├── ScreenTimeViewModel.kt
│ │ │ │ └── components
│ │ │ │ │ ├── CounterPauseSeparator.kt
│ │ │ │ │ ├── DailyScreenTimeChart.kt
│ │ │ │ │ └── SingleScreenTimeSessionCard.kt
│ │ │ └── statistics
│ │ │ │ ├── StatisticsScreen.kt
│ │ │ │ ├── StatisticsScreenEvent.kt
│ │ │ │ ├── StatisticsScreenState.kt
│ │ │ │ ├── StatisticsViewModel.kt
│ │ │ │ └── components
│ │ │ │ └── AllTimeUnlocksChart.kt
│ │ │ ├── settings
│ │ │ ├── SettingsScreen.kt
│ │ │ ├── application_blocked
│ │ │ │ ├── ApplicationBlockedScreen.kt
│ │ │ │ ├── ApplicationBlockedScreenEvent.kt
│ │ │ │ ├── ApplicationBlockedScreenState.kt
│ │ │ │ └── ApplicationBlockedViewModel.kt
│ │ │ ├── components
│ │ │ │ └── SettingsEntry.kt
│ │ │ ├── daily_wrap_up_settings
│ │ │ │ ├── DailyWrapUpSettingsScreen.kt
│ │ │ │ ├── DailyWrapUpSettingsScreenEvent.kt
│ │ │ │ ├── DailyWrapUpSettingsScreenState.kt
│ │ │ │ ├── DailyWrapUpSettingsViewModel.kt
│ │ │ │ └── components
│ │ │ │ │ └── CardTimePicker.kt
│ │ │ ├── data_backup
│ │ │ │ ├── DataBackupScreen.kt
│ │ │ │ ├── DataBackupScreenEvent.kt
│ │ │ │ ├── DataBackupScreenState.kt
│ │ │ │ └── DataBackupViewModel.kt
│ │ │ ├── mobilizing_notifications
│ │ │ │ ├── MobilizingNotificationsScreen.kt
│ │ │ │ ├── MobilizingNotificationsScreenEvent.kt
│ │ │ │ ├── MobilizingNotificationsScreenState.kt
│ │ │ │ ├── MobilizingNotificationsViewModel.kt
│ │ │ │ └── components
│ │ │ │ │ └── ComboBox.kt
│ │ │ └── user_interface_theme
│ │ │ │ ├── UserInterfaceThemeScreen.kt
│ │ │ │ ├── UserInterfaceThemeScreenEvent.kt
│ │ │ │ ├── UserInterfaceThemeScreenState.kt
│ │ │ │ └── UserInterfaceThemeViewModel.kt
│ │ │ └── widget
│ │ │ ├── UnlockCountWidget.kt
│ │ │ └── UnlockCountWidgetReceiver.kt
│ └── res
│ │ ├── drawable-nodpi
│ │ ├── img_daily_wrapup_notification.png
│ │ ├── img_mobilizing_notification.png
│ │ ├── img_screen_time_mobilizing_notification.png
│ │ └── img_service_notification.png
│ │ ├── drawable-v24
│ │ ├── ic_launcher_foreground.xml
│ │ └── ic_notification_icon.xml
│ │ ├── drawable
│ │ ├── circular_progress.xml
│ │ └── ic_launcher_background.xml
│ │ ├── font
│ │ ├── amiko_bold.ttf
│ │ ├── amiko_regular.ttf
│ │ └── amiko_semibold.ttf
│ │ ├── layout-night
│ │ └── semi_transparent_blue_rect_marker_view.xml
│ │ ├── layout
│ │ ├── progress_bar.xml
│ │ ├── semi_transparent_blue_rect_marker_view.xml
│ │ └── spinner_time_picker.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── 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-night
│ │ └── themes.xml
│ │ ├── values-pl
│ │ └── strings.xml
│ │ ├── values-tr
│ │ └── strings.xml
│ │ ├── values
│ │ ├── strings.xml
│ │ └── themes.xml
│ │ ├── xml-v31
│ │ └── unlock_count_widget_info.xml
│ │ └── xml
│ │ ├── data_extraction_rules.xml
│ │ └── unlock_count_widget_info.xml
│ └── test
│ └── java
│ └── com
│ └── sweak
│ └── unlockmaster
│ ├── data
│ └── repository
│ │ ├── CounterPausedEventsRepositoryFake.kt
│ │ ├── CounterUnpausedEventsRepositoryFake.kt
│ │ ├── LockEventsRepositoryFake.kt
│ │ ├── TimeRepositoryFake.kt
│ │ └── UnlockEventsRepositoryFake.kt
│ └── domain
│ └── use_case
│ ├── screen_time
│ ├── GetHourlyUsageMinutesForGivenDayUseCaseTest.kt
│ ├── GetScreenTimeDurationForGivenDayUseCaseTest.kt
│ └── GetSessionEventsForGivenDayUseCaseTest.kt
│ └── unlock_events
│ ├── GetAllTimeDaysToUnlockEventCountsUseCaseTest.kt
│ └── GetLastWeekUnlockEventCountsUseCaseTest.kt
├── build.gradle
├── fastlane
└── metadata
│ └── android
│ └── en-US
│ ├── changelogs
│ └── 14.txt
│ ├── full_description.txt
│ ├── images
│ ├── icon.png
│ └── phoneScreenshots
│ │ ├── DailyWrapUpScreen.jpg
│ │ ├── HomeScreen.jpg
│ │ ├── ScreenTimeLimitSetupScreen.jpg
│ │ ├── ScreenTimeScreen.jpg
│ │ ├── StatisticsScreen.jpg
│ │ └── UnlockLimitSetupScreen.jpg
│ └── short_description.txt
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea
5 | .DS_Store
6 | /build
7 | /captures
8 | .externalNativeBuild
9 | .cxx
10 | local.properties
11 | unlock-master-keystore.jks
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | UnlockMaster
6 |
7 |
8 |
9 |
10 |
11 |
12 | **UnlockMaster: Unlock the Power of Mindful Smartphone Use**
13 |
14 | Are you tired of mindlessly unlocking your phone, only to find yourself lost in endless scrolling or compulsively checking your apps? Say hello to UnlockMaster, your ultimate companion for conscious smartphone usage.
15 |
16 | **Unlock Your Potential**
17 |
18 | UnlockMaster believes that every unlock can be a step towards a more mindful digital life. Our app empowers you to regain control over your screen time by tracking your unlocks and setting your own unlock limit.
19 |
20 | **Stay Informed**
21 |
22 | With real-time notifications, UnlockMaster keeps you in the loop. You'll receive updates on your unlock count in relation to your limit, serving as a friendly reminder to stay on track.
23 |
24 | **Set Your Goals**
25 |
26 | UnlockMaster is all about helping you achieve your smartphone usage goals. Receive motivational notifications as you're nearing your daily unlock limit, so you can make more conscious choices further in the day.
27 |
28 | **Reflect and Refine**
29 |
30 | At the end of each day, our app shows you a daily wrap-up notification. Tap it to discover insightful summaries, helpful suggestions for adjusting your unlock limit, and more.
31 |
32 | **Visualize Your Progress**
33 |
34 | UnlockMaster doesn't just track unlocks; it also provides you with eye-catching charts. Monitor your unlocks and screen time with beautiful charts, giving you a clear picture of your progress.
35 |
36 | **Unlock the potential of mindful smartphone use with UnlockMaster.**
37 | **Take control of your digital life, *one unlock at a time*.**
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | ## Setup
51 | * Download [`unlock-master-android-signed.apk`](https://github.com/sweakpl/unlock-master/releases),
52 | * Put it e.g. in a `Downloads` folder in Your Android device,
53 | * Go to the `Downloads` folder on the Android device,
54 | * Tap the file and install - the app doesn't require any special permissions.
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | /release
3 | /src/main/res/values/secrets.xml
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'org.jetbrains.kotlin.android'
4 | id 'dagger.hilt.android.plugin'
5 | id 'com.google.devtools.ksp'
6 | }
7 |
8 | android {
9 | namespace 'com.sweak.unlockmaster'
10 | compileSdk 35
11 |
12 | defaultConfig {
13 | applicationId "com.sweak.unlockmaster"
14 | minSdk 21
15 | targetSdk 35
16 | versionCode 14
17 | versionName "1.4.5"
18 |
19 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
20 | vectorDrawables {
21 | useSupportLibrary true
22 | }
23 | }
24 |
25 | buildTypes {
26 | release {
27 | minifyEnabled true
28 | shrinkResources true
29 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
30 | }
31 | }
32 | compileOptions {
33 | coreLibraryDesugaringEnabled true
34 | sourceCompatibility JavaVersion.VERSION_17
35 | targetCompatibility JavaVersion.VERSION_17
36 | }
37 | kotlinOptions {
38 | jvmTarget = '17'
39 | }
40 | buildFeatures {
41 | compose true
42 | }
43 | composeOptions {
44 | kotlinCompilerExtensionVersion '1.5.3'
45 | }
46 | packagingOptions {
47 | resources {
48 | excludes += '/META-INF/{AL2.0,LGPL2.1}'
49 | }
50 | }
51 | dependenciesInfo {
52 | // Disables dependency metadata when building APKs:
53 | includeInApk = false
54 | // Enabled dependency metadata when building App Bundles:
55 | includeInBundle = true
56 | }
57 | }
58 |
59 | dependencies {
60 |
61 | // Core
62 | implementation 'androidx.core:core-ktx:1.15.0'
63 |
64 | // Jetpack Compose
65 | implementation 'androidx.activity:activity-compose:1.9.3'
66 | implementation 'androidx.compose.ui:ui:1.7.6'
67 | implementation 'androidx.compose.ui:ui-tooling-preview:1.7.6'
68 | implementation 'androidx.navigation:navigation-compose:2.8.5'
69 | implementation 'androidx.compose.material:material-icons-extended:1.7.6'
70 | implementation 'androidx.compose.material3:material3:1.3.1'
71 | debugImplementation 'androidx.compose.ui:ui-tooling:1.7.6'
72 |
73 | // Glance
74 | implementation('androidx.glance:glance-appwidget:1.1.1')
75 | implementation('androidx.glance:glance-material3:1.1.1')
76 |
77 | // Coroutines
78 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.7'
79 |
80 | // Dagger Hilt
81 | implementation 'com.google.dagger:hilt-android:2.51'
82 | ksp 'com.google.dagger:hilt-compiler:2.51'
83 | implementation 'androidx.hilt:hilt-navigation-compose:1.2.0'
84 |
85 | // Room
86 | implementation 'androidx.room:room-ktx:2.6.1'
87 | annotationProcessor 'androidx.room:room-compiler:2.6.1'
88 | ksp 'androidx.room:room-compiler:2.6.1'
89 |
90 | // Preferences DataStore
91 | implementation 'androidx.datastore:datastore-preferences:1.1.1'
92 |
93 | // Permission handling
94 | implementation 'com.google.accompanist:accompanist-permissions:0.36.0'
95 |
96 | // API < 26 support for DateTime API
97 | // noinspection GradleDependency
98 | coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
99 |
100 | // View components
101 | implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'
102 | implementation 'com.google.android.material:material:1.12.0'
103 |
104 | // JSON handling for backup files
105 | implementation 'com.google.code.gson:gson:2.11.0'
106 |
107 | // Test
108 | testImplementation 'junit:junit:4.13.2'
109 | testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0'
110 | androidTestImplementation 'androidx.test.ext:junit:1.2.1'
111 | androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.7.6'
112 | debugImplementation 'androidx.compose.ui:ui-test-manifest:1.7.6'
113 | }
--------------------------------------------------------------------------------
/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 | # Keeping chart classes so that chart animations work fine
24 | -keep class com.github.mikephil.charting.** { *; }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/data/local/database/UnlockMasterDatabase.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.data.local.database
2 |
3 | import androidx.room.Database
4 | import androidx.room.RoomDatabase
5 | import androidx.room.migration.Migration
6 | import androidx.sqlite.db.SupportSQLiteDatabase
7 | import com.sweak.unlockmaster.data.local.database.dao.*
8 | import com.sweak.unlockmaster.data.local.database.entities.*
9 |
10 | @Database(
11 | entities = [
12 | UnlockEventEntity::class,
13 | LockEventEntity::class,
14 | ScreenOnEventEntity::class,
15 | UnlockLimitEntity::class,
16 | ScreenTimeLimitEntity::class,
17 | CounterPausedEventEntity::class,
18 | CounterUnpausedEventEntity::class
19 | ],
20 | version = 6,
21 | exportSchema = false
22 | )
23 | abstract class UnlockMasterDatabase : RoomDatabase() {
24 | abstract fun unlockEventsDao(): UnlockEventsDao
25 | abstract fun lockEventsDao(): LockEventsDao
26 | abstract fun screenOnEventsDao(): ScreenOnEventsDao
27 | abstract fun unlockLimitsDao(): UnlockLimitsDao
28 | abstract fun screenTimeLimitsDao(): ScreenTimeLimitsDao
29 | abstract fun counterPausedEventsDao(): CounterPausedEventsDao
30 | abstract fun counterUnpausedEventsDao(): CounterUnpausedEventsDao
31 |
32 | companion object {
33 | val MIGRATION_5_6: Migration = object : Migration(5, 6) {
34 | override fun migrate(db: SupportSQLiteDatabase) {
35 | db.execSQL("CREATE TABLE IF NOT EXISTS screen_time_limit (" +
36 | "limitApplianceDayTimeInMillis INTEGER PRIMARY KEY NOT NULL, " +
37 | "limitAmountMinutes INTEGER NOT NULL)")
38 | }
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/data/local/database/dao/CounterPausedEventsDao.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.data.local.database.dao
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Delete
5 | import androidx.room.Insert
6 | import androidx.room.Query
7 | import com.sweak.unlockmaster.data.local.database.entities.CounterPausedEventEntity
8 |
9 | @Dao
10 | interface CounterPausedEventsDao {
11 |
12 | @Insert
13 | suspend fun insert(counterPausedEventEntity: CounterPausedEventEntity)
14 |
15 | @Insert
16 | suspend fun insertAll(counterPausedEventsEntities: List)
17 |
18 | @Query("SELECT * FROM counter_paused_event")
19 | suspend fun getAllCounterPausedEvents(): List
20 |
21 | @Delete
22 | suspend fun deleteAll(counterPausedEventsEntities: List)
23 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/data/local/database/dao/CounterUnpausedEventsDao.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.data.local.database.dao
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Delete
5 | import androidx.room.Insert
6 | import androidx.room.Query
7 | import com.sweak.unlockmaster.data.local.database.entities.CounterUnpausedEventEntity
8 |
9 | @Dao
10 | interface CounterUnpausedEventsDao {
11 |
12 | @Insert
13 | suspend fun insert(counterUnpausedEventEntity: CounterUnpausedEventEntity)
14 |
15 | @Insert
16 | suspend fun insertAll(counterUnpausedEventsEntities: List)
17 |
18 | @Query("SELECT * FROM counter_unpaused_event")
19 | suspend fun getAllCounterUnpausedEvents(): List
20 |
21 | @Delete
22 | suspend fun deleteAll(counterUnpausedEventsEntities: List)
23 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/data/local/database/dao/LockEventsDao.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.data.local.database.dao
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Delete
5 | import androidx.room.Insert
6 | import androidx.room.Query
7 | import com.sweak.unlockmaster.data.local.database.entities.LockEventEntity
8 |
9 | @Dao
10 | interface LockEventsDao {
11 |
12 | @Insert
13 | suspend fun insert(lockEventEntity: LockEventEntity)
14 |
15 | @Insert
16 | suspend fun insertAll(lockEventsEntities: List)
17 |
18 | @Query("SELECT * FROM lock_event")
19 | suspend fun getAllLockEvents(): List
20 |
21 | @Delete
22 | suspend fun deleteAll(lockEventsEntities: List)
23 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/data/local/database/dao/ScreenOnEventsDao.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.data.local.database.dao
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Delete
5 | import androidx.room.Insert
6 | import androidx.room.Query
7 | import com.sweak.unlockmaster.data.local.database.entities.ScreenOnEventEntity
8 |
9 | @Dao
10 | interface ScreenOnEventsDao {
11 |
12 | @Insert
13 | suspend fun insert(screenOnEventEntity: ScreenOnEventEntity)
14 |
15 | @Insert
16 | suspend fun insertAll(screenOnEventsEntities: List)
17 |
18 | @Query("SELECT * FROM screen_on_event")
19 | suspend fun getAllScreenOnEvents(): List
20 |
21 | @Delete
22 | suspend fun deleteAll(screenOnEventsEntities: List)
23 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/data/local/database/dao/ScreenTimeLimitsDao.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.data.local.database.dao
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Delete
5 | import androidx.room.Insert
6 | import androidx.room.Query
7 | import androidx.room.Update
8 | import com.sweak.unlockmaster.data.local.database.entities.ScreenTimeLimitEntity
9 |
10 | @Dao
11 | interface ScreenTimeLimitsDao {
12 |
13 | @Insert
14 | suspend fun insert(screenTimeLimitEntity: ScreenTimeLimitEntity)
15 |
16 | @Insert
17 | suspend fun insertAll(screenTimeLimitsEntities: List)
18 |
19 | @Update
20 | suspend fun update(screenTimeLimitEntity: ScreenTimeLimitEntity)
21 |
22 | @Delete
23 | suspend fun delete(screenTimeLimitEntity: ScreenTimeLimitEntity)
24 |
25 | @Query("SELECT * FROM screen_time_limit")
26 | suspend fun getAllScreenTimeLimits(): List
27 |
28 | @Delete
29 | suspend fun deleteAll(screenTimeLimitsEntities: List)
30 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/data/local/database/dao/UnlockEventsDao.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.data.local.database.dao
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Delete
5 | import androidx.room.Insert
6 | import androidx.room.Query
7 | import com.sweak.unlockmaster.data.local.database.entities.UnlockEventEntity
8 |
9 | @Dao
10 | interface UnlockEventsDao {
11 |
12 | @Insert
13 | suspend fun insert(unlockEventEntity: UnlockEventEntity)
14 |
15 | @Insert
16 | suspend fun insertAll(unlockEventsEntities: List)
17 |
18 | @Query("SELECT * FROM unlock_event")
19 | suspend fun getAllUnlockEvents(): List
20 |
21 | @Delete
22 | suspend fun deleteAll(unlockEventsEntities: List)
23 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/data/local/database/dao/UnlockLimitsDao.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.data.local.database.dao
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Delete
5 | import androidx.room.Insert
6 | import androidx.room.Query
7 | import androidx.room.Update
8 | import com.sweak.unlockmaster.data.local.database.entities.UnlockLimitEntity
9 |
10 | @Dao
11 | interface UnlockLimitsDao {
12 |
13 | @Insert
14 | suspend fun insert(unlockLimitEntity: UnlockLimitEntity)
15 |
16 | @Insert
17 | suspend fun insertAll(unlockLimitsEntities: List)
18 |
19 | @Update
20 | suspend fun update(unlockLimitEntity: UnlockLimitEntity)
21 |
22 | @Delete
23 | suspend fun delete(unlockLimitEntity: UnlockLimitEntity)
24 |
25 | @Query("SELECT * FROM unlock_limit")
26 | suspend fun getAllUnlockLimits(): List
27 |
28 | @Delete
29 | suspend fun deleteAll(unlockLimitsEntities: List)
30 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/data/local/database/entities/CounterPausedEventEntity.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.data.local.database.entities
2 |
3 | import androidx.annotation.Keep
4 | import androidx.room.Entity
5 | import androidx.room.PrimaryKey
6 |
7 | @Keep
8 | @Entity(tableName = "counter_paused_event")
9 | data class CounterPausedEventEntity(
10 | @PrimaryKey val timeInMillis: Long
11 | )
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/data/local/database/entities/CounterUnpausedEventEntity.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.data.local.database.entities
2 |
3 | import androidx.annotation.Keep
4 | import androidx.room.Entity
5 | import androidx.room.PrimaryKey
6 |
7 | @Keep
8 | @Entity(tableName = "counter_unpaused_event")
9 | data class CounterUnpausedEventEntity(
10 | @PrimaryKey val timeInMillis: Long
11 | )
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/data/local/database/entities/LockEventEntity.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.data.local.database.entities
2 |
3 | import androidx.annotation.Keep
4 | import androidx.room.Entity
5 | import androidx.room.PrimaryKey
6 |
7 | @Keep
8 | @Entity(tableName = "lock_event")
9 | data class LockEventEntity(
10 | @PrimaryKey val timeInMillis: Long
11 | )
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/data/local/database/entities/ScreenOnEventEntity.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.data.local.database.entities
2 |
3 | import androidx.annotation.Keep
4 | import androidx.room.Entity
5 | import androidx.room.PrimaryKey
6 |
7 | @Keep
8 | @Entity(tableName = "screen_on_event")
9 | data class ScreenOnEventEntity(
10 | @PrimaryKey val timeInMillis: Long
11 | )
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/data/local/database/entities/ScreenTimeLimitEntity.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.data.local.database.entities
2 |
3 | import androidx.annotation.Keep
4 | import androidx.room.Entity
5 | import androidx.room.PrimaryKey
6 |
7 | @Keep
8 | @Entity(tableName = "screen_time_limit")
9 | data class ScreenTimeLimitEntity(
10 | @PrimaryKey val limitApplianceDayTimeInMillis: Long,
11 | val limitAmountMinutes: Int
12 | )
13 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/data/local/database/entities/UnlockEventEntity.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.data.local.database.entities
2 |
3 | import androidx.annotation.Keep
4 | import androidx.room.Entity
5 | import androidx.room.PrimaryKey
6 |
7 | @Keep
8 | @Entity(tableName = "unlock_event")
9 | data class UnlockEventEntity(
10 | @PrimaryKey val timeInMillis: Long
11 | )
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/data/local/database/entities/UnlockLimitEntity.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.data.local.database.entities
2 |
3 | import androidx.annotation.Keep
4 | import androidx.room.Entity
5 | import androidx.room.PrimaryKey
6 |
7 | @Keep
8 | @Entity(tableName = "unlock_limit")
9 | data class UnlockLimitEntity(
10 | @PrimaryKey val limitApplianceDayTimeInMillis: Long,
11 | val limitAmount: Int
12 | )
13 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/data/management/UnlockMasterAlarmManagerImpl.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.data.management
2 |
3 | import android.app.AlarmManager
4 | import android.app.Application
5 | import android.app.PendingIntent
6 | import android.content.Intent
7 | import android.os.Build
8 | import com.sweak.unlockmaster.domain.management.UnlockMasterAlarmManager
9 | import com.sweak.unlockmaster.domain.model.DailyWrapUpNotificationsTime
10 | import com.sweak.unlockmaster.domain.repository.TimeRepository
11 | import javax.inject.Inject
12 | import javax.inject.Named
13 |
14 | class UnlockMasterAlarmManagerImpl @Inject constructor(
15 | private val alarmManager: AlarmManager,
16 | private val timeRepository: TimeRepository,
17 | @Named("DailyWrapUpAlarmIntent") private val dailyWrapUpAlarmIntent: Intent,
18 | private val application: Application
19 | ) : UnlockMasterAlarmManager {
20 |
21 | override fun scheduleNewDailyWrapUpNotifications(
22 | dailyWrapUpNotificationsTime: DailyWrapUpNotificationsTime
23 | ) {
24 | val alarmPendingIntent = PendingIntent.getBroadcast(
25 | application.applicationContext,
26 | DAILY_WRAP_UP_ALARM_REQUEST_CODE,
27 | dailyWrapUpAlarmIntent,
28 | PendingIntent.FLAG_UPDATE_CURRENT or
29 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
30 | PendingIntent.FLAG_IMMUTABLE
31 | else 0
32 | )
33 |
34 | alarmManager.setRepeating(
35 | AlarmManager.RTC_WAKEUP,
36 | timeRepository.getFutureTimeInMillisOfSpecifiedHourOfDayAndMinute(
37 | dailyWrapUpNotificationsTime.hourOfDay,
38 | dailyWrapUpNotificationsTime.minute
39 | ),
40 | AlarmManager.INTERVAL_DAY,
41 | alarmPendingIntent
42 | )
43 | }
44 |
45 | companion object {
46 | const val DAILY_WRAP_UP_ALARM_REQUEST_CODE = 100
47 | }
48 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/data/repository/CounterPausedEventsRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.data.repository
2 |
3 | import com.sweak.unlockmaster.data.local.database.dao.CounterPausedEventsDao
4 | import com.sweak.unlockmaster.data.local.database.entities.CounterPausedEventEntity
5 | import com.sweak.unlockmaster.domain.model.UnlockMasterEvent.CounterPausedEvent
6 | import com.sweak.unlockmaster.domain.repository.CounterPausedEventsRepository
7 |
8 | class CounterPausedEventsRepositoryImpl(
9 | private val counterPausedEventsDao: CounterPausedEventsDao
10 | ) : CounterPausedEventsRepository {
11 |
12 | override suspend fun addCounterPausedEvent(counterPausedEvent: CounterPausedEvent) {
13 | counterPausedEventsDao.insert(
14 | CounterPausedEventEntity(timeInMillis = counterPausedEvent.timeInMillis)
15 | )
16 | }
17 |
18 | override suspend fun getCounterPausedEventsSinceTime(
19 | sinceTimeInMillis: Long
20 | ): List =
21 | counterPausedEventsDao.getAllCounterPausedEvents()
22 | .filter {
23 | it.timeInMillis >= sinceTimeInMillis
24 | }
25 | .map {
26 | CounterPausedEvent(counterPausedTimeInMillis = it.timeInMillis)
27 | }
28 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/data/repository/CounterUnpausedEventsRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.data.repository
2 |
3 | import com.sweak.unlockmaster.data.local.database.dao.CounterUnpausedEventsDao
4 | import com.sweak.unlockmaster.data.local.database.entities.CounterUnpausedEventEntity
5 | import com.sweak.unlockmaster.domain.model.UnlockMasterEvent.CounterUnpausedEvent
6 | import com.sweak.unlockmaster.domain.repository.CounterUnpausedEventsRepository
7 |
8 | class CounterUnpausedEventsRepositoryImpl(
9 | private val counterUnpausedEventsDao: CounterUnpausedEventsDao
10 | ) : CounterUnpausedEventsRepository {
11 |
12 | override suspend fun addCounterUnpausedEvent(counterUnpausedEvent: CounterUnpausedEvent) {
13 | counterUnpausedEventsDao.insert(
14 | CounterUnpausedEventEntity(timeInMillis = counterUnpausedEvent.timeInMillis)
15 | )
16 | }
17 |
18 | override suspend fun getCounterUnpausedEventsSinceTime(
19 | sinceTimeInMillis: Long
20 | ): List =
21 | counterUnpausedEventsDao.getAllCounterUnpausedEvents()
22 | .filter {
23 | it.timeInMillis >= sinceTimeInMillis
24 | }
25 | .map {
26 | CounterUnpausedEvent(counterUnpausedTimeInMillis = it.timeInMillis)
27 | }
28 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/data/repository/LockEventsRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.data.repository
2 |
3 | import android.database.sqlite.SQLiteConstraintException
4 | import com.sweak.unlockmaster.data.local.database.dao.LockEventsDao
5 | import com.sweak.unlockmaster.data.local.database.entities.LockEventEntity
6 | import com.sweak.unlockmaster.domain.model.UnlockMasterEvent.LockEvent
7 | import com.sweak.unlockmaster.domain.repository.LockEventsRepository
8 |
9 | class LockEventsRepositoryImpl(
10 | private val lockEventsDao: LockEventsDao
11 | ) : LockEventsRepository {
12 |
13 | override suspend fun addLockEvent(lockEvent: LockEvent) {
14 | try {
15 | lockEventsDao.insert(
16 | LockEventEntity(timeInMillis = lockEvent.timeInMillis)
17 | )
18 | } catch (_: SQLiteConstraintException) { /* no-op */ }
19 | }
20 |
21 | override suspend fun getLockEventsSinceTime(sinceTimeInMillis: Long): List =
22 | lockEventsDao.getAllLockEvents()
23 | .filter {
24 | it.timeInMillis >= sinceTimeInMillis
25 | }
26 | .map {
27 | LockEvent(lockTimeInMillis = it.timeInMillis)
28 | }
29 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/data/repository/ScreenOnEventsRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.data.repository
2 |
3 | import android.database.sqlite.SQLiteConstraintException
4 | import com.sweak.unlockmaster.data.local.database.dao.ScreenOnEventsDao
5 | import com.sweak.unlockmaster.data.local.database.entities.ScreenOnEventEntity
6 | import com.sweak.unlockmaster.domain.model.UnlockMasterEvent.ScreenOnEvent
7 | import com.sweak.unlockmaster.domain.repository.ScreenOnEventsRepository
8 | import javax.inject.Inject
9 |
10 | class ScreenOnEventsRepositoryImpl @Inject constructor(
11 | private val screenOnEventsDao: ScreenOnEventsDao
12 | ) : ScreenOnEventsRepository {
13 |
14 | override suspend fun addScreenOnEvent(screenOnEvent: ScreenOnEvent) {
15 | try {
16 | screenOnEventsDao.insert(
17 | ScreenOnEventEntity(timeInMillis = screenOnEvent.timeInMillis)
18 | )
19 | } catch (_: SQLiteConstraintException) { /* no-op */ }
20 | }
21 |
22 | override suspend fun getLatestScreenOnEvent(): ScreenOnEvent? =
23 | screenOnEventsDao.getAllScreenOnEvents()
24 | .maxByOrNull {
25 | it.timeInMillis
26 | }?.let {
27 | ScreenOnEvent(screenOnTimeInMillis = it.timeInMillis)
28 | }
29 |
30 | override suspend fun getScreenOnEventsSinceTimeAndUntilTime(
31 | sinceTimeInMillis: Long,
32 | untilTimeInMillis: Long
33 | ): List =
34 | screenOnEventsDao.getAllScreenOnEvents()
35 | .filter {
36 | it.timeInMillis in sinceTimeInMillis until untilTimeInMillis
37 | }.map {
38 | ScreenOnEvent(screenOnTimeInMillis = it.timeInMillis)
39 | }
40 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/data/repository/ScreenTimeLimitsRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.data.repository
2 |
3 | import com.sweak.unlockmaster.data.local.database.dao.ScreenTimeLimitsDao
4 | import com.sweak.unlockmaster.data.local.database.entities.ScreenTimeLimitEntity
5 | import com.sweak.unlockmaster.domain.model.ScreenTimeLimit
6 | import com.sweak.unlockmaster.domain.repository.ScreenTimeLimitsRepository
7 |
8 | class ScreenTimeLimitsRepositoryImpl(
9 | private val screenTimeLimitsDao: ScreenTimeLimitsDao
10 | ) : ScreenTimeLimitsRepository {
11 |
12 | override suspend fun addScreenTimeLimit(screenTimeLimit: ScreenTimeLimit) {
13 | screenTimeLimitsDao.insert(
14 | ScreenTimeLimitEntity(
15 | limitApplianceDayTimeInMillis = screenTimeLimit.limitApplianceTimeInMillis,
16 | limitAmountMinutes = screenTimeLimit.limitAmountMinutes
17 | )
18 | )
19 | }
20 |
21 | override suspend fun updateScreenTimeLimit(screenTimeLimit: ScreenTimeLimit) {
22 | screenTimeLimitsDao.update(
23 | ScreenTimeLimitEntity(
24 | limitApplianceDayTimeInMillis = screenTimeLimit.limitApplianceTimeInMillis,
25 | limitAmountMinutes = screenTimeLimit.limitAmountMinutes
26 | )
27 | )
28 | }
29 |
30 | override suspend fun getScreenTimeLimitActiveAtTime(timeInMillis: Long): ScreenTimeLimit? {
31 | val allScreenTimeLimits = screenTimeLimitsDao.getAllScreenTimeLimits()
32 | val applianceTime = allScreenTimeLimits
33 | .filter {
34 | it.limitApplianceDayTimeInMillis <= timeInMillis
35 | }
36 | .maxOfOrNull {
37 | it.limitApplianceDayTimeInMillis
38 | }
39 |
40 | return applianceTime?.let { getScreenTimeLimitWithApplianceTime(it) }
41 | }
42 |
43 | override suspend fun getScreenTimeLimitWithApplianceTime(
44 | limitApplianceTimeInMillis: Long
45 | ): ScreenTimeLimit? =
46 | screenTimeLimitsDao.getAllScreenTimeLimits()
47 | .firstOrNull {
48 | it.limitApplianceDayTimeInMillis == limitApplianceTimeInMillis
49 | }?.let {
50 | ScreenTimeLimit(
51 | limitApplianceTimeInMillis = it.limitApplianceDayTimeInMillis,
52 | limitAmountMinutes = it.limitAmountMinutes
53 | )
54 | }
55 |
56 | override suspend fun deleteScreenTimeLimitWithApplianceTime(limitApplianceTimeInMillis: Long) {
57 | getScreenTimeLimitWithApplianceTime(limitApplianceTimeInMillis)?.let {
58 | screenTimeLimitsDao.delete(
59 | ScreenTimeLimitEntity(
60 | limitApplianceDayTimeInMillis = it.limitApplianceTimeInMillis,
61 | limitAmountMinutes = it.limitAmountMinutes
62 | )
63 | )
64 | }
65 | }
66 |
67 | override suspend fun getAllScreenTimeLimits(): List =
68 | screenTimeLimitsDao.getAllScreenTimeLimits().map {
69 | ScreenTimeLimit(
70 | limitApplianceTimeInMillis = it.limitApplianceDayTimeInMillis,
71 | limitAmountMinutes = it.limitAmountMinutes
72 | )
73 | }
74 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/data/repository/TimeRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.data.repository
2 |
3 | import com.sweak.unlockmaster.domain.repository.TimeRepository
4 | import com.sweak.unlockmaster.domain.toTimeInMillis
5 | import java.time.Instant
6 | import java.time.LocalDateTime
7 | import java.time.ZoneId
8 | import java.time.ZonedDateTime
9 |
10 | class TimeRepositoryImpl : TimeRepository {
11 |
12 | override fun getCurrentTimeInMillis(): Long = System.currentTimeMillis()
13 |
14 | override fun getTodayBeginningTimeInMillis(): Long =
15 | ZonedDateTime.now()
16 | .withHour(0)
17 | .withMinute(0)
18 | .withSecond(0)
19 | .withNano(0)
20 | .toTimeInMillis()
21 |
22 | override fun getTomorrowBeginningTimeInMillis(): Long =
23 | ZonedDateTime.now()
24 | .plusDays(1)
25 | .withHour(0)
26 | .withMinute(0)
27 | .withSecond(0)
28 | .withNano(0)
29 | .toTimeInMillis()
30 |
31 | override fun getSixDaysBeforeDayBeginningTimeInMillis(): Long =
32 | ZonedDateTime.now()
33 | .minusDays(6)
34 | .withHour(0)
35 | .withMinute(0)
36 | .withSecond(0)
37 | .withNano(0)
38 | .toTimeInMillis()
39 |
40 | override fun getBeginningOfGivenDayTimeInMillis(timeInMillis: Long): Long =
41 | ZonedDateTime.ofInstant(
42 | Instant.ofEpochMilli(timeInMillis),
43 | ZoneId.systemDefault()
44 | )
45 | .withHour(0)
46 | .withMinute(0)
47 | .withSecond(0)
48 | .withNano(0)
49 | .toTimeInMillis()
50 |
51 | override fun getFutureTimeInMillisOfSpecifiedHourOfDayAndMinute(
52 | hourOfDay: Int,
53 | minute: Int
54 | ): Long {
55 | var alarmZonedDateTime = ZonedDateTime.of(LocalDateTime.now(), ZoneId.systemDefault())
56 | alarmZonedDateTime = alarmZonedDateTime.withHour(hourOfDay)
57 | alarmZonedDateTime = alarmZonedDateTime.withMinute(minute)
58 | alarmZonedDateTime = alarmZonedDateTime.withSecond(0)
59 | alarmZonedDateTime = alarmZonedDateTime.withNano(0)
60 |
61 | if (alarmZonedDateTime.toInstant().toEpochMilli() <= getCurrentTimeInMillis()) {
62 | alarmZonedDateTime = alarmZonedDateTime.plusDays(1)
63 | }
64 |
65 | return alarmZonedDateTime.toInstant().toEpochMilli()
66 | }
67 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/data/repository/UnlockEventsRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.data.repository
2 |
3 | import android.database.sqlite.SQLiteConstraintException
4 | import com.sweak.unlockmaster.data.local.database.dao.UnlockEventsDao
5 | import com.sweak.unlockmaster.data.local.database.entities.UnlockEventEntity
6 | import com.sweak.unlockmaster.domain.model.UnlockMasterEvent.UnlockEvent
7 | import com.sweak.unlockmaster.domain.repository.UnlockEventsRepository
8 |
9 | class UnlockEventsRepositoryImpl(
10 | private val unlockEventsDao: UnlockEventsDao
11 | ) : UnlockEventsRepository {
12 |
13 | override suspend fun addUnlockEvent(unlockEvent: UnlockEvent) {
14 | try {
15 | unlockEventsDao.insert(
16 | UnlockEventEntity(timeInMillis = unlockEvent.timeInMillis)
17 | )
18 | } catch (_: SQLiteConstraintException) { /* no-op */ }
19 | }
20 |
21 | override suspend fun getUnlockEventsSinceTime(sinceTimeInMillis: Long): List =
22 | unlockEventsDao.getAllUnlockEvents()
23 | .filter {
24 | it.timeInMillis >= sinceTimeInMillis
25 | }
26 | .map {
27 | UnlockEvent(unlockTimeInMillis = it.timeInMillis)
28 | }
29 |
30 | override suspend fun getUnlockEventsSinceTimeAndUntilTime(
31 | sinceTimeInMillis: Long,
32 | untilTimeInMillis: Long
33 | ): List =
34 | unlockEventsDao.getAllUnlockEvents()
35 | .filter {
36 | it.timeInMillis in sinceTimeInMillis until untilTimeInMillis
37 | }.map {
38 | UnlockEvent(unlockTimeInMillis = it.timeInMillis)
39 | }
40 |
41 | override suspend fun getLatestUnlockEvent(): UnlockEvent? =
42 | unlockEventsDao.getAllUnlockEvents()
43 | .maxByOrNull {
44 | it.timeInMillis
45 | }?.let {
46 | UnlockEvent(
47 | unlockTimeInMillis = it.timeInMillis
48 | )
49 | }
50 |
51 | override suspend fun getFirstUnlockEvent(): UnlockEvent? =
52 | unlockEventsDao.getAllUnlockEvents()
53 | .minByOrNull {
54 | it.timeInMillis
55 | }?.let {
56 | UnlockEvent(
57 | unlockTimeInMillis = it.timeInMillis
58 | )
59 | }
60 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/data/repository/UnlockLimitsRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.data.repository
2 |
3 | import com.sweak.unlockmaster.data.local.database.dao.UnlockLimitsDao
4 | import com.sweak.unlockmaster.data.local.database.entities.UnlockLimitEntity
5 | import com.sweak.unlockmaster.domain.model.UnlockLimit
6 | import com.sweak.unlockmaster.domain.repository.UnlockLimitsRepository
7 |
8 | class UnlockLimitsRepositoryImpl(
9 | private val unlockLimitsDao: UnlockLimitsDao
10 | ) : UnlockLimitsRepository {
11 |
12 | override suspend fun addUnlockLimit(unlockLimit: UnlockLimit) {
13 | unlockLimitsDao.insert(
14 | UnlockLimitEntity(
15 | limitApplianceDayTimeInMillis = unlockLimit.limitApplianceTimeInMillis,
16 | limitAmount = unlockLimit.limitAmount
17 | )
18 | )
19 | }
20 |
21 | override suspend fun updateUnlockLimit(unlockLimit: UnlockLimit) {
22 | unlockLimitsDao.update(
23 | UnlockLimitEntity(
24 | limitApplianceDayTimeInMillis = unlockLimit.limitApplianceTimeInMillis,
25 | limitAmount = unlockLimit.limitAmount
26 | )
27 | )
28 | }
29 |
30 | override suspend fun getUnlockLimitActiveAtTime(timeInMillis: Long): UnlockLimit? {
31 | val allUnlockLimits = unlockLimitsDao.getAllUnlockLimits()
32 | val applianceTime = allUnlockLimits
33 | .filter {
34 | it.limitApplianceDayTimeInMillis <= timeInMillis
35 | }
36 | .maxOfOrNull {
37 | it.limitApplianceDayTimeInMillis
38 | }
39 |
40 | return applianceTime?.let { getUnlockLimitWithApplianceTime(it) }
41 | }
42 |
43 | override suspend fun getUnlockLimitWithApplianceTime(
44 | limitApplianceTimeInMillis: Long
45 | ): UnlockLimit? =
46 | unlockLimitsDao.getAllUnlockLimits()
47 | .firstOrNull {
48 | it.limitApplianceDayTimeInMillis == limitApplianceTimeInMillis
49 | }?.let {
50 | UnlockLimit(
51 | limitApplianceTimeInMillis = it.limitApplianceDayTimeInMillis,
52 | limitAmount = it.limitAmount
53 | )
54 | }
55 |
56 | override suspend fun deleteUnlockLimitWithApplianceTime(limitApplianceTimeInMillis: Long) {
57 | getUnlockLimitWithApplianceTime(limitApplianceTimeInMillis)?.let {
58 | unlockLimitsDao.delete(
59 | UnlockLimitEntity(
60 | limitApplianceDayTimeInMillis = it.limitApplianceTimeInMillis,
61 | limitAmount = it.limitAmount
62 | )
63 | )
64 | }
65 | }
66 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/di/ViewModelModule.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.di
2 |
3 | import android.app.Application
4 | import android.content.ContentResolver
5 | import com.sweak.unlockmaster.data.local.database.UnlockMasterDatabase
6 | import com.sweak.unlockmaster.data.management.UnlockMasterBackupManagerImpl
7 | import com.sweak.unlockmaster.domain.management.UnlockMasterBackupManager
8 | import com.sweak.unlockmaster.domain.repository.TimeRepository
9 | import com.sweak.unlockmaster.domain.repository.UserSessionRepository
10 | import com.sweak.unlockmaster.domain.use_case.daily_wrap_up.ScheduleDailyWrapUpNotificationsUseCase
11 | import com.sweak.unlockmaster.domain.use_case.screen_time_limits.AddOrUpdateScreenTimeLimitForTodayUseCase
12 | import dagger.Module
13 | import dagger.Provides
14 | import dagger.hilt.InstallIn
15 | import dagger.hilt.android.components.ViewModelComponent
16 |
17 | @Module
18 | @InstallIn(ViewModelComponent::class)
19 | object ViewModelModule {
20 |
21 | @Provides
22 | fun provideUnlockMasterBackupManager(
23 | database: UnlockMasterDatabase,
24 | userSessionRepository: UserSessionRepository,
25 | timeRepository: TimeRepository,
26 | scheduleDailyWrapUpNotificationsUseCase: ScheduleDailyWrapUpNotificationsUseCase,
27 | addOrUpdateScreenTimeLimitForTodayUseCase: AddOrUpdateScreenTimeLimitForTodayUseCase
28 | ): UnlockMasterBackupManager =
29 | UnlockMasterBackupManagerImpl(
30 | database,
31 | userSessionRepository,
32 | timeRepository,
33 | scheduleDailyWrapUpNotificationsUseCase,
34 | addOrUpdateScreenTimeLimitForTodayUseCase
35 | )
36 |
37 | @Provides
38 | fun provideContentResolver(app: Application): ContentResolver = app.contentResolver
39 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/Constants.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain
2 |
3 | const val DEFAULT_UNLOCK_LIMIT = 40
4 | const val UNLOCK_LIMIT_LOWER_BOUND = 10
5 | const val UNLOCK_LIMIT_UPPER_BOUND = 70
6 |
7 | const val DEFAULT_SCREEN_TIME_LIMIT_MINUTES = 180
8 | const val SCREEN_TIME_LIMIT_MINUTES_LOWER_BOUND = 60
9 | const val SCREEN_TIME_LIMIT_MINUTES_UPPER_BOUND = 300
10 | const val SCREEN_TIME_LIMIT_INTERVAL_MINUTES = 5
11 |
12 | const val DEFAULT_MOBILIZING_NOTIFICATIONS_FREQUENCY_PERCENTAGE = 25
13 | val AVAILABLE_MOBILIZING_NOTIFICATIONS_FREQUENCY_PERCENTAGES = listOf(10, 25, 50)
14 |
15 | const val DEFAULT_DAILY_WRAP_UPS_NOTIFICATIONS_TIME_IN_MINUTES_PAST_MIDNIGHT = 1260 // 21:00
16 | const val MINIMAL_DAILY_WRAP_UPS_NOTIFICATIONS_TIME_HOUR_OF_DAY = 20 // 20:00 - 23:59 is allowed
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/DateTimeUtils.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain
2 |
3 | import java.time.ZonedDateTime
4 |
5 | fun ZonedDateTime.toTimeInMillis(): Long =
6 | this.toInstant().toEpochMilli()
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/management/UnlockMasterAlarmManager.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.management
2 |
3 | import com.sweak.unlockmaster.domain.model.DailyWrapUpNotificationsTime
4 |
5 | interface UnlockMasterAlarmManager {
6 | fun scheduleNewDailyWrapUpNotifications(
7 | dailyWrapUpNotificationsTime: DailyWrapUpNotificationsTime
8 | )
9 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/management/UnlockMasterBackupManager.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.management
2 |
3 | interface UnlockMasterBackupManager {
4 | suspend fun createDataBackupFile(): ByteArray
5 | suspend fun restoreDataFromBackupFile(backupFileBytes: ByteArray)
6 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/model/DailyWrapUpData.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.model
2 |
3 | data class DailyWrapUpData(
4 | val screenUnlocksData: ScreenUnlocksData,
5 | val screenTimeData: ScreenTimeData,
6 | val unlockLimitData: UnlockLimitData,
7 | val screenTimeLimitData: ScreenTimeLimitData?,
8 | val screenOnData: ScreenOnData
9 | ) {
10 | data class ScreenUnlocksData(
11 | val todayUnlocksCount: Int,
12 | val yesterdayUnlocksCount: Int?,
13 | val lastWeekUnlocksCount: Int?,
14 | val progress: Progress
15 | )
16 |
17 | data class ScreenTimeData(
18 | val todayScreenTimeDurationMillis: Long,
19 | val yesterdayScreenTimeDurationMillis: Long?,
20 | val lastWeekScreenTimeDurationMillis: Long?,
21 | val progress: Progress
22 | )
23 |
24 | data class UnlockLimitData(
25 | val todayUnlockLimit: Int,
26 | val tomorrowUnlockLimit: Int,
27 | val recommendedUnlockLimit: Int?,
28 | val isLimitSignificantlyExceeded: Boolean,
29 | val isLowestUnlockLimitReached: Boolean
30 | )
31 |
32 | data class ScreenTimeLimitData(
33 | val todayScreenTimeLimitDurationMinutes: Int,
34 | val tomorrowScreenTimeLimitDurationMinutes: Int,
35 | val recommendedScreenTimeLimitDurationMinutes: Int?,
36 | val isLimitSignificantlyExceeded: Boolean,
37 | val isLowestScreenTimeLimitReached: Boolean
38 | )
39 |
40 | data class ScreenOnData(
41 | val todayScreenOnsCount: Int,
42 | val yesterdayScreenOnsCount: Int?,
43 | val lastWeekScreenOnsCount: Int?,
44 | val progress: Progress,
45 | val isManyMoreScreenOnsThanUnlocks: Boolean
46 | )
47 |
48 | enum class Progress {
49 | IMPROVEMENT, REGRESS, STABLE
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/model/DailyWrapUpNotificationsTime.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.model
2 |
3 | data class DailyWrapUpNotificationsTime(
4 | val hourOfDay: Int,
5 | val minute: Int
6 | )
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/model/ScreenTimeLimit.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.model
2 |
3 | data class ScreenTimeLimit(
4 | val limitApplianceTimeInMillis: Long,
5 | val limitAmountMinutes: Int
6 | )
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/model/ScreenTimeLimitWarningState.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.model
2 |
3 | enum class ScreenTimeLimitWarningState {
4 | NO_WARNINGS_FIRED,
5 | WARNING_15_MINUTES_TO_LIMIT_FIRED,
6 | WARNING_LIMIT_REACHED_FIRED
7 | }
8 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/model/SessionEvent.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.model
2 |
3 | import java.util.*
4 |
5 | sealed class SessionEvent(val sessionStartTime: Long, val sessionEndTime: Long) {
6 | class ScreenTime(
7 | sessionStartTime: Long,
8 | sessionEndTime: Long,
9 | val sessionDuration: Long
10 | ) : SessionEvent(sessionStartTime, sessionEndTime) {
11 | override fun equals(other: Any?): Boolean {
12 | if (this === other) return true
13 | if (other !is ScreenTime) return false
14 |
15 | return sessionStartTime == other.sessionStartTime &&
16 | sessionEndTime == other.sessionEndTime &&
17 | sessionDuration == other.sessionDuration
18 | }
19 |
20 | override fun hashCode(): Int {
21 | return Objects.hash(sessionStartTime, sessionEndTime, sessionDuration)
22 | }
23 | }
24 |
25 | class CounterPaused(
26 | sessionStartTime: Long,
27 | sessionEndTime: Long
28 | ) : SessionEvent(sessionStartTime, sessionEndTime) {
29 | override fun equals(other: Any?): Boolean {
30 | if (this === other) return true
31 | if (other !is CounterPaused) return false
32 |
33 | return sessionStartTime == other.sessionStartTime &&
34 | sessionEndTime == other.sessionEndTime
35 | }
36 |
37 | override fun hashCode(): Int {
38 | return Objects.hash(sessionStartTime, sessionEndTime)
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/model/UiThemeMode.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.model
2 |
3 | enum class UiThemeMode {
4 | LIGHT,
5 | DARK,
6 | SYSTEM
7 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/model/UnlockLimit.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.model
2 |
3 | data class UnlockLimit(
4 | val limitApplianceTimeInMillis: Long,
5 | val limitAmount: Int
6 | )
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/model/UnlockMasterEvent.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.model
2 |
3 | sealed class UnlockMasterEvent(val timeInMillis: Long) {
4 | class UnlockEvent(unlockTimeInMillis: Long) : UnlockMasterEvent(unlockTimeInMillis)
5 | class LockEvent(lockTimeInMillis: Long) : UnlockMasterEvent(lockTimeInMillis)
6 | class ScreenOnEvent(screenOnTimeInMillis: Long) : UnlockMasterEvent(screenOnTimeInMillis)
7 | class CounterPausedEvent(counterPausedTimeInMillis: Long) :
8 | UnlockMasterEvent(counterPausedTimeInMillis)
9 | class CounterUnpausedEvent(counterUnpausedTimeInMillis: Long) :
10 | UnlockMasterEvent(counterUnpausedTimeInMillis)
11 | }
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/repository/CounterPausedEventsRepository.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.repository
2 |
3 | import com.sweak.unlockmaster.domain.model.UnlockMasterEvent.CounterPausedEvent
4 |
5 | interface CounterPausedEventsRepository {
6 | suspend fun addCounterPausedEvent(counterPausedEvent: CounterPausedEvent)
7 |
8 | suspend fun getCounterPausedEventsSinceTime(sinceTimeInMillis: Long): List
9 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/repository/CounterUnpausedEventsRepository.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.repository
2 |
3 | import com.sweak.unlockmaster.domain.model.UnlockMasterEvent.CounterUnpausedEvent
4 |
5 | interface CounterUnpausedEventsRepository {
6 | suspend fun addCounterUnpausedEvent(counterUnpausedEvent: CounterUnpausedEvent)
7 |
8 | suspend fun getCounterUnpausedEventsSinceTime(sinceTimeInMillis: Long): List
9 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/repository/LockEventsRepository.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.repository
2 |
3 | import com.sweak.unlockmaster.domain.model.UnlockMasterEvent.LockEvent
4 |
5 | interface LockEventsRepository {
6 | suspend fun addLockEvent(lockEvent: LockEvent)
7 |
8 | suspend fun getLockEventsSinceTime(sinceTimeInMillis: Long): List
9 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/repository/ScreenOnEventsRepository.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.repository
2 |
3 | import com.sweak.unlockmaster.domain.model.UnlockMasterEvent.ScreenOnEvent
4 |
5 | interface ScreenOnEventsRepository {
6 | suspend fun addScreenOnEvent(screenOnEvent: ScreenOnEvent)
7 |
8 | suspend fun getLatestScreenOnEvent(): ScreenOnEvent?
9 |
10 | suspend fun getScreenOnEventsSinceTimeAndUntilTime(
11 | sinceTimeInMillis: Long,
12 | untilTimeInMillis: Long
13 | ): List
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/repository/ScreenTimeLimitsRepository.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.repository
2 |
3 | import com.sweak.unlockmaster.domain.model.ScreenTimeLimit
4 |
5 | interface ScreenTimeLimitsRepository {
6 | suspend fun addScreenTimeLimit(screenTimeLimit: ScreenTimeLimit)
7 | suspend fun updateScreenTimeLimit(screenTimeLimit: ScreenTimeLimit)
8 | suspend fun getScreenTimeLimitActiveAtTime(timeInMillis: Long): ScreenTimeLimit?
9 | suspend fun getScreenTimeLimitWithApplianceTime(
10 | limitApplianceTimeInMillis: Long
11 | ): ScreenTimeLimit?
12 | suspend fun deleteScreenTimeLimitWithApplianceTime(limitApplianceTimeInMillis: Long)
13 | suspend fun getAllScreenTimeLimits(): List
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/repository/TimeRepository.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.repository
2 |
3 | interface TimeRepository {
4 | fun getCurrentTimeInMillis(): Long
5 | fun getTodayBeginningTimeInMillis(): Long
6 | fun getTomorrowBeginningTimeInMillis(): Long
7 | fun getSixDaysBeforeDayBeginningTimeInMillis(): Long
8 | fun getBeginningOfGivenDayTimeInMillis(timeInMillis: Long): Long
9 | fun getFutureTimeInMillisOfSpecifiedHourOfDayAndMinute(hourOfDay: Int, minute: Int): Long
10 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/repository/UnlockEventsRepository.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.repository
2 |
3 | import com.sweak.unlockmaster.domain.model.UnlockMasterEvent.UnlockEvent
4 |
5 | interface UnlockEventsRepository {
6 | suspend fun addUnlockEvent(unlockEvent: UnlockEvent)
7 | suspend fun getUnlockEventsSinceTime(sinceTimeInMillis: Long): List
8 |
9 | suspend fun getUnlockEventsSinceTimeAndUntilTime(
10 | sinceTimeInMillis: Long,
11 | untilTimeInMillis: Long
12 | ): List
13 | suspend fun getLatestUnlockEvent(): UnlockEvent?
14 | suspend fun getFirstUnlockEvent(): UnlockEvent?
15 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/repository/UnlockLimitsRepository.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.repository
2 |
3 | import com.sweak.unlockmaster.domain.model.UnlockLimit
4 |
5 | interface UnlockLimitsRepository {
6 | suspend fun addUnlockLimit(unlockLimit: UnlockLimit)
7 | suspend fun updateUnlockLimit(unlockLimit: UnlockLimit)
8 | suspend fun getUnlockLimitActiveAtTime(timeInMillis: Long): UnlockLimit?
9 | suspend fun getUnlockLimitWithApplianceTime(limitApplianceTimeInMillis: Long): UnlockLimit?
10 | suspend fun deleteUnlockLimitWithApplianceTime(limitApplianceTimeInMillis: Long)
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/repository/UserSessionRepository.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.repository
2 |
3 | import com.sweak.unlockmaster.domain.model.ScreenTimeLimitWarningState
4 | import com.sweak.unlockmaster.domain.model.UiThemeMode
5 | import kotlinx.coroutines.flow.Flow
6 |
7 | interface UserSessionRepository {
8 | suspend fun setIntroductionFinished()
9 | suspend fun isIntroductionFinished(): Boolean
10 | suspend fun setUnlockCounterPaused(isPaused: Boolean)
11 | suspend fun isUnlockCounterPaused(): Boolean
12 | suspend fun setMobilizingNotificationsFrequencyPercentage(percentage: Int)
13 | suspend fun getMobilizingNotificationsFrequencyPercentage(): Int
14 | suspend fun setDailyWrapUpNotificationsTimeInMinutesAfterMidnight(minutes: Int)
15 | suspend fun getDailyWrapUpNotificationsTimeInMinutesAfterMidnight(): Int
16 | suspend fun setUnlockMasterServiceProperlyClosed(wasProperlyClosed: Boolean)
17 | suspend fun wasUnlockMasterServiceProperlyClosed(): Boolean
18 | suspend fun setShouldShowUnlockMasterBlockedWarning(shouldShowWarning: Boolean)
19 | suspend fun shouldShowUnlockMasterBlockedWarning(): Boolean
20 | suspend fun setUiThemeMode(uiThemeMode: UiThemeMode)
21 | fun getUiThemeModeFlow(): Flow
22 | suspend fun setOverUnlockLimitMobilizingNotificationsEnabled(areEnabled: Boolean)
23 | suspend fun areOverUnlockLimitMobilizingNotificationsEnabled(): Boolean
24 | suspend fun setScreenTimeLimitEnabled(isEnabled: Boolean)
25 | suspend fun isScreenTimeLimitEnabled(): Boolean
26 | suspend fun setScreenTimeLimitWarningState(state: ScreenTimeLimitWarningState)
27 | suspend fun getScreenTimeLimitWarningState(): ScreenTimeLimitWarningState
28 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/use_case/counter_pause/AddCounterPausedEventUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.use_case.counter_pause
2 |
3 | import com.sweak.unlockmaster.domain.model.UnlockMasterEvent
4 | import com.sweak.unlockmaster.domain.repository.CounterPausedEventsRepository
5 | import com.sweak.unlockmaster.domain.repository.TimeRepository
6 | import javax.inject.Inject
7 |
8 | class AddCounterPausedEventUseCase @Inject constructor(
9 | private val counterPausedEventsRepository: CounterPausedEventsRepository,
10 | private val timeRepository: TimeRepository
11 | ) {
12 | suspend operator fun invoke() {
13 | counterPausedEventsRepository.addCounterPausedEvent(
14 | counterPausedEvent = UnlockMasterEvent.CounterPausedEvent(
15 | counterPausedTimeInMillis = timeRepository.getCurrentTimeInMillis()
16 | )
17 | )
18 | }
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/use_case/counter_pause/AddCounterUnpausedEventUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.use_case.counter_pause
2 |
3 | import com.sweak.unlockmaster.domain.model.UnlockMasterEvent
4 | import com.sweak.unlockmaster.domain.repository.CounterUnpausedEventsRepository
5 | import com.sweak.unlockmaster.domain.repository.TimeRepository
6 | import javax.inject.Inject
7 |
8 | class AddCounterUnpausedEventUseCase @Inject constructor(
9 | private val counterUnpausedEventsRepository: CounterUnpausedEventsRepository,
10 | private val timeRepository: TimeRepository
11 | ) {
12 | suspend operator fun invoke() {
13 | counterUnpausedEventsRepository.addCounterUnpausedEvent(
14 | counterUnpausedEvent = UnlockMasterEvent.CounterUnpausedEvent(
15 | counterUnpausedTimeInMillis = timeRepository.getCurrentTimeInMillis()
16 | )
17 | )
18 | }
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/use_case/daily_wrap_up/GetDailyWrapUpNotificationsTimeUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.use_case.daily_wrap_up
2 |
3 | import com.sweak.unlockmaster.domain.model.DailyWrapUpNotificationsTime
4 | import com.sweak.unlockmaster.domain.repository.UserSessionRepository
5 | import javax.inject.Inject
6 |
7 | class GetDailyWrapUpNotificationsTimeUseCase @Inject constructor(
8 | private val userSessionRepository: UserSessionRepository
9 | ) {
10 | suspend operator fun invoke(): DailyWrapUpNotificationsTime {
11 | val notificationTimeInMinutesAfterMidnight =
12 | userSessionRepository.getDailyWrapUpNotificationsTimeInMinutesAfterMidnight()
13 | val hourInMinutes = 60
14 |
15 | return DailyWrapUpNotificationsTime(
16 | hourOfDay = notificationTimeInMinutesAfterMidnight / hourInMinutes,
17 | minute = notificationTimeInMinutesAfterMidnight % hourInMinutes
18 | )
19 | }
20 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/use_case/daily_wrap_up/IsGivenDayEligibleForDailyWrapUpUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.use_case.daily_wrap_up
2 |
3 | import com.sweak.unlockmaster.domain.use_case.unlock_events.GetUnlockEventsCountForGivenDayUseCase
4 | import javax.inject.Inject
5 |
6 | class IsGivenDayEligibleForDailyWrapUpUseCase @Inject constructor(
7 | private val getUnlockEventsCountForGivenDayUseCase: GetUnlockEventsCountForGivenDayUseCase
8 | ) {
9 | suspend operator fun invoke(dailyWrapUpDayMillis: Long): Boolean {
10 | val unlocksCountForGivenDay = getUnlockEventsCountForGivenDayUseCase(dailyWrapUpDayMillis)
11 |
12 | return unlocksCountForGivenDay > 0
13 | }
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/use_case/daily_wrap_up/ScheduleDailyWrapUpNotificationsUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.use_case.daily_wrap_up
2 |
3 | import com.sweak.unlockmaster.domain.management.UnlockMasterAlarmManager
4 | import javax.inject.Inject
5 |
6 | class ScheduleDailyWrapUpNotificationsUseCase @Inject constructor(
7 | private val unlockMasterAlarmManager: UnlockMasterAlarmManager,
8 | private val getDailyWrapUpNotificationsTimeUseCase: GetDailyWrapUpNotificationsTimeUseCase
9 | ) {
10 | suspend operator fun invoke() {
11 | unlockMasterAlarmManager.scheduleNewDailyWrapUpNotifications(
12 | getDailyWrapUpNotificationsTimeUseCase()
13 | )
14 | }
15 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/use_case/daily_wrap_up/SetDailyWrapUpNotificationsTimeUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.use_case.daily_wrap_up
2 |
3 | import com.sweak.unlockmaster.domain.model.DailyWrapUpNotificationsTime
4 | import com.sweak.unlockmaster.domain.repository.UserSessionRepository
5 | import javax.inject.Inject
6 |
7 | class SetDailyWrapUpNotificationsTimeUseCase @Inject constructor(
8 | private val userSessionRepository: UserSessionRepository
9 | ) {
10 | suspend operator fun invoke(dailyWrapUpNotificationsTime: DailyWrapUpNotificationsTime) {
11 | val hourInMinutes = 60
12 | val notificationTimeInMinutesAfterMidnight =
13 | dailyWrapUpNotificationsTime.hourOfDay * hourInMinutes +
14 | dailyWrapUpNotificationsTime.minute
15 |
16 | userSessionRepository.setDailyWrapUpNotificationsTimeInMinutesAfterMidnight(
17 | minutes = notificationTimeInMinutesAfterMidnight
18 | )
19 | }
20 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/use_case/lock_events/AddLockEventUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.use_case.lock_events
2 |
3 | import com.sweak.unlockmaster.domain.model.UnlockMasterEvent
4 | import com.sweak.unlockmaster.domain.repository.LockEventsRepository
5 | import com.sweak.unlockmaster.domain.repository.TimeRepository
6 | import javax.inject.Inject
7 |
8 | class AddLockEventUseCase @Inject constructor(
9 | private val lockEventsRepository: LockEventsRepository,
10 | private val timeRepository: TimeRepository
11 | ) {
12 | suspend operator fun invoke() {
13 | lockEventsRepository.addLockEvent(
14 | lockEvent = UnlockMasterEvent.LockEvent(
15 | lockTimeInMillis = timeRepository.getCurrentTimeInMillis()
16 | )
17 | )
18 | }
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/use_case/lock_events/ShouldAddLockEventUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.use_case.lock_events
2 |
3 | import com.sweak.unlockmaster.domain.repository.ScreenOnEventsRepository
4 | import com.sweak.unlockmaster.domain.repository.UnlockEventsRepository
5 | import javax.inject.Inject
6 | import kotlin.math.abs
7 |
8 | class ShouldAddLockEventUseCase @Inject constructor(
9 | private val unlockEventsRepository: UnlockEventsRepository,
10 | private val screenOnEventsRepository: ScreenOnEventsRepository
11 | ) {
12 | suspend operator fun invoke(): Boolean {
13 | val latestUnlockEvent = unlockEventsRepository.getLatestUnlockEvent()
14 | val latestScreenOnEvent = screenOnEventsRepository.getLatestScreenOnEvent()
15 |
16 | if (latestUnlockEvent == null) {
17 | return false
18 | }
19 |
20 | if (latestScreenOnEvent == null) {
21 | return true
22 | }
23 |
24 | val unlockAndScreenOnDifferenceTimeInMillis =
25 | latestUnlockEvent.timeInMillis - latestScreenOnEvent.timeInMillis
26 |
27 | // The latest event is clearly an UnlockEvent so we can safely return true to add LockEvent.
28 | if (unlockAndScreenOnDifferenceTimeInMillis > 0) {
29 | return true
30 | }
31 |
32 | // When instant-unlocking (unlocking without screen interaction e.g. fingerprint unlock)
33 | // UnlockEvents are usually recorded BEFORE ScreenOnEvents.
34 | // In an instant-unlock cases, the highest time between UnlockEvent and ScreenOnEvent
35 | // recorded was ~2 seconds. With that in mind, if we have a time between first UnlockEvent
36 | // and second ScreenOnEvent that is less than 2 seconds we consider the last UnlockEvent
37 | // being and instant-unlock and thus allowing to add LockEvent by returning true.
38 | return abs(unlockAndScreenOnDifferenceTimeInMillis) <= 2000L
39 | }
40 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/use_case/screen_on_events/AddScreenOnEventUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.use_case.screen_on_events
2 |
3 | import com.sweak.unlockmaster.domain.model.UnlockMasterEvent
4 | import com.sweak.unlockmaster.domain.repository.ScreenOnEventsRepository
5 | import com.sweak.unlockmaster.domain.repository.TimeRepository
6 | import javax.inject.Inject
7 |
8 | class AddScreenOnEventUseCase @Inject constructor(
9 | private val screenOnEventsRepository: ScreenOnEventsRepository,
10 | private val timeRepository: TimeRepository
11 | ) {
12 | suspend operator fun invoke() {
13 | screenOnEventsRepository.addScreenOnEvent(
14 | screenOnEvent = UnlockMasterEvent.ScreenOnEvent(
15 | screenOnTimeInMillis = timeRepository.getCurrentTimeInMillis()
16 | )
17 | )
18 | }
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/use_case/screen_on_events/GetScreenOnEventsCountForGivenDayUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.use_case.screen_on_events
2 |
3 | import com.sweak.unlockmaster.domain.repository.ScreenOnEventsRepository
4 | import com.sweak.unlockmaster.domain.repository.TimeRepository
5 | import com.sweak.unlockmaster.domain.toTimeInMillis
6 | import java.time.Instant
7 | import java.time.ZoneId
8 | import java.time.ZonedDateTime
9 | import javax.inject.Inject
10 |
11 | class GetScreenOnEventsCountForGivenDayUseCase @Inject constructor(
12 | private val screenOnEventsRepository: ScreenOnEventsRepository,
13 | private val timeRepository: TimeRepository
14 | ) {
15 | suspend operator fun invoke(dayTimeInMillis: Long): Int {
16 | val beginningOfGivenDay = ZonedDateTime.ofInstant(
17 | Instant.ofEpochMilli(
18 | timeRepository.getBeginningOfGivenDayTimeInMillis(dayTimeInMillis)
19 | ),
20 | ZoneId.systemDefault()
21 | )
22 | val endingOfGivenDay = beginningOfGivenDay.plusDays(1)
23 |
24 | return screenOnEventsRepository.getScreenOnEventsSinceTimeAndUntilTime(
25 | sinceTimeInMillis = beginningOfGivenDay.toTimeInMillis(),
26 | untilTimeInMillis = endingOfGivenDay.toTimeInMillis()
27 | ).size
28 | }
29 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/use_case/screen_time_limits/AddOrUpdateScreenTimeLimitForTodayUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.use_case.screen_time_limits
2 |
3 | import com.sweak.unlockmaster.domain.model.ScreenTimeLimit
4 | import com.sweak.unlockmaster.domain.repository.ScreenTimeLimitsRepository
5 | import com.sweak.unlockmaster.domain.repository.TimeRepository
6 | import javax.inject.Inject
7 |
8 | class AddOrUpdateScreenTimeLimitForTodayUseCase @Inject constructor(
9 | private val screenTimeLimitsRepository: ScreenTimeLimitsRepository,
10 | private val timeRepository: TimeRepository
11 | ) {
12 | suspend operator fun invoke(limitAmountMinutes: Int) {
13 | val latestScreenTimeLimit = screenTimeLimitsRepository.getScreenTimeLimitActiveAtTime(
14 | timeInMillis = timeRepository.getCurrentTimeInMillis()
15 | )
16 | val todayBeginningTimeInMillis = timeRepository.getTodayBeginningTimeInMillis()
17 | val newScreenTimeLimit = ScreenTimeLimit(
18 | limitApplianceTimeInMillis = todayBeginningTimeInMillis,
19 | limitAmountMinutes = limitAmountMinutes
20 | )
21 |
22 | if (latestScreenTimeLimit == null) {
23 | screenTimeLimitsRepository.addScreenTimeLimit(screenTimeLimit = newScreenTimeLimit)
24 | } else {
25 | if (latestScreenTimeLimit.limitApplianceTimeInMillis < todayBeginningTimeInMillis) {
26 | screenTimeLimitsRepository.addScreenTimeLimit(screenTimeLimit = newScreenTimeLimit)
27 | } else {
28 | screenTimeLimitsRepository.updateScreenTimeLimit(
29 | screenTimeLimit = newScreenTimeLimit
30 | )
31 | }
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/use_case/screen_time_limits/AddOrUpdateScreenTimeLimitForTomorrowUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.use_case.screen_time_limits
2 |
3 | import com.sweak.unlockmaster.domain.model.ScreenTimeLimit
4 | import com.sweak.unlockmaster.domain.repository.ScreenTimeLimitsRepository
5 | import com.sweak.unlockmaster.domain.repository.TimeRepository
6 | import javax.inject.Inject
7 |
8 | class AddOrUpdateScreenTimeLimitForTomorrowUseCase @Inject constructor(
9 | private val screenTimeLimitsRepository: ScreenTimeLimitsRepository,
10 | private val timeRepository: TimeRepository
11 | ) {
12 | suspend operator fun invoke(limitAmountMinutes: Int) {
13 | val tomorrowBeginningTimeInMillis = timeRepository.getTomorrowBeginningTimeInMillis()
14 | val currentScreenTimeLimit = screenTimeLimitsRepository.getScreenTimeLimitActiveAtTime(
15 | timeInMillis = timeRepository.getCurrentTimeInMillis()
16 | )
17 | val screenTimeLimitForTomorrow =
18 | screenTimeLimitsRepository.getScreenTimeLimitWithApplianceTime(
19 | limitApplianceTimeInMillis = tomorrowBeginningTimeInMillis
20 | )
21 | val newScreenTimeLimit = ScreenTimeLimit(
22 | limitApplianceTimeInMillis = tomorrowBeginningTimeInMillis,
23 | limitAmountMinutes = limitAmountMinutes
24 | )
25 |
26 | if (screenTimeLimitForTomorrow == null) {
27 | if (currentScreenTimeLimit?.limitAmountMinutes == limitAmountMinutes) {
28 | return
29 | }
30 |
31 | screenTimeLimitsRepository.addScreenTimeLimit(screenTimeLimit = newScreenTimeLimit)
32 | } else {
33 | if (currentScreenTimeLimit?.limitAmountMinutes == limitAmountMinutes) {
34 | screenTimeLimitsRepository.deleteScreenTimeLimitWithApplianceTime(
35 | limitApplianceTimeInMillis = tomorrowBeginningTimeInMillis
36 | )
37 | return
38 | }
39 |
40 | screenTimeLimitsRepository.updateScreenTimeLimit(screenTimeLimit = newScreenTimeLimit)
41 | }
42 | }
43 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/use_case/screen_time_limits/DeleteScreenTimeLimitForTomorrowUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.use_case.screen_time_limits
2 |
3 | import com.sweak.unlockmaster.domain.repository.ScreenTimeLimitsRepository
4 | import com.sweak.unlockmaster.domain.repository.TimeRepository
5 | import javax.inject.Inject
6 |
7 | class DeleteScreenTimeLimitForTomorrowUseCase @Inject constructor(
8 | private val screenTimeLimitsRepository: ScreenTimeLimitsRepository,
9 | private val timeRepository: TimeRepository
10 | ) {
11 | suspend operator fun invoke() {
12 | screenTimeLimitsRepository.deleteScreenTimeLimitWithApplianceTime(
13 | limitApplianceTimeInMillis = timeRepository.getTomorrowBeginningTimeInMillis()
14 | )
15 | }
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/use_case/screen_time_limits/GetScreenTimeLimitMinutesForTodayUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.use_case.screen_time_limits
2 |
3 | import com.sweak.unlockmaster.domain.DEFAULT_SCREEN_TIME_LIMIT_MINUTES
4 | import com.sweak.unlockmaster.domain.repository.ScreenTimeLimitsRepository
5 | import com.sweak.unlockmaster.domain.repository.TimeRepository
6 | import javax.inject.Inject
7 |
8 | class GetScreenTimeLimitMinutesForTodayUseCase @Inject constructor(
9 | private val screenTimeLimitsRepository: ScreenTimeLimitsRepository,
10 | private val timeRepository: TimeRepository
11 | ) {
12 | suspend operator fun invoke(): Int {
13 | return screenTimeLimitsRepository.getScreenTimeLimitActiveAtTime(
14 | timeInMillis = timeRepository.getCurrentTimeInMillis()
15 | )?.limitAmountMinutes ?: DEFAULT_SCREEN_TIME_LIMIT_MINUTES
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/use_case/screen_time_limits/GetScreenTimeLimitMinutesForTomorrowUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.use_case.screen_time_limits
2 |
3 | import com.sweak.unlockmaster.domain.repository.ScreenTimeLimitsRepository
4 | import com.sweak.unlockmaster.domain.repository.TimeRepository
5 | import javax.inject.Inject
6 |
7 | class GetScreenTimeLimitMinutesForTomorrowUseCase @Inject constructor(
8 | private val screenTimeLimitsRepository: ScreenTimeLimitsRepository,
9 | private val timeRepository: TimeRepository
10 | ) {
11 | suspend operator fun invoke(): Int? {
12 | return screenTimeLimitsRepository.getScreenTimeLimitWithApplianceTime(
13 | limitApplianceTimeInMillis = timeRepository.getTomorrowBeginningTimeInMillis()
14 | )?.limitAmountMinutes
15 | }
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/use_case/unlock_events/AddUnlockEventUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.use_case.unlock_events
2 |
3 | import com.sweak.unlockmaster.domain.model.UnlockMasterEvent
4 | import com.sweak.unlockmaster.domain.repository.TimeRepository
5 | import com.sweak.unlockmaster.domain.repository.UnlockEventsRepository
6 | import javax.inject.Inject
7 |
8 | class AddUnlockEventUseCase @Inject constructor(
9 | private val unlockEventsRepository: UnlockEventsRepository,
10 | private val timeRepository: TimeRepository
11 | ) {
12 | suspend operator fun invoke() {
13 | unlockEventsRepository.addUnlockEvent(
14 | unlockEvent = UnlockMasterEvent.UnlockEvent(
15 | unlockTimeInMillis = timeRepository.getCurrentTimeInMillis()
16 | )
17 | )
18 | }
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/use_case/unlock_events/GetAllTimeDaysToUnlockEventCountsUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.use_case.unlock_events
2 |
3 | import com.sweak.unlockmaster.domain.repository.TimeRepository
4 | import com.sweak.unlockmaster.domain.repository.UnlockEventsRepository
5 | import com.sweak.unlockmaster.domain.toTimeInMillis
6 | import java.time.Instant
7 | import java.time.ZoneId
8 | import java.time.ZonedDateTime
9 | import javax.inject.Inject
10 |
11 | class GetAllTimeDaysToUnlockEventCountsUseCase @Inject constructor(
12 | private val unlockEventsRepository: UnlockEventsRepository,
13 | private val timeRepository: TimeRepository
14 | ) {
15 | suspend operator fun invoke(): List> {
16 | val firstUnlockEvent = unlockEventsRepository.getFirstUnlockEvent()
17 | val allTimeUnlockEvents = unlockEventsRepository.getUnlockEventsSinceTime(
18 | sinceTimeInMillis = firstUnlockEvent?.timeInMillis ?: 0
19 | )
20 |
21 | val dateToUnlockCountsMap = allTimeUnlockEvents.groupingBy {
22 | timeRepository.getBeginningOfGivenDayTimeInMillis(it.timeInMillis)
23 | }.eachCount()
24 |
25 | val allTimeDaysToUnlockEventCountsPairs = mutableListOf>()
26 |
27 | var daysCounter = 0
28 | var currentCountingDayDate = ZonedDateTime.ofInstant(
29 | Instant.ofEpochMilli(timeRepository.getTomorrowBeginningTimeInMillis()),
30 | ZoneId.systemDefault()
31 | ).minusDays(1)
32 | val firstUnlockEventDayDate =
33 | if (firstUnlockEvent != null) {
34 | ZonedDateTime.ofInstant(
35 | Instant.ofEpochMilli(
36 | timeRepository.getBeginningOfGivenDayTimeInMillis(
37 | firstUnlockEvent.timeInMillis
38 | )
39 | ),
40 | ZoneId.systemDefault()
41 | )
42 | } else {
43 | currentCountingDayDate
44 | }
45 |
46 | while (daysCounter < 7 || currentCountingDayDate >= firstUnlockEventDayDate) {
47 | allTimeDaysToUnlockEventCountsPairs.add(
48 | Pair(
49 | currentCountingDayDate.toTimeInMillis(),
50 | dateToUnlockCountsMap.getOrDefault(
51 | currentCountingDayDate.toTimeInMillis(),
52 | 0
53 | )
54 | )
55 | )
56 |
57 | daysCounter += 1
58 | currentCountingDayDate = currentCountingDayDate.minusDays(1)
59 | }
60 |
61 | return allTimeDaysToUnlockEventCountsPairs.reversed()
62 | }
63 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/use_case/unlock_events/GetLastWeekUnlockEventCountsUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.use_case.unlock_events
2 |
3 | import com.sweak.unlockmaster.domain.repository.TimeRepository
4 | import com.sweak.unlockmaster.domain.repository.UnlockEventsRepository
5 | import com.sweak.unlockmaster.domain.toTimeInMillis
6 | import java.time.Instant
7 | import java.time.ZoneId
8 | import java.time.ZonedDateTime
9 | import javax.inject.Inject
10 |
11 | class GetLastWeekUnlockEventCountsUseCase @Inject constructor(
12 | private val unlockEventsRepository: UnlockEventsRepository,
13 | private val timeRepository: TimeRepository
14 | ) {
15 | suspend operator fun invoke(): List {
16 | val sixDaysBeforeDayBeginningTimeInMillis =
17 | timeRepository.getSixDaysBeforeDayBeginningTimeInMillis()
18 | val lastWeekUnlockEvents = unlockEventsRepository.getUnlockEventsSinceTime(
19 | sinceTimeInMillis = sixDaysBeforeDayBeginningTimeInMillis
20 | )
21 |
22 | val dateToUnlockCountsMap = lastWeekUnlockEvents.groupingBy {
23 | timeRepository.getBeginningOfGivenDayTimeInMillis(it.timeInMillis)
24 | }.eachCount()
25 |
26 | val lastWeekUnlockEventCountsList = mutableListOf()
27 | var sixDaysBeforeDayBeginningDate = ZonedDateTime.ofInstant(
28 | Instant.ofEpochMilli(sixDaysBeforeDayBeginningTimeInMillis),
29 | ZoneId.systemDefault()
30 | )
31 |
32 | repeat(7) {
33 | lastWeekUnlockEventCountsList.add(
34 | dateToUnlockCountsMap.getOrDefault(
35 | sixDaysBeforeDayBeginningDate.toTimeInMillis(),
36 | 0
37 | )
38 | )
39 | sixDaysBeforeDayBeginningDate = sixDaysBeforeDayBeginningDate.plusDays(1)
40 | }
41 |
42 | return lastWeekUnlockEventCountsList
43 | }
44 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/use_case/unlock_events/GetUnlockEventsCountForGivenDayUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.use_case.unlock_events
2 |
3 | import com.sweak.unlockmaster.domain.repository.TimeRepository
4 | import com.sweak.unlockmaster.domain.repository.UnlockEventsRepository
5 | import com.sweak.unlockmaster.domain.toTimeInMillis
6 | import java.time.Instant
7 | import java.time.ZoneId
8 | import java.time.ZonedDateTime
9 | import javax.inject.Inject
10 |
11 | class GetUnlockEventsCountForGivenDayUseCase @Inject constructor(
12 | private val unlockEventsRepository: UnlockEventsRepository,
13 | private val timeRepository: TimeRepository
14 | ) {
15 | suspend operator fun invoke(
16 | dayTimeInMillis: Long = timeRepository.getCurrentTimeInMillis()
17 | ): Int {
18 | val dayBeginningDateTime = ZonedDateTime.ofInstant(
19 | Instant.ofEpochMilli(
20 | timeRepository.getBeginningOfGivenDayTimeInMillis(dayTimeInMillis)
21 | ),
22 | ZoneId.systemDefault()
23 | )
24 | val dayEndingDateTime = dayBeginningDateTime.plusDays(1)
25 |
26 | return unlockEventsRepository.getUnlockEventsSinceTimeAndUntilTime(
27 | sinceTimeInMillis = dayBeginningDateTime.toTimeInMillis(),
28 | untilTimeInMillis = dayEndingDateTime.toTimeInMillis()
29 | ).size
30 | }
31 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/use_case/unlock_limits/AddOrUpdateUnlockLimitForTodayUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.use_case.unlock_limits
2 |
3 | import com.sweak.unlockmaster.domain.model.UnlockLimit
4 | import com.sweak.unlockmaster.domain.repository.TimeRepository
5 | import com.sweak.unlockmaster.domain.repository.UnlockLimitsRepository
6 | import javax.inject.Inject
7 |
8 | class AddOrUpdateUnlockLimitForTodayUseCase @Inject constructor(
9 | private val unlockLimitsRepository: UnlockLimitsRepository,
10 | private val timeRepository: TimeRepository
11 | ) {
12 | suspend operator fun invoke(limitAmount: Int) {
13 | val latestUnlockLimit = unlockLimitsRepository.getUnlockLimitActiveAtTime(
14 | timeInMillis = timeRepository.getCurrentTimeInMillis()
15 | )
16 | val todayBeginningTimeInMillis = timeRepository.getTodayBeginningTimeInMillis()
17 | val newUnlockLimit = UnlockLimit(
18 | limitApplianceTimeInMillis = todayBeginningTimeInMillis,
19 | limitAmount = limitAmount
20 | )
21 |
22 | if (latestUnlockLimit == null) {
23 | unlockLimitsRepository.addUnlockLimit(unlockLimit = newUnlockLimit)
24 | } else {
25 | if (latestUnlockLimit.limitApplianceTimeInMillis < todayBeginningTimeInMillis) {
26 | unlockLimitsRepository.addUnlockLimit(unlockLimit = newUnlockLimit)
27 | } else {
28 | unlockLimitsRepository.updateUnlockLimit(unlockLimit = newUnlockLimit)
29 | }
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/use_case/unlock_limits/AddOrUpdateUnlockLimitForTomorrowUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.use_case.unlock_limits
2 |
3 | import com.sweak.unlockmaster.domain.model.UnlockLimit
4 | import com.sweak.unlockmaster.domain.repository.TimeRepository
5 | import com.sweak.unlockmaster.domain.repository.UnlockLimitsRepository
6 | import javax.inject.Inject
7 |
8 | class AddOrUpdateUnlockLimitForTomorrowUseCase @Inject constructor(
9 | private val unlockLimitsRepository: UnlockLimitsRepository,
10 | private val timeRepository: TimeRepository
11 | ) {
12 | suspend operator fun invoke(limitAmount: Int) {
13 | val tomorrowBeginningTimeInMillis = timeRepository.getTomorrowBeginningTimeInMillis()
14 | val currentUnlockLimit = unlockLimitsRepository.getUnlockLimitActiveAtTime(
15 | timeInMillis = timeRepository.getCurrentTimeInMillis()
16 | )
17 | val unlockLimitForTomorrow = unlockLimitsRepository.getUnlockLimitWithApplianceTime(
18 | limitApplianceTimeInMillis = tomorrowBeginningTimeInMillis
19 | )
20 | val newUnlockLimit = UnlockLimit(
21 | limitApplianceTimeInMillis = tomorrowBeginningTimeInMillis,
22 | limitAmount = limitAmount
23 | )
24 |
25 | if (unlockLimitForTomorrow == null) {
26 | if (currentUnlockLimit?.limitAmount == limitAmount) {
27 | return
28 | }
29 |
30 | unlockLimitsRepository.addUnlockLimit(unlockLimit = newUnlockLimit)
31 | } else {
32 | if (currentUnlockLimit?.limitAmount == limitAmount) {
33 | unlockLimitsRepository.deleteUnlockLimitWithApplianceTime(
34 | limitApplianceTimeInMillis = tomorrowBeginningTimeInMillis
35 | )
36 | return
37 | }
38 |
39 | unlockLimitsRepository.updateUnlockLimit(unlockLimit = newUnlockLimit)
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/use_case/unlock_limits/DeleteUnlockLimitForTomorrowUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.use_case.unlock_limits
2 |
3 | import com.sweak.unlockmaster.domain.repository.TimeRepository
4 | import com.sweak.unlockmaster.domain.repository.UnlockLimitsRepository
5 | import javax.inject.Inject
6 |
7 | class DeleteUnlockLimitForTomorrowUseCase @Inject constructor(
8 | private val unlockLimitsRepository: UnlockLimitsRepository,
9 | private val timeRepository: TimeRepository
10 | ) {
11 | suspend operator fun invoke() {
12 | unlockLimitsRepository.deleteUnlockLimitWithApplianceTime(
13 | limitApplianceTimeInMillis = timeRepository.getTomorrowBeginningTimeInMillis()
14 | )
15 | }
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/use_case/unlock_limits/GetUnlockLimitAmountForGivenDayUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.use_case.unlock_limits
2 |
3 | import com.sweak.unlockmaster.domain.DEFAULT_UNLOCK_LIMIT
4 | import com.sweak.unlockmaster.domain.repository.UnlockLimitsRepository
5 | import javax.inject.Inject
6 |
7 | class GetUnlockLimitAmountForGivenDayUseCase @Inject constructor(
8 | private val unlockLimitsRepository: UnlockLimitsRepository
9 | ) {
10 | suspend operator fun invoke(dayTimeInMillis: Long): Int {
11 | return unlockLimitsRepository.getUnlockLimitActiveAtTime(
12 | timeInMillis = dayTimeInMillis
13 | )?.limitAmount ?: DEFAULT_UNLOCK_LIMIT
14 | }
15 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/use_case/unlock_limits/GetUnlockLimitAmountForTodayUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.use_case.unlock_limits
2 |
3 | import com.sweak.unlockmaster.domain.DEFAULT_UNLOCK_LIMIT
4 | import com.sweak.unlockmaster.domain.repository.TimeRepository
5 | import com.sweak.unlockmaster.domain.repository.UnlockLimitsRepository
6 | import javax.inject.Inject
7 |
8 | class GetUnlockLimitAmountForTodayUseCase @Inject constructor(
9 | private val unlockLimitsRepository: UnlockLimitsRepository,
10 | private val timeRepository: TimeRepository
11 | ) {
12 | suspend operator fun invoke(): Int {
13 | return unlockLimitsRepository.getUnlockLimitActiveAtTime(
14 | timeInMillis = timeRepository.getCurrentTimeInMillis()
15 | )?.limitAmount ?: DEFAULT_UNLOCK_LIMIT
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/use_case/unlock_limits/GetUnlockLimitAmountForTomorrowUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.use_case.unlock_limits
2 |
3 | import com.sweak.unlockmaster.domain.repository.TimeRepository
4 | import com.sweak.unlockmaster.domain.repository.UnlockLimitsRepository
5 | import javax.inject.Inject
6 |
7 | class GetUnlockLimitAmountForTomorrowUseCase @Inject constructor(
8 | private val unlockLimitsRepository: UnlockLimitsRepository,
9 | private val timeRepository: TimeRepository
10 | ) {
11 | suspend operator fun invoke(): Int? {
12 | return unlockLimitsRepository.getUnlockLimitWithApplianceTime(
13 | limitApplianceTimeInMillis = timeRepository.getTomorrowBeginningTimeInMillis()
14 | )?.limitAmount
15 | }
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/domain/use_case/unlock_limits/GetUnlockLimitApplianceDayForGivenDayUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.domain.use_case.unlock_limits
2 |
3 | import com.sweak.unlockmaster.domain.repository.UnlockLimitsRepository
4 | import javax.inject.Inject
5 |
6 | class GetUnlockLimitApplianceDayForGivenDayUseCase @Inject constructor(
7 | private val unlockLimitsRepository: UnlockLimitsRepository
8 | ) {
9 | suspend operator fun invoke(dayTimeInMillis: Long): Long? {
10 | return unlockLimitsRepository.getUnlockLimitActiveAtTime(
11 | timeInMillis = dayTimeInMillis
12 | )?.limitApplianceTimeInMillis
13 | }
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/background_work/Constants.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.background_work
2 |
3 | const val FOREGROUND_SERVICE_ID = 300
4 | const val FOREGROUND_SERVICE_NOTIFICATION_ID = 300
5 | const val FOREGROUND_SERVICE_NOTIFICATION_REQUEST_CODE = 400
6 | const val FOREGROUND_SERVICE_NOTIFICATION_CHANNEL_ID =
7 | "UnlockMasterForegroundServiceNotificationChannelId"
8 |
9 | const val MOBILIZING_NOTIFICATION_CHANNEL_ID = "UnlockMasterMobilizingNotificationChannelId"
10 | const val UNLOCK_LIMIT_MOBILIZING_NOTIFICATION_ID = 500
11 | const val UNLOCK_LIMIT_MOBILIZING_NOTIFICATION_REQUEST_CODE = 600
12 | const val SCREEN_TIME_LIMIT_MOBILIZING_NOTIFICATION_ID = 900
13 | const val SCREEN_TIME_LIMIT_MOBILIZING_NOTIFICATION_REQUEST_CODE = 1000
14 |
15 | const val DAILY_WRAP_UP_NOTIFICATION_REQUEST_CODE = 700
16 | const val DAILY_WRAP_UP_NOTIFICATION_ID = 800
17 | const val DAILY_WRAP_UPS_NOTIFICATIONS_CHANNEL_ID = "dailyWrapUpNotificationChannelId"
18 |
19 | const val ACTION_UNLOCK_COUNTER_PAUSE_CHANGED =
20 | "com.sweak.unlockmaster.UNLOCK_COUNTER_PAUSE_CHANGED"
21 | const val EXTRA_IS_UNLOCK_COUNTER_PAUSED = "com.sweak.unlockmaster.EXTRA_IS_UNLOCK_COUNTER_PAUSED"
22 |
23 | const val ACTION_SCREEN_TIME_LIMIT_STATE_CHANGED =
24 | "com.sweak.unlockmaster.SCREEN_TIME_LIMIT_STATE_CHANGED"
25 | const val EXTRA_IS_SCREEN_TIME_LIMIT_ENABLED =
26 | "com.sweak.unlockmaster.EXTRA_IS_SCREEN_TIME_LIMIT_ENABLED"
27 |
28 | const val EXTRA_SHOW_DAILY_WRAP_UP_SCREEN = "com.sweak.unlockmaster.EXTRA_SHOW_DAILY_WRAP_UP_SCREEN"
29 | const val EXTRA_DAILY_WRAP_UP_DAY_MILLIS = "com.sweak.unlockmaster.EXTRA_DAILY_WRAP_UP_DAY_MILLIS"
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/background_work/global_receivers/ApplicationUpdatedReceiver.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.background_work.global_receivers
2 |
3 | import android.content.BroadcastReceiver
4 | import android.content.Context
5 | import android.content.Intent
6 | import com.sweak.unlockmaster.domain.repository.UserSessionRepository
7 | import dagger.hilt.android.AndroidEntryPoint
8 | import kotlinx.coroutines.runBlocking
9 | import javax.inject.Inject
10 |
11 | @AndroidEntryPoint
12 | class ApplicationUpdatedReceiver : BroadcastReceiver() {
13 |
14 | @Inject
15 | lateinit var userSessionRepository: UserSessionRepository
16 |
17 | override fun onReceive(context: Context, intent: Intent) {
18 | if (intent.action == Intent.ACTION_MY_PACKAGE_REPLACED) {
19 | runBlocking {
20 | if (userSessionRepository.isIntroductionFinished()) {
21 | userSessionRepository.setShouldShowUnlockMasterBlockedWarning(false)
22 | }
23 | }
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/background_work/global_receivers/BootReceiver.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.background_work.global_receivers
2 |
3 | import android.app.ActivityManager
4 | import android.app.KeyguardManager
5 | import android.content.BroadcastReceiver
6 | import android.content.Context
7 | import android.content.Intent
8 | import android.os.Build
9 | import com.sweak.unlockmaster.domain.repository.UserSessionRepository
10 | import com.sweak.unlockmaster.domain.use_case.daily_wrap_up.ScheduleDailyWrapUpNotificationsUseCase
11 | import com.sweak.unlockmaster.domain.use_case.screen_on_events.AddScreenOnEventUseCase
12 | import com.sweak.unlockmaster.domain.use_case.unlock_events.AddUnlockEventUseCase
13 | import dagger.hilt.android.AndroidEntryPoint
14 | import kotlinx.coroutines.runBlocking
15 | import javax.inject.Inject
16 |
17 | @AndroidEntryPoint
18 | class BootReceiver : BroadcastReceiver() {
19 |
20 | @Inject
21 | lateinit var userSessionRepository: UserSessionRepository
22 |
23 | @Inject
24 | lateinit var addUnlockEventUseCase: AddUnlockEventUseCase
25 |
26 | @Inject
27 | lateinit var addScreenOnEventUseCase: AddScreenOnEventUseCase
28 |
29 | @Inject
30 | lateinit var scheduleDailyWrapUpNotificationsUseCase: ScheduleDailyWrapUpNotificationsUseCase
31 |
32 | @Inject
33 | lateinit var keyguardManager: KeyguardManager
34 |
35 | private val intentActionsToFilter = listOf(
36 | "android.intent.action.BOOT_COMPLETED",
37 | "android.intent.action.ACTION_BOOT_COMPLETED",
38 | "android.intent.action.REBOOT",
39 | "android.intent.action.QUICKBOOT_POWERON",
40 | "com.htc.intent.action.QUICKBOOT_POWERON"
41 | )
42 |
43 | override fun onReceive(context: Context, intent: Intent) {
44 | if (intent.action in intentActionsToFilter) {
45 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
46 | val activityManager =
47 | context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
48 |
49 | activityManager.getHistoricalProcessStartReasons(1).firstOrNull()?.let {
50 | // On Android 15+ ACTION_BOOT_COMPLETED is delivered on post-force-close launch.
51 | // We detect that and do not act as if an actual boot happened and just return:
52 | if (it.wasForceStopped()) return
53 | }
54 | }
55 |
56 | runBlocking {
57 | if (userSessionRepository.isIntroductionFinished() &&
58 | !keyguardManager.isKeyguardLocked &&
59 | !userSessionRepository.isUnlockCounterPaused()
60 | ) {
61 | addScreenOnEventUseCase()
62 | addUnlockEventUseCase()
63 | scheduleDailyWrapUpNotificationsUseCase()
64 | userSessionRepository.setShouldShowUnlockMasterBlockedWarning(false)
65 | }
66 | }
67 | }
68 | }
69 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/background_work/global_receivers/ShutdownReceiver.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.background_work.global_receivers
2 |
3 | import android.app.KeyguardManager
4 | import android.content.BroadcastReceiver
5 | import android.content.Context
6 | import android.content.Intent
7 | import com.sweak.unlockmaster.domain.repository.UserSessionRepository
8 | import com.sweak.unlockmaster.domain.use_case.lock_events.AddLockEventUseCase
9 | import dagger.hilt.android.AndroidEntryPoint
10 | import kotlinx.coroutines.runBlocking
11 | import javax.inject.Inject
12 |
13 | @AndroidEntryPoint
14 | class ShutdownReceiver : BroadcastReceiver() {
15 |
16 | @Inject
17 | lateinit var userSessionRepository: UserSessionRepository
18 |
19 | @Inject
20 | lateinit var addLockEventUseCase: AddLockEventUseCase
21 |
22 | @Inject
23 | lateinit var keyguardManager: KeyguardManager
24 |
25 | private val intentActionsToFilter = listOf(
26 | "android.intent.action.ACTION_SHUTDOWN",
27 | "android.intent.action.QUICKBOOT_POWEROFF"
28 | )
29 |
30 | override fun onReceive(context: Context, intent: Intent) {
31 | if (intent.action in intentActionsToFilter) {
32 | runBlocking {
33 | if (userSessionRepository.isIntroductionFinished() &&
34 | !keyguardManager.isKeyguardLocked &&
35 | !userSessionRepository.isUnlockCounterPaused()
36 | ) {
37 | addLockEventUseCase()
38 | }
39 | }
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/background_work/global_receivers/TimePreferencesChangeReceiver.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.background_work.global_receivers
2 |
3 | import android.content.BroadcastReceiver
4 | import android.content.Context
5 | import android.content.Intent
6 | import com.sweak.unlockmaster.domain.repository.UserSessionRepository
7 | import com.sweak.unlockmaster.domain.use_case.daily_wrap_up.ScheduleDailyWrapUpNotificationsUseCase
8 | import dagger.hilt.android.AndroidEntryPoint
9 | import kotlinx.coroutines.runBlocking
10 | import javax.inject.Inject
11 |
12 | @AndroidEntryPoint
13 | class TimePreferencesChangeReceiver : BroadcastReceiver() {
14 |
15 | @Inject
16 | lateinit var scheduleDailyWrapUpNotificationsUseCase: ScheduleDailyWrapUpNotificationsUseCase
17 |
18 | @Inject
19 | lateinit var userSessionRepository: UserSessionRepository
20 |
21 | private val intentActionsToFilter = listOf(
22 | Intent.ACTION_TIME_CHANGED,
23 | Intent.ACTION_DATE_CHANGED,
24 | Intent.ACTION_TIMEZONE_CHANGED
25 | )
26 |
27 | override fun onReceive(context: Context, intent: Intent) {
28 | if (intent.action in intentActionsToFilter) {
29 | runBlocking {
30 | if (userSessionRepository.isIntroductionFinished()) {
31 | scheduleDailyWrapUpNotificationsUseCase()
32 | }
33 | }
34 | }
35 | }
36 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/background_work/global_receivers/screen_event_receivers/ScreenLockReceiver.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.background_work.global_receivers.screen_event_receivers
2 |
3 | import android.content.BroadcastReceiver
4 | import android.content.Context
5 | import android.content.Intent
6 |
7 | class ScreenLockReceiver : BroadcastReceiver() {
8 |
9 | var onScreenLock: (() -> Unit)? = null
10 |
11 | override fun onReceive(context: Context?, intent: Intent?) {
12 | if (intent?.action.equals(Intent.ACTION_SCREEN_OFF)) {
13 | onScreenLock?.invoke()
14 | }
15 | }
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/background_work/global_receivers/screen_event_receivers/ScreenOnReceiver.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.background_work.global_receivers.screen_event_receivers
2 |
3 | import android.content.BroadcastReceiver
4 | import android.content.Context
5 | import android.content.Intent
6 |
7 | class ScreenOnReceiver : BroadcastReceiver() {
8 |
9 | var onScreenOn: (() -> Unit)? = null
10 |
11 | override fun onReceive(context: Context?, intent: Intent?) {
12 | if (intent?.action.equals(Intent.ACTION_SCREEN_ON)) {
13 | onScreenOn?.invoke()
14 | }
15 | }
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/background_work/global_receivers/screen_event_receivers/ScreenUnlockReceiver.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.background_work.global_receivers.screen_event_receivers
2 |
3 | import android.content.BroadcastReceiver
4 | import android.content.Context
5 | import android.content.Intent
6 |
7 | class ScreenUnlockReceiver : BroadcastReceiver() {
8 |
9 | var onScreenUnlock: (() -> Unit)? = null
10 |
11 | override fun onReceive(context: Context?, intent: Intent?) {
12 | if (intent?.action.equals(Intent.ACTION_USER_PRESENT)) {
13 | onScreenUnlock?.invoke()
14 | }
15 | }
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/background_work/local_receivers/ScreenTimeLimitStateReceiver.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.background_work.local_receivers
2 |
3 | import android.content.BroadcastReceiver
4 | import android.content.Context
5 | import android.content.Intent
6 | import com.sweak.unlockmaster.presentation.background_work.ACTION_SCREEN_TIME_LIMIT_STATE_CHANGED
7 | import com.sweak.unlockmaster.presentation.background_work.EXTRA_IS_SCREEN_TIME_LIMIT_ENABLED
8 |
9 | class ScreenTimeLimitStateReceiver : BroadcastReceiver() {
10 |
11 | var onScreenTimeLimitStateChanged: ((isEnabled: Boolean) -> Unit)? = null
12 |
13 | override fun onReceive(context: Context?, intent: Intent?) {
14 | intent?.let {
15 | if (intent.action == ACTION_SCREEN_TIME_LIMIT_STATE_CHANGED) {
16 | val isScreenTimeLimitEnabled = intent.getBooleanExtra(
17 | EXTRA_IS_SCREEN_TIME_LIMIT_ENABLED,
18 | true
19 | )
20 |
21 | onScreenTimeLimitStateChanged?.invoke(isScreenTimeLimitEnabled)
22 | }
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/background_work/local_receivers/UnlockCounterPauseReceiver.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.background_work.local_receivers
2 |
3 | import android.content.BroadcastReceiver
4 | import android.content.Context
5 | import android.content.Intent
6 | import com.sweak.unlockmaster.presentation.background_work.ACTION_UNLOCK_COUNTER_PAUSE_CHANGED
7 | import com.sweak.unlockmaster.presentation.background_work.EXTRA_IS_UNLOCK_COUNTER_PAUSED
8 |
9 | class UnlockCounterPauseReceiver : BroadcastReceiver() {
10 |
11 | var onCounterPauseChanged: ((isPaused: Boolean) -> Unit)? = null
12 |
13 | override fun onReceive(context: Context?, intent: Intent?) {
14 | intent?.let {
15 | if (intent.action == ACTION_UNLOCK_COUNTER_PAUSE_CHANGED) {
16 | val isUnlockCounterPaused = intent.getBooleanExtra(
17 | EXTRA_IS_UNLOCK_COUNTER_PAUSED,
18 | false
19 | )
20 |
21 | onCounterPauseChanged?.invoke(isUnlockCounterPaused)
22 | }
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/common/Screen.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.common
2 |
3 | sealed class Screen(val route: String) {
4 | data object WelcomeScreen : Screen("welcome_screen")
5 | data object IntroductionScreen : Screen("introduction_screen")
6 | data object UnlockLimitSetupScreen : Screen("unlock_limit_setup_screen")
7 | data object ScreenTimeLimitSetupScreen : Screen("screen_time_limit_setup_screen")
8 | data object WorkInBackgroundScreen : Screen("work_in_background_screen")
9 | data object SetupCompleteScreen : Screen("setup_complete_screen")
10 | data object HomeScreen : Screen("home_screen")
11 | data object SettingsScreen : Screen("settings_screen")
12 | data object ScreenTimeScreen : Screen("screen_time_screen")
13 | data object StatisticsScreen : Screen("statistics_screen")
14 | data object MobilizingNotificationsScreen : Screen("mobilizing_notifications_screen")
15 | data object DailyWrapUpSettingsScreen : Screen("daily_wrap_ups_setting_screen")
16 | data object DailyWrapUpScreen : Screen("daily_wrap_up_screen")
17 | data object ApplicationBlockedScreen : Screen("application_blocked_screen")
18 | data object UserInterfaceThemeScreen : Screen("user_interface_theme_screen")
19 | data object DataBackupScreen : Screen("data_backup_screen")
20 |
21 | fun withArguments(vararg arguments: String): String {
22 | return buildString {
23 | append(route)
24 | arguments.forEach { argument ->
25 | append("/$argument")
26 | }
27 | }
28 | }
29 |
30 | companion object {
31 | const val KEY_IS_UPDATING_EXISTING_UNLOCK_LIMIT = "isUpdatingExistingUnlockLimit"
32 | const val KEY_IS_UPDATING_EXISTING_SCREEN_TIME_LIMIT = "isUpdatingExistingScreenTimeLimit"
33 | const val KEY_DISPLAYED_SCREEN_TIME_DAY_MILLIS = "displayedScreenTimeDayMillis"
34 | const val KEY_IS_LAUNCHED_FROM_SETTINGS = "isLaunchedFromSettings"
35 | const val KEY_DAILY_WRAP_UP_DAY_MILLIS = "dailyWrapUpDayMillis"
36 | }
37 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/common/components/InformationCard.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.common.components
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.Spacer
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.foundation.layout.size
8 | import androidx.compose.foundation.layout.width
9 | import androidx.compose.material3.Card
10 | import androidx.compose.material3.CardDefaults
11 | import androidx.compose.material3.Icon
12 | import androidx.compose.material3.MaterialTheme
13 | import androidx.compose.material3.Text
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.ui.Alignment
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.graphics.vector.ImageVector
18 | import androidx.compose.ui.text.font.FontWeight
19 | import com.sweak.unlockmaster.presentation.common.theme.space
20 |
21 | @Composable
22 | fun InformationCard(
23 | title: String,
24 | description: String,
25 | icon: ImageVector,
26 | iconContentDescription: String,
27 | modifier: Modifier = Modifier
28 | ) {
29 | Card(
30 | modifier = modifier,
31 | colors = CardDefaults.cardColors(
32 | containerColor = MaterialTheme.colorScheme.surface
33 | )
34 | ) {
35 | Row(
36 | verticalAlignment = Alignment.CenterVertically
37 | ) {
38 | Spacer(modifier = Modifier.width(MaterialTheme.space.medium))
39 |
40 | Icon(
41 | imageVector = icon,
42 | contentDescription = iconContentDescription,
43 | modifier = Modifier.size(size = MaterialTheme.space.xLarge)
44 | )
45 |
46 | Column(
47 | modifier = Modifier.padding(all = MaterialTheme.space.medium)
48 | ) {
49 | Text(
50 | text = title,
51 | style = MaterialTheme.typography.headlineMedium.copy(
52 | fontWeight = FontWeight.SemiBold
53 | ),
54 | modifier = Modifier.padding(bottom = MaterialTheme.space.xSmall)
55 | )
56 |
57 | Text(
58 | text = description,
59 | style = MaterialTheme.typography.titleSmall
60 | )
61 | }
62 | }
63 | }
64 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/common/components/NavigationBar.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.common.components
2 |
3 | import androidx.compose.material.icons.Icons
4 | import androidx.compose.material.icons.automirrored.outlined.ArrowBackIos
5 | import androidx.compose.material3.CenterAlignedTopAppBar
6 | import androidx.compose.material3.ExperimentalMaterial3Api
7 | import androidx.compose.material3.Icon
8 | import androidx.compose.material3.IconButton
9 | import androidx.compose.material3.MaterialTheme
10 | import androidx.compose.material3.Text
11 | import androidx.compose.material3.TopAppBarDefaults
12 | import androidx.compose.material3.TopAppBarScrollBehavior
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.res.stringResource
16 | import com.sweak.unlockmaster.R
17 |
18 | @OptIn(ExperimentalMaterial3Api::class)
19 | @Composable
20 | fun NavigationBar(
21 | title: String,
22 | onNavigationButtonClick: () -> Unit,
23 | modifier: Modifier = Modifier,
24 | scrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
25 | ) {
26 | CenterAlignedTopAppBar(
27 | colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
28 | containerColor = MaterialTheme.colorScheme.background,
29 | titleContentColor = MaterialTheme.colorScheme.onBackground,
30 | navigationIconContentColor = MaterialTheme.colorScheme.onBackground
31 | ),
32 | title = {
33 | Text(
34 | text = title,
35 | style = MaterialTheme.typography.displayMedium
36 | )
37 | },
38 | navigationIcon = {
39 | IconButton(onClick = onNavigationButtonClick) {
40 | Icon(
41 | imageVector = Icons.AutoMirrored.Outlined.ArrowBackIos,
42 | contentDescription = stringResource(R.string.content_description_back_icon)
43 | )
44 | }
45 | },
46 | scrollBehavior = scrollBehavior,
47 | modifier = modifier
48 | )
49 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/common/components/ObserveAsEvents.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.common.components
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.LaunchedEffect
5 | import androidx.lifecycle.compose.LocalLifecycleOwner
6 | import androidx.lifecycle.Lifecycle
7 | import androidx.lifecycle.repeatOnLifecycle
8 | import kotlinx.coroutines.Dispatchers
9 | import kotlinx.coroutines.flow.Flow
10 | import kotlinx.coroutines.withContext
11 |
12 | @Composable
13 | fun ObserveAsEvents(flow: Flow, onEvent: (T) -> Unit) {
14 | val lifecycleOwner = LocalLifecycleOwner.current
15 |
16 | // More information on this approach: https://www.youtube.com/watch?v=njchj9d_Lf8
17 | LaunchedEffect(key1 = flow, key2 = lifecycleOwner.lifecycle) {
18 | // Collect the flow when the lifecycle of calling component is at least STARTED:
19 | lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
20 | // using Dispatchers.Main.immediate will ensure that no events will be missed
21 | // when e.g. there happens to be a configuration change:
22 | withContext(Dispatchers.Main.immediate) {
23 | flow.collect(onEvent)
24 | }
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/common/components/OnResume.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.common.components
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.DisposableEffect
5 | import androidx.lifecycle.Lifecycle
6 | import androidx.lifecycle.LifecycleEventObserver
7 | import androidx.lifecycle.LifecycleOwner
8 | import androidx.lifecycle.compose.LocalLifecycleOwner
9 |
10 | @Composable
11 | fun OnResume(
12 | lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
13 | onResumeCallback: () -> Unit
14 | ) {
15 | DisposableEffect(key1 = lifecycleOwner) {
16 | val observer = LifecycleEventObserver { _, event ->
17 | if (event == Lifecycle.Event.ON_RESUME) {
18 | onResumeCallback()
19 | }
20 | }
21 |
22 | lifecycleOwner.lifecycle.addObserver(observer)
23 |
24 | onDispose {
25 | lifecycleOwner.lifecycle.removeObserver(observer)
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/common/components/ProceedButton.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.common.components
2 |
3 | import androidx.compose.foundation.layout.fillMaxWidth
4 | import androidx.compose.foundation.layout.height
5 | import androidx.compose.material3.ButtonDefaults
6 | import androidx.compose.material3.ElevatedButton
7 | import androidx.compose.material3.MaterialTheme
8 | import androidx.compose.material3.Text
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Modifier
11 | import com.sweak.unlockmaster.presentation.common.theme.space
12 |
13 | @Composable
14 | fun ProceedButton(
15 | text: String,
16 | onClick: () -> Unit,
17 | modifier: Modifier = Modifier,
18 | enabled: Boolean = true
19 | ) {
20 | ElevatedButton(
21 | onClick = onClick,
22 | enabled = enabled,
23 | colors = ButtonDefaults.elevatedButtonColors(
24 | containerColor = MaterialTheme.colorScheme.primary,
25 | contentColor = MaterialTheme.colorScheme.onPrimary
26 | ),
27 | modifier = modifier
28 | .fillMaxWidth()
29 | .height(MaterialTheme.space.xLarge)
30 | ) {
31 | Text(text = text)
32 | }
33 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/common/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.common.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Riptide = Color(0xFF7BEFB2)
6 | val OceanGreen = Color(0xFF45BC82)
7 | val DeepSea = Color(0xFF00905A)
8 | val Lochmara = Color(0xFF0077C2)
9 | val Porcelain = Color(0xFFF3F4F5)
10 | val Alto = Color(0xFFD8D8D8)
11 | val MineShaft = Color(0xFF343434)
12 | val GrayAsparagus = Color(0xFF4D524D)
13 | val Monza = Color(0xFFB00020)
14 | val AlizarinCrimson = Color(0xFFDA2042)
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/common/theme/Shape.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.common.theme
2 |
3 | import androidx.compose.foundation.shape.RoundedCornerShape
4 | import androidx.compose.material3.Shapes
5 | import androidx.compose.ui.unit.dp
6 |
7 | val Shapes = Shapes(
8 | small = RoundedCornerShape(8.dp),
9 | medium = RoundedCornerShape(4.dp),
10 | large = RoundedCornerShape(0.dp)
11 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/common/theme/Space.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.common.theme
2 |
3 | import androidx.compose.material3.MaterialTheme
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.ReadOnlyComposable
6 | import androidx.compose.runtime.compositionLocalOf
7 | import androidx.compose.ui.unit.Dp
8 | import androidx.compose.ui.unit.dp
9 |
10 | data class Space(
11 | val default: Dp = 0.dp,
12 | val xSmall: Dp = 4.dp,
13 | val small: Dp = 8.dp,
14 | val smallMedium: Dp = 12.dp,
15 | val medium: Dp = 16.dp,
16 | val mediumLarge: Dp = 24.dp,
17 | val large: Dp = 32.dp,
18 | val xLarge: Dp = 48.dp,
19 | val xxLarge: Dp = 64.dp,
20 | val xxxLarge: Dp = 128.dp,
21 | )
22 |
23 | val LocalSpace = compositionLocalOf { Space() }
24 |
25 | val MaterialTheme.space: Space
26 | @Composable
27 | @ReadOnlyComposable
28 | get() = LocalSpace.current
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/common/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.common.theme
2 |
3 | import androidx.compose.foundation.isSystemInDarkTheme
4 | import androidx.compose.material3.MaterialTheme
5 | import androidx.compose.material3.darkColorScheme
6 | import androidx.compose.material3.lightColorScheme
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.runtime.CompositionLocalProvider
9 | import androidx.compose.ui.graphics.Color
10 | import com.sweak.unlockmaster.domain.model.UiThemeMode
11 |
12 | private val lightColorPalette = lightColorScheme(
13 | primary = Riptide,
14 | onPrimary = Color.Black,
15 | secondary = OceanGreen,
16 | onSecondary = Color.Black,
17 | tertiary = Lochmara,
18 | onTertiary = Color.Black,
19 | background = Porcelain,
20 | onBackground = Color.Black,
21 | surface = Color.White,
22 | surfaceVariant = Color.White,
23 | onSurface = Color.Black,
24 | onSurfaceVariant = Color.Black,
25 | error = Monza,
26 | onError = Color.White
27 | )
28 |
29 | private val darkColorPalette = darkColorScheme(
30 | primary = DeepSea,
31 | onPrimary = Alto,
32 | secondary = OceanGreen,
33 | onSecondary = Color.Black,
34 | tertiary = Lochmara,
35 | onTertiary = Color.Black,
36 | background = MineShaft,
37 | onBackground = Alto,
38 | surface = GrayAsparagus,
39 | surfaceVariant = GrayAsparagus,
40 | onSurface = Alto,
41 | onSurfaceVariant = Alto,
42 | error = AlizarinCrimson,
43 | onError = Color.White
44 | )
45 |
46 | @Composable
47 | fun UnlockMasterTheme(
48 | uiThemeMode: UiThemeMode = UiThemeMode.SYSTEM,
49 | content: @Composable () -> Unit
50 | ) {
51 | CompositionLocalProvider(LocalSpace provides Space()) {
52 | val isDarkModeEnabled = when (uiThemeMode) {
53 | UiThemeMode.LIGHT -> false
54 | UiThemeMode.DARK -> true
55 | UiThemeMode.SYSTEM -> isSystemInDarkTheme()
56 | }
57 |
58 | val colorPalette = if (isDarkModeEnabled) darkColorPalette else lightColorPalette
59 |
60 | MaterialTheme(
61 | colorScheme = colorPalette,
62 | typography = Typography,
63 | shapes = Shapes,
64 | content = content
65 | )
66 | }
67 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/common/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.common.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.Font
6 | import androidx.compose.ui.text.font.FontFamily
7 | import androidx.compose.ui.text.font.FontWeight
8 | import androidx.compose.ui.unit.sp
9 | import com.sweak.unlockmaster.R
10 |
11 | val amikoFamily = FontFamily(
12 | Font(R.font.amiko_regular, FontWeight.Normal),
13 | Font(R.font.amiko_semibold, FontWeight.SemiBold),
14 | Font(R.font.amiko_bold, FontWeight.Bold)
15 | )
16 |
17 | val Typography = Typography(
18 | displayLarge = TextStyle(
19 | fontFamily = amikoFamily,
20 | fontWeight = FontWeight.Bold,
21 | fontSize = 20.sp
22 | ),
23 | displayMedium = TextStyle(
24 | fontFamily = amikoFamily,
25 | fontWeight = FontWeight.Bold,
26 | fontSize = 18.sp
27 | ),
28 | displaySmall = TextStyle(
29 | fontFamily = amikoFamily,
30 | fontWeight = FontWeight.SemiBold,
31 | fontSize = 16.sp
32 | ),
33 | headlineMedium = TextStyle(
34 | fontFamily = amikoFamily,
35 | fontWeight = FontWeight.Normal,
36 | fontSize = 14.sp
37 | ),
38 | titleMedium = TextStyle(
39 | fontFamily = amikoFamily,
40 | fontWeight = FontWeight.Normal,
41 | fontSize = 12.sp
42 | ),
43 | titleSmall = TextStyle(
44 | fontFamily = amikoFamily,
45 | fontWeight = FontWeight.Normal,
46 | fontSize = 10.sp
47 | ),
48 | labelLarge = TextStyle(
49 | fontFamily = amikoFamily,
50 | fontWeight = FontWeight.SemiBold,
51 | fontSize = 18.sp
52 | )
53 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/common/util/ThrottledNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.common.util
2 |
3 | import androidx.lifecycle.Lifecycle
4 | import androidx.lifecycle.LifecycleOwner
5 | import androidx.navigation.NavController
6 | import androidx.navigation.NavOptionsBuilder
7 |
8 | fun NavController.popBackStackThrottled(lifecycleOwner: LifecycleOwner) {
9 | val currentState = lifecycleOwner.lifecycle.currentState
10 |
11 | if (currentState.isAtLeast(Lifecycle.State.RESUMED)) {
12 | popBackStack()
13 | }
14 | }
15 |
16 | fun NavController.navigateThrottled(route: String, lifecycleOwner: LifecycleOwner) {
17 | val currentState = lifecycleOwner.lifecycle.currentState
18 |
19 | if (currentState.isAtLeast(Lifecycle.State.RESUMED)) {
20 | navigate(route)
21 | }
22 | }
23 |
24 | fun NavController.navigateThrottled(
25 | route: String,
26 | lifecycleOwner: LifecycleOwner,
27 | builder: NavOptionsBuilder.() -> Unit
28 | ) {
29 | val currentState = lifecycleOwner.lifecycle.currentState
30 |
31 | if (currentState.isAtLeast(Lifecycle.State.RESUMED)) {
32 | navigate(route, builder)
33 | }
34 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/daily_wrap_up/DailyWrapUpScreenEvent.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.daily_wrap_up
2 |
3 | sealed class DailyWrapUpScreenEvent {
4 | data class ScreenOnEventsInformationDialogVisible(val isVisible: Boolean) :
5 | DailyWrapUpScreenEvent()
6 |
7 | data object ApplySuggestedUnlockLimit : DailyWrapUpScreenEvent()
8 |
9 | data object ApplySuggestedScreenTimeLimit : DailyWrapUpScreenEvent()
10 | }
11 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/daily_wrap_up/DailyWrapUpScreenState.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.daily_wrap_up
2 |
3 | import com.sweak.unlockmaster.presentation.daily_wrap_up.components.DailyWrapUpCriterionPreviewType
4 | import com.sweak.unlockmaster.presentation.daily_wrap_up.components.DailyWrapUpScreenOnEventsDetailsData
5 | import com.sweak.unlockmaster.presentation.daily_wrap_up.components.DailyWrapUpScreenTimeDetailsData
6 | import com.sweak.unlockmaster.presentation.daily_wrap_up.components.DailyWrapUpScreenTimeLimitDetailsData
7 | import com.sweak.unlockmaster.presentation.daily_wrap_up.components.DailyWrapUpScreenUnlocksDetailsData
8 | import com.sweak.unlockmaster.presentation.daily_wrap_up.components.DailyWrapUpUnlockLimitDetailsData
9 |
10 | data class DailyWrapUpScreenState(
11 | val isInitializing: Boolean = true,
12 |
13 | val screenUnlocksPreviewData: DailyWrapUpCriterionPreviewType.ScreenUnlocks? = null,
14 | val screenTimePreviewData: DailyWrapUpCriterionPreviewType.ScreenTime? = null,
15 | val unlockLimitPreviewData: DailyWrapUpCriterionPreviewType.UnlockLimit? = null,
16 | val screenTimeLimitPreviewData: DailyWrapUpCriterionPreviewType.ScreenTimeLimit? = null,
17 | val screenOnEventsPreviewData: DailyWrapUpCriterionPreviewType.ScreenOnEvents? = null,
18 |
19 | val screenUnlocksDetailsData: DailyWrapUpScreenUnlocksDetailsData? = null,
20 | val screenTimeDetailsData: DailyWrapUpScreenTimeDetailsData? = null,
21 | val unlockLimitDetailsData: DailyWrapUpUnlockLimitDetailsData? = null,
22 | val screenTimeLimitDetailsData: DailyWrapUpScreenTimeLimitDetailsData? = null,
23 | val screenOnEventsDetailsData: DailyWrapUpScreenOnEventsDetailsData? = null,
24 |
25 | val isScreenOnEventsInformationDialogVisible: Boolean = false
26 | )
27 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/introduction/background_work/WorkInBackgroundScreenEvent.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.introduction.background_work
2 |
3 | sealed class WorkInBackgroundScreenEvent {
4 | data object CheckIfIgnoringBatteryOptimizations : WorkInBackgroundScreenEvent()
5 |
6 | data class IsIgnoreBatteryOptimizationsRequestUnavailableDialogVisible(
7 | val isVisible: Boolean
8 | ) : WorkInBackgroundScreenEvent()
9 |
10 | data object UserTriedToGrantNotificationsPermission : WorkInBackgroundScreenEvent()
11 |
12 | data class IsNotificationsPermissionDialogVisible(
13 | val isVisible: Boolean
14 | ) : WorkInBackgroundScreenEvent()
15 |
16 | data class IsWebBrowserNotFoundDialogVisible(
17 | val isVisible: Boolean
18 | ) : WorkInBackgroundScreenEvent()
19 | }
20 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/introduction/background_work/WorkInBackgroundScreenState.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.introduction.background_work
2 |
3 | data class WorkInBackgroundScreenState(
4 | val isIgnoringBatteryOptimizations: Boolean = false,
5 | val isIgnoreBatteryOptimizationsRequestUnavailable: Boolean = false,
6 | val isIgnoreBatteryOptimizationsRequestUnavailableDialogVisible: Boolean = false,
7 | val hasUserTriedToGrantNotificationsPermission: Boolean = false,
8 | val isNotificationsPermissionDialogVisible: Boolean = false,
9 | val isWebBrowserNotFoundDialogVisible: Boolean = false
10 | )
11 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/introduction/background_work/WorkInBackgroundViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.introduction.background_work
2 |
3 | import android.os.Build
4 | import android.os.PowerManager
5 | import androidx.compose.runtime.getValue
6 | import androidx.compose.runtime.mutableStateOf
7 | import androidx.compose.runtime.setValue
8 | import androidx.lifecycle.ViewModel
9 | import androidx.lifecycle.viewModelScope
10 | import dagger.hilt.android.lifecycle.HiltViewModel
11 | import kotlinx.coroutines.delay
12 | import kotlinx.coroutines.launch
13 | import javax.inject.Inject
14 | import javax.inject.Named
15 |
16 | @HiltViewModel
17 | class WorkInBackgroundViewModel @Inject constructor(
18 | private val powerManager: PowerManager,
19 | @Named("PackageName") private val packageName: String
20 | ) : ViewModel() {
21 |
22 | var state by mutableStateOf(WorkInBackgroundScreenState())
23 |
24 | init {
25 | state = state.copy(
26 | isIgnoringBatteryOptimizations =
27 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
28 | powerManager.isIgnoringBatteryOptimizations(packageName)
29 | } else true
30 | )
31 | }
32 |
33 | fun onEvent(event: WorkInBackgroundScreenEvent) {
34 | when (event) {
35 | is WorkInBackgroundScreenEvent.CheckIfIgnoringBatteryOptimizations ->
36 | viewModelScope.launch {
37 | // This delay is supposed to ensure that
38 | // powerManager.isIgnoringBatteryOptimizations returns an updated value - it can
39 | // take some time until it is updated on some systems.
40 | delay(1000)
41 |
42 | state = state.copy(
43 | isIgnoringBatteryOptimizations =
44 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
45 | powerManager.isIgnoringBatteryOptimizations(packageName)
46 | } else true
47 | )
48 | }
49 | is WorkInBackgroundScreenEvent.IsIgnoreBatteryOptimizationsRequestUnavailableDialogVisible ->
50 | state = state.copy(
51 | isIgnoreBatteryOptimizationsRequestUnavailable = true,
52 | isIgnoreBatteryOptimizationsRequestUnavailableDialogVisible = event.isVisible
53 | )
54 | is WorkInBackgroundScreenEvent.UserTriedToGrantNotificationsPermission ->
55 | state = state.copy(hasUserTriedToGrantNotificationsPermission = true)
56 | is WorkInBackgroundScreenEvent.IsNotificationsPermissionDialogVisible ->
57 | state = state.copy(isNotificationsPermissionDialogVisible = event.isVisible)
58 | is WorkInBackgroundScreenEvent.IsWebBrowserNotFoundDialogVisible ->
59 | state = state.copy(isWebBrowserNotFoundDialogVisible = event.isVisible)
60 | }
61 | }
62 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/introduction/components/UnlockLimitPickerSlider.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.introduction.components
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.material.icons.Icons
7 | import androidx.compose.material.icons.outlined.Add
8 | import androidx.compose.material.icons.outlined.Remove
9 | import androidx.compose.material3.Icon
10 | import androidx.compose.material3.IconButton
11 | import androidx.compose.material3.MaterialTheme
12 | import androidx.compose.material3.Slider
13 | import androidx.compose.material3.SliderDefaults
14 | import androidx.compose.material3.Text
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.ui.Alignment
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.res.stringResource
19 | import androidx.compose.ui.unit.sp
20 | import com.sweak.unlockmaster.R
21 | import com.sweak.unlockmaster.presentation.common.theme.space
22 | import kotlin.math.roundToInt
23 |
24 | @Composable
25 | fun UnlockLimitPickerSlider(
26 | pickedLimit: Int,
27 | limitRange: IntRange,
28 | onNewLimitPicked: (Int) -> Unit,
29 | modifier: Modifier = Modifier
30 | ) {
31 | Column(
32 | horizontalAlignment = Alignment.CenterHorizontally,
33 | modifier = modifier
34 | ) {
35 | Text(
36 | text = pickedLimit.toString(),
37 | style = MaterialTheme.typography.displayLarge.copy(fontSize = 48.sp),
38 | modifier = Modifier.padding(bottom = MaterialTheme.space.small)
39 | )
40 |
41 | Row(
42 | verticalAlignment = Alignment.CenterVertically
43 | ) {
44 | IconButton(
45 | onClick = {
46 | if (pickedLimit > limitRange.first) {
47 | onNewLimitPicked(pickedLimit - 1)
48 | }
49 | }
50 | ) {
51 | Icon(
52 | imageVector = Icons.Outlined.Remove,
53 | tint = MaterialTheme.colorScheme.primary,
54 | contentDescription = stringResource(R.string.content_description_subtract_icon)
55 | )
56 | }
57 |
58 | Slider(
59 | value = pickedLimit.toFloat(),
60 | onValueChange = {
61 | onNewLimitPicked(it.roundToInt())
62 | },
63 | valueRange = limitRange.run { first.toFloat()..last.toFloat() },
64 | colors = SliderDefaults.colors(
65 | inactiveTrackColor = MaterialTheme.colorScheme.surface
66 | ),
67 | modifier = Modifier.weight(1f)
68 | )
69 |
70 | IconButton(
71 | onClick = {
72 | if (pickedLimit < limitRange.last) {
73 | onNewLimitPicked(pickedLimit + 1)
74 | }
75 | }
76 | ) {
77 | Icon(
78 | imageVector = Icons.Outlined.Add,
79 | tint = MaterialTheme.colorScheme.primary,
80 | contentDescription = stringResource(R.string.content_description_add_icon)
81 | )
82 | }
83 | }
84 | }
85 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/introduction/limit_setup/screen_time/ScreenTimeLimitSetupScreenEvent.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.introduction.limit_setup.screen_time
2 |
3 | sealed class ScreenTimeLimitSetupScreenEvent {
4 | data class PickNewScreenTimeLimit(
5 | val newScreenTimeLimitMinutes: Int
6 | ) : ScreenTimeLimitSetupScreenEvent()
7 |
8 | data class ConfirmSelectedSettings(
9 | val screenTimeLimitStateChangedCallback: (Boolean) -> Unit
10 | ) : ScreenTimeLimitSetupScreenEvent()
11 |
12 | data object TryToggleScreenTimeLimitState : ScreenTimeLimitSetupScreenEvent()
13 |
14 | data object DisableScreenTimeLimit : ScreenTimeLimitSetupScreenEvent()
15 |
16 | data class IsScreenTimeLimitDisableConfirmationDialogVisible(val isVisible: Boolean) :
17 | ScreenTimeLimitSetupScreenEvent()
18 |
19 | data object ConfirmRemoveScreenTimeLimitForTomorrow : ScreenTimeLimitSetupScreenEvent()
20 |
21 | data class IsRemoveScreenTimeLimitForTomorrowDialogVisible(val isVisible: Boolean) :
22 | ScreenTimeLimitSetupScreenEvent()
23 |
24 | data class IsSettingsNotSavedDialogVisible(val isVisible: Boolean) :
25 | ScreenTimeLimitSetupScreenEvent()
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/introduction/limit_setup/screen_time/ScreenTimeLimitSetupScreenState.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.introduction.limit_setup.screen_time
2 |
3 | data class ScreenTimeLimitSetupScreenState(
4 | val isScreenTimeLimitEnabled: Boolean? = null,
5 | val pickedScreenTimeLimitMinutes: Int? = null,
6 | val availableScreenTimeLimitRange: IntRange? = null,
7 | val screenTimeLimitIntervalMinutes: Int? = null,
8 | val screenTimeLimitMinutesForTomorrow: Int? = null,
9 | val isRemoveScreenTimeLimitForTomorrowDialogVisible: Boolean = false,
10 | val isScreenTimeLimitDisableConfirmationDialogVisible: Boolean = false,
11 | val hasUserChangedAnySettings: Boolean = false,
12 | val isSettingsNotSavedDialogVisible: Boolean = false
13 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/introduction/limit_setup/unlock/UnlockLimitSetupScreenEvent.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.introduction.limit_setup.unlock
2 |
3 | sealed class UnlockLimitSetupScreenEvent {
4 | data class PickNewUnlockLimit(val newUnlockLimit: Int) : UnlockLimitSetupScreenEvent()
5 |
6 | data object SubmitSelectedUnlockLimit : UnlockLimitSetupScreenEvent()
7 |
8 | data object ConfirmRemoveUnlockLimitForTomorrow : UnlockLimitSetupScreenEvent()
9 |
10 | data class IsRemoveUnlockLimitForTomorrowDialogVisible(val isVisible: Boolean) :
11 | UnlockLimitSetupScreenEvent()
12 |
13 | data class IsSettingsNotSavedDialogVisible(val isVisible: Boolean) :
14 | UnlockLimitSetupScreenEvent()
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/introduction/limit_setup/unlock/UnlockLimitSetupScreenState.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.introduction.limit_setup.unlock
2 |
3 | data class UnlockLimitSetupScreenState(
4 | val pickedUnlockLimit: Int? = null,
5 | val availableUnlockLimitRange: IntRange? = null,
6 | val unlockLimitForTomorrow: Int? = null,
7 | val isRemoveUnlockLimitForTomorrowDialogVisible: Boolean = false,
8 | val hasUserChangedAnySettings: Boolean = false,
9 | val isSettingsNotSavedDialogVisible: Boolean = false
10 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/introduction/welcome/WelcomeScreen.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.introduction.welcome
2 |
3 | import androidx.compose.foundation.Image
4 | import androidx.compose.foundation.background
5 | import androidx.compose.foundation.layout.Arrangement
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.PaddingValues
8 | import androidx.compose.foundation.layout.fillMaxSize
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.foundation.layout.size
11 | import androidx.compose.material3.ButtonDefaults
12 | import androidx.compose.material3.ElevatedButton
13 | import androidx.compose.material3.MaterialTheme
14 | import androidx.compose.material3.Text
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.ui.Alignment
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.graphics.Brush
19 | import androidx.compose.ui.graphics.Color
20 | import androidx.lifecycle.compose.LocalLifecycleOwner
21 | import androidx.compose.ui.res.painterResource
22 | import androidx.compose.ui.res.stringResource
23 | import androidx.compose.ui.text.style.TextAlign
24 | import androidx.navigation.NavController
25 | import com.sweak.unlockmaster.R
26 | import com.sweak.unlockmaster.presentation.common.Screen
27 | import com.sweak.unlockmaster.presentation.common.theme.space
28 | import com.sweak.unlockmaster.presentation.common.util.navigateThrottled
29 |
30 | @Composable
31 | fun WelcomeScreen(navController: NavController) {
32 | val lifecycleOwner = LocalLifecycleOwner.current
33 |
34 | Column(
35 | verticalArrangement = Arrangement.Center,
36 | horizontalAlignment = Alignment.CenterHorizontally,
37 | modifier = Modifier
38 | .fillMaxSize()
39 | .background(
40 | brush = Brush.verticalGradient(
41 | listOf(
42 | MaterialTheme.colorScheme.primary,
43 | MaterialTheme.colorScheme.secondary
44 | )
45 | )
46 | )
47 | ) {
48 | Image(
49 | painter = painterResource(R.drawable.ic_notification_icon),
50 | contentDescription = stringResource(R.string.content_description_application_icon),
51 | modifier = Modifier.size(MaterialTheme.space.xxxLarge)
52 | )
53 |
54 | Text(
55 | text = stringResource(R.string.welcome_to_unlock_master),
56 | style = MaterialTheme.typography.displayLarge,
57 | color = Color.White,
58 | modifier = Modifier.padding(all = MaterialTheme.space.medium)
59 | )
60 |
61 | Text(
62 | text = stringResource(R.string.welcome_to_unlock_master_subtitle),
63 | textAlign = TextAlign.Center,
64 | style = MaterialTheme.typography.titleMedium,
65 | color = Color.White,
66 | modifier = Modifier
67 | .padding(
68 | start = MaterialTheme.space.medium,
69 | end = MaterialTheme.space.medium,
70 | bottom = MaterialTheme.space.large
71 | )
72 | )
73 |
74 | ElevatedButton(
75 | onClick = {
76 | navController.navigateThrottled(
77 | Screen.IntroductionScreen.withArguments(false.toString()),
78 | lifecycleOwner
79 | )
80 | },
81 | colors = ButtonDefaults.elevatedButtonColors(
82 | containerColor = Color.White,
83 | contentColor = Color.Black
84 | ),
85 | contentPadding = PaddingValues(
86 | horizontal = MaterialTheme.space.xxLarge,
87 | vertical = MaterialTheme.space.small
88 | )
89 | ) {
90 | Text(
91 | text = stringResource(R.string.lets_start)
92 | )
93 | }
94 | }
95 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/main/home/HomeScreenEvent.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.main.home
2 |
3 | sealed class HomeScreenEvent {
4 | data class TryPauseOrUnpauseUnlockCounter(
5 | val pauseChangedCallback: (Boolean) -> Unit
6 | ) : HomeScreenEvent()
7 | data class PauseUnlockCounter(
8 | val pauseChangedCallback: (Boolean) -> Unit
9 | ) : HomeScreenEvent()
10 | data class IsUnlockCounterPauseConfirmationDialogVisible(
11 | val isVisible: Boolean
12 | ) : HomeScreenEvent()
13 | data object DismissUnlockMasterBlockedWarning : HomeScreenEvent()
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/main/home/HomeScreenState.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.main.home
2 |
3 | import com.github.mikephil.charting.data.BarEntry
4 |
5 | data class HomeScreenState(
6 | val isInitializing: Boolean = true,
7 | val unlockCount: Int? = null,
8 | val unlockLimit: Int? = null,
9 | val isUnlockCounterPaused: Boolean? = null,
10 | val isUnlockCounterPauseConfirmationDialogVisible: Boolean = false,
11 | val shouldShowUnlockMasterBlockedWarning: Boolean = false,
12 | val unlockLimitForTomorrow: Int? = null,
13 | val todayScreenTimeDurationMillis: Long? = null,
14 | val isScreenTimeLimitEnabled: Boolean = true,
15 | val screenTimeLimitMinutes: Int? = null,
16 | val screenTimeLimitForTomorrowMinutes: Int? = null,
17 | val lastWeekUnlockEventCounts: List = emptyList()
18 | )
19 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/main/home/components/SemiTransparentBlueRectangleMarkerView.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.main.home.components
2 |
3 | import android.content.Context
4 | import com.github.mikephil.charting.components.MarkerView
5 | import com.github.mikephil.charting.utils.MPPointF
6 | import com.sweak.unlockmaster.R
7 |
8 | class SemiTransparentBlueRectangleMarkerView(context: Context) :
9 | MarkerView(context, R.layout.semi_transparent_blue_rect_marker_view) {
10 |
11 | override fun getOffset(): MPPointF = MPPointF(-(width / 2.0f), height.toFloat())
12 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/main/screen_time/ScreenTimeScreenState.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.main.screen_time
2 |
3 | import com.github.mikephil.charting.data.Entry
4 |
5 | data class ScreenTimeScreenState(
6 | val isInitializing: Boolean = true,
7 | val screenTimeMinutesPerHourEntries: List = emptyList(),
8 | val todayScreenTimeDurationMillis: Long? = null,
9 | val uiReadySessionEvents: List = emptyList()
10 | ) {
11 | sealed class UIReadySessionEvent(val startAndEndTimesInMillis: Pair) {
12 | class ScreenTime(
13 | screenSessionStartAndEndTimesInMillis: Pair,
14 | val screenSessionDurationMillis: Long
15 | ) : UIReadySessionEvent(screenSessionStartAndEndTimesInMillis)
16 |
17 | class CounterPaused(
18 | counterPauseSessionStartAndEndTimesInMillis: Pair
19 | ) : UIReadySessionEvent(counterPauseSessionStartAndEndTimesInMillis)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/main/screen_time/ScreenTimeViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.main.screen_time
2 |
3 | import androidx.compose.runtime.getValue
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.compose.runtime.setValue
6 | import androidx.lifecycle.SavedStateHandle
7 | import androidx.lifecycle.ViewModel
8 | import androidx.lifecycle.viewModelScope
9 | import com.github.mikephil.charting.data.Entry
10 | import com.sweak.unlockmaster.domain.model.SessionEvent.CounterPaused
11 | import com.sweak.unlockmaster.domain.model.SessionEvent.ScreenTime
12 | import com.sweak.unlockmaster.domain.use_case.screen_time.GetHourlyUsageMinutesForGivenDayUseCase
13 | import com.sweak.unlockmaster.domain.use_case.screen_time.GetScreenTimeDurationForGivenDayUseCase
14 | import com.sweak.unlockmaster.domain.use_case.screen_time.GetSessionEventsForGivenDayUseCase
15 | import com.sweak.unlockmaster.presentation.common.Screen
16 | import com.sweak.unlockmaster.presentation.main.screen_time.ScreenTimeScreenState.UIReadySessionEvent
17 | import dagger.hilt.android.lifecycle.HiltViewModel
18 | import kotlinx.coroutines.launch
19 | import javax.inject.Inject
20 |
21 | @HiltViewModel
22 | class ScreenTimeViewModel @Inject constructor(
23 | savedStateHandle: SavedStateHandle,
24 | private val getHourlyUsageMinutesForGivenDayUseCase: GetHourlyUsageMinutesForGivenDayUseCase,
25 | private val getScreenTimeDurationForGivenDayUseCase: GetScreenTimeDurationForGivenDayUseCase,
26 | private val getSessionEventsForGivenDayUseCase: GetSessionEventsForGivenDayUseCase
27 | ) : ViewModel() {
28 |
29 | private val displayedDayTimeInMillis: Long =
30 | checkNotNull(savedStateHandle[Screen.KEY_DISPLAYED_SCREEN_TIME_DAY_MILLIS])
31 |
32 | var state by mutableStateOf(ScreenTimeScreenState())
33 |
34 | fun refresh() = viewModelScope.launch {
35 | state = state.copy(
36 | isInitializing = false,
37 | screenTimeMinutesPerHourEntries =
38 | getHourlyUsageMinutesForGivenDayUseCase(displayedDayTimeInMillis)
39 | .mapIndexed { index, minutes -> Entry(index.toFloat(), minutes.toFloat()) },
40 | todayScreenTimeDurationMillis =
41 | getScreenTimeDurationForGivenDayUseCase(displayedDayTimeInMillis),
42 | uiReadySessionEvents =
43 | getSessionEventsForGivenDayUseCase(displayedDayTimeInMillis)
44 | .map {
45 | when (it) {
46 | is ScreenTime -> {
47 | UIReadySessionEvent.ScreenTime(
48 | screenSessionStartAndEndTimesInMillis =
49 | Pair(it.sessionStartTime, it.sessionEndTime),
50 | screenSessionDurationMillis = it.sessionDuration
51 | )
52 | }
53 | is CounterPaused -> {
54 | UIReadySessionEvent.CounterPaused(
55 | counterPauseSessionStartAndEndTimesInMillis =
56 | Pair(it.sessionStartTime, it.sessionEndTime)
57 | )
58 | }
59 | }
60 | }
61 | )
62 | }
63 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/main/screen_time/components/CounterPauseSeparator.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.main.screen_time.components
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.Spacer
6 | import androidx.compose.foundation.layout.height
7 | import androidx.compose.foundation.layout.padding
8 | import androidx.compose.material3.MaterialTheme
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.res.stringResource
14 | import androidx.compose.ui.unit.dp
15 | import com.sweak.unlockmaster.R
16 | import com.sweak.unlockmaster.presentation.common.theme.space
17 | import com.sweak.unlockmaster.presentation.common.util.TimeFormat
18 | import com.sweak.unlockmaster.presentation.common.util.getTimeString
19 |
20 | @Composable
21 | fun CounterPauseSeparator(
22 | counterPauseSessionStartAndEndTimesInMillis: Pair,
23 | timeFormat: TimeFormat,
24 | modifier: Modifier = Modifier
25 | ) {
26 | Row(
27 | verticalAlignment = Alignment.CenterVertically,
28 | modifier = modifier
29 | ) {
30 | Spacer(
31 | modifier = Modifier
32 | .height(1.dp)
33 | .background(color = MaterialTheme.colorScheme.onSurface)
34 | .weight(1f)
35 | )
36 |
37 | Text(
38 | text = stringResource(R.string.counter_paused_colon) +
39 | " " +
40 | getTimeString(counterPauseSessionStartAndEndTimesInMillis.first, timeFormat) +
41 | " - " +
42 | getTimeString(counterPauseSessionStartAndEndTimesInMillis.second, timeFormat),
43 | style = MaterialTheme.typography.titleSmall,
44 | modifier = Modifier.padding(horizontal = MaterialTheme.space.small)
45 | )
46 |
47 | Spacer(
48 | modifier = Modifier
49 | .height(1.dp)
50 | .background(color = MaterialTheme.colorScheme.onSurface)
51 | .weight(1f)
52 | )
53 | }
54 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/main/screen_time/components/DailyScreenTimeChart.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.main.screen_time.components
2 |
3 | import android.text.format.DateFormat
4 | import androidx.compose.material3.MaterialTheme
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.Modifier
7 | import androidx.compose.ui.graphics.toArgb
8 | import androidx.compose.ui.viewinterop.AndroidView
9 | import androidx.core.content.res.ResourcesCompat
10 | import com.github.mikephil.charting.charts.LineChart
11 | import com.github.mikephil.charting.components.AxisBase
12 | import com.github.mikephil.charting.components.XAxis
13 | import com.github.mikephil.charting.data.Entry
14 | import com.github.mikephil.charting.data.LineData
15 | import com.github.mikephil.charting.data.LineDataSet
16 | import com.github.mikephil.charting.formatter.IndexAxisValueFormatter
17 | import com.sweak.unlockmaster.R
18 | import kotlin.math.roundToInt
19 |
20 | @Composable
21 | fun DailyScreenTimeChart(
22 | screenTimeMinutesPerHourEntries: List,
23 | modifier: Modifier = Modifier
24 | ) {
25 | val lineArgbColor: Int = MaterialTheme.colorScheme.secondary.toArgb()
26 | val textArgbColor = MaterialTheme.colorScheme.onBackground.toArgb()
27 |
28 | AndroidView(
29 | factory = { context ->
30 | LineChart(context).apply {
31 | setScaleEnabled(false)
32 | description.isEnabled = false
33 | axisRight.isEnabled = false
34 | legend.isEnabled = false
35 | isHighlightPerTapEnabled = false
36 | isHighlightPerDragEnabled = false
37 |
38 | axisLeft.apply {
39 | setDrawGridLines(false)
40 | setDrawAxisLine(false)
41 | setDrawLabels(false)
42 | axisMinimum = -10f
43 | axisMaximum = 70f
44 | }
45 |
46 | xAxis.apply {
47 | setDrawGridLines(false)
48 | setDrawAxisLine(false)
49 | isGranularityEnabled = true
50 | granularity = 1f
51 | labelCount = 24
52 | position = XAxis.XAxisPosition.BOTTOM
53 | textSize = 10f
54 | textColor = textArgbColor
55 | typeface = ResourcesCompat.getFont(context, R.font.amiko_regular)
56 | valueFormatter = object : IndexAxisValueFormatter() {
57 | override fun getAxisLabel(value: Float, axis: AxisBase): String =
58 | value.roundToInt().run {
59 | val is24HoursFormat = DateFormat.is24HourFormat(context)
60 |
61 | when (this) {
62 | 6 -> if (is24HoursFormat) "6:00" else "6:00 AM"
63 | 12 -> if (is24HoursFormat) "12:00" else "12:00 PM"
64 | 18 -> if (is24HoursFormat) "18:00" else "6:00 PM"
65 | else -> ""
66 | }
67 | }
68 | }
69 | }
70 | }
71 | },
72 | update = {
73 | val lineData = LineData(
74 | LineDataSet(screenTimeMinutesPerHourEntries, "screenTimeMinutesPerHourEntries")
75 | .apply {
76 | lineWidth = 8f
77 | color = lineArgbColor
78 | mode = LineDataSet.Mode.CUBIC_BEZIER
79 | setDrawCircles(false)
80 | setDrawValues(false)
81 | setDrawHighlightIndicators(false)
82 | }
83 | )
84 |
85 | it.data = lineData
86 | it.invalidate()
87 | },
88 | modifier = modifier
89 | )
90 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/main/screen_time/components/SingleScreenTimeSessionCard.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.main.screen_time.components
2 |
3 | import androidx.compose.foundation.layout.Row
4 | import androidx.compose.foundation.layout.fillMaxWidth
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.foundation.layout.size
7 | import androidx.compose.material.icons.Icons
8 | import androidx.compose.material.icons.outlined.AccessTime
9 | import androidx.compose.material3.CardDefaults
10 | import androidx.compose.material3.ElevatedCard
11 | import androidx.compose.material3.Icon
12 | import androidx.compose.material3.MaterialTheme
13 | import androidx.compose.material3.Text
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.ui.Alignment
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.res.stringResource
18 | import androidx.compose.ui.text.style.TextOverflow
19 | import com.sweak.unlockmaster.R
20 | import com.sweak.unlockmaster.presentation.common.theme.space
21 | import com.sweak.unlockmaster.presentation.common.util.Duration
22 | import com.sweak.unlockmaster.presentation.common.util.TimeFormat
23 | import com.sweak.unlockmaster.presentation.common.util.getCompactDurationString
24 | import com.sweak.unlockmaster.presentation.common.util.getTimeString
25 |
26 | @Composable
27 | fun SingleScreenTimeSessionCard(
28 | screenSessionStartAndEndTimesInMillis: Pair,
29 | screenSessionDuration: Duration,
30 | timeFormat: TimeFormat,
31 | modifier: Modifier = Modifier
32 | ) {
33 | ElevatedCard(
34 | colors = CardDefaults.elevatedCardColors(
35 | containerColor = MaterialTheme.colorScheme.surface
36 | ),
37 | elevation = CardDefaults.elevatedCardElevation(
38 | defaultElevation = MaterialTheme.space.xSmall
39 | ),
40 | modifier = modifier
41 | ) {
42 | Row(
43 | verticalAlignment = Alignment.CenterVertically,
44 | modifier = Modifier
45 | .fillMaxWidth()
46 | .padding(all = MaterialTheme.space.medium)
47 | ) {
48 | Icon(
49 | imageVector = Icons.Outlined.AccessTime,
50 | contentDescription = stringResource(R.string.content_description_clock_icon),
51 | modifier = Modifier.size(size = MaterialTheme.space.mediumLarge)
52 | )
53 |
54 | Text(
55 | text = getTimeString(screenSessionStartAndEndTimesInMillis.first, timeFormat) +
56 | " - " +
57 | getTimeString(screenSessionStartAndEndTimesInMillis.second, timeFormat),
58 | style = MaterialTheme.typography.titleMedium,
59 | modifier = Modifier
60 | .padding(horizontal = MaterialTheme.space.smallMedium)
61 | .weight(1f)
62 | )
63 |
64 | Text(
65 | text = getCompactDurationString(screenSessionDuration),
66 | style = MaterialTheme.typography.displayMedium,
67 | overflow = TextOverflow.Ellipsis,
68 | maxLines = 1
69 | )
70 | }
71 | }
72 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/main/statistics/StatisticsScreenEvent.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.main.statistics
2 |
3 | sealed class StatisticsScreenEvent {
4 | class SelectChartValue(val selectedEntryIndex: Int) : StatisticsScreenEvent()
5 | class ScreenOnEventsInformationDialogVisible(val isVisible: Boolean) : StatisticsScreenEvent()
6 | }
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/main/statistics/StatisticsScreenState.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.main.statistics
2 |
3 | import com.github.mikephil.charting.data.BarEntry
4 |
5 | data class StatisticsScreenState(
6 | val isInitializing: Boolean = true,
7 | val allTimeUnlockEventCounts: List = emptyList(),
8 | val currentlyHighlightedDayTimeInMillis: Long = System.currentTimeMillis(),
9 | val unlockEventsCount: Int = 0,
10 | val unlockLimitAmount: Int = 0,
11 | val screenOnEventsCount: Int = 0,
12 | val screenTimeDurationMillis: Long? = null,
13 | val screenTimeLimitDurationMillis: Long? = null,
14 | val isScreenOnEventsInformationDialogVisible: Boolean = false
15 | )
16 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/settings/application_blocked/ApplicationBlockedScreenEvent.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.settings.application_blocked
2 |
3 | sealed class ApplicationBlockedScreenEvent {
4 | data object CheckIfIgnoringBatteryOptimizations : ApplicationBlockedScreenEvent()
5 |
6 | data class IsIgnoreBatteryOptimizationsRequestUnavailableDialogVisible(
7 | val isVisible: Boolean
8 | ) : ApplicationBlockedScreenEvent()
9 |
10 | data class IsWebBrowserNotFoundDialogVisible(
11 | val isVisible: Boolean
12 | ) : ApplicationBlockedScreenEvent()
13 | }
14 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/settings/application_blocked/ApplicationBlockedScreenState.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.settings.application_blocked
2 |
3 | data class ApplicationBlockedScreenState(
4 | val isIgnoringBatteryOptimizations: Boolean = false,
5 | val isIgnoreBatteryOptimizationsRequestUnavailable: Boolean = false,
6 | val isIgnoreBatteryOptimizationsRequestUnavailableDialogVisible: Boolean = false,
7 | val isWebBrowserNotFoundDialogVisible: Boolean = false
8 | )
9 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/settings/application_blocked/ApplicationBlockedViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.settings.application_blocked
2 |
3 | import android.os.Build
4 | import android.os.PowerManager
5 | import androidx.compose.runtime.getValue
6 | import androidx.compose.runtime.mutableStateOf
7 | import androidx.compose.runtime.setValue
8 | import androidx.lifecycle.ViewModel
9 | import androidx.lifecycle.viewModelScope
10 | import dagger.hilt.android.lifecycle.HiltViewModel
11 | import kotlinx.coroutines.delay
12 | import kotlinx.coroutines.launch
13 | import javax.inject.Inject
14 | import javax.inject.Named
15 |
16 | @HiltViewModel
17 | class ApplicationBlockedViewModel @Inject constructor(
18 | private val powerManager: PowerManager,
19 | @Named("PackageName") private val packageName: String
20 | ) : ViewModel() {
21 |
22 | var state by mutableStateOf(ApplicationBlockedScreenState())
23 |
24 | init {
25 | viewModelScope.launch {
26 | state = state.copy(
27 | isIgnoringBatteryOptimizations =
28 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
29 | powerManager.isIgnoringBatteryOptimizations(packageName)
30 | } else true
31 | )
32 | }
33 | }
34 |
35 | fun onEvent(event: ApplicationBlockedScreenEvent) {
36 | when (event) {
37 | is ApplicationBlockedScreenEvent.CheckIfIgnoringBatteryOptimizations ->
38 | viewModelScope.launch {
39 | // This delay is supposed to ensure that
40 | // powerManager.isIgnoringBatteryOptimizations returns an updated value - it can
41 | // take some time until it is updated on some systems.
42 | delay(1000)
43 |
44 | state = state.copy(
45 | isIgnoringBatteryOptimizations =
46 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
47 | powerManager.isIgnoringBatteryOptimizations(packageName)
48 | } else true
49 | )
50 | }
51 | is ApplicationBlockedScreenEvent.IsIgnoreBatteryOptimizationsRequestUnavailableDialogVisible ->
52 | state = state.copy(
53 | isIgnoreBatteryOptimizationsRequestUnavailable = true,
54 | isIgnoreBatteryOptimizationsRequestUnavailableDialogVisible = event.isVisible
55 | )
56 | is ApplicationBlockedScreenEvent.IsWebBrowserNotFoundDialogVisible ->
57 | state = state.copy(isWebBrowserNotFoundDialogVisible = event.isVisible)
58 | }
59 | }
60 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/settings/components/SettingsEntry.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.settings.components
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.layout.Arrangement
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.Row
8 | import androidx.compose.foundation.layout.Spacer
9 | import androidx.compose.foundation.layout.fillMaxWidth
10 | import androidx.compose.foundation.layout.height
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.automirrored.outlined.NavigateNext
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.ui.Alignment
20 | import androidx.compose.ui.Modifier
21 | import androidx.compose.ui.res.stringResource
22 | import androidx.compose.ui.text.style.TextOverflow
23 | import androidx.compose.ui.unit.dp
24 | import com.sweak.unlockmaster.R
25 | import com.sweak.unlockmaster.presentation.common.theme.space
26 |
27 | @Composable
28 | fun SettingsEntry(
29 | settingsEntryTitle: String,
30 | onEntryClick: () -> Unit,
31 | modifier: Modifier = Modifier
32 | ) {
33 | Column(modifier = modifier.clickable(onClick = onEntryClick)) {
34 | Row(
35 | horizontalArrangement = Arrangement.SpaceBetween,
36 | verticalAlignment = Alignment.CenterVertically,
37 | modifier = Modifier
38 | .fillMaxWidth()
39 | .padding(vertical = MaterialTheme.space.medium)
40 | ) {
41 | Text(
42 | text = settingsEntryTitle,
43 | style = MaterialTheme.typography.headlineMedium,
44 | overflow = TextOverflow.Ellipsis,
45 | maxLines = 1,
46 | modifier = Modifier.padding(end = MaterialTheme.space.large)
47 | )
48 |
49 | Icon(
50 | imageVector = Icons.AutoMirrored.Outlined.NavigateNext,
51 | contentDescription = stringResource(R.string.content_description_next_icon),
52 | modifier = Modifier.size(size = MaterialTheme.space.mediumLarge)
53 | )
54 | }
55 |
56 | Spacer(
57 | modifier = Modifier
58 | .fillMaxWidth()
59 | .height(1.dp)
60 | .background(color = MaterialTheme.colorScheme.onBackground)
61 | )
62 | }
63 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/settings/daily_wrap_up_settings/DailyWrapUpSettingsScreenEvent.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.settings.daily_wrap_up_settings
2 |
3 | sealed class DailyWrapUpSettingsScreenEvent {
4 | data class SelectNewDailyWrapUpSettingsNotificationsTime(
5 | val newNotificationHourOfDay: Int,
6 | val newNotificationMinute: Int,
7 | ) : DailyWrapUpSettingsScreenEvent()
8 |
9 | data object ConfirmNewSelectedDailyWrapUpSettings : DailyWrapUpSettingsScreenEvent()
10 |
11 | data class IsInvalidTimeSelectedDialogVisible(val isVisible: Boolean) :
12 | DailyWrapUpSettingsScreenEvent()
13 |
14 | data class IsSettingsNotSavedDialogVisible(val isVisible: Boolean) :
15 | DailyWrapUpSettingsScreenEvent()
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/settings/daily_wrap_up_settings/DailyWrapUpSettingsScreenState.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.settings.daily_wrap_up_settings
2 |
3 | data class DailyWrapUpSettingsScreenState(
4 | val notificationHourOfDay: Int? = null,
5 | val notificationMinute: Int? = null,
6 | val isInvalidTimeSelectedDialogVisible: Boolean = false,
7 | val hasInitialTimeBeenSet: Boolean = false,
8 | val hasUserChangedAnySettings: Boolean = false,
9 | val isSettingsNotSavedDialogVisible: Boolean = false
10 | )
11 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/settings/daily_wrap_up_settings/components/CardTimePicker.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.settings.daily_wrap_up_settings.components
2 |
3 | import android.annotation.SuppressLint
4 | import android.os.Build
5 | import android.text.format.DateFormat
6 | import android.view.LayoutInflater
7 | import android.widget.TimePicker
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.material3.CardDefaults
10 | import androidx.compose.material3.ElevatedCard
11 | import androidx.compose.material3.MaterialTheme
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.ui.Alignment
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.platform.LocalContext
16 | import androidx.compose.ui.viewinterop.AndroidView
17 | import com.sweak.unlockmaster.R
18 | import com.sweak.unlockmaster.presentation.common.theme.space
19 |
20 | @SuppressLint("InflateParams")
21 | @Composable
22 | fun CardTimePicker(
23 | hourOfDay: Int,
24 | minute: Int,
25 | onTimeChanged: (hourOfDay: Int, minute: Int) -> Unit,
26 | modifier: Modifier = Modifier
27 | ) {
28 | val context = LocalContext.current
29 | val is24HourFormat = DateFormat.is24HourFormat(context)
30 |
31 | ElevatedCard(
32 | colors = CardDefaults.elevatedCardColors(
33 | containerColor = MaterialTheme.colorScheme.surface
34 | ),
35 | elevation = CardDefaults.elevatedCardElevation(
36 | defaultElevation = MaterialTheme.space.xSmall
37 | ),
38 | modifier = modifier
39 | ) {
40 | AndroidView(
41 | factory = {
42 | (LayoutInflater.from(it)
43 | .inflate(R.layout.spinner_time_picker, null) as TimePicker)
44 | .apply {
45 | // Right after composing the TimePicker it calls the timeChangedListener
46 | // with the current time which breaks the uiState - we have to prevent the
47 | // uiState update after this initial timeChangedListener call:
48 | var isInitialUpdate = true
49 |
50 | setOnTimeChangedListener { _, hourOfDay, minute ->
51 | if (!isInitialUpdate) {
52 | onTimeChanged(hourOfDay, minute)
53 | } else {
54 | isInitialUpdate = false
55 | }
56 | }
57 | }
58 | },
59 | update = {
60 | it.setIs24HourView(is24HourFormat)
61 |
62 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
63 | it.hour = hourOfDay
64 | it.minute = minute
65 | } else {
66 | it.currentHour = hourOfDay
67 | it.currentMinute = minute
68 | }
69 | },
70 | modifier = Modifier
71 | .padding(all = MaterialTheme.space.medium)
72 | .align(Alignment.CenterHorizontally)
73 | )
74 | }
75 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/settings/data_backup/DataBackupScreenEvent.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.settings.data_backup
2 |
3 | import android.net.Uri
4 | import androidx.activity.compose.ManagedActivityResultLauncher
5 |
6 | sealed class DataBackupScreenEvent {
7 | data class CreateBackupClicked(
8 | val createBackupFileLauncher: ManagedActivityResultLauncher
9 | ) : DataBackupScreenEvent()
10 |
11 | data class PerformDataBackupCreation(val dataBackupFileUri: Uri?) : DataBackupScreenEvent()
12 |
13 | data class RestoreDataClicked(
14 | val restoreFromBackupLauncher: ManagedActivityResultLauncher, Uri?>
15 | ) : DataBackupScreenEvent()
16 |
17 | data class PerformDataRestorationFromBackup(val dataBackupFileUri: Uri?) :
18 | DataBackupScreenEvent()
19 |
20 | data class IsCounterPausedErrorDialogVisible(val isVisible: Boolean) : DataBackupScreenEvent()
21 |
22 | data class IsBackupCreationErrorDialogVisible(val isVisible: Boolean) : DataBackupScreenEvent()
23 |
24 | data class IsDataRestorationErrorDialogVisible(val isVisible: Boolean) : DataBackupScreenEvent()
25 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/settings/data_backup/DataBackupScreenState.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.settings.data_backup
2 |
3 | data class DataBackupScreenState(
4 | val isInTheProcessOfCreatingBackup: Boolean = false,
5 | val isInTheProcessOfRestoringData: Boolean = false,
6 | val isCounterPausedErrorDialogVisible: Boolean = false,
7 | val isBackupCreationErrorDialogVisible: Boolean = false,
8 | val isDataRestorationErrorDialogVisible: Boolean = false
9 | )
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/settings/mobilizing_notifications/MobilizingNotificationsScreenEvent.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.settings.mobilizing_notifications
2 |
3 | sealed class MobilizingNotificationsScreenEvent {
4 | data class SelectNewFrequencyPercentageIndex(
5 | val newPercentageIndex: Int
6 | ) : MobilizingNotificationsScreenEvent()
7 |
8 | data class ToggleOverLimitNotifications(
9 | val areOverLimitNotificationsEnabled: Boolean
10 | ) : MobilizingNotificationsScreenEvent()
11 | data object ConfirmSelectedSettings : MobilizingNotificationsScreenEvent()
12 |
13 | data class IsSettingsNotSavedDialogVisible(val isVisible: Boolean) :
14 | MobilizingNotificationsScreenEvent()
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/settings/mobilizing_notifications/MobilizingNotificationsScreenState.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.settings.mobilizing_notifications
2 |
3 | data class MobilizingNotificationsScreenState(
4 | val selectedMobilizingNotificationsFrequencyPercentageIndex: Int? = null,
5 | val availableMobilizingNotificationsFrequencyPercentages: List? = null,
6 | val areOverLimitNotificationsEnabled: Boolean? = null,
7 | val hasUserChangedAnySettings: Boolean = false,
8 | val isSettingsNotSavedDialogVisible: Boolean = false
9 | )
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/settings/user_interface_theme/UserInterfaceThemeScreenEvent.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.settings.user_interface_theme
2 |
3 | import com.sweak.unlockmaster.domain.model.UiThemeMode
4 |
5 | sealed class UserInterfaceThemeScreenEvent {
6 | data class SelectUiThemeMode(val uiThemeMode: UiThemeMode) : UserInterfaceThemeScreenEvent()
7 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/settings/user_interface_theme/UserInterfaceThemeScreenState.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.settings.user_interface_theme
2 |
3 | import com.sweak.unlockmaster.domain.model.UiThemeMode
4 |
5 | data class UserInterfaceThemeScreenState(
6 | val selectedUiThemeMode: UiThemeMode = UiThemeMode.SYSTEM,
7 | val availableUiThemeModes: List = UiThemeMode.entries.toList()
8 | )
9 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/settings/user_interface_theme/UserInterfaceThemeViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.settings.user_interface_theme
2 |
3 | import androidx.compose.runtime.getValue
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.compose.runtime.setValue
6 | import androidx.lifecycle.ViewModel
7 | import androidx.lifecycle.viewModelScope
8 | import com.sweak.unlockmaster.domain.repository.UserSessionRepository
9 | import dagger.hilt.android.lifecycle.HiltViewModel
10 | import kotlinx.coroutines.launch
11 | import javax.inject.Inject
12 |
13 | @HiltViewModel
14 | class UserInterfaceThemeViewModel @Inject constructor(
15 | private val userSessionRepository: UserSessionRepository
16 | ) : ViewModel() {
17 |
18 | var state by mutableStateOf(UserInterfaceThemeScreenState())
19 |
20 | init {
21 | viewModelScope.launch {
22 | userSessionRepository.getUiThemeModeFlow().collect {
23 | state = state.copy(selectedUiThemeMode = it)
24 | }
25 | }
26 | }
27 |
28 | fun onEvent(event: UserInterfaceThemeScreenEvent) {
29 | when (event) {
30 | is UserInterfaceThemeScreenEvent.SelectUiThemeMode -> {
31 | if (event.uiThemeMode == state.selectedUiThemeMode) return
32 |
33 | viewModelScope.launch {
34 | userSessionRepository.setUiThemeMode(event.uiThemeMode)
35 | state = state.copy(selectedUiThemeMode = event.uiThemeMode)
36 | }
37 | }
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/widget/UnlockCountWidget.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.widget
2 |
3 | import android.content.Context
4 | import android.widget.RemoteViews
5 | import androidx.compose.ui.graphics.Color
6 | import androidx.compose.ui.unit.dp
7 | import androidx.compose.ui.unit.sp
8 | import androidx.datastore.preferences.core.intPreferencesKey
9 | import androidx.glance.GlanceId
10 | import androidx.glance.GlanceModifier
11 | import androidx.glance.LocalContext
12 | import androidx.glance.action.actionStartActivity
13 | import androidx.glance.action.clickable
14 | import androidx.glance.appwidget.AndroidRemoteViews
15 | import androidx.glance.appwidget.GlanceAppWidget
16 | import androidx.glance.appwidget.provideContent
17 | import androidx.glance.background
18 | import androidx.glance.currentState
19 | import androidx.glance.layout.Alignment
20 | import androidx.glance.layout.Alignment.Companion.Center
21 | import androidx.glance.layout.Box
22 | import androidx.glance.layout.Column
23 | import androidx.glance.layout.fillMaxSize
24 | import androidx.glance.layout.size
25 | import androidx.glance.text.FontFamily
26 | import androidx.glance.text.FontWeight
27 | import androidx.glance.text.Text
28 | import androidx.glance.text.TextStyle
29 | import androidx.glance.unit.ColorProvider
30 | import com.sweak.unlockmaster.R
31 | import com.sweak.unlockmaster.presentation.MainActivity
32 |
33 | class UnlockCountWidget : GlanceAppWidget() {
34 |
35 | override suspend fun provideGlance(context: Context, id: GlanceId) {
36 | provideContent {
37 | Box(
38 | modifier = GlanceModifier
39 | .fillMaxSize()
40 | .background(ColorProvider(Color(0xFFF3F4F5)))
41 | .clickable(actionStartActivity()),
42 | contentAlignment = Center
43 | ) {
44 | val unlockCount = currentState(UNLOCK_EVENTS_COUNT_PREFERENCES_KEY) ?: 0
45 | val unlockLimit = currentState(UNLOCK_LIMIT_PREFERENCES_KEY) ?: 1
46 |
47 | val remoteView = RemoteViews(context.packageName, R.layout.progress_bar)
48 | remoteView.setProgressBar(
49 | R.id.progress_bar,
50 | unlockLimit,
51 | unlockCount,
52 | false
53 | )
54 |
55 | AndroidRemoteViews(
56 | remoteViews = remoteView,
57 | modifier = GlanceModifier.size(size = 144.dp)
58 | )
59 |
60 | Column(
61 | horizontalAlignment = Alignment.CenterHorizontally
62 | ) {
63 | Text(
64 | text = unlockCount.toString(),
65 | style = TextStyle(
66 | color = ColorProvider(Color.Black),
67 | fontSize = 48.sp,
68 | fontWeight = FontWeight.Bold,
69 | fontFamily = FontFamily.SansSerif
70 | )
71 | )
72 |
73 | Text(
74 | text = LocalContext.current.resources.getQuantityString(
75 | R.plurals.unlocks,
76 | unlockCount
77 | ),
78 | style = TextStyle(
79 | color = ColorProvider(Color.Black),
80 | fontSize = 14.sp,
81 | fontWeight = FontWeight.Medium,
82 | fontFamily = FontFamily.SansSerif
83 | )
84 | )
85 | }
86 | }
87 | }
88 | }
89 |
90 | companion object {
91 | val UNLOCK_EVENTS_COUNT_PREFERENCES_KEY = intPreferencesKey("unlockEventsCountKey")
92 | val UNLOCK_LIMIT_PREFERENCES_KEY = intPreferencesKey("unlockLimitKey")
93 | }
94 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sweak/unlockmaster/presentation/widget/UnlockCountWidgetReceiver.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.presentation.widget
2 |
3 | import android.appwidget.AppWidgetManager
4 | import android.content.Context
5 | import android.content.Intent
6 | import androidx.glance.appwidget.GlanceAppWidget
7 | import androidx.glance.appwidget.GlanceAppWidgetManager
8 | import androidx.glance.appwidget.GlanceAppWidgetReceiver
9 | import androidx.glance.appwidget.state.updateAppWidgetState
10 | import com.sweak.unlockmaster.domain.use_case.unlock_events.GetUnlockEventsCountForGivenDayUseCase
11 | import com.sweak.unlockmaster.domain.use_case.unlock_limits.GetUnlockLimitAmountForTodayUseCase
12 | import com.sweak.unlockmaster.presentation.widget.UnlockCountWidget.Companion.UNLOCK_EVENTS_COUNT_PREFERENCES_KEY
13 | import com.sweak.unlockmaster.presentation.widget.UnlockCountWidget.Companion.UNLOCK_LIMIT_PREFERENCES_KEY
14 | import dagger.hilt.android.AndroidEntryPoint
15 | import kotlinx.coroutines.CoroutineScope
16 | import kotlinx.coroutines.Dispatchers
17 | import kotlinx.coroutines.launch
18 | import javax.inject.Inject
19 |
20 | @AndroidEntryPoint
21 | class MyAppWidgetReceiver : GlanceAppWidgetReceiver() {
22 |
23 | override val glanceAppWidget: GlanceAppWidget = UnlockCountWidget()
24 |
25 | @Inject
26 | lateinit var getUnlockEventsCountForGivenDayUseCase: GetUnlockEventsCountForGivenDayUseCase
27 |
28 | @Inject
29 | lateinit var getUnlockLimitAmountForTodayUseCase: GetUnlockLimitAmountForTodayUseCase
30 |
31 | private val receiverScope = CoroutineScope(Dispatchers.IO)
32 |
33 | override fun onReceive(context: Context, intent: Intent) {
34 | super.onReceive(context, intent)
35 |
36 | if (intent.action == AppWidgetManager.ACTION_APPWIDGET_UPDATE) {
37 | receiverScope.launch {
38 | val glanceIds = GlanceAppWidgetManager(context)
39 | .getGlanceIds(UnlockCountWidget::class.java).also {
40 | it.ifEmpty { return@launch }
41 | }
42 | val unlockEventsCount = getUnlockEventsCountForGivenDayUseCase()
43 | val unlockLimit = getUnlockLimitAmountForTodayUseCase()
44 |
45 | glanceIds.forEach {
46 | updateAppWidgetState(context, it) { preferences ->
47 | preferences[UNLOCK_EVENTS_COUNT_PREFERENCES_KEY] = unlockEventsCount
48 | preferences[UNLOCK_LIMIT_PREFERENCES_KEY] = unlockLimit
49 | }
50 | glanceAppWidget.update(context, it)
51 | }
52 | }
53 | }
54 | }
55 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-nodpi/img_daily_wrapup_notification.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sweakpl/unlock-master/e1c004607ce2474f82fb65737d05f092916ea0b2/app/src/main/res/drawable-nodpi/img_daily_wrapup_notification.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-nodpi/img_mobilizing_notification.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sweakpl/unlock-master/e1c004607ce2474f82fb65737d05f092916ea0b2/app/src/main/res/drawable-nodpi/img_mobilizing_notification.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-nodpi/img_screen_time_mobilizing_notification.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sweakpl/unlock-master/e1c004607ce2474f82fb65737d05f092916ea0b2/app/src/main/res/drawable-nodpi/img_screen_time_mobilizing_notification.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-nodpi/img_service_notification.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sweakpl/unlock-master/e1c004607ce2474f82fb65737d05f092916ea0b2/app/src/main/res/drawable-nodpi/img_service_notification.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
15 |
19 |
23 |
24 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_notification_icon.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
15 |
19 |
23 |
24 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/circular_progress.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | -
5 |
6 |
11 |
12 |
17 |
18 |
19 |
20 |
21 |
22 | -
23 |
24 |
29 |
30 |
35 |
36 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
7 |
9 |
10 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/app/src/main/res/font/amiko_bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sweakpl/unlock-master/e1c004607ce2474f82fb65737d05f092916ea0b2/app/src/main/res/font/amiko_bold.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/amiko_regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sweakpl/unlock-master/e1c004607ce2474f82fb65737d05f092916ea0b2/app/src/main/res/font/amiko_regular.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/amiko_semibold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sweakpl/unlock-master/e1c004607ce2474f82fb65737d05f092916ea0b2/app/src/main/res/font/amiko_semibold.ttf
--------------------------------------------------------------------------------
/app/src/main/res/layout-night/semi_transparent_blue_rect_marker_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/progress_bar.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/semi_transparent_blue_rect_marker_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/spinner_time_picker.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sweakpl/unlock-master/e1c004607ce2474f82fb65737d05f092916ea0b2/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sweakpl/unlock-master/e1c004607ce2474f82fb65737d05f092916ea0b2/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sweakpl/unlock-master/e1c004607ce2474f82fb65737d05f092916ea0b2/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sweakpl/unlock-master/e1c004607ce2474f82fb65737d05f092916ea0b2/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sweakpl/unlock-master/e1c004607ce2474f82fb65737d05f092916ea0b2/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sweakpl/unlock-master/e1c004607ce2474f82fb65737d05f092916ea0b2/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sweakpl/unlock-master/e1c004607ce2474f82fb65737d05f092916ea0b2/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sweakpl/unlock-master/e1c004607ce2474f82fb65737d05f092916ea0b2/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sweakpl/unlock-master/e1c004607ce2474f82fb65737d05f092916ea0b2/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sweakpl/unlock-master/e1c004607ce2474f82fb65737d05f092916ea0b2/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/xml-v31/unlock_count_widget_info.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
29 |
30 |
36 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/unlock_count_widget_info.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/test/java/com/sweak/unlockmaster/data/repository/CounterPausedEventsRepositoryFake.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.data.repository
2 |
3 | import com.sweak.unlockmaster.domain.model.UnlockMasterEvent.CounterPausedEvent
4 | import com.sweak.unlockmaster.domain.repository.CounterPausedEventsRepository
5 |
6 | class CounterPausedEventsRepositoryFake : CounterPausedEventsRepository {
7 |
8 | var counterPausedEventsSinceTimeToBeReturned: List = emptyList()
9 |
10 | override suspend fun addCounterPausedEvent(counterPausedEvent: CounterPausedEvent) {
11 | TODO("Not yet implemented")
12 | }
13 |
14 | override suspend fun getCounterPausedEventsSinceTime(sinceTimeInMillis: Long): List {
15 | return counterPausedEventsSinceTimeToBeReturned
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/sweak/unlockmaster/data/repository/CounterUnpausedEventsRepositoryFake.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.data.repository
2 |
3 | import com.sweak.unlockmaster.domain.model.UnlockMasterEvent.CounterUnpausedEvent
4 | import com.sweak.unlockmaster.domain.repository.CounterUnpausedEventsRepository
5 |
6 | class CounterUnpausedEventsRepositoryFake : CounterUnpausedEventsRepository {
7 |
8 | var counterUnpausedEventsSinceTimeToBeReturned: List = emptyList()
9 |
10 | override suspend fun addCounterUnpausedEvent(counterUnpausedEvent: CounterUnpausedEvent) {
11 | TODO("Not yet implemented")
12 | }
13 |
14 | override suspend fun getCounterUnpausedEventsSinceTime(sinceTimeInMillis: Long): List {
15 | return counterUnpausedEventsSinceTimeToBeReturned
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/sweak/unlockmaster/data/repository/LockEventsRepositoryFake.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.data.repository
2 |
3 | import com.sweak.unlockmaster.domain.model.UnlockMasterEvent.LockEvent
4 | import com.sweak.unlockmaster.domain.repository.LockEventsRepository
5 |
6 | class LockEventsRepositoryFake : LockEventsRepository {
7 |
8 | var lockEventsSinceTimeToBeReturned: List = emptyList()
9 |
10 | override suspend fun addLockEvent(lockEvent: LockEvent) {
11 | TODO("Not yet implemented")
12 | }
13 |
14 | override suspend fun getLockEventsSinceTime(sinceTimeInMillis: Long): List =
15 | lockEventsSinceTimeToBeReturned
16 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/sweak/unlockmaster/data/repository/TimeRepositoryFake.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.data.repository
2 |
3 | import com.sweak.unlockmaster.domain.repository.TimeRepository
4 | import com.sweak.unlockmaster.domain.toTimeInMillis
5 | import java.time.Instant
6 | import java.time.ZoneId
7 | import java.time.ZonedDateTime
8 |
9 | class TimeRepositoryFake : TimeRepository {
10 |
11 | var currentTimeInMillisToBeReturned: Long = 0
12 | var todayBeginningTimeInMillisToBeReturned: Long = 0
13 | var tomorrowBeginningTimeInMillisToBeReturned: Long = 0
14 | var sixDaysBeforeDayBeginningTimeInMillisToBeReturned: Long = 0
15 |
16 | override fun getCurrentTimeInMillis(): Long = currentTimeInMillisToBeReturned
17 |
18 | override fun getTodayBeginningTimeInMillis(): Long = todayBeginningTimeInMillisToBeReturned
19 |
20 | override fun getTomorrowBeginningTimeInMillis(): Long =
21 | tomorrowBeginningTimeInMillisToBeReturned
22 |
23 | override fun getSixDaysBeforeDayBeginningTimeInMillis(): Long =
24 | sixDaysBeforeDayBeginningTimeInMillisToBeReturned
25 |
26 | override fun getBeginningOfGivenDayTimeInMillis(timeInMillis: Long): Long =
27 | ZonedDateTime.ofInstant(
28 | Instant.ofEpochMilli(timeInMillis),
29 | ZoneId.systemDefault()
30 | )
31 | .withHour(0)
32 | .withMinute(0)
33 | .withSecond(0)
34 | .withNano(0)
35 | .toTimeInMillis()
36 |
37 | override fun getFutureTimeInMillisOfSpecifiedHourOfDayAndMinute(
38 | hourOfDay: Int,
39 | minute: Int
40 | ): Long {
41 | TODO("Not yet implemented")
42 | }
43 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/sweak/unlockmaster/data/repository/UnlockEventsRepositoryFake.kt:
--------------------------------------------------------------------------------
1 | package com.sweak.unlockmaster.data.repository
2 |
3 | import com.sweak.unlockmaster.domain.model.UnlockMasterEvent.UnlockEvent
4 | import com.sweak.unlockmaster.domain.repository.UnlockEventsRepository
5 |
6 | class UnlockEventsRepositoryFake : UnlockEventsRepository {
7 |
8 | var unlockEventsSinceTimeToBeReturned: List = emptyList()
9 | var firstUnlockEventToBeReturned: UnlockEvent? = null
10 |
11 | override suspend fun addUnlockEvent(unlockEvent: UnlockEvent) {
12 | TODO("Not yet implemented")
13 | }
14 |
15 | override suspend fun getUnlockEventsSinceTime(sinceTimeInMillis: Long): List =
16 | unlockEventsSinceTimeToBeReturned
17 |
18 | override suspend fun getUnlockEventsSinceTimeAndUntilTime(
19 | sinceTimeInMillis: Long,
20 | untilTimeInMillis: Long
21 | ): List {
22 | TODO("Not yet implemented")
23 | }
24 |
25 | override suspend fun getLatestUnlockEvent(): UnlockEvent? {
26 | TODO("Not yet implemented")
27 | }
28 |
29 | override suspend fun getFirstUnlockEvent(): UnlockEvent? = firstUnlockEventToBeReturned
30 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | dependencies {
3 | classpath 'com.google.dagger:hilt-android-gradle-plugin:2.51'
4 | }
5 | }
6 |
7 | plugins {
8 | id 'com.android.application' version '8.8.0' apply false
9 | id 'com.android.library' version '8.8.0' apply false
10 | id 'org.jetbrains.kotlin.android' version '1.9.10' apply false
11 | id 'com.google.devtools.ksp' version '1.9.10-1.0.13' apply false
12 | }
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/14.txt:
--------------------------------------------------------------------------------
1 | * Bug fixes.
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/full_description.txt:
--------------------------------------------------------------------------------
1 | ### UnlockMaster: Unlock the Power of Mindful Smartphone Use
2 |
3 | Are you tired of mindlessly unlocking your phone, only to find yourself lost in endless scrolling or compulsively checking your apps? Say hello to UnlockMaster, your ultimate companion for conscious smartphone usage.
4 |
5 | **Unlock Your Potential**
6 |
7 | UnlockMaster believes that every unlock can be a step towards a more mindful digital life. Our app empowers you to regain control over your screen time by tracking your unlocks and setting your own unlock limit.
8 |
9 | **Stay Informed**
10 |
11 | With real-time notifications, UnlockMaster keeps you in the loop. You'll receive updates on your unlock count in relation to your limit, serving as a friendly reminder to stay on track.
12 |
13 | **Set Your Goals**
14 |
15 | UnlockMaster is all about helping you achieve your smartphone usage goals. Receive motivational notifications as you're nearing your daily unlock limit, so you can make more conscious choices further in the day.
16 |
17 | **Reflect and Refine**
18 |
19 | At the end of each day, our app shows you a daily wrap-up notification. Tap it to discover insightful summaries, helpful suggestions for adjusting your unlock limit, and more.
20 |
21 | **Visualize Your Progress**
22 |
23 | UnlockMaster doesn't just track unlocks; it also provides you with eye-catching charts. Monitor your unlocks and screen time with beautiful charts, giving you a clear picture of your progress.
24 |
25 | ### Unlock the potential of mindful smartphone use with UnlockMaster.
26 | ### Take control of your digital life, *one unlock at a time*.
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sweakpl/unlock-master/e1c004607ce2474f82fb65737d05f092916ea0b2/fastlane/metadata/android/en-US/images/icon.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/DailyWrapUpScreen.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sweakpl/unlock-master/e1c004607ce2474f82fb65737d05f092916ea0b2/fastlane/metadata/android/en-US/images/phoneScreenshots/DailyWrapUpScreen.jpg
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/HomeScreen.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sweakpl/unlock-master/e1c004607ce2474f82fb65737d05f092916ea0b2/fastlane/metadata/android/en-US/images/phoneScreenshots/HomeScreen.jpg
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/ScreenTimeLimitSetupScreen.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sweakpl/unlock-master/e1c004607ce2474f82fb65737d05f092916ea0b2/fastlane/metadata/android/en-US/images/phoneScreenshots/ScreenTimeLimitSetupScreen.jpg
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/ScreenTimeScreen.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sweakpl/unlock-master/e1c004607ce2474f82fb65737d05f092916ea0b2/fastlane/metadata/android/en-US/images/phoneScreenshots/ScreenTimeScreen.jpg
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/StatisticsScreen.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sweakpl/unlock-master/e1c004607ce2474f82fb65737d05f092916ea0b2/fastlane/metadata/android/en-US/images/phoneScreenshots/StatisticsScreen.jpg
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/UnlockLimitSetupScreen.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sweakpl/unlock-master/e1c004607ce2474f82fb65737d05f092916ea0b2/fastlane/metadata/android/en-US/images/phoneScreenshots/UnlockLimitSetupScreen.jpg
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/short_description.txt:
--------------------------------------------------------------------------------
1 | App promoting mindful phone use by tracking unlocks, setting limits, and stats.
--------------------------------------------------------------------------------
/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.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
24 | android.defaults.buildfeatures.buildconfig=true
25 | android.nonFinalResIds=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sweakpl/unlock-master/e1c004607ce2474f82fb65737d05f092916ea0b2/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Jan 04 13:31:53 CET 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | google()
5 | mavenCentral()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | maven { url 'https://jitpack.io' }
14 | }
15 | }
16 | rootProject.name = "UnlockMaster"
17 | include ':app'
18 |
--------------------------------------------------------------------------------