Skip to content

Commit eb7bdc8

Browse files
authored
Merge pull request #1 from emrepbu/Feature/APISupport
feat: Implement API integration for SMS forwarding
2 parents 94b91ee + 916f4ee commit eb7bdc8

19 files changed

Lines changed: 816 additions & 23 deletions

app/build.gradle.kts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ plugins {
44
alias(libs.plugins.kotlin.compose)
55
alias(libs.plugins.devtools.ksp)
66
alias(libs.plugins.hilt.android)
7+
kotlin("plugin.serialization") version "1.9.0"
78
}
89

910
android {
@@ -82,6 +83,22 @@ dependencies {
8283
implementation(libs.gson)
8384
implementation(libs.androidx.datastore.preferences)
8485
implementation(libs.jakarta.mail)
86+
87+
// Ktor dependencies
88+
implementation("io.ktor:ktor-client-android:2.3.5")
89+
implementation("io.ktor:ktor-client-core:2.3.5")
90+
implementation("io.ktor:ktor-client-cio:2.3.5")
91+
implementation("io.ktor:ktor-client-content-negotiation:2.3.5")
92+
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.5")
93+
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
94+
95+
// Ktor dependencies
96+
implementation("io.ktor:ktor-client-android:2.3.5")
97+
implementation("io.ktor:ktor-client-core:2.3.5")
98+
implementation("io.ktor:ktor-client-cio:2.3.5")
99+
implementation("io.ktor:ktor-client-content-negotiation:2.3.5")
100+
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.5")
101+
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
85102

86103
testImplementation(libs.junit)
87104

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.emrepbu.smsgateway.data.remote.api.model
2+
3+
import kotlinx.serialization.Serializable
4+
5+
@Serializable
6+
data class SmsRequest(
7+
val phoneNumber: String,
8+
val message: String,
9+
val senderName: String? = null,
10+
val timestamp: Long = System.currentTimeMillis()
11+
)
12+
13+
@Serializable
14+
data class ApiResponse(
15+
val success: Boolean,
16+
val message: String,
17+
val data: String? = null
18+
)
19+
20+
sealed class ApiResult<out T> {
21+
data class Success<T>(val data: T) : ApiResult<T>()
22+
data class Error(val message: String, val code: Int? = null) : ApiResult<Nothing>()
23+
data object Loading : ApiResult<Nothing>()
24+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package com.emrepbu.smsgateway.data.remote.api.service
2+
3+
import android.util.Log
4+
import com.emrepbu.smsgateway.data.remote.api.model.ApiResponse
5+
import com.emrepbu.smsgateway.data.remote.api.model.ApiResult
6+
import com.emrepbu.smsgateway.data.remote.api.model.SmsRequest
7+
import io.ktor.client.*
8+
import io.ktor.client.call.*
9+
import io.ktor.client.engine.cio.*
10+
import io.ktor.client.plugins.*
11+
import io.ktor.client.plugins.contentnegotiation.*
12+
import io.ktor.client.request.*
13+
import io.ktor.client.statement.*
14+
import io.ktor.http.*
15+
import io.ktor.serialization.kotlinx.json.*
16+
import kotlinx.coroutines.Dispatchers
17+
import kotlinx.coroutines.withContext
18+
import kotlinx.serialization.json.Json
19+
import javax.inject.Inject
20+
import javax.inject.Singleton
21+
22+
private const val TAG = "ApiService"
23+
24+
@Singleton
25+
class ApiService @Inject constructor() {
26+
private val client = HttpClient(CIO) {
27+
install(ContentNegotiation) {
28+
json(Json {
29+
prettyPrint = true
30+
isLenient = true
31+
ignoreUnknownKeys = true
32+
})
33+
}
34+
install(HttpTimeout) {
35+
requestTimeoutMillis = 15000 // 15 saniye
36+
connectTimeoutMillis = 15000
37+
socketTimeoutMillis = 15000
38+
}
39+
defaultRequest {
40+
contentType(ContentType.Application.Json)
41+
accept(ContentType.Application.Json)
42+
}
43+
}
44+
45+
suspend fun sendSms(apiUrl: String, phoneNumber: String, message: String, senderName: String? = null, authToken: String? = null): ApiResult<ApiResponse> {
46+
return try {
47+
withContext(Dispatchers.IO) {
48+
val response = client.post(apiUrl) {
49+
setBody(SmsRequest(phoneNumber, message, senderName))
50+
if (!authToken.isNullOrBlank()) {
51+
header("Authorization", "Bearer $authToken")
52+
}
53+
}
54+
55+
when (response.status) {
56+
HttpStatusCode.OK -> {
57+
val responseData: ApiResponse = response.body()
58+
ApiResult.Success(responseData)
59+
}
60+
else -> {
61+
ApiResult.Error("API isteği başarısız: ${response.status.description}", response.status.value)
62+
}
63+
}
64+
}
65+
} catch (e: ClientRequestException) {
66+
Log.e(TAG, "HTTP Hatası: ${e.response.status.value}")
67+
ApiResult.Error("HTTP Hatası: ${e.response.status.description}", e.response.status.value)
68+
} catch (e: ServerResponseException) {
69+
Log.e(TAG, "Sunucu Hatası: ${e.response.status.value}")
70+
ApiResult.Error("Sunucu Hatası: ${e.response.status.description}", e.response.status.value)
71+
} catch (e: HttpRequestTimeoutException) {
72+
Log.e(TAG, "Bağlantı zaman aşımına uğradı", e)
73+
ApiResult.Error("Bağlantı zaman aşımına uğradı. İnternet bağlantınızı kontrol edin.")
74+
} catch (e: Exception) {
75+
Log.e(TAG, "API isteği sırasında hata oluştu", e)
76+
ApiResult.Error("İstek gönderilirken bir hata oluştu: ${e.localizedMessage}")
77+
}
78+
}
79+
80+
fun close() {
81+
client.close()
82+
}
83+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.emrepbu.smsgateway.data.repository
2+
3+
import androidx.datastore.core.DataStore
4+
import androidx.datastore.preferences.core.Preferences
5+
import androidx.datastore.preferences.core.booleanPreferencesKey
6+
import androidx.datastore.preferences.core.edit
7+
import androidx.datastore.preferences.core.stringPreferencesKey
8+
import com.emrepbu.smsgateway.domain.model.ApiConfig
9+
import com.emrepbu.smsgateway.domain.repository.ApiConfigRepository
10+
import kotlinx.coroutines.flow.Flow
11+
import kotlinx.coroutines.flow.map
12+
import javax.inject.Inject
13+
import javax.inject.Singleton
14+
15+
@Singleton
16+
class ApiConfigRepositoryImpl @Inject constructor(
17+
private val dataStore: DataStore<Preferences>
18+
) : ApiConfigRepository {
19+
20+
private object PreferencesKeys {
21+
val API_ENABLED = booleanPreferencesKey("api_enabled")
22+
val API_URL = stringPreferencesKey("api_url")
23+
val API_AUTH_TOKEN = stringPreferencesKey("api_auth_token")
24+
val API_CUSTOM_SENDER_NAME = stringPreferencesKey("api_custom_sender_name")
25+
}
26+
27+
override fun getApiConfig(): Flow<ApiConfig> {
28+
return dataStore.data.map { preferences ->
29+
ApiConfig(
30+
enabled = preferences[PreferencesKeys.API_ENABLED] ?: false,
31+
apiUrl = preferences[PreferencesKeys.API_URL] ?: "",
32+
authToken = preferences[PreferencesKeys.API_AUTH_TOKEN] ?: "",
33+
customSenderName = preferences[PreferencesKeys.API_CUSTOM_SENDER_NAME] ?: ""
34+
)
35+
}
36+
}
37+
38+
override suspend fun updateApiConfig(apiConfig: ApiConfig) {
39+
dataStore.edit { preferences ->
40+
preferences[PreferencesKeys.API_ENABLED] = apiConfig.enabled
41+
preferences[PreferencesKeys.API_URL] = apiConfig.apiUrl
42+
preferences[PreferencesKeys.API_AUTH_TOKEN] = apiConfig.authToken
43+
preferences[PreferencesKeys.API_CUSTOM_SENDER_NAME] = apiConfig.customSenderName
44+
}
45+
}
46+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.emrepbu.smsgateway.data.repository
2+
3+
import com.emrepbu.smsgateway.data.remote.api.model.ApiResponse
4+
import com.emrepbu.smsgateway.data.remote.api.model.ApiResult
5+
import com.emrepbu.smsgateway.data.remote.api.service.ApiService
6+
import com.emrepbu.smsgateway.domain.repository.ApiConfigRepository
7+
import kotlinx.coroutines.flow.first
8+
import javax.inject.Inject
9+
import javax.inject.Singleton
10+
11+
@Singleton
12+
class ApiRepository @Inject constructor(
13+
private val apiService: ApiService,
14+
private val apiConfigRepository: ApiConfigRepository
15+
) {
16+
suspend fun sendSms(phoneNumber: String, message: String, senderName: String? = null): ApiResult<ApiResponse> {
17+
val apiConfig = apiConfigRepository.getApiConfig().first()
18+
19+
if (!apiConfig.enabled || apiConfig.apiUrl.isBlank()) {
20+
return ApiResult.Error("API integration is not properly configured")
21+
}
22+
23+
return apiService.sendSms(
24+
apiUrl = apiConfig.apiUrl,
25+
phoneNumber = phoneNumber,
26+
message = message,
27+
senderName = senderName ?: apiConfig.customSenderName.takeIf { it.isNotBlank() },
28+
authToken = apiConfig.authToken.takeIf { it.isNotBlank() }
29+
)
30+
}
31+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.emrepbu.smsgateway.di
2+
3+
import com.emrepbu.smsgateway.data.remote.api.service.ApiService
4+
import dagger.Module
5+
import dagger.Provides
6+
import dagger.hilt.InstallIn
7+
import dagger.hilt.components.SingletonComponent
8+
import javax.inject.Singleton
9+
10+
@Module
11+
@InstallIn(SingletonComponent::class)
12+
object ApiModule {
13+
14+
@Provides
15+
@Singleton
16+
fun provideApiService(): ApiService {
17+
return ApiService()
18+
}
19+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.emrepbu.smsgateway.di
2+
3+
import android.content.Context
4+
import androidx.datastore.core.DataStore
5+
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
6+
import androidx.datastore.preferences.core.Preferences
7+
import androidx.datastore.preferences.preferencesDataStoreFile
8+
import dagger.Module
9+
import dagger.Provides
10+
import dagger.hilt.InstallIn
11+
import dagger.hilt.android.qualifiers.ApplicationContext
12+
import dagger.hilt.components.SingletonComponent
13+
import javax.inject.Singleton
14+
15+
@Module
16+
@InstallIn(SingletonComponent::class)
17+
object DataStoreModule {
18+
19+
private const val API_PREFERENCES = "api_preferences"
20+
21+
@Singleton
22+
@Provides
23+
fun providePreferencesDataStore(@ApplicationContext appContext: Context): DataStore<Preferences> {
24+
return PreferenceDataStoreFactory.create(
25+
produceFile = { appContext.preferencesDataStoreFile(API_PREFERENCES) }
26+
)
27+
}
28+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.emrepbu.smsgateway.di
2+
3+
import com.emrepbu.smsgateway.data.repository.ApiConfigRepositoryImpl
4+
import com.emrepbu.smsgateway.domain.repository.ApiConfigRepository
5+
import dagger.Binds
6+
import dagger.Module
7+
import dagger.hilt.InstallIn
8+
import dagger.hilt.components.SingletonComponent
9+
import javax.inject.Singleton
10+
11+
@Module
12+
@InstallIn(SingletonComponent::class)
13+
abstract class RepositoryModule {
14+
15+
@Binds
16+
@Singleton
17+
abstract fun bindConfigRepository(
18+
configRepositoryImpl: ApiConfigRepositoryImpl
19+
): ApiConfigRepository
20+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.emrepbu.smsgateway.domain.model
2+
3+
/**
4+
* Represents API configuration settings for SMS forwarding.
5+
*
6+
* @property enabled Indicates whether the API integration is enabled.
7+
* @property apiUrl The URL of the API to which SMS messages should be sent.
8+
* @property authToken Optional authentication token for API authorization.
9+
* @property customSenderName Optional custom sender name to use in API requests.
10+
*/
11+
data class ApiConfig(
12+
val enabled: Boolean = false,
13+
val apiUrl: String = "",
14+
val authToken: String = "",
15+
val customSenderName: String = ""
16+
)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.emrepbu.smsgateway.domain.repository
2+
3+
import com.emrepbu.smsgateway.domain.model.ApiConfig
4+
import kotlinx.coroutines.flow.Flow
5+
6+
/**
7+
* Interface for accessing and modifying configuration settings.
8+
*/
9+
interface ApiConfigRepository {
10+
/**
11+
* Gets the API configuration settings.
12+
*
13+
* @return A Flow emitting the current API configuration.
14+
*/
15+
fun getApiConfig(): Flow<ApiConfig>
16+
17+
/**
18+
* Updates the API configuration settings.
19+
*
20+
* @param apiConfig The new API configuration settings.
21+
*/
22+
suspend fun updateApiConfig(apiConfig: ApiConfig)
23+
}

0 commit comments

Comments
 (0)