Skip to content

Commit

Permalink
Added support for more Android devices + better structure at the app …
Browse files Browse the repository at this point in the history
…startup thanks to app core manager
  • Loading branch information
D4rK7355608 committed Sep 26, 2024
1 parent 39d71a4 commit ecf725b
Show file tree
Hide file tree
Showing 16 changed files with 720 additions and 545 deletions.
2 changes: 1 addition & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
159 changes: 39 additions & 120 deletions app/src/main/kotlin/com/d4rk/cleaner/data/core/AppCoreManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand All @@ -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)
}
}
}
121 changes: 121 additions & 0 deletions app/src/main/kotlin/com/d4rk/cleaner/data/core/ads/AdsCoreManager.kt
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading

0 comments on commit ecf725b

Please sign in to comment.