Migration of Item management to SharedItems (#345)

* Refactor Item addition and deletion

* Metods to filter the items according to read and starred status

* Remove displayed items only if displaying unread items

* Remove unnecessary api calls on tab change and delegate item storage to SharedItems

* Store articles in SharedItems when they get fetched

* Add tag filtering

* Mark items as read

* Disable sorting function

* Add function to get the unread status of an element.

* Fetch items on pull gesture

* Move marking as read logic in SharedItems.

* Delegate item status to SharedItems

* Allow changing unread status of items

* Use full article position reference and not the relative one

* Delegate marking items as unread to SharedItems

* Delegate database addition of Items to SharedItems.

* Function to only provide connectivity information

* Better database management

* Sort items by date

* Provide information about item caching to SharedItems

* Add missing imports

* Update database after fetching articles

* Add missing variable

* Remove unused import

* Use coroutines to access database

* Use coroutines to simultaneously fetch articles.

* Update database after fetching articles.

* Don't block thread when accessing the database

* Prevent crash if connectivity is lost while fetching articles

* Show "Not connected" snackbar if there is no connection or connection is lost during download

* Use coroutines in the background sync

* Added function to get only new items

* Introduced function to filter articles

* Don't execute background sync if the option is disabled

* Improve item filtering

* Apply filters when they are selected on the UI

* Handle infinite scroll

* Incorrect parameters were passed

* Simplify tab selection logic

* Upgrade kotlin jvm to version 1.8

* On tab change fetch new items if the item list is not completely populated

* Remove redundant assignations.

* Fetch articles when changing tag, source or search if the list is not fully populated

* Fetch only the article in the tab selected

* Correct inconsistent position address

* Disable swiping articles only if favorites are selected

* Delegate badge count to SharedItems

* Clear the database when the app starts in order to avoid accumulation and inconsistencies

* Remove unused functions and variables

* Do not overwrite fetched items with old copies from the database

* Display "There's nothing here" only if there are no articles

* Adapt function to read all articles to the new changes

* Use IO Dispatcher for Database and Network computations

* Adapt Background sync to the usage of SharedItems

* Handle refresh gesture appropriately by refreshing the whole items list

* Remove unused imports
This commit is contained in:
davidoskky 2021-09-25 13:45:51 +02:00 committed by GitHub
parent 304b6c3761
commit 46e723a238
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 663 additions and 684 deletions

View File

@ -82,6 +82,9 @@ android {
dimension "build" dimension "build"
} }
} }
kotlinOptions {
jvmTarget = '1.8'
}
} }
dependencies { dependencies {
@ -113,10 +116,13 @@ dependencies {
transitive = true transitive = true
} }
// Async
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
// Retrofit + http logging + okhttp // Retrofit + http logging + okhttp
implementation 'com.squareup.retrofit2:retrofit:2.3.0' implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.okhttp3:logging-interceptor:3.9.0' implementation 'com.squareup.okhttp3:logging-interceptor:3.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.3.0' implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.burgstaller:okhttp-digest:1.12' implementation 'com.burgstaller:okhttp-digest:1.12'
// Material-ish things // Material-ish things
@ -148,6 +154,7 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-common-java8:2.3.0-rc01" implementation "androidx.lifecycle:lifecycle-common-java8:2.3.0-rc01"
implementation "androidx.room:room-runtime:2.3.0-alpha04" implementation "androidx.room:room-runtime:2.3.0-alpha04"
implementation "androidx.room:room-ktx:2.3.0-alpha04"
kapt "androidx.room:room-compiler:2.3.0-alpha04" kapt "androidx.room:room-compiler:2.3.0-alpha04"
implementation "android.arch.work:work-runtime-ktx:$work_version" implementation "android.arch.work:work-runtime-ktx:$work_version"

View File

@ -18,6 +18,7 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.view.MenuItemCompat import androidx.core.view.MenuItemCompat
import androidx.core.view.doOnNextLayout
import androidx.recyclerview.widget.* import androidx.recyclerview.widget.*
import androidx.room.Room import androidx.room.Room
import androidx.work.Constraints import androidx.work.Constraints
@ -43,7 +44,6 @@ import apps.amine.bou.readerforselfoss.utils.SharedItems
import apps.amine.bou.readerforselfoss.utils.bottombar.maybeShow import apps.amine.bou.readerforselfoss.utils.bottombar.maybeShow
import apps.amine.bou.readerforselfoss.utils.bottombar.removeBadge import apps.amine.bou.readerforselfoss.utils.bottombar.removeBadge
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
import apps.amine.bou.readerforselfoss.utils.flattenTags
import apps.amine.bou.readerforselfoss.utils.longHash import apps.amine.bou.readerforselfoss.utils.longHash
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
import apps.amine.bou.readerforselfoss.utils.persistence.toEntity import apps.amine.bou.readerforselfoss.utils.persistence.toEntity
@ -58,7 +58,6 @@ import com.ashokvarma.bottomnavigation.BottomNavigationItem
import com.ashokvarma.bottomnavigation.TextBadgeItem import com.ashokvarma.bottomnavigation.TextBadgeItem
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.ftinc.scoop.Scoop import com.ftinc.scoop.Scoop
import com.mikepenz.aboutlibraries.Libs
import com.mikepenz.aboutlibraries.LibsBuilder import com.mikepenz.aboutlibraries.LibsBuilder
import com.mikepenz.materialdrawer.Drawer import com.mikepenz.materialdrawer.Drawer
import com.mikepenz.materialdrawer.holder.BadgeStyle import com.mikepenz.materialdrawer.holder.BadgeStyle
@ -66,10 +65,12 @@ import com.mikepenz.materialdrawer.holder.StringHolder
import com.mikepenz.materialdrawer.model.DividerDrawerItem import com.mikepenz.materialdrawer.model.DividerDrawerItem
import com.mikepenz.materialdrawer.model.PrimaryDrawerItem import com.mikepenz.materialdrawer.model.PrimaryDrawerItem
import com.mikepenz.materialdrawer.model.SecondaryDrawerItem import com.mikepenz.materialdrawer.model.SecondaryDrawerItem
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import retrofit2.Call import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
import java.text.SimpleDateFormat
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.concurrent.thread import kotlin.concurrent.thread
@ -129,10 +130,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
private var recyclerAdapter: RecyclerView.Adapter<*>? = null private var recyclerAdapter: RecyclerView.Adapter<*>? = null
private var badgeNew: Int = -1
private var badgeAll: Int = -1
private var badgeFavs: Int = -1
private var fromTabShortcut: Boolean = false private var fromTabShortcut: Boolean = false
private var offlineShortcut: Boolean = false private var offlineShortcut: Boolean = false
@ -213,7 +210,11 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
allItems = ArrayList() allItems = ArrayList()
lastFetchDone = false lastFetchDone = false
handleDrawerItems() handleDrawerItems()
getElementsAccordingToTab() CoroutineScope(Dispatchers.Main).launch {
refreshFocusedItems(applicationContext, api, db)
getElementsAccordingToTab()
binding.swipeRefreshLayout.isRefreshing = false
}
} }
val simpleItemTouchCallback = val simpleItemTouchCallback =
@ -225,7 +226,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
recyclerView: RecyclerView, recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder viewHolder: RecyclerView.ViewHolder
): Int = ): Int =
if (elementsShown != UNREAD_SHOWN && elementsShown != READ_SHOWN) { if (elementsShown == FAV_SHOWN) {
0 0
} else { } else {
super.getSwipeDirs( super.getSwipeDirs(
@ -247,16 +248,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
if (i != null) { if (i != null) {
val adapter = binding.recyclerView.adapter as ItemsAdapter<*> val adapter = binding.recyclerView.adapter as ItemsAdapter<*>
val wasItemUnread = adapter.unreadItemStatusAtIndex(position)
adapter.handleItemAtIndex(position) adapter.handleItemAtIndex(position)
if (wasItemUnread) {
badgeNew--
} else {
badgeNew++
}
reloadBadgeContent() reloadBadgeContent()
val tagHashes = i.tags.tags.split(",").map { it.longHash() } val tagHashes = i.tags.tags.split(",").map { it.longHash() }
@ -392,56 +385,14 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
} }
private fun getAndStoreAllItems() { private fun getAndStoreAllItems() {
api.allNewItems().enqueue(object : Callback<List<Item>> { CoroutineScope(Dispatchers.Main).launch {
override fun onFailure(call: Call<List<Item>>, t: Throwable) { binding.swipeRefreshLayout.isRefreshing = true
} getAndStoreAllItems(applicationContext ,api, db)
this@HomeActivity.isNetworkAccessible(this@HomeActivity.findViewById(R.id.coordLayout), offlineShortcut)
override fun onResponse( handleListResult()
call: Call<List<Item>>, binding.swipeRefreshLayout.isRefreshing = false
response: Response<List<Item>> SharedItems.updateDatabase(db)
) { }
enqueueArticles(response, true)
}
})
api.allReadItems().enqueue(object : Callback<List<Item>> {
override fun onFailure(call: Call<List<Item>>, t: Throwable) {
}
override fun onResponse(
call: Call<List<Item>>,
response: Response<List<Item>>
) {
enqueueArticles(response, false)
}
})
api.allStarredItems().enqueue(object : Callback<List<Item>> {
override fun onFailure(call: Call<List<Item>>, t: Throwable) {
}
override fun onResponse(
call: Call<List<Item>>,
response: Response<List<Item>>
) {
enqueueArticles(response, false)
}
})
}
private fun enqueueArticles(response: Response<List<Item>>, clearDatabase: Boolean) {
thread {
if (response.body() != null) {
val apiItems = (response.body() as ArrayList<Item>).filter {
maybeTagFilter != null || filter(it.tags.tags)
} as ArrayList<Item>
if (clearDatabase) {
db.itemsDao().deleteAllItems()
}
db.itemsDao()
.insertAllItems(*(apiItems.map { it.toEntity() }).toTypedArray())
}
}
} }
override fun onStop() { override fun onStop() {
@ -461,6 +412,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
displayAccountHeader = sharedPref.getBoolean("account_header_displaying", false) displayAccountHeader = sharedPref.getBoolean("account_header_displaying", false)
infiniteScroll = sharedPref.getBoolean("infinite_loading", false) infiniteScroll = sharedPref.getBoolean("infinite_loading", false)
itemsCaching = sharedPref.getBoolean("items_caching", false) itemsCaching = sharedPref.getBoolean("items_caching", false)
SharedItems.itemsCaching = itemsCaching
updateSources = sharedPref.getBoolean("update_sources", true) updateSources = sharedPref.getBoolean("update_sources", true)
markOnScroll = sharedPref.getBoolean("mark_on_scroll", false) markOnScroll = sharedPref.getBoolean("mark_on_scroll", false)
hiddenTags = if (sharedPref.getString("hidden_tags", "")!!.isNotEmpty()) { hiddenTags = if (sharedPref.getString("hidden_tags", "")!!.isNotEmpty()) {
@ -597,7 +549,9 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
.withOnDrawerItemClickListener { _, _, _ -> .withOnDrawerItemClickListener { _, _, _ ->
allItems = ArrayList() allItems = ArrayList()
maybeTagFilter = it maybeTagFilter = it
SharedItems.tagFilter = it.tag
getElementsAccordingToTab() getElementsAccordingToTab()
fetchOnEmptyList()
false false
} }
if (it.unread > 0) { if (it.unread > 0) {
@ -648,7 +602,9 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
.withOnDrawerItemClickListener { _, _, _ -> .withOnDrawerItemClickListener { _, _, _ ->
allItems = ArrayList() allItems = ArrayList()
maybeTagFilter = it maybeTagFilter = it
SharedItems.tagFilter = it.tag
getElementsAccordingToTab() getElementsAccordingToTab()
fetchOnEmptyList()
false false
} }
if (it.unread > 0) { if (it.unread > 0) {
@ -680,7 +636,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
.withOnDrawerItemClickListener { _, _, _ -> .withOnDrawerItemClickListener { _, _, _ ->
allItems = ArrayList() allItems = ArrayList()
maybeSourceFilter = tag maybeSourceFilter = tag
SharedItems.sourceIDFilter = tag.id.toLong()
SharedItems.sourceFilter = tag.title
getElementsAccordingToTab() getElementsAccordingToTab()
fetchOnEmptyList()
false false
} }
if (tag.getIcon(this@HomeActivity).isNotBlank()) { if (tag.getIcon(this@HomeActivity).isNotBlank()) {
@ -710,8 +669,12 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
.withOnDrawerItemClickListener { _, _, _ -> .withOnDrawerItemClickListener { _, _, _ ->
allItems = ArrayList() allItems = ArrayList()
maybeSourceFilter = null maybeSourceFilter = null
SharedItems.sourceFilter = null
SharedItems.sourceIDFilter = null
maybeTagFilter = null maybeTagFilter = null
SharedItems.tagFilter = null
getElementsAccordingToTab() getElementsAccordingToTab()
fetchOnEmptyList()
false false
} }
) )
@ -900,7 +863,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
binding.recyclerView.layoutManager = layoutManager binding.recyclerView.layoutManager = layoutManager
} }
} else {
} }
} }
} }
@ -933,69 +895,28 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
offset = 0 offset = 0
lastFetchDone = false lastFetchDone = false
if (itemsCaching) { elementsShown = position + 1
getElementsAccordingToTab()
binding.recyclerView.scrollToPosition(0)
if (!binding.swipeRefreshLayout.isRefreshing) { fetchOnEmptyList()
binding.swipeRefreshLayout.post { binding.swipeRefreshLayout.isRefreshing = true }
}
thread {
val dbItems = db.itemsDao().items().map { it.toView() }.sortedByDescending {
SimpleDateFormat(Config.dateTimeFormatter).parse(it.datetime)
}
runOnUiThread {
if (dbItems.isNotEmpty()) {
items = when (position) {
0 -> ArrayList(dbItems.filter { it.unread })
1 -> ArrayList(dbItems.filter { !it.unread })
2 -> ArrayList(dbItems.filter { it.starred })
else -> ArrayList(dbItems.filter { it.unread })
}
handleListResult()
when (position) {
0 -> getUnRead()
1 -> getRead()
2 -> getStarred()
else -> Unit
}
} else {
if (this@HomeActivity.isNetworkAccessible(this@HomeActivity.findViewById(R.id.coordLayout), offlineShortcut)) {
when (position) {
0 -> getUnRead()
1 -> getRead()
2 -> getStarred()
else -> Unit
}
getAndStoreAllItems()
}
}
}
}
} else {
when (position) {
0 -> getUnRead()
1 -> getRead()
2 -> getStarred()
else -> Unit
}
}
} }
}) })
} }
private fun fetchOnEmptyList() {
binding.recyclerView.doOnNextLayout {
if (SharedItems.focusedItems.size - 1 == getLastVisibleItem()) {
getElementsAccordingToTab(true)
}
}
}
private fun handleInfiniteScroll() { private fun handleInfiniteScroll() {
recyclerViewScrollListener = object : RecyclerView.OnScrollListener() { recyclerViewScrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrolled(localRecycler: RecyclerView, dx: Int, dy: Int) { override fun onScrolled(localRecycler: RecyclerView, dx: Int, dy: Int) {
if (dy > 0) { if (dy > 0) {
val manager = binding.recyclerView.layoutManager val lastVisibleItem = getLastVisibleItem()
val lastVisibleItem: Int = when (manager) {
is StaggeredGridLayoutManager -> manager.findLastCompletelyVisibleItemPositions(
null
).last()
is GridLayoutManager -> manager.findLastCompletelyVisibleItemPosition()
else -> 0
}
if (lastVisibleItem == (items.size - 1) && items.size < maxItemNumber()) { if (lastVisibleItem == (items.size - 1) && items.size < maxItemNumber()) {
getElementsAccordingToTab(appendResults = true) getElementsAccordingToTab(appendResults = true)
@ -1008,13 +929,22 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
binding.recyclerView.addOnScrollListener(recyclerViewScrollListener) binding.recyclerView.addOnScrollListener(recyclerViewScrollListener)
} }
private fun getLastVisibleItem() : Int {
val manager = binding.recyclerView.layoutManager
return when (manager) {
is StaggeredGridLayoutManager -> manager.findLastCompletelyVisibleItemPositions(
null
).last()
is GridLayoutManager -> manager.findLastCompletelyVisibleItemPosition()
else -> 0
}
}
private fun mayBeEmpty() = private fun mayBeEmpty() =
if (items.isEmpty()) { if (items.isEmpty()) {
binding.emptyText.visibility = View.VISIBLE binding.emptyText.visibility = View.VISIBLE
binding.recyclerView.visibility = View.GONE
} else { } else {
binding.emptyText.visibility = View.GONE binding.emptyText.visibility = View.GONE
binding.recyclerView.visibility = View.VISIBLE
} }
private fun getElementsAccordingToTab( private fun getElementsAccordingToTab(
@ -1030,155 +960,52 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
} }
} }
offset = if (appendResults && offsetOverride === null) { offset = if (appendResults) {
(offset + itemsNumber) SharedItems.focusedItems.size - 1
} else { } else {
offsetOverride ?: 0 0
} }
firstVisible = if (appendResults) firstVisible else 0 firstVisible = if (appendResults) firstVisible else 0
if (itemsCaching) { doGetAccordingToTab()
if (!binding.swipeRefreshLayout.isRefreshing) {
binding.swipeRefreshLayout.post { binding.swipeRefreshLayout.isRefreshing = true }
}
thread {
val dbItems = db.itemsDao().items().map { it.toView() }.sortedByDescending {
SimpleDateFormat(Config.dateTimeFormatter).parse(it.datetime)
}
runOnUiThread {
if (dbItems.isNotEmpty()) {
items = when (elementsShown) {
UNREAD_SHOWN -> ArrayList(dbItems.filter { it.unread })
READ_SHOWN -> ArrayList(dbItems.filter { !it.unread })
FAV_SHOWN -> ArrayList(dbItems.filter { it.starred })
else -> ArrayList(dbItems.filter { it.unread })
}
handleListResult()
doGetAccordingToTab()
} else {
if (this@HomeActivity.isNetworkAccessible(this@HomeActivity.findViewById(R.id.coordLayout), offlineShortcut)) {
doGetAccordingToTab()
getAndStoreAllItems()
}
}
}
}
} else {
doGetAccordingToTab()
}
}
private fun filter(tags: String): Boolean {
val tagsList = tags.replace("\\s".toRegex(), "").split(",")
return tagsList.intersect(hiddenTags).isEmpty()
}
private fun doCallTo(
appendResults: Boolean,
toastMessage: Int,
call: (String?, Long?, String?) -> Call<List<Item>>
) {
fun handleItemsResponse(response: Response<List<Item>>) {
val shouldUpdate = (response.body()?.toSet() != items.toSet())
if (response.body() != null) {
if (shouldUpdate) {
getAndStoreAllItems()
items = response.body() as ArrayList<Item>
items = items.filter {
maybeTagFilter != null || filter(it.tags.tags)
} as ArrayList<Item>
if (allItems.isEmpty()) {
allItems = items
} else {
items.forEach {
if (!allItems.contains(it)) allItems.add(it)
}
}
SharedItems.focusedItems = items
SharedItems.items = allItems
}
} else {
if (!appendResults) {
items = ArrayList()
allItems = ArrayList()
}
}
handleListResult(appendResults)
if (!appendResults) mayBeEmpty()
binding.swipeRefreshLayout.isRefreshing = false
}
if (!binding.swipeRefreshLayout.isRefreshing) {
binding.swipeRefreshLayout.post { binding.swipeRefreshLayout.isRefreshing = true }
}
if (this@HomeActivity.isNetworkAccessible(this@HomeActivity.findViewById(R.id.coordLayout), offlineShortcut)) {
call(maybeTagFilter?.tag, maybeSourceFilter?.id?.toLong(), maybeSearchFilter)
.enqueue(object : Callback<List<Item>> {
override fun onResponse(
call: Call<List<Item>>,
response: Response<List<Item>>
) {
handleItemsResponse(response)
}
override fun onFailure(call: Call<List<Item>>, t: Throwable) {
binding.swipeRefreshLayout.isRefreshing = false
Toast.makeText(
this@HomeActivity,
toastMessage,
Toast.LENGTH_SHORT
).show()
}
})
} else {
binding.swipeRefreshLayout.post { binding.swipeRefreshLayout.isRefreshing = false }
}
} }
private fun getUnRead(appendResults: Boolean = false) { private fun getUnRead(appendResults: Boolean = false) {
elementsShown = UNREAD_SHOWN CoroutineScope(Dispatchers.Main).launch {
doCallTo(appendResults, R.string.cant_get_new_elements) { t, id, f -> if (appendResults || !SharedItems.fetchedUnread) {
api.newItems( binding.swipeRefreshLayout.isRefreshing = true
t, getUnreadItems(applicationContext, api, db, offset)
id, binding.swipeRefreshLayout.isRefreshing = false
f, }
itemsNumber, SharedItems.getUnRead()
offset items = SharedItems.focusedItems
) handleListResult()
} }
} }
private fun getRead(appendResults: Boolean = false) { private fun getRead(appendResults: Boolean = false) {
elementsShown = READ_SHOWN CoroutineScope(Dispatchers.Main).launch {
doCallTo(appendResults, R.string.cant_get_read) { t, id, f -> if (appendResults || !SharedItems.fetchedAll) {
api.readItems( binding.swipeRefreshLayout.isRefreshing = true
t, getReadItems(applicationContext, api, db, offset)
id, binding.swipeRefreshLayout.isRefreshing = false
f, }
itemsNumber, SharedItems.getAll()
offset items = SharedItems.focusedItems
) handleListResult()
} }
} }
private fun getStarred(appendResults: Boolean = false) { private fun getStarred(appendResults: Boolean = false) {
elementsShown = FAV_SHOWN CoroutineScope(Dispatchers.Main).launch {
doCallTo(appendResults, R.string.cant_get_favs) { t, id, f -> if (appendResults || !SharedItems.fetchedStarred) {
api.starredItems( binding.swipeRefreshLayout.isRefreshing = true
t, getStarredItems(applicationContext, api, db, offset)
id, binding.swipeRefreshLayout.isRefreshing = false
f, }
itemsNumber, SharedItems.getStarred()
offset items = SharedItems.focusedItems
) handleListResult()
} }
} }
@ -1238,59 +1065,35 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
} }
binding.recyclerView.adapter = recyclerAdapter binding.recyclerView.adapter = recyclerAdapter
} else { } else {
if (!appendResults) {
(recyclerAdapter as ItemsAdapter<*>).updateAllItems() (recyclerAdapter as ItemsAdapter<*>).updateAllItems()
} else {
(recyclerAdapter as ItemsAdapter<*>).addItemsAtEnd(items)
}
} }
reloadBadges() reloadBadges()
mayBeEmpty()
} }
private fun reloadBadges() { private fun reloadBadges() {
if (this@HomeActivity.isNetworkAccessible(null, offlineShortcut) && (displayUnreadCount || displayAllCount)) { if (displayUnreadCount || displayAllCount) {
api.stats.enqueue(object : Callback<Stats> { CoroutineScope(Dispatchers.Main).launch {
override fun onResponse(call: Call<Stats>, response: Response<Stats>) { reloadBadges(applicationContext, api)
if (response.body() != null) { reloadBadgeContent()
}
badgeNew = response.body()!!.unread
badgeAll = response.body()!!.total
badgeFavs = response.body()!!.starred
reloadBadgeContent()
}
}
override fun onFailure(call: Call<Stats>, t: Throwable) {
}
})
} else {
reloadBadgeContent(succeeded = false)
} }
} }
private fun reloadBadgeContent(succeeded: Boolean = true) { private fun reloadBadgeContent() {
if (succeeded) { if (displayUnreadCount) {
if (displayUnreadCount) { tabNewBadge
tabNewBadge .setText(SharedItems.badgeUnread.toString())
.setText(badgeNew.toString()) .maybeShow()
.maybeShow() }
} if (displayAllCount) {
if (displayAllCount) { tabArchiveBadge
tabArchiveBadge .setText(SharedItems.badgeAll.toString())
.setText(badgeAll.toString()) .maybeShow()
.maybeShow() tabStarredBadge
tabStarredBadge .setText(SharedItems.badgeStarred.toString())
.setText(badgeFavs.toString()) .maybeShow()
.maybeShow()
} else {
tabArchiveBadge.removeBadge()
tabStarredBadge.removeBadge()
}
} else {
tabNewBadge.removeBadge()
tabArchiveBadge.removeBadge()
tabStarredBadge.removeBadge()
} }
} }
@ -1310,14 +1113,18 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
override fun onQueryTextChange(p0: String?): Boolean { override fun onQueryTextChange(p0: String?): Boolean {
if (p0.isNullOrBlank()) { if (p0.isNullOrBlank()) {
maybeSearchFilter = null maybeSearchFilter = null
SharedItems.searchFilter = null
getElementsAccordingToTab() getElementsAccordingToTab()
fetchOnEmptyList()
} }
return false return false
} }
override fun onQueryTextSubmit(p0: String?): Boolean { override fun onQueryTextSubmit(p0: String?): Boolean {
maybeSearchFilter = p0 maybeSearchFilter = p0
SharedItems.searchFilter = p0
getElementsAccordingToTab() getElementsAccordingToTab()
fetchOnEmptyList()
return false return false
} }
@ -1387,64 +1194,33 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
R.id.readAll -> { R.id.readAll -> {
if (elementsShown == UNREAD_SHOWN) { if (elementsShown == UNREAD_SHOWN) {
needsConfirmation(R.string.readAll, R.string.markall_dialog_message) { needsConfirmation(R.string.readAll, R.string.markall_dialog_message) {
binding.swipeRefreshLayout.isRefreshing = false binding.swipeRefreshLayout.isRefreshing = true
val ids = allItems.map { it.id }
val itemsByTag: Map<Long, Int> =
allItems.flattenTags()
.groupBy { it.tags.tags.longHash() }
.map { it.key to it.value.size }
.toMap()
if (ids.isNotEmpty() && this@HomeActivity.isNetworkAccessible(null, offlineShortcut)) { if (this@HomeActivity.isNetworkAccessible(null, offlineShortcut)) {
api.readAll(ids).enqueue(object : Callback<SuccessResponse> { CoroutineScope(Dispatchers.Main).launch {
override fun onResponse( val success = readAll(applicationContext, api, db)
call: Call<SuccessResponse>, if (success) {
response: Response<SuccessResponse> Toast.makeText(
) { this@HomeActivity,
if (response.body() != null && response.body()!!.isSuccess) { R.string.all_posts_read,
Toast.makeText( Toast.LENGTH_SHORT
this@HomeActivity, ).show()
R.string.all_posts_read, tabNewBadge.removeBadge()
Toast.LENGTH_SHORT
).show()
tabNewBadge.removeBadge()
handleDrawerItems() handleDrawerItems()
getElementsAccordingToTab() getElementsAccordingToTab()
} else { } else {
Toast.makeText(
this@HomeActivity,
R.string.all_posts_not_read,
Toast.LENGTH_SHORT
).show()
}
binding.swipeRefreshLayout.isRefreshing = false
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
Toast.makeText( Toast.makeText(
this@HomeActivity, this@HomeActivity,
R.string.all_posts_not_read, R.string.all_posts_not_read,
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
binding.swipeRefreshLayout.isRefreshing = false
} }
}) handleListResult()
items = ArrayList() binding.swipeRefreshLayout.isRefreshing = false
allItems = ArrayList() }
} }
if (items.isEmpty()) {
Toast.makeText(
this@HomeActivity,
R.string.nothing_here,
Toast.LENGTH_SHORT
).show()
}
handleListResult()
} }
} }
return true return true
@ -1458,10 +1234,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
private fun maxItemNumber(): Int = private fun maxItemNumber(): Int =
when (elementsShown) { when (elementsShown) {
UNREAD_SHOWN -> badgeNew UNREAD_SHOWN -> SharedItems.badgeUnread
READ_SHOWN -> badgeAll READ_SHOWN -> SharedItems.badgeAll
FAV_SHOWN -> badgeFavs FAV_SHOWN -> SharedItems.badgeStarred
else -> badgeNew // if !elementsShown then unread are fetched. else -> SharedItems.badgeUnread // if !elementsShown then unread are fetched.
} }
private fun updateItems(adapterItems: ArrayList<Item>) { private fun updateItems(adapterItems: ArrayList<Item>) {
@ -1505,7 +1281,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
} }
if (this@HomeActivity.isNetworkAccessible(null, offlineShortcut)) { if (this@HomeActivity.isNetworkAccessible(null, offlineShortcut)) {
thread { CoroutineScope(Dispatchers.Main).launch {
val actions = db.actionsDao().actions() val actions = db.actionsDao().actions()
actions.forEach { action -> actions.forEach { action ->

View File

@ -2,7 +2,6 @@ package apps.amine.bou.readerforselfoss.adapters
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import androidx.cardview.widget.CardView
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View

View File

@ -2,22 +2,12 @@ package apps.amine.bou.readerforselfoss.adapters
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import android.text.Spannable
import android.text.style.ClickableSpan
import android.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import apps.amine.bou.readerforselfoss.R
import apps.amine.bou.readerforselfoss.api.selfoss.Item import apps.amine.bou.readerforselfoss.api.selfoss.Item
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
import apps.amine.bou.readerforselfoss.databinding.ListItemBinding import apps.amine.bou.readerforselfoss.databinding.ListItemBinding
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
import apps.amine.bou.readerforselfoss.themes.AppColors import apps.amine.bou.readerforselfoss.themes.AppColors
@ -27,19 +17,11 @@ import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
import apps.amine.bou.readerforselfoss.utils.glide.bitmapCenterCrop import apps.amine.bou.readerforselfoss.utils.glide.bitmapCenterCrop
import apps.amine.bou.readerforselfoss.utils.glide.circularBitmapDrawable import apps.amine.bou.readerforselfoss.utils.glide.circularBitmapDrawable
import apps.amine.bou.readerforselfoss.utils.openInBrowserAsNewTask
import apps.amine.bou.readerforselfoss.utils.openItemUrl import apps.amine.bou.readerforselfoss.utils.openItemUrl
import apps.amine.bou.readerforselfoss.utils.shareLink
import apps.amine.bou.readerforselfoss.utils.sourceAndDateText import apps.amine.bou.readerforselfoss.utils.sourceAndDateText
import apps.amine.bou.readerforselfoss.utils.toTextDrawableString import apps.amine.bou.readerforselfoss.utils.toTextDrawableString
import com.amulyakhare.textdrawable.TextDrawable import com.amulyakhare.textdrawable.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator import com.amulyakhare.textdrawable.util.ColorGenerator
import com.like.LikeButton
import com.like.OnLikeListener
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
class ItemListAdapter( class ItemListAdapter(

View File

@ -2,26 +2,16 @@ package apps.amine.bou.readerforselfoss.adapters
import android.app.Activity import android.app.Activity
import android.graphics.Color import android.graphics.Color
import com.google.android.material.snackbar.Snackbar
import androidx.recyclerview.widget.RecyclerView
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import androidx.recyclerview.widget.RecyclerView
import apps.amine.bou.readerforselfoss.R import apps.amine.bou.readerforselfoss.R
import apps.amine.bou.readerforselfoss.api.selfoss.Item import apps.amine.bou.readerforselfoss.api.selfoss.Item
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
import apps.amine.bou.readerforselfoss.themes.AppColors import apps.amine.bou.readerforselfoss.themes.AppColors
import apps.amine.bou.readerforselfoss.utils.Config import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.SharedItems import apps.amine.bou.readerforselfoss.utils.SharedItems
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible import com.google.android.material.snackbar.Snackbar
import apps.amine.bou.readerforselfoss.utils.persistence.toEntity
import apps.amine.bou.readerforselfoss.utils.succeeded
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import kotlin.concurrent.thread
abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapter<VH>() { abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapter<VH>() {
abstract var items: ArrayList<Item> abstract var items: ArrayList<Item>
@ -47,34 +37,11 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
Snackbar.LENGTH_LONG Snackbar.LENGTH_LONG
) )
.setAction(R.string.undo_string) { .setAction(R.string.undo_string) {
items.add(position, i) SharedItems.unreadItem(app, api, db, i)
thread { if (SharedItems.displayedItems == "unread") {
db.itemsDao().insertAllItems(i.toEntity()) addItemAtIndex(i, position)
}
notifyItemInserted(position)
updateItems(items)
if (app.isNetworkAccessible(null)) {
api.unmarkItem(i.id).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
items.remove(i)
thread {
db.itemsDao().delete(i.toEntity())
}
notifyItemRemoved(position)
updateItems(items)
}
})
} else { } else {
thread { notifyItemChanged(position)
db.actionsDao().insertAllActions(ActionEntity(i.id, false, true, false, false))
}
} }
} }
@ -84,7 +51,7 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
s.show() s.show()
} }
private fun markSnackbar(i: Item, position: Int) { private fun markSnackbar(position: Int) {
val s = Snackbar val s = Snackbar
.make( .make(
app.findViewById(R.id.coordLayout), app.findViewById(R.id.coordLayout),
@ -92,34 +59,13 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
Snackbar.LENGTH_LONG Snackbar.LENGTH_LONG
) )
.setAction(R.string.undo_string) { .setAction(R.string.undo_string) {
items.add(position, i) SharedItems.readItem(app, api, db, items[position])
thread { items = SharedItems.focusedItems
db.itemsDao().delete(i.toEntity()) if (SharedItems.displayedItems == "unread") {
} notifyItemRemoved(position)
notifyItemInserted(position) updateItems(items)
updateItems(items)
if (app.isNetworkAccessible(null)) {
api.markItem(i.id).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
items.remove(i)
thread {
db.itemsDao().insertAllItems(i.toEntity())
}
notifyItemRemoved(position)
updateItems(items)
}
})
} else { } else {
thread { notifyItemChanged(position)
db.actionsDao().insertAllActions(ActionEntity(i.id, true, false, false, false))
}
} }
} }
@ -130,99 +76,30 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
} }
fun handleItemAtIndex(position: Int) { fun handleItemAtIndex(position: Int) {
if (unreadItemStatusAtIndex(position)) { if (SharedItems.unreadItemStatusAtIndex(position)) {
readItemAtIndex(position) readItemAtIndex(position)
} else { } else {
unreadItemAtIndex(position) unreadItemAtIndex(position)
} }
} }
fun unreadItemStatusAtIndex(position: Int): Boolean {
return items[position].unread
}
private fun readItemAtIndex(position: Int) { private fun readItemAtIndex(position: Int) {
val i = items[position] val i = items[position]
items.remove(i) SharedItems.readItem(app, api, db, i)
notifyItemRemoved(position) if (SharedItems.displayedItems == "unread") {
updateItems(items) items.remove(i)
notifyItemRemoved(position)
thread { updateItems(items)
db.itemsDao().delete(i.toEntity())
}
if (app.isNetworkAccessible(null)) {
api.markItem(i.id).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
unmarkSnackbar(i, position)
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
Toast.makeText(
app,
app.getString(R.string.cant_mark_read),
Toast.LENGTH_SHORT
).show()
items.add(position, i)
notifyItemInserted(position)
updateItems(items)
thread {
db.itemsDao().insertAllItems(i.toEntity())
}
}
})
} else { } else {
thread { notifyItemChanged(position)
db.actionsDao().insertAllActions(ActionEntity(i.id, true, false, false, false))
}
} }
unmarkSnackbar(i, position)
} }
private fun unreadItemAtIndex(position: Int) { private fun unreadItemAtIndex(position: Int) {
val i = items[position] SharedItems.unreadItem(app, api, db, items[position])
items.remove(i) notifyItemChanged(position)
notifyItemRemoved(position) markSnackbar(position)
updateItems(items)
thread {
db.itemsDao().insertAllItems(i.toEntity())
}
if (app.isNetworkAccessible(null)) {
api.unmarkItem(i.id).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
markSnackbar(i, position)
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
Toast.makeText(
app,
app.getString(R.string.cant_mark_unread),
Toast.LENGTH_SHORT
).show()
items.add(i)
notifyItemInserted(position)
updateItems(items)
thread {
db.itemsDao().delete(i.toEntity())
}
}
})
} else {
thread {
db.actionsDao().insertAllActions(ActionEntity(i.id, false, true, false, false))
}
}
} }
fun addItemAtIndex(item: Item, position: Int) { fun addItemAtIndex(item: Item, position: Int) {

View File

@ -3,6 +3,7 @@ package apps.amine.bou.readerforselfoss.api.selfoss
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import apps.amine.bou.readerforselfoss.utils.Config import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.SharedItems
import apps.amine.bou.readerforselfoss.utils.getUnsafeHttpClient import apps.amine.bou.readerforselfoss.utils.getUnsafeHttpClient
import com.burgstaller.okhttp.AuthenticationCacheInterceptor import com.burgstaller.okhttp.AuthenticationCacheInterceptor
import com.burgstaller.okhttp.CachingAuthenticatorDecorator import com.burgstaller.okhttp.CachingAuthenticatorDecorator
@ -142,54 +143,50 @@ class SelfossApi(
fun login(): Call<SuccessResponse> = fun login(): Call<SuccessResponse> =
service.loginToSelfoss(config.userLogin, config.userPassword) service.loginToSelfoss(config.userLogin, config.userPassword)
fun readItems( suspend fun readItems(
tag: String?,
sourceId: Long?,
search: String?,
itemsNumber: Int, itemsNumber: Int,
offset: Int offset: Int
): Call<List<Item>> = ): retrofit2.Response<List<Item>> =
getItems("read", tag, sourceId, search, itemsNumber, offset) getItems("read", SharedItems.tagFilter, SharedItems.sourceIDFilter, SharedItems.searchFilter, itemsNumber, offset)
fun newItems( suspend fun newItems(
tag: String?,
sourceId: Long?,
search: String?,
itemsNumber: Int, itemsNumber: Int,
offset: Int offset: Int
): Call<List<Item>> = ): retrofit2.Response<List<Item>> =
getItems("unread", tag, sourceId, search, itemsNumber, offset) getItems("unread", SharedItems.tagFilter, SharedItems.sourceIDFilter, SharedItems.searchFilter, itemsNumber, offset)
fun starredItems( suspend fun starredItems(
tag: String?,
sourceId: Long?,
search: String?,
itemsNumber: Int, itemsNumber: Int,
offset: Int offset: Int
): Call<List<Item>> = ): retrofit2.Response<List<Item>> =
getItems("starred", tag, sourceId, search, itemsNumber, offset) getItems("starred", SharedItems.tagFilter, SharedItems.sourceIDFilter, SharedItems.searchFilter, itemsNumber, offset)
fun allItems(): Call<List<Item>> = fun allItems(): Call<List<Item>> =
service.allItems(userName, password) service.allItems(userName, password)
fun allNewItems(): Call<List<Item>> = suspend fun allNewItems(): retrofit2.Response<List<Item>> =
getItems("unread", null, null, null, 200, 0) getItems("unread", null, null, null, 200, 0)
fun allReadItems(): Call<List<Item>> = suspend fun allReadItems(): retrofit2.Response<List<Item>> =
getItems("read", null, null, null, 200, 0) getItems("read", null, null, null, 200, 0)
fun allStarredItems(): Call<List<Item>> = suspend fun allStarredItems(): retrofit2.Response<List<Item>> =
getItems("read", null, null, null, 200, 0) getItems("read", null, null, null, 200, 0)
private fun getItems( private suspend fun getItems(
type: String, type: String,
tag: String?, tag: String?,
sourceId: Long?, sourceId: Long?,
search: String?, search: String?,
items: Int, items: Int,
offset: Int offset: Int
): Call<List<Item>> = ): retrofit2.Response<List<Item>> =
service.getItems(type, tag, sourceId, search, userName, password, items, offset) service.getItems(type, tag, sourceId, search, null, userName, password, items, offset)
suspend fun updateItems(
updatedSince: String
): retrofit2.Response<List<Item>> =
service.getItems("read", null, null, null, updatedSince, userName, password, 200, 0)
fun markItem(itemId: String): Call<SuccessResponse> = fun markItem(itemId: String): Call<SuccessResponse> =
service.markAsRead(itemId, userName, password) service.markAsRead(itemId, userName, password)
@ -197,7 +194,7 @@ class SelfossApi(
fun unmarkItem(itemId: String): Call<SuccessResponse> = fun unmarkItem(itemId: String): Call<SuccessResponse> =
service.unmarkAsRead(itemId, userName, password) service.unmarkAsRead(itemId, userName, password)
fun readAll(ids: List<String>): Call<SuccessResponse> = suspend fun readAll(ids: List<String>): SuccessResponse =
service.markAllAsRead(ids, userName, password) service.markAllAsRead(ids, userName, password)
fun starrItem(itemId: String): Call<SuccessResponse> = fun starrItem(itemId: String): Call<SuccessResponse> =
@ -206,8 +203,7 @@ class SelfossApi(
fun unstarrItem(itemId: String): Call<SuccessResponse> = fun unstarrItem(itemId: String): Call<SuccessResponse> =
service.unstarr(itemId, userName, password) service.unstarr(itemId, userName, password)
val stats: Call<Stats> suspend fun stats(): retrofit2.Response<Stats> = service.stats(userName, password)
get() = service.stats(userName, password)
val tags: Call<List<Tag>> val tags: Call<List<Tag>>
get() = service.tags(userName, password) get() = service.tags(userName, password)

View File

@ -0,0 +1,134 @@
package apps.amine.bou.readerforselfoss.api.selfoss
import android.content.Context
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
import apps.amine.bou.readerforselfoss.utils.SharedItems
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAvailable
import kotlinx.coroutines.*
import retrofit2.Response
suspend fun getAndStoreAllItems(context: Context, api: SelfossApi, db: AppDatabase) = withContext(Dispatchers.IO) {
if (isNetworkAvailable(context)) {
launch {
try {
enqueueArticles(api.allNewItems(), db, true)
} catch (e: Throwable) {}
}
launch {
try {
enqueueArticles(api.allReadItems(), db, false)
} catch (e: Throwable) {}
}
launch {
try {
enqueueArticles(api.allStarredItems(), db, false)
} catch (e: Throwable) {}
}
} else {
launch { SharedItems.updateDatabase(db) }
}
}
suspend fun updateItems(context: Context, api: SelfossApi, db: AppDatabase) = coroutineScope {
if (isNetworkAvailable(context)) {
launch {
try {
enqueueArticles(api.updateItems(SharedItems.items[0].datetime), db, true)
} catch (e: Throwable) {}
}
}
}
suspend fun refreshFocusedItems(context: Context, api: SelfossApi, db: AppDatabase) = withContext(Dispatchers.IO) {
if (isNetworkAvailable(context)) {
val response = when (SharedItems.displayedItems) {
"read" -> api.readItems(200, 0)
"unread" -> api.newItems(200, 0)
"starred" -> api.starredItems(200, 0)
else -> api.readItems(200, 0)
}
if (response.isSuccessful) {
SharedItems.refreshFocusedItems(response.body() as ArrayList<Item>)
SharedItems.updateDatabase(db)
}
}
}
suspend fun getReadItems(context: Context, api: SelfossApi, db: AppDatabase, offset: Int) = withContext(Dispatchers.IO) {
if (isNetworkAvailable(context)) {
try {
enqueueArticles(api.readItems( 200, offset), db, false)
SharedItems.fetchedAll = true
SharedItems.updateDatabase(db)
} catch (e: Throwable) {}
}
}
suspend fun getUnreadItems(context: Context, api: SelfossApi, db: AppDatabase, offset: Int) = withContext(Dispatchers.IO) {
if (isNetworkAvailable(context)) {
try {
if (!SharedItems.fetchedUnread) {
SharedItems.clearDBItems(db)
}
enqueueArticles(api.newItems(200, offset), db, false)
SharedItems.fetchedUnread = true
} catch (e: Throwable) {}
}
SharedItems.updateDatabase(db)
}
suspend fun getStarredItems(context: Context, api: SelfossApi, db: AppDatabase, offset: Int) = withContext(Dispatchers.IO) {
if (isNetworkAvailable(context)) {
try {
enqueueArticles(api.starredItems(200, offset), db, false)
SharedItems.fetchedStarred = true
SharedItems.updateDatabase(db)
} catch (e: Throwable) {
}
}
}
suspend fun readAll(context: Context, api: SelfossApi, db: AppDatabase): Boolean {
var success = false
if (isNetworkAvailable(context)) {
try {
val ids = SharedItems.focusedItems.map { it.id }
if (ids.isNotEmpty()) {
val result = api.readAll(ids)
SharedItems.readItems(db, ids)
success = result.isSuccess
}
} catch (e: Throwable) {}
}
return success
}
suspend fun reloadBadges(context: Context, api: SelfossApi) = withContext(Dispatchers.IO) {
if (isNetworkAvailable(context)) {
try {
val response = api.stats()
if (response.isSuccessful) {
val badges = response.body()
SharedItems.badgeUnread = badges!!.unread
SharedItems.badgeAll = badges.total
SharedItems.badgeStarred = badges.starred
}
} catch (e: Throwable) {}
} else {
SharedItems.computeBadges()
}
}
private fun enqueueArticles(response: Response<List<Item>>, db: AppDatabase, clearDatabase: Boolean) {
if (response.isSuccessful) {
if (clearDatabase) {
CoroutineScope(Dispatchers.IO).launch {
SharedItems.clearDBItems(db)
}
}
val allItems = response.body() as ArrayList<Item>
SharedItems.appendNewItems(allItems)
}
}

View File

@ -88,7 +88,7 @@ data class Item(
@SerializedName("datetime") val datetime: String, @SerializedName("datetime") val datetime: String,
@SerializedName("title") val title: String, @SerializedName("title") val title: String,
@SerializedName("content") val content: String, @SerializedName("content") val content: String,
@SerializedName("unread") val unread: Boolean, @SerializedName("unread") var unread: Boolean,
@SerializedName("starred") var starred: Boolean, @SerializedName("starred") var starred: Boolean,
@SerializedName("thumbnail") val thumbnail: String?, @SerializedName("thumbnail") val thumbnail: String?,
@SerializedName("icon") val icon: String?, @SerializedName("icon") val icon: String?,

View File

@ -1,6 +1,7 @@
package apps.amine.bou.readerforselfoss.api.selfoss package apps.amine.bou.readerforselfoss.api.selfoss
import retrofit2.Call import retrofit2.Call
import retrofit2.Response
import retrofit2.http.DELETE import retrofit2.http.DELETE
import retrofit2.http.Field import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded import retrofit2.http.FormUrlEncoded
@ -16,16 +17,17 @@ internal interface SelfossService {
fun loginToSelfoss(@Query("username") username: String, @Query("password") password: String): Call<SuccessResponse> fun loginToSelfoss(@Query("username") username: String, @Query("password") password: String): Call<SuccessResponse>
@GET("items") @GET("items")
fun getItems( suspend fun getItems(
@Query("type") type: String, @Query("type") type: String,
@Query("tag") tag: String?, @Query("tag") tag: String?,
@Query("source") source: Long?, @Query("source") source: Long?,
@Query("search") search: String?, @Query("search") search: String?,
@Query("updatedsince") updatedSince: String?,
@Query("username") username: String, @Query("username") username: String,
@Query("password") password: String, @Query("password") password: String,
@Query("items") items: Int, @Query("items") items: Int,
@Query("offset") offset: Int @Query("offset") offset: Int
): Call<List<Item>> ): Response<List<Item>>
@GET("items") @GET("items")
fun allItems( fun allItems(
@ -51,11 +53,11 @@ internal interface SelfossService {
@FormUrlEncoded @FormUrlEncoded
@POST("mark") @POST("mark")
fun markAllAsRead( suspend fun markAllAsRead(
@Field("ids[]") ids: List<String>, @Field("ids[]") ids: List<String>,
@Query("username") username: String, @Query("username") username: String,
@Query("password") password: String @Query("password") password: String
): Call<SuccessResponse> ): SuccessResponse
@Headers("Content-Type: application/x-www-form-urlencoded") @Headers("Content-Type: application/x-www-form-urlencoded")
@POST("starr/{id}") @POST("starr/{id}")
@ -74,10 +76,10 @@ internal interface SelfossService {
): Call<SuccessResponse> ): Call<SuccessResponse>
@GET("stats") @GET("stats")
fun stats( suspend fun stats(
@Query("username") username: String, @Query("username") username: String,
@Query("password") password: String @Query("password") password: String
): Call<Stats> ): Response<Stats>
@GET("tags") @GET("tags")
fun tags( fun tags(

View File

@ -13,16 +13,19 @@ import androidx.work.Worker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import apps.amine.bou.readerforselfoss.MainActivity import apps.amine.bou.readerforselfoss.MainActivity
import apps.amine.bou.readerforselfoss.R import apps.amine.bou.readerforselfoss.R
import apps.amine.bou.readerforselfoss.api.selfoss.Item
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.getAndStoreAllItems
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_1_2 import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_1_2
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_2_3 import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_2_3
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_3_4 import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_3_4
import apps.amine.bou.readerforselfoss.utils.Config import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible import apps.amine.bou.readerforselfoss.utils.SharedItems
import apps.amine.bou.readerforselfoss.utils.persistence.toEntity import apps.amine.bou.readerforselfoss.utils.network.isNetworkAvailable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import retrofit2.Call import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
@ -33,123 +36,99 @@ import kotlin.concurrent.thread
class LoadingWorker(val context: Context, params: WorkerParameters) : Worker(context, params) { class LoadingWorker(val context: Context, params: WorkerParameters) : Worker(context, params) {
lateinit var db: AppDatabase lateinit var db: AppDatabase
override fun doWork(): Result { override fun doWork(): Result {
if (context.isNetworkAccessible(null)) { val settings =
this.context.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
val sharedPref = PreferenceManager.getDefaultSharedPreferences(this.context)
val periodicRefresh = sharedPref.getBoolean("periodic_refresh", false)
if (periodicRefresh) {
val api = SelfossApi(
this.context,
null,
settings.getBoolean("isSelfSignedCert", false),
sharedPref.getString("api_timeout", "-1")!!.toLong()
)
val notificationManager = if (isNetworkAvailable(context)) {
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notification = NotificationCompat.Builder(applicationContext, Config.syncChannelId) CoroutineScope(Dispatchers.IO).launch {
.setContentTitle(context.getString(R.string.loading_notification_title)) val notificationManager =
.setContentText(context.getString(R.string.loading_notification_text)) applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
.setOngoing(true)
.setPriority(PRIORITY_LOW)
.setChannelId(Config.syncChannelId)
.setSmallIcon(R.drawable.ic_stat_cloud_download_black_24dp)
notificationManager.notify(1, notification.build()) val notification =
NotificationCompat.Builder(applicationContext, Config.syncChannelId)
.setContentTitle(context.getString(R.string.loading_notification_title))
.setContentText(context.getString(R.string.loading_notification_text))
.setOngoing(true)
.setPriority(PRIORITY_LOW)
.setChannelId(Config.syncChannelId)
.setSmallIcon(R.drawable.ic_stat_cloud_download_black_24dp)
val settings = notificationManager.notify(1, notification.build())
this.context.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
val sharedPref = PreferenceManager.getDefaultSharedPreferences(this.context)
val notifyNewItems = sharedPref.getBoolean("notify_new_items", false)
db = Room.databaseBuilder( val notifyNewItems = sharedPref.getBoolean("notify_new_items", false)
applicationContext,
AppDatabase::class.java, "selfoss-database"
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).addMigrations(MIGRATION_3_4).build()
val api = SelfossApi( db = Room.databaseBuilder(
this.context, applicationContext,
null, AppDatabase::class.java, "selfoss-database"
settings.getBoolean("isSelfSignedCert", false), ).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3)
sharedPref.getString("api_timeout", "-1")!!.toLong() .addMigrations(MIGRATION_3_4).build()
)
api.allNewItems().enqueue(object : Callback<List<Item>> {
override fun onFailure(call: Call<List<Item>>, t: Throwable) {
Timer("", false).schedule(4000) {
notificationManager.cancel(1)
}
}
override fun onResponse(
call: Call<List<Item>>,
response: Response<List<Item>>
) {
storeItems(response, true, notifyNewItems, notificationManager)
}
})
api.allReadItems().enqueue(object : Callback<List<Item>> {
override fun onFailure(call: Call<List<Item>>, t: Throwable) {
Timer("", false).schedule(4000) {
notificationManager.cancel(1)
}
}
override fun onResponse(
call: Call<List<Item>>,
response: Response<List<Item>>
) {
storeItems(response, false, notifyNewItems, notificationManager)
}
})
api.allStarredItems().enqueue(object : Callback<List<Item>> {
override fun onFailure(call: Call<List<Item>>, t: Throwable) {
Timer("", false).schedule(4000) {
notificationManager.cancel(1)
}
}
override fun onResponse(
call: Call<List<Item>>,
response: Response<List<Item>>
) {
storeItems(response, false, notifyNewItems, notificationManager)
}
})
thread {
val actions = db.actionsDao().actions() val actions = db.actionsDao().actions()
actions.forEach { action -> actions.forEach { action ->
when { when {
action.read -> doAndReportOnFail(api.markItem(action.articleId), action) action.read -> doAndReportOnFail(
action.unread -> doAndReportOnFail(api.unmarkItem(action.articleId), action) api.markItem(action.articleId),
action.starred -> doAndReportOnFail(api.starrItem(action.articleId), action) action
)
action.unread -> doAndReportOnFail(
api.unmarkItem(action.articleId),
action
)
action.starred -> doAndReportOnFail(
api.starrItem(action.articleId),
action
)
action.unstarred -> doAndReportOnFail( action.unstarred -> doAndReportOnFail(
api.unstarrItem(action.articleId), api.unstarrItem(action.articleId),
action action
) )
} }
} }
getAndStoreAllItems(context, api, db)
SharedItems.updateDatabase(db)
storeItems(notifyNewItems, notificationManager)
} }
} }
return Result.success()
} }
return Result.success()
}
private fun storeItems(response: Response<List<Item>>, newItems: Boolean, notifyNewItems: Boolean, notificationManager: NotificationManager) { private fun storeItems(notifyNewItems: Boolean, notificationManager: NotificationManager) {
thread { CoroutineScope(Dispatchers.IO).launch {
if (response.body() != null) { val apiItems = SharedItems.items
val apiItems = (response.body() as ArrayList<Item>)
if (newItems) {
db.itemsDao().deleteAllItems()
}
db.itemsDao()
.insertAllItems(*(apiItems.map { it.toEntity() }).toTypedArray())
val newSize = apiItems.filter { it.unread }.size val newSize = apiItems.filter { it.unread }.size
if (newItems && notifyNewItems && newSize > 0) { if (notifyNewItems && newSize > 0) {
val intent = Intent(context, MainActivity::class.java).apply { val intent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
} }
val pendingIntent: PendingIntent = PendingIntent.getActivity(context, 0, intent, 0) val pendingIntent: PendingIntent =
PendingIntent.getActivity(context, 0, intent, 0)
val newItemsNotification = NotificationCompat.Builder(applicationContext, Config.newItemsChannelId) val newItemsNotification =
NotificationCompat.Builder(applicationContext, Config.newItemsChannelId)
.setContentTitle(context.getString(R.string.new_items_notification_title)) .setContentTitle(context.getString(R.string.new_items_notification_title))
.setContentText(context.getString(R.string.new_items_notification_text, newSize)) .setContentText(
context.getString(
R.string.new_items_notification_text,
newSize
)
)
.setPriority(PRIORITY_DEFAULT) .setPriority(PRIORITY_DEFAULT)
.setChannelId(Config.newItemsChannelId) .setChannelId(Config.newItemsChannelId)
.setContentIntent(pendingIntent) .setContentIntent(pendingIntent)
@ -161,7 +140,6 @@ class LoadingWorker(val context: Context, params: WorkerParameters) : Worker(con
} }
} }
apiItems.map { it.preloadImages(context) } apiItems.map { it.preloadImages(context) }
}
Timer("", false).schedule(4000) { Timer("", false).schedule(4000) {
notificationManager.cancel(1) notificationManager.cancel(1)
} }

View File

@ -10,7 +10,7 @@ import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
@Dao @Dao
interface ActionsDao { interface ActionsDao {
@Query("SELECT * FROM actions order by id asc") @Query("SELECT * FROM actions order by id asc")
fun actions(): List<ActionEntity> suspend fun actions(): List<ActionEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAllActions(vararg actions: ActionEntity) fun insertAllActions(vararg actions: ActionEntity)

View File

@ -13,17 +13,17 @@ import androidx.room.Update
@Dao @Dao
interface ItemsDao { interface ItemsDao {
@Query("SELECT * FROM items order by id desc") @Query("SELECT * FROM items order by id desc")
fun items(): List<ItemEntity> suspend fun items(): List<ItemEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAllItems(vararg items: ItemEntity) suspend fun insertAllItems(vararg items: ItemEntity)
@Query("DELETE FROM items") @Query("DELETE FROM items")
fun deleteAllItems() suspend fun deleteAllItems()
@Delete @Delete
fun delete(item: ItemEntity) suspend fun delete(item: ItemEntity)
@Update @Update
fun updateItem(item: ItemEntity) suspend fun updateItem(item: ItemEntity)
} }

View File

@ -10,9 +10,14 @@ import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
import apps.amine.bou.readerforselfoss.utils.persistence.toEntity import apps.amine.bou.readerforselfoss.utils.persistence.toEntity
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
import apps.amine.bou.readerforselfoss.utils.persistence.toView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import retrofit2.Call import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
import java.text.SimpleDateFormat
import kotlin.concurrent.thread import kotlin.concurrent.thread
/* /*
@ -38,30 +43,170 @@ object SharedItems {
set(value) { set(value) {
field = when { field = when {
value < 0 -> 0 value < 0 -> 0
value > focusedItems.size -> focusedItems.size value > items.size -> items.size
else -> value else -> value
} }
} }
var displayedItems: String = "unread"
set(value) {
field = when (value) {
"all" -> "all"
"unread" -> "unread"
"read" -> "read"
"starred" -> "starred"
else -> "all"
}
}
fun readItem(app: Context, api: SelfossApi, db: AppDatabase, item: Item) { var searchFilter: String? = null
if (focusedItems.contains(item)) { var sourceIDFilter: Long? = null
position = focusedItems.indexOf(item) var sourceFilter: String? = null
readItemAtIndex(app, api, db) var tagFilter: String? = null
var itemsCaching = false
var fetchedUnread = false
var fetchedAll = false
var fetchedStarred = false
var badgeUnread = -1
var badgeAll = -1
var badgeStarred = -1
fun appendNewItems(newItems: ArrayList<Item>) {
val tmpItems = items
if (tmpItems != newItems) {
newItems.removeAll(tmpItems)
tmpItems.addAll(newItems)
items = tmpItems
sortItems()
getFocusedItems()
} }
} }
fun readItemAtIndex(app: Context, api: SelfossApi, db: AppDatabase) { fun refreshFocusedItems(newItems: ArrayList<Item>) {
val i = focusedItems[position] val tmpItems = items
var tmpItems = items tmpItems.removeAll(focusedItems)
tmpItems.remove(i)
items = tmpItems
var tmpFocusedItems = focusedItems
tmpFocusedItems.remove(i)
focusedItems = tmpFocusedItems
thread { appendNewItems(newItems)
db.itemsDao().delete(i.toEntity()) }
suspend fun clearDBItems(db: AppDatabase) {
db.itemsDao().deleteAllItems()
}
suspend fun updateDatabase(db: AppDatabase) {
if (itemsCaching) {
if (items.isEmpty()) {
getFromDB(db)
}
db.itemsDao().deleteAllItems()
db.itemsDao().insertAllItems(*(items.map { it.toEntity() }).toTypedArray())
} }
}
fun filter() {
fun filterSearch(item: Item): Boolean {
return if (!searchFilter.isEmptyOrNullOrNullString()) {
var matched = item.title.contains(searchFilter.toString(), true)
matched = matched || item.content.contains(searchFilter.toString(), true)
matched = matched || item.sourcetitle.contains(searchFilter.toString(), true)
matched
} else {
true
}
}
var tmpItems = focusedItems
if (tagFilter != null) {
tmpItems = tmpItems.filter { it.tags.tags.contains(tagFilter.toString()) } as ArrayList<Item>
}
if (searchFilter != null) {
tmpItems = tmpItems.filter { filterSearch(it) } as ArrayList<Item>
}
if (sourceFilter != null) {
tmpItems = tmpItems.filter { it.sourcetitle == sourceFilter } as ArrayList<Item>
}
focusedItems = tmpItems
}
private fun getFocusedItems() {
when (displayedItems) {
"all" -> getAll()
"unread" -> getUnRead()
"read" -> getRead()
"starred" -> getStarred()
else -> getUnRead()
}
}
fun getUnRead() {
displayedItems = "unread"
focusedItems = items.filter { item -> item.unread } as ArrayList<Item>
filter()
}
fun getRead() {
displayedItems = "read"
focusedItems = items.filter { item -> !item.unread } as ArrayList<Item>
filter()
}
fun getStarred() {
displayedItems = "starred"
focusedItems = items.filter { item -> item.starred } as ArrayList<Item>
filter()
}
fun getAll() {
displayedItems = "all"
focusedItems = items
filter()
}
suspend fun getFromDB(db: AppDatabase) {
if (itemsCaching) {
val dbItems = db.itemsDao().items().map { it.toView() } as ArrayList<Item>
appendNewItems(dbItems)
}
}
private fun removeItemAtIndex(index: Int) {
val i = focusedItems[index]
val tmpItems = focusedItems
tmpItems.remove(i)
focusedItems = tmpItems
}
fun addItemAtIndex(newItem: Item, index: Int) {
val tmpItems = focusedItems
tmpItems.add(index, newItem)
focusedItems = tmpItems
}
fun readItem(app: Context, api: SelfossApi, db: AppDatabase, item: Item) {
if (items.contains(item)) {
position = items.indexOf(item)
readItemAtPosition(app, api, db)
}
}
fun readItems(db: AppDatabase, ids: List<String>) {
for (id in ids) {
val match = items.filter { it -> it.id == id }
if (match.isNotEmpty() && match.size == 1) {
position = items.indexOf(match[0])
val tmpItems = items
tmpItems[position].unread = false
items = tmpItems
resetDBItem(db)
badgeUnread--
}
}
}
private fun readItemAtPosition(app: Context, api: SelfossApi, db: AppDatabase) {
val i = items[position]
if (app.isNetworkAccessible(null)) { if (app.isNetworkAccessible(null)) {
api.markItem(i.id).enqueue(object : Callback<SuccessResponse> { api.markItem(i.id).enqueue(object : Callback<SuccessResponse> {
@ -70,7 +215,13 @@ object SharedItems {
response: Response<SuccessResponse> response: Response<SuccessResponse>
) { ) {
//unmarkSnackbar(i, position) val tmpItems = items
tmpItems[position].unread = false
items = tmpItems
resetDBItem(db)
getFocusedItems()
badgeUnread--
} }
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) { override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
@ -79,24 +230,82 @@ object SharedItems {
app.getString(R.string.cant_mark_read), app.getString(R.string.cant_mark_read),
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
tmpItems.add(position, i)
tmpFocusedItems.add(position, i)
items = tmpItems
focusedItems = tmpFocusedItems
thread {
db.itemsDao().insertAllItems(i.toEntity())
}
} }
}) })
} else { } else if (itemsCaching) {
thread { thread {
db.actionsDao().insertAllActions(ActionEntity(i.id, true, false, false, false)) db.actionsDao().insertAllActions(ActionEntity(i.id, true, false, false, false))
} }
} }
if (position > focusedItems.size) { if (position > items.size) {
position -= 1 position -= 1
} }
} }
fun unreadItem(app: Context, api: SelfossApi, db: AppDatabase, item: Item) {
if (items.contains(item) && !item.unread) {
position = items.indexOf(item)
unreadItemAtPosition(app, api, db)
}
}
private fun unreadItemAtPosition(app: Context, api: SelfossApi, db: AppDatabase) {
val i = items[position]
if (app.isNetworkAccessible(null)) {
api.unmarkItem(i.id).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
val tmpItems = items
tmpItems[position].unread = true
items = tmpItems
resetDBItem(db)
getFocusedItems()
badgeUnread++
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
Toast.makeText(
app,
app.getString(R.string.cant_mark_unread),
Toast.LENGTH_SHORT
).show()
}
})
} else if (itemsCaching) {
thread {
db.actionsDao().insertAllActions(ActionEntity(i.id, false, true, false, false))
}
}
}
private fun resetDBItem(db: AppDatabase) {
if (itemsCaching) {
val i = items[position]
CoroutineScope(Dispatchers.IO).launch {
db.itemsDao().delete(i.toEntity())
db.itemsDao().insertAllItems(i.toEntity())
}
}
}
fun unreadItemStatusAtIndex(position: Int): Boolean {
return focusedItems[position].unread
}
fun computeBadges() {
badgeUnread = items.filter { item -> item.unread }.size
badgeStarred = items.filter { item -> item.starred }.size
badgeAll = items.size
}
private fun sortItems() {
val tmpItems = ArrayList(items.sortedByDescending { SimpleDateFormat(Config.dateTimeFormatter).parse((it.datetime)) })
items = tmpItems
}
} }

View File

@ -3,7 +3,8 @@ package apps.amine.bou.readerforselfoss.utils.network
import android.content.Context import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.NetworkInfo import android.net.NetworkCapabilities
import android.os.Build
import android.view.View import android.view.View
import android.widget.TextView import android.widget.TextView
import apps.amine.bou.readerforselfoss.R import apps.amine.bou.readerforselfoss.R
@ -14,9 +15,7 @@ var view: View? = null
lateinit var s: Snackbar lateinit var s: Snackbar
fun Context.isNetworkAccessible(v: View?, overrideOffline: Boolean = false): Boolean { fun Context.isNetworkAccessible(v: View?, overrideOffline: Boolean = false): Boolean {
val cm = this.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val networkIsAccessible = isNetworkAvailable(this)
val activeNetwork: NetworkInfo? = cm.activeNetworkInfo
val networkIsAccessible = activeNetwork != null && activeNetwork.isConnectedOrConnecting
if (v != null && (!networkIsAccessible || overrideOffline) && (!snackBarShown || v != view)) { if (v != null && (!networkIsAccessible || overrideOffline) && (!snackBarShown || v != view)) {
view = v view = v
@ -43,3 +42,23 @@ fun Context.isNetworkAccessible(v: View?, overrideOffline: Boolean = false): Boo
} }
return if(overrideOffline) overrideOffline else networkIsAccessible return if(overrideOffline) overrideOffline else networkIsAccessible
} }
fun isNetworkAvailable(context: Context): Boolean {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val network = connectivityManager.activeNetwork ?: return false
val networkCapabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
return when {
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) -> true
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
else -> false
}
} else {
val network = connectivityManager.activeNetworkInfo ?: return false
return network.isConnectedOrConnecting
}
}