Compare commits

...

4 Commits

21 changed files with 515 additions and 6 deletions

View File

@ -7,6 +7,9 @@ plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.compose)
alias(libs.plugins.hilt)
alias(libs.plugins.kotlin.serialization)
} }
// Keystore // Keystore
@ -88,6 +91,7 @@ dependencies {
implementation(libs.hilt.android) implementation(libs.hilt.android)
ksp(libs.hilt.compiler) ksp(libs.hilt.compiler)
implementation(libs.hilt.navigation.compose) implementation(libs.hilt.navigation.compose)
implementation(libs.matomo)
// Compose // Compose
implementation(platform(libs.compose.bom)) implementation(platform(libs.compose.bom))
@ -96,9 +100,16 @@ dependencies {
// Timber // Timber
implementation(libs.timber) implementation(libs.timber)
// Compose Navigation
implementation(libs.androidx.navigation.compose)
// Tests // Tests
testImplementation(libs.test.junit) testImplementation(libs.test.junit)
androidTestImplementation(libs.test.junit) androidTestImplementation(libs.test.junit)
androidTestImplementation(libs.test.espresso) androidTestImplementation(libs.test.espresso)
androidTestImplementation(libs.test.androidx.junit) androidTestImplementation(libs.test.androidx.junit)
// Kotlin serialization
implementation(libs.kotlin.serialization)
} }

View File

@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools" > xmlns:tools="http://schemas.android.com/tools" >
<application <application
android:name=".DemoApplication"
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"

View File

@ -0,0 +1,13 @@
package fr.openium.consentium
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class DemoApplication : Application() {
override fun onCreate() {
super.onCreate()
}
}

View File

@ -1,9 +1,11 @@
package fr.openium.consentium package fr.openium.consentium
import DemoNavGraph
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
@ -11,19 +13,28 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.navigation.compose.rememberNavController
import dagger.hilt.android.AndroidEntryPoint
import fr.openium.consentium.ui.theme.ConsentiumTheme import fr.openium.consentium.ui.theme.ConsentiumTheme
@AndroidEntryPoint
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
val navHostController = rememberNavController()
ConsentiumTheme { ConsentiumTheme {
Scaffold( modifier = Modifier.fillMaxSize() ) { innerPadding -> Scaffold( modifier = Modifier.fillMaxSize() ) { paddingValues ->
Greeting( Box(
name = "Android", modifier = Modifier
modifier = Modifier.padding(innerPadding) .fillMaxSize()
) .padding(paddingValues)
) {
DemoNavGraph(navHostController = navHostController)
}
} }
} }
} }

View File

