├── .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 | Get it on Google Play 9 | Get it on IzzyOnDroid 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 | --------------------------------------------------------------------------------