Skip to content

Commit

Permalink
Merge pull request #349 from toquete/ads
Browse files Browse the repository at this point in the history
Adding ads banner
  • Loading branch information
toquete authored Dec 5, 2024
2 parents 662f5c1 + c58f4a0 commit b1e0700
Show file tree
Hide file tree
Showing 29 changed files with 392 additions and 62 deletions.
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ indent_style = space
trim_trailing_whitespace = true
max_line_length = 120
ij_visual_guides = 120
ij_continuation_indent_size = 8
ij_continuation_indent_size = 4

[{*.kt,*.kts}]
insert_final_newline = true
Expand Down
5 changes: 1 addition & 4 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,9 @@ dependencies {
implementation(libs.splashscreen)
implementation(libs.work.runtime)
implementation(libs.coil)
implementation(libs.firebase.appcheck)
implementation(libs.firebase.appcheck.ktx)
implementation(libs.firebase.appcheck.debug)
implementation(libs.hilt.work)
implementation(libs.firebase.crashlytics)
ksp(libs.hilt.work.compiler)
implementation(libs.play.services.ads)

testImplementation(project(":core:testing"))
testImplementation(libs.robolectric)
Expand Down
7 changes: 7 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@
android:authorities="${applicationId}.androidx-startup"
tools:node="remove">
</provider>
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="ca-app-pub-6303957371601323~7187753748" />
<property
android:name="android.adservices.AD_SERVICES_CONFIG"
android:resource="@xml/gma_ad_services_config"
tools:replace="android:resource" />
</application>

</manifest>
51 changes: 51 additions & 0 deletions app/src/main/java/com/toquete/boxbox/BoxBoxApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,33 @@ import coil.ImageLoaderFactory
import coil.disk.DiskCache
import coil.memory.MemoryCache
import coil.request.CachePolicy
import com.google.android.gms.ads.MobileAds
import com.google.firebase.Firebase
import com.google.firebase.FirebaseApp
import com.google.firebase.FirebaseOptions
import com.google.firebase.appcheck.AppCheckProviderFactory
import com.google.firebase.appcheck.appCheck
import com.google.firebase.initialize
import com.google.firebase.remoteconfig.ConfigUpdate
import com.google.firebase.remoteconfig.ConfigUpdateListener
import com.google.firebase.remoteconfig.FirebaseRemoteConfigException
import com.google.firebase.remoteconfig.ktx.remoteConfigSettings
import com.google.firebase.remoteconfig.remoteConfig
import com.toquete.boxbox.core.common.annotation.IoDispatcher
import com.toquete.boxbox.util.remoteconfig.remoteConfigDefaults
import com.toquete.boxbox.worker.SyncWorker
import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext

private const val APP_CHECK_DEBUG_STORE = "com.google.firebase.appcheck.debug.store.%s"
private const val APP_CHECK_DEBUG_TOKEN_KEY = "com.google.firebase.appcheck.debug.DEBUG_SECRET"
private const val MEMORY_CACHE_PERCENT = 0.1
private const val DISK_CACHE_PERCENT = 0.03
private const val MINIMUM_REMOTE_CONFIG_FETCH_INTERVAL = 0L
const val SYNC_WORK_NAME = "SYNC_WORK_NAME"

@HiltAndroidApp
Expand All @@ -40,6 +52,10 @@ class BoxBoxApplication : Application(), Configuration.Provider, ImageLoaderFact
@Inject
lateinit var appCheckProviderFactory: AppCheckProviderFactory

@Inject
@IoDispatcher
lateinit var ioDispatcher: CoroutineContext

override val workManagerConfiguration: Configuration
get() = Configuration.Builder()
.setWorkerFactory(workerFactory)
Expand All @@ -50,6 +66,8 @@ class BoxBoxApplication : Application(), Configuration.Provider, ImageLoaderFact
setupAppCheck()
setupSyncWork()
setupTimber()
setupMobileAds()
setupRemoteConfig()
}

override fun newImageLoader(): ImageLoader {
Expand Down Expand Up @@ -98,4 +116,37 @@ class BoxBoxApplication : Application(), Configuration.Provider, ImageLoaderFact
Firebase.initialize(this)
Firebase.appCheck.installAppCheckProviderFactory(appCheckProviderFactory)
}