@ -0,0 +1,48 @@
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.runtime.Composable
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.navigation
import fr.openium.consentium.ui.screens.main.MainScreen
import fr.openium.consentium.ui.screens.splash.SplashScreen
private const val NAV_ANIMATION_TIME = 100
@Composable
fun DemoNavGraph(navHostController: NavHostController) {
NavHost(
navController = navHostController,
startDestination = Destination.Splash,
enterTransition = {
fadeIn(animationSpec = tween(NAV_ANIMATION_TIME))
},
exitTransition = {
fadeOut(animationSpec = tween(NAV_ANIMATION_TIME))
},
) {
composable<Destination.Splash> {
SplashScreen(
navigateToMain = { ->
navHostController.navigate(Destination.Main) {
popUpTo(navHostController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
})
}
composable<Destination.Main> {
MainScreen()
}
}
}

View File

@ -0,0 +1,13 @@
import kotlinx.serialization.Serializable
sealed interface Destination {
@Serializable
data object Splash : Destination
@Serializable
data object Main : Destination
}

View File

@ -0,0 +1,14 @@
package fr.openium.consentium.ui.screens.main
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.hilt.navigation.compose.hiltViewModel
@Composable
fun MainScreen(
viewModel: MainScreenViewModel = hiltViewModel()
) {
Text("Main")
}

View File

@ -0,0 +1,11 @@
package fr.openium.consentium.ui.screens.main
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class MainScreenViewModel @Inject constructor(
) : ViewModel() {
}

View File

@ -0,0 +1,50 @@
package fr.openium.consentium.ui.screens.splash
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
@Composable
fun SplashScreen(
viewModel: SplashScreenViewModel = hiltViewModel(),
navigateToMain: () -> Unit,
) {
val state by viewModel.state.collectAsState()
Text("Splash")
LaunchedEffect(Unit) {
viewModel.initMain()
}
when (val _state = state) {
is SplashScreenViewModel.State.Loading -> Box(
modifier = Modifier.fillMaxSize()
) {
CircularProgressIndicator(
modifier = Modifier
.size(200.dp)
.align(Alignment.Center)
)
//tracking matomo
}
is SplashScreenViewModel.State.Loaded -> {
navigateToMain()
}
}
}

View File

@ -0,0 +1,39 @@
package fr.openium.consentium.ui.screens.splash
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class SplashScreenViewModel @Inject constructor(
) : ViewModel() {
private val _state = MutableStateFlow<State>(State.Loading)
val state: StateFlow<State> = _state
fun initMain() {
val delay = viewModelScope.launch {
delay(1500L)
}
viewModelScope.launch {
_state.value = State.Loading
try {
delay.join()
_state.value = State.Loaded
} catch (e: Exception) {
e.printStackTrace()
}
}
}
sealed interface State {
data object Loading : State
data object Loaded : State
}
}

View File

@ -15,6 +15,12 @@ plugins {
// Agp // Agp
alias(libs.plugins.android.library) apply false alias(libs.plugins.android.library) apply false
// Hilt //Hilt
alias(libs.plugins.hilt) apply false alias(libs.plugins.hilt) apply false
//Kotlin serialization
alias(libs.plugins.kotlin.serialization) apply false
} }

View File

@ -3,6 +3,7 @@ plugins {
alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.android)
alias(libs.plugins.hilt) alias(libs.plugins.hilt)
alias(libs.plugins.ksp) alias(libs.plugins.ksp)
alias(libs.plugins.serialization)
} }
android { android {
@ -43,6 +44,22 @@ dependencies {
implementation(libs.hilt.android) implementation(libs.hilt.android)
ksp(libs.hilt.compiler) ksp(libs.hilt.compiler)
// Serialization
implementation(libs.serializationJson)
// Retrofit
api(libs.retrofit)
implementation(libs.retrofitConverter)
implementation(libs.logging.interceptor)
// OkHttp
api(libs.okhttp)
implementation(platform(libs.okhttp.bom))
implementation(libs.okhttp)
// Timber
implementation(libs.timber)
// Test // Test
testImplementation(libs.test.junit) testImplementation(libs.test.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)

View File

@ -0,0 +1,37 @@
package fr.openium.consentium
import android.content.Context
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import fr.openium.consentium.data.di.ConsentiumUrl
import fr.openium.consentium.data.di.NetworkModule
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import timber.log.Timber
@Module
@InstallIn(SingletonComponent::class)
class DebugNetworkModule {
@Provides
fun provideOkHttpBuilder(
@ApplicationContext context: Context,
): OkHttpClient.Builder =
NetworkModule.standardOkHttpBuilder(context)
.addNetworkInterceptor(HttpLoggingInterceptor { message ->
Timber.tag("OkHttp")
Timber.v(message)
}.apply {
level = HttpLoggingInterceptor.Level.BODY
})
@ConsentiumUrl
@Provides
fun okHttpUrlConsentium(@ApplicationContext context: Context): HttpUrl =
context.getString(R.string.backend_url).toHttpUrl()
}

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="backend_url" translatable="false" tools:ignore="UnusedResources">https://consentium-api-dev.openium.fr/api/v1/app</string>
</resources>

View File

@ -0,0 +1,78 @@
package fr.openium.consentium.data.di
import android.content.Context
import dagger.Lazy
import dagger.Module
import dagger.Provides
import dagger.Reusable
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import fr.openium.consentium.data.remote.ConsentiumApi
import kotlinx.serialization.json.Json
import okhttp3.Cache
import okhttp3.HttpUrl
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.kotlinx.serialization.asConverterFactory
import java.util.concurrent.TimeUnit
import javax.inject.Qualifier
import javax.inject.Singleton
private val json by lazy {
Json {
isLenient = true
ignoreUnknownKeys = true
}
}
@Module
@InstallIn(SingletonComponent::class)
internal object NetworkModule {
@OkHttpClientDefault
@Provides
@Singleton
fun okHttpClientDefault(
okHttpBuilder: OkHttpClient.Builder,
): OkHttpClient = okHttpBuilder.build()
fun standardOkHttpBuilder(
context: Context,
): OkHttpClient.Builder {
val cacheSize = (20 * 1024 * 1024).toLong() // 20 MiB
val cache = Cache(context.cacheDir, cacheSize)
return OkHttpClient.Builder()
.cache(cache)
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
}
@Reusable
@Provides
fun provideConsentiumApi(
@ConsentiumUrl url: HttpUrl,
@OkHttpClientDefault okHttpClient: Lazy<OkHttpClient>,
): ConsentiumApi =
createRetrofit(
url,
okHttpClient
).create(ConsentiumApi::class.java)
private fun createRetrofit(url: HttpUrl, okHttpClient: Lazy<OkHttpClient>): Retrofit =
Retrofit.Builder()
.callFactory { request ->
okHttpClient.get().newCall(request)
}.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.baseUrl(url)
.build()
}
@Qualifier
@Retention(AnnotationRetention.BINARY)
internal annotation class ConsentiumUrl
@Qualifier
@Retention(AnnotationRetention.BINARY)
internal annotation class OkHttpClientDefault

View File

@ -0,0 +1,25 @@
package fr.openium.consentium.data.remote
import fr.openium.consentium.data.remote.model.GetConsent
import fr.openium.consentium.data.remote.model.PatchConsent
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.PATCH
internal interface ConsentiumApi {
@GET("/consents")
suspend fun getConsents(
applicationId: String,
installationId: String,
): GetConsent.GetConsentPayloadDTO
@PATCH("/consents")
suspend fun setConsents(
applicationId: String,
installationId: String,
@Body patchConsent: PatchConsent.PatchConsentPayloadDTO,
): Any
}

View File

@ -0,0 +1,44 @@
package fr.openium.consentium.data.remote.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
internal sealed interface GetConsent {
@Serializable
data class GetConsentPayloadDTO(
@SerialName("installationId") val installationId: String,
@SerialName("purposes") val purposes: List<PurposeDTO>? = null,
@SerialName("isValid") val isValid: Boolean,
)
@Serializable
data class PurposeDTO(
@SerialName("identifier") val identifier: String,
@SerialName("isRequired") val isRequired: Boolean,
@SerialName("isAccepted") val isAccepted: PurposeStatus,
@SerialName("vendors") val vendors: List<VendorDTO>? = null,
)
@Serializable
data class VendorDTO(
@SerialName("identifier") val identifier: String,
@SerialName("isRequired") val isRequired: Boolean,
@SerialName("isAccepted") val isAccepted: Boolean,
)
@Serializable
enum class PurposeStatus {
@SerialName("ACCEPTED")
ACCEPTED,
@SerialName("REJECTED")
REJECTED,
@SerialName("NOT_DEFINED")
NOT_DEFINED,
}
}

View File

@ -0,0 +1,27 @@
package fr.openium.consentium.data.remote.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
internal sealed interface PatchConsent {
@Serializable
data class PatchConsentPayloadDTO(
@SerialName("installationId") val installationId: String,
@SerialName("purposes") val purposes: List<PurposeDTO>? = null,
)
@Serializable
data class PurposeDTO(
@SerialName("identifier") val identifier: String,
@SerialName("isAccepted") val isAccepted: Boolean,
@SerialName("vendors") val vendors: List<VendorDTO>? = null,
)
@Serializable
data class VendorDTO(
@SerialName("identifier") val identifier: String,
@SerialName("isAccepted") val isAccepted: Boolean,
)
}

View File

@ -0,0 +1,13 @@
package fr.openium.consentium.data.repository
import fr.openium.consentium.data.local.ConsentiumDataStore
import fr.openium.consentium.data.remote.ConsentiumApi
import javax.inject.Inject
internal class ConsentiumRepository @Inject constructor(
private val consentiumApi: ConsentiumApi,
private val consentiumDataStore: ConsentiumDataStore,
) {
}

View File

@ -26,6 +26,17 @@ timber = "5.0.1"
# Material # Material
material = "1.12.0" material = "1.12.0"
# Serialization
serialization = "2.0.21"
jsonSerialization = "1.7.3"
# Retrofit
retrofit = "2.11.0"
loggingInterceptor = "4.12.0"
# Okhttp
okhttpBom = "4.12.0"
# Test # Test
junit = "4.13.2" junit = "4.13.2"
espressoCore = "3.6.1" espressoCore = "3.6.1"
@ -40,6 +51,16 @@ kotlin = "2.0.0"
ksp = "2.0.0-1.0.23" ksp = "2.0.0-1.0.23"
junitVersion = "1.2.1" junitVersion = "1.2.1"
# Matomo
matomo = "4.3"
# Serialization
serialization = "1.7.1"
navigationCompose = "2.8.2"
[libraries] [libraries]
# AndroidX # AndroidX
@ -70,9 +91,32 @@ compose-material3 = { group = "androidx.compose.material3", name = "material3" }
# Material # Material
material = { group = "com.google.android.material", name = "material", version.ref = "material" } material = { group = "com.google.android.material", name = "material", version.ref = "material" }
# Matomo
matomo = { module = "com.github.matomo-org:matomo-sdk-android", version.ref = "matomo" }
# Kotlin serizalization
kotlin-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" }
# Compose navigation
androidx-navigation-common-ktx = { group = "androidx.navigation", name = "navigation-common-ktx", version.ref = "navigationCompose" }
androidx-navigation-runtime-ktx = { group = "androidx.navigation", name = "navigation-runtime-ktx", version.ref = "navigationCompose" }
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
# Preferences DataStore # Preferences DataStore
preferencesDataStore = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "preferencesDataStore" } preferencesDataStore = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "preferencesDataStore" }
# Json serialization
serializationJson = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "jsonSerialization" }
# Retrofit
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
retrofitConverter = { group = "com.squareup.retrofit2", name = "converter-kotlinx-serialization", version.ref = "retrofit" }
logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "loggingInterceptor" }
# Okhttp
okhttp-bom = { module = "com.squareup.okhttp3:okhttp-bom", version.ref = "okhttpBom" }
okhttp = { module = "com.squareup.okhttp3:okhttp" }
# Test # Test
test-junit = { group = "junit", name = "junit", version.ref = "junit" } test-junit = { group = "junit", name = "junit", version.ref = "junit" }
test-androidx-junit = { group = "androidx.test.ext", name = "junit-ktx", version.ref = "junitExtVersion" } test-androidx-junit = { group = "androidx.test.ext", name = "junit-ktx", version.ref = "junitExtVersion" }
@ -86,6 +130,7 @@ kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "ko
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
android-library = { id = "com.android.library", version.ref = "agp" } android-library = { id = "com.android.library", version.ref = "agp" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "serialization" }
[bundles] [bundles]
androidx = ["androidx-core-ktx", "androidx-activity-compose"] androidx = ["androidx-core-ktx", "androidx-activity-compose"]

View File

@ -16,6 +16,8 @@ dependencyResolutionManagement {
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
maven { url = uri("https://jitpack.io/") }
} }
} }