diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 019d1264550b79c477d85b8b7dbca22dbd473d66..1663009b3d98c1f01d4a62e3b77b8ee847155aff 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -51,6 +51,7 @@ dependencies { implementation("com.squareup.retrofit2:retrofit:2.9.0") implementation("com.squareup.retrofit2:converter-gson:2.9.0") implementation("androidx.security:security-crypto:1.1.0-alpha06") + implementation(libs.androidx.work.runtime.ktx) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) diff --git a/app/src/main/java/com/example/myapplication/LoginActivity.kt b/app/src/main/java/com/example/myapplication/LoginActivity.kt index 551b78bed9725344a57cac64f6693be38a350f5d..01d2e0fdf3bcce5ae1a8a9da5a2db23c5acec311 100644 --- a/app/src/main/java/com/example/myapplication/LoginActivity.kt +++ b/app/src/main/java/com/example/myapplication/LoginActivity.kt @@ -9,10 +9,11 @@ import androidx.appcompat.app.AppCompatActivity import com.example.myapplication.databinding.ActivityLoginBinding import com.example.myapplication.repository.AuthRepository import com.example.myapplication.ui.login.UserViewModel +import com.example.myapplication.util.EventBus import com.example.myapplication.util.LoginListener import com.example.myapplication.util.SecretPreference -class LoginActivity : AppCompatActivity() , LoginListener { +class LoginActivity : AppCompatActivity() { private lateinit var binding: ActivityLoginBinding private var userViewModel : UserViewModel = UserViewModel() @@ -23,7 +24,7 @@ class LoginActivity : AppCompatActivity() , LoginListener { super.onCreate(savedInstanceState) secretPreference = SecretPreference(this) - authRepository = AuthRepository(this, secretPreference) + authRepository = AuthRepository(secretPreference) binding = ActivityLoginBinding.inflate(layoutInflater) setContentView(binding.root) @@ -38,9 +39,17 @@ class LoginActivity : AppCompatActivity() , LoginListener { authRepository.loginRequest(binding.emailInput.text.toString(), binding.passwordInput.text.toString()) Log.d("Development", "Activity: Login request sent") } + + EventBus.subscribe("LOGIN_SUCCESS") { + onLoginSuccess() + } + + EventBus.subscribe("LOGIN_FAIL") { + onLoginFailure() + } } - override fun onLoginSuccess() { + fun onLoginSuccess() { userViewModel.setUser(binding.emailInput.text.toString(), binding.passwordInput.text.toString()) Log.d("Development", "Activity: Login success") val preference : SharedPreferences = getSharedPreferences("secret_shared_prefs", MODE_PRIVATE) @@ -48,7 +57,7 @@ class LoginActivity : AppCompatActivity() , LoginListener { finish() } - override fun onLoginFailure() { + fun onLoginFailure() { Log.d("Development", "Activity: Login failed") } diff --git a/app/src/main/java/com/example/myapplication/MainActivity.kt b/app/src/main/java/com/example/myapplication/MainActivity.kt index 4d6ef5da340dda102514f278d9206df3c9dda050..614fbf0f0a8406d9d08888eb2ad78b606dde3c4a 100644 --- a/app/src/main/java/com/example/myapplication/MainActivity.kt +++ b/app/src/main/java/com/example/myapplication/MainActivity.kt @@ -4,7 +4,6 @@ import android.content.Context import android.content.Intent import android.net.ConnectivityManager import android.net.NetworkCapabilities -import android.net.NetworkInfo import android.os.Bundle import android.util.Log import com.google.android.material.bottomnavigation.BottomNavigationView @@ -13,11 +12,18 @@ import androidx.navigation.findNavController import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.setupActionBarWithNavController import androidx.navigation.ui.setupWithNavController +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager import com.example.myapplication.databinding.ActivityMainBinding +import com.example.myapplication.service.TokenExpiryWorker +import com.example.myapplication.util.EventBus +import com.example.myapplication.util.SecretPreference +import java.util.concurrent.TimeUnit -class MainActivity : AppCompatActivity() { +class MainActivity : AppCompatActivity(){ private lateinit var binding: ActivityMainBinding + private lateinit var secretPreference : SecretPreference override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -44,10 +50,25 @@ class MainActivity : AppCompatActivity() { val loginIntent = Intent(this, LoginActivity::class.java) startActivity(loginIntent) + + } override fun onStart() { super.onStart() + + // CHECK TOKEN for expiry + val backgroundWork = PeriodicWorkRequestBuilder<TokenExpiryWorker>(15, TimeUnit.MINUTES).build() + WorkManager.getInstance(this).enqueue(backgroundWork) + + // event listener for token expiry + EventBus.subscribe("TOKEN_EXPIRED") { + Log.i("Development", "Token expired") + secretPreference = SecretPreference(this) + secretPreference.clearToken() + val loginIntent = Intent(this, LoginActivity::class.java) + startActivity(loginIntent) + } } private fun isOnline(): Boolean { @@ -57,4 +78,5 @@ class MainActivity : AppCompatActivity() { return networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) } + } \ No newline at end of file diff --git a/app/src/main/java/com/example/myapplication/backendconnect/BackendService.kt b/app/src/main/java/com/example/myapplication/backendconnect/BackendService.kt index fa10fd0b88a21d86796bdb4df5dfde4c80c5ec6f..39b709bdf2309cb61d0f7637d374c4f12bf2aa17 100644 --- a/app/src/main/java/com/example/myapplication/backendconnect/BackendService.kt +++ b/app/src/main/java/com/example/myapplication/backendconnect/BackendService.kt @@ -4,6 +4,7 @@ import com.example.myapplication.model.auth.Token import com.example.myapplication.model.auth.UserCred import retrofit2.Call import retrofit2.http.Body +import retrofit2.http.Header import retrofit2.http.POST @@ -12,7 +13,7 @@ interface BackendService { fun login(@Body userCred: UserCred): Call<Token> @POST("auth/token") - fun tokenCheck(@Body token: Token): Call<Token> + fun tokenCheck(@Header("Authorization") token : String): Call<Token> // @POST("bill/upload") // fun uploadBill(@Body bill: Bill): Call<BillResponse> diff --git a/app/src/main/java/com/example/myapplication/repository/AuthRepository.kt b/app/src/main/java/com/example/myapplication/repository/AuthRepository.kt index 139f5ad9618c67254bbe83e49f8acac7f48657c7..05d37051d471ef2bec300eab66cdc3d2501be94d 100644 --- a/app/src/main/java/com/example/myapplication/repository/AuthRepository.kt +++ b/app/src/main/java/com/example/myapplication/repository/AuthRepository.kt @@ -1,10 +1,10 @@ package com.example.myapplication.repository -import android.content.Context import android.util.Log import com.example.myapplication.backendconnect.Client import com.example.myapplication.model.auth.Token import com.example.myapplication.model.auth.UserCred +import com.example.myapplication.util.EventBus import com.example.myapplication.util.LoginListener import com.example.myapplication.util.SecretPreference import retrofit2.Call @@ -12,7 +12,6 @@ import retrofit2.Callback import retrofit2.Response class AuthRepository ( - private val loginListener: LoginListener, private val secretPreference: SecretPreference) { fun loginRequest(email: String, password: String){ @@ -29,19 +28,51 @@ class AuthRepository ( secretPreference.saveToken(token?.token ?: "") // callback the loginActivity - loginListener.onLoginSuccess() + //loginListener.onLoginSuccess() + EventBus.publish("LOGIN_SUCCESS") } else { Log.d("Development", "LOGIN FAILED, http code : ${response.code()}") - loginListener.onLoginFailure() + //loginListener.onLoginFailure() + EventBus.publish("LOGIN_FAIL") } } override fun onFailure(call: Call<Token>, t: Throwable) { Log.d("Development", "LOGIN FAILED, error on delivery : ${t.message}") - loginListener.onLoginFailure() +// loginListener.onLoginFailure() + EventBus.publish("LOGIN_FAIL") } } ) } + + fun tokenCheckRequest() { + Log.d("Development", "Token check request to backend service") + val token = secretPreference.getToken() ?: "" + + Client.connect.tokenCheck("Bearer $token").enqueue( + object : Callback<Token> { + override fun onResponse(call: Call<Token>, response: Response<Token>) { + Log.d("Development", "Response: ${response.body()}") + if (response.isSuccessful) { + Log.d("Development", "Token check success: Valid token") + } + else if (response.code() == 401) { + Log.d("Development", "Token check failed: Invalid token or expired") + EventBus.publish("TOKEN_EXPIRED") + } + else { + Log.d("Development", "TOKEN CHECK FAILED, http code : ${response.code()}") + EventBus.publish("TOKEN_EXPIRED") + } + } + + override fun onFailure(call: Call<Token>, t: Throwable) { + Log.d("Development", "TOKEN CHECK FAILED, error on delivery : ${t.message}") + } + } + ) + } + } \ No newline at end of file diff --git a/app/src/main/java/com/example/myapplication/service/TokenExpiryWorker.kt b/app/src/main/java/com/example/myapplication/service/TokenExpiryWorker.kt new file mode 100644 index 0000000000000000000000000000000000000000..3b6548d6386626d9c9cafa7584f2577676b8ea2a --- /dev/null +++ b/app/src/main/java/com/example/myapplication/service/TokenExpiryWorker.kt @@ -0,0 +1,23 @@ +package com.example.myapplication.service + +import android.content.Context +import android.util.Log +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.example.myapplication.repository.AuthRepository +import com.example.myapplication.util.LoginListener +import com.example.myapplication.util.SecretPreference + +class TokenExpiryWorker (appContext: Context, workerParams: WorkerParameters) + : CoroutineWorker(appContext, workerParams) { + + private val secretPreference = SecretPreference(applicationContext) + private val authRepository = AuthRepository(secretPreference) + + override suspend fun doWork(): Result { + Log.i("Development", "Token expiry worker started") + authRepository.tokenCheckRequest() + return Result.success() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/myapplication/util/EventBus.kt b/app/src/main/java/com/example/myapplication/util/EventBus.kt new file mode 100644 index 0000000000000000000000000000000000000000..a3b8bb06f325991835b7b0fd00dfa89befe902b3 --- /dev/null +++ b/app/src/main/java/com/example/myapplication/util/EventBus.kt @@ -0,0 +1,28 @@ +package com.example.myapplication.util + +import android.util.Log + +object EventBus { + + private var listeners: MutableMap<String, MutableList<() -> Unit>> = mutableMapOf() + + fun subscribe(event: String, listener: () -> Unit) { + if (listeners.containsKey(event)) { + // add a new listener to an existing event + listeners[event]?.add(listener) + } else { + // add a new event with a new listener + listeners[event] = mutableListOf(listener) + } + } + + fun publish(event: String) { + if (listeners.containsKey(event)) { + // call all listeners for the event + listeners[event]?.forEach { it() } + } else { + // no listeners for the event + Log.d("Development","No listeners for event: $event") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/myapplication/util/SecretPreference.kt b/app/src/main/java/com/example/myapplication/util/SecretPreference.kt index e0fd205f8288888970b764b9cbc80732e2388b7d..8569b03b5d2391629dafa9ff5a274ca3784bc677 100644 --- a/app/src/main/java/com/example/myapplication/util/SecretPreference.kt +++ b/app/src/main/java/com/example/myapplication/util/SecretPreference.kt @@ -25,4 +25,9 @@ class SecretPreference (private val context: Context) { fun getToken(): String? = sharedPreferences.getString("token", null) + fun clearToken(){ + sharedPreferences.edit().remove("token").apply() + Log.i("Development", "Token cleared") + } + } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 24e84bf62a4162bf104791cc38c446f296b778d3..9b6360c0ac9326d81bbdaf1dd976fd3edf99f4ba 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,6 +12,7 @@ lifecycleLivedataKtx = "2.7.0" lifecycleViewmodelKtx = "2.7.0" navigationFragmentKtx = "2.6.0" navigationUiKtx = "2.6.0" +workRuntimeKtx = "2.9.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -25,6 +26,7 @@ androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecy androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" } androidx-navigation-fragment-ktx = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "navigationFragmentKtx" } androidx-navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navigationUiKtx" } +androidx-work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "workRuntimeKtx" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" }