private fun setupMobileAds() {
CoroutineScope(ioDispatcher).launch {
MobileAds.initialize(this@BoxBoxApplication)
}
}

private fun setupRemoteConfig() {
Firebase.remoteConfig.apply {
if (BuildConfig.DEBUG) {
val configSettings = remoteConfigSettings {
minimumFetchIntervalInSeconds = MINIMUM_REMOTE_CONFIG_FETCH_INTERVAL
}
setConfigSettingsAsync(configSettings)
}
setDefaultsAsync(remoteConfigDefaults)
addOnConfigUpdateListener(object : ConfigUpdateListener {
override fun onUpdate(configUpdate: ConfigUpdate) {
activate().addOnCompleteListener { task ->
if (task.isSuccessful) {
Timber.d("Remote config activated")
} else {
Timber.e(task.exception, "Failed to activate remote config")
}
}
}

override fun onError(error: FirebaseRemoteConfigException) {
Timber.e(error, "Failed to update remote config")
}
})
}
}
}
31 changes: 31 additions & 0 deletions app/src/main/java/com/toquete/boxbox/di/FirebaseModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.toquete.boxbox.di

import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.ktx.Firebase
import com.google.firebase.remoteconfig.FirebaseRemoteConfig
import com.google.firebase.remoteconfig.ktx.remoteConfig
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
internal object FirebaseModule {

@Provides
@Singleton
fun providesFirebaseAnalytics(): FirebaseAnalytics = Firebase.analytics

@Provides
@Singleton
fun providesFirebaseCrashlytics(): FirebaseCrashlytics = Firebase.crashlytics

@Provides
@Singleton
fun providesFirebaseRemoteConfig(): FirebaseRemoteConfig = Firebase.remoteConfig
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.toquete.boxbox.di

import com.toquete.boxbox.domain.repository.RemoteConfigRepository
import com.toquete.boxbox.util.remoteconfig.FirebaseRemoteConfigRepository
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent

@Module
@InstallIn(SingletonComponent::class)
fun interface FirebaseRemoteConfigRepositoryModule {

@Binds
fun bindRemoteConfigRepository(
firebaseRemoteConfigRepository: FirebaseRemoteConfigRepository
): RemoteConfigRepository
}
4 changes: 3 additions & 1 deletion app/src/main/java/com/toquete/boxbox/ui/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@ class MainActivity : ComponentActivity() {
darkTheme = isDarkTheme,
dynamicColors = isDynamicColors
) {
MainScreen(navController = navController)
if (!uiState.isLoading) {
MainScreen(navController = navController)
}
}
}
}
Expand Down
33 changes: 20 additions & 13 deletions app/src/main/java/com/toquete/boxbox/ui/MainViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,36 @@ package com.toquete.boxbox.ui

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.toquete.boxbox.domain.repository.RemoteConfigRepository
import com.toquete.boxbox.domain.repository.UserPreferencesRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject

