feat: Add api interaction logic
This commit is contained in:
parent
8d5242d780
commit
ec81d4ee12
|
@ -47,4 +47,7 @@ dependencies {
|
|||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
|
||||
implementation("androidx.core:core-splashscreen:1.0.1")
|
||||
implementation("com.squareup.retrofit2:retrofit:2.11.0")
|
||||
implementation("com.squareup.retrofit2:converter-gson:2.11.0")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.1")
|
||||
}
|
|
@ -2,6 +2,8 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
|
|
|
@ -6,25 +6,36 @@ import androidx.appcompat.app.AppCompatActivity
|
|||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import com.p_vacho.neat_calendar.api.RetrofitClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
private var apiReachable = false // Variable to track API reachability
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
// Attach the splash screen to the activity before initialization begins
|
||||
// (before super.onCreate) to make it show up during the activity init.
|
||||
val splashScreen = installSplashScreen()
|
||||
|
||||
// Keep the splash screen visible until initialization is complete
|
||||
splashScreen.setKeepOnScreenCondition {
|
||||
// TODO: Once implemented, make sure keep the splashscreen on until
|
||||
// all of the necessary data are obtained from the API, for now,
|
||||
// no initialization delay is needed.
|
||||
false
|
||||
}
|
||||
splashScreen.setKeepOnScreenCondition { !apiReachable }
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
// Check API availability in a coroutine
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
apiReachable = RetrofitClient.isApiReachable()
|
||||
if (!apiReachable) {
|
||||
// Handle unreachable API, e.g., show an error message or retry
|
||||
showApiErrorDialog()
|
||||
}
|
||||
}
|
||||
|
||||
// Handle window insets for proper layout adjustment
|
||||
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
|
||||
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
|
@ -32,4 +43,46 @@ class MainActivity : AppCompatActivity() {
|
|||
insets
|
||||
}
|
||||
}
|
||||
|
||||
// Show an error dialog if the API is unreachable
|
||||
private fun showApiErrorDialog() {
|
||||
var retryCount = 0;
|
||||
|
||||
val dialog = androidx.appcompat.app.AlertDialog.Builder(this)
|
||||
.setTitle("Error")
|
||||
.setMessage(
|
||||
"The API is unreachable.\n" +
|
||||
"Please check your network connection."
|
||||
)
|
||||
.setPositiveButton("Retry", null) // Listener set later for custom handling
|
||||
.setNegativeButton("Close") { _, _ ->
|
||||
// Close the app or navigate to an error screen
|
||||
finish()
|
||||
}
|
||||
.setCancelable(false)
|
||||
.create()
|
||||
|
||||
// Show dialog and handle "Retry" button dynamically
|
||||
dialog.setOnShowListener {
|
||||
val retryButton = dialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_POSITIVE)
|
||||
retryButton.setOnClickListener {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
retryCount++
|
||||
val reachable = RetrofitClient.isApiReachable()
|
||||
if (reachable) {
|
||||
apiReachable = true
|
||||
dialog.dismiss() // Close dialog and proceed
|
||||
} else {
|
||||
// Update dialog message for failed retry
|
||||
dialog.setMessage(
|
||||
"Retry failed. Attempts: $retryCount\n" +
|
||||
"Please check you network and try again."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package com.p_vacho.neat_calendar.api
|
||||
|
||||
import com.p_vacho.neat_calendar.api.services.AuthService
|
||||
import com.p_vacho.neat_calendar.api.services.GeneralService
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import android.util.Log
|
||||
import okhttp3.ResponseBody
|
||||
|
||||
object RetrofitClient {
|
||||
private const val BASE_URL = "https://90f2-213-160-184-230.ngrok-free.app"
|
||||
|
||||
private val retrofit: Retrofit by lazy {
|
||||
Retrofit.Builder()
|
||||
.baseUrl(BASE_URL)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build()
|
||||
}
|
||||
|
||||
val authService: AuthService by lazy {
|
||||
retrofit.create(AuthService::class.java)
|
||||
}
|
||||
|
||||
val generalService: GeneralService by lazy {
|
||||
retrofit.create(GeneralService::class.java)
|
||||
}
|
||||
|
||||
suspend fun isApiReachable(): Boolean {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val responseBody: ResponseBody = RetrofitClient.generalService.ping()
|
||||
val responseText = responseBody.string() // Extract the string content from ResponseBody
|
||||
if (responseText != "pong") {
|
||||
Log.w("API", "Unexpected response from ping: $responseText")
|
||||
}
|
||||
responseText == "pong"
|
||||
} catch (e: Exception) {
|
||||
Log.e("API", "Error while checking API reachability", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package com.p_vacho.neat_calendar.api.models
|
||||
|
||||
data class LoginRequest(
|
||||
val username: String,
|
||||
val password: String,
|
||||
)
|
||||
|
||||
data class LoginResponse(
|
||||
val access_token: String,
|
||||
val expires_in: Int,
|
||||
val refresh_token_expires_in: Int,
|
||||
val token_type: String,
|
||||
val user_id: String,
|
||||
val refresh_token: String,
|
||||
)
|
||||
|
||||
data class RefreshResponse(
|
||||
val access_token: String,
|
||||
val expires_in: Int,
|
||||
val token_type: String,
|
||||
val user_id: String,
|
||||
)
|
||||
|
||||
data class SessionResponse(
|
||||
val id: String,
|
||||
val session_type: String, // "refresh" or "access"
|
||||
val parent_session_id: String?,
|
||||
val expires_at: String, // ISO 8601
|
||||
val created_at: String, // ISO 8601
|
||||
val revoked: Boolean,
|
||||
)
|
|
@ -0,0 +1,22 @@
|
|||
package com.p_vacho.neat_calendar.api.services
|
||||
|
||||
import com.p_vacho.neat_calendar.api.models.*
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.POST
|
||||
|
||||
interface AuthService {
|
||||
@POST("auth/login")
|
||||
suspend fun login(@Body loginRequest: LoginRequest): LoginResponse
|
||||
|
||||
@POST("auth/refresh")
|
||||
suspend fun refreshToken(@Header("Authorization") refreshToken: String): RefreshResponse
|
||||
|
||||
@POST("auth/logout")
|
||||
suspend fun logout(@Header("Authorization") refreshToken: String): Response<Unit>
|
||||
|
||||
@GET("session")
|
||||
suspend fun getSession(@Header("Authorization") token: String): SessionResponse
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package com.p_vacho.neat_calendar.api.services
|
||||
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.http.GET
|
||||
|
||||
interface GeneralService {
|
||||
@GET("ping")
|
||||
suspend fun ping(): ResponseBody // pong
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
package com.p_vacho.neat_calendar.auth
|
||||
|
||||
import com.p_vacho.neat_calendar.api.RetrofitClient
|
||||
import com.p_vacho.neat_calendar.api.models.LoginRequest
|
||||
import com.p_vacho.neat_calendar.api.models.SessionResponse
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class AuthRepository(private val tokenManager: TokenManager) {
|
||||
|
||||
// Login user and store tokens
|
||||
suspend fun login(username: String, password: String): Boolean {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val response = RetrofitClient.authService.login(LoginRequest(username, password))
|
||||
tokenManager.storeAccessToken(response.access_token, response.expires_in)
|
||||
tokenManager.storeRefreshToken(response.refresh_token, response.refresh_token_expires_in)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate token by fetching session info
|
||||
suspend fun validateToken(): SessionResponse? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val token = tokenManager.accessToken ?: return@withContext null
|
||||
try {
|
||||
RetrofitClient.authService.getSession("Bearer $token")
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh access token if valid refresh token exists
|
||||
suspend fun refreshAccessToken(): Boolean {
|
||||
return withContext(Dispatchers.IO) {
|
||||
if (tokenManager.isRefreshTokenExpired()) {
|
||||
// Refresh token is expired; log out user
|
||||
logout()
|
||||
return@withContext false
|
||||
}
|
||||
|
||||
try {
|
||||
val refreshToken = tokenManager.refreshToken ?: return@withContext false
|
||||
val response = RetrofitClient.authService.refreshToken("Bearer $refreshToken")
|
||||
tokenManager.storeAccessToken(response.access_token, response.expires_in)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Logout user and clear tokens
|
||||
suspend fun logout(): Boolean {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val refreshToken = tokenManager.refreshToken ?: return@withContext false
|
||||
val response = RetrofitClient.authService.logout("Bearer $refreshToken")
|
||||
if (response.isSuccessful) {
|
||||
tokenManager.clearTokens()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package com.p_vacho.neat_calendar.auth
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
|
||||
class TokenManager(context: Context) {
|
||||
private val prefs: SharedPreferences =
|
||||
context.getSharedPreferences("auth_prefs", Context.MODE_PRIVATE)
|
||||
|
||||
var accessToken: String?
|
||||
get() = prefs.getString("access_token", null)
|
||||
private set(value) = prefs.edit().putString("access_token", value).apply()
|
||||
|
||||
var refreshToken: String?
|
||||
get() = prefs.getString("refresh_token", null)
|
||||
private set(value) = prefs.edit().putString("refresh_token", value).apply()
|
||||
|
||||
var accessTokenExpiresAt: Long
|
||||
get() = prefs.getLong("access_token_expires_at", 0L)
|
||||
private set(value) = prefs.edit().putLong("access_token_expires_at", value).apply()
|
||||
|
||||
var refreshTokenExpiresAt: Long
|
||||
get() = prefs.getLong("refresh_token_expires_at", 0L)
|
||||
private set(value) = prefs.edit().putLong("refresh_token_expires_at", value).apply()
|
||||
|
||||
fun isAccessTokenExpired(): Boolean {
|
||||
return accessTokenExpiresAt * 1000 <= System.currentTimeMillis()
|
||||
}
|
||||
|
||||
fun isRefreshTokenExpired(): Boolean {
|
||||
return refreshTokenExpiresAt * 1000 <= System.currentTimeMillis()
|
||||
}
|
||||
|
||||
fun storeAccessToken(accessToken: String, expiresIn: Int) {
|
||||
this.accessToken = accessToken
|
||||
this.accessTokenExpiresAt = System.currentTimeMillis() / 1000 + expiresIn
|
||||
}
|
||||
|
||||
fun storeRefreshToken(refreshToken: String, expiresIn: Int) {
|
||||
this.refreshToken = refreshToken
|
||||
this.refreshTokenExpiresAt = System.currentTimeMillis() / 1000 + expiresIn
|
||||
}
|
||||
|
||||
fun clearTokens() {
|
||||
accessToken = null
|
||||
refreshToken = null
|
||||
accessTokenExpiresAt = 0L
|
||||
refreshTokenExpiresAt = 0L
|
||||
}
|
||||
}
|
4
app/src/main/res/xml/network_security_config.xml
Normal file
4
app/src/main/res/xml/network_security_config.xml
Normal file
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</PreferenceScreen>
|
Loading…
Reference in a new issue