diff --git a/app/src/androidTest/java/org/wikipedia/database/AppDatabaseTests.kt b/app/src/androidTest/java/org/wikipedia/database/AppDatabaseTests.kt index de5a65d1a2e..b0d87ce03e6 100644 --- a/app/src/androidTest/java/org/wikipedia/database/AppDatabaseTests.kt +++ b/app/src/androidTest/java/org/wikipedia/database/AppDatabaseTests.kt @@ -5,10 +5,15 @@ import androidx.room.Room import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.count -import kotlinx.coroutines.flow.first -import org.hamcrest.CoreMatchers.* +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import org.hamcrest.CoreMatchers.equalTo +import org.hamcrest.CoreMatchers.notNullValue +import org.hamcrest.CoreMatchers.nullValue import org.hamcrest.MatcherAssert.assertThat import org.junit.After import org.junit.Before @@ -22,7 +27,7 @@ import org.wikipedia.search.db.RecentSearchDao import org.wikipedia.talk.db.TalkPageSeen import org.wikipedia.talk.db.TalkPageSeenDao import org.wikipedia.util.log.L -import java.util.* +import java.util.Date @RunWith(AndroidJUnit4::class) class AppDatabaseTests { @@ -100,8 +105,8 @@ class AppDatabaseTests { notificationDao.insertNotifications(notifications) - var enWikiList = notificationDao.getNotificationsByWiki(listOf("enwiki")).first() - val zhWikiList = notificationDao.getNotificationsByWiki(listOf("zhwiki")).first() + var enWikiList = notificationDao.getNotificationsByWiki(listOf("enwiki")) + val zhWikiList = notificationDao.getNotificationsByWiki(listOf("zhwiki")) assertThat(enWikiList, notNullValue()) assertThat(enWikiList.first().id, equalTo(123759827)) assertThat(zhWikiList.first().id, equalTo(2470933)) @@ -114,18 +119,18 @@ class AppDatabaseTests { notificationDao.updateNotification(firstEnNotification) // get updated item - enWikiList = notificationDao.getNotificationsByWiki(listOf("enwiki")).first() + enWikiList = notificationDao.getNotificationsByWiki(listOf("enwiki")) assertThat(enWikiList.first().id, equalTo(123759827)) assertThat(enWikiList.first().isUnread, equalTo(true)) notificationDao.deleteNotification(firstEnNotification) assertThat(notificationDao.getAllNotifications().size, equalTo(2)) - assertThat(notificationDao.getNotificationsByWiki(listOf("enwiki")).first().size, equalTo(1)) + assertThat(notificationDao.getNotificationsByWiki(listOf("enwiki")).size, equalTo(1)) - notificationDao.deleteNotification(notificationDao.getNotificationsByWiki(listOf("enwiki")).first().first()) - assertThat(notificationDao.getNotificationsByWiki(listOf("enwiki")).first().isEmpty(), equalTo(true)) + notificationDao.deleteNotification(notificationDao.getNotificationsByWiki(listOf("enwiki")).first()) + assertThat(notificationDao.getNotificationsByWiki(listOf("enwiki")).isEmpty(), equalTo(true)) - notificationDao.deleteNotification(notificationDao.getNotificationsByWiki(listOf("zhwiki")).first().first()) + notificationDao.deleteNotification(notificationDao.getNotificationsByWiki(listOf("zhwiki")).first()) assertThat(notificationDao.getAllNotifications().isEmpty(), equalTo(true)) } } diff --git a/app/src/androidTest/java/org/wikipedia/database/UpgradeFromPreRoomTest.kt b/app/src/androidTest/java/org/wikipedia/database/UpgradeFromPreRoomTest.kt index b5f4c2f141c..3866c9df340 100644 --- a/app/src/androidTest/java/org/wikipedia/database/UpgradeFromPreRoomTest.kt +++ b/app/src/androidTest/java/org/wikipedia/database/UpgradeFromPreRoomTest.kt @@ -4,7 +4,6 @@ import androidx.room.Room import androidx.room.testing.MigrationTestHelper import androidx.test.core.app.ApplicationProvider import androidx.test.platform.app.InstrumentationRegistry -import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.CoreMatchers.nullValue @@ -123,7 +122,7 @@ class UpgradeFromPreRoomTest(private val fromVersion: Int) { val historyEntry = historyDao.findEntryBy("ru.wikipedia.org", "ru", "Обама,_Барак")!! assertThat(historyEntry.displayTitle, equalTo("Обама, Барак")) - val talkPageSeen = talkPageSeenDao.getAll().first() + val talkPageSeen = talkPageSeenDao.getAll() if (fromVersion == 22) { assertThat(talkPageSeen.count(), equalTo(2)) assertThat(offlineObjectDao.getOfflineObject("https://en.wikipedia.org/api/rest_v1/page/summary/Joe_Biden")!!.path, equalTo("/data/user/0/org.wikipedia.dev/files/offline_files/481b1ef996728fd9994bd97ab19733d8")) diff --git a/app/src/main/java/org/wikipedia/WikipediaApp.kt b/app/src/main/java/org/wikipedia/WikipediaApp.kt index 61f13707879..1f70072bc6b 100644 --- a/app/src/main/java/org/wikipedia/WikipediaApp.kt +++ b/app/src/main/java/org/wikipedia/WikipediaApp.kt @@ -258,7 +258,9 @@ class WikipediaApp : Application() { Prefs.tempAccountCreateDay = 0L Prefs.tempAccountDialogShown = false SharedPreferenceCookieManager.instance.clearAllCookies() - AppDatabase.instance.notificationDao().deleteAll() + MainScope().launch { + AppDatabase.instance.notificationDao().deleteAll() + } FlowEventBus.post(LoggedOutEvent()) L.d("Logout complete.") } diff --git a/app/src/main/java/org/wikipedia/database/AppDatabase.kt b/app/src/main/java/org/wikipedia/database/AppDatabase.kt index b47f7830507..49d7837f052 100644 --- a/app/src/main/java/org/wikipedia/database/AppDatabase.kt +++ b/app/src/main/java/org/wikipedia/database/AppDatabase.kt @@ -354,7 +354,6 @@ abstract class AppDatabase : RoomDatabase() { MIGRATION_23_24, MIGRATION_24_25, MIGRATION_25_26, MIGRATION_26_27, MIGRATION_26_28, MIGRATION_27_28, MIGRATION_28_29, MIGRATION_29_30, MIGRATION_30_31) - .allowMainThreadQueries() // TODO: remove after resolving main thread issues in DAO classes .fallbackToDestructiveMigration(false) .build() } diff --git a/app/src/main/java/org/wikipedia/dataclient/okhttp/OfflineCacheInterceptor.kt b/app/src/main/java/org/wikipedia/dataclient/okhttp/OfflineCacheInterceptor.kt index 6a2072d5fbd..5f17c89ba7a 100644 --- a/app/src/main/java/org/wikipedia/dataclient/okhttp/OfflineCacheInterceptor.kt +++ b/app/src/main/java/org/wikipedia/dataclient/okhttp/OfflineCacheInterceptor.kt @@ -1,17 +1,35 @@ package org.wikipedia.dataclient.okhttp -import okhttp3.* +import kotlinx.coroutines.runBlocking +import okhttp3.Interceptor +import okhttp3.MediaType import okhttp3.MediaType.Companion.toMediaTypeOrNull -import okio.* +import okhttp3.Protocol +import okhttp3.Request +import okhttp3.Response +import okhttp3.ResponseBody +import okio.Buffer +import okio.BufferedSink +import okio.BufferedSource +import okio.Source +import okio.Timeout +import okio.buffer +import okio.sink +import okio.source +import okio.use import org.wikipedia.WikipediaApp import org.wikipedia.database.AppDatabase import org.wikipedia.offline.db.OfflineObject import org.wikipedia.util.StringUtil import org.wikipedia.util.UriUtil import org.wikipedia.util.log.L -import java.io.* +import java.io.BufferedReader +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream import java.io.IOException -import java.util.* +import java.io.InputStreamReader +import java.io.OutputStreamWriter class OfflineCacheInterceptor : Interceptor { @@ -49,7 +67,9 @@ class OfflineCacheInterceptor : Interceptor { throw networkException } } - val obj = AppDatabase.instance.offlineObjectDao().findObject(url, lang) + val obj = runBlocking { + AppDatabase.instance.offlineObjectDao().findObject(url, lang) + } if (obj == null) { L.w("Offline object not present in database.") throw networkException @@ -139,8 +159,8 @@ class OfflineCacheInterceptor : Interceptor { } ?: response } - private inner class CacheWritingSource constructor(private val source: BufferedSource, private val cacheSink: BufferedSink, - private val obj: OfflineObject, private val title: String) : Source { + private inner class CacheWritingSource(private val source: BufferedSource, private val cacheSink: BufferedSink, + private val obj: OfflineObject, private val title: String) : Source { private var cacheRequestClosed = false private var failed = false @@ -163,8 +183,9 @@ class OfflineCacheInterceptor : Interceptor { // The cache response is complete! cacheSink.close() if (!failed) { - // update the record in the database! - AppDatabase.instance.offlineObjectDao().addObject(obj.url, obj.lang, obj.path, title) + runBlocking { + AppDatabase.instance.offlineObjectDao().addObject(obj.url, obj.lang, obj.path, title) + } } } return -1 @@ -192,9 +213,9 @@ class OfflineCacheInterceptor : Interceptor { } } - private inner class CacheWritingResponseBody constructor(private val source: Source, - private val contentType: String?, - private val contentLength: Long) : ResponseBody() { + private inner class CacheWritingResponseBody(private val source: Source, + private val contentType: String?, + private val contentLength: Long) : ResponseBody() { override fun contentType(): MediaType? { return contentType?.toMediaTypeOrNull() } @@ -208,8 +229,8 @@ class OfflineCacheInterceptor : Interceptor { } } - private inner class CachedResponseBody constructor(private val file: File, - private val contentType: String?) : ResponseBody() { + private inner class CachedResponseBody(private val file: File, + private val contentType: String?) : ResponseBody() { override fun contentType(): MediaType? { return contentType?.toMediaTypeOrNull() } diff --git a/app/src/main/java/org/wikipedia/notifications/NotificationPollBroadcastReceiver.kt b/app/src/main/java/org/wikipedia/notifications/NotificationPollBroadcastReceiver.kt index 0c2ff32d79a..2a5688b0fb9 100644 --- a/app/src/main/java/org/wikipedia/notifications/NotificationPollBroadcastReceiver.kt +++ b/app/src/main/java/org/wikipedia/notifications/NotificationPollBroadcastReceiver.kt @@ -9,7 +9,10 @@ import android.os.SystemClock import androidx.annotation.StringRes import androidx.core.app.PendingIntentCompat import androidx.core.app.RemoteInput +import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.wikipedia.Constants import org.wikipedia.R @@ -146,26 +149,30 @@ class NotificationPollBroadcastReceiver : BroadcastReceiver() { return } - // The notifications that we need to display are those that don't exist in our db yet. - val notificationsToDisplay = notifications.filter { - AppDatabase.instance.notificationDao().getNotificationById(it.wiki, it.id) == null - } - AppDatabase.instance.notificationDao().insertNotifications(notificationsToDisplay) + MainScope().launch(CoroutineExceptionHandler { _, throwable -> + L.w(throwable) + }) { + // The notifications that we need to display are those that don't exist in our db yet. + val notificationsToDisplay = notifications.filter { + AppDatabase.instance.notificationDao().getNotificationById(it.wiki, it.id) == null + } + AppDatabase.instance.notificationDao().insertNotifications(notificationsToDisplay) - if (notificationsToDisplay.isNotEmpty()) { - Prefs.notificationUnreadCount = notificationsToDisplay.size - FlowEventBus.post(UnreadNotificationsEvent()) - } + if (notificationsToDisplay.isNotEmpty()) { + Prefs.notificationUnreadCount = notificationsToDisplay.size + FlowEventBus.post(UnreadNotificationsEvent()) + } - if (notificationsToDisplay.size > 2) { - // Record that there is an incoming notification to track/compare further actions on it. - NotificationPresenter.showMultipleUnread(context, notificationsToDisplay.size) - } else { - for (n in notificationsToDisplay) { + if (notificationsToDisplay.size > 2) { // Record that there is an incoming notification to track/compare further actions on it. - NotificationPresenter.showNotification(context, n, - dbWikiNameMap.getOrElse(n.wiki) { n.wiki }, - dbWikiSiteMap.getValue(n.wiki).languageCode) + NotificationPresenter.showMultipleUnread(context, notificationsToDisplay.size) + } else { + for (n in notificationsToDisplay) { + // Record that there is an incoming notification to track/compare further actions on it. + NotificationPresenter.showNotification(context, n, + dbWikiNameMap.getOrElse(n.wiki) { n.wiki }, + dbWikiSiteMap.getValue(n.wiki).languageCode) + } } } } diff --git a/app/src/main/java/org/wikipedia/notifications/NotificationRepository.kt b/app/src/main/java/org/wikipedia/notifications/NotificationRepository.kt index 4b8936a5ed9..a35937fed43 100644 --- a/app/src/main/java/org/wikipedia/notifications/NotificationRepository.kt +++ b/app/src/main/java/org/wikipedia/notifications/NotificationRepository.kt @@ -8,11 +8,7 @@ import org.wikipedia.notifications.db.NotificationDao class NotificationRepository(private val notificationDao: NotificationDao) { - fun getAllNotifications() = notificationDao.getAllNotifications() - - private fun insertNotifications(notifications: List) { - notificationDao.insertNotifications(notifications) - } + suspend fun getAllNotifications() = notificationDao.getAllNotifications() suspend fun updateNotification(notification: Notification) { notificationDao.updateNotification(notification) @@ -28,7 +24,7 @@ class NotificationRepository(private val notificationDao: NotificationDao) { var newContinueStr: String? = null val response = ServiceFactory.get(WikipediaApp.instance.wikiSite).getAllNotifications(wikiList, filter, continueStr) response.query?.notifications?.let { - insertNotifications(it.list.orEmpty()) + notificationDao.insertNotifications(it.list.orEmpty()) newContinueStr = it.continueStr } return newContinueStr diff --git a/app/src/main/java/org/wikipedia/notifications/NotificationViewModel.kt b/app/src/main/java/org/wikipedia/notifications/NotificationViewModel.kt index 2a999c3d11b..e054e68ad9c 100644 --- a/app/src/main/java/org/wikipedia/notifications/NotificationViewModel.kt +++ b/app/src/main/java/org/wikipedia/notifications/NotificationViewModel.kt @@ -42,7 +42,7 @@ class NotificationViewModel : ViewModel() { fetchAndSave() } - private fun filterAndPostNotifications() { + private suspend fun filterAndPostNotifications() { val pair = Pair(processList(notificationRepository.getAllNotifications()), !currentContinueStr.isNullOrEmpty()) _uiState.value = Resource.Success(pair) } diff --git a/app/src/main/java/org/wikipedia/notifications/db/NotificationDao.kt b/app/src/main/java/org/wikipedia/notifications/db/NotificationDao.kt index 33cb76a6278..68c5347c748 100644 --- a/app/src/main/java/org/wikipedia/notifications/db/NotificationDao.kt +++ b/app/src/main/java/org/wikipedia/notifications/db/NotificationDao.kt @@ -6,12 +6,11 @@ import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.Update -import kotlinx.coroutines.flow.Flow @Dao interface NotificationDao { @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insertNotifications(notifications: List) + suspend fun insertNotifications(notifications: List) @Update(onConflict = OnConflictStrategy.REPLACE) suspend fun updateNotification(notification: Notification) @@ -20,14 +19,14 @@ interface NotificationDao { suspend fun deleteNotification(notification: Notification) @Query("DELETE FROM Notification") - fun deleteAll() + suspend fun deleteAll() @Query("SELECT * FROM Notification") - fun getAllNotifications(): List + suspend fun getAllNotifications(): List @Query("SELECT * FROM Notification WHERE `wiki` IN (:wiki)") - fun getNotificationsByWiki(wiki: List): Flow> + suspend fun getNotificationsByWiki(wiki: List): List @Query("SELECT * FROM Notification WHERE `wiki` IN (:wiki) AND `id` IN (:id)") - fun getNotificationById(wiki: String, id: Long): Notification? + suspend fun getNotificationById(wiki: String, id: Long): Notification? } diff --git a/app/src/main/java/org/wikipedia/offline/db/OfflineObjectDao.kt b/app/src/main/java/org/wikipedia/offline/db/OfflineObjectDao.kt index 3fe1b036369..dd957cebd82 100644 --- a/app/src/main/java/org/wikipedia/offline/db/OfflineObjectDao.kt +++ b/app/src/main/java/org/wikipedia/offline/db/OfflineObjectDao.kt @@ -5,6 +5,7 @@ import androidx.room.Delete import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import androidx.room.Transaction import androidx.room.Update import org.wikipedia.database.AppDatabase import org.wikipedia.dataclient.WikiSite @@ -15,33 +16,31 @@ import java.io.File @Dao interface OfflineObjectDao { @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insertOfflineObject(obj: OfflineObject) + suspend fun insertOfflineObject(obj: OfflineObject) @Update(onConflict = OnConflictStrategy.REPLACE) - fun updateOfflineObject(obj: OfflineObject) + suspend fun updateOfflineObject(obj: OfflineObject) @Query("SELECT * FROM OfflineObject WHERE url = :url AND lang = :lang LIMIT 1") - fun getOfflineObject(url: String, lang: String): OfflineObject? + suspend fun getOfflineObject(url: String, lang: String): OfflineObject? @Query("SELECT * FROM OfflineObject WHERE url = :url LIMIT 1") - fun getOfflineObject(url: String): OfflineObject? + suspend fun getOfflineObject(url: String): OfflineObject? @Query("SELECT * FROM OfflineObject WHERE url LIKE '%/' || :urlFragment || '/%' LIMIT 1") - fun searchForOfflineObject(urlFragment: String): OfflineObject? + suspend fun searchForOfflineObject(urlFragment: String): OfflineObject? @Query("SELECT * FROM OfflineObject WHERE url LIKE '%' || :urlFragment || '%'") - fun searchForOfflineObjects(urlFragment: String): List + suspend fun searchForOfflineObjects(urlFragment: String): List @Query("SELECT * FROM OfflineObject WHERE usedByStr LIKE '%|' || :id || '|%'") - fun getFromUsedById(id: Long): List + suspend fun getFromUsedById(id: Long): List @Delete - fun deleteOfflineObject(obj: OfflineObject) + suspend fun deleteOfflineObject(obj: OfflineObject) - @Query("DELETE FROM OfflineObject") - fun deleteAll() - - fun findObject(url: String, lang: String?): OfflineObject? { + @Transaction + suspend fun findObject(url: String, lang: String?): OfflineObject? { var obj = if (lang.isNullOrEmpty()) getOfflineObject(url) else getOfflineObject(url, lang) // Couldn't find an exact match, so... @@ -56,7 +55,8 @@ interface OfflineObjectDao { return obj } - fun addObject(url: String, lang: String, path: String, pageTitle: String) { + @Transaction + suspend fun addObject(url: String, lang: String, path: String, pageTitle: String) { // first find this item if it already exists in the db var obj = getOfflineObject(url, lang) @@ -88,7 +88,8 @@ interface OfflineObjectDao { } } - fun deleteObjectsForPageId(ids: List) { + @Transaction + suspend fun deleteObjectsForPageId(ids: List) { ids.forEach { id -> getFromUsedById(id).forEach { obj -> if (obj.usedBy.contains(id)) { @@ -105,7 +106,7 @@ interface OfflineObjectDao { } } - fun getTotalBytesForPageId(id: Long): Long { + suspend fun getTotalBytesForPageId(id: Long): Long { var totalBytes: Long = 0 try { totalBytes = getFromUsedById(id).sumOf { File("${it.path}.1").length() } diff --git a/app/src/main/java/org/wikipedia/page/PageFragment.kt b/app/src/main/java/org/wikipedia/page/PageFragment.kt index fa0a950e62a..80aab1a2d40 100644 --- a/app/src/main/java/org/wikipedia/page/PageFragment.kt +++ b/app/src/main/java/org/wikipedia/page/PageFragment.kt @@ -936,7 +936,12 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi L.e(t) }) { if (!page.thumbUrl.equals(title.thumbUrl, true) || !page.description.equals(title.description, true)) { - AppDatabase.instance.readingListPageDao().updateMetadataByTitle(page, title.description, title.thumbUrl) + AppDatabase.instance.readingListPageDao().updateThumbAndDescriptionByName( + lang = page.wiki.languageCode, + apiTitle = page.apiTitle, + thumbUrl = title.thumbUrl, + description = title.description + ) } } } @@ -1077,13 +1082,11 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi requireActivity().invalidateOptionsMenu() } - fun updateBookmarkAndMenuOptionsFromDao() { + suspend fun updateBookmarkAndMenuOptionsFromDao() { title?.let { - lifecycleScope.launch { - model.readingListPage = AppDatabase.instance.readingListPageDao().findPageInAnyList(it) - updateQuickActionsAndMenuOptions() - requireActivity().invalidateOptionsMenu() - } + model.readingListPage = AppDatabase.instance.readingListPageDao().findPageInAnyList(it) + updateQuickActionsAndMenuOptions() + requireActivity().invalidateOptionsMenu() } } diff --git a/app/src/main/java/org/wikipedia/readinglist/AddToReadingListDialog.kt b/app/src/main/java/org/wikipedia/readinglist/AddToReadingListDialog.kt index c7de2bd2afb..f44a4eb3f52 100644 --- a/app/src/main/java/org/wikipedia/readinglist/AddToReadingListDialog.kt +++ b/app/src/main/java/org/wikipedia/readinglist/AddToReadingListDialog.kt @@ -115,7 +115,11 @@ open class AddToReadingListDialog : ExtendedBottomSheetDialogFragment() { private fun showCreateListDialog() { readingListTitleDialog(requireActivity(), "", "", readingLists.map { it.title }, callback = object : ReadingListTitleDialog.Callback { override fun onSuccess(text: String, description: String) { - addAndDismiss(AppDatabase.instance.readingListDao().createList(text, description), titles) + lifecycleScope.launch(CoroutineExceptionHandler { _, throwable -> + L.e(throwable) + }) { + addAndDismiss(AppDatabase.instance.readingListDao().createList(text, description), titles) + } } }).show() } diff --git a/app/src/main/java/org/wikipedia/readinglist/ReadingListBehaviorsUtil.kt b/app/src/main/java/org/wikipedia/readinglist/ReadingListBehaviorsUtil.kt index 6509e9cf16b..b4a8b79c473 100644 --- a/app/src/main/java/org/wikipedia/readinglist/ReadingListBehaviorsUtil.kt +++ b/app/src/main/java/org/wikipedia/readinglist/ReadingListBehaviorsUtil.kt @@ -65,19 +65,24 @@ object ReadingListBehaviorsUtil { private fun savePagesForOffline(activity: Activity, selectedPages: List, forcedSave: Boolean) { if (selectedPages.isNotEmpty()) { - for (page in selectedPages) { - resetPageProgress(page) + MainScope().launch(exceptionHandler) { + for (page in selectedPages) { + resetPageProgress(page) + } + AppDatabase.instance.readingListPageDao() + .markPagesForOffline(selectedPages, true, forcedSave) + showMultiSelectOfflineStateChangeSnackbar(activity, selectedPages, true) } - AppDatabase.instance.readingListPageDao().markPagesForOffline(selectedPages, true, forcedSave) - showMultiSelectOfflineStateChangeSnackbar(activity, selectedPages, true) } } fun removePagesFromOffline(activity: Activity, selectedPages: List, callback: Callback) { if (selectedPages.isNotEmpty()) { - AppDatabase.instance.readingListPageDao().markPagesForOffline(selectedPages, offline = false, forcedSave = false) - showMultiSelectOfflineStateChangeSnackbar(activity, selectedPages, false) - callback.onCompleted() + MainScope().launch(exceptionHandler) { + AppDatabase.instance.readingListPageDao().markPagesForOffline(selectedPages, offline = false, forcedSave = false) + showMultiSelectOfflineStateChangeSnackbar(activity, selectedPages, false) + callback.onCompleted() + } } } @@ -89,15 +94,22 @@ object ReadingListBehaviorsUtil { MaterialAlertDialogBuilder(activity) .setMessage(activity.getString(R.string.reading_list_delete_confirm, readingList.title)) .setPositiveButton(R.string.reading_list_delete_dialog_ok_button_text) { _, _ -> - AppDatabase.instance.readingListDao().deleteList(readingList) - AppDatabase.instance.readingListPageDao().markPagesForDeletion(readingList, readingList.pages, false) - callback.onCompleted() } + MainScope().launch(exceptionHandler) { + AppDatabase.instance.readingListDao().deleteList(readingList) + AppDatabase.instance.readingListPageDao() + .markPagesForDeletion(readingList, readingList.pages, false) + callback.onCompleted() + } + } .setNegativeButton(R.string.reading_list_delete_dialog_cancel_button_text, null) .show() } else { - AppDatabase.instance.readingListDao().deleteList(readingList) - AppDatabase.instance.readingListPageDao().markPagesForDeletion(readingList, readingList.pages, false) - callback.onCompleted() + MainScope().launch(exceptionHandler) { + AppDatabase.instance.readingListDao().deleteList(readingList) + AppDatabase.instance.readingListPageDao() + .markPagesForDeletion(readingList, readingList.pages, false) + callback.onCompleted() + } } } @@ -106,33 +118,35 @@ object ReadingListBehaviorsUtil { .setTitle(R.string.reading_list_delete_lists_confirm_dialog_title) .setMessage(activity.resources.getQuantityString(R.plurals.reading_list_delete_lists_confirm_dialog_message, readingLists.size, readingLists.size)) .setPositiveButton(R.string.reading_list_delete_lists_dialog_delete_button_text) { _, _ -> - readingLists.filterNot { it.isDefault }.forEach { - AppDatabase.instance.readingListDao().deleteList(it) - AppDatabase.instance.readingListPageDao().markPagesForDeletion(it, it.pages, false) + MainScope().launch(exceptionHandler) { + readingLists.filterNot { it.isDefault }.forEach { + AppDatabase.instance.readingListDao().deleteList(it) + AppDatabase.instance.readingListPageDao().markPagesForDeletion(it, it.pages, false) + } + callback.onCompleted() } - callback.onCompleted() } .setNegativeButton(R.string.reading_list_delete_dialog_cancel_button_text, null) .show() } - fun deletePages(activity: AppCompatActivity, listsContainPage: List, readingListPage: ReadingListPage, snackbarCallback: SnackbarCallback, callback: Callback) { - if (listsContainPage.size > 1) { - activity.lifecycleScope.launch(exceptionHandler) { - val lists = withContext(Dispatchers.IO) { - val pages = AppDatabase.instance.readingListPageDao().getAllPageOccurrences(ReadingListPage.toPageTitle(readingListPage)) - AppDatabase.instance.readingListDao().getListsFromPageOccurrences(pages) - } - RemoveFromReadingListsDialog(lists).deleteOrShowDialog(activity) { list, page -> - showDeletePageFromListsUndoSnackbar(activity, list, page, snackbarCallback) - callback.onCompleted() - } + fun deletePages(activity: Activity, listsContainPage: List, readingListPage: ReadingListPage, snackbarCallback: SnackbarCallback, callback: Callback) { + MainScope().launch(exceptionHandler) { + if (listsContainPage.size > 1) { + val lists = withContext(Dispatchers.IO) { + val pages = AppDatabase.instance.readingListPageDao().getAllPageOccurrences(ReadingListPage.toPageTitle(readingListPage)) + AppDatabase.instance.readingListDao().getListsFromPageOccurrences(pages) + } + RemoveFromReadingListsDialog(lists).deleteOrShowDialog(activity) { list, page -> + showDeletePageFromListsUndoSnackbar(activity, list, page, snackbarCallback) + callback.onCompleted() + } + } else { + AppDatabase.instance.readingListPageDao().markPagesForDeletion(listsContainPage[0], listOf(readingListPage)) + listsContainPage[0].pages.remove(readingListPage) + showDeletePagesUndoSnackbar(activity, listsContainPage[0], listOf(readingListPage), snackbarCallback) + callback.onCompleted() } - } else { - AppDatabase.instance.readingListPageDao().markPagesForDeletion(listsContainPage[0], listOf(readingListPage)) - listsContainPage[0].pages.remove(readingListPage) - showDeletePagesUndoSnackbar(activity, listsContainPage[0], listOf(readingListPage), snackbarCallback) - callback.onCompleted() } } @@ -153,23 +167,25 @@ object ReadingListBehaviorsUtil { return } - val tempLists = AppDatabase.instance.readingListDao().getListsWithoutContents() - val existingTitles = ArrayList() - for (list in tempLists) { - existingTitles.add(list.title) + MainScope().launch(exceptionHandler) { + val existingTitles = AppDatabase.instance.readingListDao().getListsWithoutContents() + .map { it.title } + .filterNot { it == readingList.title } + + ReadingListTitleDialog.readingListTitleDialog( + activity, readingList.title, readingList.description, existingTitles, + callback = object : ReadingListTitleDialog.Callback { + override fun onSuccess(text: String, description: String) { + MainScope().launch(exceptionHandler) { + readingList.title = text + readingList.description = description + readingList.dirty = true + AppDatabase.instance.readingListDao().updateList(readingList, true) + callback.onCompleted() + } + } + }).show() } - existingTitles.remove(readingList.title) - - ReadingListTitleDialog.readingListTitleDialog(activity, readingList.title, readingList.description, existingTitles, - callback = object : ReadingListTitleDialog.Callback { - override fun onSuccess(text: String, description: String) { - readingList.title = text - readingList.description = description - readingList.dirty = true - AppDatabase.instance.readingListDao().updateList(readingList, true) - callback.onCompleted() - } - }).show() } private fun showDeletePageFromListsUndoSnackbar(activity: Activity, lists: List?, page: ReadingListPage, callback: SnackbarCallback) { @@ -186,8 +202,10 @@ object ReadingListBehaviorsUtil { FeedbackUtil.makeSnackbar(activity, activity.getString(R.string.reading_list_item_deleted_from_list, page.displayTitle, readingListNames)) .setAction(R.string.reading_list_item_delete_undo) { - AppDatabase.instance.readingListPageDao().addPageToLists(lists, page, true) - callback.onUndoDeleteClicked() + MainScope().launch(exceptionHandler) { + AppDatabase.instance.readingListPageDao().addPageToLists(lists, page, true) + callback.onUndoDeleteClicked() + } } .show() } @@ -201,13 +219,13 @@ object ReadingListBehaviorsUtil { pages[0].displayTitle, readingList.title) else activity.resources.getQuantityString(R.plurals.reading_list_articles_deleted_from_list, pages.size, pages.size, readingList.title)) .setAction(R.string.reading_list_item_delete_undo) { - val newPages = ArrayList() - for (page in pages) { - newPages.add(ReadingListPage(ReadingListPage.toPageTitle(page))) + MainScope().launch(exceptionHandler) { + val newPages = pages.map { ReadingListPage(ReadingListPage.toPageTitle(it)) } + AppDatabase.instance.readingListPageDao().addPagesToList(readingList, newPages, true) + readingList.pages.addAll(newPages) + callback.onUndoDeleteClicked() } - AppDatabase.instance.readingListPageDao().addPagesToList(readingList, newPages, true) - readingList.pages.addAll(newPages) - callback.onUndoDeleteClicked() } + } .show() } @@ -217,13 +235,14 @@ object ReadingListBehaviorsUtil { } FeedbackUtil.makeSnackbar(activity, activity.getString(R.string.reading_list_deleted, readingList.title)) .setAction(R.string.reading_list_item_delete_undo) { - val newList = AppDatabase.instance.readingListDao().createList(readingList.title, readingList.description) - val newPages = ArrayList() - for (page in readingList.pages) { - newPages.add(ReadingListPage(ReadingListPage.toPageTitle(page))) + MainScope().launch(exceptionHandler) { + val newList = AppDatabase.instance.readingListDao() + .createList(readingList.title, readingList.description) + val newPages = readingList.pages.map { ReadingListPage(ReadingListPage.toPageTitle(it)) } + AppDatabase.instance.readingListPageDao() + .addPagesToList(newList, newPages, true) + callback.onUndoDeleteClicked() } - AppDatabase.instance.readingListPageDao().addPagesToList(newList, newPages, true) - callback.onUndoDeleteClicked() } .show() } @@ -235,13 +254,16 @@ object ReadingListBehaviorsUtil { val snackBar = FeedbackUtil.makeSnackbar(activity, getDeleteListMessage(activity, readingLists)) if (!(readingLists.size == 1 && readingLists[0].isDefault)) { snackBar.setAction(R.string.reading_list_item_delete_undo) { - readingLists.filterNot { it.isDefault }.forEach { - val newList = AppDatabase.instance.readingListDao().createList(it.title, it.description) - val newPages = ArrayList() - for (page in it.pages) { - newPages.add(ReadingListPage(ReadingListPage.toPageTitle(page))) + MainScope().launch(exceptionHandler) { + readingLists.filterNot { it.isDefault }.forEach { + val newList = AppDatabase.instance.readingListDao() + .createList(it.title, it.description) + val newPages = it.pages.map { page -> + ReadingListPage(ReadingListPage.toPageTitle(page)) + } + AppDatabase.instance.readingListPageDao() + .addPagesToList(newList, newPages, true) } - AppDatabase.instance.readingListPageDao().addPagesToList(newList, newPages, true) } callback.onUndoDeleteClicked() } @@ -261,12 +283,12 @@ object ReadingListBehaviorsUtil { } } - fun togglePageOffline(activity: AppCompatActivity, page: ReadingListPage?, callback: Callback) { + fun togglePageOffline(activity: Activity, page: ReadingListPage?, callback: Callback) { if (page == null) { return } if (page.offline) { - activity.lifecycleScope.launch(exceptionHandler) { + MainScope().launch(exceptionHandler) { val lists = withContext(Dispatchers.IO) { val pages = AppDatabase.instance.readingListPageDao().getAllPageOccurrences(ReadingListPage.toPageTitle(page)) AppDatabase.instance.readingListDao().getListsFromPageOccurrences(pages) @@ -304,23 +326,19 @@ object ReadingListBehaviorsUtil { if (addToDefault) { // If the title is a redirect, resolve it before saving to the reading list. (activity as AppCompatActivity).lifecycleScope.launch(exceptionHandler) { - var finalPageTitle = title - try { - ServiceFactory.get(title.wikiSite).getInfoByPageIdsOrTitles(null, title.prefixedText) - .query?.firstPage()?.let { - finalPageTitle = PageTitle(it.title, title.wikiSite, it.thumbUrl(), it.description, it.displayTitle(title.wikiSite.languageCode), null) - } - } finally { - val defaultList = AppDatabase.instance.readingListDao().getDefaultList() - val addedTitles = AppDatabase.instance.readingListPageDao().addPagesToListIfNotExist(defaultList, listOf(finalPageTitle)) - if (addedTitles.isNotEmpty()) { - FeedbackUtil.makeSnackbar(activity, activity.getString(R.string.reading_list_article_added_to_default_list, StringUtil.fromHtml(finalPageTitle.displayText))) - .setAction(R.string.reading_list_add_to_list_button) { - moveToList(activity, defaultList.id, finalPageTitle, invokeSource, false, listener) - }.show() - } else { - FeedbackUtil.showMessage(activity, activity.getString(R.string.reading_list_article_already_exists_message, defaultList.title, title.displayText)) - } + val pageInfo = ServiceFactory.get(title.wikiSite).getInfoByPageIdsOrTitles(null, title.prefixedText).query?.firstPage() + val finalPageTitle = pageInfo?.let { + PageTitle(it.title, title.wikiSite, it.thumbUrl(), it.description, it.displayTitle(title.wikiSite.languageCode), null) + } ?: title + val defaultList = AppDatabase.instance.readingListDao().getDefaultList() + val addedTitles = AppDatabase.instance.readingListPageDao().addPagesToListIfNotExist(defaultList, listOf(finalPageTitle)) + if (addedTitles.isNotEmpty()) { + FeedbackUtil.makeSnackbar(activity, activity.getString(R.string.reading_list_article_added_to_default_list, StringUtil.fromHtml(finalPageTitle.displayText))) + .setAction(R.string.reading_list_add_to_list_button) { + moveToList(activity, defaultList.id, finalPageTitle, invokeSource, false, listener) + }.show() + } else { + FeedbackUtil.showMessage(activity, activity.getString(R.string.reading_list_article_already_exists_message, defaultList.title, title.displayText)) } } } else { @@ -336,10 +354,18 @@ object ReadingListBehaviorsUtil { } private fun toggleOffline(activity: Activity, page: ReadingListPage, forcedSave: Boolean) { - AppDatabase.instance.readingListPageDao().markPageForOffline(page, !page.offline, forcedSave) - FeedbackUtil.showMessage(activity, + + MainScope().launch(exceptionHandler) { + AppDatabase.instance.readingListPageDao() + .markPagesForOffline(listOf(page), !page.offline, forcedSave) + FeedbackUtil.showMessage( + activity, activity.resources.getQuantityString( - if (page.offline) R.plurals.reading_list_article_offline_message else R.plurals.reading_list_article_not_offline_message, 1)) + if (page.offline) R.plurals.reading_list_article_offline_message else R.plurals.reading_list_article_not_offline_message, + 1 + ) + ) + } } private fun showMobileDataWarningDialog(activity: Activity, listener: DialogInterface.OnClickListener) { diff --git a/app/src/main/java/org/wikipedia/readinglist/ReadingListFragment.kt b/app/src/main/java/org/wikipedia/readinglist/ReadingListFragment.kt index 349a3c626e0..4a469260d59 100644 --- a/app/src/main/java/org/wikipedia/readinglist/ReadingListFragment.kt +++ b/app/src/main/java/org/wikipedia/readinglist/ReadingListFragment.kt @@ -44,7 +44,6 @@ import org.wikipedia.activity.BaseActivity import org.wikipedia.analytics.eventplatform.ReadingListsAnalyticsHelper import org.wikipedia.analytics.eventplatform.RecommendedReadingListEvent import org.wikipedia.concurrency.FlowEventBus -import org.wikipedia.database.AppDatabase import org.wikipedia.databinding.FragmentReadingListBinding import org.wikipedia.events.NewRecommendedReadingListEvent import org.wikipedia.events.PageDownloadEvent @@ -180,6 +179,43 @@ class ReadingListFragment : Fragment(), MenuProvider, ReadingListItemActionsDial } } } + launch { + viewModel.saveReadingListFlow.collect { resource -> + when (resource) { + is Resource.Success -> { + if (isRecommendedList) { + RecommendedReadingListEvent.submit("add_list_new", "rrl_discover", countSaved = resource.data.pages.size) + } + + requireActivity().startActivity(MainActivity.newIntent(requireContext()) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP).putExtra(Constants.INTENT_EXTRA_PREVIEW_SAVED_READING_LISTS, true)) + requireActivity().finish() + } + is Resource.Error -> { + L.e(resource.throwable) + FeedbackUtil.showError(requireActivity(), resource.throwable) + } + } + } + } + launch { + viewModel.deleteSelectedPagesFlow.collect { resource -> + when (resource) { + is Resource.Success -> { + readingList?.let { + val pages = resource.data + it.pages.removeAll(pages) + ReadingListBehaviorsUtil.showDeletePagesUndoSnackbar(requireActivity(), it, pages) { updateReadingListData() } + update() + } + } + is Resource.Error -> { + L.e(resource.throwable) + FeedbackUtil.showError(requireActivity(), resource.throwable) + } + } + } + } launch { viewModel.recommendedListFlow.collect { when (it) { @@ -518,7 +554,7 @@ class ReadingListFragment : Fragment(), MenuProvider, ReadingListItemActionsDial } private fun rename() { - ReadingListBehaviorsUtil.renameReadingList(requireActivity(), readingList) { + ReadingListBehaviorsUtil.renameReadingList(requireActivity() as AppCompatActivity, readingList) { update() } } @@ -579,24 +615,14 @@ class ReadingListFragment : Fragment(), MenuProvider, ReadingListItemActionsDial previewSaveDialog = MaterialAlertDialogBuilder(requireContext()) .setPositiveButton(R.string.reading_lists_preview_save_dialog_save) { _, _ -> + lifecycleScope it.pages.clear() it.pages.addAll(savedPages) it.listTitle = readingListTitle if (readingListMode == ReadingListMode.RECOMMENDED) { it.description = null } - // Save reading list to database - it.id = AppDatabase.instance.readingListDao().insertReadingList(it) - AppDatabase.instance.readingListPageDao().addPagesToList(it, it.pages, true) - Prefs.readingListRecentReceivedId = it.id - - if (isRecommendedList) { - RecommendedReadingListEvent.submit("add_list_new", "rrl_discover", countSaved = it.pages.size) - } - - requireActivity().startActivity(MainActivity.newIntent(requireContext()) - .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP).putExtra(Constants.INTENT_EXTRA_PREVIEW_SAVED_READING_LISTS, true)) - requireActivity().finish() + viewModel.saveReadingList(it) } .setNegativeButton(R.string.reading_lists_preview_save_dialog_cancel, null) .create() @@ -625,13 +651,7 @@ class ReadingListFragment : Fragment(), MenuProvider, ReadingListItemActionsDial private fun deleteSelectedPages() { readingList?.let { - val pages = selectedPages - if (pages.isNotEmpty()) { - AppDatabase.instance.readingListPageDao().markPagesForDeletion(it, pages) - it.pages.removeAll(pages) - ReadingListBehaviorsUtil.showDeletePagesUndoSnackbar(requireActivity(), it, pages) { updateReadingListData() } - update() - } + viewModel.deleteSelectedPages(it, selectedPages) } } @@ -666,7 +686,7 @@ class ReadingListFragment : Fragment(), MenuProvider, ReadingListItemActionsDial override fun onToggleItemOffline(pageId: Long) { val page = getPageById(pageId) ?: return - ReadingListBehaviorsUtil.togglePageOffline(requireActivity() as AppCompatActivity, page) { + ReadingListBehaviorsUtil.togglePageOffline(requireActivity(), page) { adapter.notifyDataSetChanged() update() } @@ -701,7 +721,7 @@ class ReadingListFragment : Fragment(), MenuProvider, ReadingListItemActionsDial val page = getPageById(pageId) ?: return readingList?.let { val listsContainPage = if (currentSearchQuery.isNullOrEmpty()) listOf(it) else ReadingListBehaviorsUtil.getListsContainPage(page) - ReadingListBehaviorsUtil.deletePages(requireActivity() as AppCompatActivity, listsContainPage, page, { updateReadingListData() }, { + ReadingListBehaviorsUtil.deletePages(requireActivity(), listsContainPage, page, { updateReadingListData() }, { update() }) } @@ -837,7 +857,7 @@ class ReadingListFragment : Fragment(), MenuProvider, ReadingListItemActionsDial ReadingListMode.DEFAULT -> { readingList?.let { if (currentSearchQuery.isNullOrEmpty()) { - ReadingListBehaviorsUtil.deletePages(requireActivity() as AppCompatActivity, listOf(it), page, { updateReadingListData() }, { + ReadingListBehaviorsUtil.deletePages(requireActivity(), listOf(it), page, { updateReadingListData() }, { update() }) } @@ -980,7 +1000,7 @@ class ReadingListFragment : Fragment(), MenuProvider, ReadingListItemActionsDial } override fun onRename(readingList: ReadingList) { - ReadingListBehaviorsUtil.renameReadingList(requireActivity(), readingList) { update(readingList) } + ReadingListBehaviorsUtil.renameReadingList(requireActivity() as AppCompatActivity, readingList) { update(readingList) } } override fun onDelete(readingList: ReadingList) { diff --git a/app/src/main/java/org/wikipedia/readinglist/ReadingListFragmentViewModel.kt b/app/src/main/java/org/wikipedia/readinglist/ReadingListFragmentViewModel.kt index 7b3c75e9c7d..60c30f9284f 100644 --- a/app/src/main/java/org/wikipedia/readinglist/ReadingListFragmentViewModel.kt +++ b/app/src/main/java/org/wikipedia/readinglist/ReadingListFragmentViewModel.kt @@ -27,6 +27,12 @@ class ReadingListFragmentViewModel : ViewModel() { private val _updateListFlow = MutableSharedFlow>() val updateListFlow = _updateListFlow.asSharedFlow() + private val _saveReadingListFlow = MutableSharedFlow>() + val saveReadingListFlow = _saveReadingListFlow.asSharedFlow() + + private val _deleteSelectedPagesFlow = MutableSharedFlow>>() + val deleteSelectedPagesFlow = _deleteSelectedPagesFlow.asSharedFlow() + private val _recommendedListFlow = MutableStateFlow(Resource()) val recommendedListFlow = _recommendedListFlow.asStateFlow() @@ -59,6 +65,32 @@ class ReadingListFragmentViewModel : ViewModel() { } } + fun saveReadingList(readingList: ReadingList) { + viewModelScope.launch(CoroutineExceptionHandler { _, throwable -> + viewModelScope.launch { + _saveReadingListFlow.emit(Resource.Error(throwable)) + } + }) { + readingList.id = AppDatabase.instance.readingListDao().insertReadingList(readingList) + AppDatabase.instance.readingListPageDao().addPagesToList(readingList, readingList.pages, true) + Prefs.readingListRecentReceivedId = readingList.id + _saveReadingListFlow.emit(Resource.Success(readingList)) + } + } + + fun deleteSelectedPages(readingList: ReadingList, pages: List) { + viewModelScope.launch(CoroutineExceptionHandler { _, throwable -> + viewModelScope.launch { + _deleteSelectedPagesFlow.emit(Resource.Error(throwable)) + } + }) { + if (pages.isNotEmpty()) { + AppDatabase.instance.readingListPageDao().markPagesForDeletion(readingList, pages) + _deleteSelectedPagesFlow.emit(Resource.Success(pages)) + } + } + } + fun generateRecommendedReadingList() { viewModelScope.launch(CoroutineExceptionHandler { _, throwable -> viewModelScope.launch { diff --git a/app/src/main/java/org/wikipedia/readinglist/ReadingListItemActionsDialog.kt b/app/src/main/java/org/wikipedia/readinglist/ReadingListItemActionsDialog.kt index 521edc77bc3..587459c618c 100644 --- a/app/src/main/java/org/wikipedia/readinglist/ReadingListItemActionsDialog.kt +++ b/app/src/main/java/org/wikipedia/readinglist/ReadingListItemActionsDialog.kt @@ -5,9 +5,11 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.os.bundleOf +import kotlinx.coroutines.launch import org.wikipedia.R import org.wikipedia.activity.FragmentUtil import org.wikipedia.database.AppDatabase +import org.wikipedia.extensions.coroutineScope import org.wikipedia.page.ExtendedBottomSheetDialogFragment import org.wikipedia.readinglist.database.ReadingList import org.wikipedia.readinglist.database.ReadingListPage @@ -32,10 +34,18 @@ class ReadingListItemActionsDialog : ExtendedBottomSheetDialogFragment() { actionsView.setBackgroundColor(ResourceUtil.getThemedColor(requireContext(), R.attr.paper_color)) actionsView.callback = ItemActionsCallback() - AppDatabase.instance.readingListPageDao().getPageById(requireArguments().getLong(ARG_READING_LIST_PAGE))?.let { - readingListPage = it - val removeFromListText = if (requireArguments().getInt(ARG_READING_LIST_SIZE) == 1) getString(R.string.reading_list_remove_from_list, requireArguments().getString(ARG_READING_LIST_NAME)) else getString(R.string.reading_list_remove_from_lists) - actionsView.setState(it.displayTitle, removeFromListText, it.offline, requireArguments().getBoolean(ARG_READING_LIST_HAS_ACTION_MODE)) + actionsView.coroutineScope().launch { + AppDatabase.instance.readingListPageDao() + .getPageById(requireArguments().getLong(ARG_READING_LIST_PAGE))?.let { + readingListPage = it + val removeFromListText = + if (requireArguments().getInt(ARG_READING_LIST_SIZE) == 1) getString( + R.string.reading_list_remove_from_list, + requireArguments().getString(ARG_READING_LIST_NAME) + ) else getString(R.string.reading_list_remove_from_lists) + actionsView.setState(it.displayTitle, removeFromListText, it.offline, requireArguments().getBoolean(ARG_READING_LIST_HAS_ACTION_MODE) + ) + } } return actionsView } diff --git a/app/src/main/java/org/wikipedia/readinglist/ReadingListPreviewSaveDialogView.kt b/app/src/main/java/org/wikipedia/readinglist/ReadingListPreviewSaveDialogView.kt index 382903743e9..15b69e6fa72 100644 --- a/app/src/main/java/org/wikipedia/readinglist/ReadingListPreviewSaveDialogView.kt +++ b/app/src/main/java/org/wikipedia/readinglist/ReadingListPreviewSaveDialogView.kt @@ -10,14 +10,18 @@ import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.launch import org.wikipedia.R import org.wikipedia.database.AppDatabase import org.wikipedia.databinding.ItemReadingListPreviewSaveSelectItemBinding import org.wikipedia.databinding.ViewReadingListPreviewSaveDialogBinding +import org.wikipedia.extensions.coroutineScope import org.wikipedia.readinglist.database.ReadingList import org.wikipedia.readinglist.database.ReadingListPage import org.wikipedia.util.DateUtil import org.wikipedia.util.StringUtil +import org.wikipedia.util.log.L import org.wikipedia.views.DefaultViewHolder import org.wikipedia.views.DrawableItemDecoration import org.wikipedia.views.ViewUtil @@ -35,7 +39,7 @@ class ReadingListPreviewSaveDialogView(context: Context, attrs: AttributeSet? = private lateinit var readingList: ReadingList private lateinit var savedReadingListPages: MutableList private lateinit var callback: Callback - private var currentReadingLists: MutableList + private var currentReadingLists = emptyList() var readingListMode = ReadingListMode.PREVIEW init { @@ -45,7 +49,11 @@ class ReadingListPreviewSaveDialogView(context: Context, attrs: AttributeSet? = ) binding.recyclerView.layoutManager = LinearLayoutManager(context) binding.recyclerView.addItemDecoration(DrawableItemDecoration(context, R.attr.list_divider, drawStart = true, drawEnd = true, skipSearchBar = true)) - currentReadingLists = AppDatabase.instance.readingListDao().getAllLists().toMutableList() + coroutineScope().launch(CoroutineExceptionHandler { + _, throwable -> L.w(throwable) + }) { + currentReadingLists = AppDatabase.instance.readingListDao().getAllLists() + } binding.readingListTitle.doOnTextChanged { _, _, _, _ -> validateTitleAndList() } diff --git a/app/src/main/java/org/wikipedia/readinglist/ReadingListsExportImportHelper.kt b/app/src/main/java/org/wikipedia/readinglist/ReadingListsExportImportHelper.kt index 6f273b8f9be..764980cef22 100644 --- a/app/src/main/java/org/wikipedia/readinglist/ReadingListsExportImportHelper.kt +++ b/app/src/main/java/org/wikipedia/readinglist/ReadingListsExportImportHelper.kt @@ -13,6 +13,9 @@ import androidx.core.app.NotificationCompat import androidx.core.app.PendingIntentCompat import androidx.core.content.ContextCompat import androidx.core.content.getSystemService +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.launch import kotlinx.serialization.Serializable import org.wikipedia.R import org.wikipedia.activity.BaseActivity @@ -77,16 +80,20 @@ object ReadingListsExportImportHelper : BaseActivity.Callback { .setStyle(NotificationCompat.BigTextStyle().bigText(context.getString(R.string.reading_list_notification_text, numOfLists))) } - fun importLists(activity: BaseActivity, jsonString: String) { + fun importLists(activity: AppCompatActivity, jsonString: String) { ReadingListsAnalyticsHelper.logImportStart(activity) - try { + activity.lifecycleScope.launch(CoroutineExceptionHandler { _, throwable -> + FeedbackUtil.showMessage(activity, R.string.reading_lists_import_failure_message) + ReadingListsAnalyticsHelper.logImportCancelled(activity) + }) { val contents: ExportableContents = JsonUtil.decodeFromString(jsonString)!! val readingLists = contents.readingListsV1 for (list in readingLists) { val allLists = AppDatabase.instance.readingListDao().getAllLists() - val existingTitles = AppDatabase.instance.readingListDao().getAllLists().map { it.title } - if (existingTitles.contains(list.name)) { - allLists.filter { it.title == list.name }.forEach { addTitlesToList(list, it) } + if (allLists.any { it.title == list.name }) { + allLists.filter { it.title == list.name }.forEach { + addTitlesToList(list, it) + } continue } val readingList = AppDatabase.instance.readingListDao().createList(list.name!!, list.description) @@ -94,13 +101,10 @@ object ReadingListsExportImportHelper : BaseActivity.Callback { ReadingListsAnalyticsHelper.logImportFinished(activity, list.pages.size) } FeedbackUtil.showMessage(activity, activity.resources.getQuantityString(R.plurals.reading_list_import_success_message, readingLists.size)) - } catch (e: Exception) { - FeedbackUtil.showMessage(activity, R.string.reading_lists_import_failure_message) - ReadingListsAnalyticsHelper.logImportCancelled(activity) } } - private fun addTitlesToList(exportedList: ExportableReadingList, list: ReadingList) { + private suspend fun addTitlesToList(exportedList: ExportableReadingList, list: ReadingList) { val titles = exportedList.pages.map { page -> PageTitle(page.title, WikiSite.forLanguageCode(page.lang)).also { if (page.ns != Namespace.MAIN.code()) { it.namespace = Namespace.of(page.ns).name } diff --git a/app/src/main/java/org/wikipedia/readinglist/ReadingListsFragment.kt b/app/src/main/java/org/wikipedia/readinglist/ReadingListsFragment.kt index 579c5648fd8..ab31b9879f5 100644 --- a/app/src/main/java/org/wikipedia/readinglist/ReadingListsFragment.kt +++ b/app/src/main/java/org/wikipedia/readinglist/ReadingListsFragment.kt @@ -36,6 +36,7 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import org.wikipedia.Constants @@ -186,7 +187,7 @@ class ReadingListsFragment : Fragment(), SortReadingListsDialog.Callback, Readin override fun onToggleItemOffline(pageId: Long) { val page = getPageById(pageId) ?: return - ReadingListBehaviorsUtil.togglePageOffline(requireActivity() as AppCompatActivity, page) { this.updateLists() } + ReadingListBehaviorsUtil.togglePageOffline(requireActivity(), page) { this.updateLists() } } override fun onShareItem(pageId: Long) { @@ -212,7 +213,7 @@ class ReadingListsFragment : Fragment(), SortReadingListsDialog.Callback, Readin override fun onDeleteItem(pageId: Long) { val page = getPageById(pageId) ?: return - ReadingListBehaviorsUtil.deletePages(requireActivity() as AppCompatActivity, ReadingListBehaviorsUtil.getListsContainPage(page), page, { this.updateLists() }) { this.updateLists() } + ReadingListBehaviorsUtil.deletePages(requireActivity(), ReadingListBehaviorsUtil.getListsContainPage(page), page, { this.updateLists() }) { this.updateLists() } } private fun getPageById(id: Long): ReadingListPage? { @@ -230,8 +231,12 @@ class ReadingListsFragment : Fragment(), SortReadingListsDialog.Callback, Readin ReadingListTitleDialog.readingListTitleDialog(requireActivity(), getString(R.string.reading_list_name_sample), "", existingTitles, callback = object : ReadingListTitleDialog.Callback { override fun onSuccess(text: String, description: String) { - AppDatabase.instance.readingListDao().createList(text, description) - updateLists() + viewLifecycleOwner.lifecycleScope.launch(CoroutineExceptionHandler { _, throwable -> + L.w(throwable) + }) { + AppDatabase.instance.readingListDao().createList(text, description) + updateLists() + } } }).show() } @@ -510,7 +515,7 @@ class ReadingListsFragment : Fragment(), SortReadingListsDialog.Callback, Readin L.w("Attempted to rename default list.") return } - ReadingListBehaviorsUtil.renameReadingList(requireActivity(), readingList) { + ReadingListBehaviorsUtil.renameReadingList(requireActivity() as AppCompatActivity, readingList) { ReadingListSyncAdapter.manualSync() updateLists(currentSearchQuery, true) } @@ -861,7 +866,7 @@ class ReadingListsFragment : Fragment(), SortReadingListsDialog.Callback, Readin binding.swipeRefreshLayout.isRefreshing = true activity?.contentResolver?.openInputStream(uri)?.use { inputStream -> val inputString = inputStream.bufferedReader().use { it.readText() } - ReadingListsExportImportHelper.importLists(activity as BaseActivity, inputString) + ReadingListsExportImportHelper.importLists(activity as AppCompatActivity, inputString) importMode = true } } diff --git a/app/src/main/java/org/wikipedia/readinglist/RemoveFromReadingListsDialog.kt b/app/src/main/java/org/wikipedia/readinglist/RemoveFromReadingListsDialog.kt index fabba6d0d95..54a36ae0898 100644 --- a/app/src/main/java/org/wikipedia/readinglist/RemoveFromReadingListsDialog.kt +++ b/app/src/main/java/org/wikipedia/readinglist/RemoveFromReadingListsDialog.kt @@ -1,11 +1,15 @@ package org.wikipedia.readinglist -import android.content.Context +import android.app.Activity import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.launch import org.wikipedia.R import org.wikipedia.database.AppDatabase import org.wikipedia.readinglist.database.ReadingList import org.wikipedia.readinglist.database.ReadingListPage +import org.wikipedia.util.log.L class RemoveFromReadingListsDialog(private val listsContainingPage: List) { fun interface Callback { @@ -16,32 +20,42 @@ class RemoveFromReadingListsDialog(private val listsContainingPage: List, ReadingList.SORT_BY_NAME_ASC) } - fun deleteOrShowDialog(context: Context, callback: Callback?) { - if (listsContainingPage.isNullOrEmpty()) { + fun deleteOrShowDialog(activity: Activity, callback: Callback?) { + if (listsContainingPage.isEmpty()) { return } if (listsContainingPage.size == 1 && listsContainingPage[0].pages.isNotEmpty()) { - AppDatabase.instance.readingListPageDao().markPagesForDeletion(listsContainingPage[0], listOf(listsContainingPage[0].pages[0])) - callback?.onDeleted(listsContainingPage, listsContainingPage[0].pages[0]) + MainScope().launch(CoroutineExceptionHandler { _, throwable -> + L.w(throwable) + }) { + AppDatabase.instance.readingListPageDao().markPagesForDeletion(listsContainingPage[0], listOf(listsContainingPage[0].pages[0])) + callback?.onDeleted(listsContainingPage, listsContainingPage[0].pages[0]) + } return } - showDialog(context, callback) + showDialog(activity, callback) } - private fun showDialog(context: Context, callback: Callback?) { + private fun showDialog(activity: Activity, callback: Callback?) { val selectedLists = BooleanArray(listsContainingPage.size) - MaterialAlertDialogBuilder(context) + MaterialAlertDialogBuilder(activity) .setTitle(R.string.reading_list_remove_from_lists) .setPositiveButton(R.string.reading_list_remove_list_dialog_ok_button_text) { _, _ -> - val newLists = (listsContainingPage zip selectedLists.asIterable()) - .filter { (_, selected) -> selected } - .map { (listContainingPage, _) -> - AppDatabase.instance.readingListPageDao().markPagesForDeletion(listContainingPage, - listOf(listContainingPage.pages[0])) - listContainingPage + MainScope().launch(CoroutineExceptionHandler { _, throwable -> + L.w(throwable) + }) { + val newLists = (listsContainingPage zip selectedLists.asIterable()) + .filter { (_, selected) -> selected } + .map { (listContainingPage, _) -> + AppDatabase.instance.readingListPageDao().markPagesForDeletion( + listContainingPage, + listOf(listContainingPage.pages[0]) + ) + listContainingPage + } + if (newLists.isNotEmpty()) { + callback?.onDeleted(newLists, listsContainingPage[0].pages[0]) } - if (newLists.isNotEmpty()) { - callback?.onDeleted(newLists, listsContainingPage[0].pages[0]) } } .setNegativeButton(R.string.reading_list_remove_from_list_dialog_cancel_button_text, null) diff --git a/app/src/main/java/org/wikipedia/readinglist/db/ReadingListDao.kt b/app/src/main/java/org/wikipedia/readinglist/db/ReadingListDao.kt index b416b959ccf..88b2ab9e25b 100644 --- a/app/src/main/java/org/wikipedia/readinglist/db/ReadingListDao.kt +++ b/app/src/main/java/org/wikipedia/readinglist/db/ReadingListDao.kt @@ -1,6 +1,12 @@ package org.wikipedia.readinglist.db -import androidx.room.* +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import androidx.room.Update import org.wikipedia.R import org.wikipedia.database.AppDatabase import org.wikipedia.readinglist.database.ReadingList @@ -12,16 +18,16 @@ import org.wikipedia.util.log.L @Dao interface ReadingListDao { @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insertReadingList(list: ReadingList): Long + suspend fun insertReadingList(list: ReadingList): Long @Update(onConflict = OnConflictStrategy.REPLACE) - fun updateReadingList(list: ReadingList) + suspend fun updateReadingList(list: ReadingList) @Delete - fun deleteReadingList(list: ReadingList) + suspend fun deleteReadingList(list: ReadingList) @Query("SELECT * FROM ReadingList") - fun getListsWithoutContents(): List + suspend fun getListsWithoutContents(): List @Query("SELECT * FROM ReadingList WHERE id = :id") suspend fun getListById(id: Long): ReadingList? @@ -30,9 +36,9 @@ interface ReadingListDao { suspend fun getListsByIds(readingListIds: Set): List @Query("UPDATE ReadingList SET remoteId = -1") - fun markAllListsUnsynced() + suspend fun markAllListsUnsynced() - fun getAllLists(): List { + suspend fun getAllLists(): List { val lists = getListsWithoutContents() lists.forEach { AppDatabase.instance.readingListPageDao().populateListPages(it) @@ -48,7 +54,7 @@ interface ReadingListDao { } } - fun getAllListsWithUnsyncedPages(): List { + suspend fun getAllListsWithUnsyncedPages(): List { val lists = getListsWithoutContents() val pages = AppDatabase.instance.readingListPageDao().getAllPagesToBeSynced() pages.forEach { page -> @@ -57,11 +63,12 @@ interface ReadingListDao { return lists } - fun updateList(list: ReadingList, queueForSync: Boolean) { + suspend fun updateList(list: ReadingList, queueForSync: Boolean) { updateLists(listOf(list), queueForSync) } - fun updateLists(lists: List, queueForSync: Boolean) { + @Transaction + suspend fun updateLists(lists: List, queueForSync: Boolean) { for (list in lists) { if (queueForSync) { list.dirty = true @@ -74,7 +81,7 @@ interface ReadingListDao { } } - fun deleteList(list: ReadingList, queueForSync: Boolean = true) { + suspend fun deleteList(list: ReadingList, queueForSync: Boolean = true) { if (list.isDefault) { L.w("Attempted to delete the default list.") return @@ -93,7 +100,7 @@ interface ReadingListDao { return lists } - fun createList(title: String, description: String?): ReadingList { + suspend fun createList(title: String, description: String?): ReadingList { if (title.isEmpty()) { L.w("Attempted to create list with empty title (default).") return getDefaultList() @@ -101,14 +108,14 @@ interface ReadingListDao { return createNewList(title, description) } - fun getDefaultList(): ReadingList { + suspend fun getDefaultList(): ReadingList { return getListsWithoutContents().find { it.isDefault } ?: run { L.w("(Re)creating default list.") createNewList("", L10nUtil.getString(R.string.default_reading_list_description)) } } - private fun createNewList(title: String, description: String?): ReadingList { + private suspend fun createNewList(title: String, description: String?): ReadingList { val protoList = ReadingList(title, description) protoList.id = insertReadingList(protoList) return protoList diff --git a/app/src/main/java/org/wikipedia/readinglist/db/ReadingListPageDao.kt b/app/src/main/java/org/wikipedia/readinglist/db/ReadingListPageDao.kt index a2e65a94428..f3a15661222 100644 --- a/app/src/main/java/org/wikipedia/readinglist/db/ReadingListPageDao.kt +++ b/app/src/main/java/org/wikipedia/readinglist/db/ReadingListPageDao.kt @@ -24,31 +24,31 @@ import org.wikipedia.util.StringUtil @Dao interface ReadingListPageDao { @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insertReadingListPage(page: ReadingListPage): Long + suspend fun insertReadingListPage(page: ReadingListPage): Long @Update(onConflict = OnConflictStrategy.REPLACE) - fun updateReadingListPage(page: ReadingListPage) + suspend fun updateReadingListPage(page: ReadingListPage) - @Delete - fun deleteReadingListPage(page: ReadingListPage) + @Update(onConflict = OnConflictStrategy.REPLACE) + suspend fun updateReadingListPages(pages: List) - @Query("SELECT * FROM ReadingListPage") - fun getAllPages(): List + @Delete + suspend fun deleteReadingListPage(page: ReadingListPage) @Query("SELECT COUNT(*) FROM ReadingListPage") suspend fun getPagesCount(): Int @Query("SELECT * FROM ReadingListPage WHERE id = :id") - fun getPageById(id: Long): ReadingListPage? + suspend fun getPageById(id: Long): ReadingListPage? @Query("SELECT * FROM ReadingListPage WHERE status = :status AND offline = :offline") - fun getPagesByStatus(status: Long, offline: Boolean): List + suspend fun getPagesByStatus(status: Long, offline: Boolean): List @Query("SELECT * FROM ReadingListPage WHERE status = :status") - fun getPagesByStatus(status: Long): List + suspend fun getPagesByStatus(status: Long): List @Query("SELECT * FROM ReadingListPage WHERE wiki = :wiki AND lang = :lang AND namespace = :ns AND apiTitle = :apiTitle AND listId = :listId AND status != :excludedStatus") - fun getPageByParams(wiki: WikiSite, lang: String, ns: Namespace, + suspend fun getPageByParams(wiki: WikiSite, lang: String, ns: Namespace, apiTitle: String, listId: Long, excludedStatus: Long): ReadingListPage? @Query("SELECT * FROM ReadingListPage WHERE wiki = :wiki AND lang = :lang AND namespace = :ns AND apiTitle = :apiTitle AND status != :excludedStatus") @@ -56,92 +56,79 @@ interface ReadingListPageDao { apiTitle: String, excludedStatus: Long): ReadingListPage? @Query("SELECT * FROM ReadingListPage WHERE wiki = :wiki AND lang = :lang AND namespace = :ns AND apiTitle = :apiTitle AND status != :excludedStatus") - fun getPagesByParams(wiki: WikiSite, lang: String, ns: Namespace, + suspend fun getPagesByParams(wiki: WikiSite, lang: String, ns: Namespace, apiTitle: String, excludedStatus: Long): List @Query("SELECT * FROM ReadingListPage ORDER BY RANDOM() DESC LIMIT :limit") suspend fun getPagesByRandom(limit: Int): List @Query("SELECT * FROM ReadingListPage WHERE listId = :listId AND status != :excludedStatus") - fun getPagesByListId(listId: Long, excludedStatus: Long): List + suspend fun getPagesByListId(listId: Long, excludedStatus: Long): List @Query("UPDATE ReadingListPage SET thumbUrl = :thumbUrl, description = :description WHERE lang = :lang AND apiTitle = :apiTitle") - fun updateThumbAndDescriptionByName(lang: String, apiTitle: String, thumbUrl: String?, description: String?) + suspend fun updateThumbAndDescriptionByName(lang: String, apiTitle: String, thumbUrl: String?, description: String?) @Query("UPDATE ReadingListPage SET status = :newStatus WHERE status = :oldStatus AND offline = :offline") - fun updateStatus(oldStatus: Long, newStatus: Long, offline: Boolean) + suspend fun updateStatus(oldStatus: Long, newStatus: Long, offline: Boolean) @Query("SELECT * FROM ReadingListPage WHERE lang = :lang ORDER BY RANDOM() LIMIT 1") - fun getRandomPage(lang: String): ReadingListPage? + suspend fun getRandomPage(lang: String): ReadingListPage? @Query("SELECT * FROM ReadingListPage WHERE UPPER(displayTitle) LIKE UPPER(:term) ESCAPE '\\'") - fun findPageBySearchTerm(term: String): List + suspend fun findPageBySearchTerm(term: String): List @Query("DELETE FROM ReadingListPage WHERE status = :status") - fun deletePagesByStatus(status: Long) + suspend fun deletePagesByStatus(status: Long) @Query("UPDATE ReadingListPage SET remoteId = -1") - fun markAllPagesUnsynced() + suspend fun markAllPagesUnsynced() @Query("SELECT * FROM ReadingListPage WHERE remoteId < 1") - fun getAllPagesToBeSynced(): List + suspend fun getAllPagesToBeSynced(): List - fun getAllPagesToBeSaved() = getPagesByStatus(ReadingListPage.STATUS_QUEUE_FOR_SAVE, true) + suspend fun getAllPagesToBeSaved() = getPagesByStatus(ReadingListPage.STATUS_QUEUE_FOR_SAVE, true) - fun getAllPagesToBeForcedSave() = getPagesByStatus(ReadingListPage.STATUS_QUEUE_FOR_FORCED_SAVE, true) + suspend fun getAllPagesToBeForcedSave() = getPagesByStatus(ReadingListPage.STATUS_QUEUE_FOR_FORCED_SAVE, true) - fun getAllPagesToBeUnsaved() = getPagesByStatus(ReadingListPage.STATUS_SAVED, false) + suspend fun getAllPagesToBeUnsaved() = getPagesByStatus(ReadingListPage.STATUS_SAVED, false) - fun getAllPagesToBeDeleted() = getPagesByStatus(ReadingListPage.STATUS_QUEUE_FOR_DELETE) + suspend fun getAllPagesToBeDeleted() = getPagesByStatus(ReadingListPage.STATUS_QUEUE_FOR_DELETE) - fun populateListPages(list: ReadingList) { + suspend fun populateListPages(list: ReadingList) { list.pages.addAll(getPagesByListId(list.id, ReadingListPage.STATUS_QUEUE_FOR_DELETE)) } - fun addPagesToList(list: ReadingList, pages: List, queueForSync: Boolean) { - addPagesToList(list, pages) - if (queueForSync) { - ReadingListSyncAdapter.manualSync() - } - } - @Transaction - fun addPagesToList(list: ReadingList, pages: List) { + suspend fun addPagesToList(list: ReadingList, pages: List, queueForSync: Boolean) { for (page in pages) { insertPageIntoDb(list, page) } FlowEventBus.post(ArticleSavedOrDeletedEvent(true, *pages.toTypedArray())) SavedPageSyncService.enqueue() + if (queueForSync) { + ReadingListSyncAdapter.manualSync() + } } @Transaction - fun addPagesToListIfNotExist(list: ReadingList, titles: List): List { - val addedTitles = mutableListOf() + suspend fun addPagesToListIfNotExist(list: ReadingList, titles: List): List { + val pages = mutableListOf() for (title in titles) { if (getPageByTitle(list, title) != null) { continue } - addPageToList(list, title) - addedTitles.add(title.displayText) + val page = addPageToList(list, title) + pages.add(page) } - if (addedTitles.isNotEmpty()) { + if (pages.isNotEmpty()) { SavedPageSyncService.enqueue() ReadingListSyncAdapter.manualSync() + FlowEventBus.post(ArticleSavedOrDeletedEvent(true, *pages.toTypedArray())) } - return addedTitles + return pages.map { it.displayTitle } } @Transaction - fun updatePages(pages: List) { - for (page in pages) { - updateReadingListPage(page) - } - } - - suspend fun updateMetadataByTitle(pageProto: ReadingListPage, description: String?, thumbUrl: String?) { - updateThumbAndDescriptionByName(pageProto.lang, pageProto.apiTitle, thumbUrl, description) - } - suspend fun findPageInAnyList(title: PageTitle): ReadingListPage? { return getPageByParams( title.wikiSite, title.wikiSite.languageCode, title.namespace(), @@ -149,7 +136,7 @@ interface ReadingListPageDao { ) } - fun findPageForSearchQueryInAnyList(wikiSite: WikiSite, searchQuery: String): SearchResults { + suspend fun findPageForSearchQueryInAnyList(wikiSite: WikiSite, searchQuery: String): SearchResults { var normalizedQuery = StringUtils.stripAccents(searchQuery) if (normalizedQuery.isEmpty()) { return SearchResults() @@ -166,16 +153,8 @@ interface ReadingListPageDao { }.toMutableList()) } - fun pageExistsInList(list: ReadingList, title: PageTitle): Boolean { - return getPageByTitle(list, title) != null - } - - fun resetUnsavedPageStatus() { - updateStatus(ReadingListPage.STATUS_SAVED, ReadingListPage.STATUS_QUEUE_FOR_SAVE, false) - } - @Transaction - fun markPagesForDeletion(list: ReadingList, pages: List, queueForSync: Boolean = true) { + suspend fun markPagesForDeletion(list: ReadingList, pages: List, queueForSync: Boolean = true) { for (page in pages) { page.status = ReadingListPage.STATUS_QUEUE_FOR_DELETE updateReadingListPage(page) @@ -187,31 +166,27 @@ interface ReadingListPageDao { SavedPageSyncService.enqueue() } - fun markPageForOffline(page: ReadingListPage, offline: Boolean, forcedSave: Boolean) { - markPagesForOffline(listOf(page), offline, forcedSave) - } - @Transaction - fun markPagesForOffline(pages: List, offline: Boolean, forcedSave: Boolean) { - for (page in pages) { - if (page.offline == offline && !forcedSave) { - continue - } - page.offline = offline - if (forcedSave) { - page.status = ReadingListPage.STATUS_QUEUE_FOR_FORCED_SAVE + suspend fun markPagesForOffline(pages: List, offline: Boolean, forcedSave: Boolean) { + val updatedPages = pages.map { + if (it.offline != offline || !forcedSave) { + it.offline = offline + if (forcedSave) { + it.status = ReadingListPage.STATUS_QUEUE_FOR_FORCED_SAVE + } } - updateReadingListPage(page) + it } + updateReadingListPages(updatedPages) SavedPageSyncService.enqueue() } - fun purgeDeletedPages() { + suspend fun purgeDeletedPages() { deletePagesByStatus(ReadingListPage.STATUS_QUEUE_FOR_DELETE) } @Transaction - fun movePagesToListAndDeleteSourcePages(sourceList: ReadingList, destList: ReadingList, titles: List): List { + suspend fun movePagesToListAndDeleteSourcePages(sourceList: ReadingList, destList: ReadingList, titles: List): List { val movedTitles = mutableListOf() for (title in titles) { movePageToList(sourceList, destList, title) @@ -224,14 +199,16 @@ interface ReadingListPageDao { return movedTitles } - private fun movePageToList(sourceList: ReadingList, destList: ReadingList, title: PageTitle) { + @Transaction + suspend fun movePageToList(sourceList: ReadingList, destList: ReadingList, title: PageTitle) { if (sourceList.id == destList.id) { return } val sourceReadingListPage = getPageByTitle(sourceList, title) if (sourceReadingListPage != null) { if (getPageByTitle(destList, title) == null) { - addPageToList(destList, title) + val page = addPageToList(destList, title) + FlowEventBus.post(ArticleSavedOrDeletedEvent(true, page)) } markPagesForDeletion(sourceList, listOf(sourceReadingListPage)) ReadingListSyncAdapter.manualSync() @@ -239,23 +216,14 @@ interface ReadingListPageDao { } } - fun getPageByTitle(list: ReadingList, title: PageTitle): ReadingListPage? { + suspend fun getPageByTitle(list: ReadingList, title: PageTitle): ReadingListPage? { return getPageByParams( title.wikiSite, title.wikiSite.languageCode, title.namespace(), title.prefixedText, list.id, ReadingListPage.STATUS_QUEUE_FOR_DELETE ) } - fun addPageToList(list: ReadingList, title: PageTitle, queueForSync: Boolean) { - addPageToList(list, title) - SavedPageSyncService.enqueue() - if (queueForSync) { - ReadingListSyncAdapter.manualSync() - } - } - - @Transaction - fun addPageToLists(lists: List, page: ReadingListPage, queueForSync: Boolean) { + suspend fun addPageToLists(lists: List, page: ReadingListPage, queueForSync: Boolean) { for (list in lists) { if (getPageByTitle(list, ReadingListPage.toPageTitle(page)) != null) { continue @@ -271,20 +239,20 @@ interface ReadingListPageDao { } } - fun getAllPageOccurrences(title: PageTitle): List { + suspend fun getAllPageOccurrences(title: PageTitle): List { return getPagesByParams( title.wikiSite, title.wikiSite.languageCode, title.namespace(), title.prefixedText, ReadingListPage.STATUS_QUEUE_FOR_DELETE ) } - private fun addPageToList(list: ReadingList, title: PageTitle) { + private suspend fun addPageToList(list: ReadingList, title: PageTitle): ReadingListPage { val protoPage = ReadingListPage(title) insertPageIntoDb(list, protoPage) - FlowEventBus.post(ArticleSavedOrDeletedEvent(true, protoPage)) + return protoPage } - private fun insertPageIntoDb(list: ReadingList, page: ReadingListPage) { + private suspend fun insertPageIntoDb(list: ReadingList, page: ReadingListPage) { page.listId = list.id page.id = insertReadingListPage(page) } diff --git a/app/src/main/java/org/wikipedia/readinglist/sync/ReadingListSyncAdapter.kt b/app/src/main/java/org/wikipedia/readinglist/sync/ReadingListSyncAdapter.kt index ab71d20a70f..2d70caa705d 100644 --- a/app/src/main/java/org/wikipedia/readinglist/sync/ReadingListSyncAdapter.kt +++ b/app/src/main/java/org/wikipedia/readinglist/sync/ReadingListSyncAdapter.kt @@ -1,6 +1,7 @@ package org.wikipedia.readinglist.sync -import android.content.* +import android.content.ContentResolver +import android.content.Context import android.os.Bundle import androidx.core.os.bundleOf import androidx.work.Constraints @@ -339,7 +340,7 @@ class ReadingListSyncAdapter(context: Context, params: WorkerParameters) : Corou for (i in ids.indices) { localPages[i].remoteId = ids[i] } - AppDatabase.instance.readingListPageDao().updatePages(localPages) + AppDatabase.instance.readingListPageDao().updateReadingListPages(localPages) } } catch (t: Throwable) { // TODO: optimization opportunity -- if the server can return the ID @@ -385,7 +386,7 @@ class ReadingListSyncAdapter(context: Context, params: WorkerParameters) : Corou } } } - AppDatabase.instance.readingListPageDao().updatePages(localPages) + AppDatabase.instance.readingListPageDao().updateReadingListPages(localPages) } } } catch (e: CancellationException) { @@ -447,7 +448,7 @@ class ReadingListSyncAdapter(context: Context, params: WorkerParameters) : Corou return extras } - private fun createOrUpdatePage(listForPage: ReadingList, + private suspend fun createOrUpdatePage(listForPage: ReadingList, remotePage: RemoteReadingListEntry) { val remoteTitle = pageTitleFromRemoteEntry(remotePage) var localPage = listForPage.pages.find { ReadingListPage.toPageTitle(it) == remoteTitle } @@ -456,9 +457,7 @@ class ReadingListSyncAdapter(context: Context, params: WorkerParameters) : Corou if (localPage == null) { localPage = ReadingListPage(pageTitleFromRemoteEntry(remotePage)) localPage.listId = listForPage.id - if (AppDatabase.instance.readingListPageDao().pageExistsInList(listForPage, remoteTitle)) { - updateOnly = true - } + updateOnly = AppDatabase.instance.readingListPageDao().getPageByTitle(listForPage, remoteTitle) != null } localPage.remoteId = remotePage.id if (updateOnly) { @@ -470,7 +469,7 @@ class ReadingListSyncAdapter(context: Context, params: WorkerParameters) : Corou } } - private fun deletePageByTitle(listForPage: ReadingList, title: PageTitle) { + private suspend fun deletePageByTitle(listForPage: ReadingList, title: PageTitle) { var localPage = listForPage.pages.find { ReadingListPage.toPageTitle(it) == title } if (localPage == null) { localPage = AppDatabase.instance.readingListPageDao().getPageByTitle(listForPage, title) diff --git a/app/src/main/java/org/wikipedia/savedpages/SavedPageSyncService.kt b/app/src/main/java/org/wikipedia/savedpages/SavedPageSyncService.kt index cf2777dfcc3..001125d38ea 100644 --- a/app/src/main/java/org/wikipedia/savedpages/SavedPageSyncService.kt +++ b/app/src/main/java/org/wikipedia/savedpages/SavedPageSyncService.kt @@ -32,7 +32,11 @@ import org.wikipedia.readinglist.database.ReadingListPage import org.wikipedia.readinglist.sync.ReadingListSyncAdapter import org.wikipedia.readinglist.sync.ReadingListSyncEvent import org.wikipedia.settings.Prefs -import org.wikipedia.util.* +import org.wikipedia.util.DeviceUtil +import org.wikipedia.util.DimenUtil +import org.wikipedia.util.ImageUrlUtil +import org.wikipedia.util.ThrowableUtil +import org.wikipedia.util.UriUtil import org.wikipedia.util.log.L import org.wikipedia.views.CircularProgressBar import java.io.IOException @@ -66,7 +70,11 @@ class SavedPageSyncService(context: Context, params: WorkerParameters) : Corouti shouldSendSyncEvent = true } if (pagesToUnSave.isNotEmpty()) { - AppDatabase.instance.readingListPageDao().resetUnsavedPageStatus() + AppDatabase.instance.readingListPageDao().updateStatus( + oldStatus = ReadingListPage.STATUS_SAVED, + newStatus = ReadingListPage.STATUS_QUEUE_FOR_SAVE, + offline = false + ) shouldSendSyncEvent = true } } diff --git a/app/src/main/java/org/wikipedia/settings/dev/DeveloperSettingsPreferenceLoader.kt b/app/src/main/java/org/wikipedia/settings/dev/DeveloperSettingsPreferenceLoader.kt index dbf270188ff..0cb8b214cda 100644 --- a/app/src/main/java/org/wikipedia/settings/dev/DeveloperSettingsPreferenceLoader.kt +++ b/app/src/main/java/org/wikipedia/settings/dev/DeveloperSettingsPreferenceLoader.kt @@ -113,7 +113,14 @@ internal class DeveloperSettingsPreferenceLoader(fragment: PreferenceFragmentCom val pages = (0 until numberOfArticles).map { ReadingListPage(PageTitle("Malformed page $it", WikiSite.forLanguageCode("foo"))) } + fragment.lifecycleScope.launch(CoroutineExceptionHandler { _, caught -> + MaterialAlertDialogBuilder(activity) + .setMessage(caught.message) + .setPositiveButton(android.R.string.ok, null) + .show() + }) { AppDatabase.instance.readingListPageDao().addPagesToList(AppDatabase.instance.readingListDao().getDefaultList(), pages, true) + } true } findPreference(R.string.preference_key_missing_description_test).onPreferenceClickListener = Preference.OnPreferenceClickListener { @@ -262,30 +269,44 @@ internal class DeveloperSettingsPreferenceLoader(fragment: PreferenceFragmentCom } private fun createTestReadingList(listName: String, numOfLists: Int, numOfArticles: Int) { - var index = 0 - AppDatabase.instance.readingListDao().getListsWithoutContents().asReversed().forEach { - if (it.title.contains(listName)) { - val trimmedListTitle = it.title.substring(listName.length).trim() - index = trimmedListTitle.toIntOrNull()?.coerceAtLeast(index) ?: index - return + fragment.lifecycleScope.launch(CoroutineExceptionHandler { _, caught -> + MaterialAlertDialogBuilder(activity) + .setMessage(caught.message) + .setPositiveButton(android.R.string.ok, null) + .show() + }) { + var index = 0 + AppDatabase.instance.readingListDao().getListsWithoutContents().asReversed().forEach { + if (it.title.contains(listName)) { + val trimmedListTitle = it.title.substring(listName.length).trim() + index = trimmedListTitle.toIntOrNull()?.coerceAtLeast(index) ?: index + return@forEach + } } - } - for (i in 0 until numOfLists) { - index += 1 - val list = AppDatabase.instance.readingListDao().createList("$listName $index", "") - val pages = (0 until numOfArticles).map { - ReadingListPage(PageTitle("${it + 1}", WikipediaApp.instance.wikiSite)) + for (i in 0 until numOfLists) { + index += 1 + val list = AppDatabase.instance.readingListDao().createList("$listName $index", "") + val pages = (0 until numOfArticles).map { + ReadingListPage(PageTitle("${it + 1}", WikipediaApp.instance.wikiSite)) + } + AppDatabase.instance.readingListPageDao().addPagesToList(list, pages, true) } - AppDatabase.instance.readingListPageDao().addPagesToList(list, pages, true) } } private fun deleteTestReadingList(listName: String, numOfLists: Int) { - var remainingNumOfLists = numOfLists - AppDatabase.instance.readingListDao().getAllLists().forEach { - if (it.title.contains(listName) && remainingNumOfLists > 0) { - AppDatabase.instance.readingListDao().deleteList(it) - remainingNumOfLists-- + fragment.lifecycleScope.launch(CoroutineExceptionHandler { _, caught -> + MaterialAlertDialogBuilder(activity) + .setMessage(caught.message) + .setPositiveButton(android.R.string.ok, null) + .show() + }) { + var remainingNumOfLists = numOfLists + AppDatabase.instance.readingListDao().getAllLists().forEach { + if (it.title.contains(listName) && remainingNumOfLists > 0) { + AppDatabase.instance.readingListDao().deleteList(it) + remainingNumOfLists-- + } } } } diff --git a/app/src/main/java/org/wikipedia/talk/TalkTopicHolder.kt b/app/src/main/java/org/wikipedia/talk/TalkTopicHolder.kt index 1ee50998454..9167d5bd9f3 100644 --- a/app/src/main/java/org/wikipedia/talk/TalkTopicHolder.kt +++ b/app/src/main/java/org/wikipedia/talk/TalkTopicHolder.kt @@ -17,10 +17,14 @@ import org.wikipedia.databinding.ItemTalkTopicBinding import org.wikipedia.dataclient.discussiontools.ThreadItem import org.wikipedia.page.Namespace import org.wikipedia.richtext.RichTextUtil -import org.wikipedia.util.* +import org.wikipedia.util.DateUtil +import org.wikipedia.util.FeedbackUtil +import org.wikipedia.util.ResourceUtil +import org.wikipedia.util.ShareUtil +import org.wikipedia.util.StringUtil import org.wikipedia.util.log.L import org.wikipedia.views.SwipeableItemTouchHelperCallback -import java.util.* +import java.util.Date class TalkTopicHolder internal constructor( private val binding: ItemTalkTopicBinding, @@ -37,7 +41,7 @@ class TalkTopicHolder internal constructor( val topicTitle = RichTextUtil.stripHtml(threadItem.html).trim().ifEmpty { context.getString(R.string.talk_no_subject) } StringUtil.setHighlightedAndBoldenedText(binding.topicTitleText, topicTitle, viewModel.currentSearchQuery) binding.topicTitleText.setTextColor(ResourceUtil.getThemedColor(context, if (threadItem.seen) android.R.attr.textColorTertiary else R.attr.primary_color)) - itemView.setOnClickListener(this) + itemView.setOnClickListener(this@TalkTopicHolder) // setting tag for swipe action text if (!threadItem.seen) { @@ -120,8 +124,9 @@ class TalkTopicHolder internal constructor( } private fun markAsSeen(force: Boolean = false) { - viewModel.markAsSeen(threadItem, force) - bindingAdapter?.notifyDataSetChanged() + viewModel.markAsSeen(threadItem, force) { + bindingAdapter?.notifyDataSetChanged() + } } private fun showOverflowMenu(anchorView: View) { diff --git a/app/src/main/java/org/wikipedia/talk/TalkTopicsViewModel.kt b/app/src/main/java/org/wikipedia/talk/TalkTopicsViewModel.kt index 201c728996a..2f5dc785d6a 100644 --- a/app/src/main/java/org/wikipedia/talk/TalkTopicsViewModel.kt +++ b/app/src/main/java/org/wikipedia/talk/TalkTopicsViewModel.kt @@ -39,6 +39,8 @@ class TalkTopicsViewModel(var pageTitle: PageTitle) : ViewModel() { } private val threadItems = mutableListOf() + private val seenThreadItemsSha = mutableSetOf() + var sortedThreadItems = listOf() var isWatched = false var hasWatchlistExpiry = false @@ -107,6 +109,10 @@ class TalkTopicsViewModel(var pageTitle: PageTitle) : ViewModel() { threadItems.addAll(discussionToolsInfoResponse.pageInfo?.threads ?: emptyList()) sortAndFilterThreadItems() + seenThreadItemsSha.clear() + val shaList = threadItems.mapNotNull { threadSha(it) } + seenThreadItemsSha.addAll(talkPageDao.getFor(shaList).map { it.sha }) + val watchStatus = if (WikipediaApp.instance.isOnline && AccountUtil.isLoggedIn) ServiceFactory.get(pageTitle.wikiSite) .getWatchedStatus(pageTitle.prefixedText).query?.firstPage()!! else MwQueryPage() isWatched = watchStatus.watched @@ -129,20 +135,25 @@ class TalkTopicsViewModel(var pageTitle: PageTitle) : ViewModel() { } } - fun markAsSeen(threadItem: ThreadItem?, force: Boolean = false) { + fun markAsSeen(threadItem: ThreadItem?, force: Boolean = false, action: (() -> Unit)) { threadSha(threadItem)?.let { viewModelScope.launch(actionHandler) { if (topicSeen(threadItem) && !force) { talkPageDao.deleteTalkPageSeen(it) + seenThreadItemsSha.remove(it) } else { talkPageDao.insertTalkPageSeen(TalkPageSeen(it)) + seenThreadItemsSha.add(it) } + action() } } } fun topicSeen(threadItem: ThreadItem?): Boolean { - return threadSha(threadItem)?.run { talkPageDao.getTalkPageSeen(this) != null } ?: false + return threadSha(threadItem)?.let { + seenThreadItemsSha.any { sha -> sha == it } + } ?: false } private fun threadSha(threadItem: ThreadItem?): String? { diff --git a/app/src/main/java/org/wikipedia/talk/db/TalkPageSeenDao.kt b/app/src/main/java/org/wikipedia/talk/db/TalkPageSeenDao.kt index b7fdabf77d5..b452560d8bb 100644 --- a/app/src/main/java/org/wikipedia/talk/db/TalkPageSeenDao.kt +++ b/app/src/main/java/org/wikipedia/talk/db/TalkPageSeenDao.kt @@ -4,7 +4,6 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query -import kotlinx.coroutines.flow.Flow @Dao interface TalkPageSeenDao { @@ -12,10 +11,13 @@ interface TalkPageSeenDao { suspend fun insertTalkPageSeen(talkPageSeen: TalkPageSeen) @Query("SELECT * FROM TalkPageSeen WHERE sha = :sha LIMIT 1") - fun getTalkPageSeen(sha: String): TalkPageSeen? + suspend fun getTalkPageSeen(sha: String): TalkPageSeen? @Query("SELECT * FROM TalkPageSeen") - fun getAll(): Flow> + suspend fun getAll(): List + + @Query("SELECT * FROM TalkPageSeen WHERE sha IN (:shaList)") + suspend fun getFor(shaList: List): List @Query("DELETE FROM TalkPageSeen WHERE sha = :sha") suspend fun deleteTalkPageSeen(sha: String)