From ecf725bd394c2be0de45d4895e69aa39857d32b6 Mon Sep 17 00:00:00 2001 From: D4rK7355608 Date: Thu, 26 Sep 2024 09:42:52 +0300 Subject: [PATCH] Added support for more Android devices + better structure at the app startup thanks to app core manager --- app/build.gradle.kts | 2 +- .../d4rk/cleaner/data/core/AppCoreManager.kt | 159 +++------ .../cleaner/data/core/ads/AdsCoreManager.kt | 121 +++++++ .../core/datastore/DataStoreCoreManager.kt | 28 ++ .../d4rk/cleaner/data/datastore/DataStore.kt | 63 ++-- .../cleaner/ui/appmanager/AppManagerScreen.kt | 233 +++++++------ .../ui/appmanager/AppManagerViewModel.kt | 51 ++- .../AppManagerRepositoryImplementation.kt | 127 ++++--- .../com/d4rk/cleaner/ui/main/MainActivity.kt | 55 +-- .../com/d4rk/cleaner/ui/main/MainScreen.kt | 319 +++++++++--------- .../com/d4rk/cleaner/ui/main/MainViewModel.kt | 16 +- .../ui/main/repository/MainRepository.kt | 44 ++- .../MainRepositoryImplementation.kt | 42 ++- .../interfaces/OnShowAdCompleteListener.kt | 5 + .../ic_launcher.xml | 0 .../ic_shortcut_settings.xml | 0 16 files changed, 720 insertions(+), 545 deletions(-) create mode 100644 app/src/main/kotlin/com/d4rk/cleaner/data/core/ads/AdsCoreManager.kt create mode 100644 app/src/main/kotlin/com/d4rk/cleaner/data/core/datastore/DataStoreCoreManager.kt create mode 100644 app/src/main/kotlin/com/d4rk/cleaner/utils/interfaces/OnShowAdCompleteListener.kt rename app/src/main/res/{mipmap-anydpi => mipmap-anydpi-v26}/ic_launcher.xml (100%) rename app/src/main/res/{mipmap-anydpi => mipmap-anydpi-v26}/ic_shortcut_settings.xml (100%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f08038b..8ecb78d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -12,7 +12,7 @@ android { namespace = "com.d4rk.cleaner" defaultConfig { applicationId = "com.d4rk.cleaner" - minSdk = 26 + minSdk = 23 targetSdk = 34 versionCode = 118 versionName = "2.0.0" diff --git a/app/src/main/kotlin/com/d4rk/cleaner/data/core/AppCoreManager.kt b/app/src/main/kotlin/com/d4rk/cleaner/data/core/AppCoreManager.kt index f363865..7ed4735 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/data/core/AppCoreManager.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/data/core/AppCoreManager.kt @@ -4,73 +4,79 @@ package com.d4rk.cleaner.data.core import android.app.Activity import android.app.Application -import android.content.Context import android.os.Bundle import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.OnLifecycleEvent import androidx.lifecycle.ProcessLifecycleOwner import androidx.multidex.MultiDexApplication -import com.d4rk.cleaner.constants.ads.AdsConstants -import com.d4rk.cleaner.constants.ui.bottombar.BottomBarRoutes -import com.d4rk.cleaner.data.datastore.DataStore -import com.d4rk.cleaner.notifications.managers.AppUsageNotificationsManager -import com.google.android.gms.ads.AdError -import com.google.android.gms.ads.AdRequest -import com.google.android.gms.ads.FullScreenContentCallback -import com.google.android.gms.ads.LoadAdError -import com.google.android.gms.ads.MobileAds -import com.google.android.gms.ads.appopen.AppOpenAd +import com.d4rk.cleaner.data.core.ads.AdsCoreManager +import com.d4rk.cleaner.data.core.datastore.DataStoreCoreManager import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import java.util.Date -@Suppress("SameParameterValue") class AppCoreManager : MultiDexApplication(), Application.ActivityLifecycleCallbacks, LifecycleObserver { - private lateinit var appOpenAdManager: AppOpenAdManager - private var currentActivity: Activity? = null - private lateinit var dataStore: DataStore + + private val dataStoreCoreManager: DataStoreCoreManager = + DataStoreCoreManager(this) + private val adsCoreManager: AdsCoreManager = + AdsCoreManager(this) + + private enum class AppInitializationStage { + DATA_STORE, + ADS + } + + private var currentStage = AppInitializationStage.DATA_STORE private var isAppLoaded = false + override fun onCreate() { super.onCreate() registerActivityLifecycleCallbacks(this) - MobileAds.initialize(this) ProcessLifecycleOwner.get().lifecycle.addObserver(this) - dataStore = DataStore.getInstance(this@AppCoreManager) - appOpenAdManager = AppOpenAdManager() - val notificationsManager = AppUsageNotificationsManager(this) - notificationsManager.scheduleAppUsageCheck() CoroutineScope(Dispatchers.Main).launch { - val startupPage: String = - dataStore.getStartupPage().firstOrNull() ?: BottomBarRoutes.HOME - val showLabels: Boolean = dataStore.getShowBottomBarLabels().firstOrNull() ?: true - - markAppAsLoaded() + if (dataStoreCoreManager.initializeDataStore()) { + proceedToNextStage() + } } } - private fun markAppAsLoaded() { - isAppLoaded = true + private fun proceedToNextStage() { + when (currentStage) { + AppInitializationStage.DATA_STORE -> { + currentStage = AppInitializationStage.ADS + adsCoreManager.setDataStore(dataStoreCoreManager.dataStore) + adsCoreManager.initializeAds() + markAppAsLoaded() + } + + else -> { + // All stages completed + } + } } fun isAppLoaded(): Boolean { return isAppLoaded } + private fun markAppAsLoaded() { + isAppLoaded = true + } + @OnLifecycleEvent(Lifecycle.Event.ON_START) fun onMoveToForeground() { - currentActivity?.let { appOpenAdManager.showAdIfAvailable(it) } + currentActivity?.let { adsCoreManager.showAdIfAvailable(it) } } + private var currentActivity: Activity? = null + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {} override fun onActivityStarted(activity: Activity) { - if (!appOpenAdManager.isShowingAd) { + if (!adsCoreManager.isShowingAd) { currentActivity = activity } } @@ -80,91 +86,4 @@ class AppCoreManager : MultiDexApplication(), Application.ActivityLifecycleCallb override fun onActivityStopped(activity: Activity) {} override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {} override fun onActivityDestroyed(activity: Activity) {} - - interface OnShowAdCompleteListener { - @Suppress("EmptyMethod") - fun onShowAdComplete() - } - - private inner class AppOpenAdManager { - private var appOpenAd: AppOpenAd? = null - private var isLoadingAd = false - var isShowingAd = false - private var loadTime: Long = 0 - - fun loadAd(context: Context) { - if (isLoadingAd || isAdAvailable()) { - return - } - isLoadingAd = true - val request: AdRequest = AdRequest.Builder().build() - AppOpenAd.load(context, - AdsConstants.APP_OPEN_UNIT_ID, - request, - AppOpenAd.APP_OPEN_AD_ORIENTATION_PORTRAIT, - object : AppOpenAd.AppOpenAdLoadCallback() { - override fun onAdLoaded(ad: AppOpenAd) { - appOpenAd = ad - isLoadingAd = false - loadTime = Date().time - } - - override fun onAdFailedToLoad(loadAdError: LoadAdError) { - isLoadingAd = false - } - }) - } - - private fun wasLoadTimeLessThanNHoursAgo(numHours: Long): Boolean { - val dateDifference: Long = Date().time - loadTime - val numMilliSecondsPerHour: Long = 3600000 - return dateDifference < numMilliSecondsPerHour * numHours - } - - @Suppress("BooleanMethodIsAlwaysInverted") - private fun isAdAvailable(): Boolean { - return appOpenAd != null && wasLoadTimeLessThanNHoursAgo(4) - } - - fun showAdIfAvailable(activity: Activity) { - showAdIfAvailable(activity, object : OnShowAdCompleteListener { - override fun onShowAdComplete() { - } - }) - } - - fun showAdIfAvailable( - activity: Activity, onShowAdCompleteListener: OnShowAdCompleteListener - ) { - val isAdsChecked: Boolean = runBlocking { dataStore.ads.first() } - if (isShowingAd || !isAdsChecked) { - return - } - if (!isAdAvailable()) { - onShowAdCompleteListener.onShowAdComplete() - loadAd(activity) - return - } - appOpenAd!!.fullScreenContentCallback = object : FullScreenContentCallback() { - override fun onAdDismissedFullScreenContent() { - appOpenAd = null - isShowingAd = false - onShowAdCompleteListener.onShowAdComplete() - loadAd(activity) - } - - override fun onAdFailedToShowFullScreenContent(adError: AdError) { - appOpenAd = null - isShowingAd = false - onShowAdCompleteListener.onShowAdComplete() - loadAd(activity) - } - - override fun onAdShowedFullScreenContent() { - } - } - isShowingAd = true - appOpenAd!!.show(activity) - } - } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/d4rk/cleaner/data/core/ads/AdsCoreManager.kt b/app/src/main/kotlin/com/d4rk/cleaner/data/core/ads/AdsCoreManager.kt new file mode 100644 index 0000000..c3cbed9 --- /dev/null +++ b/app/src/main/kotlin/com/d4rk/cleaner/data/core/ads/AdsCoreManager.kt @@ -0,0 +1,121 @@ +package com.d4rk.cleaner.data.core.ads + +import android.app.Activity +import android.content.Context +import com.d4rk.cleaner.constants.ads.AdsConstants +import com.d4rk.cleaner.data.datastore.DataStore +import com.d4rk.cleaner.utils.interfaces.OnShowAdCompleteListener +import com.google.android.gms.ads.AdError +import com.google.android.gms.ads.AdRequest +import com.google.android.gms.ads.FullScreenContentCallback +import com.google.android.gms.ads.LoadAdError +import com.google.android.gms.ads.MobileAds +import com.google.android.gms.ads.appopen.AppOpenAd +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking +import java.util.Date + +open class AdsCoreManager(protected val context: Context) { + + private lateinit var dataStore: DataStore + private var appOpenAdManager: AppOpenAdManager? = null + val isShowingAd: Boolean + get() = appOpenAdManager?.isShowingAd == true + + fun initializeAds() { + MobileAds.initialize(context) + appOpenAdManager = AppOpenAdManager() + } + + fun showAdIfAvailable(activity: Activity) { + appOpenAdManager?.showAdIfAvailable(activity) + } + + fun setDataStore(dataStore: DataStore) { + this.dataStore = dataStore + } + + private inner class AppOpenAdManager { + private var appOpenAd: AppOpenAd? = null + private var isLoadingAd = false + var isShowingAd = false + private var loadTime: Long = 0 + + fun loadAd(context: Context) { + if (isLoadingAd || isAdAvailable()) { + return + } + isLoadingAd = true + val request: AdRequest = AdRequest.Builder().build() + @Suppress("DEPRECATION") + AppOpenAd.load(context, + AdsConstants.APP_OPEN_UNIT_ID, + request, + AppOpenAd.APP_OPEN_AD_ORIENTATION_PORTRAIT, + object : AppOpenAd.AppOpenAdLoadCallback() { + override fun onAdLoaded(ad: AppOpenAd) { + appOpenAd = ad + isLoadingAd = false + loadTime = Date().time + } + + override fun onAdFailedToLoad(loadAdError: LoadAdError) { + isLoadingAd = false + } + }) + } + + private fun wasLoadTimeLessThanNHoursAgo(): Boolean { + val dateDifference: Long = Date().time - loadTime + val numMilliSecondsPerHour: Long = 3600000 + return dateDifference < numMilliSecondsPerHour * 4 + } + + private fun isAdAvailable(): Boolean { + return appOpenAd != null && wasLoadTimeLessThanNHoursAgo() + } + + fun showAdIfAvailable(activity: Activity) { + showAdIfAvailable(activity, object : OnShowAdCompleteListener { + override fun onShowAdComplete() { + } + }) + } + + fun showAdIfAvailable( + activity: Activity, + onShowAdCompleteListener: OnShowAdCompleteListener + ) { + val isAdsChecked: Boolean = + runBlocking { dataStore.ads.first() } + if (isShowingAd || !isAdsChecked) { + return + } + if (!isAdAvailable()) { + onShowAdCompleteListener.onShowAdComplete() + loadAd(activity) + return + } + appOpenAd?.fullScreenContentCallback = object : FullScreenContentCallback() { + override fun onAdDismissedFullScreenContent() { + appOpenAd = null + isShowingAd = false + onShowAdCompleteListener.onShowAdComplete() + loadAd(activity) + } + + override fun onAdFailedToShowFullScreenContent(adError: AdError) { + appOpenAd = null + isShowingAd = false + onShowAdCompleteListener.onShowAdComplete() + loadAd(activity) + } + + override fun onAdShowedFullScreenContent() { + } + } + isShowingAd = true + appOpenAd?.show(activity) + } + } +} \ No newline at end of file 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 new file mode 100644 index 0000000..2252612 --- /dev/null +++ b/app/src/main/kotlin/com/d4rk/cleaner/data/core/datastore/DataStoreCoreManager.kt @@ -0,0 +1,28 @@ +package com.d4rk.cleaner.data.core.datastore + +import android.content.Context +import com.d4rk.cleaner.constants.ui.bottombar.BottomBarRoutes +import com.d4rk.cleaner.data.datastore.DataStore +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.firstOrNull + +open class DataStoreCoreManager(protected val context: Context) { + + private var isDataStoreLoaded = false + lateinit var dataStore: DataStore + + suspend fun initializeDataStore(): Boolean = coroutineScope { + dataStore = DataStore.getInstance(context.applicationContext) + + listOf( + async { dataStore.getStartupPage().firstOrNull() ?: BottomBarRoutes.HOME }, + async { dataStore.getShowBottomBarLabels().firstOrNull() ?: true }, + async { dataStore.getLanguage().firstOrNull() ?: "en" }, + ).awaitAll() + + isDataStoreLoaded = true + isDataStoreLoaded + } +} \ No newline at end of file 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 72cfb3e..d87caf5 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 @@ -29,7 +29,8 @@ class DataStore(context: Context) { } // Last used app notifications - private val lastUsedKey = longPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_LAST_USED) + private val lastUsedKey = + longPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_LAST_USED) val lastUsed: Flow = dataStore.data.map { preferences -> preferences[lastUsedKey] ?: 0 } @@ -41,7 +42,8 @@ class DataStore(context: Context) { } // Startup - private val startupKey = booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_STARTUP) + private val startupKey = + booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_STARTUP) val startup: Flow = dataStore.data.map { preferences -> preferences[startupKey] ?: true } @@ -54,7 +56,8 @@ class DataStore(context: Context) { // Display val themeModeState = mutableStateOf(value = "follow_system") - private val themeModeKey = stringPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_THEME_MODE) + private val themeModeKey = + stringPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_THEME_MODE) val themeMode: Flow = dataStore.data.map { preferences -> preferences[themeModeKey] ?: "follow_system" } @@ -65,7 +68,8 @@ class DataStore(context: Context) { } } - private val amoledModeKey = booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_AMOLED_MODE) + private val amoledModeKey = + booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_AMOLED_MODE) val amoledMode: Flow = dataStore.data.map { preferences -> preferences[amoledModeKey] ?: false } @@ -76,7 +80,8 @@ class DataStore(context: Context) { } } - private val dynamicColorsKey = booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_DYNAMIC_COLORS) + private val dynamicColorsKey = + booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_DYNAMIC_COLORS) val dynamicColors: Flow = dataStore.data.map { preferences -> preferences[dynamicColorsKey] ?: true } @@ -87,7 +92,8 @@ class DataStore(context: Context) { } } - private val bouncyButtonsKey = booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_BOUNCY_BUTTONS) + private val bouncyButtonsKey = + booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_BOUNCY_BUTTONS) val bouncyButtons: Flow = dataStore.data.map { preferences -> preferences[bouncyButtonsKey] ?: true } @@ -100,23 +106,27 @@ class DataStore(context: Context) { fun getStartupPage(): Flow { return dataStore.data.map { preferences -> - preferences[stringPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_STARTUP_PAGE)] ?: BottomBarRoutes.HOME + preferences[stringPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_STARTUP_PAGE)] + ?: BottomBarRoutes.HOME } } suspend fun saveStartupPage(startupPage: String) { dataStore.edit { preferences -> - preferences[stringPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_STARTUP_PAGE)] = startupPage + preferences[stringPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_STARTUP_PAGE)] = + startupPage } } fun getShowBottomBarLabels(): Flow { return dataStore.data.map { preferences -> - preferences[booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_SHOW_BOTTOM_BAR_LABELS)] ?: true + preferences[booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_SHOW_BOTTOM_BAR_LABELS)] + ?: true } } - private val languageKey = stringPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_LANGUAGE) + private val languageKey = + stringPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_LANGUAGE) fun getLanguage(): Flow = dataStore.data.map { preferences -> preferences[languageKey] ?: "en" @@ -129,7 +139,8 @@ class DataStore(context: Context) { } // Cleaning - private val genericFilterKey = booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_GENERIC_FILTER) + private val genericFilterKey = + booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_GENERIC_FILTER) val genericFilter: Flow = dataStore.data.map { preferences -> preferences[genericFilterKey] ?: false } @@ -140,7 +151,8 @@ class DataStore(context: Context) { } } - private val deleteEmptyFoldersKey = booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_DELETE_EMPTY_FOLDERS) + private val deleteEmptyFoldersKey = + booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_DELETE_EMPTY_FOLDERS) val deleteEmptyFolders: Flow = dataStore.data.map { preferences -> preferences[deleteEmptyFoldersKey] ?: true } @@ -151,7 +163,8 @@ class DataStore(context: Context) { } } - private val deleteArchivesKey = booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_DELETE_ARCHIVES) + private val deleteArchivesKey = + booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_DELETE_ARCHIVES) val deleteArchives: Flow = dataStore.data.map { preferences -> preferences[deleteArchivesKey] ?: false } @@ -162,7 +175,8 @@ class DataStore(context: Context) { } } - private val deleteInvalidMediaKey = booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_DELETE_INVALID_MEDIA) + private val deleteInvalidMediaKey = + booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_DELETE_INVALID_MEDIA) val deleteInvalidMedia: Flow = dataStore.data.map { preferences -> preferences[deleteInvalidMediaKey] ?: false } @@ -173,7 +187,8 @@ class DataStore(context: Context) { } } - private val deleteCorpseFilesKey = booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_DELETE_CORPSE_FILES) + private val deleteCorpseFilesKey = + booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_DELETE_CORPSE_FILES) val deleteCorpseFiles: Flow = dataStore.data.map { preferences -> preferences[deleteCorpseFilesKey] ?: false } @@ -184,7 +199,8 @@ class DataStore(context: Context) { } } - private val deleteApkFilesKey = booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_DELETE_APK_FILES) + private val deleteApkFilesKey = + booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_DELETE_APK_FILES) val deleteApkFiles: Flow = dataStore.data.map { preferences -> preferences[deleteApkFilesKey] ?: true } @@ -195,7 +211,8 @@ class DataStore(context: Context) { } } - private val deleteAudioFilesKey = booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_DELETE_AUDIO_FILES) + private val deleteAudioFilesKey = + booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_DELETE_AUDIO_FILES) val deleteAudioFiles: Flow = dataStore.data.map { preferences -> preferences[deleteAudioFilesKey] ?: true } @@ -206,7 +223,8 @@ class DataStore(context: Context) { } } - private val deleteVideoFilesKey = booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_DELETE_VIDEO_FILES) + private val deleteVideoFilesKey = + booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_DELETE_VIDEO_FILES) val deleteVideoFiles: Flow = dataStore.data.map { preferences -> preferences[deleteVideoFilesKey] ?: true } @@ -217,7 +235,8 @@ class DataStore(context: Context) { } } - private val deleteImageFilesKey = booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_DELETE_IMAGE_FILES) + private val deleteImageFilesKey = + booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_DELETE_IMAGE_FILES) val deleteImageFiles: Flow = dataStore.data.map { preferences -> preferences[deleteImageFilesKey] ?: true } @@ -228,7 +247,8 @@ class DataStore(context: Context) { } } - private val clipboardCleanKey = booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_CLIPBOARD_CLEAN) + private val clipboardCleanKey = + booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_CLIPBOARD_CLEAN) val clipboardClean: Flow = dataStore.data.map { preferences -> preferences[clipboardCleanKey] ?: false } @@ -240,7 +260,8 @@ class DataStore(context: Context) { } // Usage and Diagnostics - private val usageAndDiagnosticsKey = booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_USAGE_AND_DIAGNOSTICS) + private val usageAndDiagnosticsKey = + booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_USAGE_AND_DIAGNOSTICS) val usageAndDiagnostics: Flow = dataStore.data.map { preferences -> preferences[usageAndDiagnosticsKey] ?: true } diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/appmanager/AppManagerScreen.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/appmanager/AppManagerScreen.kt index e9aaefd..ed1f49c 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/appmanager/AppManagerScreen.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/appmanager/AppManagerScreen.kt @@ -6,7 +6,6 @@ import android.content.pm.ApplicationInfo import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.graphics.drawable.Drawable -import android.util.Log import android.view.SoundEffectConstants import android.view.View import androidx.compose.animation.core.Transition @@ -79,71 +78,70 @@ import java.io.File */ @Composable fun AppManagerScreen() { - val viewModel : AppManagerViewModel = viewModel() - val context : Context = LocalContext.current - val view : View = LocalView.current - val tabs : List = listOf( - stringResource(id = R.string.installed_apps) , - stringResource(id = R.string.system_apps) , - stringResource(id = R.string.app_install_files) , + val viewModel: AppManagerViewModel = viewModel() + val context: Context = LocalContext.current + val view: View = LocalView.current + val tabs: List = listOf( + stringResource(id = R.string.installed_apps), + stringResource(id = R.string.system_apps), + stringResource(id = R.string.app_install_files), ) - val pagerState : PagerState = rememberPagerState(pageCount = { tabs.size }) - val coroutineScope : CoroutineScope = rememberCoroutineScope() - val isLoading : Boolean by viewModel.isLoading.collectAsState() - val transition : Transition = - updateTransition(targetState = ! isLoading , label = "LoadingTransition") - val contentAlpha : Float by transition.animateFloat(label = "Content Alpha") { + val pagerState: PagerState = rememberPagerState(pageCount = { tabs.size }) + val coroutineScope: CoroutineScope = rememberCoroutineScope() + val isLoading: Boolean by viewModel.isLoading.collectAsState() + val transition: Transition = + updateTransition(targetState = !isLoading, label = "LoadingTransition") + val contentAlpha: Float by transition.animateFloat(label = "Content Alpha") { if (it) 1f else 0f } - val uiState : UiAppManagerModel by viewModel.uiState.collectAsState() - val uiErrorModel : UiErrorModel by viewModel.uiErrorModel.collectAsState() + val uiState: UiAppManagerModel by viewModel.uiState.collectAsState() + val uiErrorModel: UiErrorModel by viewModel.uiErrorModel.collectAsState() LaunchedEffect(context) { - if (! PermissionsUtils.hasUsageAccessPermissions(context)) { + if (!PermissionsUtils.hasUsageAccessPermissions(context)) { PermissionsUtils.requestUsageAccess(context as Activity) } } if (uiErrorModel.showErrorDialog) { - ErrorAlertDialog(errorMessage = uiErrorModel.errorMessage , - onDismiss = { viewModel.dismissErrorDialog() }) + ErrorAlertDialog(errorMessage = uiErrorModel.errorMessage, + onDismiss = { viewModel.dismissErrorDialog() }) } if (isLoading) { Box( - modifier = Modifier.fillMaxSize() , contentAlignment = Alignment.Center + modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { CircularProgressIndicator() } - } - else { + } else { Column( - modifier = Modifier.alpha(contentAlpha) , + modifier = Modifier.alpha(contentAlpha), ) { TabRow( - selectedTabIndex = pagerState.currentPage , + selectedTabIndex = pagerState.currentPage, indicator = { tabPositions -> TabRowDefaults.PrimaryIndicator( - modifier = Modifier.tabIndicatorOffset(tabPositions[pagerState.currentPage]) , + modifier = Modifier.tabIndicatorOffset(tabPositions[pagerState.currentPage]), shape = RoundedCornerShape( - topStart = 3.dp , - topEnd = 3.dp , - bottomEnd = 0.dp , - bottomStart = 0.dp , - ) , + topStart = 3.dp, + topEnd = 3.dp, + bottomEnd = 0.dp, + bottomStart = 0.dp, + ), ) - } , + }, ) { - tabs.forEachIndexed { index , title -> - Tab(modifier = Modifier.bounceClick() , text = { + tabs.forEachIndexed { index, title -> + Tab(modifier = Modifier.bounceClick(), text = { Text( - text = title , - maxLines = 1 , - overflow = TextOverflow.Ellipsis , + text = title, + maxLines = 1, + overflow = TextOverflow.Ellipsis, color = MaterialTheme.colorScheme.onSurface ) - } , selected = pagerState.currentPage == index , onClick = { + }, selected = pagerState.currentPage == index, onClick = { view.playSoundEffect(SoundEffectConstants.CLICK) coroutineScope.launch { pagerState.animateScrollToPage(index) @@ -153,20 +151,20 @@ fun AppManagerScreen() { } HorizontalPager( - modifier = Modifier.hapticPagerSwipe(pagerState) , - state = pagerState , + modifier = Modifier.hapticPagerSwipe(pagerState), + state = pagerState, ) { page -> when (page) { - 0 -> AppsComposable(apps = uiState.installedApps.filter { app : ApplicationInfo -> + 0 -> AppsComposable(apps = uiState.installedApps.filter { app: ApplicationInfo -> app.flags and ApplicationInfo.FLAG_SYSTEM == 0 - } , isLoading , viewModel = viewModel) + }, isLoading, viewModel = viewModel) - 1 -> AppsComposable(apps = uiState.installedApps.filter { app : ApplicationInfo -> + 1 -> AppsComposable(apps = uiState.installedApps.filter { app: ApplicationInfo -> app.flags and ApplicationInfo.FLAG_SYSTEM != 0 - } , isLoading , viewModel = viewModel) + }, isLoading, viewModel = viewModel) 2 -> ApksComposable( - apkFiles = uiState.apkFiles , isLoading , viewModel = viewModel + apkFiles = uiState.apkFiles, isLoading, viewModel = viewModel ) } } @@ -181,22 +179,20 @@ fun AppManagerScreen() { */ @Composable fun AppsComposable( - apps : List , isLoading : Boolean , viewModel : AppManagerViewModel + apps: List, isLoading: Boolean, viewModel: AppManagerViewModel ) { if (isLoading) { - Box(modifier = Modifier.fillMaxSize() , contentAlignment = Alignment.Center) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { CircularProgressIndicator() } - } - else if (apps.isEmpty()) { - Box(modifier = Modifier.fillMaxSize() , contentAlignment = Alignment.Center) { + } else if (apps.isEmpty()) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text(text = stringResource(id = R.string.no_app_installed)) } - } - else { + } else { LazyColumn(modifier = Modifier.fillMaxSize()) { - items(items = apps , key = { app -> app.packageName }) { app -> - AppItemComposable(app , viewModel = viewModel) + items(items = apps, key = { app -> app.packageName }) { app -> + AppItemComposable(app, viewModel = viewModel) } } } @@ -209,26 +205,26 @@ fun AppsComposable( */ @Composable fun AppItemComposable( - app : ApplicationInfo , viewModel : AppManagerViewModel + app: ApplicationInfo, viewModel: AppManagerViewModel ) { - val context : Context = LocalContext.current - val view : View = LocalView.current - val packageManager : PackageManager = context.packageManager - val appName : String = app.loadLabel(packageManager).toString() - val apkPath : String = app.publicSourceDir + val context: Context = LocalContext.current + val view: View = LocalView.current + val packageManager: PackageManager = context.packageManager + val appName: String = app.loadLabel(packageManager).toString() + val apkPath: String = app.publicSourceDir val apkFile = File(apkPath) - val sizeInBytes : Long = apkFile.length() - val sizeInKB : Long = sizeInBytes / 1024 - val sizeInMB : Long = sizeInKB / 1024 - val appSize : String = "%.2f MB".format(sizeInMB.toFloat()) - var showMenu : Boolean by remember { mutableStateOf(value = false) } - val model : Drawable = app.loadIcon(packageManager) - OutlinedCard(modifier = Modifier.padding(start = 8.dp , end = 8.dp , top = 8.dp)) { + val sizeInBytes: Long = apkFile.length() + val sizeInKB: Long = sizeInBytes / 1024 + val sizeInMB: Long = sizeInKB / 1024 + val appSize: String = "%.2f MB".format(sizeInMB.toFloat()) + var showMenu: Boolean by remember { mutableStateOf(value = false) } + val model: Drawable = app.loadIcon(packageManager) + OutlinedCard(modifier = Modifier.padding(start = 8.dp, end = 8.dp, top = 8.dp)) { Row( modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - .clip(RoundedCornerShape(16.dp)) , + .fillMaxWidth() + .padding(16.dp) + .clip(RoundedCornerShape(16.dp)), verticalAlignment = Alignment.CenterVertically ) { AsyncImage( @@ -239,45 +235,45 @@ fun AppItemComposable( ) Column( modifier = Modifier - .padding(16.dp) - .weight(1f) + .padding(16.dp) + .weight(1f) ) { Text( - text = appName , - style = MaterialTheme.typography.titleMedium , + text = appName, + style = MaterialTheme.typography.titleMedium, ) Text( - text = appSize , style = MaterialTheme.typography.bodyMedium + text = appSize, style = MaterialTheme.typography.bodyMedium ) } Box { - IconButton(modifier = Modifier.bounceClick() , onClick = { + IconButton(modifier = Modifier.bounceClick(), onClick = { view.playSoundEffect(SoundEffectConstants.CLICK) showMenu = true }) { - Icon(Icons.Outlined.MoreVert , contentDescription = null) + Icon(Icons.Outlined.MoreVert, contentDescription = null) } - DropdownMenu(expanded = showMenu , onDismissRequest = { + DropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) { - DropdownMenuItem(modifier = Modifier.bounceClick() , text = { + DropdownMenuItem(modifier = Modifier.bounceClick(), text = { Text(stringResource(id = R.string.uninstall)) - } , onClick = { + }, onClick = { view.playSoundEffect(SoundEffectConstants.CLICK) viewModel.uninstallApp(app.packageName) }) DropdownMenuItem( - modifier = Modifier.bounceClick() , - text = { Text(stringResource(id = R.string.share)) } , + modifier = Modifier.bounceClick(), + text = { Text(stringResource(id = R.string.share)) }, onClick = { view.playSoundEffect(SoundEffectConstants.CLICK) viewModel.shareApp(app.packageName) }) DropdownMenuItem( - modifier = Modifier.bounceClick() , - text = { Text(stringResource(id = R.string.app_info)) } , + modifier = Modifier.bounceClick(), + text = { Text(stringResource(id = R.string.app_info)) }, onClick = { view.playSoundEffect(SoundEffectConstants.CLICK) viewModel.openAppInfo(app.packageName) @@ -293,22 +289,20 @@ fun AppItemComposable( */ @Composable fun ApksComposable( - apkFiles : List , isLoading : Boolean , viewModel : AppManagerViewModel + apkFiles: List, isLoading: Boolean, viewModel: AppManagerViewModel ) { if (isLoading) { - Box(modifier = Modifier.fillMaxSize() , contentAlignment = Alignment.Center) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { CircularProgressIndicator() } - } - else if (apkFiles.isEmpty()) { - Box(modifier = Modifier.fillMaxSize() , contentAlignment = Alignment.Center) { + } else if (apkFiles.isEmpty()) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text(text = stringResource(id = R.string.no_apk_found)) } - } - else { + } else { LazyColumn(modifier = Modifier.fillMaxSize()) { - items(items = apkFiles , key = { apkInfo -> apkInfo.id }) { apkInfo -> - ApkItemComposable(apkPath = apkInfo.path , viewModel = viewModel) + items(items = apkFiles, key = { apkInfo -> apkInfo.id }) { apkInfo -> + ApkItemComposable(apkPath = apkInfo.path, viewModel = viewModel) } } } @@ -320,34 +314,33 @@ fun ApksComposable( * @param apkPath Path to the APK file. */ @Composable -fun ApkItemComposable(apkPath : String , viewModel : AppManagerViewModel) { - val context : Context = LocalContext.current - val view : View = LocalView.current +fun ApkItemComposable(apkPath: String, viewModel: AppManagerViewModel) { + val context: Context = LocalContext.current + val view: View = LocalView.current val apkFile = File(apkPath) - val sizeInBytes : Long = apkFile.length() - val sizeInKB : Long = sizeInBytes / 1024 - val sizeInMB : Long = sizeInKB / 1024 - val apkSize : String = "%.2f MB".format(sizeInMB.toFloat()) - val apkName : String = apkFile.name + val sizeInBytes: Long = apkFile.length() + val sizeInKB: Long = sizeInBytes / 1024 + val sizeInMB: Long = sizeInKB / 1024 + val apkSize: String = "%.2f MB".format(sizeInMB.toFloat()) + val apkName: String = apkFile.name - val packageInfo : PackageInfo? = context.packageManager.getPackageArchiveInfo(apkPath , 0) + val packageInfo: PackageInfo? = context.packageManager.getPackageArchiveInfo(apkPath, 0) - var showMenu : Boolean by remember { mutableStateOf(value = false) } + var showMenu: Boolean by remember { mutableStateOf(value = false) } - val iconDrawable : Drawable? = remember(apkPath) { + val iconDrawable: Drawable? = remember(apkPath) { packageInfo?.applicationInfo?.loadIcon(context.packageManager) } val model = iconDrawable?.toBitmapDrawable(context.resources) ?: ImageBitmap.imageResource(id = R.mipmap.ic_launcher) - OutlinedCard(modifier = Modifier.padding(start = 8.dp , end = 8.dp , top = 8.dp)) { - + OutlinedCard(modifier = Modifier.padding(start = 8.dp, end = 8.dp, top = 8.dp)) { Row( modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - .clip(RoundedCornerShape(16.dp)) , + .fillMaxWidth() + .padding(16.dp) + .clip(RoundedCornerShape(16.dp)), verticalAlignment = Alignment.CenterVertically ) { AsyncImage( @@ -358,40 +351,40 @@ fun ApkItemComposable(apkPath : String , viewModel : AppManagerViewModel) { ) Column( modifier = Modifier - .padding(16.dp) - .weight(1f) + .padding(16.dp) + .weight(1f) ) { Text( - text = apkName , - style = MaterialTheme.typography.titleMedium , + text = apkName, + style = MaterialTheme.typography.titleMedium, ) Text( - text = apkSize , style = MaterialTheme.typography.bodyMedium + text = apkSize, style = MaterialTheme.typography.bodyMedium ) } Box { - IconButton(modifier = Modifier.bounceClick() , onClick = { + IconButton(modifier = Modifier.bounceClick(), onClick = { view.playSoundEffect(SoundEffectConstants.CLICK) showMenu = true }) { - Icon(Icons.Outlined.MoreVert , contentDescription = null) + Icon(Icons.Outlined.MoreVert, contentDescription = null) } - DropdownMenu(expanded = showMenu , onDismissRequest = { + DropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) { DropdownMenuItem( - modifier = Modifier.bounceClick() , - text = { Text(stringResource(id = R.string.share)) } , + modifier = Modifier.bounceClick(), + text = { Text(stringResource(id = R.string.share)) }, onClick = { view.playSoundEffect(SoundEffectConstants.CLICK) viewModel.shareApk(apkPath) }) DropdownMenuItem( - modifier = Modifier.bounceClick() , - text = { Text(stringResource(id = R.string.install)) } , + modifier = Modifier.bounceClick(), + text = { Text(stringResource(id = R.string.install)) }, onClick = { view.playSoundEffect(SoundEffectConstants.CLICK) viewModel.installApk(apkPath) diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/appmanager/AppManagerViewModel.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/appmanager/AppManagerViewModel.kt index b7da74f..bfe68d0 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/appmanager/AppManagerViewModel.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/appmanager/AppManagerViewModel.kt @@ -17,29 +17,28 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -class AppManagerViewModel(application : Application) : BaseViewModel(application) { +class AppManagerViewModel(application: Application) : BaseViewModel(application) { private val repository = AppManagerRepository(application) private val _uiState = MutableStateFlow(UiAppManagerModel()) - val uiState : StateFlow = _uiState.asStateFlow() + val uiState: StateFlow = _uiState.asStateFlow() private val packageRemovedReceiver = object : BroadcastReceiver() { - override fun onReceive(context : Context? , intent : Intent?) { + override fun onReceive(context: Context?, intent: Intent?) { if (intent?.action == Intent.ACTION_PACKAGE_REMOVED) { - viewModelScope.launch { - loadAppData() - } + loadAppData() } } } init { - viewModelScope.launch(coroutineExceptionHandler) { - loadAppData() - } + loadAppData() + registerPackageRemovedReceiver() + } + private fun registerPackageRemovedReceiver() { val filter = IntentFilter(Intent.ACTION_PACKAGE_REMOVED) filter.addDataScheme("package") - getApplication().registerReceiver(packageRemovedReceiver , filter) + getApplication().registerReceiver(packageRemovedReceiver, filter) } override fun onCleared() { @@ -47,9 +46,9 @@ class AppManagerViewModel(application : Application) : BaseViewModel(application super.onCleared() } - private suspend fun loadAppData() { - showLoading() + private fun loadAppData() { viewModelScope.launch(coroutineExceptionHandler) { + showLoading() loadInstalledAppsAndApks() }.invokeOnCompletion { hideLoading() @@ -59,46 +58,46 @@ class AppManagerViewModel(application : Application) : BaseViewModel(application private suspend fun loadInstalledAppsAndApks() { repository.getInstalledApps { installedApps -> viewModelScope.launch { - val apkFilesDeferred : Deferred> = async { - var apkFiles : List = emptyList() + val apkFilesDeferred: Deferred> = async { + var apkFiles: List = emptyList() repository.getApkFilesFromStorage { files -> apkFiles = files } apkFiles } - val apkFiles : List = apkFilesDeferred.await() - _uiState.value = UiAppManagerModel(installedApps , apkFiles) + val apkFiles: List = apkFilesDeferred.await() + _uiState.value = UiAppManagerModel(installedApps, apkFiles) } } } - fun installApk(apkPath : String) { + fun installApk(apkPath: String) { viewModelScope.launch(coroutineExceptionHandler) { - repository.installApk(apkPath , onSuccess = {}) + repository.installApk(apkPath, onSuccess = {}) } } - fun shareApk(apkPath : String) { + fun shareApk(apkPath: String) { viewModelScope.launch(coroutineExceptionHandler) { - repository.shareApk(apkPath , onSuccess = { }) + repository.shareApk(apkPath, onSuccess = { }) } } - fun shareApp(packageName : String) { + fun shareApp(packageName: String) { viewModelScope.launch(coroutineExceptionHandler) { - repository.shareApp(packageName , onSuccess = { }) + repository.shareApp(packageName, onSuccess = { }) } } - fun openAppInfo(packageName : String) { + fun openAppInfo(packageName: String) { viewModelScope.launch(coroutineExceptionHandler) { - repository.openAppInfo(packageName , onSuccess = {}) + repository.openAppInfo(packageName, onSuccess = {}) } } - fun uninstallApp(packageName : String) { + fun uninstallApp(packageName: String) { viewModelScope.launch(coroutineExceptionHandler) { - repository.uninstallApp(packageName , onSuccess = { }) + repository.uninstallApp(packageName, onSuccess = { }) } } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/appmanager/repository/AppManagerRepositoryImplementation.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/appmanager/repository/AppManagerRepositoryImplementation.kt index e9cb9ff..f0550ff 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/appmanager/repository/AppManagerRepositoryImplementation.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/appmanager/repository/AppManagerRepositoryImplementation.kt @@ -12,98 +12,149 @@ import androidx.core.content.FileProvider import com.d4rk.cleaner.data.model.ui.appmanager.ui.ApkInfo import java.io.File -abstract class AppManagerRepositoryImplementation(val application : Application) { +/** + * Abstract base class for repository implementations related to application management. + * + * This class provides common functionality for interacting with installed applications, APK files, + * and the Android system for app management tasks. + * + * @property application The application context. + */ +abstract class AppManagerRepositoryImplementation(val application: Application) { - fun getInstalledAppsFromPackageManager() : List { + /** + * Retrieves a list of all installed applications on the device. + * + * @return A list of [ApplicationInfo] objects representing the installed applications. + */ + fun getInstalledAppsFromPackageManager(): List { return application.packageManager.getInstalledApplications(PackageManager.GET_META_DATA) } - fun getApkFilesFromMediaStore() : List { - val apkFiles : MutableList = mutableListOf() - val uri : Uri = MediaStore.Files.getContentUri("external") - val projection : Array = arrayOf( - MediaStore.Files.FileColumns._ID , - MediaStore.Files.FileColumns.DATA , + /** + * Retrieves a list of APK files from the device's media store. + * + * @return A list of [ApkInfo] objects representing the found APK files. + */ + fun getApkFilesFromMediaStore(): List { + val apkFiles: MutableList = mutableListOf() + val uri: Uri = MediaStore.Files.getContentUri("external") + val projection: Array = arrayOf( + MediaStore.Files.FileColumns._ID, + MediaStore.Files.FileColumns.DATA, MediaStore.Files.FileColumns.SIZE ) val selection = "${MediaStore.Files.FileColumns.MIME_TYPE} = ?" - val selectionArgs : Array = arrayOf("application/vnd.android.package-archive") - val cursor : Cursor? = application.contentResolver.query( - uri , projection , selection , selectionArgs , null + val selectionArgs: Array = arrayOf("application/vnd.android.package-archive") + val cursor: Cursor? = application.contentResolver.query( + uri, projection, selection, selectionArgs, null ) cursor?.use { - val idColumn : Int = it.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID) - val dataColumn : Int = it.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATA) - val sizeColumn : Int = it.getColumnIndexOrThrow(MediaStore.Files.FileColumns.SIZE) + val idColumn: Int = it.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID) + val dataColumn: Int = it.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATA) + val sizeColumn: Int = it.getColumnIndexOrThrow(MediaStore.Files.FileColumns.SIZE) while (it.moveToNext()) { - val id : Long = it.getLong(idColumn) - val path : String = it.getString(dataColumn) - val size : Long = it.getLong(sizeColumn) - apkFiles.add(ApkInfo(id , path , size)) + val id: Long = it.getLong(idColumn) + val path: String = it.getString(dataColumn) + val size: Long = it.getLong(sizeColumn) + apkFiles.add(ApkInfo(id, path, size)) } } return apkFiles } - fun installApk(apkPath : String) { + /** + * Initiates the installation process for an APK file. + * + * @param apkPath The file path of the APK to be installed. + */ + fun installApk(apkPath: String) { val apkFile = File(apkPath) val installIntent = Intent(Intent.ACTION_VIEW) - val contentUri : Uri = FileProvider.getUriForFile( - application , "${application.packageName}.fileprovider" , apkFile + val contentUri: Uri = FileProvider.getUriForFile( + application, "${application.packageName}.fileprovider", apkFile ) installIntent.setDataAndType( - contentUri , "application/vnd.android.package-archive" + contentUri, "application/vnd.android.package-archive" ) installIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK installIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) application.startActivity(installIntent) } - fun prepareShareIntent(apkPath : String) : Intent { + /** + * Creates an intent for sharing an APK file. + * + * @param apkPath The file path of the APK to be shared. + * @return An [Intent] configured for sharing the APK. + */ + fun prepareShareIntent(apkPath: String): Intent { val apkFile = File(apkPath) val shareIntent = Intent(Intent.ACTION_SEND) shareIntent.type = "application/vnd.android.package-archive" - val contentUri : Uri = FileProvider.getUriForFile( - application , "${application.packageName}.fileprovider" , apkFile + val contentUri: Uri = FileProvider.getUriForFile( + application, "${application.packageName}.fileprovider", apkFile ) - shareIntent.putExtra(Intent.EXTRA_STREAM , contentUri) + shareIntent.putExtra(Intent.EXTRA_STREAM, contentUri) shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) return shareIntent } - fun shareApp(packageName : String) : Intent { + /** + * Creates an intent for sharing an application via its Play Store link. + * + * @param packageName The package name of the application to be shared. + * @return An [Intent] configured for sharing the app. + */ + fun shareApp(packageName: String): Intent { val shareIntent = Intent(Intent.ACTION_SEND) shareIntent.type = "text/plain" - shareIntent.putExtra(Intent.EXTRA_SUBJECT , "Check out this app") + shareIntent.putExtra(Intent.EXTRA_SUBJECT, "Check out this app") val playStoreLink = "https://play.google.com/store/apps/details?id=$packageName" val shareMessage = "Check out this app: ${getAppName(packageName)}\n$playStoreLink" - shareIntent.putExtra(Intent.EXTRA_TEXT , shareMessage) + shareIntent.putExtra(Intent.EXTRA_TEXT, shareMessage) return shareIntent } - private fun getAppName(packageName : String) : String { + /** + * Retrieves the user-friendly name of an application. + * + * @param packageName The package name of the application. + * @return The application name, or the package name if the name cannot be found. + */ + private fun getAppName(packageName: String): String { return try { - val appInfo : ApplicationInfo = - application.packageManager.getApplicationInfo(packageName , 0) + val appInfo: ApplicationInfo = + application.packageManager.getApplicationInfo(packageName, 0) appInfo.loadLabel(application.packageManager).toString() - } catch (e : PackageManager.NameNotFoundException) { + } catch (e: PackageManager.NameNotFoundException) { packageName } } - fun openAppInfo(packageName : String) { + /** + * Opens the application info screen in system settings for a given package. + * + * @param packageName The package name of the application. + */ + fun openAppInfo(packageName: String) { val appInfoIntent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) - val packageUri : Uri = Uri.fromParts("package" , packageName , null) + val packageUri: Uri = Uri.fromParts("package", packageName, null) appInfoIntent.data = packageUri appInfoIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) application.startActivity(appInfoIntent) } - fun uninstallApp(packageName : String) { - val uri : Uri = Uri.fromParts("package" , packageName , null) - val intent = Intent(Intent.ACTION_DELETE , uri) + /** + * Initiates the uninstallation process for an application. + * + * @param packageName The package name of the application to be uninstalled. + */ + fun uninstallApp(packageName: String) { + val uri: Uri = Uri.fromParts("package", packageName, null) + val intent = Intent(Intent.ACTION_DELETE, uri) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) application.startActivity(intent) } diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/main/MainActivity.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/main/MainActivity.kt index 8a9e856..8380dc8 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/main/MainActivity.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/main/MainActivity.kt @@ -20,10 +20,7 @@ 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.settings.display.theme.style.AppTheme -import com.d4rk.cleaner.ui.startup.StartupActivity -import com.d4rk.cleaner.utils.IntentUtils import com.google.android.gms.ads.MobileAds -import com.google.android.gms.oss.licenses.OssLicensesMenuActivity import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import com.google.android.play.core.appupdate.AppUpdateInfo @@ -32,9 +29,6 @@ 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 com.google.firebase.analytics.FirebaseAnalytics -import com.google.firebase.crashlytics.FirebaseCrashlytics -import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.tasks.await @@ -54,7 +48,6 @@ class MainActivity : AppCompatActivity() { enableEdgeToEdge() dataStore = DataStore.getInstance(this@MainActivity) MobileAds.initialize(this@MainActivity) - startupScreen() setupUpdateNotifications() setContent { AppTheme { @@ -65,7 +58,6 @@ class MainActivity : AppCompatActivity() { } } } - setupSettings() } override fun onResume() { @@ -116,7 +108,7 @@ class MainActivity : AppCompatActivity() { when (resultCode) { RESULT_OK -> { val snackbar: Snackbar = Snackbar.make( - findViewById(android.R.id.content) , R.string.snack_app_updated , + findViewById(android.R.id.content), R.string.snack_app_updated, Snackbar.LENGTH_LONG ).setAction(android.R.string.ok, null) snackbar.show() @@ -180,20 +172,20 @@ class MainActivity : AppCompatActivity() { } } } catch (e: Exception) { - if (! BuildConfig.DEBUG) { + if (!BuildConfig.DEBUG) { when (e) { is NoConnectionError, is TimeoutError -> { Snackbar.make( - findViewById(android.R.id.content) , - getString(R.string.snack_network_error) , + findViewById(android.R.id.content), + getString(R.string.snack_network_error), Snackbar.LENGTH_LONG ).show() } else -> { Snackbar.make( - findViewById(android.R.id.content) , - getString(R.string.snack_general_error) , + findViewById(android.R.id.content), + getString(R.string.snack_general_error), Snackbar.LENGTH_LONG ).show() } @@ -212,7 +204,7 @@ class MainActivity : AppCompatActivity() { */ private fun showUpdateFailedSnackbar() { val snackbar: Snackbar = Snackbar.make( - findViewById(android.R.id.content) , R.string.snack_update_failed , Snackbar.LENGTH_LONG + findViewById(android.R.id.content), R.string.snack_update_failed, Snackbar.LENGTH_LONG ).setAction(R.string.try_again) { checkForFlexibleUpdate() } @@ -223,37 +215,4 @@ class MainActivity : AppCompatActivity() { appUpdateManager = AppUpdateManagerFactory.create(this) appUpdateNotificationsManager = AppUpdateNotificationsManager(this) } - - /** - * Configures application settings based on data stored in a DataStore. - * - * This function uses a lifecycle coroutine scope to asynchronously retrieve the value of `usageAndDiagnostics` - * from the DataStore. It then adjusts the Firebase Analytics and Crashlytics collection settings based on the retrieved value. - * - * If `usageAndDiagnostics` is enabled, both Firebase Analytics and Crashlytics data collection will be enabled. If it's not, data collection will be disabled. - * - * @see androidx.lifecycle.lifecycleScope - * @see androidx.datastore.preferences.core.DataStore - * @see com.google.firebase.analytics.FirebaseAnalytics - * @see com.google.firebase.crashlytics.FirebaseCrashlytics - */ - private fun setupSettings() { - lifecycleScope.launch { - val isEnabled : Boolean = dataStore.usageAndDiagnostics.first() - FirebaseAnalytics.getInstance(this@MainActivity) - .setAnalyticsCollectionEnabled(isEnabled) - FirebaseCrashlytics.getInstance().isCrashlyticsCollectionEnabled = isEnabled - } - } - - private fun startupScreen() { - lifecycleScope.launch { - if (dataStore.startup.first()) { - dataStore.saveStartup(isFirstTime = false) - IntentUtils.openActivity( - this@MainActivity , StartupActivity::class.java - ) - } - } - } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/main/MainScreen.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/main/MainScreen.kt index bd83182..fe2707e 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/main/MainScreen.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/main/MainScreen.kt @@ -45,6 +45,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavBackStackEntry import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost @@ -73,193 +74,199 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable fun MainComposable() { - val bottomBarItems : List = listOf( - BottomNavigationScreen.Home , - BottomNavigationScreen.AppManager , + + val viewModel: MainViewModel = viewModel() + + val bottomBarItems: List = listOf( + BottomNavigationScreen.Home, + BottomNavigationScreen.AppManager, BottomNavigationScreen.MemoryManager ) - val drawerItems : List = listOf( + val drawerItems: List = listOf( NavigationDrawerItem( - title = R.string.image_optimizer , selectedIcon = Icons.Outlined.Image - ) , + title = R.string.image_optimizer, selectedIcon = Icons.Outlined.Image + ), NavigationDrawerItem( - title = R.string.settings , - selectedIcon = Icons.Outlined.Settings , - ) , + title = R.string.settings, + selectedIcon = Icons.Outlined.Settings, + ), NavigationDrawerItem( - title = R.string.help_and_feedback , - selectedIcon = Icons.AutoMirrored.Outlined.HelpOutline , - ) , + title = R.string.help_and_feedback, + selectedIcon = Icons.AutoMirrored.Outlined.HelpOutline, + ), NavigationDrawerItem( - title = R.string.updates , - selectedIcon = Icons.AutoMirrored.Outlined.EventNote , - ) , + title = R.string.updates, + selectedIcon = Icons.AutoMirrored.Outlined.EventNote, + ), NavigationDrawerItem( - title = R.string.share , selectedIcon = Icons.Outlined.Share - ) , + title = R.string.share, selectedIcon = Icons.Outlined.Share + ), ) - val drawerState : DrawerState = rememberDrawerState(initialValue = DrawerValue.Closed) - val scope : CoroutineScope = rememberCoroutineScope() - val navController : NavHostController = rememberNavController() - val context : Context = LocalContext.current - val view : View = LocalView.current - val dataStore : DataStore = DataStore.getInstance(context) - val startupPage : String = - dataStore.getStartupPage().collectAsState(initial = BottomBarRoutes.HOME).value - val showLabels : Boolean = - dataStore.getShowBottomBarLabels().collectAsState(initial = true).value - val selectedItemIndex : Int by rememberSaveable { mutableIntStateOf(value = - 1) } + val drawerState: DrawerState = rememberDrawerState(initialValue = DrawerValue.Closed) + val scope: CoroutineScope = rememberCoroutineScope() + val navController: NavHostController = rememberNavController() + val context: Context = LocalContext.current + val view: View = LocalView.current + val dataStore: DataStore = DataStore.getInstance(context) + val startupPage: String = + dataStore.getStartupPage().collectAsState(initial = BottomBarRoutes.HOME).value + val showLabels: Boolean = + dataStore.getShowBottomBarLabels().collectAsState(initial = true).value + val selectedItemIndex: Int by rememberSaveable { mutableIntStateOf(value = -1) } ModalNavigationDrawer( modifier = Modifier.hapticDrawerSwipe(drawerState), - drawerState = drawerState , drawerContent = { - ModalDrawerSheet { - Spacer(modifier = Modifier.height(16.dp)) - drawerItems.forEachIndexed { index , item -> - val title: String = stringResource(id = item.title) - NavigationDrawerItem(label = { Text(text = title) } , - selected = index == selectedItemIndex , - onClick = { - when (item.title) { + drawerState = drawerState, drawerContent = { + ModalDrawerSheet { + Spacer(modifier = Modifier.height(16.dp)) + drawerItems.forEachIndexed { index, item -> + val title: String = stringResource(id = item.title) + NavigationDrawerItem(label = { Text(text = title) }, + selected = index == selectedItemIndex, + onClick = { + when (item.title) { - R.string.image_optimizer -> { - view.playSoundEffect(SoundEffectConstants.CLICK) - IntentUtils.openActivity( - context , ImagePickerActivity::class.java - ) - } + R.string.image_optimizer -> { + view.playSoundEffect(SoundEffectConstants.CLICK) + IntentUtils.openActivity( + context, ImagePickerActivity::class.java + ) + } - R.string.settings -> { - view.playSoundEffect(SoundEffectConstants.CLICK) - IntentUtils.openActivity( - context , SettingsActivity::class.java - ) - } + R.string.settings -> { + view.playSoundEffect(SoundEffectConstants.CLICK) + IntentUtils.openActivity( + context, SettingsActivity::class.java + ) + } - R.string.help_and_feedback -> { - view.playSoundEffect(SoundEffectConstants.CLICK) - IntentUtils.openActivity( - context , HelpActivity::class.java - ) - } + R.string.help_and_feedback -> { + view.playSoundEffect(SoundEffectConstants.CLICK) + IntentUtils.openActivity( + context, HelpActivity::class.java + ) + } - R.string.updates -> { - view.playSoundEffect(SoundEffectConstants.CLICK) - IntentUtils.openUrl( - context , - url = "https://github.com/D4rK7355608/${context.packageName}/blob/master/CHANGELOG.md" - ) - } + R.string.updates -> { + view.playSoundEffect(SoundEffectConstants.CLICK) + IntentUtils.openUrl( + context, + url = "https://github.com/D4rK7355608/${context.packageName}/blob/master/CHANGELOG.md" + ) + } - R.string.share -> { - view.playSoundEffect(SoundEffectConstants.CLICK) - IntentUtils.shareApp(context) - } - } - scope.launch { - drawerState.close() - } - } , - icon = { - Icon( - item.selectedIcon , contentDescription = title - ) - } , - badge = { - item.badgeCount?.let { - Text(text = item.badgeCount.toString()) - } - } , - modifier = Modifier - .padding(NavigationDrawerItemDefaults.ItemPadding) - .bounceClick() - ) - if (item.title == R.string.image_optimizer) { - HorizontalDivider(modifier = Modifier.padding(8.dp)) + R.string.share -> { + view.playSoundEffect(SoundEffectConstants.CLICK) + IntentUtils.shareApp(context) + } + } + scope.launch { + drawerState.close() + } + }, + icon = { + Icon( + item.selectedIcon, contentDescription = title + ) + }, + badge = { + item.badgeCount?.let { + Text(text = item.badgeCount.toString()) + } + }, + modifier = Modifier + .padding(NavigationDrawerItemDefaults.ItemPadding) + .bounceClick() + ) + if (item.title == R.string.image_optimizer) { + HorizontalDivider(modifier = Modifier.padding(8.dp)) + } } } - } - } , content = { - Scaffold(topBar = { - TopAppBar(title = { - Text(text = stringResource(id = R.string.app_name)) - } , navigationIcon = { - IconButton(modifier = Modifier.bounceClick() , onClick = { - view.playSoundEffect(SoundEffectConstants.CLICK) - scope.launch { - drawerState.apply { - if (isClosed) open() else close() + }, content = { + Scaffold(topBar = { + TopAppBar(title = { + Text(text = stringResource(id = R.string.app_name)) + }, navigationIcon = { + IconButton(modifier = Modifier.bounceClick(), onClick = { + view.playSoundEffect(SoundEffectConstants.CLICK) + scope.launch { + drawerState.apply { + if (isClosed) open() else close() + } } + }) { + Icon( + imageVector = Icons.Default.Menu, + contentDescription = stringResource(id = R.string.navigation_drawer_open) + ) } - }) { - Icon( - imageVector = Icons.Default.Menu , - contentDescription = stringResource(id = R.string.navigation_drawer_open) - ) - } - } , actions = { - IconButton(modifier = Modifier.bounceClick() , onClick = { - view.playSoundEffect(SoundEffectConstants.CLICK) - IntentUtils.openActivity(context , SupportActivity::class.java) - }) { - Icon( - Icons.Outlined.VolunteerActivism , - contentDescription = stringResource(id = R.string.support_us) + }, actions = { + IconButton(modifier = Modifier.bounceClick(), onClick = { + view.playSoundEffect(SoundEffectConstants.CLICK) + IntentUtils.openActivity(context, SupportActivity::class.java) + }) { + Icon( + Icons.Outlined.VolunteerActivism, + contentDescription = stringResource(id = R.string.support_us) + ) + } + }) + }, bottomBar = { + Column { + FullBannerAdsComposable( + modifier = Modifier.fillMaxWidth(), + dataStore = dataStore ) - } - }) - } , bottomBar = { - Column { - FullBannerAdsComposable(modifier = Modifier.fillMaxWidth() , dataStore = dataStore) - NavigationBar { - val navBackStackEntry : NavBackStackEntry? by navController.currentBackStackEntryAsState() - val currentRoute : String? = navBackStackEntry?.destination?.route - bottomBarItems.forEach { screen -> - NavigationBarItem(modifier = Modifier.bounceClick(), icon = { - val iconResource : ImageVector = + NavigationBar { + val navBackStackEntry: NavBackStackEntry? by navController.currentBackStackEntryAsState() + val currentRoute: String? = navBackStackEntry?.destination?.route + bottomBarItems.forEach { screen -> + NavigationBarItem(modifier = Modifier.bounceClick(), icon = { + val iconResource: ImageVector = if (currentRoute == screen.route) screen.selectedIcon else screen.icon - Icon(iconResource , contentDescription = null) - } , + Icon(iconResource, contentDescription = null) + }, - label = { - if (showLabels) Text( - text = stringResource( - id = screen.title - ) - ) - } , - selected = currentRoute == screen.route , - onClick = { - view.playSoundEffect(SoundEffectConstants.CLICK) - navController.navigate(screen.route) { - popUpTo(navController.graph.startDestinationId) - launchSingleTop = true - } - }) + label = { + if (showLabels) Text( + text = stringResource( + id = screen.title + ) + ) + }, + selected = currentRoute == screen.route, + onClick = { + view.playSoundEffect(SoundEffectConstants.CLICK) + navController.navigate(screen.route) { + popUpTo(navController.graph.startDestinationId) + launchSingleTop = true + } + }) + } } } - } - }) { innerPadding -> - NavHost(navController , startDestination = startupPage) { - composable(BottomNavigationScreen.Home.route) { - Box(modifier = Modifier.padding(innerPadding)) { - HomeScreen() + }) { paddingValues -> + NavHost(navController, startDestination = startupPage) { + composable(BottomNavigationScreen.Home.route) { + Box(modifier = Modifier.padding(paddingValues)) { + HomeScreen() + } } - } - composable(BottomNavigationScreen.AppManager.route) { - Box(modifier = Modifier.padding(innerPadding)) { - AppManagerScreen() + composable(BottomNavigationScreen.AppManager.route) { + Box(modifier = Modifier.padding(paddingValues)) { + AppManagerScreen() + } } - } - composable(BottomNavigationScreen.MemoryManager.route) { - Box(modifier = Modifier.padding(innerPadding)) { - MemoryManagerComposable() + composable(BottomNavigationScreen.MemoryManager.route) { + Box(modifier = Modifier.padding(paddingValues)) { + MemoryManagerComposable() + } } } } - } - }) + }) } \ No newline at end of file diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/main/MainViewModel.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/main/MainViewModel.kt index 8bc5fae..692bc56 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/main/MainViewModel.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/main/MainViewModel.kt @@ -7,23 +7,29 @@ import com.d4rk.cleaner.ui.main.repository.MainRepository import com.d4rk.cleaner.ui.startup.StartupActivity import com.d4rk.cleaner.utils.IntentUtils import com.d4rk.cleaner.utils.viewmodel.BaseViewModel -import kotlinx.coroutines.Dispatchers 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) init { checkAndHandleStartup() + configureSettings() } private fun checkAndHandleStartup() { - viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch(coroutineExceptionHandler) { repository.checkAndHandleStartup { isFirstTime -> if (isFirstTime) { - IntentUtils.openActivity(getApplication() , StartupActivity::class.java) + IntentUtils.openActivity(getApplication(), StartupActivity::class.java) } } } } + + private fun configureSettings() { + viewModelScope.launch(coroutineExceptionHandler) { + repository.setupSettings() + } + } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/main/repository/MainRepository.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/main/repository/MainRepository.kt index 91312b8..c41ed53 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/main/repository/MainRepository.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/main/repository/MainRepository.kt @@ -6,18 +6,44 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.first import kotlinx.coroutines.withContext -class MainRepository(dataStore : DataStore , application : Application) : +/** + * Concrete implementation of the main repository for managing application settings and startup state. + * + * @property dataStore The data store used to persist settings and startup information. + * @property application The application context. + */ +class MainRepository(val dataStore: DataStore, application: Application) : MainRepositoryImplementation(application) { - suspend fun checkAndHandleStartup(onSuccess : (Boolean) -> Unit) { - - val dataStore : DataStore = DataStore.getInstance(application) - val isFirstTime : Boolean = dataStore.startup.first() - if (isFirstTime) { - dataStore.saveStartup(isFirstTime = false) + /** + * Checks the application startup state and performs actions based on whether it's the first launch. + * + * This function checks if the app is launched for the first time and invokes the `onSuccess` callback + * with the result on the main thread. + * + * @param onSuccess A callback function that receives a boolean indicating if it's the first launch. + */ + suspend fun checkAndHandleStartup(onSuccess: (Boolean) -> Unit) { + withContext(Dispatchers.IO) { + val isFirstTime: Boolean = checkStartup() + withContext(Dispatchers.Main) { + onSuccess(isFirstTime) + } } - withContext(Dispatchers.Main) { - onSuccess(isFirstTime) + } + + /** + * Sets up Firebase Analytics and Crashlytics based on stored settings. + * + * This function retrieves the "usageAndDiagnostics" setting from the data store and configures + * Firebase Analytics and Crashlytics accordingly. + */ + suspend fun setupSettings() { + withContext(Dispatchers.IO) { + val isEnabled: Boolean = dataStore.usageAndDiagnostics.first() + withContext(Dispatchers.Main) { + setupDiagnosticSettings(isEnabled) + } } } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/main/repository/MainRepositoryImplementation.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/main/repository/MainRepositoryImplementation.kt index 63f4057..37c3853 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/main/repository/MainRepositoryImplementation.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/main/repository/MainRepositoryImplementation.kt @@ -1,7 +1,47 @@ package com.d4rk.cleaner.ui.main.repository import android.app.Application +import com.d4rk.cleaner.data.datastore.DataStore +import com.google.firebase.analytics.FirebaseAnalytics +import com.google.firebase.crashlytics.FirebaseCrashlytics +import kotlinx.coroutines.flow.first +import com.d4rk.cleaner.data.datastore.DataStore.Companion as DataStore1 -abstract class MainRepositoryImplementation(val application : Application) { +/** + * Abstract base class for repository implementations related to main application functionality. + * + * This class provides common functionality for managing application startup state. + * + * @property application The application context. + */ +abstract class MainRepositoryImplementation(val application: Application) { + /** + * Checks if the application is being launched for the first time. + * + * This function retrieves the startup state from a data store and updates it if it's the first launch. + * + * @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() + if (isFirstTime) { + dataStore.saveStartup(isFirstTime = false) + } + return isFirstTime + } + + /** + * Configures Firebase Analytics and Crashlytics data collection. + * + * Enables or disables data collection for both Firebase Analytics and Crashlytics + * based on the provided flag. + * + * @param isEnabled `true` to enable data collection, `false` to disable. + */ + fun setupDiagnosticSettings(isEnabled: Boolean) { + FirebaseAnalytics.getInstance(application).setAnalyticsCollectionEnabled(isEnabled) + FirebaseCrashlytics.getInstance().isCrashlyticsCollectionEnabled = isEnabled + } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/d4rk/cleaner/utils/interfaces/OnShowAdCompleteListener.kt b/app/src/main/kotlin/com/d4rk/cleaner/utils/interfaces/OnShowAdCompleteListener.kt new file mode 100644 index 0000000..ddd6194 --- /dev/null +++ b/app/src/main/kotlin/com/d4rk/cleaner/utils/interfaces/OnShowAdCompleteListener.kt @@ -0,0 +1,5 @@ +package com.d4rk.cleaner.utils.interfaces + +interface OnShowAdCompleteListener { + fun onShowAdComplete() +} \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml similarity index 100% rename from app/src/main/res/mipmap-anydpi/ic_launcher.xml rename to app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml diff --git a/app/src/main/res/mipmap-anydpi/ic_shortcut_settings.xml b/app/src/main/res/mipmap-anydpi-v26/ic_shortcut_settings.xml similarity index 100% rename from app/src/main/res/mipmap-anydpi/ic_shortcut_settings.xml rename to app/src/main/res/mipmap-anydpi-v26/ic_shortcut_settings.xml