@HiltViewModel
class MainViewModel @Inject constructor(
preferencesRepository: UserPreferencesRepository
preferencesRepository: UserPreferencesRepository,
remoteConfigRepository: RemoteConfigRepository
) : ViewModel() {

val state: StateFlow<MainState> = preferencesRepository.userPreferences
.map { preferences ->
MainState(
isLoading = false,
darkThemeConfig = preferences.darkThemeConfig,
colorConfig = preferences.colorConfig
)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = MainState()
val state: StateFlow<MainState> = combine(
preferencesRepository.userPreferences,
remoteConfigRepository.fetchAndActivate()
) { preferences, _ ->
MainState(
isLoading = false,
darkThemeConfig = preferences.darkThemeConfig,
colorConfig = preferences.colorConfig
)
}.catch {
emit(MainState(isLoading = false))
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = MainState()
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.toquete.boxbox.util.remoteconfig

import com.google.firebase.remoteconfig.FirebaseRemoteConfig
import com.google.firebase.remoteconfig.get
import com.toquete.boxbox.core.common.annotation.Generated
import com.toquete.boxbox.core.common.annotation.IoDispatcher
import com.toquete.boxbox.core.model.RemoteConfigs
import com.toquete.boxbox.domain.repository.RemoteConfigRepository
import com.toquete.boxbox.util.remoteconfig.RemoteConfigKeys.IS_AD_BANNER_VISIBLE
import jakarta.inject.Inject
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import timber.log.Timber
import kotlin.coroutines.CoroutineContext

@Generated
class FirebaseRemoteConfigRepository @Inject constructor(
private val firebaseRemoteConfig: FirebaseRemoteConfig,
@IoDispatcher private val dispatcher: CoroutineContext
) : RemoteConfigRepository {

override val remoteConfigs: Flow<RemoteConfigs> = flow {
emit(
RemoteConfigs(isAdBannerVisible = firebaseRemoteConfig[IS_AD_BANNER_VISIBLE].asBoolean())
)
}.flowOn(dispatcher)

override fun fetchAndActivate(): Flow<Boolean> = callbackFlow {
firebaseRemoteConfig.fetchAndActivate()
.addOnCompleteListener { task ->
if (task.isSuccessful) {
trySend(true)
} else {
Timber.e(task.exception, "Failed to fetch and activate remote config")
trySend(false)
}
close(task.exception)
}
awaitClose()
}.flowOn(dispatcher)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.toquete.boxbox.util.remoteconfig

val remoteConfigDefaults = mapOf("is_ad_banner_visible" to false)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.toquete.boxbox.util.remoteconfig

object RemoteConfigKeys {
const val IS_AD_BANNER_VISIBLE = "is_ad_banner_visible"
}
29 changes: 28 additions & 1 deletion app/src/test/java/com/toquete/boxbox/ui/MainViewModelTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@ import com.toquete.boxbox.core.model.DarkThemeConfig
import com.toquete.boxbox.core.model.UserPreferences
import com.toquete.boxbox.core.testing.data.preferences
import com.toquete.boxbox.core.testing.util.MainDispatcherRule
import com.toquete.boxbox.domain.repository.RemoteConfigRepository
import com.toquete.boxbox.domain.repository.UserPreferencesRepository
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import java.io.IOException
import kotlin.test.assertEquals

class MainViewModelTest {
Expand All @@ -24,13 +27,16 @@ class MainViewModelTest {
val mainDispatcherRule = MainDispatcherRule()

private val preferencesRepository: UserPreferencesRepository = mockk(relaxed = true)
private val remoteConfigRepository: RemoteConfigRepository = mockk(relaxed = true)

private lateinit var viewModel: MainViewModel

@Test
fun `init should send success state`() = runTest {
val userPreferencesFlow = MutableSharedFlow<UserPreferences>()
val remoteConfigFlow = MutableSharedFlow<Boolean>()
every { preferencesRepository.userPreferences } returns userPreferencesFlow
every { remoteConfigRepository.fetchAndActivate() } returns remoteConfigFlow

setupViewModel()

Expand All @@ -41,6 +47,8 @@ class MainViewModelTest {
assertEquals(MainState(), viewModel.state.value)

userPreferencesFlow.emit(preferences)
remoteConfigFlow.emit(true)

assertEquals(
MainState(
isLoading = false,
Expand All @@ -53,7 +61,26 @@ class MainViewModelTest {
backgroundScope.cancel()
}

@Test
fun `init should send default state with loading false on error`() = runTest {
val userPreferencesFlow = MutableSharedFlow<UserPreferences>()
every { preferencesRepository.userPreferences } returns userPreferencesFlow
every { remoteConfigRepository.fetchAndActivate() } returns flow { throw IOException() }

setupViewModel()

backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
viewModel.state.collect()
}

userPreferencesFlow.emit(preferences)

assertEquals(MainState(isLoading = false), viewModel.state.value)

backgroundScope.cancel()
}

private fun setupViewModel() {
viewModel = MainViewModel(preferencesRepository)
viewModel = MainViewModel(preferencesRepository, remoteConfigRepository)
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
package com.toquete.boxbox.core.common.annotation

@Target(
AnnotationTarget.FIELD,
AnnotationTarget.PROPERTY,
AnnotationTarget.FUNCTION,
AnnotationTarget.CLASS,
AnnotationTarget.FILE
)
annotation class Generated
Loading

0 comments on commit b1e0700

Please sign in to comment.