feat: Handle public instances (#126)
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing

Co-authored-by: davidoskky <davidoskky@hidden.hidden>
Co-committed-by: davidoskky <davidoskky@hidden.hidden>
This commit is contained in:
davidoskky 2023-01-28 10:25:28 +00:00 committed by Amine Louveau
parent fc31a4399c
commit f28e702549
13 changed files with 206 additions and 71 deletions

View File

@ -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

View File

@ -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)

View File

@ -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,12 +137,17 @@ class ReaderActivity : AppCompatActivity(), DIAware {
inflater.inflate(R.menu.reader_menu, menu)
toolbarMenu = menu
alignmentMenu()
if (appSettingsService.getPublicAccess()) {
menu.removeItem(R.id.star)
} else {
if (allItems.isNotEmpty() && allItems[currentItem].starred) {
canRemoveFromFavorite()
} else {
canFavorite()
}
alignmentMenu()
binding.pager.registerOnPageChangeCallback(
object : ViewPager2.OnPageChangeCallback() {
@ -159,6 +164,7 @@ class ReaderActivity : AppCompatActivity(), DIAware {
}
}
)
}
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 -> {

View File

@ -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())

View File

@ -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))

View File

@ -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" />
<RelativeLayout
android:layout_width="0dp"
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/sourceTitleAndDate">
<ImageButton
android:id="@+id/favButton"
android:id="@+id/browserBtn"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:adjustViewBounds="true"
android:background="@android:color/transparent"
android:elevation="5dp"
android:padding="4dp"
android:scaleType="centerCrop"
app:srcCompat="@drawable/ic_menu_heart_60dp"
app:tint="@color/ic_menu_heart_color" />
app:srcCompat="@drawable/ic_open_in_browser_black_24dp"
app:tint="?android:attr/textColorPrimary" />
<ImageButton
android:id="@+id/shareBtn"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_centerVertical="true"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:layout_toLeftOf="@+id/favButton"
android:layout_toStartOf="@+id/favButton"
android:layout_marginStart="16dp"
android:adjustViewBounds="true"
android:background="@android:color/transparent"
android:elevation="5dp"
@ -129,23 +119,21 @@
app:tint="?android:attr/textColorPrimary" />
<ImageButton
android:id="@+id/browserBtn"
android:id="@+id/favButton"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_centerVertical="true"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:layout_toLeftOf="@+id/shareBtn"
android:layout_toStartOf="@+id/shareBtn"
android:layout_marginStart="16dp"
android:adjustViewBounds="true"
android:background="@android:color/transparent"
android:elevation="5dp"
android:padding="4dp"
android:scaleType="centerCrop"
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" />
</RelativeLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -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">
<com.github.rubensousa.floatingtoolbar.FloatingToolbar
android:id="@+id/floatingToolbar"
@ -96,10 +96,9 @@
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom|right"
android:layout_gravity="end|bottom|end"
android:layout_marginBottom="16dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:src="@drawable/ic_add_white_24dp"

View File

@ -0,0 +1,16 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/open_action"
android:icon="@drawable/ic_open_in_browser_white_24dp"
android:title="@string/reader_action_open"
app:showAsAction="ifRoom" />
<item
android:id="@+id/share_action"
android:icon="@drawable/ic_share_white_24dp"
android:title="@string/reader_action_share"
app:showAsAction="ifRoom" />
</menu>

View File

@ -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

View File

@ -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

View File

@ -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)
}
}
}
}

View File

@ -191,7 +191,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
}
})
suspend fun version(): StatusAndData<SelfossModel.ApiVersion> =
suspend fun apiInformation(): StatusAndData<SelfossModel.ApiInformation> =
bodyOrFailure(client.tryToGet(url("/api/about")))
suspend fun markAsRead(id: String): SuccessResponse =

View File

@ -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"