From f28e7025496e845b77dd6f7374038e117bcd0927 Mon Sep 17 00:00:00 2001 From: davidoskky Date: Sat, 28 Jan 2023 10:25:28 +0000 Subject: [PATCH] feat: Handle public instances (#126) Co-authored-by: davidoskky Co-committed-by: davidoskky --- .../android/HomeActivity.kt | 12 ++- .../android/LoginActivity.kt | 2 +- .../android/ReaderActivity.kt | 48 ++++++------ .../android/adapters/ItemCardAdapter.kt | 4 + .../android/fragments/ArticleFragment.kt | 3 + androidApp/src/main/res/layout/card_item.xml | 40 ++++------ .../src/main/res/layout/fragment_article.xml | 5 +- .../main/res/menu/reader_toolbar_no_read.xml | 16 ++++ androidApp/src/test/kotlin/RepositoryTest.kt | 75 +++++++++++++++++-- .../readerforselfossv2/model/SelfossModel.kt | 21 +++++- .../repository/RepositoryImpl.kt | 18 ++++- .../readerforselfossv2/rest/SelfossApi.kt | 2 +- .../service/AppSettingsService.kt | 31 ++++++-- 13 files changed, 206 insertions(+), 71 deletions(-) create mode 100644 androidApp/src/main/res/menu/reader_toolbar_no_read.xml diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/HomeActivity.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/HomeActivity.kt index 6f4537b..ab4d547 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/HomeActivity.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/HomeActivity.kt @@ -115,10 +115,16 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar } } + val swipeDirs = if (appSettingsService.getPublicAccess()) { + 0 + } else { + ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT + } + val simpleItemTouchCallback = object : ItemTouchHelper.SimpleCallback( 0, - ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT + swipeDirs ) { override fun getSwipeDirs( recyclerView: RecyclerView, @@ -511,6 +517,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar override fun onCreateOptionsMenu(menu: Menu): Boolean { val inflater = menuInflater inflater.inflate(R.menu.home_menu, menu) + if (appSettingsService.getPublicAccess()) { + menu.removeItem(R.id.readAll) + menu.removeItem(R.id.action_sources) + } val searchItem = menu.findItem(R.id.action_search) val searchView = searchItem.getActionView() as SearchView diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/LoginActivity.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/LoginActivity.kt index a47a1af..2a48ea7 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/LoginActivity.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/LoginActivity.kt @@ -128,7 +128,7 @@ class LoginActivity : AppCompatActivity(), DIAware { private fun goToMain() { CoroutineScope(Dispatchers.Main).launch { - repository.updateApiVersion() + repository.updateApiInformation() ACRA.errorReporter.putCustomData("SELFOSS_API_VERSION", appSettingsService.getApiVersion().toString()) } val intent = Intent(this, HomeActivity::class.java) diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/ReaderActivity.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/ReaderActivity.kt index 70ec25d..8ca576d 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/ReaderActivity.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/ReaderActivity.kt @@ -84,7 +84,7 @@ class ReaderActivity : AppCompatActivity(), DIAware { } private fun readItem(item: SelfossModel.Item) { - if (appSettingsService.isMarkOnScrollEnabled()) { + if (appSettingsService.isMarkOnScrollEnabled() && !appSettingsService.getPublicAccess()) { CoroutineScope(Dispatchers.IO).launch { repository.markAsRead(item) } @@ -137,28 +137,34 @@ class ReaderActivity : AppCompatActivity(), DIAware { inflater.inflate(R.menu.reader_menu, menu) toolbarMenu = menu - if (allItems.isNotEmpty() && allItems[currentItem].starred) { - canRemoveFromFavorite() - } else { - canFavorite() - } alignmentMenu() - binding.pager.registerOnPageChangeCallback( - object : ViewPager2.OnPageChangeCallback() { - - override fun onPageSelected(position: Int) { - super.onPageSelected(position) - - if (allItems[position].starred) { - canRemoveFromFavorite() - } else { - canFavorite() - } - readItem(allItems[position]) - } + if (appSettingsService.getPublicAccess()) { + menu.removeItem(R.id.star) + } else { + if (allItems.isNotEmpty() && allItems[currentItem].starred) { + canRemoveFromFavorite() + } else { + canFavorite() } - ) + + + binding.pager.registerOnPageChangeCallback( + object : ViewPager2.OnPageChangeCallback() { + + override fun onPageSelected(position: Int) { + super.onPageSelected(position) + + if (allItems[position].starred) { + canRemoveFromFavorite() + } else { + canFavorite() + } + readItem(allItems[position]) + } + } + ) + } return true } @@ -177,7 +183,7 @@ class ReaderActivity : AppCompatActivity(), DIAware { when (item.itemId) { android.R.id.home -> { - onBackPressed() + onBackPressedDispatcher.onBackPressed() return true } R.id.star -> { diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/ItemCardAdapter.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/ItemCardAdapter.kt index d9b5f55..f21bd77 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/ItemCardAdapter.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/ItemCardAdapter.kt @@ -56,6 +56,10 @@ class ItemCardAdapter( val itm = items[position] binding.favButton.isSelected = itm.starred + if (appSettingsService.getPublicAccess()) { + binding.favButton.visibility = View.GONE + } + binding.title.text = itm.title.getHtmlDecoded() binding.title.setOnTouchListener(LinkOnTouchListener()) diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/fragments/ArticleFragment.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/fragments/ArticleFragment.kt index 3fb842c..4dd0221 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/fragments/ArticleFragment.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/fragments/ArticleFragment.kt @@ -195,6 +195,9 @@ class ArticleFragment : Fragment(), DIAware { private fun handleFloatingToolbar(): FloatingToolbar { val floatingToolbar: FloatingToolbar = binding.floatingToolbar + if (appSettingsService.getPublicAccess()) { + floatingToolbar.setMenu(R.menu.reader_toolbar_no_read) + } floatingToolbar.attachFab(fab) floatingToolbar.background = ColorDrawable(resources.getColor(R.color.colorAccent)) diff --git a/androidApp/src/main/res/layout/card_item.xml b/androidApp/src/main/res/layout/card_item.xml index 023be37..42e2b6e 100644 --- a/androidApp/src/main/res/layout/card_item.xml +++ b/androidApp/src/main/res/layout/card_item.xml @@ -47,7 +47,6 @@ android:id="@+id/sourceImage" android:layout_width="40dp" android:layout_height="40dp" - android:layout_marginLeft="8dp" android:layout_marginStart="8dp" android:layout_marginTop="8dp" app:layout_constraintLeft_toLeftOf="parent" @@ -85,41 +84,32 @@ app:layout_constraintTop_toBottomOf="@+id/title" tools:text="Google Actualité Il y a 5h" /> - + app:srcCompat="@drawable/ic_open_in_browser_black_24dp" + app:tint="?android:attr/textColorPrimary" /> + app:srcCompat="@drawable/ic_menu_heart_60dp" + app:tint="@color/ic_menu_heart_color" /> - + + + diff --git a/androidApp/src/main/res/layout/fragment_article.xml b/androidApp/src/main/res/layout/fragment_article.xml index 8e6db28..664015d 100644 --- a/androidApp/src/main/res/layout/fragment_article.xml +++ b/androidApp/src/main/res/layout/fragment_article.xml @@ -83,7 +83,7 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintLeft_toLeftOf="parent" - android:layout_gravity="end|bottom|right"> + android:layout_gravity="end|bottom|end"> + + + + + + \ No newline at end of file diff --git a/androidApp/src/test/kotlin/RepositoryTest.kt b/androidApp/src/test/kotlin/RepositoryTest.kt index 38245b2..ec307b9 100644 --- a/androidApp/src/test/kotlin/RepositoryTest.kt +++ b/androidApp/src/test/kotlin/RepositoryTest.kt @@ -20,6 +20,8 @@ import org.junit.Test private const val BASE_URL = "https://test.com/selfoss/" +private const val USERNAME = "username" + private const val SPOUT = "spouts\\rss\\fulltextrss" private const val IMAGE_URL = "b3aa8a664d08eb15d6ff1db2fa83e0d9.png" @@ -49,7 +51,7 @@ class RepositoryTest { repository = Repository(api, appSettingsService, isConnectionAvailable, db) runBlocking { - repository.updateApiVersion() + repository.updateApiInformation() } } @@ -58,12 +60,13 @@ class RepositoryTest { clearAllMocks() every { appSettingsService.getApiVersion() } returns 4 every { appSettingsService.getBaseUrl() } returns BASE_URL + every { appSettingsService.getUserName() } returns USERNAME every { appSettingsService.isItemCachingEnabled() } returns false every { appSettingsService.isUpdateSourcesEnabled() } returns false - coEvery { api.version() } returns StatusAndData( + coEvery { api.apiInformation() } returns StatusAndData( success = true, - data = SelfossModel.ApiVersion("2.19-ba1e8e3", "4.0.0") + data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(false, true)) ) coEvery { api.stats() } returns StatusAndData( success = true, @@ -81,7 +84,7 @@ class RepositoryTest { fun instantiate_repository() { initializeRepository() - coVerify(exactly = 1) { api.version() } + coVerify(exactly = 1) { api.apiInformation() } } @Test @@ -90,7 +93,7 @@ class RepositoryTest { initializeRepository(MutableStateFlow(false)) - coVerify(exactly = 0) { api.version() } + coVerify(exactly = 0) { api.apiInformation() } coVerify(exactly = 0) { api.stats() } } @@ -110,10 +113,70 @@ class RepositoryTest { verify(exactly = 1) { appSettingsService.updateApiVersion(4) } } + @Test + fun get_public_access() { + every { appSettingsService.updatePublicAccess(any()) } returns Unit + coEvery { api.apiInformation() } returns StatusAndData( + success = true, + data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(true, true)) + ) + every { appSettingsService.getUserName() } returns "" + + initializeRepository() + + coVerify(exactly = 1) { api.apiInformation() } + coVerify(exactly = 1) { appSettingsService.updatePublicAccess(true) } + } + + @Test + fun get_public_access_username_not_empty() { + every { appSettingsService.updatePublicAccess(any()) } returns Unit + coEvery { api.apiInformation() } returns StatusAndData( + success = true, + data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(true, true)) + ) + every { appSettingsService.getUserName() } returns "username" + + initializeRepository() + + coVerify(exactly = 1) { api.apiInformation() } + coVerify(exactly = 0) { appSettingsService.updatePublicAccess(true) } + } + + @Test + fun get_public_access_no_auth() { + every { appSettingsService.updatePublicAccess(any()) } returns Unit + coEvery { api.apiInformation() } returns StatusAndData( + success = true, + data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(true, false)) + ) + every { appSettingsService.getUserName() } returns "" + + initializeRepository() + + coVerify(exactly = 1) { api.apiInformation() } + coVerify(exactly = 0) { appSettingsService.updatePublicAccess(true) } + } + + @Test + fun get_public_access_disabled() { + every { appSettingsService.updatePublicAccess(any()) } returns Unit + coEvery { api.apiInformation() } returns StatusAndData( + success = true, + data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(false, true)) + ) + every { appSettingsService.getUserName() } returns "" + + initializeRepository() + + coVerify(exactly = 1) { api.apiInformation() } + coVerify(exactly = 0) { appSettingsService.updatePublicAccess(true) } + } + @Test fun get_api_1_date_with_api_4_version_stored() { every { appSettingsService.getApiVersion() } returns 4 - coEvery { api.version() } returns StatusAndData(success = false, null) + coEvery { api.apiInformation() } returns StatusAndData(success = false, null) val itemParameters = FakeItemParameters() itemParameters.datetime = "2021-04-23 11:45:32" coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns diff --git a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/model/SelfossModel.kt b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/model/SelfossModel.kt index e5eec2a..8fdf5e8 100644 --- a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/model/SelfossModel.kt +++ b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/model/SelfossModel.kt @@ -35,17 +35,32 @@ class SelfossModel { ) @Serializable - data class ApiVersion( + data class ApiInformation( val version: String?, - val apiversion: String? + val apiversion: String?, + val configuration: ApiConfiguration? ) { - fun getApiMajorVersion() : Int { + fun getApiMajorVersion(): Int { var versionNumber = 0 if (apiversion != null) { versionNumber = apiversion.substringBefore(".").toInt() } return versionNumber } + + fun getApiConfiguration() = configuration ?: ApiConfiguration(null, null) + } + + @Serializable + data class ApiConfiguration( + @Serializable(with = BooleanSerializer::class) + val publicMode: Boolean?, + @Serializable(with = BooleanSerializer::class) + val authEnabled: Boolean? + ) { + fun isAuthEnabled() = authEnabled ?: true + + fun isPublicModeEnabled() = publicMode ?: false } @Serializable diff --git a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/repository/RepositoryImpl.kt b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/repository/RepositoryImpl.kt index fb03172..1bcf35e 100644 --- a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/repository/RepositoryImpl.kt +++ b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/repository/RepositoryImpl.kt @@ -439,13 +439,23 @@ class Repository( api.refreshLoginInformation() } - suspend fun updateApiVersion() { + suspend fun updateApiInformation() { val apiMajorVersion = appSettingsService.getApiVersion() if (isNetworkAvailable()) { - val fetchedVersion = api.version() - if (fetchedVersion.success && fetchedVersion.data != null && fetchedVersion.data.getApiMajorVersion() != apiMajorVersion) { - appSettingsService.updateApiVersion(fetchedVersion.data.getApiMajorVersion()) + val fetchedInformation = api.apiInformation() + if (fetchedInformation.success && fetchedInformation.data != null) { + if (fetchedInformation.data.getApiMajorVersion() != apiMajorVersion) { + appSettingsService.updateApiVersion(fetchedInformation.data.getApiMajorVersion()) + } + // Check if we're accessing the instance in public mode + // This happens when auth and public mode are enabled but + // no credentials are provided to login + if (appSettingsService.getUserName().isEmpty() + && fetchedInformation.data.getApiConfiguration().isAuthEnabled() + && fetchedInformation.data.getApiConfiguration().isPublicModeEnabled()) { + appSettingsService.updatePublicAccess(true) + } } } } diff --git a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/rest/SelfossApi.kt b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/rest/SelfossApi.kt index c41e157..4d10b3b 100644 --- a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/rest/SelfossApi.kt +++ b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/rest/SelfossApi.kt @@ -191,7 +191,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { } }) - suspend fun version(): StatusAndData = + suspend fun apiInformation(): StatusAndData = bodyOrFailure(client.tryToGet(url("/api/about"))) suspend fun markAsRead(id: String): SuccessResponse = diff --git a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/service/AppSettingsService.kt b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/service/AppSettingsService.kt index d5b622d..35716a9 100644 --- a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/service/AppSettingsService.kt +++ b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/service/AppSettingsService.kt @@ -7,6 +7,7 @@ class AppSettingsService { // Api related private var _apiVersion: Int = -1 + private var _publicAccess: Boolean? = null private var _baseUrl: String = "" private var _userName: String = "" private var _password: String = "" @@ -48,10 +49,32 @@ class AppSettingsService { return _apiVersion } + + fun updateApiVersion(apiMajorVersion: Int) { + settings.putInt(API_VERSION_MAJOR, apiMajorVersion) + refreshApiVersion() + } + private fun refreshApiVersion() { _apiVersion = settings.getInt(API_VERSION_MAJOR, -1) } + fun getPublicAccess(): Boolean { + if (_publicAccess == null) { + refreshPublicAccess() + } + return _publicAccess!! + } + + fun updatePublicAccess(publicAccess: Boolean) { + settings.putBoolean(API_PUBLIC_ACCESS, publicAccess) + refreshPublicAccess() + } + + private fun refreshPublicAccess() { + _publicAccess = settings.getBoolean(API_PUBLIC_ACCESS, false) + } + fun getBaseUrl(): String { if (_baseUrl.isEmpty()) { refreshBaseUrl() @@ -333,6 +356,7 @@ class AppSettingsService { refreshUsername() refreshBaseUrl() refreshApiVersion() + refreshPublicAccess() } fun refreshUserSettings() { @@ -376,11 +400,6 @@ class AppSettingsService { refreshApiSettings() } - fun updateApiVersion(apiMajorVersion: Int) { - settings.putInt(API_VERSION_MAJOR, apiMajorVersion) - refreshApiVersion() - } - fun clearAll() { settings.clear() refreshApiSettings() @@ -409,6 +428,8 @@ class AppSettingsService { const val API_VERSION_MAJOR = "apiVersionMajor" + const val API_PUBLIC_ACCESS = "apiPublicAccess" + const val API_ITEMS_NUMBER = "prefer_api_items_number" const val API_TIMEOUT = "api_timeout"