From 46e723a2385ab9db1c2d1bc2ceaa186845f737a1 Mon Sep 17 00:00:00 2001 From: davidoskky Date: Sat, 25 Sep 2021 13:45:51 +0200 Subject: [PATCH] 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 --- app/build.gradle | 11 +- .../bou/readerforselfoss/HomeActivity.kt | 484 +++++------------- .../adapters/ItemCardAdapter.kt | 1 - .../adapters/ItemListAdapter.kt | 18 - .../readerforselfoss/adapters/ItemsAdapter.kt | 171 +------ .../api/selfoss/SelfossApi.kt | 52 +- .../api/selfoss/SelfossFetching.kt | 134 +++++ .../api/selfoss/SelfossModels.kt | 2 +- .../api/selfoss/SelfossService.kt | 14 +- .../readerforselfoss/background/background.kt | 160 +++--- .../persistence/dao/ActionsDao.kt | 2 +- .../persistence/dao/ItemsDao.kt | 10 +- .../bou/readerforselfoss/utils/SharedItems.kt | 261 +++++++++- .../utils/network/NetworkUtils.kt | 27 +- 14 files changed, 663 insertions(+), 684 deletions(-) create mode 100644 app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/SelfossFetching.kt diff --git a/app/build.gradle b/app/build.gradle index e058e99..e3615c2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -82,6 +82,9 @@ android { dimension "build" } } + kotlinOptions { + jvmTarget = '1.8' + } } dependencies { @@ -113,10 +116,13 @@ dependencies { transitive = true } + // Async + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9' + // 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.retrofit2:converter-gson:2.3.0' + implementation 'com.squareup.retrofit2:converter-gson:2.9.0' implementation 'com.burgstaller:okhttp-digest:1.12' // Material-ish things @@ -148,6 +154,7 @@ dependencies { implementation "androidx.lifecycle:lifecycle-common-java8:2.3.0-rc01" 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" implementation "android.arch.work:work-runtime-ktx:$work_version" diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/HomeActivity.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/HomeActivity.kt index 3c2c4a8..65d3cd5 100644 --- a/app/src/main/java/apps/amine/bou/readerforselfoss/HomeActivity.kt +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/HomeActivity.kt @@ -18,6 +18,7 @@ import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.SearchView import androidx.core.view.MenuItemCompat +import androidx.core.view.doOnNextLayout import androidx.recyclerview.widget.* import androidx.room.Room 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.removeBadge 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.network.isNetworkAccessible import apps.amine.bou.readerforselfoss.utils.persistence.toEntity @@ -58,7 +58,6 @@ import com.ashokvarma.bottomnavigation.BottomNavigationItem import com.ashokvarma.bottomnavigation.TextBadgeItem import com.bumptech.glide.Glide import com.ftinc.scoop.Scoop -import com.mikepenz.aboutlibraries.Libs import com.mikepenz.aboutlibraries.LibsBuilder import com.mikepenz.materialdrawer.Drawer 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.PrimaryDrawerItem import com.mikepenz.materialdrawer.model.SecondaryDrawerItem +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import retrofit2.Call import retrofit2.Callback import retrofit2.Response -import java.text.SimpleDateFormat import java.util.concurrent.TimeUnit import kotlin.concurrent.thread @@ -129,10 +130,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { 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 offlineShortcut: Boolean = false @@ -213,7 +210,11 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { allItems = ArrayList() lastFetchDone = false handleDrawerItems() - getElementsAccordingToTab() + CoroutineScope(Dispatchers.Main).launch { + refreshFocusedItems(applicationContext, api, db) + getElementsAccordingToTab() + binding.swipeRefreshLayout.isRefreshing = false + } } val simpleItemTouchCallback = @@ -225,7 +226,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder ): Int = - if (elementsShown != UNREAD_SHOWN && elementsShown != READ_SHOWN) { + if (elementsShown == FAV_SHOWN) { 0 } else { super.getSwipeDirs( @@ -247,16 +248,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { if (i != null) { val adapter = binding.recyclerView.adapter as ItemsAdapter<*> - val wasItemUnread = adapter.unreadItemStatusAtIndex(position) - adapter.handleItemAtIndex(position) - if (wasItemUnread) { - badgeNew-- - } else { - badgeNew++ - } - reloadBadgeContent() val tagHashes = i.tags.tags.split(",").map { it.longHash() } @@ -392,56 +385,14 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { } private fun getAndStoreAllItems() { - api.allNewItems().enqueue(object : Callback> { - override fun onFailure(call: Call>, t: Throwable) { - } - - override fun onResponse( - call: Call>, - response: Response> - ) { - enqueueArticles(response, true) - } - }) - - api.allReadItems().enqueue(object : Callback> { - override fun onFailure(call: Call>, t: Throwable) { - } - - override fun onResponse( - call: Call>, - response: Response> - ) { - enqueueArticles(response, false) - } - }) - - api.allStarredItems().enqueue(object : Callback> { - override fun onFailure(call: Call>, t: Throwable) { - } - - override fun onResponse( - call: Call>, - response: Response> - ) { - enqueueArticles(response, false) - } - }) - } - - private fun enqueueArticles(response: Response>, clearDatabase: Boolean) { - thread { - if (response.body() != null) { - val apiItems = (response.body() as ArrayList).filter { - maybeTagFilter != null || filter(it.tags.tags) - } as ArrayList - if (clearDatabase) { - db.itemsDao().deleteAllItems() - } - db.itemsDao() - .insertAllItems(*(apiItems.map { it.toEntity() }).toTypedArray()) - } - } + CoroutineScope(Dispatchers.Main).launch { + binding.swipeRefreshLayout.isRefreshing = true + getAndStoreAllItems(applicationContext ,api, db) + this@HomeActivity.isNetworkAccessible(this@HomeActivity.findViewById(R.id.coordLayout), offlineShortcut) + handleListResult() + binding.swipeRefreshLayout.isRefreshing = false + SharedItems.updateDatabase(db) + } } override fun onStop() { @@ -461,6 +412,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { displayAccountHeader = sharedPref.getBoolean("account_header_displaying", false) infiniteScroll = sharedPref.getBoolean("infinite_loading", false) itemsCaching = sharedPref.getBoolean("items_caching", false) + SharedItems.itemsCaching = itemsCaching updateSources = sharedPref.getBoolean("update_sources", true) markOnScroll = sharedPref.getBoolean("mark_on_scroll", false) hiddenTags = if (sharedPref.getString("hidden_tags", "")!!.isNotEmpty()) { @@ -597,7 +549,9 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { .withOnDrawerItemClickListener { _, _, _ -> allItems = ArrayList() maybeTagFilter = it + SharedItems.tagFilter = it.tag getElementsAccordingToTab() + fetchOnEmptyList() false } if (it.unread > 0) { @@ -648,7 +602,9 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { .withOnDrawerItemClickListener { _, _, _ -> allItems = ArrayList() maybeTagFilter = it + SharedItems.tagFilter = it.tag getElementsAccordingToTab() + fetchOnEmptyList() false } if (it.unread > 0) { @@ -680,7 +636,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { .withOnDrawerItemClickListener { _, _, _ -> allItems = ArrayList() maybeSourceFilter = tag + SharedItems.sourceIDFilter = tag.id.toLong() + SharedItems.sourceFilter = tag.title getElementsAccordingToTab() + fetchOnEmptyList() false } if (tag.getIcon(this@HomeActivity).isNotBlank()) { @@ -710,8 +669,12 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { .withOnDrawerItemClickListener { _, _, _ -> allItems = ArrayList() maybeSourceFilter = null + SharedItems.sourceFilter = null + SharedItems.sourceIDFilter = null maybeTagFilter = null + SharedItems.tagFilter = null getElementsAccordingToTab() + fetchOnEmptyList() false } ) @@ -900,7 +863,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS binding.recyclerView.layoutManager = layoutManager } - } else { } } } @@ -933,69 +895,28 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { offset = 0 lastFetchDone = false - if (itemsCaching) { + elementsShown = position + 1 + getElementsAccordingToTab() + binding.recyclerView.scrollToPosition(0) - 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 (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 - } - } + fetchOnEmptyList() } }) } + private fun fetchOnEmptyList() { + binding.recyclerView.doOnNextLayout { + if (SharedItems.focusedItems.size - 1 == getLastVisibleItem()) { + getElementsAccordingToTab(true) + } + } + } + private fun handleInfiniteScroll() { recyclerViewScrollListener = object : RecyclerView.OnScrollListener() { override fun onScrolled(localRecycler: RecyclerView, dx: Int, dy: Int) { if (dy > 0) { - val manager = binding.recyclerView.layoutManager - val lastVisibleItem: Int = when (manager) { - is StaggeredGridLayoutManager -> manager.findLastCompletelyVisibleItemPositions( - null - ).last() - is GridLayoutManager -> manager.findLastCompletelyVisibleItemPosition() - else -> 0 - } + val lastVisibleItem = getLastVisibleItem() if (lastVisibleItem == (items.size - 1) && items.size < maxItemNumber()) { getElementsAccordingToTab(appendResults = true) @@ -1008,13 +929,22 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { 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() = if (items.isEmpty()) { binding.emptyText.visibility = View.VISIBLE - binding.recyclerView.visibility = View.GONE } else { binding.emptyText.visibility = View.GONE - binding.recyclerView.visibility = View.VISIBLE } private fun getElementsAccordingToTab( @@ -1030,155 +960,52 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { } } - offset = if (appendResults && offsetOverride === null) { - (offset + itemsNumber) + offset = if (appendResults) { + SharedItems.focusedItems.size - 1 } else { - offsetOverride ?: 0 + 0 } firstVisible = if (appendResults) firstVisible else 0 - if (itemsCaching) { - - 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> - ) { - fun handleItemsResponse(response: Response>) { - val shouldUpdate = (response.body()?.toSet() != items.toSet()) - if (response.body() != null) { - if (shouldUpdate) { - getAndStoreAllItems() - items = response.body() as ArrayList - items = items.filter { - maybeTagFilter != null || filter(it.tags.tags) - } as ArrayList - - 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> { - override fun onResponse( - call: Call>, - response: Response> - ) { - handleItemsResponse(response) - } - - override fun onFailure(call: Call>, t: Throwable) { - binding.swipeRefreshLayout.isRefreshing = false - Toast.makeText( - this@HomeActivity, - toastMessage, - Toast.LENGTH_SHORT - ).show() - } - }) - } else { - binding.swipeRefreshLayout.post { binding.swipeRefreshLayout.isRefreshing = false } - } + doGetAccordingToTab() } private fun getUnRead(appendResults: Boolean = false) { - elementsShown = UNREAD_SHOWN - doCallTo(appendResults, R.string.cant_get_new_elements) { t, id, f -> - api.newItems( - t, - id, - f, - itemsNumber, - offset - ) + CoroutineScope(Dispatchers.Main).launch { + if (appendResults || !SharedItems.fetchedUnread) { + binding.swipeRefreshLayout.isRefreshing = true + getUnreadItems(applicationContext, api, db, offset) + binding.swipeRefreshLayout.isRefreshing = false + } + SharedItems.getUnRead() + items = SharedItems.focusedItems + handleListResult() } } private fun getRead(appendResults: Boolean = false) { - elementsShown = READ_SHOWN - doCallTo(appendResults, R.string.cant_get_read) { t, id, f -> - api.readItems( - t, - id, - f, - itemsNumber, - offset - ) + CoroutineScope(Dispatchers.Main).launch { + if (appendResults || !SharedItems.fetchedAll) { + binding.swipeRefreshLayout.isRefreshing = true + getReadItems(applicationContext, api, db, offset) + binding.swipeRefreshLayout.isRefreshing = false + } + SharedItems.getAll() + items = SharedItems.focusedItems + handleListResult() } } private fun getStarred(appendResults: Boolean = false) { - elementsShown = FAV_SHOWN - doCallTo(appendResults, R.string.cant_get_favs) { t, id, f -> - api.starredItems( - t, - id, - f, - itemsNumber, - offset - ) + CoroutineScope(Dispatchers.Main).launch { + if (appendResults || !SharedItems.fetchedStarred) { + binding.swipeRefreshLayout.isRefreshing = true + getStarredItems(applicationContext, api, db, offset) + binding.swipeRefreshLayout.isRefreshing = false + } + SharedItems.getStarred() + items = SharedItems.focusedItems + handleListResult() } } @@ -1238,59 +1065,35 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { } binding.recyclerView.adapter = recyclerAdapter } else { - if (!appendResults) { (recyclerAdapter as ItemsAdapter<*>).updateAllItems() - } else { - (recyclerAdapter as ItemsAdapter<*>).addItemsAtEnd(items) - } } reloadBadges() + mayBeEmpty() } private fun reloadBadges() { - if (this@HomeActivity.isNetworkAccessible(null, offlineShortcut) && (displayUnreadCount || displayAllCount)) { - api.stats.enqueue(object : Callback { - override fun onResponse(call: Call, response: Response) { - if (response.body() != null) { - - badgeNew = response.body()!!.unread - badgeAll = response.body()!!.total - badgeFavs = response.body()!!.starred - reloadBadgeContent() - } - } - - override fun onFailure(call: Call, t: Throwable) { - } - }) - } else { - reloadBadgeContent(succeeded = false) + if (displayUnreadCount || displayAllCount) { + CoroutineScope(Dispatchers.Main).launch { + reloadBadges(applicationContext, api) + reloadBadgeContent() + } } } - private fun reloadBadgeContent(succeeded: Boolean = true) { - if (succeeded) { - if (displayUnreadCount) { - tabNewBadge - .setText(badgeNew.toString()) - .maybeShow() - } - if (displayAllCount) { - tabArchiveBadge - .setText(badgeAll.toString()) - .maybeShow() - tabStarredBadge - .setText(badgeFavs.toString()) - .maybeShow() - } else { - tabArchiveBadge.removeBadge() - tabStarredBadge.removeBadge() - } - } else { - tabNewBadge.removeBadge() - tabArchiveBadge.removeBadge() - tabStarredBadge.removeBadge() + private fun reloadBadgeContent() { + if (displayUnreadCount) { + tabNewBadge + .setText(SharedItems.badgeUnread.toString()) + .maybeShow() + } + if (displayAllCount) { + tabArchiveBadge + .setText(SharedItems.badgeAll.toString()) + .maybeShow() + tabStarredBadge + .setText(SharedItems.badgeStarred.toString()) + .maybeShow() } } @@ -1310,14 +1113,18 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { override fun onQueryTextChange(p0: String?): Boolean { if (p0.isNullOrBlank()) { maybeSearchFilter = null + SharedItems.searchFilter = null getElementsAccordingToTab() + fetchOnEmptyList() } return false } override fun onQueryTextSubmit(p0: String?): Boolean { maybeSearchFilter = p0 + SharedItems.searchFilter = p0 getElementsAccordingToTab() + fetchOnEmptyList() return false } @@ -1387,64 +1194,33 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { R.id.readAll -> { if (elementsShown == UNREAD_SHOWN) { needsConfirmation(R.string.readAll, R.string.markall_dialog_message) { - binding.swipeRefreshLayout.isRefreshing = false - val ids = allItems.map { it.id } - val itemsByTag: Map = - allItems.flattenTags() - .groupBy { it.tags.tags.longHash() } - .map { it.key to it.value.size } - .toMap() + binding.swipeRefreshLayout.isRefreshing = true - if (ids.isNotEmpty() && this@HomeActivity.isNetworkAccessible(null, offlineShortcut)) { - api.readAll(ids).enqueue(object : Callback { - override fun onResponse( - call: Call, - response: Response - ) { - if (response.body() != null && response.body()!!.isSuccess) { - Toast.makeText( - this@HomeActivity, - R.string.all_posts_read, - Toast.LENGTH_SHORT - ).show() - tabNewBadge.removeBadge() + if (this@HomeActivity.isNetworkAccessible(null, offlineShortcut)) { + CoroutineScope(Dispatchers.Main).launch { + val success = readAll(applicationContext, api, db) + if (success) { + Toast.makeText( + this@HomeActivity, + R.string.all_posts_read, + Toast.LENGTH_SHORT + ).show() + tabNewBadge.removeBadge() - handleDrawerItems() + handleDrawerItems() - getElementsAccordingToTab() - } else { - Toast.makeText( - this@HomeActivity, - R.string.all_posts_not_read, - Toast.LENGTH_SHORT - ).show() - - - } - - binding.swipeRefreshLayout.isRefreshing = false - } - - override fun onFailure(call: Call, t: Throwable) { + getElementsAccordingToTab() + } else { Toast.makeText( this@HomeActivity, R.string.all_posts_not_read, Toast.LENGTH_SHORT ).show() - binding.swipeRefreshLayout.isRefreshing = false } - }) - items = ArrayList() - allItems = ArrayList() + handleListResult() + binding.swipeRefreshLayout.isRefreshing = false + } } - if (items.isEmpty()) { - Toast.makeText( - this@HomeActivity, - R.string.nothing_here, - Toast.LENGTH_SHORT - ).show() - } - handleListResult() } } return true @@ -1458,10 +1234,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { private fun maxItemNumber(): Int = when (elementsShown) { - UNREAD_SHOWN -> badgeNew - READ_SHOWN -> badgeAll - FAV_SHOWN -> badgeFavs - else -> badgeNew // if !elementsShown then unread are fetched. + UNREAD_SHOWN -> SharedItems.badgeUnread + READ_SHOWN -> SharedItems.badgeAll + FAV_SHOWN -> SharedItems.badgeStarred + else -> SharedItems.badgeUnread // if !elementsShown then unread are fetched. } private fun updateItems(adapterItems: ArrayList) { @@ -1505,7 +1281,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { } if (this@HomeActivity.isNetworkAccessible(null, offlineShortcut)) { - thread { + CoroutineScope(Dispatchers.Main).launch { val actions = db.actionsDao().actions() actions.forEach { action -> diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/adapters/ItemCardAdapter.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/adapters/ItemCardAdapter.kt index 4b5897c..6ab58c9 100644 --- a/app/src/main/java/apps/amine/bou/readerforselfoss/adapters/ItemCardAdapter.kt +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/adapters/ItemCardAdapter.kt @@ -2,7 +2,6 @@ package apps.amine.bou.readerforselfoss.adapters import android.app.Activity import android.content.Context -import androidx.cardview.widget.CardView import androidx.recyclerview.widget.RecyclerView import android.view.LayoutInflater import android.view.View diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/adapters/ItemListAdapter.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/adapters/ItemListAdapter.kt index 477d3b5..76f5455 100644 --- a/app/src/main/java/apps/amine/bou/readerforselfoss/adapters/ItemListAdapter.kt +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/adapters/ItemListAdapter.kt @@ -2,22 +2,12 @@ package apps.amine.bou.readerforselfoss.adapters import android.app.Activity import android.content.Context -import androidx.constraintlayout.widget.ConstraintLayout 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.MotionEvent -import android.view.View import android.view.ViewGroup -import android.widget.TextView -import android.widget.Toast 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.SelfossApi -import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse import apps.amine.bou.readerforselfoss.databinding.ListItemBinding import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase 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.glide.bitmapCenterCrop 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.shareLink import apps.amine.bou.readerforselfoss.utils.sourceAndDateText import apps.amine.bou.readerforselfoss.utils.toTextDrawableString import com.amulyakhare.textdrawable.TextDrawable 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 class ItemListAdapter( diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/adapters/ItemsAdapter.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/adapters/ItemsAdapter.kt index 68d132c..8d803aa 100644 --- a/app/src/main/java/apps/amine/bou/readerforselfoss/adapters/ItemsAdapter.kt +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/adapters/ItemsAdapter.kt @@ -2,26 +2,16 @@ package apps.amine.bou.readerforselfoss.adapters import android.app.Activity import android.graphics.Color -import com.google.android.material.snackbar.Snackbar -import androidx.recyclerview.widget.RecyclerView import android.widget.TextView -import android.widget.Toast +import androidx.recyclerview.widget.RecyclerView 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.SuccessResponse 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.utils.Config import apps.amine.bou.readerforselfoss.utils.SharedItems -import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible -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 +import com.google.android.material.snackbar.Snackbar abstract class ItemsAdapter : RecyclerView.Adapter() { abstract var items: ArrayList @@ -47,34 +37,11 @@ abstract class ItemsAdapter : RecyclerView.Adapte Snackbar.LENGTH_LONG ) .setAction(R.string.undo_string) { - items.add(position, i) - thread { - db.itemsDao().insertAllItems(i.toEntity()) - } - notifyItemInserted(position) - updateItems(items) - - if (app.isNetworkAccessible(null)) { - api.unmarkItem(i.id).enqueue(object : Callback { - override fun onResponse( - call: Call, - response: Response - ) { - } - - override fun onFailure(call: Call, t: Throwable) { - items.remove(i) - thread { - db.itemsDao().delete(i.toEntity()) - } - notifyItemRemoved(position) - updateItems(items) - } - }) + SharedItems.unreadItem(app, api, db, i) + if (SharedItems.displayedItems == "unread") { + addItemAtIndex(i, position) } else { - thread { - db.actionsDao().insertAllActions(ActionEntity(i.id, false, true, false, false)) - } + notifyItemChanged(position) } } @@ -84,7 +51,7 @@ abstract class ItemsAdapter : RecyclerView.Adapte s.show() } - private fun markSnackbar(i: Item, position: Int) { + private fun markSnackbar(position: Int) { val s = Snackbar .make( app.findViewById(R.id.coordLayout), @@ -92,34 +59,13 @@ abstract class ItemsAdapter : RecyclerView.Adapte Snackbar.LENGTH_LONG ) .setAction(R.string.undo_string) { - items.add(position, i) - thread { - db.itemsDao().delete(i.toEntity()) - } - notifyItemInserted(position) - updateItems(items) - - if (app.isNetworkAccessible(null)) { - api.markItem(i.id).enqueue(object : Callback { - override fun onResponse( - call: Call, - response: Response - ) { - } - - override fun onFailure(call: Call, t: Throwable) { - items.remove(i) - thread { - db.itemsDao().insertAllItems(i.toEntity()) - } - notifyItemRemoved(position) - updateItems(items) - } - }) + SharedItems.readItem(app, api, db, items[position]) + items = SharedItems.focusedItems + if (SharedItems.displayedItems == "unread") { + notifyItemRemoved(position) + updateItems(items) } else { - thread { - db.actionsDao().insertAllActions(ActionEntity(i.id, true, false, false, false)) - } + notifyItemChanged(position) } } @@ -130,99 +76,30 @@ abstract class ItemsAdapter : RecyclerView.Adapte } fun handleItemAtIndex(position: Int) { - if (unreadItemStatusAtIndex(position)) { + if (SharedItems.unreadItemStatusAtIndex(position)) { readItemAtIndex(position) } else { unreadItemAtIndex(position) } } - fun unreadItemStatusAtIndex(position: Int): Boolean { - return items[position].unread - } - private fun readItemAtIndex(position: Int) { val i = items[position] - items.remove(i) - notifyItemRemoved(position) - updateItems(items) - - thread { - db.itemsDao().delete(i.toEntity()) - } - - if (app.isNetworkAccessible(null)) { - api.markItem(i.id).enqueue(object : Callback { - override fun onResponse( - call: Call, - response: Response - ) { - - unmarkSnackbar(i, position) - } - - override fun onFailure(call: Call, 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()) - } - } - }) + SharedItems.readItem(app, api, db, i) + if (SharedItems.displayedItems == "unread") { + items.remove(i) + notifyItemRemoved(position) + updateItems(items) } else { - thread { - db.actionsDao().insertAllActions(ActionEntity(i.id, true, false, false, false)) - } + notifyItemChanged(position) } + unmarkSnackbar(i, position) } private fun unreadItemAtIndex(position: Int) { - val i = items[position] - items.remove(i) - notifyItemRemoved(position) - updateItems(items) - - thread { - db.itemsDao().insertAllItems(i.toEntity()) - } - - if (app.isNetworkAccessible(null)) { - api.unmarkItem(i.id).enqueue(object : Callback { - override fun onResponse( - call: Call, - response: Response - ) { - - markSnackbar(i, position) - } - - override fun onFailure(call: Call, 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)) - } - } + SharedItems.unreadItem(app, api, db, items[position]) + notifyItemChanged(position) + markSnackbar(position) } fun addItemAtIndex(item: Item, position: Int) { diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/SelfossApi.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/SelfossApi.kt index 038bc01..645cc0e 100644 --- a/app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/SelfossApi.kt +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/SelfossApi.kt @@ -3,6 +3,7 @@ package apps.amine.bou.readerforselfoss.api.selfoss import android.app.Activity import android.content.Context import apps.amine.bou.readerforselfoss.utils.Config +import apps.amine.bou.readerforselfoss.utils.SharedItems import apps.amine.bou.readerforselfoss.utils.getUnsafeHttpClient import com.burgstaller.okhttp.AuthenticationCacheInterceptor import com.burgstaller.okhttp.CachingAuthenticatorDecorator @@ -142,54 +143,50 @@ class SelfossApi( fun login(): Call = service.loginToSelfoss(config.userLogin, config.userPassword) - fun readItems( - tag: String?, - sourceId: Long?, - search: String?, + suspend fun readItems( itemsNumber: Int, offset: Int - ): Call> = - getItems("read", tag, sourceId, search, itemsNumber, offset) + ): retrofit2.Response> = + getItems("read", SharedItems.tagFilter, SharedItems.sourceIDFilter, SharedItems.searchFilter, itemsNumber, offset) - fun newItems( - tag: String?, - sourceId: Long?, - search: String?, + suspend fun newItems( itemsNumber: Int, offset: Int - ): Call> = - getItems("unread", tag, sourceId, search, itemsNumber, offset) + ): retrofit2.Response> = + getItems("unread", SharedItems.tagFilter, SharedItems.sourceIDFilter, SharedItems.searchFilter, itemsNumber, offset) - fun starredItems( - tag: String?, - sourceId: Long?, - search: String?, + suspend fun starredItems( itemsNumber: Int, offset: Int - ): Call> = - getItems("starred", tag, sourceId, search, itemsNumber, offset) + ): retrofit2.Response> = + getItems("starred", SharedItems.tagFilter, SharedItems.sourceIDFilter, SharedItems.searchFilter, itemsNumber, offset) fun allItems(): Call> = service.allItems(userName, password) - fun allNewItems(): Call> = + suspend fun allNewItems(): retrofit2.Response> = getItems("unread", null, null, null, 200, 0) - fun allReadItems(): Call> = + suspend fun allReadItems(): retrofit2.Response> = getItems("read", null, null, null, 200, 0) - fun allStarredItems(): Call> = - getItems("read", null, null, null, 200, 0) + suspend fun allStarredItems(): retrofit2.Response> = + getItems("read", null, null, null, 200, 0) - private fun getItems( + private suspend fun getItems( type: String, tag: String?, sourceId: Long?, search: String?, items: Int, offset: Int - ): Call> = - service.getItems(type, tag, sourceId, search, userName, password, items, offset) + ): retrofit2.Response> = + service.getItems(type, tag, sourceId, search, null, userName, password, items, offset) + + suspend fun updateItems( + updatedSince: String + ): retrofit2.Response> = + service.getItems("read", null, null, null, updatedSince, userName, password, 200, 0) fun markItem(itemId: String): Call = service.markAsRead(itemId, userName, password) @@ -197,7 +194,7 @@ class SelfossApi( fun unmarkItem(itemId: String): Call = service.unmarkAsRead(itemId, userName, password) - fun readAll(ids: List): Call = + suspend fun readAll(ids: List): SuccessResponse = service.markAllAsRead(ids, userName, password) fun starrItem(itemId: String): Call = @@ -206,8 +203,7 @@ class SelfossApi( fun unstarrItem(itemId: String): Call = service.unstarr(itemId, userName, password) - val stats: Call - get() = service.stats(userName, password) + suspend fun stats(): retrofit2.Response = service.stats(userName, password) val tags: Call> get() = service.tags(userName, password) diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/SelfossFetching.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/SelfossFetching.kt new file mode 100644 index 0000000..ff14180 --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/SelfossFetching.kt @@ -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) + 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>, db: AppDatabase, clearDatabase: Boolean) { + if (response.isSuccessful) { + if (clearDatabase) { + CoroutineScope(Dispatchers.IO).launch { + SharedItems.clearDBItems(db) + } + } + val allItems = response.body() as ArrayList + SharedItems.appendNewItems(allItems) + } +} \ No newline at end of file diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/SelfossModels.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/SelfossModels.kt index 3bf313f..18caa15 100644 --- a/app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/SelfossModels.kt +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/SelfossModels.kt @@ -88,7 +88,7 @@ data class Item( @SerializedName("datetime") val datetime: String, @SerializedName("title") val title: String, @SerializedName("content") val content: String, - @SerializedName("unread") val unread: Boolean, + @SerializedName("unread") var unread: Boolean, @SerializedName("starred") var starred: Boolean, @SerializedName("thumbnail") val thumbnail: String?, @SerializedName("icon") val icon: String?, diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/SelfossService.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/SelfossService.kt index 371213b..00274d4 100644 --- a/app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/SelfossService.kt +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/SelfossService.kt @@ -1,6 +1,7 @@ package apps.amine.bou.readerforselfoss.api.selfoss import retrofit2.Call +import retrofit2.Response import retrofit2.http.DELETE import retrofit2.http.Field import retrofit2.http.FormUrlEncoded @@ -16,16 +17,17 @@ internal interface SelfossService { fun loginToSelfoss(@Query("username") username: String, @Query("password") password: String): Call @GET("items") - fun getItems( + suspend fun getItems( @Query("type") type: String, @Query("tag") tag: String?, @Query("source") source: Long?, @Query("search") search: String?, + @Query("updatedsince") updatedSince: String?, @Query("username") username: String, @Query("password") password: String, @Query("items") items: Int, @Query("offset") offset: Int - ): Call> + ): Response> @GET("items") fun allItems( @@ -51,11 +53,11 @@ internal interface SelfossService { @FormUrlEncoded @POST("mark") - fun markAllAsRead( + suspend fun markAllAsRead( @Field("ids[]") ids: List, @Query("username") username: String, @Query("password") password: String - ): Call + ): SuccessResponse @Headers("Content-Type: application/x-www-form-urlencoded") @POST("starr/{id}") @@ -74,10 +76,10 @@ internal interface SelfossService { ): Call @GET("stats") - fun stats( + suspend fun stats( @Query("username") username: String, @Query("password") password: String - ): Call + ): Response @GET("tags") fun tags( diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/background/background.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/background/background.kt index 7566c08..50cfd56 100644 --- a/app/src/main/java/apps/amine/bou/readerforselfoss/background/background.kt +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/background/background.kt @@ -13,16 +13,19 @@ import androidx.work.Worker import androidx.work.WorkerParameters import apps.amine.bou.readerforselfoss.MainActivity 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.getAndStoreAllItems import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase 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_2_3 import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_3_4 import apps.amine.bou.readerforselfoss.utils.Config -import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible -import apps.amine.bou.readerforselfoss.utils.persistence.toEntity +import apps.amine.bou.readerforselfoss.utils.SharedItems +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.Callback import retrofit2.Response @@ -33,123 +36,99 @@ import kotlin.concurrent.thread class LoadingWorker(val context: Context, params: WorkerParameters) : Worker(context, params) { lateinit var db: AppDatabase - override fun doWork(): Result { - if (context.isNetworkAccessible(null)) { +override fun doWork(): Result { + 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 = - applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + if (isNetworkAvailable(context)) { - 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) + CoroutineScope(Dispatchers.IO).launch { + val notificationManager = + applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - 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 = - this.context.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE) - val sharedPref = PreferenceManager.getDefaultSharedPreferences(this.context) - val notifyNewItems = sharedPref.getBoolean("notify_new_items", false) + notificationManager.notify(1, notification.build()) - db = Room.databaseBuilder( - applicationContext, - AppDatabase::class.java, "selfoss-database" - ).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).addMigrations(MIGRATION_3_4).build() + val notifyNewItems = sharedPref.getBoolean("notify_new_items", false) - val api = SelfossApi( - this.context, - null, - settings.getBoolean("isSelfSignedCert", false), - sharedPref.getString("api_timeout", "-1")!!.toLong() - ) + db = Room.databaseBuilder( + applicationContext, + AppDatabase::class.java, "selfoss-database" + ).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3) + .addMigrations(MIGRATION_3_4).build() - api.allNewItems().enqueue(object : Callback> { - override fun onFailure(call: Call>, t: Throwable) { - Timer("", false).schedule(4000) { - notificationManager.cancel(1) - } - } - - override fun onResponse( - call: Call>, - response: Response> - ) { - storeItems(response, true, notifyNewItems, notificationManager) - } - }) - api.allReadItems().enqueue(object : Callback> { - override fun onFailure(call: Call>, t: Throwable) { - Timer("", false).schedule(4000) { - notificationManager.cancel(1) - } - } - - override fun onResponse( - call: Call>, - response: Response> - ) { - storeItems(response, false, notifyNewItems, notificationManager) - } - }) - api.allStarredItems().enqueue(object : Callback> { - override fun onFailure(call: Call>, t: Throwable) { - Timer("", false).schedule(4000) { - notificationManager.cancel(1) - } - } - - override fun onResponse( - call: Call>, - response: Response> - ) { - storeItems(response, false, notifyNewItems, notificationManager) - } - }) - - thread { val actions = db.actionsDao().actions() actions.forEach { action -> when { - action.read -> doAndReportOnFail(api.markItem(action.articleId), action) - action.unread -> doAndReportOnFail(api.unmarkItem(action.articleId), action) - action.starred -> doAndReportOnFail(api.starrItem(action.articleId), action) + action.read -> doAndReportOnFail( + api.markItem(action.articleId), + action + ) + action.unread -> doAndReportOnFail( + api.unmarkItem(action.articleId), + action + ) + action.starred -> doAndReportOnFail( + api.starrItem(action.articleId), + action + ) action.unstarred -> doAndReportOnFail( api.unstarrItem(action.articleId), action ) } } + + getAndStoreAllItems(context, api, db) + SharedItems.updateDatabase(db) + storeItems(notifyNewItems, notificationManager) } } - return Result.success() } + return Result.success() +} - private fun storeItems(response: Response>, newItems: Boolean, notifyNewItems: Boolean, notificationManager: NotificationManager) { - thread { - if (response.body() != null) { - val apiItems = (response.body() as ArrayList) + private fun storeItems(notifyNewItems: Boolean, notificationManager: NotificationManager) { + CoroutineScope(Dispatchers.IO).launch { + val apiItems = SharedItems.items - if (newItems) { - db.itemsDao().deleteAllItems() - } - db.itemsDao() - .insertAllItems(*(apiItems.map { it.toEntity() }).toTypedArray()) val newSize = apiItems.filter { it.unread }.size - if (newItems && notifyNewItems && newSize > 0) { + if (notifyNewItems && newSize > 0) { val intent = Intent(context, MainActivity::class.java).apply { 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)) - .setContentText(context.getString(R.string.new_items_notification_text, newSize)) + .setContentText( + context.getString( + R.string.new_items_notification_text, + newSize + ) + ) .setPriority(PRIORITY_DEFAULT) .setChannelId(Config.newItemsChannelId) .setContentIntent(pendingIntent) @@ -161,7 +140,6 @@ class LoadingWorker(val context: Context, params: WorkerParameters) : Worker(con } } apiItems.map { it.preloadImages(context) } - } Timer("", false).schedule(4000) { notificationManager.cancel(1) } diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/persistence/dao/ActionsDao.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/persistence/dao/ActionsDao.kt index e18d452..055d9e2 100644 --- a/app/src/main/java/apps/amine/bou/readerforselfoss/persistence/dao/ActionsDao.kt +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/persistence/dao/ActionsDao.kt @@ -10,7 +10,7 @@ import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity @Dao interface ActionsDao { @Query("SELECT * FROM actions order by id asc") - fun actions(): List + suspend fun actions(): List @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertAllActions(vararg actions: ActionEntity) diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/persistence/dao/ItemsDao.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/persistence/dao/ItemsDao.kt index dd5d34b..b76ca14 100644 --- a/app/src/main/java/apps/amine/bou/readerforselfoss/persistence/dao/ItemsDao.kt +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/persistence/dao/ItemsDao.kt @@ -13,17 +13,17 @@ import androidx.room.Update @Dao interface ItemsDao { @Query("SELECT * FROM items order by id desc") - fun items(): List + suspend fun items(): List @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insertAllItems(vararg items: ItemEntity) + suspend fun insertAllItems(vararg items: ItemEntity) @Query("DELETE FROM items") - fun deleteAllItems() + suspend fun deleteAllItems() @Delete - fun delete(item: ItemEntity) + suspend fun delete(item: ItemEntity) @Update - fun updateItem(item: ItemEntity) + suspend fun updateItem(item: ItemEntity) } \ No newline at end of file diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/utils/SharedItems.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/utils/SharedItems.kt index 280eee9..32a9c34 100644 --- a/app/src/main/java/apps/amine/bou/readerforselfoss/utils/SharedItems.kt +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/utils/SharedItems.kt @@ -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.utils.persistence.toEntity 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.Callback import retrofit2.Response +import java.text.SimpleDateFormat import kotlin.concurrent.thread /* @@ -38,30 +43,170 @@ object SharedItems { set(value) { field = when { value < 0 -> 0 - value > focusedItems.size -> focusedItems.size + value > items.size -> items.size 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) { - if (focusedItems.contains(item)) { - position = focusedItems.indexOf(item) - readItemAtIndex(app, api, db) + var searchFilter: String? = null + var sourceIDFilter: Long? = null + var sourceFilter: String? = null + 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) { + val tmpItems = items + if (tmpItems != newItems) { + newItems.removeAll(tmpItems) + tmpItems.addAll(newItems) + items = tmpItems + + sortItems() + getFocusedItems() } } - fun readItemAtIndex(app: Context, api: SelfossApi, db: AppDatabase) { - val i = focusedItems[position] - var tmpItems = items - tmpItems.remove(i) - items = tmpItems - var tmpFocusedItems = focusedItems - tmpFocusedItems.remove(i) - focusedItems = tmpFocusedItems + fun refreshFocusedItems(newItems: ArrayList) { + val tmpItems = items + tmpItems.removeAll(focusedItems) - thread { - db.itemsDao().delete(i.toEntity()) + appendNewItems(newItems) + } + + 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 + } + if (searchFilter != null) { + tmpItems = tmpItems.filter { filterSearch(it) } as ArrayList + } + if (sourceFilter != null) { + tmpItems = tmpItems.filter { it.sourcetitle == sourceFilter } as ArrayList + } + 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 + filter() + } + + fun getRead() { + displayedItems = "read" + focusedItems = items.filter { item -> !item.unread } as ArrayList + filter() + } + + fun getStarred() { + displayedItems = "starred" + focusedItems = items.filter { item -> item.starred } as ArrayList + 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 + 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) { + 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)) { api.markItem(i.id).enqueue(object : Callback { @@ -70,7 +215,13 @@ object SharedItems { response: Response ) { - //unmarkSnackbar(i, position) + val tmpItems = items + tmpItems[position].unread = false + items = tmpItems + + resetDBItem(db) + getFocusedItems() + badgeUnread-- } override fun onFailure(call: Call, t: Throwable) { @@ -79,24 +230,82 @@ object SharedItems { app.getString(R.string.cant_mark_read), Toast.LENGTH_SHORT ).show() - tmpItems.add(position, i) - tmpFocusedItems.add(position, i) - items = tmpItems - focusedItems = tmpFocusedItems - - thread { - db.itemsDao().insertAllItems(i.toEntity()) - } } }) - } else { + } else if (itemsCaching) { thread { db.actionsDao().insertAllActions(ActionEntity(i.id, true, false, false, false)) } } - if (position > focusedItems.size) { + if (position > items.size) { 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 { + override fun onResponse( + call: Call, + response: Response + ) { + + val tmpItems = items + tmpItems[position].unread = true + items = tmpItems + + resetDBItem(db) + getFocusedItems() + badgeUnread++ + } + + override fun onFailure(call: Call, 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 + } } \ No newline at end of file diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/utils/network/NetworkUtils.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/utils/network/NetworkUtils.kt index a08c479..8b93a4a 100644 --- a/app/src/main/java/apps/amine/bou/readerforselfoss/utils/network/NetworkUtils.kt +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/utils/network/NetworkUtils.kt @@ -3,7 +3,8 @@ package apps.amine.bou.readerforselfoss.utils.network import android.content.Context import android.graphics.Color import android.net.ConnectivityManager -import android.net.NetworkInfo +import android.net.NetworkCapabilities +import android.os.Build import android.view.View import android.widget.TextView import apps.amine.bou.readerforselfoss.R @@ -14,9 +15,7 @@ var view: View? = null lateinit var s: Snackbar fun Context.isNetworkAccessible(v: View?, overrideOffline: Boolean = false): Boolean { - val cm = this.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - val activeNetwork: NetworkInfo? = cm.activeNetworkInfo - val networkIsAccessible = activeNetwork != null && activeNetwork.isConnectedOrConnecting + val networkIsAccessible = isNetworkAvailable(this) if (v != null && (!networkIsAccessible || overrideOffline) && (!snackBarShown || v != view)) { view = v @@ -42,4 +41,24 @@ fun Context.isNetworkAccessible(v: View?, overrideOffline: Boolean = false): Boo s.dismiss() } 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 + } } \ No newline at end of file