From 67b1dd815d39c48fd006df8152b45eaca64dfef2 Mon Sep 17 00:00:00 2001 From: Mihai-Cristian Condrea Date: Sun, 20 Oct 2024 19:26:20 +0300 Subject: [PATCH] Reworked the app's main code for better structure. --- app/build.gradle.kts | 2 +- .../core/datastore/DataStoreCoreManager.kt | 2 +- .../d4rk/cleaner/data/datastore/DataStore.kt | 23 +++ .../ui/navigation/NavigationDrawerItem.kt | 2 +- .../data/model/ui/screens/UiMainModel.kt | 9 + .../workers/AppUsageNotificationWorker.kt | 2 +- .../ui/components/animations/Animations.kt | 2 +- .../ui/components/dialogs/ErrorAlertDialog.kt | 2 +- .../dialogs/SelectLanguageAlertDialog.kt | 8 +- .../components/navigation/NavigationDrawer.kt | 9 +- .../cleaner/ui/screens/home/HomeViewModel.kt | 4 +- .../screens/home/repository/HomeRepository.kt | 4 + .../ImageOptimizerComposable.kt | 2 +- .../imagepicker/ImagePickerScreen.kt | 2 +- .../cleaner/ui/screens/main/MainActivity.kt | 157 +++++------------- .../cleaner/ui/screens/main/MainScreen.kt | 11 +- .../cleaner/ui/screens/main/MainViewModel.kt | 53 +++++- .../screens/main/repository/MainRepository.kt | 49 +++++- .../MainRepositoryImplementation.kt | 56 ++++++- .../cleaning/CleaningSettingsComposable.kt | 2 +- .../display/DisplaySettingsComposable.kt | 2 +- .../display/theme/ThemeSettingsComposable.kt | 2 +- .../settings/display/theme/style/Theme.kt | 2 +- .../privacy/ads/AdsSettingsComposable.kt | 2 +- .../usage/UsageAndDiagnosticsComposable.kt | 2 +- .../ui/screens/support/SupportScreen.kt | 2 +- 26 files changed, 254 insertions(+), 159 deletions(-) create mode 100644 app/src/main/kotlin/com/d4rk/cleaner/data/model/ui/screens/UiMainModel.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1548a82..edebb81 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -14,7 +14,7 @@ android { applicationId = "com.d4rk.cleaner" minSdk = 23 targetSdk = 35 - versionCode = 134 + versionCode = 138 versionName = "3.0.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" resourceConfigurations += listOf( diff --git a/app/src/main/kotlin/com/d4rk/cleaner/data/core/datastore/DataStoreCoreManager.kt b/app/src/main/kotlin/com/d4rk/cleaner/data/core/datastore/DataStoreCoreManager.kt index 24a1c03..146b6f7 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/data/core/datastore/DataStoreCoreManager.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/data/core/datastore/DataStoreCoreManager.kt @@ -14,7 +14,7 @@ open class DataStoreCoreManager(protected val context: Context) { lateinit var dataStore: DataStore suspend fun initializeDataStore(): Boolean = coroutineScope { - dataStore = DataStore.getInstance(context.applicationContext) + dataStore = DataStore.getInstance(context = context.applicationContext) listOf( async { dataStore.getStartupPage().firstOrNull() ?: BottomBarRoutes.HOME }, diff --git a/app/src/main/kotlin/com/d4rk/cleaner/data/datastore/DataStore.kt b/app/src/main/kotlin/com/d4rk/cleaner/data/datastore/DataStore.kt index b8ba5e7..a00476d 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/data/datastore/DataStore.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/data/datastore/DataStore.kt @@ -182,6 +182,29 @@ class DataStore(context: Context) { } } + private val trashSizeKey = longPreferencesKey(name = "trash_size") + val trashSize: Flow = dataStore.data.map { preferences -> + preferences[trashSizeKey] ?: 0L + } + + suspend fun addTrashSize(size: Long) { + dataStore.edit { preferences -> + preferences[trashSizeKey] = (preferences[trashSizeKey] ?: 0L) + size + } + } + + suspend fun subtractTrashSize(size: Long) { + dataStore.edit { preferences -> + preferences[trashSizeKey] = (preferences[trashSizeKey] ?: 0L) - size + } + } + + suspend fun clearTrashSize() { + dataStore.edit { preferences -> + preferences[trashSizeKey] = 0L + } + } + private val genericFilterKey = booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_GENERIC_FILTER) val genericFilter: Flow = dataStore.data.map { preferences -> diff --git a/app/src/main/kotlin/com/d4rk/cleaner/data/model/ui/navigation/NavigationDrawerItem.kt b/app/src/main/kotlin/com/d4rk/cleaner/data/model/ui/navigation/NavigationDrawerItem.kt index 0d946db..f460d88 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/data/model/ui/navigation/NavigationDrawerItem.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/data/model/ui/navigation/NavigationDrawerItem.kt @@ -3,5 +3,5 @@ package com.d4rk.cleaner.data.model.ui.navigation import androidx.compose.ui.graphics.vector.ImageVector data class NavigationDrawerItem( - val title: Int, val selectedIcon: ImageVector, val badgeCount: Int? = null + val title: Int, val selectedIcon: ImageVector, val badgeText: String = "" ) \ No newline at end of file diff --git a/app/src/main/kotlin/com/d4rk/cleaner/data/model/ui/screens/UiMainModel.kt b/app/src/main/kotlin/com/d4rk/cleaner/data/model/ui/screens/UiMainModel.kt new file mode 100644 index 0000000..c351f7a --- /dev/null +++ b/app/src/main/kotlin/com/d4rk/cleaner/data/model/ui/screens/UiMainModel.kt @@ -0,0 +1,9 @@ +package com.d4rk.cleaner.data.model.ui.screens + +import com.d4rk.cleaner.data.model.ui.navigation.BottomNavigationScreen + +data class UiMainModel( + val isNavigationDrawerOpen: Boolean = false, + val currentBottomNavigationScreen: BottomNavigationScreen = BottomNavigationScreen.Home, + val trashSize: String = "0 KB", +) \ No newline at end of file diff --git a/app/src/main/kotlin/com/d4rk/cleaner/notifications/workers/AppUsageNotificationWorker.kt b/app/src/main/kotlin/com/d4rk/cleaner/notifications/workers/AppUsageNotificationWorker.kt index 1aa2844..1bc83d0 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/notifications/workers/AppUsageNotificationWorker.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/notifications/workers/AppUsageNotificationWorker.kt @@ -25,7 +25,7 @@ import kotlinx.coroutines.runBlocking */ class AppUsageNotificationWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) { - private val dataStore = DataStore.getInstance(context) + private val dataStore = DataStore.getInstance(context = context) private val appUsageChannelId = "app_usage_channel" private val appUsageNotificationId = 0 diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/components/animations/Animations.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/components/animations/Animations.kt index bb3b7bd..099d662 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/components/animations/Animations.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/components/animations/Animations.kt @@ -30,7 +30,7 @@ fun Modifier.bounceClick( ) : Modifier = composed { var buttonState : ButtonState by remember { mutableStateOf(ButtonState.Idle) } val context : Context = LocalContext.current - val dataStore : DataStore = DataStore.getInstance(context) + val dataStore : DataStore = DataStore.getInstance(context = context) val bouncyButtonsEnabled : Boolean by dataStore.bouncyButtons.collectAsState(initial = true) val scale : Float by animateFloatAsState( if (buttonState == ButtonState.Pressed && animationEnabled && bouncyButtonsEnabled) 0.96f else 1f , diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/components/dialogs/ErrorAlertDialog.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/components/dialogs/ErrorAlertDialog.kt index bf569f9..168949b 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/components/dialogs/ErrorAlertDialog.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/components/dialogs/ErrorAlertDialog.kt @@ -14,7 +14,7 @@ fun ErrorAlertDialog( AlertDialog( onDismissRequest = onDismiss, title = { Text(text = "Error") }, - text = { Text(errorMessage) }, + text = { Text(text = errorMessage) }, confirmButton = { TextButton(onClick = onDismiss) { Text(text = stringResource(id = android.R.string.ok)) diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/components/dialogs/SelectLanguageAlertDialog.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/components/dialogs/SelectLanguageAlertDialog.kt index a78d7b6..970bd28 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/components/dialogs/SelectLanguageAlertDialog.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/components/dialogs/SelectLanguageAlertDialog.kt @@ -53,11 +53,11 @@ fun SelectLanguageAlertDialog( onLanguageSelected(selectedLanguage.value) onDismiss() }) { - Text(stringResource(id = android.R.string.ok)) + Text(text = stringResource(id = android.R.string.ok)) } }, dismissButton = { TextButton(onClick = onDismiss) { - Text(stringResource(id = android.R.string.cancel)) + Text(text = stringResource(id = android.R.string.cancel)) } }) } @@ -74,7 +74,7 @@ fun SelectLanguageAlertDialogContent( } Column { - Text(stringResource(id = R.string.dialog_language_subtitle)) + Text(text = stringResource(id = R.string.dialog_language_subtitle)) Box( modifier = Modifier .fillMaxWidth() @@ -103,7 +103,7 @@ fun SelectLanguageAlertDialogContent( Spacer(modifier = Modifier.height(24.dp)) Icon(imageVector = Icons.Outlined.Info, contentDescription = null) Spacer(modifier = Modifier.height(12.dp)) - Text(stringResource(id = R.string.dialog_info_language)) + Text(text = stringResource(id = R.string.dialog_info_language)) } LaunchedEffect(selectedLanguage.value) { diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/components/navigation/NavigationDrawer.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/components/navigation/NavigationDrawer.kt index 02a29d2..6d3a25e 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/components/navigation/NavigationDrawer.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/components/navigation/NavigationDrawer.kt @@ -39,6 +39,7 @@ import androidx.navigation.NavHostController import com.d4rk.cleaner.R import com.d4rk.cleaner.data.datastore.DataStore import com.d4rk.cleaner.data.model.ui.navigation.NavigationDrawerItem +import com.d4rk.cleaner.data.model.ui.screens.UiMainModel import com.d4rk.cleaner.ui.components.animations.bounceClick import com.d4rk.cleaner.ui.components.animations.hapticDrawerSwipe import com.d4rk.cleaner.ui.screens.help.HelpActivity @@ -57,15 +58,15 @@ fun NavigationDrawer( view: View, dataStore: DataStore, context: Context, + uiState: UiMainModel, ) { val scope: CoroutineScope = rememberCoroutineScope() - val drawerItems: List = listOf( NavigationDrawerItem( title = R.string.image_optimizer, selectedIcon = Icons.Outlined.Image ), NavigationDrawerItem( - title = R.string.trash, selectedIcon = Icons.Outlined.DeleteOutline + title = R.string.trash, selectedIcon = Icons.Outlined.DeleteOutline, badgeText = uiState.trashSize, ), NavigationDrawerItem( title = R.string.settings, @@ -164,7 +165,9 @@ fun NavigationDrawer( ) }, badge = { - item.badgeCount?.let { Text(text = it.toString()) } + item.badgeText.isNotBlank().let { + Text(text = item.badgeText) + } }, modifier = Modifier .padding( diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/home/HomeViewModel.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/home/HomeViewModel.kt index 334e9a4..820a43a 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/home/HomeViewModel.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/home/HomeViewModel.kt @@ -222,9 +222,10 @@ class HomeViewModel(application : Application) : BaseViewModel(application) { */ fun moveToTrash() { viewModelScope.launch(context = Dispatchers.Default + coroutineExceptionHandler) { + showLoading() val filesToMove = _uiState.value.analyzeState.fileSelectionMap.filter { it.value }.keys.toList() - showLoading() + val totalTrashSize = filesToMove.sumOf { it.length() } repository.moveToTrash(filesToMove) { _uiState.update { currentUiState -> currentUiState.copy(analyzeState = currentUiState.analyzeState.copy( @@ -237,6 +238,7 @@ class HomeViewModel(application : Application) : BaseViewModel(application) { } updateStorageInfo() } + repository.addTrashSize(totalTrashSize) hideLoading() } } diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/home/repository/HomeRepository.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/home/repository/HomeRepository.kt index 0c351a7..30e2c80 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/home/repository/HomeRepository.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/home/repository/HomeRepository.kt @@ -140,6 +140,10 @@ class HomeRepository( } } + suspend fun addTrashSize(size: Long) { + dataStore.addTrashSize(size) + } + /** * Restores the specified files from the trash directory. * @param filesToRestore The set of files to restore from trash. diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/imageoptimizer/imageoptimizer/ImageOptimizerComposable.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/imageoptimizer/imageoptimizer/ImageOptimizerComposable.kt index d01c9ba..8f6970c 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/imageoptimizer/imageoptimizer/ImageOptimizerComposable.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/imageoptimizer/imageoptimizer/ImageOptimizerComposable.kt @@ -49,7 +49,7 @@ fun ImageOptimizerComposable( activity: ImageOptimizerActivity , viewModel: ImageOptimizerViewModel ) { val context: Context = LocalContext.current - val dataStore: DataStore = DataStore.getInstance(context) + val dataStore: DataStore = DataStore.getInstance(context = context) val coroutineScope: CoroutineScope = rememberCoroutineScope() val adsState: State = dataStore.ads.collectAsState(initial = true) val tabs: List = listOf( diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/imageoptimizer/imagepicker/ImagePickerScreen.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/imageoptimizer/imagepicker/ImagePickerScreen.kt index 5baa409..74616b9 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/imageoptimizer/imagepicker/ImagePickerScreen.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/imageoptimizer/imagepicker/ImagePickerScreen.kt @@ -44,7 +44,7 @@ fun ImagePickerComposable( ) { val context: Context = LocalContext.current val view: View = LocalView.current - val dataStore: DataStore = DataStore.getInstance(context) + val dataStore: DataStore = DataStore.getInstance(context = context) val adsState: State = dataStore.ads.collectAsState(initial = true) LaunchedEffect(key1 = viewModel.selectedImageUri) { diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/main/MainActivity.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/main/MainActivity.kt index d574da4..6d5ef2b 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/main/MainActivity.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/main/MainActivity.kt @@ -5,6 +5,7 @@ import android.os.Build import android.os.Bundle import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.layout.fillMaxSize @@ -12,51 +13,39 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.ui.Modifier import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen -import androidx.lifecycle.lifecycleScope -import com.android.volley.NoConnectionError -import com.android.volley.TimeoutError -import com.d4rk.cleaner.BuildConfig import com.d4rk.cleaner.R import com.d4rk.cleaner.data.core.AppCoreManager import com.d4rk.cleaner.data.datastore.DataStore import com.d4rk.cleaner.notifications.managers.AppUpdateNotificationsManager -import com.d4rk.cleaner.notifications.managers.AppUsageNotificationsManager import com.d4rk.cleaner.ui.screens.settings.display.theme.style.AppTheme import com.google.android.gms.ads.MobileAds import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar -import com.google.android.play.core.appupdate.AppUpdateInfo import com.google.android.play.core.appupdate.AppUpdateManager import com.google.android.play.core.appupdate.AppUpdateManagerFactory import com.google.android.play.core.install.model.ActivityResult -import com.google.android.play.core.install.model.AppUpdateType -import com.google.android.play.core.install.model.UpdateAvailability -import kotlinx.coroutines.launch -import kotlinx.coroutines.tasks.await class MainActivity : AppCompatActivity() { - private lateinit var dataStore: DataStore - private lateinit var appUpdateManager: AppUpdateManager - private var appUpdateNotificationsManager: AppUpdateNotificationsManager = - AppUpdateNotificationsManager(this) + private lateinit var dataStore : DataStore + private val viewModel : MainViewModel by viewModels() + private lateinit var appUpdateManager : AppUpdateManager + private lateinit var appUpdateNotificationsManager : AppUpdateNotificationsManager - override fun onCreate(savedInstanceState: Bundle?) { + override fun onCreate(savedInstanceState : Bundle?) { super.onCreate(savedInstanceState) installSplashScreen().apply { setKeepOnScreenCondition { - !(application as AppCoreManager).isAppLoaded() + ! (application as AppCoreManager).isAppLoaded() } } enableEdgeToEdge() - dataStore = DataStore.getInstance(this@MainActivity) - MobileAds.initialize(this@MainActivity) - setupUpdateNotifications() + initializeActivityComponents() setContent { AppTheme { Surface( - modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background + modifier = Modifier.fillMaxSize() , color = MaterialTheme.colorScheme.background ) { - MainComposable() + MainScreen(viewModel = viewModel) } } } @@ -65,10 +54,14 @@ class MainActivity : AppCompatActivity() { @RequiresApi(Build.VERSION_CODES.O) override fun onResume() { super.onResume() - val appUsageNotificationsManager = AppUsageNotificationsManager(this) - appUsageNotificationsManager.scheduleAppUsageCheck() - appUpdateNotificationsManager.checkAndSendUpdateNotification() - checkForFlexibleUpdate() + with(viewModel) { + checkAndHandleStartup() + configureSettings() + loadTrashSize() + checkForUpdates(activity = this@MainActivity , appUpdateManager = appUpdateManager) + checkAndScheduleUpdateNotifications(appUpdateNotificationsManager) + checkAppUsageNotifications() + } } /** @@ -85,10 +78,10 @@ class MainActivity : AppCompatActivity() { @Suppress("DEPRECATION") override fun onBackPressed() { MaterialAlertDialogBuilder(this).setTitle(R.string.close).setMessage(R.string.summary_close) - .setPositiveButton(android.R.string.yes) { _, _ -> - super.onBackPressed() - moveTaskToBack(true) - }.setNegativeButton(android.R.string.no, null).apply { show() } + .setPositiveButton(android.R.string.yes) { _ , _ -> + super.onBackPressed() + moveTaskToBack(true) + }.setNegativeButton(android.R.string.no , null).apply { show() } } /** @@ -105,16 +98,13 @@ class MainActivity : AppCompatActivity() { * @param data An Intent, which can return result data to the caller (various data can be attached to Intent "extras"). */ @Deprecated("Deprecated in Java") - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) + override fun onActivityResult(requestCode : Int , resultCode : Int , data : Intent?) { + super.onActivityResult(requestCode , resultCode , data) + println("Cleaner for Android -> Play Update: onActivityResult: requestCode=$requestCode, resultCode=$resultCode") if (requestCode == 1) { when (resultCode) { RESULT_OK -> { - val snackbar: Snackbar = Snackbar.make( - findViewById(android.R.id.content), R.string.snack_app_updated, - Snackbar.LENGTH_LONG - ).setAction(android.R.string.ok, null) - snackbar.show() + showUpdateSuccessfulSnackbar() } ActivityResult.RESULT_IN_APP_UPDATE_FAILED -> { @@ -124,78 +114,18 @@ class MainActivity : AppCompatActivity() { } } - /** - * Checks for the availability of updates and triggers the appropriate update flow if conditions are met. - * - * This function uses the lifecycle scope to asynchronously check for available updates using the - * Google Play Core library. If an update is available and meets certain conditions, it triggers - * the update flow. The update can be of two types: IMMEDIATE or FLEXIBLE. - * - * For an IMMEDIATE update, it checks if the client version is more than 90 days old. If so, it triggers the update. - * For a FLEXIBLE update, it checks if the client version is less than 90 days old. If so, it triggers the update. - * - * The function also ensures that no developer-triggered update is in progress before triggering a new update. - * - * @param lifecycleScope The lifecycle scope used for launching coroutines, obtained from the hosting activity. - */ - private fun checkForFlexibleUpdate() { - lifecycleScope.launch { - try { - val appUpdateInfo: AppUpdateInfo = appUpdateManager.appUpdateInfo.await() - if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE && appUpdateInfo.isUpdateTypeAllowed( - AppUpdateType.IMMEDIATE - ) && appUpdateInfo.updateAvailability() != UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS - ) { - @Suppress("DEPRECATION") appUpdateManager.appUpdateInfo.addOnSuccessListener { info -> - when { - info.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE && info.isUpdateTypeAllowed( - AppUpdateType.IMMEDIATE - ) -> { - info.clientVersionStalenessDays()?.let { - if (it > 90) { - appUpdateManager.startUpdateFlowForResult( - info, AppUpdateType.IMMEDIATE, this@MainActivity, 1 - ) - } - } - } - - info.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE && info.isUpdateTypeAllowed( - AppUpdateType.FLEXIBLE - ) -> { - info.clientVersionStalenessDays()?.let { - if (it < 90) { - appUpdateManager.startUpdateFlowForResult( - info, AppUpdateType.FLEXIBLE, this@MainActivity, 1 - ) - } - } - } - } - } - } - } catch (e: Exception) { - if (!BuildConfig.DEBUG) { - when (e) { - is NoConnectionError, is TimeoutError -> { - Snackbar.make( - findViewById(android.R.id.content), - getString(R.string.snack_network_error), - Snackbar.LENGTH_LONG - ).show() - } + private fun initializeActivityComponents() { + MobileAds.initialize(this@MainActivity) + dataStore = DataStore.getInstance(context = this@MainActivity) + appUpdateManager = AppUpdateManagerFactory.create(this@MainActivity) + appUpdateNotificationsManager = AppUpdateNotificationsManager(this) + } - else -> { - Snackbar.make( - findViewById(android.R.id.content), - getString(R.string.snack_general_error), - Snackbar.LENGTH_LONG - ).show() - } - } - } - } - } + private fun showUpdateSuccessfulSnackbar() { + val snackbar : Snackbar = Snackbar.make( + findViewById(android.R.id.content) , R.string.snack_app_updated , Snackbar.LENGTH_LONG + ).setAction(android.R.string.ok , null) + snackbar.show() } /** @@ -206,16 +136,13 @@ class MainActivity : AppCompatActivity() { * to check for updates and initiate the appropriate update flow if conditions are met. */ private fun showUpdateFailedSnackbar() { - val snackbar: Snackbar = Snackbar.make( - findViewById(android.R.id.content), R.string.snack_update_failed, Snackbar.LENGTH_LONG + val snackbar : Snackbar = Snackbar.make( + findViewById(android.R.id.content) , R.string.snack_update_failed , Snackbar.LENGTH_LONG ).setAction(R.string.try_again) { - checkForFlexibleUpdate() + viewModel.checkForUpdates( + activity = this@MainActivity , appUpdateManager = appUpdateManager + ) } snackbar.show() } - - private fun setupUpdateNotifications() { - appUpdateManager = AppUpdateManagerFactory.create(this) - appUpdateNotificationsManager = AppUpdateNotificationsManager(this) - } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/main/MainScreen.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/main/MainScreen.kt index 1afb11e..5da88ee 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/main/MainScreen.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/main/MainScreen.kt @@ -6,26 +6,31 @@ import androidx.compose.material3.DrawerState import androidx.compose.material3.DrawerValue import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController import com.d4rk.cleaner.data.datastore.DataStore +import com.d4rk.cleaner.data.model.ui.screens.UiMainModel import com.d4rk.cleaner.ui.components.navigation.NavigationDrawer @Composable -fun MainComposable() { +fun MainScreen(viewModel : MainViewModel) { val drawerState : DrawerState = rememberDrawerState(initialValue = DrawerValue.Closed) val navController : NavHostController = rememberNavController() val context : Context = LocalContext.current val view : View = LocalView.current - val dataStore : DataStore = DataStore.getInstance(context) + val dataStore : DataStore = DataStore.getInstance(context = context) + val uiState : UiMainModel by viewModel.uiState.collectAsState() NavigationDrawer( navHostController = navController , drawerState = drawerState , view = view , dataStore = dataStore , - context = context + context = context , + uiState = uiState , ) } \ No newline at end of file diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/main/MainViewModel.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/main/MainViewModel.kt index 417c31c..f1f1c79 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/main/MainViewModel.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/main/MainViewModel.kt @@ -1,33 +1,68 @@ package com.d4rk.cleaner.ui.screens.main +import android.app.Activity import android.app.Application +import android.os.Build +import androidx.annotation.RequiresApi import androidx.lifecycle.viewModelScope import com.d4rk.cleaner.data.datastore.DataStore +import com.d4rk.cleaner.data.model.ui.screens.UiMainModel +import com.d4rk.cleaner.notifications.managers.AppUpdateNotificationsManager import com.d4rk.cleaner.ui.screens.main.repository.MainRepository import com.d4rk.cleaner.ui.screens.startup.StartupActivity -import com.d4rk.cleaner.utils.IntentUtils import com.d4rk.cleaner.ui.viewmodel.BaseViewModel +import com.d4rk.cleaner.utils.IntentUtils +import com.google.android.play.core.appupdate.AppUpdateManager +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -class MainViewModel(application: Application) : BaseViewModel(application) { - private val repository = MainRepository(DataStore(application), application) +class MainViewModel(application : Application) : BaseViewModel(application) { + private val repository = MainRepository(DataStore(application) , application) + private val _uiState = MutableStateFlow(UiMainModel()) + val uiState : StateFlow = _uiState - init { - checkAndHandleStartup() - configureSettings() + fun loadTrashSize() { + viewModelScope.launch(coroutineExceptionHandler) { + repository.getTrashSize { trashSize -> + _uiState.update { it.copy(trashSize = trashSize) } + } + } + } + + fun checkForUpdates(activity : Activity , appUpdateManager : AppUpdateManager) { + viewModelScope.launch(coroutineExceptionHandler) { + repository.checkForUpdates( + appUpdateManager = appUpdateManager , activity = activity + ) + } + } + + @RequiresApi(Build.VERSION_CODES.O) + fun checkAndScheduleUpdateNotifications(appUpdateNotificationsManager : AppUpdateNotificationsManager) { + viewModelScope.launch(coroutineExceptionHandler) { + repository.checkAndScheduleUpdateNotifications(appUpdateNotificationsManager) + } + } + + fun checkAppUsageNotifications() { + viewModelScope.launch(coroutineExceptionHandler) { + repository.checkAppUsageNotifications() + } } - private fun checkAndHandleStartup() { + fun checkAndHandleStartup() { viewModelScope.launch(coroutineExceptionHandler) { repository.checkAndHandleStartup { isFirstTime -> if (isFirstTime) { - IntentUtils.openActivity(getApplication(), StartupActivity::class.java) + IntentUtils.openActivity(getApplication() , StartupActivity::class.java) } } } } - private fun configureSettings() { + fun configureSettings() { viewModelScope.launch(coroutineExceptionHandler) { repository.setupSettings() } diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/main/repository/MainRepository.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/main/repository/MainRepository.kt index 0cca0da..adfcde9 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/main/repository/MainRepository.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/main/repository/MainRepository.kt @@ -1,7 +1,13 @@ package com.d4rk.cleaner.ui.screens.main.repository +import android.app.Activity import android.app.Application +import android.os.Build +import androidx.annotation.RequiresApi import com.d4rk.cleaner.data.datastore.DataStore +import com.d4rk.cleaner.notifications.managers.AppUpdateNotificationsManager +import com.d4rk.cleaner.utils.cleaning.StorageUtils +import com.google.android.play.core.appupdate.AppUpdateManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.first import kotlinx.coroutines.withContext @@ -12,9 +18,18 @@ import kotlinx.coroutines.withContext * @property dataStore The data store used to persist settings and startup information. * @property application The application context. */ -class MainRepository(val dataStore: DataStore, application: Application) : +class MainRepository(val dataStore : DataStore , application : Application) : MainRepositoryImplementation(application) { + suspend fun checkForUpdates( + activity : Activity , + appUpdateManager : AppUpdateManager , + ) { + withContext(Dispatchers.IO) { + checkForUpdatesLogic(activity , appUpdateManager) + } + } + /** * Checks the application startup state and performs actions based on whether it's the first launch. * @@ -23,15 +38,28 @@ class MainRepository(val dataStore: DataStore, application: Application) : * * @param onSuccess A callback function that receives a boolean indicating if it's the first launch. */ - suspend fun checkAndHandleStartup(onSuccess: (Boolean) -> Unit) { + suspend fun checkAndHandleStartup(onSuccess : (Boolean) -> Unit) { withContext(Dispatchers.IO) { - val isFirstTime: Boolean = checkStartup() + val isFirstTime : Boolean = checkStartup() withContext(Dispatchers.Main) { onSuccess(isFirstTime) } } } + @RequiresApi(Build.VERSION_CODES.O) + suspend fun checkAndScheduleUpdateNotifications(appUpdateNotificationsManager : AppUpdateNotificationsManager) { + withContext(Dispatchers.IO) { + checkAndScheduleUpdateNotificationsLogic(appUpdateNotificationsManager) + } + } + + suspend fun checkAppUsageNotifications() { + withContext(Dispatchers.IO) { + checkAppUsageNotificationsManager() + } + } + /** * Sets up Firebase Analytics and Crashlytics based on stored settings. * @@ -40,10 +68,23 @@ class MainRepository(val dataStore: DataStore, application: Application) : */ suspend fun setupSettings() { withContext(Dispatchers.IO) { - val isEnabled: Boolean = dataStore.usageAndDiagnostics.first() + val isEnabled : Boolean = dataStore.usageAndDiagnostics.first() withContext(Dispatchers.Main) { setupDiagnosticSettings(isEnabled) } } } + + /** + * Retrieves the current trash size from DataStore and formats it. + */ + suspend fun getTrashSize(onSuccess : (String) -> Unit) { + withContext(Dispatchers.IO) { + val size = dataStore.trashSize.first() + val formattedSize = StorageUtils.formatSize(size) + withContext(Dispatchers.Main) { + onSuccess(formattedSize) + } + } + } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/main/repository/MainRepositoryImplementation.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/main/repository/MainRepositoryImplementation.kt index 6713f38..f8c39cb 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/main/repository/MainRepositoryImplementation.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/main/repository/MainRepositoryImplementation.kt @@ -1,10 +1,20 @@ package com.d4rk.cleaner.ui.screens.main.repository +import android.app.Activity import android.app.Application +import android.os.Build +import androidx.annotation.RequiresApi import com.d4rk.cleaner.data.datastore.DataStore +import com.d4rk.cleaner.notifications.managers.AppUpdateNotificationsManager +import com.d4rk.cleaner.notifications.managers.AppUsageNotificationsManager +import com.google.android.play.core.appupdate.AppUpdateManager +import com.google.android.play.core.install.model.ActivityResult +import com.google.android.play.core.install.model.AppUpdateType +import com.google.android.play.core.install.model.UpdateAvailability import com.google.firebase.analytics.FirebaseAnalytics import com.google.firebase.crashlytics.FirebaseCrashlytics import kotlinx.coroutines.flow.first +import kotlinx.coroutines.tasks.await import com.d4rk.cleaner.data.datastore.DataStore.Companion as DataStore1 /** @@ -14,7 +24,7 @@ import com.d4rk.cleaner.data.datastore.DataStore.Companion as DataStore1 * * @property application The application context. */ -abstract class MainRepositoryImplementation(val application: Application) { +abstract class MainRepositoryImplementation(val application : Application) { /** * Checks if the application is being launched for the first time. @@ -23,9 +33,9 @@ abstract class MainRepositoryImplementation(val application: Application) { * * @return `true` if it's the first launch, `false` otherwise. */ - suspend fun checkStartup(): Boolean { - val dataStore: DataStore = DataStore1.getInstance(application) - val isFirstTime: Boolean = dataStore.startup.first() + suspend fun checkStartup() : Boolean { + val dataStore : DataStore = DataStore1.getInstance(application) + val isFirstTime : Boolean = dataStore.startup.first() if (isFirstTime) { dataStore.saveStartup(isFirstTime = false) } @@ -40,8 +50,44 @@ abstract class MainRepositoryImplementation(val application: Application) { * * @param isEnabled `true` to enable data collection, `false` to disable. */ - fun setupDiagnosticSettings(isEnabled: Boolean) { + fun setupDiagnosticSettings(isEnabled : Boolean) { FirebaseAnalytics.getInstance(application).setAnalyticsCollectionEnabled(isEnabled) FirebaseCrashlytics.getInstance().isCrashlyticsCollectionEnabled = isEnabled } + + suspend fun checkForUpdatesLogic(activity : Activity, appUpdateManager : AppUpdateManager) : Int { + + try { + var updateResult = Activity.RESULT_CANCELED + val appUpdateInfo = appUpdateManager.appUpdateInfo.await() + + if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE && appUpdateInfo.isUpdateTypeAllowed( + AppUpdateType.IMMEDIATE + ) && appUpdateInfo.updateAvailability() != UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS + ) { + appUpdateInfo.clientVersionStalenessDays()?.let { stalenessDays -> + val updateType = + if (stalenessDays > 90) AppUpdateType.IMMEDIATE else AppUpdateType.FLEXIBLE + @Suppress("DEPRECATION") appUpdateManager.startUpdateFlowForResult( + appUpdateInfo , updateType , activity , 1 + ) + updateResult = Activity.RESULT_OK + } + } + return updateResult + + } catch (e : Exception) { + return ActivityResult.RESULT_IN_APP_UPDATE_FAILED + } + } + + @RequiresApi(Build.VERSION_CODES.O) + fun checkAndScheduleUpdateNotificationsLogic(appUpdateNotificationsManager : AppUpdateNotificationsManager) { + appUpdateNotificationsManager.checkAndSendUpdateNotification() + } + + fun checkAppUsageNotificationsManager() { + val appUsageNotificationsManager = AppUsageNotificationsManager(application) + appUsageNotificationsManager.scheduleAppUsageCheck() + } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/settings/cleaning/CleaningSettingsComposable.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/settings/cleaning/CleaningSettingsComposable.kt index 3fbba41..ca73a1f 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/settings/cleaning/CleaningSettingsComposable.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/settings/cleaning/CleaningSettingsComposable.kt @@ -22,7 +22,7 @@ import kotlinx.coroutines.launch @Composable fun CleaningSettingsComposable(activity: CleaningSettingsActivity) { val context: Context = LocalContext.current - val dataStore: DataStore = DataStore.getInstance(context) + val dataStore: DataStore = DataStore.getInstance(context = context) val genericFilter: Boolean by dataStore.genericFilter.collectAsState(initial = true) val deleteEmptyFolders: Boolean by dataStore.deleteEmptyFolders.collectAsState(initial = true) val deleteArchives: Boolean by dataStore.deleteArchives.collectAsState(initial = false) diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/settings/display/DisplaySettingsComposable.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/settings/display/DisplaySettingsComposable.kt index 507609f..93b9dc8 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/settings/display/DisplaySettingsComposable.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/settings/display/DisplaySettingsComposable.kt @@ -40,7 +40,7 @@ import kotlinx.coroutines.launch @Composable fun DisplaySettingsComposable(activity: DisplaySettingsActivity) { val context: Context = LocalContext.current - val dataStore: DataStore = DataStore.getInstance(context) + val dataStore: DataStore = DataStore.getInstance(context = context) var showLanguageDialog: Boolean by remember { mutableStateOf(value = false) } var showStartupDialog: Boolean by remember { mutableStateOf(value = false) } val themeMode: String = dataStore.themeMode.collectAsState(initial = "follow_system").value diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/settings/display/theme/ThemeSettingsComposable.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/settings/display/theme/ThemeSettingsComposable.kt index e7bdb43..1cba087 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/settings/display/theme/ThemeSettingsComposable.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/settings/display/theme/ThemeSettingsComposable.kt @@ -37,7 +37,7 @@ import kotlinx.coroutines.launch @Composable fun ThemeSettingsComposable(activity: ThemeSettingsActivity) { val context: Context = LocalContext.current - val dataStore: DataStore = DataStore.getInstance(context) + val dataStore: DataStore = DataStore.getInstance(context = context) val scope: CoroutineScope = rememberCoroutineScope() val themeMode: String = dataStore.themeMode.collectAsState(initial = "follow_system").value val isAmoledMode: State = dataStore.amoledMode.collectAsState(initial = false) diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/settings/display/theme/style/Theme.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/settings/display/theme/style/Theme.kt index c493407..04a6338 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/settings/display/theme/style/Theme.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/settings/display/theme/style/Theme.kt @@ -145,7 +145,7 @@ fun AppTheme( content: @Composable () -> Unit ) { val context: Context = LocalContext.current - val dataStore: DataStore = DataStore.getInstance(context) + val dataStore: DataStore = DataStore.getInstance(context = context) val themeMode: String = dataStore.themeMode.collectAsState(initial = "follow_system").value val isDynamicColors: Boolean = dataStore.dynamicColors.collectAsState(initial = true).value val isAmoledMode: Boolean = dataStore.amoledMode.collectAsState(initial = false).value diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/settings/privacy/ads/AdsSettingsComposable.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/settings/privacy/ads/AdsSettingsComposable.kt index 48abb08..40c73c4 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/settings/privacy/ads/AdsSettingsComposable.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/settings/privacy/ads/AdsSettingsComposable.kt @@ -46,7 +46,7 @@ import kotlinx.coroutines.launch @Composable fun AdsSettingsComposable(activity: AdsSettingsActivity) { val context: Context = LocalContext.current - val dataStore: DataStore = DataStore.getInstance(context) + val dataStore: DataStore = DataStore.getInstance(context = context) val switchState: State = dataStore.ads.collectAsState(initial = ! BuildConfig.DEBUG) val scope: CoroutineScope = rememberCoroutineScope() TopAppBarScaffoldWithBackButton( diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/settings/privacy/usage/UsageAndDiagnosticsComposable.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/settings/privacy/usage/UsageAndDiagnosticsComposable.kt index 16359f5..4de8fe4 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/settings/privacy/usage/UsageAndDiagnosticsComposable.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/settings/privacy/usage/UsageAndDiagnosticsComposable.kt @@ -42,7 +42,7 @@ import kotlinx.coroutines.launch @Composable fun UsageAndDiagnosticsComposable(activity: UsageAndDiagnosticsActivity) { val context: Context = LocalContext.current - val dataStore: DataStore = DataStore.getInstance(context) + val dataStore: DataStore = DataStore.getInstance(context = context) val switchState: State = dataStore.usageAndDiagnostics.collectAsState(initial = ! BuildConfig.DEBUG) val scope: CoroutineScope = rememberCoroutineScope() TopAppBarScaffoldWithBackButton( diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/support/SupportScreen.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/support/SupportScreen.kt index 4a2b790..abe256f 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/support/SupportScreen.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/support/SupportScreen.kt @@ -45,7 +45,7 @@ import com.d4rk.cleaner.ui.components.navigation.TopAppBarScaffoldWithBackButton fun SupportComposable(viewModel: SupportViewModel , activity: SupportActivity) { val context: Context = LocalContext.current val view: View = LocalView.current - val dataStore: DataStore = DataStore.getInstance(context) + val dataStore: DataStore = DataStore.getInstance(context = context) val billingClient: BillingClient = rememberBillingClient(context, viewModel) TopAppBarScaffoldWithBackButton( title = stringResource(id = R.string.support_us),