From 6a0716c5baa4b9fa68e508cf791bc851869cd243 Mon Sep 17 00:00:00 2001 From: Marcello Date: Sun, 5 Apr 2026 18:15:26 +0200 Subject: [PATCH 1/3] Initialize the `data` module - Create a new Android library module `data` with namespace `eu.project.data`. - Configure `build.gradle.kts` with `compileSdk` 36, `minSdk` 26, and Java 11 compatibility. - Add standard dependencies for AndroidX Core, AppCompat, Material, JUnit, and Espresso. - Register the `:data` module in `settings.gradle.kts`. - Add default ProGuard configuration files and a `.gitignore` for the new module. --- data/.gitignore | 1 + data/build.gradle.kts | 44 +++++++++++++++++++++++++++++++ data/consumer-rules.pro | 0 data/proguard-rules.pro | 21 +++++++++++++++ data/src/main/AndroidManifest.xml | 2 ++ settings.gradle.kts | 1 + 6 files changed, 69 insertions(+) create mode 100644 data/.gitignore create mode 100644 data/build.gradle.kts create mode 100644 data/consumer-rules.pro create mode 100644 data/proguard-rules.pro create mode 100644 data/src/main/AndroidManifest.xml diff --git a/data/.gitignore b/data/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/data/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/build.gradle.kts b/data/build.gradle.kts new file mode 100644 index 0000000..dbf23a3 --- /dev/null +++ b/data/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) +} + +android { + namespace = "eu.project.data" + compileSdk { + version = release(36) + } + + defaultConfig { + minSdk = 26 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } +} + +dependencies { + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.material) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) +} \ No newline at end of file diff --git a/data/consumer-rules.pro b/data/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/data/proguard-rules.pro b/data/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/data/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/data/src/main/AndroidManifest.xml b/data/src/main/AndroidManifest.xml new file mode 100644 index 0000000..568741e --- /dev/null +++ b/data/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 10ea37c..ad86f8c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -32,3 +32,4 @@ include(":localData") include(":remoteData") include(":auth") include(":feature:authenticate") +include(":data") From 5477fff552beb22c3543030516d7f9c2ce183c64 Mon Sep 17 00:00:00 2001 From: Marcello Date: Sun, 5 Apr 2026 23:28:25 +0200 Subject: [PATCH 2/3] Configure the `:data` module and enable cleartext traffic for debug builds - Enable `android:usesCleartextTraffic` in a new debug `AndroidManifest.xml` to allow HTTP communication during development. - Update `data/build.gradle.kts` to enable `buildConfig` and define `API_BASE_URL` fields for debug and release builds, sourced from local properties. - Add Hilt, Retrofit, OkHttp, and Gson dependencies to the `:data` module. - Add testing dependencies including MockK, MockWebServer, and Coroutines test utilities. - Include project dependencies for `:common` and `:auth` in the `:data` module. --- app/src/debug/AndroidManifest.xml | 8 ++++++ data/build.gradle.kts | 47 +++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 app/src/debug/AndroidManifest.xml diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..cbc4f57 --- /dev/null +++ b/app/src/debug/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/data/build.gradle.kts b/data/build.gradle.kts index dbf23a3..e3b4d86 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -1,6 +1,10 @@ +import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties + plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) + alias(libs.plugins.google.devtools.ksp) + alias(libs.plugins.hilt.android) } android { @@ -17,14 +21,31 @@ android { } buildTypes { + debug { + isMinifyEnabled = false + + val apiBaseUrlDebug = gradleLocalProperties(rootDir, providers) + .getProperty("API_BASE_URL_DEBUG") ?: "\"http://example.com\"" + + buildConfigField("String", "API_BASE_URL", apiBaseUrlDebug) + } release { isMinifyEnabled = false + + val apiBaseUrlRelease = gradleLocalProperties(rootDir, providers) + .getProperty("API_BASE_URL_RELEASE") ?: "\"https://example.com\"" + + buildConfigField("String", "API_BASE_URL", apiBaseUrlRelease) + proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } } + buildFeatures { + buildConfig = true + } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 @@ -41,4 +62,30 @@ dependencies { testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) + + // Hilt + implementation(libs.hilt.android) + ksp(libs.hilt.android.compiler) + + // Retrofit + implementation(libs.retrofit) + + // OkHttp + implementation(libs.okhttp) + implementation(libs.logging.interceptor) + testImplementation(libs.mockwebserver) + + // Gson converter + implementation(libs.converter.gson) + + // MockK + testImplementation(libs.mockk) + testImplementation(libs.mockk.android) + testImplementation(libs.mockk.agent) + + // Coroutines test + testImplementation(libs.kotlinx.coroutines.test) + + implementation(project(":common")) + implementation(project(":auth")) } \ No newline at end of file From 15ac393fc46e91d89455c8183b575d2f3ff8f3b8 Mon Sep 17 00:00:00 2001 From: Marcello Date: Sun, 5 Apr 2026 23:48:14 +0200 Subject: [PATCH 3/3] Implement network infrastructure and `TestRepository` - Create `ApplicationOkHttpClient` to configure `OkHttpClient` with custom timeouts and interceptors. - Implement `AuthzInterceptor` to automatically attach JWT access tokens from `AuthzManager` to outgoing requests. - Implement `LoggingInterceptor` using `HttpLoggingInterceptor`, enabling full body logging in debug builds. - Define `WebApplicationClient` and its Retrofit-based implementation `WebApplicationClientImplD` to manage API endpoints. - Introduce `TestEndpoint` and `TestRepository` to handle initial connectivity testing. - Configure Dagger Hilt modules `ClientModule` and `RepositoryModule` to provide network dependencies and repositories. --- .../remote/client/ApplicationOkHttpClient.kt | 23 ++++++++++ .../remote/client/WebApplicationClient.kt | 7 +++ .../client/WebApplicationClientImplD.kt | 24 ++++++++++ .../eu/project/data/remote/di/ClientModule.kt | 45 +++++++++++++++++++ .../data/remote/di/RepositoryModule.kt | 22 +++++++++ .../data/remote/endpoint/TestEndpoint.kt | 10 +++++ .../remote/interceptor/AuthzInterceptor.kt | 28 ++++++++++++ .../remote/interceptor/LoggingInterceptor.kt | 26 +++++++++++ .../data/remote/repository/TestRepository.kt | 5 +++ .../remote/repository/TestRepositoryImpl.kt | 22 +++++++++ 10 files changed, 212 insertions(+) create mode 100644 data/src/main/java/eu/project/data/remote/client/ApplicationOkHttpClient.kt create mode 100644 data/src/main/java/eu/project/data/remote/client/WebApplicationClient.kt create mode 100644 data/src/main/java/eu/project/data/remote/client/WebApplicationClientImplD.kt create mode 100644 data/src/main/java/eu/project/data/remote/di/ClientModule.kt create mode 100644 data/src/main/java/eu/project/data/remote/di/RepositoryModule.kt create mode 100644 data/src/main/java/eu/project/data/remote/endpoint/TestEndpoint.kt create mode 100644 data/src/main/java/eu/project/data/remote/interceptor/AuthzInterceptor.kt create mode 100644 data/src/main/java/eu/project/data/remote/interceptor/LoggingInterceptor.kt create mode 100644 data/src/main/java/eu/project/data/remote/repository/TestRepository.kt create mode 100644 data/src/main/java/eu/project/data/remote/repository/TestRepositoryImpl.kt diff --git a/data/src/main/java/eu/project/data/remote/client/ApplicationOkHttpClient.kt b/data/src/main/java/eu/project/data/remote/client/ApplicationOkHttpClient.kt new file mode 100644 index 0000000..0a192f2 --- /dev/null +++ b/data/src/main/java/eu/project/data/remote/client/ApplicationOkHttpClient.kt @@ -0,0 +1,23 @@ +package eu.project.data.remote.client + +import eu.project.data.remote.interceptor.AuthzInterceptor +import eu.project.data.remote.interceptor.LoggingInterceptor +import okhttp3.OkHttpClient +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class ApplicationOkHttpClient @Inject constructor( + authzInterceptor: AuthzInterceptor, + loggingInterceptor: LoggingInterceptor +) { + + val client = OkHttpClient.Builder() + .addInterceptor(authzInterceptor) + .addInterceptor(loggingInterceptor) + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .retryOnConnectionFailure(true) + .build() +} \ No newline at end of file diff --git a/data/src/main/java/eu/project/data/remote/client/WebApplicationClient.kt b/data/src/main/java/eu/project/data/remote/client/WebApplicationClient.kt new file mode 100644 index 0000000..5a2cd22 --- /dev/null +++ b/data/src/main/java/eu/project/data/remote/client/WebApplicationClient.kt @@ -0,0 +1,7 @@ +package eu.project.data.remote.client + +import eu.project.data.remote.endpoint.TestEndpoint + +internal interface WebApplicationClient { + val testEndpoint: TestEndpoint +} \ No newline at end of file diff --git a/data/src/main/java/eu/project/data/remote/client/WebApplicationClientImplD.kt b/data/src/main/java/eu/project/data/remote/client/WebApplicationClientImplD.kt new file mode 100644 index 0000000..96afb6c --- /dev/null +++ b/data/src/main/java/eu/project/data/remote/client/WebApplicationClientImplD.kt @@ -0,0 +1,24 @@ +package eu.project.data.remote.client + +import eu.project.data.BuildConfig +import eu.project.data.remote.endpoint.TestEndpoint +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.create +import javax.inject.Inject + +internal class WebApplicationClientImplD @Inject constructor( + okHttpClient: ApplicationOkHttpClient, +): WebApplicationClient { + + private val apiBaseUrl = BuildConfig.API_BASE_URL + + private val retrofit = Retrofit.Builder() + .baseUrl(apiBaseUrl) + .client(okHttpClient.client) + .addConverterFactory(GsonConverterFactory.create()) + .build() + + override val testEndpoint: TestEndpoint + get() = retrofit.create() +} \ No newline at end of file diff --git a/data/src/main/java/eu/project/data/remote/di/ClientModule.kt b/data/src/main/java/eu/project/data/remote/di/ClientModule.kt new file mode 100644 index 0000000..e60c89d --- /dev/null +++ b/data/src/main/java/eu/project/data/remote/di/ClientModule.kt @@ -0,0 +1,45 @@ +package eu.project.data.remote.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import eu.project.auth.authz.AuthzManager +import eu.project.data.remote.client.ApplicationOkHttpClient +import eu.project.data.remote.client.WebApplicationClient +import eu.project.data.remote.client.WebApplicationClientImplD +import eu.project.data.remote.interceptor.AuthzInterceptor +import eu.project.data.remote.interceptor.LoggingInterceptor +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal class ClientModule { + + @Provides + @Singleton + fun provideAuthzInterceptor( + authzManager: AuthzManager + ): AuthzInterceptor = + AuthzInterceptor(authzManager) + + @Provides + @Singleton + fun provideApplicationOkHttpClient( + authzInterceptor: AuthzInterceptor, + loggingInterceptor: LoggingInterceptor + ): ApplicationOkHttpClient = + ApplicationOkHttpClient( + authzInterceptor = authzInterceptor, + loggingInterceptor = loggingInterceptor + ) + + @Provides + @Singleton + fun provideWebApplicationClient( + applicationOkHttpClient: ApplicationOkHttpClient + ): WebApplicationClient = + WebApplicationClientImplD( + okHttpClient = applicationOkHttpClient + ) +} \ No newline at end of file diff --git a/data/src/main/java/eu/project/data/remote/di/RepositoryModule.kt b/data/src/main/java/eu/project/data/remote/di/RepositoryModule.kt new file mode 100644 index 0000000..e72e5b6 --- /dev/null +++ b/data/src/main/java/eu/project/data/remote/di/RepositoryModule.kt @@ -0,0 +1,22 @@ +package eu.project.data.remote.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import eu.project.data.remote.client.WebApplicationClient +import eu.project.data.remote.repository.TestRepository +import eu.project.data.remote.repository.TestRepositoryImpl +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal class RepositoryModule { + + @Provides + @Singleton + fun provideTestRepository ( + webApplicationClient: WebApplicationClient + ): TestRepository = + TestRepositoryImpl(webApplicationClient.testEndpoint) +} \ No newline at end of file diff --git a/data/src/main/java/eu/project/data/remote/endpoint/TestEndpoint.kt b/data/src/main/java/eu/project/data/remote/endpoint/TestEndpoint.kt new file mode 100644 index 0000000..9d2aaae --- /dev/null +++ b/data/src/main/java/eu/project/data/remote/endpoint/TestEndpoint.kt @@ -0,0 +1,10 @@ +package eu.project.data.remote.endpoint + +import retrofit2.Response +import retrofit2.http.GET + +internal interface TestEndpoint { + + @GET("/") + suspend fun test(): Response +} \ No newline at end of file diff --git a/data/src/main/java/eu/project/data/remote/interceptor/AuthzInterceptor.kt b/data/src/main/java/eu/project/data/remote/interceptor/AuthzInterceptor.kt new file mode 100644 index 0000000..4e6be46 --- /dev/null +++ b/data/src/main/java/eu/project/data/remote/interceptor/AuthzInterceptor.kt @@ -0,0 +1,28 @@ +package eu.project.data.remote.interceptor + +import eu.project.auth.authz.AuthzManager +import okhttp3.Interceptor +import okhttp3.Response +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class AuthzInterceptor @Inject constructor( + private val authzManager: AuthzManager +): Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + + // try to get JWT access token to be passed into the 'Authorization' header + val rawAccessToken = + authzManager.getAccessToken()?.value ?: + return chain.proceed(chain.request()) // no token available, proceed without authorization + + val originalRequest = chain.request() + val modifiedRequest = originalRequest.newBuilder() + .addHeader("Authorization", "Bearer $rawAccessToken") + .build() + + return chain.proceed(modifiedRequest) + } +} \ No newline at end of file diff --git a/data/src/main/java/eu/project/data/remote/interceptor/LoggingInterceptor.kt b/data/src/main/java/eu/project/data/remote/interceptor/LoggingInterceptor.kt new file mode 100644 index 0000000..9bba066 --- /dev/null +++ b/data/src/main/java/eu/project/data/remote/interceptor/LoggingInterceptor.kt @@ -0,0 +1,26 @@ +package eu.project.data.remote.interceptor + +import eu.project.data.BuildConfig +import okhttp3.Interceptor +import okhttp3.Response +import okhttp3.logging.HttpLoggingInterceptor +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class LoggingInterceptor @Inject constructor() : Interceptor { + + private val logger = HttpLoggingInterceptor().apply { + level = + if (BuildConfig.DEBUG) { + HttpLoggingInterceptor.Level.BODY + } + else { + HttpLoggingInterceptor.Level.NONE + } + } + + override fun intercept(chain: Interceptor.Chain): Response { + return logger.intercept(chain) + } +} \ No newline at end of file diff --git a/data/src/main/java/eu/project/data/remote/repository/TestRepository.kt b/data/src/main/java/eu/project/data/remote/repository/TestRepository.kt new file mode 100644 index 0000000..ea7d3e6 --- /dev/null +++ b/data/src/main/java/eu/project/data/remote/repository/TestRepository.kt @@ -0,0 +1,5 @@ +package eu.project.data.remote.repository + +interface TestRepository { + suspend fun test(): Result +} \ No newline at end of file diff --git a/data/src/main/java/eu/project/data/remote/repository/TestRepositoryImpl.kt b/data/src/main/java/eu/project/data/remote/repository/TestRepositoryImpl.kt new file mode 100644 index 0000000..a5384e2 --- /dev/null +++ b/data/src/main/java/eu/project/data/remote/repository/TestRepositoryImpl.kt @@ -0,0 +1,22 @@ +package eu.project.data.remote.repository + +import eu.project.data.remote.endpoint.TestEndpoint +import retrofit2.Response +import javax.inject.Inject + +internal class TestRepositoryImpl @Inject constructor( + private val testEndpoint: TestEndpoint +): TestRepository { + + override suspend fun test(): Result { + return runCatching { + val response: Response = testEndpoint.test() + if (response.isSuccessful) { + response.body() ?: error("Empty response body") + } + else { + throw IllegalStateException("HTTP ${response.code()}: ${response.errorBody()?.string()}") + } + } + } +} \ No newline at end of file