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/.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..e3b4d86 --- /dev/null +++ b/data/build.gradle.kts @@ -0,0 +1,91 @@ +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 { + namespace = "eu.project.data" + compileSdk { + version = release(36) + } + + defaultConfig { + minSdk = 26 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + 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 + } + 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) + + // 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 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/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 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")