diff --git a/app/src/main/java/gq/kirmanak/mealie/MainActivity.kt b/app/src/main/java/gq/kirmanak/mealie/MainActivity.kt index b3fcedb..d3d09e6 100644 --- a/app/src/main/java/gq/kirmanak/mealie/MainActivity.kt +++ b/app/src/main/java/gq/kirmanak/mealie/MainActivity.kt @@ -2,6 +2,7 @@ package gq.kirmanak.mealie import android.os.Bundle import android.view.Menu +import android.view.MenuItem import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope @@ -48,4 +49,15 @@ class MainActivity : AppCompatActivity() { menu.findItem(R.id.logout).isVisible = isAuthenticated return true } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + Timber.v("onOptionsItemSelected() called with: item = $item") + val result = if (item.itemId == R.id.logout) { + authViewModel.logout() + true + } else { + super.onOptionsItemSelected(item) + } + return result + } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealie/data/auth/AuthRepo.kt b/app/src/main/java/gq/kirmanak/mealie/data/auth/AuthRepo.kt index e1d547a..0889203 100644 --- a/app/src/main/java/gq/kirmanak/mealie/data/auth/AuthRepo.kt +++ b/app/src/main/java/gq/kirmanak/mealie/data/auth/AuthRepo.kt @@ -10,4 +10,6 @@ interface AuthRepo { suspend fun getToken(): String? fun authenticationStatuses(): Flow + + fun logout() } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealie/data/auth/AuthStorage.kt b/app/src/main/java/gq/kirmanak/mealie/data/auth/AuthStorage.kt index 79e1cef..1ae6dc0 100644 --- a/app/src/main/java/gq/kirmanak/mealie/data/auth/AuthStorage.kt +++ b/app/src/main/java/gq/kirmanak/mealie/data/auth/AuthStorage.kt @@ -3,11 +3,13 @@ package gq.kirmanak.mealie.data.auth import kotlinx.coroutines.flow.Flow interface AuthStorage { - suspend fun storeAuthData(token: String, baseUrl: String) + fun storeAuthData(token: String, baseUrl: String) suspend fun getBaseUrl(): String? suspend fun getToken(): String? fun tokenObservable(): Flow + + fun clearAuthData() } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealie/data/auth/impl/AuthRepoImpl.kt b/app/src/main/java/gq/kirmanak/mealie/data/auth/impl/AuthRepoImpl.kt index 0b8c96d..d90c331 100644 --- a/app/src/main/java/gq/kirmanak/mealie/data/auth/impl/AuthRepoImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealie/data/auth/impl/AuthRepoImpl.kt @@ -38,4 +38,9 @@ class AuthRepoImpl @Inject constructor( Timber.v("authenticationStatuses() called") return storage.tokenObservable().map { it != null } } + + override fun logout() { + Timber.v("logout() called") + storage.clearAuthData() + } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealie/data/auth/impl/AuthStorageImpl.kt b/app/src/main/java/gq/kirmanak/mealie/data/auth/impl/AuthStorageImpl.kt index 44c2c74..4b772c6 100644 --- a/app/src/main/java/gq/kirmanak/mealie/data/auth/impl/AuthStorageImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealie/data/auth/impl/AuthStorageImpl.kt @@ -25,7 +25,7 @@ class AuthStorageImpl @Inject constructor(@ApplicationContext private val contex private val sharedPreferences: SharedPreferences get() = PreferenceManager.getDefaultSharedPreferences(context) - override suspend fun storeAuthData(token: String, baseUrl: String) { + override fun storeAuthData(token: String, baseUrl: String) { Timber.v("storeAuthData() called with: token = $token, baseUrl = $baseUrl") sharedPreferences.edit() .putString(TOKEN_KEY, token) @@ -47,6 +47,10 @@ class AuthStorageImpl @Inject constructor(@ApplicationContext private val contex return token } + private suspend fun getString(key: String): String? = withContext(Dispatchers.Default) { + sharedPreferences.getString(key, null) + } + override fun tokenObservable(): Flow { Timber.v("tokenObservable() called") return callbackFlow { @@ -70,7 +74,11 @@ class AuthStorageImpl @Inject constructor(@ApplicationContext private val contex } } - private suspend fun getString(key: String): String? = withContext(Dispatchers.Default) { - sharedPreferences.getString(key, null) + override fun clearAuthData() { + Timber.v("clearAuthData() called") + sharedPreferences.edit() + .remove(TOKEN_KEY) + .remove(BASE_URL_KEY) + .apply() } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealie/ui/auth/AuthenticationViewModel.kt b/app/src/main/java/gq/kirmanak/mealie/ui/auth/AuthenticationViewModel.kt index ed598ba..47c1d80 100644 --- a/app/src/main/java/gq/kirmanak/mealie/ui/auth/AuthenticationViewModel.kt +++ b/app/src/main/java/gq/kirmanak/mealie/ui/auth/AuthenticationViewModel.kt @@ -24,4 +24,9 @@ class AuthenticationViewModel @Inject constructor( Timber.v("authenticationStatuses() called") return authRepo.authenticationStatuses() } + + fun logout() { + Timber.v("logout() called") + authRepo.logout() + } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealie/ui/recipes/RecipesFragment.kt b/app/src/main/java/gq/kirmanak/mealie/ui/recipes/RecipesFragment.kt index f213f7f..39780da 100644 --- a/app/src/main/java/gq/kirmanak/mealie/ui/recipes/RecipesFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealie/ui/recipes/RecipesFragment.kt @@ -7,9 +7,11 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager import dagger.hilt.android.AndroidEntryPoint import gq.kirmanak.mealie.databinding.FragmentRecipesBinding +import gq.kirmanak.mealie.ui.auth.AuthenticationViewModel import kotlinx.coroutines.flow.collectLatest import timber.log.Timber @@ -19,6 +21,7 @@ class RecipesFragment : Fragment() { private val binding: FragmentRecipesBinding get() = checkNotNull(_binding) { "Binding requested when fragment is off screen" } private val viewModel by viewModels() + private val authViewModel by viewModels() override fun onCreateView( inflater: LayoutInflater, @@ -33,13 +36,33 @@ class RecipesFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) Timber.v("onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState") + setupRecipeAdapter() + listenToAuthStatuses() + } + + private fun listenToAuthStatuses() { + Timber.v("listenToAuthStatuses() called") + lifecycleScope.launchWhenCreated { + authViewModel.authenticationStatuses().collectLatest { + Timber.v("listenToAuthStatuses: new auth status = $it") + if (!it) navigateToAuthFragment() + } + } + } + + private fun navigateToAuthFragment() { + Timber.v("navigateToAuthFragment() called") + findNavController().navigate(RecipesFragmentDirections.actionRecipesFragmentToAuthenticationFragment()) + } + + private fun setupRecipeAdapter() { + Timber.v("setupRecipeAdapter() called") binding.recipes.layoutManager = LinearLayoutManager(requireContext()) val recipesPagingAdapter = RecipesPagingAdapter(viewModel) binding.recipes.adapter = recipesPagingAdapter lifecycleScope.launchWhenResumed { - Timber.d("onViewCreated: coroutine started") viewModel.recipeFlow.collectLatest { - Timber.d("onViewCreated: received update") + Timber.d("setupRecipeAdapter: received update") recipesPagingAdapter.submitData(it) } } diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 412f615..8fd46a4 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -19,5 +19,11 @@ android:id="@+id/recipesFragment" android:name="gq.kirmanak.mealie.ui.recipes.RecipesFragment" android:label="fragment_recipes" - tools:layout="@layout/fragment_recipes" /> + tools:layout="@layout/fragment_recipes"> + + \ No newline at end of file diff --git a/app/src/test/java/gq/kirmanak/mealie/data/auth/impl/AuthStorageImplTest.kt b/app/src/test/java/gq/kirmanak/mealie/data/auth/impl/AuthStorageImplTest.kt index ebd8150..151e451 100644 --- a/app/src/test/java/gq/kirmanak/mealie/data/auth/impl/AuthStorageImplTest.kt +++ b/app/src/test/java/gq/kirmanak/mealie/data/auth/impl/AuthStorageImplTest.kt @@ -53,4 +53,25 @@ class AuthStorageImplTest : HiltRobolectricTest() { subject.storeAuthData(TEST_TOKEN, TEST_URL) assertThat(subject.tokenObservable().first()).isEqualTo(TEST_TOKEN) } + + @Test + fun `when clearAuthData then first token is null`() = runBlocking { + subject.storeAuthData(TEST_TOKEN, TEST_URL) + subject.clearAuthData() + assertThat(subject.tokenObservable().first()).isNull() + } + + @Test + fun `when clearAuthData then getToken returns null`() = runBlocking { + subject.storeAuthData(TEST_TOKEN, TEST_URL) + subject.clearAuthData() + assertThat(subject.getToken()).isNull() + } + + @Test + fun `when clearAuthData then getBaseUrl returns null`() = runBlocking { + subject.storeAuthData(TEST_TOKEN, TEST_URL) + subject.clearAuthData() + assertThat(subject.getBaseUrl()).isNull() + } } \ No newline at end of file